Skip to main content

sim_lib_view_daw/
keyboard.rs

1//! On-screen performance keyboard Scene descriptors.
2//!
3//! The keyboard is a pure Scene descriptor: it names the bound performance
4//! source, player chain, instrument, and stream bridge route, then leaves
5//! interaction to the browser Intent emitter.
6
7use sim_kernel::{Expr, Symbol};
8use sim_lib_scene::{data_map, node, sym};
9use sim_value::build::{int, list, text, uint};
10
11/// Stable lens id for the on-screen performance keyboard.
12pub const PERFORMANCE_KEYBOARD_VIEW_ID: &str = "view:performance-keyboard";
13
14/// Demo fixture name for the bound player-chain keyboard.
15pub const PERFORMANCE_KEYBOARD_DEMO_FIXTURE: &str = "player-chain-instrument";
16
17/// Where browser keyboard gestures are sent.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct PerformanceKeyboardBinding {
20    /// Intent target: the runtime performance source that accepts events.
21    pub target: Symbol,
22    /// Performance event source id.
23    pub source: Symbol,
24    /// Browser or MIDI input id.
25    pub input: Symbol,
26    /// Player chain that receives source output.
27    pub player_chain: Symbol,
28    /// Instrument target at the end of the chain.
29    pub instrument: Symbol,
30    /// Browser stream bridge route used by the shell.
31    pub stream: Symbol,
32    /// MIDI channel, zero based.
33    pub channel: u8,
34}
35
36impl PerformanceKeyboardBinding {
37    /// Build the standard browser keyboard binding.
38    pub fn browser(player_chain: Symbol, instrument: Symbol) -> Self {
39        Self {
40            target: Symbol::qualified("music/performance-source", "keyboard"),
41            source: Symbol::qualified("music/performance-source", "keyboard"),
42            input: Symbol::qualified("midi/input", "keyboard"),
43            player_chain,
44            instrument,
45            stream: Symbol::qualified("stream/browser", "performance-keyboard"),
46            channel: 0,
47        }
48    }
49}
50
51/// Serializable physical-key mapping for browser performance input.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct PerformanceKeyMap {
54    /// Stable map name.
55    pub name: String,
56    /// Whether the browser editor may rewrite and persist this map.
57    pub editable: bool,
58    /// Physical keys and their performance actions.
59    pub entries: Vec<PerformanceKeyMapEntry>,
60    /// Current key velocity used by note entries.
61    pub velocity: u8,
62    /// Current transpose, in semitones.
63    pub transpose: i8,
64    /// Whether degree entries should be scale-locked by the browser.
65    pub scale_lock: bool,
66}
67
68impl Default for PerformanceKeyMap {
69    fn default() -> Self {
70        Self::qwerty_two_row()
71    }
72}
73
74impl PerformanceKeyMap {
75    /// Default two-row chromatic map with controls for live performance.
76    pub fn qwerty_two_row() -> Self {
77        let mut entries = Vec::new();
78        push_degree_row(&mut entries, &LOWER_ROW, 0);
79        push_degree_row(&mut entries, &UPPER_ROW, 1);
80        entries.extend([
81            PerformanceKeyMapEntry::new("Space", " ", "Sustain", PerformanceKeyAction::Sustain),
82            PerformanceKeyMapEntry::new(
83                "BracketLeft",
84                "[",
85                "Octave down",
86                PerformanceKeyAction::OctaveShift { amount: -1 },
87            ),
88            PerformanceKeyMapEntry::new(
89                "BracketRight",
90                "]",
91                "Octave up",
92                PerformanceKeyAction::OctaveShift { amount: 1 },
93            ),
94            PerformanceKeyMapEntry::new(
95                "Comma",
96                ",",
97                "Transpose down",
98                PerformanceKeyAction::Transpose { amount: -1 },
99            ),
100            PerformanceKeyMapEntry::new(
101                "Period",
102                ".",
103                "Transpose up",
104                PerformanceKeyAction::Transpose { amount: 1 },
105            ),
106            PerformanceKeyMapEntry::new(
107                "Backslash",
108                "\\",
109                "Scale lock",
110                PerformanceKeyAction::ScaleLock,
111            ),
112            PerformanceKeyMapEntry::new("Escape", "Esc", "Panic", PerformanceKeyAction::Panic),
113            PerformanceKeyMapEntry::new(
114                "F1",
115                "F1",
116                "Velocity low",
117                PerformanceKeyAction::Velocity { value: 40 },
118            ),
119            PerformanceKeyMapEntry::new(
120                "F2",
121                "F2",
122                "Velocity medium",
123                PerformanceKeyAction::Velocity { value: 72 },
124            ),
125            PerformanceKeyMapEntry::new(
126                "F3",
127                "F3",
128                "Velocity strong",
129                PerformanceKeyAction::Velocity { value: 100 },
130            ),
131            PerformanceKeyMapEntry::new(
132                "F4",
133                "F4",
134                "Velocity full",
135                PerformanceKeyAction::Velocity { value: 127 },
136            ),
137        ]);
138        Self {
139            name: "qwerty-two-row".to_owned(),
140            editable: true,
141            entries,
142            velocity: 96,
143            transpose: 0,
144            scale_lock: false,
145        }
146    }
147}
148
149/// A single physical key action.
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct PerformanceKeyMapEntry {
152    /// KeyboardEvent.code value.
153    pub code: String,
154    /// KeyboardEvent.key value when available.
155    pub key: String,
156    /// Short display label.
157    pub label: String,
158    /// Performance action triggered by the key.
159    pub action: PerformanceKeyAction,
160}
161
162impl PerformanceKeyMapEntry {
163    /// Build a physical key binding.
164    pub fn new(
165        code: impl Into<String>,
166        key: impl Into<String>,
167        label: impl Into<String>,
168        action: PerformanceKeyAction,
169    ) -> Self {
170        Self {
171            code: code.into(),
172            key: key.into(),
173            label: label.into(),
174            action,
175        }
176    }
177}
178
179/// Browser-side performance action for a physical key.
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub enum PerformanceKeyAction {
182    /// Map to a chromatic degree from the keyboard base.
183    Degree {
184        /// Scale degree relative to the keyboard base.
185        degree: i8,
186        /// Octave offset applied to the degree.
187        octave: i8,
188    },
189    /// Map directly to a MIDI note number.
190    Note {
191        /// MIDI note number to emit.
192        midi: i32,
193    },
194    /// Hold or release sustain.
195    Sustain,
196    /// Shift emitted notes by whole octaves.
197    OctaveShift {
198        /// Number of octaves to shift, signed.
199        amount: i8,
200    },
201    /// Shift emitted notes by semitones.
202    Transpose {
203        /// Number of semitones to shift, signed.
204        amount: i8,
205    },
206    /// Release all held notes.
207    Panic,
208    /// Toggle scale-locking for degree entries.
209    ScaleLock,
210    /// Set note-on velocity for later key presses.
211    Velocity {
212        /// Velocity value to apply to later notes.
213        value: u8,
214    },
215}
216
217/// Display state for the keyboard surface.
218#[derive(Clone, Debug, PartialEq, Eq)]
219pub struct PerformanceKeyboardState {
220    /// First displayed MIDI note before octave shift.
221    pub base_midi: i32,
222    /// Number of octaves to display.
223    pub octaves: u8,
224    /// Octave shift applied to emitted notes.
225    pub octave_shift: i8,
226    /// Pitch classes highlighted as in-scale.
227    pub scale_lock: Vec<u8>,
228    /// Notes currently held by the source.
229    pub held_notes: Vec<i32>,
230    /// Notes generated by downstream players.
231    pub generated_notes: Vec<i32>,
232    /// Sustain toggle state.
233    pub sustain: bool,
234    /// Current pitch-bend value in MIDI 14-bit range.
235    pub pitch_bend: u16,
236    /// Physical-key mapping for computer-keyboard performance.
237    pub key_map: PerformanceKeyMap,
238}
239
240impl Default for PerformanceKeyboardState {
241    fn default() -> Self {
242        Self {
243            base_midi: 48,
244            octaves: 3,
245            octave_shift: 0,
246            scale_lock: vec![0, 2, 4, 5, 7, 9, 11],
247            held_notes: Vec::new(),
248            generated_notes: Vec::new(),
249            sustain: false,
250            pitch_bend: 8192,
251            key_map: PerformanceKeyMap::default(),
252        }
253    }
254}
255
256/// Render the keyboard as a single `scene/keyboard` node.
257pub fn performance_keyboard_view(
258    binding: &PerformanceKeyboardBinding,
259    state: &PerformanceKeyboardState,
260) -> Expr {
261    let shifted_base = state.base_midi + i32::from(state.octave_shift) * 12;
262    node(
263        "keyboard",
264        vec![
265            ("lens", sym(PERFORMANCE_KEYBOARD_VIEW_ID)),
266            ("role", sym("performance-keyboard")),
267            ("label", text("On-screen keyboard")),
268            ("target", Expr::Symbol(binding.target.clone())),
269            ("source", Expr::Symbol(binding.source.clone())),
270            ("input", Expr::Symbol(binding.input.clone())),
271            ("channel", uint(u64::from(binding.channel))),
272            ("base-midi", int(i64::from(shifted_base))),
273            ("octaves", uint(u64::from(state.octaves))),
274            ("octave-shift", int(i64::from(state.octave_shift))),
275            ("sustain", Expr::Bool(state.sustain)),
276            ("pitch-bend", uint(u64::from(state.pitch_bend))),
277            ("key-map", key_map_expr(&state.key_map)),
278            (
279                "scale-lock",
280                list(
281                    state
282                        .scale_lock
283                        .iter()
284                        .map(|note| uint(u64::from(*note)))
285                        .collect(),
286                ),
287            ),
288            (
289                "held-notes",
290                list(
291                    state
292                        .held_notes
293                        .iter()
294                        .map(|note| int(i64::from(*note)))
295                        .collect(),
296                ),
297            ),
298            (
299                "generated-notes",
300                list(
301                    state
302                        .generated_notes
303                        .iter()
304                        .map(|note| int(i64::from(*note)))
305                        .collect(),
306                ),
307            ),
308            ("binding", binding_expr(binding)),
309            ("keys", list(keys(shifted_base, state))),
310        ],
311    )
312}
313
314/// A deterministic shell demo binding the keyboard through a player chain to a
315/// SUP instrument descriptor.
316pub fn performance_keyboard_demo_scene() -> Expr {
317    let binding = PerformanceKeyboardBinding::browser(
318        Symbol::qualified("music/player-chain", "onscreen-keyboard"),
319        Symbol::qualified("audio-synth/instrument", "dx7"),
320    );
321    let state = PerformanceKeyboardState {
322        held_notes: vec![60, 64],
323        generated_notes: vec![67, 72],
324        sustain: true,
325        ..PerformanceKeyboardState::default()
326    };
327    performance_keyboard_view(&binding, &state)
328}
329
330fn binding_expr(binding: &PerformanceKeyboardBinding) -> Expr {
331    data_map(vec![
332        ("target", Expr::Symbol(binding.target.clone())),
333        ("source", Expr::Symbol(binding.source.clone())),
334        ("input", Expr::Symbol(binding.input.clone())),
335        ("player-chain", Expr::Symbol(binding.player_chain.clone())),
336        ("instrument", Expr::Symbol(binding.instrument.clone())),
337        ("stream", Expr::Symbol(binding.stream.clone())),
338        ("channel", uint(u64::from(binding.channel))),
339    ])
340}
341
342fn key_map_expr(key_map: &PerformanceKeyMap) -> Expr {
343    data_map(vec![
344        ("name", text(key_map.name.clone())),
345        ("editable", Expr::Bool(key_map.editable)),
346        ("velocity", uint(u64::from(key_map.velocity))),
347        ("transpose", int(i64::from(key_map.transpose))),
348        ("scale-lock", Expr::Bool(key_map.scale_lock)),
349        (
350            "entries",
351            list(key_map.entries.iter().map(key_map_entry_expr).collect()),
352        ),
353    ])
354}
355
356fn key_map_entry_expr(entry: &PerformanceKeyMapEntry) -> Expr {
357    let mut fields = vec![
358        ("code", text(entry.code.clone())),
359        ("key", text(entry.key.clone())),
360        ("label", text(entry.label.clone())),
361    ];
362    match entry.action {
363        PerformanceKeyAction::Degree { degree, octave } => {
364            fields.extend([
365                ("action", text("degree")),
366                ("degree", int(i64::from(degree))),
367                ("octave", int(i64::from(octave))),
368            ]);
369        }
370        PerformanceKeyAction::Note { midi } => {
371            fields.extend([("action", text("note")), ("midi", int(i64::from(midi)))]);
372        }
373        PerformanceKeyAction::Sustain => fields.push(("action", text("sustain"))),
374        PerformanceKeyAction::OctaveShift { amount } => {
375            fields.extend([
376                ("action", text("octave-shift")),
377                ("amount", int(i64::from(amount))),
378            ]);
379        }
380        PerformanceKeyAction::Transpose { amount } => {
381            fields.extend([
382                ("action", text("transpose")),
383                ("amount", int(i64::from(amount))),
384            ]);
385        }
386        PerformanceKeyAction::Panic => fields.push(("action", text("panic"))),
387        PerformanceKeyAction::ScaleLock => fields.push(("action", text("scale-lock"))),
388        PerformanceKeyAction::Velocity { value } => {
389            fields.extend([
390                ("action", text("velocity")),
391                ("value", uint(u64::from(value))),
392            ]);
393        }
394    }
395    data_map(fields)
396}
397
398fn keys(base_midi: i32, state: &PerformanceKeyboardState) -> Vec<Expr> {
399    let count = usize::from(state.octaves) * 12;
400    (0..count)
401        .map(|offset| {
402            let midi = base_midi + offset as i32;
403            let class = midi.rem_euclid(12) as u8;
404            data_map(vec![
405                ("midi", int(i64::from(midi))),
406                ("label", text(note_label(midi))),
407                ("white", Expr::Bool(is_white_key(class))),
408                ("scale", Expr::Bool(state.scale_lock.contains(&class))),
409                ("held", Expr::Bool(state.held_notes.contains(&midi))),
410                (
411                    "generated",
412                    Expr::Bool(state.generated_notes.contains(&midi)),
413                ),
414            ])
415        })
416        .collect()
417}
418
419const LOWER_ROW: [(&str, &str, &str, i8); 12] = [
420    ("KeyZ", "z", "Z", 0),
421    ("KeyS", "s", "S", 1),
422    ("KeyX", "x", "X", 2),
423    ("KeyD", "d", "D", 3),
424    ("KeyC", "c", "C", 4),
425    ("KeyV", "v", "V", 5),
426    ("KeyG", "g", "G", 6),
427    ("KeyB", "b", "B", 7),
428    ("KeyH", "h", "H", 8),
429    ("KeyN", "n", "N", 9),
430    ("KeyJ", "j", "J", 10),
431    ("KeyM", "m", "M", 11),
432];
433
434const UPPER_ROW: [(&str, &str, &str, i8); 12] = [
435    ("KeyQ", "q", "Q", 0),
436    ("Digit2", "2", "2", 1),
437    ("KeyW", "w", "W", 2),
438    ("Digit3", "3", "3", 3),
439    ("KeyE", "e", "E", 4),
440    ("KeyR", "r", "R", 5),
441    ("Digit5", "5", "5", 6),
442    ("KeyT", "t", "T", 7),
443    ("Digit6", "6", "6", 8),
444    ("KeyY", "y", "Y", 9),
445    ("Digit7", "7", "7", 10),
446    ("KeyU", "u", "U", 11),
447];
448
449fn push_degree_row(
450    entries: &mut Vec<PerformanceKeyMapEntry>,
451    row: &[(&str, &str, &str, i8)],
452    octave: i8,
453) {
454    entries.extend(row.iter().map(|(code, key, label, degree)| {
455        PerformanceKeyMapEntry::new(
456            *code,
457            *key,
458            *label,
459            PerformanceKeyAction::Degree {
460                degree: *degree,
461                octave,
462            },
463        )
464    }));
465}
466
467fn is_white_key(class: u8) -> bool {
468    matches!(class, 0 | 2 | 4 | 5 | 7 | 9 | 11)
469}
470
471fn note_label(midi: i32) -> String {
472    const NAMES: [&str; 12] = [
473        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
474    ];
475    let class = midi.rem_euclid(12) as usize;
476    let octave = midi.div_euclid(12) - 1;
477    format!("{}{}", NAMES[class], octave)
478}