Gui method default Keyboard control

I have a little app in SC that has a Keyboard at the top.
It is cumbersome to add effects and corresponding sliders and other gui widgets. I thought maybe going the way of Jit with its auto Gui ability would be a better way. I just don’t know of a way to add keyboard control.

this is the app I have going currently. It’s nice for making sounds for sampling. Or just banging out little melodies for loops. But Id like to be able to add effects or remove them at will, and it’s hard the way it is set up now to do that.
I am using the PortedPlugins for some things.

(
// Define global variables for buses and synths
~samplerBus = Bus.audio(s, 2);
~bitCrusherBus = Bus.audio(s, 2);
~chorusBus = Bus.audio(s, 2);
~reverbBus = Bus.audio(s, 2);

// Define SynthDefs
SynthDef(\samplerSynth, { |out=0, bufnum, rate=1, amp=0.5, gate=1, filterFreq=500, filterRes=0.1, filterOverdrive=0|
    var sig, env;
    env = EnvGen.kr(Env.adsr(0.01, 3, 4, 7), gate, doneAction: 2);
    sig = PlayBuf.ar(2, bufnum, rate * BufRateScale.kr(bufnum), loop: 0) * env * amp;
    sig = VADiodeFilter.ar(sig, filterFreq, filterRes, filterOverdrive);
    Out.ar(out, sig);
}).add;

SynthDef(\bitCrusher, { |in, out, wet=1, amp=0.8, bits=32, resampleRate=22050, lag=0|
    var sig = In.ar(in, 2);
    sig = sig.round(2 ** (1 - bits));
    sig = Latch.ar(sig, Impulse.ar(resampleRate));
    sig = sig.lag(lag * 0.001);
    sig = XFade2.ar(In.ar(in, 2), sig, wet * 2 - 1);
    Out.ar(out, sig * amp);
}).add;

SynthDef(\chorus, { |in, out, wet=0.5, rate=0.4, depth=0.03, amp=0.9|
    var sig = In.ar(in, 2);
    var modulation = SinOsc.kr(rate, 0, depth, depth + 0.004);
    var effect = AllpassL.ar(sig, 0.1, modulation, 0.1);
    sig = XFade2.ar(sig, effect, wet * 2 - 1);
    Out.ar(out, sig * amp);
}).add;

SynthDef(\reverb, { |in, out, roomSize=10, revTime=3, damping=0.5, inputBW=0.5, dry=1, early=0.7, tail=0.5, wet=0.5, amp=0.7|
    var sig = In.ar(in, 2);
    var verb = GVerb.ar(sig, roomSize, revTime, damping, inputBW, spread: 15, drylevel: dry, earlyreflevel: early, taillevel: tail);
    sig = XFade2.ar(sig, verb, wet * 2 - 1);
    Out.ar(out, sig * amp);
}).add;

SynthDef(\limiter, { |in, out, level=0.99, dur=0.01|
    var sig = In.ar(in, 2);
    sig = Limiter.ar(sig, level, dur);
    Out.ar(out, sig);
}).add;

~createKeyboard = {
    var buffer, bitCrusherSynth, chorusSynth, reverbSynth, limiterSynth;
    var startTime, currentOctave = 0;
    var keyboardActive = false;
    var chordNotes = Set.new;
    var looping = false, playRoutine;
    var tempoClock;
    var filterFreq = 500, filterRes = 0.1, filterOverdrive = 0;
    var bcWet = 1, bcBits = 32, bcRs = 22050, bcLag = 0;
    var chorusWet = 0.2, chorusRate = 0.01, chorusDepth = 0.0004;
    var gverbRoomSize = 10, gverbRevTime = 3, gverbDamping = 0.5, gverbInputBW = 0.5, gverbDry = 1, gverbEarly = 0.7, gverbTail = 0.5, gverbWet = 0.5;

    buffer = Buffer.read(s, "/Users/ss/Documents/sounds/Pmoog.wav");

    tempoClock = TempoClock.new(2);

    // Create effect synths
    bitCrusherSynth = Synth(\bitCrusher, [\in, ~samplerBus, \out, ~bitCrusherBus]);
    chorusSynth = Synth(\chorus, [\in, ~bitCrusherBus, \out, ~chorusBus], addAction: \addToTail);
    reverbSynth = Synth(\reverb, [\in, ~chorusBus, \out, ~reverbBus], addAction: \addToTail);
    limiterSynth = Synth(\limiter, [\in, ~reverbBus, \out, 0], addAction: \addToTail);

    ~win = Window("Sampler Keyboard with Effects", Rect(100, 100, 1000, 990)).front;
    ~scrollView = ScrollView(~win, Rect(0, 0, 1000, 600));
    ~contentView = View.new(~scrollView, Rect(0, 0, 980, 1200));

    ~notes = [\C, \Cs, \D, \Ds, \E, \F, \Fs, \G, \Gs, \A, \As, \B,
               \C, \Cs, \D, \Ds, \E, \F, \Fs, \G, \Gs, \A, \As, \B];
    ~scaleDegrees = (0..23);

    ~keyboardMap = Dictionary[
        $a -> 0, $w -> 1, $s -> 2, $e -> 3, $d -> 4, $f -> 5, $t -> 6, $g -> 7, $y -> 8, $h -> 9, $u -> 10, $j -> 11,
        $k -> 12, $o -> 13, $l -> 14, $p -> 15, $; -> 16, $' -> 17,
        $z -> 0, $x -> 2, $c -> 4, $v -> 5, $b -> 7, $n -> 9, $m -> 11, $, -> 12, $. -> 14, $/ -> 16
    ];

    ~sequence = List[];
    ~activeSynths = Dictionary.new;

    ~playNote = { |degree|
        var adjustedDegree = degree + (currentOctave * 12);
        var rate = 2.pow(adjustedDegree / 12);
        var synth;

        ~activeSynths[adjustedDegree].do({ |syn| syn.set(\gate, 0) });

        synth = Synth(\samplerSynth, [
            \out, ~samplerBus,
            \bufnum, buffer, \rate, rate, \amp, 0.5,
            \filterFreq, filterFreq, \filterRes, filterRes, \filterOverdrive, filterOverdrive
        ]);
        ~activeSynths[adjustedDegree] = synth;

        chordNotes.add(adjustedDegree);

        if(~capturingTime, {
            var elapsedTime = Main.elapsedTime - startTime;
            if(chordNotes.size > 1, {
                ~sequence.add([chordNotes.asArray.sort, elapsedTime.round(0.001)]);
            }, {
                ~sequence.add([adjustedDegree, elapsedTime.round(0.001)]);
            });
        }, {
            if(chordNotes.size > 1, {
                ~sequence.add([chordNotes.asArray.sort, nil]);
            }, {
                ~sequence.add([adjustedDegree, nil]);
            });
        });

        if(degree < 24, {
            ~buttons[degree].states = [[~notes[degree].asString, Color.white, Color.black]];
            AppClock.sched(0.2, {
                ~buttons[degree].states = [[~notes[degree].asString, Color.black, Color.white]];
                nil
            });
        });
    };

    ~stopNote = { |degree|
        var adjustedDegree = degree + (currentOctave * 12);
        ~activeSynths[adjustedDegree].do({ |syn| syn.set(\gate, 0) });
        ~activeSynths[adjustedDegree] = nil;
        chordNotes.remove(adjustedDegree);
    };

    ~buttons = ~notes.collect({ |note, i|
        Button(~win, Rect(10 + (i * 40), 10, 35, 100))
            .states_([[note.asString, Color.black, Color.white]])
            .mouseDownAction_({ ~playNote.(i) })
            .mouseUpAction_({ ~stopNote.(i) });
    });

    ~seqDisplay = TextView(~win, Rect(10, 120, 980, 100))
        .string_("Played sequence: ")
        .editable_(false);

    Button(~win, Rect(10, 230, 100, 30))
        .states_([["Clear Sequence"]])
        .action_({
            ~sequence.clear;
            ~seqDisplay.string_("Played sequence: ");
        });

    Button(~win, Rect(120, 230, 100, 30))
        .states_([["Print Sequence"]])
        .action_({
            var noteArray, durArray;
            noteArray = ~sequence.collect({ |item| item[0] });
            durArray = ~sequence.collect({ |item, i|
                if(item[1].isNil, {
                    0.5
                }, {
                    if(i == 0, {
                        0.5
                    }, {
                        var prevTime = ~sequence[i-1][1];
                        if(prevTime.isNil, {
                            0.5
                        }, {
                            (item[1] - prevTime).max(0.01)
                        });
                    });
                });
            });
            "Note/Chord Array:".postln;
            noteArray.postln;
            "Duration Array:".postln;
            durArray.postln;
            "Pbind pattern:".postln;
            ("Pbind(\\degree, " ++ noteArray.collect({|item| item.asArray}).asCompileString ++
             ", \\dur, " ++ durArray.asCompileString ++ ")").postln;
        });

    Button(~win, Rect(230, 230, 100, 30))
        .states_([
            ["Play Sequence", Color.black, Color.green],
            ["Stop Sequence", Color.white, Color.red]
        ])
        .action_({ |but|
            if(but.value == 1, {
                looping = true;
                playRoutine = Routine({
                    while { looping } {
                        var prevTime = 0;
                        ~sequence.do({ |item|
                            var degrees = item[0].asArray;
                            var time = item[1];
                            var synths;
                            if(time.notNil, {
                                (time - prevTime).wait;
                                prevTime = time;
                            }, {
                                0.5.wait;
                            });
                            synths = degrees.collect({ |degree|
                                var rate = 2.pow(degree / 12);
                                Synth(\samplerSynth, [
                                    \out, ~samplerBus,
                                    \bufnum, buffer, \rate, rate, \amp, 0.5,
                                    \filterFreq, filterFreq, \filterRes, filterRes, \filterOverdrive, filterOverdrive
                                ]);
                            });
                            AppClock.sched(0.2, { synths.do(_.set(\gate, 0)); nil });
                        });
                        0.1.wait;
                    }
                }).play(tempoClock);
            }, {
                looping = false;
                playRoutine.stop;
            });
        });

    ~timingButton = Button(~win, Rect(340, 230, 150, 30))
        .states_([
            ["Start Timing Capture", Color.black, Color.green],
            ["Stop Timing Capture", Color.white, Color.red]
        ])
        .action_({ |but|
            if(but.value == 1, {
                ~capturingTime = true;
                startTime = Main.elapsedTime;
            }, {
                ~capturingTime = false;
            });
        });

    Button(~win, Rect(500, 230, 100, 30))
        .states_([["Octave Down"]])
        .action_({
            currentOctave = (currentOctave - 1).clip(-1, 7);
            ~updateOctaveDisplay.value;
        });

    Button(~win, Rect(610, 230, 100, 30))
        .states_([["Octave Up"]])
        .action_({
            currentOctave = (currentOctave + 1).clip(-1, 7);
            ~updateOctaveDisplay.value;
        });

    ~octaveDisplay = StaticText(~win, Rect(720, 230, 100, 30))
        .string_("Octave: 0")
        .align_(\center);

    ~updateOctaveDisplay = {
        ~octaveDisplay.string_("Octave: " ++ currentOctave);
    };

    Button(~win, Rect(830, 230, 150, 30))
        .states_([
            ["Activate Keyboard", Color.black, Color.green],
            ["Deactivate Keyboard", Color.white, Color.red]
        ])
        .action_({ |but|
            keyboardActive = but.value == 1;
            if(keyboardActive, {
                ~win.view.focus(true);
            });
        });

    Slider(~win, Rect(10, 270, 300, 20))
        .action_({ |sl|
            var tempo = sl.value.linexp(0, 1, 0.5, 4);
            tempoClock.tempo = tempo;
            ~tempoDisplay.string = "Tempo: " ++ (tempo * 60).round(0.1) ++ " BPM";
        })
        .value_(0.6349);

    ~tempoDisplay = StaticText(~win, Rect(320, 270, 150, 20))
        .string_("Tempo: 120.0 BPM");

    // Filter controls
    StaticText(~win, Rect(10, 330, 100, 20)).string_("Filter Freq:");
    Slider(~win, Rect(10, 350, 300, 20))
        .action_({ |sl|
            filterFreq = sl.value.linexp(0, 1, 20, 20000);
            ~filterFreqDisplay.string = "Filter Freq: " ++ filterFreq.round(0.1) ++ " Hz";
        })
        .value_(0.5);
    ~filterFreqDisplay = StaticText(~win, Rect(320, 350, 150, 20))
        .string_("Filter Freq: 500.0 Hz");

    StaticText(~win, Rect(10, 380, 100, 20)).string_("Filter Res:");
    Slider(~win, Rect(10, 400, 300, 20))
        .action_({ |sl|
            filterRes = sl.value.linlin(0, 1, 0.0, 1.0);

~filterResDisplay.string = "Filter Res: " ++ filterRes.round(0.001);
        })
        .value_(0.1);
    ~filterResDisplay = StaticText(~win, Rect(320, 400, 150, 20))
        .string_("Filter Res: 0.1");

    StaticText(~win, Rect(10, 430, 100, 20)).string_("Filter Overdrive:");
    Slider(~win, Rect(10, 450, 300, 20))
        .action_({ |sl|
            filterOverdrive = sl.value.linlin(0, 1, 0.0, 1.0);
            ~filterOverdriveDisplay.string = "Filter Overdrive: " ++ filterOverdrive.round(0.001);
        })
        .value_(0);
    ~filterOverdriveDisplay = StaticText(~win, Rect(320, 450, 150, 20))
        .string_("Filter Overdrive: 0.0");

    // Bitcrusher controls
    StaticText(~win, Rect(10, 480, 100, 20)).string_("BC Wet:");
    Slider(~win, Rect(10, 500, 300, 20))
        .action_({ |sl|
            bcWet = sl.value;
            bitCrusherSynth.set(\wet, bcWet);
            ~bcWetDisplay.string = "BC Wet: " ++ bcWet.round(0.001);
        })
        .value_(1);
    ~bcWetDisplay = StaticText(~win, Rect(320, 500, 150, 20))
        .string_("BC Wet: 1.0");

    StaticText(~win, Rect(10, 530, 100, 20)).string_("BC Bits:");
    Slider(~win, Rect(10, 550, 300, 20))
        .action_({ |sl|
            bcBits = sl.value.linlin(0, 1, 1, 32).round;
            bitCrusherSynth.set(\bits, bcBits);
            ~bcBitsDisplay.string = "BC Bits: " ++ bcBits;
        })
        .value_(1);
    ~bcBitsDisplay = StaticText(~win, Rect(320, 550, 150, 20))
        .string_("BC Bits: 32");

    StaticText(~win, Rect(10, 580, 100, 20)).string_("BC Rs:");
    Slider(~win, Rect(10, 600, 300, 20))
        .action_({ |sl|
            bcRs = sl.value.linexp(0, 1, 100, 44100).round;
            bitCrusherSynth.set(\resampleRate, bcRs);
            ~bcRsDisplay.string = "BC Rs: " ++ bcRs ++ " Hz";
        })
        .value_(0.7);
    ~bcRsDisplay = StaticText(~win, Rect(320, 600, 150, 20))
        .string_("BC Rs: 22050 Hz");

    StaticText(~win, Rect(10, 630, 100, 20)).string_("BC Lag:");
    Slider(~win, Rect(10, 650, 300, 20))
        .action_({ |sl|
            bcLag = sl.value * 100;
            bitCrusherSynth.set(\lag, bcLag);
            ~bcLagDisplay.string = "BC Lag: " ++ bcLag.round(0.001) ++ " ms";
        })
        .value_(0);
    ~bcLagDisplay = StaticText(~win, Rect(320, 650, 150, 20))
        .string_("BC Lag: 0 ms");

    // Chorus controls
    StaticText(~win, Rect(10, 680, 100, 20)).string_("Chorus Wet:");
    Slider(~win, Rect(10, 700, 300, 20))
        .action_({ |sl|
            chorusWet = sl.value;
            chorusSynth.set(\wet, chorusWet);
            ~chorusWetDisplay.string = "Chorus Wet: " ++ chorusWet.round(0.001);
        })
        .value_(0.1);
    ~chorusWetDisplay = StaticText(~win, Rect(320, 700, 150, 20))
        .string_("Chorus Wet: 0.5");

    StaticText(~win, Rect(10, 730, 100, 20)).string_("Chorus Rate:");
    Slider(~win, Rect(10, 750, 300, 20))
        .action_({ |sl|
            chorusRate = sl.value.linexp(0, 1, 0.01, 0.5);
            chorusSynth.set(\rate, chorusRate);
            ~chorusRateDisplay.string = "Chorus Rate: " ++ chorusRate.round(0.001) ++ " Hz";
        })
        .value_(0.01);
    ~chorusRateDisplay = StaticText(~win, Rect(320, 750, 150, 20))
        .string_("Chorus Rate: 0.01 Hz");

    StaticText(~win, Rect(10, 780, 100, 20)).string_("Chorus Depth:");
    Slider(~win, Rect(10, 800, 300, 20))
        .action_({ |sl|
            chorusDepth = sl.value * 0.1;
            chorusSynth.set(\depth, chorusDepth);
            ~chorusDepthDisplay.string = "Chorus Depth: " ++ chorusDepth.round(0.001);
        })
        .value_(0.004);
    ~chorusDepthDisplay = StaticText(~win, Rect(320, 800, 150, 20))
        .string_("Chorus Depth: 0.03");

    // Reverb controls
    StaticText(~win, Rect(500, 330, 100, 20)).string_("Room Size:");
    Slider(~win, Rect(500, 350, 300, 20))
        .action_({ |sl|
            gverbRoomSize = sl.value.linlin(0, 1, 1, 10);
            reverbSynth.set(\roomSize, gverbRoomSize);
            ~gverbRoomSizeDisplay.string = "Room Size: " ++ gverbRoomSize.round(0.1);
        })
        .value_(0.03);
    ~gverbRoomSizeDisplay = StaticText(~win, Rect(810, 350, 150, 20))
        .string_("Room Size: 10.0");

    StaticText(~win, Rect(500, 380, 100, 20)).string_("Rev Time:");
    Slider(~win, Rect(500, 400, 300, 20))
        .action_({ |sl|
            gverbRevTime = sl.value.linlin(0, 1, 0.1, 22);
            reverbSynth.set(\revTime, gverbRevTime);
            ~gverbRevTimeDisplay.string = "Rev Time: " ++ gverbRevTime.round(0.1) ++ " s";
        })
        .value_(0.029);
    ~gverbRevTimeDisplay = StaticText(~win, Rect(810, 400, 150, 20))
        .string_("Rev Time: 3.0 s");

    StaticText(~win, Rect(500, 430, 100, 20)).string_("Damping:");
    Slider(~win, Rect(500, 450, 300, 20))
        .action_({ |sl|
            gverbDamping = sl.value;
            reverbSynth.set(\damping, gverbDamping);
            ~gverbDampingDisplay.string = "Damping: " ++ gverbDamping.round(0.001);
        })
        .value_(0.5);
    ~gverbDampingDisplay = StaticText(~win, Rect(810, 450, 150, 20))
        .string_("Damping: 0.5");

    StaticText(~win, Rect(500, 480, 100, 20)).string_("Input BW:");
    Slider(~win, Rect(500, 500, 300, 20))
        .action_({ |sl|
            gverbInputBW = sl.value;
            reverbSynth.set(\inputBW, gverbInputBW);
            ~gverbInputBWDisplay.string = "Input BW: " ++ gverbInputBW.round(0.001);
        })
        .value_(0.5);
    ~gverbInputBWDisplay = StaticText(~win, Rect(810, 500, 150, 20))
        .string_("Input BW: 0.5");

    StaticText(~win, Rect(500, 530, 100, 20)).string_("Dry Level:");
    Slider(~win, Rect(500, 550, 300, 20))
        .action_({ |sl|
            gverbDry = sl.value;
            reverbSynth.set(\dry, gverbDry);
            ~gverbDryDisplay.string = "Dry Level: " ++ gverbDry.round(0.001);
        })
        .value_(1);
    ~gverbDryDisplay = StaticText(~win, Rect(810, 550, 150, 20))
        .string_("Dry Level: 1.0");

    StaticText(~win, Rect(500, 580, 100, 20)).string_("Early Ref Level:");
    Slider(~win, Rect(500, 600, 300, 20))
        .action_({ |sl|
            gverbEarly = sl.value;
            reverbSynth.set(\early, gverbEarly);
            ~gverbEarlyDisplay.string = "Early Ref Level: " ++ gverbEarly.round(0.001);
        })
        .value_(0.7);
    ~gverbEarlyDisplay = StaticText(~win, Rect(810, 600, 150, 20))
        .string_("Early Ref Level: 0.7");

    StaticText(~win, Rect(500, 630, 100, 20)).string_("Tail Level:");
    Slider(~win, Rect(500, 650, 300, 20))
        .action_({ |sl|
            gverbTail = sl.value;
            reverbSynth.set(\tail, gverbTail);
            ~gverbTailDisplay.string = "Tail Level: " ++ gverbTail.round(0.001);
        })
        .value_(0.5);
    ~gverbTailDisplay = StaticText(~win, Rect(810, 650, 150, 20))
        .string_("Tail Level: 0.5");

    StaticText(~win, Rect(500, 680, 100, 20)).string_("Reverb Wet/Dry:");
    Slider(~win, Rect(500, 700, 300, 20))
        .action_({ |sl|
            gverbWet = sl.value;
            reverbSynth.set(\wet, gverbWet);
            ~gverbWetDisplay.string = "Reverb Wet/Dry: " ++ gverbWet.round(0.001);
        })
        .value_(0.5);
    ~gverbWetDisplay = StaticText(~win, Rect(810, 700, 150, 20))
        .string_("Reverb Wet/Dry: 0.5");

    ~updateSeqDisplay = {
        ~seqDisplay.string_("Played sequence: " ++ ~sequence.collect({ |item|
            var notes = item[0];
            var time = item[1];
            var noteStr = if(notes.isArray, {
                "[" ++ notes.collect(_.asString).join(", ") ++ "]"
            }, {
                notes.asString
            });
            if(time.isNil, {
                noteStr
            }, {
                noteStr ++ "@" ++ time.round(0.001).asString
            });
        }).join(", "));
    };

    Routine({
        loop {
            ~updateSeqDisplay.();
            0.1.wait;
        }
    }).play(AppClock);

    ~win.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
        if(keyboardActive, {
            var degree = ~keyboardMap[char.toLower];
            if(degree.notNil, {
                ~playNote.(degree);
            });
            if(char.isDecDigit, {
                var num = char.digit;
                currentOctave = num - 2;
                currentOctave = currentOctave.clip(-1, 7);
                ~updateOctaveDisplay.value;
            });
        });
    };

    ~win.view.keyUpAction = { |view, char, modifiers, unicode, keycode|
        if(keyboardActive, {
            var degree = ~keyboardMap[char.toLower];
            if(degree.notNil, {
                ~stopNote.(degree);
            });
        });
    };

    ~win.onClose = {
        buffer.free;
        ~samplerBus.free;
        ~bitCrusherBus.free;
        ~chorusBus.free;
        ~reverbBus.free;
        bitCrusherSynth.free;
        chorusSynth.free;
        reverbSynth.free;
        limiterSynth.free;
        tempoClock.stop;
    };
};

// Call createKeyboard in a way that allows for server synchronization
s.waitForBoot({
    ~createKeyboard.value;
});
)

this works, but I was hoping there would be a simple automatic keyboard for the gui method
garageband has a nice one.

(
// Make sure server is booted and clean
s.waitForBoot({
    
    // Clear any existing Ndefs
    Ndef.clear;
    
    // Create the synth definition
    Ndef(\piano, { 
        var trig = \trig.tr(0);
        var freq = \freq.kr(440);
        var env = EnvGen.kr(Env.perc(0.01, 1), trig);
        var sig = SinOsc.ar(freq) * env * \amp.kr(0.5);
        sig ! 2  // stereo output
    }).play;
    
    // Create window
    w = Window("Piano", Rect(200, 200, 400, 150));
    w.front;
    w.view.decorator = FlowLayout(w.bounds);
    
    // Create basic white keys (C4 to C5)
    ["C", "D", "E", "F", "G", "A", "B", "C"].do { |note, i|
        var midiNote = 60 + i;  // C4 = 60
        Button(w, 45@100)
        .states_([[note, Color.black, Color.white]])
        .action_({ 
            Ndef(\piano).set(
                \freq, midiNote.midicps,
                \trig, 1
            );
        });
    };
    
    // Add cleanup when window closes
    w.onClose = {
        Ndef(\piano).clear;
    };
});
)

Are you looking for a keyboard widget?

I provide one in my Graphical Module Quark.

Exactly, does it fit work with the Jit lib?

looks very cool, but I was hoping for a simple default gui behavior in the NdefGui.