Skip to main content

sim_lib_music_core/
performance_source.rs

1use std::collections::BTreeMap;
2
3use sim_kernel::{Error, Result, Symbol};
4
5use crate::{
6    Channel, LaneId, Music, PerformanceEvent, PerformanceInput, PerformanceIntent, PerformanceTake,
7    Pitch, Tick,
8};
9
10/// Binds a named performance input to a lane and channel.
11///
12/// Connects an input device identifier to the [`LaneId`] its events land on and the
13/// [`Channel`] they default to.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct PerformanceInputBinding {
16    /// Symbol identifying the bound input.
17    pub input_id: Symbol,
18    /// Lane the input's events are routed to.
19    pub lane_id: LaneId,
20    /// Channel assigned to the input.
21    pub channel: Channel,
22}
23
24impl PerformanceInputBinding {
25    /// Creates a binding from an input id, lane, and channel.
26    pub fn new(input_id: Symbol, lane_id: LaneId, channel: Channel) -> Self {
27        Self {
28            input_id,
29            lane_id,
30            channel,
31        }
32    }
33}
34
35/// A set of allowed pitch classes that incoming notes are snapped to.
36///
37/// Holds the permitted pitch classes (0..12); [`apply`](Self::apply) maps any pitch
38/// to the nearest allowed class.
39///
40/// # Examples
41///
42/// ```
43/// use sim_lib_music_core::{Pitch, ScaleLock};
44///
45/// let lock = ScaleLock::major();
46/// // C-sharp (class 1) is not in C major and snaps down to C.
47/// assert_eq!(lock.apply(Pitch::from_semitone(1)).semitone(), 0);
48/// // A pitch already in the scale is left unchanged.
49/// assert_eq!(lock.apply(Pitch::from_semitone(2)).semitone(), 2);
50/// ```
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct ScaleLock {
53    /// Sorted, deduplicated allowed pitch classes (each in 0..12).
54    pub allowed_classes: Vec<u8>,
55}
56
57impl ScaleLock {
58    /// Builds a scale lock from a list of pitch classes.
59    ///
60    /// The list is sorted and deduplicated; returns an error if it is empty or any
61    /// class is outside 0..12.
62    pub fn new(mut allowed_classes: Vec<u8>) -> Result<Self> {
63        if allowed_classes.is_empty() || allowed_classes.iter().any(|class| *class >= 12) {
64            return Err(Error::Eval(
65                "scale lock pitch classes must be in 0..12".to_owned(),
66            ));
67        }
68        allowed_classes.sort_unstable();
69        allowed_classes.dedup();
70        Ok(Self { allowed_classes })
71    }
72
73    /// Returns a scale lock for the diatonic major scale.
74    pub fn major() -> Self {
75        Self::new(vec![0, 2, 4, 5, 7, 9, 11]).expect("major scale lock is valid")
76    }
77
78    /// Snaps `pitch` to the nearest allowed pitch class.
79    ///
80    /// Pitches already in the lock pass through unchanged; otherwise the smallest
81    /// semitone shift to an allowed class is applied, preferring downward ties.
82    pub fn apply(&self, pitch: Pitch) -> Pitch {
83        let semitone = pitch.semitone();
84        let class = semitone.rem_euclid(12) as u8;
85        if self.allowed_classes.binary_search(&class).is_ok() {
86            return pitch;
87        }
88        let delta = (-6..=6)
89            .filter(|delta| {
90                let candidate = (class as i32 + delta).rem_euclid(12) as u8;
91                self.allowed_classes.binary_search(&candidate).is_ok()
92            })
93            .min_by_key(|delta| (delta.abs(), (*delta > 0) as u8))
94            .expect("scale lock has at least one class");
95        Pitch::from_semitone(semitone + delta)
96    }
97}
98
99/// Identity key for a held note, combining channel and pitch.
100///
101/// Used to track note-on/note-off pairing in source and clip state.
102#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
103pub struct PerformanceNoteKey {
104    /// Raw channel number.
105    pub channel: u8,
106    /// Pitch in semitones.
107    pub semitone: i32,
108}
109
110impl PerformanceNoteKey {
111    /// Builds a key from a channel and pitch.
112    pub fn new(channel: Channel, pitch: Pitch) -> Self {
113        Self {
114            channel: channel.0,
115            semitone: pitch.semitone(),
116        }
117    }
118}
119
120/// A note currently held down at a source.
121///
122/// Records the active note's pitch, velocity, channel, start tick, and whether it
123/// was released while the sustain pedal was down.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct HeldPerformanceNote {
126    /// Pitch of the held note.
127    pub pitch: Pitch,
128    /// Attack velocity.
129    pub velocity: u8,
130    /// Channel the note plays on.
131    pub channel: Channel,
132    /// Tick at which the note started.
133    pub started_at: Tick,
134    /// Whether note-off arrived while sustain was held.
135    pub released_while_sustained: bool,
136}
137
138/// Mutable performance state tracked by a source.
139///
140/// Holds the currently sounding notes plus the transforms applied to incoming
141/// gestures: sustain pedal, octave shift, transpose, and optional scale lock.
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct PerformanceSourceState {
144    /// Notes currently held, keyed by channel and pitch.
145    pub held_notes: BTreeMap<PerformanceNoteKey, HeldPerformanceNote>,
146    /// Whether the sustain pedal is down.
147    pub sustain_pedal: bool,
148    /// Octave shift applied to incoming pitches.
149    pub octave_shift: i8,
150    /// Semitone transpose applied to incoming pitches.
151    pub transpose: i8,
152    /// Optional scale lock snapping incoming pitches.
153    pub scale_lock: Option<ScaleLock>,
154    /// Default channel for the source.
155    pub channel: Channel,
156}
157
158impl PerformanceSourceState {
159    /// Creates empty state defaulting to `channel`.
160    pub fn new(channel: Channel) -> Self {
161        Self {
162            held_notes: BTreeMap::new(),
163            sustain_pedal: false,
164            octave_shift: 0,
165            transpose: 0,
166            scale_lock: None,
167            channel,
168        }
169    }
170
171    /// Returns the number of notes currently held.
172    pub fn held_note_count(&self) -> usize {
173        self.held_notes.len()
174    }
175
176    fn transform_pitch(&self, pitch: Pitch) -> Pitch {
177        let transposed =
178            pitch.transpose(i32::from(self.transpose) + i32::from(self.octave_shift) * 12);
179        self.scale_lock
180            .as_ref()
181            .map(|lock| lock.apply(transposed))
182            .unwrap_or(transposed)
183    }
184
185    fn observe_event(&mut self, event: &PerformanceEvent) {
186        match &event.intent {
187            PerformanceIntent::NoteOn {
188                pitch,
189                velocity,
190                channel,
191            } => {
192                self.held_notes.insert(
193                    PerformanceNoteKey::new(*channel, *pitch),
194                    HeldPerformanceNote {
195                        pitch: *pitch,
196                        velocity: *velocity,
197                        channel: *channel,
198                        started_at: event.time,
199                        released_while_sustained: false,
200                    },
201                );
202            }
203            PerformanceIntent::NoteOff { pitch, channel, .. } => {
204                let key = PerformanceNoteKey::new(*channel, *pitch);
205                if self.sustain_pedal {
206                    if let Some(note) = self.held_notes.get_mut(&key) {
207                        note.released_while_sustained = true;
208                    }
209                } else {
210                    self.held_notes.remove(&key);
211                }
212            }
213            PerformanceIntent::Sustain { down, .. } => {
214                self.sustain_pedal = *down;
215                if !down {
216                    self.held_notes
217                        .retain(|_, note| !note.released_while_sustained);
218                }
219            }
220            PerformanceIntent::Panic => {
221                self.held_notes.clear();
222                self.sustain_pedal = false;
223            }
224            PerformanceIntent::Aftertouch { .. }
225            | PerformanceIntent::PitchBend { .. }
226            | PerformanceIntent::Parameter { .. } => {}
227        }
228    }
229}
230
231/// A live performance source that turns inputs into events and captures takes.
232///
233/// Implementors bind an input, poll queued [`PerformanceInput`]s into transformed
234/// [`PerformanceEvent`](crate::PerformanceEvent)s, emit panics, record capture takes,
235/// and render a captured [`PerformanceTake`] into [`Music`](crate::Music).
236pub trait PerformanceSource {
237    /// Binds an input to this source, setting its lane and channel.
238    fn bind_input(&mut self, binding: PerformanceInputBinding) -> Result<()>;
239    /// Transforms queued inputs into events, updating held-note state.
240    fn poll_events(&mut self, inputs: Vec<PerformanceInput>) -> Result<Vec<PerformanceEvent>>;
241    /// Emits note-offs for all held notes followed by a panic event.
242    fn panic(&mut self, input_time: Tick) -> Result<Vec<PerformanceEvent>>;
243    /// Begins capturing emitted events under `take_id`.
244    fn capture_start(&mut self, take_id: Symbol) -> Result<()>;
245    /// Ends capture and returns the recorded [`PerformanceTake`].
246    fn capture_stop(&mut self) -> Result<PerformanceTake>;
247    /// Renders a captured take into a [`Music`](crate::Music) clip.
248    fn as_clip(&self, take: &PerformanceTake) -> Result<Music>;
249}
250
251/// An in-memory [`PerformanceSource`] holding state and an optional capture.
252#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct MemoryPerformanceSource {
254    source_id: Symbol,
255    binding: Option<PerformanceInputBinding>,
256    state: PerformanceSourceState,
257    capture: Option<PerformanceCapture>,
258}
259
260impl MemoryPerformanceSource {
261    /// Creates an unbound source with the given id and default channel.
262    pub fn new(source_id: Symbol, channel: Channel) -> Self {
263        Self {
264            source_id,
265            binding: None,
266            state: PerformanceSourceState::new(channel),
267            capture: None,
268        }
269    }
270
271    /// Returns the symbol identifying this source.
272    pub fn source_id(&self) -> &Symbol {
273        &self.source_id
274    }
275
276    /// Returns a shared reference to the source's state.
277    pub fn state(&self) -> &PerformanceSourceState {
278        &self.state
279    }
280
281    /// Returns a mutable reference to the source's state.
282    pub fn state_mut(&mut self) -> &mut PerformanceSourceState {
283        &mut self.state
284    }
285
286    /// Sets the octave shift applied to incoming pitches.
287    pub fn set_octave_shift(&mut self, octave_shift: i8) {
288        self.state.octave_shift = octave_shift;
289    }
290
291    /// Sets the semitone transpose applied to incoming pitches.
292    pub fn set_transpose(&mut self, transpose: i8) {
293        self.state.transpose = transpose;
294    }
295
296    /// Sets or clears the scale lock applied to incoming pitches.
297    pub fn set_scale_lock(&mut self, scale_lock: Option<ScaleLock>) {
298        self.state.scale_lock = scale_lock;
299    }
300
301    fn binding(&self) -> Result<&PerformanceInputBinding> {
302        self.binding
303            .as_ref()
304            .ok_or_else(|| Error::Eval("performance source input is not bound".to_owned()))
305    }
306
307    fn push_capture(&mut self, events: &[PerformanceEvent]) {
308        if let Some(capture) = &mut self.capture {
309            capture.events.extend_from_slice(events);
310        }
311    }
312}
313
314impl PerformanceSource for MemoryPerformanceSource {
315    fn bind_input(&mut self, binding: PerformanceInputBinding) -> Result<()> {
316        self.state.channel = binding.channel;
317        self.binding = Some(binding);
318        Ok(())
319    }
320
321    fn poll_events(&mut self, inputs: Vec<PerformanceInput>) -> Result<Vec<PerformanceEvent>> {
322        let binding = self.binding()?.clone();
323        let mut events = Vec::new();
324        for input in inputs {
325            let event = PerformanceEvent {
326                lane_id: binding.lane_id.clone(),
327                source_id: self.source_id.clone(),
328                input_time: input.input_time,
329                time: input.input_time,
330                intent: transform_intent(input.intent, &self.state),
331            };
332            self.state.observe_event(&event);
333            events.push(event);
334        }
335        self.push_capture(&events);
336        Ok(events)
337    }
338
339    fn panic(&mut self, input_time: Tick) -> Result<Vec<PerformanceEvent>> {
340        let binding = self.binding()?.clone();
341        let mut events = self
342            .state
343            .held_notes
344            .values()
345            .map(|note| PerformanceEvent {
346                lane_id: binding.lane_id.clone(),
347                source_id: self.source_id.clone(),
348                input_time,
349                time: input_time,
350                intent: PerformanceIntent::NoteOff {
351                    pitch: note.pitch,
352                    velocity: 0,
353                    channel: note.channel,
354                },
355            })
356            .collect::<Vec<_>>();
357        events.push(PerformanceEvent {
358            lane_id: binding.lane_id,
359            source_id: self.source_id.clone(),
360            input_time,
361            time: input_time,
362            intent: PerformanceIntent::Panic,
363        });
364        for event in &events {
365            self.state.observe_event(event);
366        }
367        self.push_capture(&events);
368        Ok(events)
369    }
370
371    fn capture_start(&mut self, take_id: Symbol) -> Result<()> {
372        if self.capture.is_some() {
373            return Err(Error::Eval(
374                "performance capture is already active".to_owned(),
375            ));
376        }
377        self.capture = Some(PerformanceCapture {
378            take_id,
379            events: Vec::new(),
380        });
381        Ok(())
382    }
383
384    fn capture_stop(&mut self) -> Result<PerformanceTake> {
385        let capture = self
386            .capture
387            .take()
388            .ok_or_else(|| Error::Eval("performance capture is not active".to_owned()))?;
389        PerformanceTake::new(self.source_id.clone(), capture.take_id, capture.events)
390    }
391
392    fn as_clip(&self, take: &PerformanceTake) -> Result<Music> {
393        take.as_clip()
394    }
395}
396
397#[derive(Clone, Debug, PartialEq, Eq)]
398struct PerformanceCapture {
399    take_id: Symbol,
400    events: Vec<PerformanceEvent>,
401}
402
403fn transform_intent(
404    intent: PerformanceIntent,
405    state: &PerformanceSourceState,
406) -> PerformanceIntent {
407    match intent {
408        PerformanceIntent::NoteOn {
409            pitch,
410            velocity,
411            channel,
412        } => PerformanceIntent::NoteOn {
413            pitch: state.transform_pitch(pitch),
414            velocity,
415            channel,
416        },
417        PerformanceIntent::NoteOff {
418            pitch,
419            velocity,
420            channel,
421        } => PerformanceIntent::NoteOff {
422            pitch: state.transform_pitch(pitch),
423            velocity,
424            channel,
425        },
426        PerformanceIntent::Aftertouch {
427            pitch,
428            pressure,
429            channel,
430        } => PerformanceIntent::Aftertouch {
431            pitch: state.transform_pitch(pitch),
432            pressure,
433            channel,
434        },
435        other => other,
436    }
437}