Roland JX-08 Midi-Editor – with Randomizer

andreasPython Code1 month ago378 Views

What this is

A desktop MIDI controller + step sequencer for the Roland JX-08, written in Python.
It sends MIDI CC/Program Change to the synth, lets you tweak every important parameter, play an on-screen/typing keyboard, run a 16/32/48/64-step sequencer, save/load presets (JSON), and even randomize musically sensible patches.

Built with:

  • tkinter – tabs + sliders + buttons GUI
  • pygame.midi – MIDI output (CC, PC, Note On/Off)
  • json – preset save/load

Install (Windows/macOS/Linux)

# (Optional but recommended) create a venv
python -m venv .venv && . .venv/Scripts/activate   # Windows
# or: source .venv/bin/activate                     # macOS/Linux

# Required
pip install pygame
tkinter is used for the UI and ships with the standard Python installer on Windows/macOS.
If you need a virtual MIDI cable (to route from your DAW), install a system tool like loopMIDI (Windows) or enable IAC Driver (macOS).
  1. Run it
    Save the script as jx08_full.py, then: python jx08_full.py
  2. Connect
    In the Conn tab pick your MIDI Output (your hardware interface or a virtual port).
    Choose Active Part (A/B) and MIDI channels (CH.A / CH.B / System CH).
  3. Control the synth
    • Tabs for Perf, LFO, DCO/Mixer, Filter/ENV expose all key JX-08 CCs (cutoff, reso, waves, ranges, envs, portamento, etc.).
    • Move a control → the app sends the corresponding MIDI CC to the currently active part.
  4. Program changes & presets (Program tab)
    • Part PC: Bank MSB (0–1) + Program (0–127) → send to the active part.
    • System PC: select patterns on the System channel.
    • Presets: Save writes all CCs + sequencer settings to a JSON file; Load reads it back and applies values; Apply to JX-08 retransmits all CCs currently shown in the UI.
  5. Sequencer & keyboard (Sequencer tab)
    • Length: 16/32/48/64 steps.
    • Per-step notes: click a step’s note label (top), then play the mini piano or your computer keyboard to assign the note.
    • Typing keyboard: layout auto-detect (DE/EN) with manual override.
      • DE whites: Y X C V B N M • blacks: S D G H J
      • EN whites: Z X C V B N M • blacks: S D G H J
      • , / . to octave down/up.
    • Transport: Play/Stop, BPM, Gate, Velocity, Swing, per-step On and Accent.
  6. Random Sound
    • The Random Sound button sets musically weighted random values for waves, ranges, LFO, envelopes, filter, FX, etc., updates the UI, and immediately sends the CCs to the JX-08.

Why pygame (and not python-rtmidi)?

  • The project uses pygame.midi which is cross-platform and simple to install via pip.
  • It’s enough for sending CC/PC/Note messages—exactly what you need for a hardware controller + sequencer.
  • No native compiler toolchain required.

Tips & troubleshooting

  • No ports listed? Open your MIDI interface/virtual port first, then start the app.
  • DAW routing:
    • Windows: create a port in loopMIDI, select it in the app, then route from your DAW to that port.
    • macOS: enable IAC Driver in Audio MIDI Setup and select it.
  • Latency: keep the app and DAW on the same machine; avoid Bluetooth MIDI for tight sequencing.
That’s it—one pip install pygame, run the script, pick your MIDI port, and you’ve got a hands-on JX-08 controller + sequencer with presets and a “happy accidents” randomizer.

Download EXE for Windows (11)

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pygame.midi as pm
import sys, locale, json, os, random
# Andreas Knaup 2025/09/19 trymypy.com
# -------------------- MIDI helper --------------------
class JX08:
    def __init__(self):
        pm.init()
        self.out = None
        self.ch_a = 0     # Part A  (0-15 => MIDI CH 1-16)
        self.ch_b = 1     # Part B
        self.sys_ch = 0   # System channel (for system PC / pattern)
        self.active = 'A' # 'A' or 'B'
    def __del__(self):
        try:
            if self.out: self.out.close()
        finally:
            pm.quit()
    def list_outputs(self):
        outs=[]
        for i in range(pm.get_count()):
            interf, name, inp, outp, opened = pm.get_device_info(i)
            if outp: outs.append((i, name.decode(errors="ignore")))
        return outs
    def open(self, device_id):
        if self.out: self.out.close()
        self.out = pm.Output(device_id)
    def ch(self):
        return self.ch_a if self.active=='A' else self.ch_b
    def cc(self, cc, val, ch=None):
        if not self.out: return
        c = self.ch() if ch is None else ch
        self.out.write_short(0xB0 | (c & 0x0F), cc & 0x7F, int(val) & 0x7F)
    def pc(self, msb, lsb, pc, ch=None):
        if not self.out: return
        c = self.ch() if ch is None else ch
        self.out.write_short(0xB0 | (c & 0x0F), 0,   msb & 0x7F)   # CC0
        self.out.write_short(0xB0 | (c & 0x0F), 32,  lsb & 0x7F)   # CC32
        self.out.write_short(0xC0 | (c & 0x0F), pc & 0x7F, 0)      # PC
    def system_pc(self, pc):
        if not self.out: return
        self.out.write_short(0xC0 | (self.sys_ch & 0x0F), pc & 0x7F, 0)
    def note_on(self, note=60, vel=100):
        if not self.out: return
        self.out.write_short(0x90 | (self.ch() & 0x0F), note & 0x7F, vel & 0x7F)
    def note_off(self, note=60):
        if not self.out: return
        self.out.write_short(0x80 | (self.ch() & 0x0F), note & 0x7F, 0)

jx = JX08()

# -------------------- Root + Notebook --------------------
root = tk.Tk()
root.title("Roland JX-08 – Controller + Sequencer (DE/EN auto) + Presets + Random")
root.minsize(840, 580)

nb = ttk.Notebook(root)
nb.pack(fill="both", expand=True)

# Tabs
tab_conn = ttk.Frame(nb)      # Connections
tab_perf = ttk.Frame(nb)      # Performance & Levels
tab_lfo  = ttk.Frame(nb)      # LFO
tab_dco  = ttk.Frame(nb)      # DCO & Mixer
tab_flt  = ttk.Frame(nb)      # Filter / VCA / Envelopes
tab_prog = ttk.Frame(nb)      # Program / Presets / Random
tab_seq  = ttk.Frame(nb)      # Sequencer (inkl. Keyboard)

for t, name in [(tab_conn,"Conn"),(tab_perf,"Perf"),(tab_lfo,"LFO"),
                (tab_dco,"DCO/Mixer"),(tab_flt,"Filter/ENV"),
                (tab_prog,"Program"),(tab_seq,"Sequencer")]:
    nb.add(t, text=name)

# -------------------- helpers & CC registry --------------------
# Registry, damit wir Save/Load/Apply/Random über alle CCs fahren können
CC_REGISTRY = []  # list of {"cc":int, "get":callable->int, "set":callable(v:int), "name":str}

def reg_control(cc, getter, setter, name):
    CC_REGISTRY.append({"cc":cc, "get":getter, "set":setter, "name":name})

def slider(frame, row, col, label, cc, init=64, a=0, b=127, span=1):
    ttk.Label(frame, text=label).grid(row=row, column=col, sticky="w", padx=8, pady=4)
    var = tk.DoubleVar(value=init)
    def on_change(v):
        jx.cc(cc, float(v))
    s = ttk.Scale(frame, from_=a, to=b, orient="horizontal", variable=var, command=on_change)
    s.grid(row=row, column=col+1, columnspan=span, sticky="ew", padx=(0,8), pady=4)
    reg_control(cc, lambda v=var: int(v.get()),
                   lambda x, v=var: v.set(int(x)), label)
    return s

def toggle(frame, row, col, label, cc):
    var = tk.BooleanVar(value=False)
    def on_toggle():
        jx.cc(cc, 127 if var.get() else 0)
    ttk.Checkbutton(frame, text=label, variable=var, command=on_toggle
                    ).grid(row=row, column=col, sticky="w", padx=8, pady=4)
    reg_control(cc, lambda v=var: 127 if v.get() else 0,
                   lambda x, v=var: v.set(bool(int(x) >= 64)), label)
    return var

def dropdown(frame, row, col, label, cc, items):
    ttk.Label(frame, text=label).grid(row=row, column=col, sticky="w", padx=8, pady=4)
    box = ttk.Combobox(frame, values=items, state="readonly", width=20)
    box.grid(row=row, column=col+1, sticky="w", padx=(0,8), pady=4)
    def sel(*_): jx.cc(cc, box.current())
    box.bind("<<ComboboxSelected>>", sel)
    box.current(0)
    # setter nutzt Combobox.current(index)
    reg_control(cc, lambda b=box: b.current(),
                   lambda x, b=box: b.current(int(x)), label)
    return box

for t in (tab_conn, tab_perf, tab_lfo, tab_dco, tab_flt, tab_prog, tab_seq):
    for i in range(8):
        t.columnconfigure(i, weight=1)

# -------------------- Conn tab --------------------
ttk.Label(tab_conn, text="MIDI Output:").grid(row=0, column=0, sticky="w", padx=8, pady=8)
outs = jx.list_outputs()
out_var = tk.StringVar()
out_map = {name: dev_id for (dev_id, name) in outs}
cmb_out = ttk.Combobox(tab_conn, textvariable=out_var, values=[name for (_,name) in outs],
                       width=48, state="readonly")
cmb_out.grid(row=0, column=1, columnspan=4, sticky="ew", padx=8, pady=8)
def open_port(*_):
    name = out_var.get()
    if name: jx.open(out_map[name])
cmb_out.bind("<<ComboboxSelected>>", open_port)
if outs: cmb_out.current(0); open_port()

ttk.Label(tab_conn, text="Active Part").grid(row=1, column=0, sticky="w", padx=8)
part_var = tk.StringVar(value='A')
ttk.Radiobutton(tab_conn, text="A", variable=part_var, value='A',
                command=lambda: setattr(jx, "active", "A")).grid(row=1, column=1, sticky="w")
ttk.Radiobutton(tab_conn, text="B", variable=part_var, value='B',
                command=lambda: setattr(jx, "active", "B")).grid(row=1, column=2, sticky="w")

ttk.Label(tab_conn, text="CH.A").grid(row=2, column=0, sticky="e", padx=8)
ch_a_var = tk.StringVar(value="1")
cmb_a = ttk.Combobox(tab_conn, textvariable=ch_a_var, values=[str(i) for i in range(1,17)],
                     width=5, state="readonly")
cmb_a.grid(row=2, column=1, sticky="w", padx=(0,8))
cmb_a.bind("<<ComboboxSelected>>", lambda *_: setattr(jx, "ch_a", int(ch_a_var.get())-1))

ttk.Label(tab_conn, text="CH.B").grid(row=2, column=2, sticky="e", padx=8)
ch_b_var = tk.StringVar(value="2")
cmb_b = ttk.Combobox(tab_conn, textvariable=ch_b_var, values=[str(i) for i in range(1,17)],
                     width=5, state="readonly")
cmb_b.grid(row=2, column=3, sticky="w", padx=(0,8))
cmb_b.bind("<<ComboboxSelected>>", lambda *_: setattr(jx, "ch_b", int(ch_b_var.get())-1))

ttk.Label(tab_conn, text="System CH").grid(row=2, column=4, sticky="e", padx=8)
sys_var = tk.StringVar(value="1")
cmb_sys = ttk.Combobox(tab_conn, textvariable=sys_var, values=[str(i) for i in range(1,17)],
                       width=5, state="readonly")
cmb_sys.grid(row=2, column=5, sticky="w", padx=(0,8))
cmb_sys.bind("<<ComboboxSelected>>", lambda *_: setattr(jx, "sys_ch", int(sys_var.get())-1))

# -------------------- Perf tab --------------------
slider(tab_perf, 0,0, "Mod Wheel (CC1)",              1)
slider(tab_perf, 0,2, "Expression (CC11)",           11)
slider(tab_perf, 0,4, "Part Level (CC7)",             7)
slider(tab_perf, 1,0, "AMP Level (CC110)",          110)
slider(tab_perf, 1,2, "Portamento Time (CC117)",    117)
toggle(tab_perf, 1,4, "Portamento SW (CC118)",      118)
toggle(tab_perf, 2,0, "Hold Pedal (CC64)",           64)
dropdown(tab_perf,2,2,"Voice Mode (CC119)",         119, ["Poly (0)","Solo (1)","Unison (2)"])

# -------------------- LFO tab --------------------
slider(tab_lfo, 0,0, "LFO Rate (CC29)",             29)
slider(tab_lfo, 0,2, "LFO Delay (CC27)",            27)
dropdown(tab_lfo,0,4,"LFO Waveform (CC35)",        35, ["Sine (0)","Square (1)","Random (2)"])
slider(tab_lfo, 1,0, "VCF LFO Depth (CC28)",        28)
slider(tab_lfo, 1,2, "DCO-1 LFO Depth (CC26)",      26)
slider(tab_lfo, 1,4, "DCO-2 LFO Depth (CC25)",      25)

# -------------------- DCO/Mixer tab --------------------
dropdown(tab_dco,0,0,"DCO-1 Wave (CC46)",          46, ["Saw","Pulse","Square","Noise"])
dropdown(tab_dco,0,2,"DCO-2 Wave (CC61)",          61, ["Saw","Pulse","Square","Noise"])
dropdown(tab_dco,0,4,"DCO-1 Range (CC47)",         47, ["16'","8'","4'","2'"])
dropdown(tab_dco,0,6,"DCO-2 Range (CC62)",         62, ["16'","8'","4'","2'"])
slider(tab_dco, 1,0, "DCO-1 Level (CC16)",         16)
slider(tab_dco, 1,2, "DCO-2 Level (CC17)",         17)
slider(tab_dco, 1,4, "DCO-2 ENV Amount (CC63)",    63)
dropdown(tab_dco,1,6,"Mixer ENV Mode (CC19)",      19, ["ENV1 (0)","ENV2 (1)"])
slider(tab_dco, 2,0, "Mixer ENV Amount (CC18)",    18)
dropdown(tab_dco,2,2,"DCO ENV Mode (CC60)",        60, ["1 Normal (0)","1 Inverse (1)","2 Normal (2)","2 Inverse (3)"])
slider(tab_dco, 2,4, "DCO Cross Mod (CC59)",       59)
slider(tab_dco, 2,6, "DCO-2 Fine Tune (CC56)",     56)
slider(tab_dco, 3,0, "DCO-2 Coarse 1Oct (CC87)",   87)

# -------------------- Filter/ENV tab --------------------
slider(tab_flt, 0,0, "VCF Cutoff (CC3)",            3)
slider(tab_flt, 0,2, "VCF Resonance (CC9)",         9)
slider(tab_flt, 0,4, "FILTER HPF (CC79)",           79)
slider(tab_flt, 0,6, "Reverb Send (CC91)",          91)
slider(tab_flt, 1,0, "VCF ENV Amount (CC81)",       81)
slider(tab_flt, 1,2, "VCF Key Follow (CC82)",       82)
dropdown(tab_flt,1,4,"VCF ENV Mode (CC84)",        84, ["1 Normal (0)","1 Inverse (1)","2 Normal (2)","2 Inverse (3)"])
dropdown(tab_flt,1,6,"AMP ENV Mode (CC109)",      109, ["ENV2 (0)","GATE (1)"])
# ENV1
slider(tab_flt, 2,0, "ENV1 Attack (CC83)",         83)
slider(tab_flt, 2,2, "ENV1 Decay (CC80)",          80)
slider(tab_flt, 2,4, "ENV1 Sustain (CC85)",        85)
slider(tab_flt, 2,6, "ENV1 Release (CC86)",        86)
slider(tab_flt, 3,0, "ENV1 Key Follow (CC104)",   104)
# ENV2
slider(tab_flt, 3,2, "ENV2 Attack (CC89)",         89)
slider(tab_flt, 3,4, "ENV2 Decay (CC90)",          90)
slider(tab_flt, 3,6, "ENV2 Sustain (CC102)",      102)
slider(tab_flt, 4,0, "ENV2 Release (CC103)",      103)
slider(tab_flt, 4,2, "ENV2 Key Follow (CC105)",   105)
# Optional
slider(tab_flt, 4,4, "Bend Pitch (CC41)",          41)

# -------------------- Program tab: Program Change + Presets + Random --------------------
prog_box = ttk.LabelFrame(tab_prog, text="Program Change (Part & System)")
prog_box.grid(row=0, column=0, columnspan=8, sticky="ew", padx=8, pady=(8,4))
for i in range(8): prog_box.columnconfigure(i, weight=1)

ttk.Label(prog_box, text="Part Bank MSB (0–1)").grid(row=0, column=0, sticky="w", padx=8, pady=4)
msb_var = tk.IntVar(value=0)
ttk.Spinbox(prog_box, from_=0, to=1, textvariable=msb_var, width=6).grid(row=0, column=1, sticky="w")

ttk.Label(prog_box, text="Part Program (0–127)").grid(row=0, column=2, sticky="w", padx=8, pady=4)
pc_var = tk.IntVar(value=0)
ttk.Spinbox(prog_box, from_=0, to=127, textvariable=pc_var, width=6).grid(row=0, column=3, sticky="w")

ttk.Button(prog_box, text="Send Part PC",
           command=lambda: jx.pc(msb_var.get(), 0, pc_var.get())).grid(row=0, column=4, sticky="w", padx=8)

ttk.Label(prog_box, text="System Pattern PC (0–127)").grid(row=1, column=0, sticky="w", padx=8, pady=4)
sys_pc_var = tk.IntVar(value=0)
ttk.Spinbox(prog_box, from_=0, to=127, textvariable=sys_pc_var, width=6).grid(row=1, column=1, sticky="w")
ttk.Button(prog_box, text="Send System PC",
           command=lambda: jx.system_pc(sys_pc_var.get())).grid(row=1, column=2, sticky="w", padx=8)

io_box = ttk.LabelFrame(tab_prog, text="Presets (Save / Load / Apply / Random)")
io_box.grid(row=1, column=0, columnspan=8, sticky="ew", padx=8, pady=(4,8))

def collect_state():
    cc_state = {str(entry["cc"]): int(entry["get"]()) for entry in CC_REGISTRY}
    prog = {"msb": int(msb_var.get()), "pc": int(pc_var.get()), "system_pc": int(sys_pc_var.get())}
    length = seq_len_var.get()
    seq = {
        "length": length,
        "steps_on":  [bool(v.get()) for v in steps_on[:length]],
        "steps_acc": [bool(v.get()) for v in steps_acc[:length]],
        "steps_note": [ (int(n) if n is not None else None) for n in steps_note[:length] ],
        "bpm": int(bpm_var.get()),
        "gate": int(gate_var.get()),
        "vel": int(vel_seq.get()),
        "swing": float(swing.get()),
        "oct": int(seq_kbd_oct.get())
    }
    return {"cc": cc_state, "program": prog, "sequence": seq}

def apply_state(st):
    if "cc" in st:
        for k, v in st["cc"].items():
            for entry in CC_REGISTRY:
                if entry["cc"] == int(k):
                    entry["set"](int(v))
                    jx.cc(entry["cc"], int(v))
                    break
    if "program" in st:
        prog = st["program"]
        msb_var.set(int(prog.get("msb", 0)))
        pc_var.set(int(prog.get("pc", 0)))
        sys_pc_var.set(int(prog.get("system_pc", 0)))
    if "sequence" in st:
        seq = st["sequence"]
        seq_len_var.set(int(seq.get("length", 16)))
        rebuild_grid()
        length = seq_len_var.get()
        so = seq.get("steps_on",  [])
        sa = seq.get("steps_acc", [])
        sn = seq.get("steps_note",[])
        for i in range(min(length, len(so))):
            steps_on[i].set(bool(so[i]))
        for i in range(min(length, len(sa))):
            steps_acc[i].set(bool(sa[i]))
        for i in range(min(length, len(sn))):
            steps_note[i] = (int(sn[i]) if sn[i] is not None else None)
            step_widgets[i][0].configure(text=("—" if steps_note[i] is None else midi_to_name(steps_note[i])))
        bpm_var.set(int(seq.get("bpm", 120)))
        gate_var.set(int(seq.get("gate", 110)))
        vel_seq.set(int(seq.get("vel", 100)))
        swing.set(float(seq.get("swing", 0.0)))
        seq_kbd_oct.set(int(seq.get("oct", 4)))
        draw_keys()

def save_preset():
    data = collect_state()
    fname = filedialog.asksaveasfilename(
        title="Preset speichern",
        defaultextension=".json",
        filetypes=[("JX-08 Preset", "*.json")],
        initialfile="jx08_preset.json"
    )
    if not fname: return
    with open(fname, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
    messagebox.showinfo("Preset", f"Gespeichert:\n{os.path.basename(fname)}")

def load_preset():
    fname = filedialog.askopenfilename(
        title="Preset laden",
        defaultextension=".json",
        filetypes=[("JX-08 Preset", "*.json")]
    )
    if not fname: return
    with open(fname, "r", encoding="utf-8") as f:
        st = json.load(f)
    apply_state(st)
    messagebox.showinfo("Preset", f"Geladen & angewendet (CCs):\n{os.path.basename(fname)}")

def apply_to_jx():
    for entry in CC_REGISTRY:
        jx.cc(entry["cc"], int(entry["get"]()))
    messagebox.showinfo("Apply", "Alle aktuellen CC-Werte an den aktiven Part gesendet.")

# ---- Randomizer helpers & function ----
def set_cc_value(cc, value):
    """UI + JX-08 gemeinsam setzen/senden."""
    for entry in CC_REGISTRY:
        if entry["cc"] == cc:
            entry["set"](int(value))
            jx.cc(cc, int(value))
            return True
    return False

def pick_weighted(items):
    """items: list of (value, weight) -> returns value"""
    total = sum(w for _, w in items)
    r = random.uniform(0, total)
    acc = 0.0
    for val, w in items:
        acc += w
        if r <= acc:
            return val
    return items[-1][0]

def randomize_sound():
    # --- Performance / global ---
    set_cc_value(7,   random.randint(100,127))   # Part Level
    set_cc_value(110, random.randint(100,127))   # AMP Level
    # Voice Mode (0=Poly,1=Solo,2=Unison) – Poly bevorzugen
    set_cc_value(119, pick_weighted([(0,0.7),(1,0.15),(2,0.15)]))

    # Portamento: selten an, moderate Zeit
    porta_on = random.random() < 0.2
    set_cc_value(118, 127 if porta_on else 0)
    set_cc_value(117, random.randint(10,60) if porta_on else 0)
    set_cc_value(64,  0)  # Sustain-Pedal aus

    # --- LFO ---
    set_cc_value(35,  pick_weighted([(0,0.55),(1,0.3),(2,0.15)]))  # Wave: Sine bevorzugt
    set_cc_value(29,  random.randint(20,100))  # Rate
    set_cc_value(27,  random.randint(0,60))    # Delay
    set_cc_value(28,  random.randint(0,90))    # LFO->VCF
    set_cc_value(26,  random.randint(0,90))    # LFO->DCO1
    set_cc_value(25,  random.randint(0,90))    # LFO->DCO2

    # --- DCO / Mixer ---
    set_cc_value(46,  random.randint(0,3))     # DCO1 Wave
    set_cc_value(61,  random.randint(0,3))     # DCO2 Wave
    set_cc_value(47,  pick_weighted([(0,0.5),(1,0.35),(2,0.1),(3,0.05)])) # DCO1 Range
    set_cc_value(62,  pick_weighted([(0,0.5),(1,0.35),(2,0.1),(3,0.05)])) # DCO2 Range
    set_cc_value(16,  random.randint(70,127))  # DCO1 Level
    set_cc_value(17,  random.randint(60,127))  # DCO2 Level
    set_cc_value(56,  max(0, min(127, int(random.gauss(64, 10)))))  # DCO2 Fine
    set_cc_value(87,  pick_weighted([(0,0.7),(64,0.2),(127,0.1)]))  # DCO2 Coarse
    set_cc_value(59,  pick_weighted([(0,0.5),(20,0.2),(40,0.15),(70,0.1),(100,0.05)])) # CrossMod
    set_cc_value(60,  random.randint(0,3))     # DCO ENV Mode
    set_cc_value(19,  random.randint(0,1))     # Mixer ENV Mode
    set_cc_value(63,  random.randint(0,100))   # DCO2 ENV Amt
    set_cc_value(18,  random.randint(0,100))   # Mixer ENV Amt

    # --- VCF / FX ---
    cutoff = int(max(8, min(120, random.choice([
        random.randint(35,95),
        random.randint(20,60),
        random.randint(70,115)
    ]))))
    set_cc_value(3, cutoff)
    set_cc_value(9,  pick_weighted([(0,0.2),(20,0.25),(40,0.25),(70,0.2),(100,0.1)])) # Res
    set_cc_value(79, pick_weighted([(0,0.5),(20,0.3),(40,0.15),(70,0.05)])) # HPF
    set_cc_value(91, pick_weighted([(0,0.2),(15,0.3),(30,0.3),(60,0.15),(90,0.05)])) # Reverb
    set_cc_value(81, random.randint(0,110))   # VCF ENV Amt
    set_cc_value(82, pick_weighted([(0,0.2),(32,0.25),(64,0.35),(96,0.2)]))
    set_cc_value(84, random.randint(0,3))     # VCF ENV Mode

    # ENV1
    set_cc_value(83, random.randint(0,60))     # A
    set_cc_value(80, random.randint(20,90))    # D
    set_cc_value(85, random.randint(40,100))   # S
    set_cc_value(86, random.randint(20,90))    # R
    set_cc_value(104, pick_weighted([(0,0.2),(32,0.25),(64,0.35),(96,0.2)]))
    # ENV2
    set_cc_value(109, random.randint(0,1))     # Amp Mode
    set_cc_value(89,  random.randint(0,70))    # A
    set_cc_value(90,  random.randint(20,90))   # D
    set_cc_value(102, random.randint(50,110))  # S
    set_cc_value(103, random.randint(20,90))   # R
    set_cc_value(105, pick_weighted([(0,0.2),(32,0.25),(64,0.35),(96,0.2)]))
    # Optional Pitch Bend Sens
    set_cc_value(41,  random.randint(0,127))

    # kleiner Ping
    try:
        jx.note_on(60, 100); root.after(120, lambda: jx.note_off(60))
    except Exception:
        pass

ttk.Button(io_box, text="Save Preset", command=save_preset).grid(row=0, column=0, sticky="w", padx=8, pady=8)
ttk.Button(io_box, text="Load Preset", command=load_preset).grid(row=0, column=1, sticky="w", padx=8, pady=8)
ttk.Button(io_box, text="Apply to JX-08 (CCs)", command=apply_to_jx).grid(row=0, column=2, sticky="w", padx=8, pady=8)
ttk.Button(io_box, text="Random Sound", command=randomize_sound).grid(row=0, column=3, sticky="w", padx=8, pady=8)

# -------------------- Sequencer (inkl. Keyboard + Layout) --------------------
seq_frame = ttk.Frame(tab_seq); seq_frame.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
tab_seq.rowconfigure(0, weight=1); tab_seq.columnconfigure(0, weight=1)

top = ttk.Frame(seq_frame); top.grid(row=0, column=0, sticky="ew")
top.columnconfigure(12, weight=1)

# detect keyboard layout (Auto / DE / EN)
def detect_layout():
    try:
        if sys.platform.startswith("win"):
            import ctypes
            hkl = ctypes.windll.user32.GetKeyboardLayout(0)
            langid = hkl & 0xffff
            if langid == 0x0407:   # German
                return "DE"
    except Exception:
        pass
    try:
        loc = (locale.getdefaultlocale() or ("",""))[0] or ""
        if loc.lower().startswith("de"):
            return "DE"
    except Exception:
        pass
    return "EN"

layout_mode = tk.StringVar(value="Auto")
resolved_layout = tk.StringVar(value=detect_layout())
def resolve_layout(): return (resolved_layout.get() if layout_mode.get()=="Auto" else layout_mode.get())

ttk.Label(top, text="Keyboard Layout").grid(row=0, column=0, sticky="e")
cmb_layout = ttk.Combobox(top, values=["Auto","DE","EN"], textvariable=layout_mode, width=6, state="readonly")
cmb_layout.grid(row=0, column=1, sticky="w", padx=(4,10))
lbl_layout = ttk.Label(top, text=f" {resolve_layout()}"); lbl_layout.grid(row=0, column=2, sticky="w")
def on_layout_change(*_): lbl_layout.config(text=f"→ {resolve_layout()}")
cmb_layout.bind("<<ComboboxSelected>>", on_layout_change)

# Transport & Settings
bpm_var   = tk.IntVar(value=120)
gate_var  = tk.IntVar(value=110)   # ms
vel_seq   = tk.IntVar(value=100)
swing     = tk.DoubleVar(value=0.0)
ttk.Label(top, text="BPM").grid(row=0, column=3, sticky="e")
ttk.Spinbox(top, from_=30,to=300, width=5, textvariable=bpm_var).grid(row=0, column=4, sticky="w")
ttk.Label(top, text="Gate (ms)").grid(row=0, column=5, sticky="e")
ttk.Spinbox(top, from_=20,to=1000, width=6, textvariable=gate_var).grid(row=0, column=6, sticky="w")
ttk.Label(top, text="Velocity").grid(row=0, column=7, sticky="e")
ttk.Spinbox(top, from_=1,to=127, width=5, textvariable=vel_seq).grid(row=0, column=8, sticky="w")
ttk.Label(top, text="Swing").grid(row=0, column=9, sticky="e")
ttk.Scale(top, from_=0.0, to=0.6, orient="horizontal", variable=swing, length=120).grid(row=0, column=10, sticky="w")

# Sequence length
seq_len_var = tk.IntVar(value=16)
ttk.Label(top, text="Steps").grid(row=0, column=11, sticky="e")
ttk.Combobox(top, values=[16,32,48,64], textvariable=seq_len_var, width=4, state="readonly").grid(row=0, column=12, sticky="w", padx=(4,0))

# Note names
note_names = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]

# Step data
MAX_STEPS = 64
steps_on   = [tk.BooleanVar(value=(i%4==0)) for i in range(MAX_STEPS)]
steps_acc  = [tk.BooleanVar(value=False)     for _ in range(MAX_STEPS)]
steps_note = [None for _ in range(MAX_STEPS)]
edit_step  = tk.IntVar(value=-1)

grid = ttk.Frame(seq_frame); grid.grid(row=1, column=0, sticky="ew", pady=(6,6))

def midi_to_name(n): return f"{note_names[n%12]}{n//12}"
def name_or_dash(n): return "" if n is None else midi_to_name(n)

step_widgets = []
def rebuild_grid(*_):
    for child in grid.winfo_children(): child.destroy()
    step_widgets.clear()
    length = seq_len_var.get()
    for i in range(length):
        col=ttk.Frame(grid, padding=(2,0)); col.grid(row=0, column=i, sticky="n")
        btn = ttk.Button(col, text=name_or_dash(steps_note[i]), width=4)
        def make_setter(idx): return lambda: edit_step.set(idx)
        btn.configure(command=make_setter(i))
        btn.grid(row=0, column=0, pady=(0,2))
        chk_on  = ttk.Checkbutton(col, text=str(i+1), variable=steps_on[i]); chk_on.grid(row=1, column=0)
        chk_acc = ttk.Checkbutton(col, text="Acc",     variable=steps_acc[i]); chk_acc.grid(row=2, column=0)
        step_widgets.append((btn, chk_on, chk_acc))
    grid.update_idletasks()
rebuild_grid()
seq_len_var.trace_add("write", rebuild_grid)

# On-screen Keyboard
kbd_frame = ttk.LabelFrame(seq_frame, text="Keyboard (Klick & PC-Tastatur)")
kbd_frame.grid(row=2, column=0, sticky="ew")
kbd_frame.columnconfigure(0, weight=1)
canvas = tk.Canvas(kbd_frame, height=140, bg="#222")
canvas.grid(row=0, column=0, columnspan=8, sticky="ew", padx=0, pady=4)
seq_kbd_oct = tk.IntVar(value=4)
ttk.Label(kbd_frame, text="Oktave").grid(row=1, column=0, sticky="e")
ttk.Spinbox(kbd_frame, from_=0, to=8, width=4, textvariable=seq_kbd_oct).grid(row=1, column=1, sticky="w")
white_notes = [0,2,4,5,7,9,11]; black_notes = [1,3,6,8,10]
key_rects = {}
def draw_keys(*_):
    canvas.delete("all"); key_rects.clear()
    W = canvas.winfo_width() or 900
    H = 140
    ww = max(10, W // (7*2))
    x = 0
    for o in range(2):
        for wn in white_notes:
            midi = (seq_kbd_oct.get()+o)*12 + wn
            r = canvas.create_rectangle(x, 0, x+ww, H, fill="white", outline="#111")
            key_rects[r] = midi
            x += ww
    blkmap = {1:0, 3:1, 6:3, 8:4, 10:5}
    def bx_for_white_idx(idx): return (idx+1)*ww
    for o in range(2):
        for bn in black_notes:
            idx = blkmap[bn] + 7*o
            midi = (seq_kbd_oct.get()+o)*12 + bn
            bw = int(ww*0.6); bh = int(H*0.65)
            bx = bx_for_white_idx(idx) - bw//2
            r = canvas.create_rectangle(bx, 0, bx+bw, bh, fill="black", outline="#000")
            key_rects[r] = midi
canvas.bind("<Configure>", draw_keys); draw_keys()

def assign_or_play(note, velocity=None):
    idx = edit_step.get()
    if 0 <= idx < seq_len_var.get():
        steps_note[idx] = note
        step_widgets[idx][0].configure(text=name_or_dash(note))
    jx.note_on(note, vel_seq.get() if velocity is None else velocity)
    root.after(max(60, gate_var.get()//2), lambda n=note: jx.note_off(n))
def on_canvas_click(evt):
    item = canvas.find_withtag("current")
    if not item: return
    rid = item[0]
    if rid in key_rects:
        assign_or_play(key_rects[rid])
canvas.bind("<Button-1>", on_canvas_click)

# PC keyboard (DE/EN aware)
def current_keymaps():
    lay = resolve_layout()
    whites = (['y','x','c','v','b','n','m'] if lay=="DE" else ['z','x','c','v','b','n','m'])
    blacks = {'s':1, 'd':3, 'g':6, 'h':8, 'j':10}
    return whites, blacks

pressed_keys = set()
def key_to_note(event):
    whites, blacks = current_keymaps()
    ch = event.keysym.lower()
    base = seq_kbd_oct.get()*12
    if ch in whites:
        idx = whites.index(ch)
        return base + [0,2,4,5,7,9,11][idx]
    if ch in blacks:
        return base + blacks[ch]
    if ch == 'comma':   # ,
        seq_kbd_oct.set(max(0, seq_kbd_oct.get()-1)); draw_keys()
    if ch == 'period':  # .
        seq_kbd_oct.set(min(8, seq_kbd_oct.get()+1)); draw_keys()
    return None
def on_key_press(event):
    n = key_to_note(event)
    if n is None: return
    if event.keysym in pressed_keys: return
    pressed_keys.add(event.keysym)
    assign_or_play(n)
def on_key_release(event):
    if event.keysym in pressed_keys:
        pressed_keys.discard(event.keysym)
root.bind("<KeyPress>", on_key_press)
root.bind("<KeyRelease>", on_key_release)

# Sequencer engine
is_running=False
cur_step=0
def step_duration_ms():
    return (60000.0 / max(1, bpm_var.get())) / 4.0  # 16th
def schedule_next():
    if not is_running: return
    dur = step_duration_ms()
    delay = int(dur + (swing.get()*dur if (cur_step % 2)==1 else 0))
    root.after(delay, tick)
def tick():
    global cur_step
    if not is_running: return
    length = seq_len_var.get()
    for i, (btn, _, _) in enumerate(step_widgets[:length]):
        btn.configure(style="HL.TButton" if i==cur_step else "TButton")
    if steps_on[cur_step].get():
        note = steps_note[cur_step]
        if note is None:
            note = seq_kbd_oct.get()*12  # default C
        vel = max(1, min(127, vel_seq.get() + (20 if steps_acc[cur_step].get() else 0)))
        jx.note_on(note, vel)
        root.after(max(10, gate_var.get()), lambda nn=note: jx.note_off(nn))
    cur_step = (cur_step + 1) % length
    schedule_next()
def start_seq():
    global is_running, cur_step
    if is_running: return
    is_running = True
    cur_step = 0
    schedule_next()
def stop_seq():
    global is_running
    is_running = False
style = ttk.Style()
style.configure("HL.TButton", foreground="#00A3FF")
playbar = ttk.Frame(seq_frame); playbar.grid(row=3, column=0, sticky="ew", pady=(6,0))
ttk.Button(playbar, text="Play", command=start_seq).pack(side="left", padx=(0,6))
ttk.Button(playbar, text="Stop", command=stop_seq).pack(side="left")

# -------------------- GO --------------------
root.mainloop()

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Previous Post

Next Post

Loading Next Post...
Loading

Signing-in 3 seconds...