Making a keyboard

Here is a keyboard to capture a little phrase for the degree and dur for the default Synthdef using the Chromatic scale. Please, Tell me if anything is off.
Use this to test it;

(

Pbind(
    \scale, Scale.chromatic.degrees,
    \degree, Pseq([8, 7, 1, 1, 3, 0],1),
    \dur, Pseq([0.606, 0.783, 1.229, 0.799, 0.836, 0.814],1)).play;

)
(
// Function to create our keyboard
~createKeyboard = {
    var startTime;

    // Define a simple synth with an envelope
    SynthDef(\keyboardSynth, { |out=0, freq=440, amp=0.5, gate=1|
        var sig, env;
        env = EnvGen.kr(Env.adsr(0.01, 0.1, 0.5, 0.1), gate, doneAction: 2);
        sig = SinOsc.ar(freq) * env * amp;
        Out.ar(out, sig ! 2);
    }).add;

    // Create a window for the keyboard
    ~win = Window("Two-Octave Chromatic Keyboard with Timing", Rect(100, 100, 1000, 450)).front;

    // Define note names and their corresponding chromatic scale degrees for two octaves
    ~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);  // 0 to 11 for first octave, 12 to 23 for second octave

    // Array to store the sequence of played notes with timing
    ~sequence = List[];

    // Timing capture flag
    ~capturingTime = false;

    // Create buttons for each note
    ~buttons = ~notes.collect({ |note, i|
        var button = Button(~win, Rect(10 + (i * 40), 10, 35, 100))
            .states_([[note.asString, Color.black, Color.white], [note.asString, Color.white, Color.black]])
            .action_({ |but|
                var freq = (60 + i).midicps;
                var synth = Synth(\keyboardSynth, [\freq, freq, \amp, 0.5]);
                if(~capturingTime, {
                    var elapsedTime = Main.elapsedTime - startTime;
                    ~sequence.add([~scaleDegrees[i], elapsedTime.round(0.001)]);
                }, {
                    ~sequence.add([~scaleDegrees[i], nil]);
                });
                but.value = 1;
                AppClock.sched(0.2, {
                    but.value = 0;
                    synth.set(\gate, 0);
                    nil
                });
            });
    });

    // Create a text field to display the sequence
    ~seqDisplay = TextView(~win, Rect(10, 120, 980, 100))
        .string_("Played sequence: ")
        .editable_(false);

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

    // Create a button to print the 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, { item[1] }, { item[1] - ~sequence[i-1][1] })
                }).round(0.001);
            });
            "Note Array:".postln;
            noteArray.postln;
            "Duration Array:".postln;
            durArray.postln;
            "Pbind pattern:".postln;
            ("Pbind(\\degree, " ++ noteArray.asCompileString ++
             ", \\dur, " ++ durArray.asCompileString ++ ")").postln;
        });

    // Create a button to play the sequence
    Button(~win, Rect(230, 230, 100, 30))
        .states_([["Play Sequence"]])
        .action_({
            Routine({
                var prevTime = 0;
                ~sequence.do({ |item|
                    var degree = item[0];
                    var time = item[1];
                    var freq = (degree + 60).midicps;
                    var synth;
                    if(time.notNil, {
                        (time - prevTime).wait;
                        prevTime = time;
                    }, {
                        0.5.wait;
                    });
                    synth = Synth(\keyboardSynth, [\freq, freq, \amp, 0.5]);
                    AppClock.sched(0.2, { synth.set(\gate, 0); nil });
                });
            }).play;
        });

    // Create a button to start/stop timing capture
    ~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;
            });
        });

    // Update sequence display function
    ~updateSeqDisplay = {
        ~seqDisplay.string_("Played sequence: " ++ ~sequence.collect({ |item|
            var note = item[0];
            var time = item[1];
            if(time.isNil, {
                note.asString
            }, {
                note.asString ++ "@" ++ time.round(0.001).asString
            });
        }).join(", "));
    };

    // Set up a routine to periodically update the sequence display
    Routine({
        loop {
            ~updateSeqDisplay.();
            0.1.wait;
        }
    }).play(AppClock);
};

// Check if server is running, boot if necessary, then create the keyboard
if(s.serverRunning.not) {
    s.waitForBoot({
        ~createKeyboard.value;
    });
} {
    ~createKeyboard.value;
};
)

Also, I want to get into other tunings, there is Scale but I believe another object has more tunings, no? I’d like to find microtunings if they are available.

The documentation can actually help you find this – the top of the Scale helpfile reads:

Scale : Object

represents a musical scale

Source: Scale.sc

Subclasses: ScaleAD

See also: Tuning

Where “See also” points to what you’re looking for.

hjh

Ok, great, thank you. Ive returned to using SC after being away for 7 years. I like using sc despite hating manuals. My Synthi is in the shop, So I got back into it looking for a solution that combines cv out, midi and sample sequencing. I was going to go all hardware, but there isn’t much out there for sequencing that is any fun. Aside from expensive sequencers that only do one thing, like the Arp sequencer or broad ones like the cirklon which is great. But a huge investment. Yes, Im using Cluade here, but ive read the original SC book, the easy intro book, and there is mountains of reading in the actual SC program. SC is a giant rtfm. You can’t really get much from Claude unless you know some stuff about sc, to point it in the right direction. Im going through the pattern tutorial you wrote now, There is always one more thing to read.

Here is a much better version. Activates computer keyboard playability.
Octaves can be controlled by using the number keys.

(updated to record Chords as well)


(
// Function to create our keyboard
~createKeyboard = {
    var startTime, currentOctave = 0;
    var keyboardActive = false;  // Variable to track keyboard activation state
    var chordNotes = Set.new;  // Set to store currently active notes for chord detection

    // Define a simple synth with an envelope
    SynthDef(\keyboardSynth, { |out=0, freq=440, amp=0.5, gate=1|
        var sig, env;
        env = EnvGen.kr(Env.adsr(0.01, 0.1, 0.5, 0.1), gate, doneAction: 2);
        sig = SinOsc.ar(freq) * env * amp;
        Out.ar(out, sig ! 2);
    }).add;

    // Create a window for the keyboard
    ~win = Window("Two-Octave Chromatic Keyboard with Octave Shift and Chord Recording", Rect(100, 100, 1000, 400)).front;

    // Define note names and their corresponding chromatic scale degrees for two octaves
    ~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);

    // Define keyboard mapping
    ~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
    ];

    // Array to store the sequence of played notes and chords with timing
    ~sequence = List[];

    // Timing capture flag
    ~capturingTime = false;

    // Dictionary to store active synths
    ~activeSynths = Dictionary.new;

    // Function to play a note
    ~playNote = { |degree|
        var adjustedDegree = degree + (currentOctave * 12);
        var freq = (adjustedDegree + 60).midicps;
        var synth;

        // Stop the previous synth for this degree if it exists
        ~activeSynths[adjustedDegree].do({ |syn| syn.set(\gate, 0) });

        synth = Synth(\keyboardSynth, [\freq, freq, \amp, 0.5]);
        ~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]);
            });
        });

        // Update button state
        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
            });
        });
    };

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

    // Create buttons for each note
    ~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) });
    });

    // Create a text field to display the sequence
    ~seqDisplay = TextView(~win, Rect(10, 120, 980, 100))
        .string_("Played sequence: ")
        .editable_(false);

    // Create buttons for various actions
    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  // Default duration if no timestamp
                }, {
                    if(i == 0, {
                        0.5  // Default duration for the first note/chord
                    }, {
                        var prevTime = ~sequence[i-1][1];
                        if(prevTime.isNil, {
                            0.5  // Default duration if previous timestamp is missing
                        }, {
                            (item[1] - prevTime).max(0.01)  // Ensure positive duration
                        });
                    });
                });
            });
            "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"]])
        .action_({
            Routine({
                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 freq = (degree + 60).midicps;
                        Synth(\keyboardSynth, [\freq, freq, \amp, 0.5]);
                    });
                    AppClock.sched(0.2, { synths.do(_.set(\gate, 0)); nil });
                });
            }).play;
        });

    ~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;
            });
        });

    // Create buttons for octave shifting
    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;
        });

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

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

    // New button to activate/deactivate keyboard input
    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);
            });
        });

    // Update sequence display function
    ~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(", "));
    };

    // Set up a routine to periodically update the sequence display
    Routine({
        loop {
            ~updateSeqDisplay.();
            0.1.wait;
        }
    }).play(AppClock);

    // Add key responder
    ~win.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
        if(keyboardActive, {
            var degree = ~keyboardMap[char.toLower];
            if(degree.notNil, {
                ~playNote.(degree);
            });
            // Number keys for octave selection
            if(char.isDecDigit, {
                var num = char.digit;
                currentOctave = num - 2;  // Shift range to be from -1 to 7
                currentOctave = currentOctave.clip(-1, 7);  // Limit range
                ~updateOctaveDisplay.value;
            });
        });
    };

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

// Execute the function immediately
~createKeyboard.value;
)

1 Like

Ill post these to sccode.org
but since im here;

And finally a Keyboard that plays a sample. Maybe some other shmuck out there like me will find it useful .
To get it to track you must prepare your sample to be Middle C
If you have a sample but don’t know its exact frequency, you can use Audacity to analyze and adjust it. Here’s a step-by-step process to prepare your sample for use in the SuperCollider keyboard app:
Determine the sample’s frequency:
Open your sample in Audacity.
Select a stable portion of the note (avoid the attack and decay).
Go to Analyze > Plot Spectrum.
In the spectrum analysis window, look for the highest peak. This is likely your fundamental frequency.
Note down this frequency.
Adjust the sample to C4 (261.63 Hz):
Select the entire audio sample.
Go to Effect > Change Pitch.
In the Change Pitch window, you’ll see “Frequency (Hz)” fields.
Enter the frequency you found in step 1 in the “from” field.
Enter 261.63 in the “to” field.
Click “OK” to apply the change.
Verify the adjustment:
After changing the pitch, use the Plot Spectrum tool again to confirm the new fundamental frequency is close to 261.63 Hz.
Fine-tune if necessary:
If it’s slightly off, you can make small adjustments using the Change Pitch effect again.
Prepare the sample as described earlier:
Trim, normalize, loop (if desired), and apply fades.
Export the adjusted sample:
Go to File > Export > Export as WAV.
(just replace my path to your own sample)

Again, activating the Computer Keyboard lets you play the Keyboard with your computer Keys if you are on a Mac, not sure if it works with PC, and the number buttons set the octave. Let me know if you care and I’ll post more variations of this Keyboard. If not, I’ll get me coat.

(
// Function to create our keyboard
~createKeyboard = {
    var startTime, currentOctave = 0;
    var keyboardActive = false;
    var chordNotes = Set.new;
    var buffer;

    // Load the audio file
    buffer = Buffer.read(s, "/Users/ss/Documents/sounds/celmid4St.wav");

    // Define a sampler synth
    SynthDef(\samplerSynth, { |out=0, bufnum, rate=1, amp=0.5, gate=1|
        var sig, env;
        env = EnvGen.kr(Env.adsr(0.01, 0.4, 1, 0.7), gate, doneAction: 2);
        sig = PlayBuf.ar(2, bufnum, rate * BufRateScale.kr(bufnum), loop: 0) * env * amp;
        Out.ar(out, sig);
    }).add;

    // Create a window for the keyboard
    ~win = Window("Sampler Keyboard with Chord Recording", Rect(100, 100, 1000, 400)).front;

    // Define note names and their corresponding chromatic scale degrees for two octaves
    ~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);

    // Define keyboard mapping
    ~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
    ];

    // Array to store the sequence of played notes and chords with timing
    ~sequence = List[];

    // Timing capture flag
    ~capturingTime = false;

    // Dictionary to store active synths
    ~activeSynths = Dictionary.new;

    // Function to play a note
    ~playNote = { |degree|
        var adjustedDegree = degree + (currentOctave * 12);
        var rate = 2.pow(adjustedDegree / 12);  // Calculate playback rate
        var synth;

        // Stop the previous synth for this degree if it exists
        ~activeSynths[adjustedDegree].do({ |syn| syn.set(\gate, 0) });

        synth = Synth(\samplerSynth, [\bufnum, buffer, \rate, rate, \amp, 0.5]);
        ~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]);
            });
        });

        // Update button state
        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
            });
        });
    };

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

    // Create buttons for each note
    ~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) });
    });

    // Create a text field to display the sequence
    ~seqDisplay = TextView(~win, Rect(10, 120, 980, 100))
        .string_("Played sequence: ")
        .editable_(false);

    // Create buttons for various actions
    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  // Default duration if no timestamp
                }, {
                    if(i == 0, {
                        0.5  // Default duration for the first note/chord
                    }, {
                        var prevTime = ~sequence[i-1][1];
                        if(prevTime.isNil, {
                            0.5  // Default duration if previous timestamp is missing
                        }, {
                            (item[1] - prevTime).max(0.01)  // Ensure positive duration
                        });
                    });
                });
            });
            "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"]])
        .action_({
            Routine({
                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 freq = (degree + 60).midicps;
                        Synth(\samplerSynth, [\freq, freq, \amp, 0.5]);
                    });
                    AppClock.sched(0.2, { synths.do(_.set(\gate, 0)); nil });
                });
            }).play;
        });

    ~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;
            });
        });

    // Create buttons for octave shifting
    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;
        });

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

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

    // New button to activate/deactivate keyboard input
    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);
            });
        });

    // Update sequence display function
    ~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(", "));
    };

    // Set up a routine to periodically update the sequence display
    Routine({
        loop {
            ~updateSeqDisplay.();
            0.1.wait;
        }
    }).play(AppClock);

    // Add key responder
    ~win.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
        if(keyboardActive, {
            var degree = ~keyboardMap[char.toLower];
            if(degree.notNil, {
                ~playNote.(degree);
            });
            // Number keys for octave selection
            if(char.isDecDigit, {
                var num = char.digit;
                currentOctave = num - 2;  // Shift range to be from -1 to 7
                currentOctave = currentOctave.clip(-1, 7);  // Limit range
                ~updateOctaveDisplay.value;
            });
        });
    };

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

// Execute the function immediately
~createKeyboard.value;
)