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
54/// A single live-performance gesture.
55///
56/// Enumerates the MIDI-style intents a [`PerformanceSource`](crate::PerformanceSource)
57/// can emit: note triggers, expression controls, the sustain pedal, parameter changes,
58/// and an all-notes-off panic.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum PerformanceIntent {
61    /// Starts a note at the given pitch and velocity on a channel.
62    NoteOn {
63        /// Pitch of the note.
64        pitch: Pitch,
65        /// Attack velocity (0..=127).
66        velocity: u8,
67        /// Channel the note plays on.
68        channel: Channel,
69    },
70    /// Releases a previously started note.
71    NoteOff {
72        /// Pitch of the note being released.
73        pitch: Pitch,
74        /// Release velocity (0..=127).
75        velocity: u8,
76        /// Channel the note plays on.
77        channel: Channel,
78    },
79    /// Applies polyphonic key pressure to a sounding note.
80    Aftertouch {
81        /// Pitch the pressure applies to.
82        pitch: Pitch,
83        /// Pressure amount (0..=127).
84        pressure: u8,
85        /// Channel the note plays on.
86        channel: Channel,
87    },
88    /// Bends pitch on a channel by a 14-bit amount.
89    PitchBend {
90        /// Raw 14-bit bend value (0..=16383).
91        value: u16,
92        /// Channel the bend applies to.
93        channel: Channel,
94    },
95    /// Sets the sustain pedal state on a channel.
96    Sustain {
97        /// Whether the pedal is depressed.
98        down: bool,
99        /// Channel the pedal applies to.
100        channel: Channel,
101    },
102    /// Sets a named parameter to an integer value.
103    Parameter {
104        /// Symbol naming the parameter target.
105        target: Symbol,
106        /// New parameter value.
107        value: i64,
108    },
109    /// Requests an all-notes-off panic and pedal reset.
110    Panic,
111}
112
113impl PerformanceIntent {
114    /// Returns the qualified symbol identifying this intent's kind.
115    pub fn kind_symbol(&self) -> Symbol {
116        Symbol::qualified("music/performance-intent", self.kind_label())
117    }
118
119    /// Returns the short kebab-case label for this intent's kind.
120    pub fn kind_label(&self) -> &'static str {
121        match self {
122            Self::NoteOn { .. } => "note-on",
123            Self::NoteOff { .. } => "note-off",
124            Self::Aftertouch { .. } => "aftertouch",
125            Self::PitchBend { .. } => "pitch-bend",
126            Self::Sustain { .. } => "sustain",
127            Self::Parameter { .. } => "parameter",
128            Self::Panic => "panic",
129        }
130    }
131
132    /// Encodes this intent as an [`Expr`] map keyed by `kind` and its fields.
133    pub fn to_expr(&self) -> Expr {
134        let mut entries = vec![(
135            Expr::Symbol(Symbol::new("kind")),
136            Expr::Symbol(self.kind_symbol()),
137        )];
138        match self {
139            Self::NoteOn {
140                pitch,
141                velocity,
142                channel,
143            }
144            | Self::NoteOff {
145                pitch,
146                velocity,
147                channel,
148            } => {
149                entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
150                entries.push((
151                    Expr::Symbol(Symbol::new("velocity")),
152                    Expr::String(velocity.to_string()),
153                ));
154                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
155            }
156            Self::Aftertouch {
157                pitch,
158                pressure,
159                channel,
160            } => {
161                entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
162                entries.push((
163                    Expr::Symbol(Symbol::new("pressure")),
164                    Expr::String(pressure.to_string()),
165                ));
166                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
167            }
168            Self::PitchBend { value, channel } => {
169                entries.push((
170                    Expr::Symbol(Symbol::new("value")),
171                    Expr::String(value.to_string()),
172                ));
173                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
174            }
175            Self::Sustain { down, channel } => {
176                entries.push((Expr::Symbol(Symbol::new("down")), Expr::Bool(*down)));
177                entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
178            }
179            Self::Parameter { target, value } => {
180                entries.push((
181                    Expr::Symbol(Symbol::new("target")),
182                    Expr::Symbol(target.clone()),
183                ));
184                entries.push((
185                    Expr::Symbol(Symbol::new("value")),
186                    Expr::String(value.to_string()),
187                ));
188            }
189            Self::Panic => {}
190        }
191        Expr::Map(entries)
192    }
193
194    /// Decodes an intent from an [`Expr`] map produced by [`to_expr`](Self::to_expr).
195    ///
196    /// Accepts both bare and `music/performance-intent`-qualified kind labels, and
197    /// returns an error for non-map exprs or unknown kinds.
198    pub fn from_expr(expr: &Expr) -> Result<Self> {
199        let Expr::Map(entries) = expr else {
200            return Err(Error::Eval("performance intent must be a map".to_owned()));
201        };
202        match symbol_field(entries, "kind")?.as_qualified_str().as_str() {
203            "note-on" | "music/performance-intent/note-on" => Ok(Self::NoteOn {
204                pitch: pitch_field(entries, "pitch")?,
205                velocity: u8_field(entries, "velocity")?,
206                channel: channel_field(entries, "channel")?,
207            }),
208            "note-off" | "music/performance-intent/note-off" => Ok(Self::NoteOff {
209                pitch: pitch_field(entries, "pitch")?,
210                velocity: u8_field(entries, "velocity")?,
211                channel: channel_field(entries, "channel")?,
212            }),
213            "aftertouch" | "music/performance-intent/aftertouch" => Ok(Self::Aftertouch {
214                pitch: pitch_field(entries, "pitch")?,
215                pressure: u8_field(entries, "pressure")?,
216                channel: channel_field(entries, "channel")?,
217            }),
218            "pitch-bend" | "music/performance-intent/pitch-bend" => Ok(Self::PitchBend {
219                value: u16_field(entries, "value")?,
220                channel: channel_field(entries, "channel")?,
221            }),
222            "sustain" | "music/performance-intent/sustain" => Ok(Self::Sustain {
223                down: bool_field(entries, "down")?,
224                channel: channel_field(entries, "channel")?,
225            }),
226            "parameter" | "music/performance-intent/parameter" => Ok(Self::Parameter {
227                target: symbol_field(entries, "target")?.clone(),
228                value: i64_field(entries, "value")?,
229            }),
230            "panic" | "music/performance-intent/panic" => Ok(Self::Panic),
231            other => Err(Error::Eval(format!(
232                "unknown performance intent kind {other}"
233            ))),
234        }
235    }
236}
237
238fn pitch_expr(pitch: Pitch) -> Expr {
239    Expr::String(pitch.semitone().to_string())
240}
241
242fn channel_expr(channel: Channel) -> Expr {
243    Expr::String(channel.0.to_string())
244}
245
246fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
247    access::entry_required_str(entries, name, "string field")
248}
249
250fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
251    access::entry_required_sym(entries, name, "symbol field")
252}
253
254fn bool_field(entries: &[(Expr, Expr)], name: &str) -> Result<bool> {
255    access::entry_required_bool(entries, name, "boolean field")
256}
257
258fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
259    string_field(entries, name)?
260        .parse::<i64>()
261        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
262}
263
264fn i32_field(entries: &[(Expr, Expr)], name: &str) -> Result<i32> {
265    string_field(entries, name)?
266        .parse::<i32>()
267        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
268}
269
270fn u8_field(entries: &[(Expr, Expr)], name: &str) -> Result<u8> {
271    string_field(entries, name)?
272        .parse::<u8>()
273        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
274}
275
276fn u16_field(entries: &[(Expr, Expr)], name: &str) -> Result<u16> {
277    string_field(entries, name)?
278        .parse::<u16>()
279        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
280}
281
282fn pitch_field(entries: &[(Expr, Expr)], name: &str) -> Result<Pitch> {
283    Ok(Pitch::from_semitone(i32_field(entries, name)?))
284}
285
286fn channel_field(entries: &[(Expr, Expr)], name: &str) -> Result<Channel> {
287    Channel::new(u8_field(entries, name)?).map_err(|err| Error::Eval(err.to_string()))
288}