MiniDexed icon indicating copy to clipboard operation
MiniDexed copied to clipboard

Glissando effect seems to affect all operators differently [solved by on-the-fly patching]

Open probonopd opened this issue 8 months ago • 10 comments

@skerjanc reported:

Glissando SYSEX catches the right function, but the glissando effect seems to affect all operators differently, thus it sounds detuned in steps. An issue of Dexed also?

Glissando on directly influences the detune parameter, i.e. they are no longer usable then

This is also true when you are not using Performance Sysex but the menu.

probonopd avatar Apr 25 '25 18:04 probonopd

I think we need a way of testing this on MiniDexed and on real hardware for comparison.

Would you be able to run the script below with MiniDexed and module 1 of your TX816 for comparison? Do you hear any differences between the two? (I know it is a bit much to ask, but if you hear differences, could you record them for comparison?)

#!/usr/bin/env python
# -*- coding: utf-8 -*- 

# Test MIDI portamento and glissando features of T816

import time
import sys
import signal
import rtmidi

def ask(question, play_fn=None):
    while True:
        reply = input(f"{question} (y/n/r): ").strip().lower()
        if reply == 'r':
            if play_fn:
                play_fn()
            continue
        if reply in ['y', 'n']:
            return reply == 'y'

def send_sysex(midiout, msg, label=""):
    # Force all SysEx messages to MIDI channel 0 (0x10)
    if len(msg) > 2 and msg[0] == 0xF0 and msg[1] == 0x43:
        msg = msg.copy()
        msg[2] = 0x10
    midiout.send_message(msg)
    print(f"Sent: {label}")

def send_parameter(midiout, parameter, value, label):
    """Helper function to send SysEx for a single parameter."""
    msg = [0xF0, 0x43, 0x10, 0x04, parameter, value, 0xF7]
    send_sysex(midiout, msg, label)

def note_on(midiout, note, velocity=100, channel=0):
    midiout.send_message([0x90 | 0, note, velocity])

def note_off(midiout, note, channel=0):
    midiout.send_message([0x80 | 0, note, 0])

def play_legato(midiout, notes, delay=0.5, channel=0):
    midiout.send_message([0x90 | 0, notes[0], 100])
    time.sleep(delay)
    midiout.send_message([0x90 | 0, notes[1], 100])
    time.sleep(delay)
    midiout.send_message([0x80 | 0, notes[0], 0])
    midiout.send_message([0x80 | 0, notes[1], 0])

def play_sequential(midiout, notes, delay=0.5, channel=0):
    note_on(midiout, notes[0], channel=0)
    time.sleep(delay)
    note_off(midiout, notes[0], channel=0)
    time.sleep(0.1)
    note_on(midiout, notes[1], channel=0)
    time.sleep(delay)
    note_off(midiout, notes[1], channel=0)

def play_chord(midiout, notes, velocity=100, duration=1.0, channel=0):
    for note in notes:
        note_on(midiout, note, velocity, 0)
    time.sleep(duration)
    for note in notes:
        note_off(midiout, note, 0)

def get_port():
    midiout = rtmidi.MidiOut()
    ports = midiout.get_ports()
    if not ports:
        print("No MIDI output ports found.")
        sys.exit(1)
    for i, port in enumerate(ports):
        print(f"{i}: {port}")
    index = int(input("Select MIDI output port: "))
    midiout.open_port(index)
    return midiout

def set_initial_values(midiout):
    # Set each parameter to its default value

    # Poly/Mono (0 = Poly)
    send_parameter(midiout, 2, 0, "Poly/Mono")

    # Pitch Bend Range
    send_parameter(midiout, 3, 0, "Pitch Bend Range")

    # Pitch Bend Step
    send_parameter(midiout, 4, 0, "Pitch Bend Step")

    # Portamento Time (0 = fast)
    send_parameter(midiout, 5, 0, "Portamento Time")

    # Portamento/Glissando (0 = Portamento)
    send_parameter(midiout, 6, 0, "Portamento/Glissando")

    # Portamento Mode (0 = Retain)
    send_parameter(midiout, 7, 0, "Portamento Mode")

    # Modulation Wheel Sensitivity
    send_parameter(midiout, 9, 0, "Modulation Wheel Sensitivity")

    # Foot Controller Sensitivity
    send_parameter(midiout, 11, 0, "Foot Controller Sensitivity")

    # After Touch Sensitivity
    send_parameter(midiout, 13, 0, "After Touch Sensitivity")

    # Breath Controller Sensitivity
    send_parameter(midiout, 15, 0, "Breath Controller Sensitivity")

    # Audio Output Level Attenuator
    send_parameter(midiout, 26, 0, "Audio Output Level Attenuator")

    # Master Tuning (64 = concert pitch)
    send_parameter(midiout, 64, 64, "Master Tuning")

def wait_for_key():
    input("Press Enter to continue...")

def test_sequence(midiout, is_mono):
    mode = "MONO" if is_mono else "POLY"
    results = []

    # Initialize values
    set_initial_values(midiout)
    time.sleep(1)

    # Switch to correct mode before starting tests
    if is_mono:
        send_sysex(midiout, [0xF0, 0x43, 0x10, 0x04, 0x02, 0x01, 0xF7], "Set MONO mode")
    else:
        send_sysex(midiout, [0xF0, 0x43, 0x10, 0x04, 0x02, 0x00, 0xF7], "Set POLY mode")
    time.sleep(0.2)
    print(f"\nYou are now in {mode} mode.")
    print("For each test, listen for how the pitch changes between two notes.")

    # --- Poly/Mono mode difference test ---
    def play_poly_mono_difference():
        print(f"Test 0: Poly/Mono Mode Difference ({mode})")
        print("A C major chord (C-E-G) will be played.")
        if is_mono:
            print("Listen: You should hear only one note at a time (not a chord).")
        else:
            print("Listen: You should hear all three notes together as a chord.")
        print("Press Enter to start the test.")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
    play_poly_mono_difference()
    if is_mono:
        result = ask("Did you hear only one note at a time (not a chord)?", play_poly_mono_difference)
    else:
        result = ask("Did you hear all three notes together as a chord?", play_poly_mono_difference)
    results.append(("0: Poly/Mono Mode Difference", result, []))

    # --- Test 1: Chord (basic poly/mono difference) ---
    def test_chord():
        print("Test 1: Chord (C-E-G)")
        print("Playing C major chord. Listen for chord vs single note.")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
    test_chord()
    results.append(("1: Chord (C-E-G)", True, []))

    # --- Test 2: Portamento Off/On ---
    def test_portamento():
        print("Test 2: Portamento Off/On (chord)")
        print("First, portamento OFF. Then, portamento ON.")
        send_parameter(midiout, 5, 0, "Portamento Time 0 (off)")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
        send_parameter(midiout, 5, 63, "Portamento Time 63 (on)")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
        send_parameter(midiout, 5, 0, "Portamento Time 0 (reset)")
    test_portamento()
    results.append(("2: Portamento Off/On (chord)", True, []))

    # --- Test 3: Detune Off/On ---
    def test_detune():
        print("Test 3: Detune Off/On (chord)")
        print("First, detune OFF. Then, detune ON.")
        send_parameter(midiout, 40, 0x20, "Detune 0 (off)")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
        send_parameter(midiout, 40, 0x40, "Detune 64 (on)")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
        send_parameter(midiout, 40, 0x20, "Detune 0 (reset)")
    test_detune()
    results.append(("3: Detune Off/On (chord)", True, []))

    # --- Test 4: Reverb Off/On ---
    def test_reverb():
        print("Test 4: Reverb Off/On (chord)")
        print("First, reverb OFF. Then, reverb ON.")
        send_parameter(midiout, 26, 0, "Reverb 0 (off)")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
        send_parameter(midiout, 26, 99, "Reverb 99 (on)")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=1.5)
        send_parameter(midiout, 26, 0, "Reverb 0 (reset)")
    test_reverb()
    results.append(("4: Reverb Off/On (chord)", True, []))

    # --- Test 5: Sustain Pedal Off/On ---
    def test_sustain():
        print("Test 5: Sustain Pedal Off/On (chord)")
        print("First, sustain pedal OFF. Then, sustain pedal ON.")
        print("Release keys quickly after playing chord.")
        # Sustain OFF
        midiout.send_message([0xB0, 64, 0])
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=0.5)
        # Sustain ON
        midiout.send_message([0xB0, 64, 127])
        wait_for_key()
        play_chord(midiout, [60, 64, 67], duration=0.5)
        midiout.send_message([0xB0, 64, 0])
    test_sustain()
    results.append(("5: Sustain Pedal Off/On (chord)", True, []))

    # --- Test 6: Velocity Sensitivity Off/On ---
    def test_velocity():
        print("Test 6: Velocity Sensitivity Off/On (chord)")
        print("First, velocity sensitivity OFF. Then, velocity sensitivity ON.")
        send_parameter(midiout, 8, 0, "Velocity Sensitivity 0 (off)")
        print("Play softly, then loudly.")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], velocity=40, duration=1.0)
        play_chord(midiout, [60, 64, 67], velocity=120, duration=1.0)
        send_parameter(midiout, 8, 99, "Velocity Sensitivity 99 (on)")
        print("Play softly, then loudly.")
        wait_for_key()
        play_chord(midiout, [60, 64, 67], velocity=40, duration=1.0)
        play_chord(midiout, [60, 64, 67], velocity=120, duration=1.0)
        send_parameter(midiout, 8, 0, "Velocity Sensitivity 0 (reset)")
    test_velocity()
    results.append(("6: Velocity Sensitivity Off/On (chord)", True, []))

    # Print summary
    print("\nTest Summary:")
    for name, passed, sysex in results:
        if passed:
            print(f"PASS: {name}")
        else:
            print(f"FAIL: {name}")
            if isinstance(sysex[0], list):
                for msg in sysex:
                    hexstr = ' '.join(f'{b:02X}' for b in msg)
                    print(f"  To reproduce: {hexstr}")
            else:
                hexstr = ' '.join(f'{b:02X}' for b in sysex)
                print(f"  To reproduce: {hexstr}")

if __name__ == "__main__":
    signal.signal(signal.SIGINT, lambda s, f: sys.exit(0))
    midiout = get_port()
    try:
        print("\n--- POLY MODE TESTS ---")
        test_sequence(midiout, is_mono=False)

        print("\n--- MONO MODE TESTS ---")
        test_sequence(midiout, is_mono=True)
    finally:
        del midiout

probonopd avatar Apr 25 '25 19:04 probonopd

Sorry, I am not the python expert. I am getting errors like: File "D:\temp\TestGlissando.py", line 66, in get_port midiout = rtmidi.MidiOut() AttributeError: module 'rtmidi' has no attribute 'MidiOut'. Did you mean: 'RtMidiOut'?

Skerjanc avatar Apr 27 '25 21:04 Skerjanc

Oops, I think I had only tested the Python script on Linux. So let's not use it for now.

Can you come up with a sequence of MIDI sysex that you can share and could run/record on both MiniDexed and TX816 so that we hear the difference? I think that would be a tremendous help. Thanks!

probonopd avatar Apr 28 '25 17:04 probonopd

You can simply hear it on MiniDexed following these steps:

  1. TG1>Portamento>Time = 70 : you will hear a long portamento between two notes.
  2. TG1>Portamento>Glissando On: you will hear the portamento quantized in semitones as expected, but totally detuned.

In this state the detune parameters of the operators are no longer properly working, causing crazy settings.

Skerjanc avatar Apr 29 '25 16:04 Skerjanc

Can reproduce it now, thanks @Skerjanc. Minimal viable reproduction:

No SysEx involved. Al done using the menus.

  • Performance -> Bank -> Laboratory
  • Performance -> Load -> 128:003 pure DX7
  • TG1 -> Portamento -> Time -> 1
  • Play only C note (no other notes)
  • TG1 -> Portamento -> Glissando -> On
  • Play only C note (no other notes) again. __The note is now deeper (detuned)! This is the bug
  • TG1 -> Portamento -> Time -> 0
  • Play only C note (no other notes) again. __The note is now higher again (as it was originally). Changing other values for Time other than 0 detunes the note, but the amount of time does not change the amount of detuning.

probonopd avatar Apr 29 '25 17:04 probonopd

I believe this bug to be in Synth_Dexed/src/dx7note.cpp.

Proposed patch:

https://github.com/probonopd/MiniDexed/blob/c2bedd11038be59a74ee649376bf57b1d44f7b6a/src/patches/dx7note.patch

If the theory is correct, then also other projects using the Synth_Dexed engine would be affected by this, and the fix would need to be made there.

probonopd avatar Apr 29 '25 18:04 probonopd

Thanks @Skerjanc for having confirmed that this patch works. I am not sure that it is the best possible patch, but at least it fixes the issue.

For now, we are patching the Synth_Dexed engine on the fly, but a proper solution would be to get this fixed there. So maybe @dcoredump or @soyersoyer (who has sent successful Pull Requests to Synth_Dexed in the past) want to have a look at this? That'd be tremendous. Thanks!

(Leaving this open until a fix is in Synth_Dexed and we can remove our on-the-fly patching.)

probonopd avatar Apr 29 '25 20:04 probonopd

Thanks for this patch. I will try to adapt it asap to Synth_Dexed.

dcoredump avatar May 01 '25 07:05 dcoredump

I don't have any hardware to test at the moment and hope I haven't done anything wrong. Can you please test 2ad9e43095?

dcoredump avatar May 01 '25 07:05 dcoredump

Tested, works. Thanks 👍

probonopd avatar May 01 '25 08:05 probonopd

This is solved, right?

soyersoyer avatar Jul 25 '25 11:07 soyersoyer

Yes. Thank you very much.

probonopd avatar Jul 25 '25 16:07 probonopd