Skip to main content

sim_lib_music_core/
performance_intent.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2use sim_value::access;
3
4use crate::{Channel, Pitch, Tick};
5
6/// A raw performance gesture submitted to a source at a given input time.
7///
8/// Pairs a [`PerformanceIntent`] with the [`Tick`] at which it was played, before
9/// any source-side transformation (transpose, scale lock) is applied.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct PerformanceInput {
12    /// Tick at which the gesture was played.
13    pub input_time: Tick,
14    /// The gesture to perform.
15    pub intent: PerformanceIntent,
16}
17
18impl PerformanceInput {
19    /// Creates an input pairing `input_time` with `intent`.
20    pub fn new(input_time: Tick, intent: PerformanceIntent) -> Self {
21        Self { input_time, intent }
22    }
23
24    /// Builds a note-on input from a raw MIDI note number and velocity.
25    pub fn note_on(input_time: Tick, channel: Channel, midi: u8, velocity: u8) -> Self {
26        Self::new(
27            input_time,
28            PerformanceIntent::NoteOn {
29                pitch: Pitch::from_midi(midi),
30                velocity,
31                channel,
32            },
33        )
34    }
35
36    /// Builds a note-off input from a raw MIDI note number and velocity.
37    pub fn note_off(input_time: Tick, channel: Channel, midi: u8, velocity: u8) -> Self {
38        Self::new(
39            input_time,
40            PerformanceIntent::NoteOff {
41                pitch: Pitch::from_midi(midi),
42                velocity,
43                channel,
44            },
45        )
46    }
47
48    /// Builds a sustain-pedal input with the pedal `down` state.
49    pub fn sustain(input_time: Tick, channel: Channel, down: bool) -> Self {
50        Self::new(input_time, PerformanceIntent::Sustain { down, channel })
51    }
52
53    /// Builds a sostenuto-pedal input with the pedal `down` state.
54    pub fn sostenuto(input_time: Tick, channel: Channel, down: bool) -> Self {
55        Self::new(input_time, PerformanceIntent::Sostenuto { down, channel })
56    }
57
58    /// Builds a channel-scoped All Notes Off input.
59    pub fn all_notes_off(input_time: Tick, channel: Channel) -> Self {
60        Self::new(input_time, PerformanceIntent::AllNotesOff { channel })
61    }
62
63    /// Builds a channel-scoped All Sound Off input.
64    pub fn all_sound_off(input_time: Tick, channel: Channel) -> Self {
65        Self::new(input_time, PerformanceIntent::AllSoundOff { channel })
66    }
67
68    /// Builds a channel-scoped Reset All Controllers input.
69    pub fn reset_controllers(input_time: Tick, channel: Channel) -> Self {
70        Self::new(input_time, PerformanceIntent::ResetControllers { channel })
71    }
72}
73
74/// A single live-performance gesture.
75///
76/// Enumerates the MIDI-style intents a [`PerformanceSource`](crate::PerformanceSource)
77/// can emit: note triggers, expression controls, the sustain pedal, parameter changes,
78/// and an all-notes-off panic.
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum PerformanceIntent {
81    /// Starts a note at the given pitch and velocity on a channel.
82    NoteOn {
83        /// Pitch of the note.
84        pitch: Pitch,
85        /// Attack velocity (0..=127).
86        velocity: u8,
87        /// Channel the note plays on.
88        channel: Channel,
89    },
90    /// Releases a previously started note.
91    NoteOff {
92        /// Pitch of the note being released.
93        pitch: Pitch,
94        /// Release velocity (0..=127).
95        velocity: u8,
96        /// Channel the note plays on.
97        channel: Channel,
98    },
99    /// Applies polyphonic key pressure to a sounding note.
100    Aftertouch {
101        /// Pitch the pressure applies to.
102        pitch: Pitch,
103        /// Pressure amount (0..=127).
104        pressure: u8,
105        /// Channel the note plays on.
106        channel: Channel,
107    },
108    /// Bends pitch on a channel by a 14-bit amount.
109    PitchBend {
110        /// Raw 14-bit bend value (0..=16383).
111        value: u16,
112        /// Channel the bend applies to.
113        channel: Channel,
114    },
115    /// Sets the sustain pedal state on a channel.
116    Sustain {
117        /// Whether the pedal is depressed.
118        down: bool,
119        /// Channel the pedal applies to.
120        channel: Channel,
121    },
122    /// Sets the sostenuto pedal state on a channel.
123    Sostenuto {
124        /// Whether the pedal is depressed.
125        down: bool,
126        /// Channel the pedal applies to.
127        channel: Channel,
128    },
129    /// Releases every key-down note on one channel, subject to hold pedals.
130    AllNotesOff {
131        /// Channel whose notes are released.
132        channel: Channel,
133    },
134    /// Silences every sounding note on one channel immediately.
135    AllSoundOff {
136        /// Channel whose sound is stopped.
137        channel: Channel,
138    },
139    /// Resets channel controllers and releases notes held only by pedals.
140    ResetControllers {
141        /// Channel whose controller state is reset.
142        channel: Channel,
143    },
144    /// Sets a named parameter to an integer value.
145    Parameter {
146        /// Symbol naming the parameter target.
147        target: Symbol,
148        /// New parameter value.
149        value: i64,
150    },
151    /// Requests an all-notes-off panic and pedal reset.
152    Panic,
153}
154
155impl PerformanceIntent {
156    /// Returns the qualified symbol identifying this intent's kind.
157    pub fn kind_symbol(&self) -> Symbol {
158        Symbol::qualified("music/performance-intent", self.kind_label())
159    }
160
161    /// Returns the short kebab-case label for this intent's kind.
162    pub fn kind_label(&self) -> &'static str {
163        match self {
164            Self::NoteOn { .. } => "note-on",
165            Self::NoteOff { .. } => "note-off",
166            Self::Aftertouch { .. } => "aftertouch",
167            Self::PitchBend { .. } => "pitch-bend",
168            Self::Sustain { .. } => "sustain",
169            Self::Sostenuto { .. } => "sostenuto",
170            Self::AllNotesOff { .. } => "all-notes-off",
171            Self::AllSoundOff { .. } => "all-sound-off",
172            Self::ResetControllers { .. } => "reset-controllers",
173            Self::Parameter { .. } => "parameter",
174            Self::Panic => "panic",
175        }
176    }
177
178    /// Encodes this intent as an [`Expr`] map keyed by `kind` and its fields.
179    pub fn to_expr(&self) -> Expr {
180        let mut entries = vec![(
181            Expr::Symbol(Symbol::new("kind")),
182            Expr::Symbol(self.kind_symbol()),
183        )];
184        match self {
185            Self::NoteOn {
186                pitch,
187                velocity,
188                channel,
189            }
190            | Self::NoteOff {
191                pitch,
192                velocity,
193                channel,
194            } => {
195                entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
196                entries.push((
197                    Expr::Symbol(Symbol::new("velocity")),
198                    Expr::String(velocity.to_string()),
199                ));
200                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
201            }
202            Self::Aftertouch {
203                pitch,
204                pressure,
205                channel,
206            } => {
207                entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
208                entries.push((
209                    Expr::Symbol(Symbol::new("pressure")),
210                    Expr::String(pressure.to_string()),
211                ));
212                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
213            }
214            Self::PitchBend { value, channel } => {
215                entries.push((
216                    Expr::Symbol(Symbol::new("value")),
217                    Expr::String(value.to_string()),
218                ));
219                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
220            }
221            Self::Sustain { down, channel } | Self::Sostenuto { down, channel } => {
222                entries.push((Expr::Symbol(Symbol::new("down")), Expr::Bool(*down)));
223                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
224            }
225            Self::AllNotesOff { channel }
226            | Self::AllSoundOff { channel }
227            | Self::ResetControllers { channel } => {
228                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
229            }
230            Self::Parameter { target, value } => {
231                entries.push((
232                    Expr::Symbol(Symbol::new("target")),
233                    Expr::Symbol(target.clone()),
234                ));
235                entries.push((
236                    Expr::Symbol(Symbol::new("value")),
237                    Expr::String(value.to_string()),
238                ));
239            }
240            Self::Panic => {}
241        }
242        Expr::Map(entries)
243    }
244
245    /// Decodes an intent from an [`Expr`] map produced by [`to_expr`](Self::to_expr).
246    ///
247    /// Accepts both bare and `music/performance-intent`-qualified kind labels, and
248    /// returns an error for non-map exprs or unknown kinds.
249    pub fn from_expr(expr: &Expr) -> Result<Self> {
250        let Expr::Map(entries) = expr else {
251            return Err(Error::Eval("performance intent must be a map".to_owned()));
252        };
253        match symbol_field(entries, "kind")?.as_qualified_str().as_str() {
254            "note-on" | "music/performance-intent/note-on" => Ok(Self::NoteOn {
255                pitch: pitch_field(entries, "pitch")?,
256                velocity: u8_field(entries, "velocity")?,
257                channel: channel_field(entries, "channel")?,
258            }),
259            "note-off" | "music/performance-intent/note-off" => Ok(Self::NoteOff {
260                pitch: pitch_field(entries, "pitch")?,
261                velocity: u8_field(entries, "velocity")?,
262                channel: channel_field(entries, "channel")?,
263            }),
264            "aftertouch" | "music/performance-intent/aftertouch" => Ok(Self::Aftertouch {
265                pitch: pitch_field(entries, "pitch")?,
266                pressure: u8_field(entries, "pressure")?,
267                channel: channel_field(entries, "channel")?,
268            }),
269            "pitch-bend" | "music/performance-intent/pitch-bend" => Ok(Self::PitchBend {
270                value: u16_field(entries, "value")?,
271                channel: channel_field(entries, "channel")?,
272            }),
273            "sustain" | "music/performance-intent/sustain" => Ok(Self::Sustain {
274                down: bool_field(entries, "down")?,
275                channel: channel_field(entries, "channel")?,
276            }),
277            "sostenuto" | "music/performance-intent/sostenuto" => Ok(Self::Sostenuto {
278                down: bool_field(entries, "down")?,
279                channel: channel_field(entries, "channel")?,
280            }),
281            "all-notes-off" | "music/performance-intent/all-notes-off" => Ok(Self::AllNotesOff {
282                channel: channel_field(entries, "channel")?,
283            }),
284            "all-sound-off" | "music/performance-intent/all-sound-off" => Ok(Self::AllSoundOff {
285                channel: channel_field(entries, "channel")?,
286            }),
287            "reset-controllers" | "music/performance-intent/reset-controllers" => {
288                Ok(Self::ResetControllers {
289                    channel: channel_field(entries, "channel")?,
290                })
291            }
292            "parameter" | "music/performance-intent/parameter" => Ok(Self::Parameter {
293                target: symbol_field(entries, "target")?.clone(),
294                value: i64_field(entries, "value")?,
295            }),
296            "panic" | "music/performance-intent/panic" => Ok(Self::Panic),
297            other => Err(Error::Eval(format!(
298                "unknown performance intent kind {other}"
299            ))),
300        }
301    }
302}
303
304fn pitch_expr(pitch: Pitch) -> Expr {
305    Expr::String(pitch.semitone().to_string())
306}
307
308fn channel_expr(channel: Channel) -> Expr {
309    Expr::String(channel.0.to_string())
310}
311
312fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
313    access::entry_required_str(entries, name, "string field")
314}
315
316fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
317    access::entry_required_sym(entries, name, "symbol field")
318}
319
320fn bool_field(entries: &[(Expr, Expr)], name: &str) -> Result<bool> {
321    access::entry_required_bool(entries, name, "boolean field")
322}
323
324fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
325    string_field(entries, name)?
326        .parse::<i64>()
327        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
328}
329
330fn i32_field(entries: &[(Expr, Expr)], name: &str) -> Result<i32> {
331    string_field(entries, name)?
332        .parse::<i32>()
333        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
334}
335
336fn u8_field(entries: &[(Expr, Expr)], name: &str) -> Result<u8> {
337    string_field(entries, name)?
338        .parse::<u8>()
339        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
340}
341
342fn u16_field(entries: &[(Expr, Expr)], name: &str) -> Result<u16> {
343    string_field(entries, name)?
344        .parse::<u16>()
345        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
346}
347
348fn pitch_field(entries: &[(Expr, Expr)], name: &str) -> Result<Pitch> {
349    Ok(Pitch::from_semitone(i32_field(entries, name)?))
350}
351
352fn channel_field(entries: &[(Expr, Expr)], name: &str) -> Result<Channel> {
353    Channel::new(u8_field(entries, name)?).map_err(|err| Error::Eval(err.to_string()))
354}