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    /// Whether the physical/logical key is still down.
137    pub key_down: bool,
138    /// Whether sostenuto captured this sounding note when the pedal descended.
139    pub sostenuto_captured: bool,
140}
141
142/// Mutable performance state tracked by a source.
143///
144/// Holds the currently sounding notes plus the transforms applied to incoming
145/// gestures: sustain pedal, octave shift, transpose, and optional scale lock.
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct PerformanceSourceState {
148    /// Notes currently held, keyed by channel and pitch.
149    pub held_notes: BTreeMap<PerformanceNoteKey, HeldPerformanceNote>,
150    /// Whether the sustain pedal is down.
151    pub sustain_pedal: bool,
152    /// Whether the sostenuto pedal is down.
153    pub sostenuto_pedal: bool,
154    /// Octave shift applied to incoming pitches.
155    pub octave_shift: i8,
156    /// Semitone transpose applied to incoming pitches.
157    pub transpose: i8,
158    /// Optional scale lock snapping incoming pitches.
159    pub scale_lock: Option<ScaleLock>,
160    /// Default channel for the source.
161    pub channel: Channel,
162}
163
164impl PerformanceSourceState {
165    /// Creates empty state defaulting to `channel`.
166    pub fn new(channel: Channel) -> Self {
167        Self {
168            held_notes: BTreeMap::new(),
169            sustain_pedal: false,
170            sostenuto_pedal: false,
171            octave_shift: 0,
172            transpose: 0,
173            scale_lock: None,
174            channel,
175        }
176    }
177
178    /// Returns the number of notes currently held.
179    pub fn held_note_count(&self) -> usize {
180        self.held_notes.len()
181    }
182
183    fn transform_pitch(&self, pitch: Pitch) -> Pitch {
184        let transposed =
185            pitch.transpose(i32::from(self.transpose) + i32::from(self.octave_shift) * 12);
186        self.scale_lock
187            .as_ref()
188            .map(|lock| lock.apply(transposed))
189            .unwrap_or(transposed)
190    }
191
192    fn observe_event(&mut self, event: &PerformanceEvent) {
193        match &event.intent {
194            PerformanceIntent::NoteOn {
195                pitch,
196                velocity,
197                channel,
198            } => {
199                self.held_notes.insert(
200                    PerformanceNoteKey::new(*channel, *pitch),
201                    HeldPerformanceNote {
202                        pitch: *pitch,
203                        velocity: *velocity,
204                        channel: *channel,
205                        started_at: event.time,
206                        released_while_sustained: false,
207                        key_down: true,
208                        sostenuto_captured: false,
209                    },
210                );
211            }
212            PerformanceIntent::NoteOff { pitch, channel, .. } => {
213                let key = PerformanceNoteKey::new(*channel, *pitch);
214                let held = if let Some(note) = self.held_notes.get_mut(&key) {
215                    note.key_down = false;
216                    note.released_while_sustained = self.sustain_pedal;
217                    self.sustain_pedal || (self.sostenuto_pedal && note.sostenuto_captured)
218                } else {
219                    false
220                };
221                if !held {
222                    self.held_notes.remove(&key);
223                }
224            }
225            PerformanceIntent::Sustain { down, .. } => {
226                self.sustain_pedal = *down;
227                if !down {
228                    self.held_notes.retain(|_, note| {
229                        note.key_down || (self.sostenuto_pedal && note.sostenuto_captured)
230                    });
231                }
232            }
233            PerformanceIntent::Sostenuto { down, .. } => {
234                if *down && !self.sostenuto_pedal {
235                    for note in self.held_notes.values_mut() {
236                        note.sostenuto_captured = true;
237                    }
238                }
239                self.sostenuto_pedal = *down;
240                if !down {
241                    self.held_notes
242                        .retain(|_, note| note.key_down || self.sustain_pedal);
243                    for note in self.held_notes.values_mut() {
244                        note.sostenuto_captured = false;
245                    }
246                }
247            }
248            PerformanceIntent::AllNotesOff { channel } => {
249                for note in self
250                    .held_notes
251                    .values_mut()
252                    .filter(|note| note.channel == *channel)
253                {
254                    note.key_down = false;
255                    note.released_while_sustained = self.sustain_pedal;
256                }
257                self.held_notes.retain(|_, note| {
258                    note.channel != *channel
259                        || self.sustain_pedal
260                        || (self.sostenuto_pedal && note.sostenuto_captured)
261                });
262            }
263            PerformanceIntent::AllSoundOff { channel } => {
264                self.held_notes.retain(|_, note| note.channel != *channel);
265            }
266            PerformanceIntent::ResetControllers { channel } => {
267                self.sustain_pedal = false;
268                self.sostenuto_pedal = false;
269                self.held_notes
270                    .retain(|_, note| note.channel != *channel || note.key_down);
271                for note in self.held_notes.values_mut() {
272                    if note.channel == *channel {
273                        note.sostenuto_captured = false;
274                        note.released_while_sustained = false;
275                    }
276                }
277            }
278            PerformanceIntent::Panic => {
279                self.held_notes.clear();
280                self.sustain_pedal = false;
281                self.sostenuto_pedal = false;
282            }
283            PerformanceIntent::Aftertouch { .. }
284            | PerformanceIntent::PitchBend { .. }
285            | PerformanceIntent::Parameter { .. } => {}
286        }
287    }
288}
289
290/// A live performance source that turns inputs into events and captures takes.
291///
292/// Implementors bind an input, poll queued [`PerformanceInput`]s into transformed
293/// [`PerformanceEvent`](crate::PerformanceEvent)s, emit panics, record capture takes,
294/// and render a captured [`PerformanceTake`] into [`Music`](crate::Music).
295pub trait PerformanceSource {
296    /// Binds an input to this source, setting its lane and channel.
297    fn bind_input(&mut self, binding: PerformanceInputBinding) -> Result<()>;
298    /// Transforms queued inputs into events, updating held-note state.
299    fn poll_events(&mut self, inputs: Vec<PerformanceInput>) -> Result<Vec<PerformanceEvent>>;
300    /// Emits note-offs for all held notes followed by a panic event.
301    fn panic(&mut self, input_time: Tick) -> Result<Vec<PerformanceEvent>>;
302    /// Begins capturing emitted events under `take_id`.
303    fn capture_start(&mut self, take_id: Symbol) -> Result<()>;
304    /// Ends capture and returns the recorded [`PerformanceTake`].
305    fn capture_stop(&mut self) -> Result<PerformanceTake>;
306    /// Renders a captured take into a [`Music`](crate::Music) clip.
307    fn as_clip(&self, take: &PerformanceTake) -> Result<Music>;
308}
309
310/// An in-memory [`PerformanceSource`] holding state and an optional capture.
311#[derive(Clone, Debug, PartialEq, Eq)]
312pub struct MemoryPerformanceSource {
313    source_id: Symbol,
314    binding: Option<PerformanceInputBinding>,
315    state: PerformanceSourceState,
316    capture: Option<PerformanceCapture>,
317}
318
319impl MemoryPerformanceSource {
320    /// Creates an unbound source with the given id and default channel.
321    pub fn new(source_id: Symbol, channel: Channel) -> Self {
322        Self {
323            source_id,
324            binding: None,
325            state: PerformanceSourceState::new(channel),
326            capture: None,
327        }
328    }
329
330    /// Returns the symbol identifying this source.
331    pub fn source_id(&self) -> &Symbol {
332        &self.source_id
333    }
334
335    /// Returns a shared reference to the source's state.
336    pub fn state(&self) -> &PerformanceSourceState {
337        &self.state
338    }
339
340    /// Returns a mutable reference to the source's state.
341    pub fn state_mut(&mut self) -> &mut PerformanceSourceState {
342        &mut self.state
343    }
344
345    /// Sets the octave shift applied to incoming pitches.
346    pub fn set_octave_shift(&mut self, octave_shift: i8) {
347        self.state.octave_shift = octave_shift;
348    }
349
350    /// Sets the semitone transpose applied to incoming pitches.
351    pub fn set_transpose(&mut self, transpose: i8) {
352        self.state.transpose = transpose;
353    }
354
355    /// Sets or clears the scale lock applied to incoming pitches.
356    pub fn set_scale_lock(&mut self, scale_lock: Option<ScaleLock>) {
357        self.state.scale_lock = scale_lock;
358    }
359
360    fn binding(&self) -> Result<&PerformanceInputBinding> {
361        self.binding
362            .as_ref()
363            .ok_or_else(|| Error::Eval("performance source input is not bound".to_owned()))
364    }
365
366    fn push_capture(&mut self, events: &[PerformanceEvent]) {
367        if let Some(capture) = &mut self.capture {
368            capture.events.extend_from_slice(events);
369        }
370    }
371}
372
373impl PerformanceSource for MemoryPerformanceSource {
374    fn bind_input(&mut self, binding: PerformanceInputBinding) -> Result<()> {
375        self.state.channel = binding.channel;
376        self.binding = Some(binding);
377        Ok(())
378    }
379
380    fn poll_events(&mut self, inputs: Vec<PerformanceInput>) -> Result<Vec<PerformanceEvent>> {
381        let binding = self.binding()?.clone();
382        let mut events = Vec::new();
383        for input in inputs {
384            let event = PerformanceEvent {
385                lane_id: binding.lane_id.clone(),
386                source_id: self.source_id.clone(),
387                input_time: input.input_time,
388                time: input.input_time,
389                intent: transform_intent(input.intent, &self.state),
390            };
391            self.state.observe_event(&event);
392            events.push(event);
393        }
394        self.push_capture(&events);
395        Ok(events)
396    }
397
398    fn panic(&mut self, input_time: Tick) -> Result<Vec<PerformanceEvent>> {
399        let binding = self.binding()?.clone();
400        let mut events = self
401            .state
402            .held_notes
403            .values()
404            .map(|note| PerformanceEvent {
405                lane_id: binding.lane_id.clone(),
406                source_id: self.source_id.clone(),
407                input_time,
408                time: input_time,
409                intent: PerformanceIntent::NoteOff {
410                    pitch: note.pitch,
411                    velocity: 0,
412                    channel: note.channel,
413                },
414            })
415            .collect::<Vec<_>>();
416        events.push(PerformanceEvent {
417            lane_id: binding.lane_id,
418            source_id: self.source_id.clone(),
419            input_time,
420            time: input_time,
421            intent: PerformanceIntent::Panic,
422        });
423        for event in &events {
424            self.state.observe_event(event);
425        }
426        self.push_capture(&events);
427        Ok(events)
428    }
429
430    fn capture_start(&mut self, take_id: Symbol) -> Result<()> {
431        if self.capture.is_some() {
432            return Err(Error::Eval(
433                "performance capture is already active".to_owned(),
434            ));
435        }
436        self.capture = Some(PerformanceCapture {
437            take_id,
438            events: Vec::new(),
439        });
440        Ok(())
441    }
442
443    fn capture_stop(&mut self) -> Result<PerformanceTake> {
444        let capture = self
445            .capture
446            .take()
447            .ok_or_else(|| Error::Eval("performance capture is not active".to_owned()))?;
448        PerformanceTake::new(self.source_id.clone(), capture.take_id, capture.events)
449    }
450
451    fn as_clip(&self, take: &PerformanceTake) -> Result<Music> {
452        take.as_clip()
453    }
454}
455
456#[derive(Clone, Debug, PartialEq, Eq)]
457struct PerformanceCapture {
458    take_id: Symbol,
459    events: Vec<PerformanceEvent>,
460}
461
462fn transform_intent(
463    intent: PerformanceIntent,
464    state: &PerformanceSourceState,
465) -> PerformanceIntent {
466    match intent {
467        PerformanceIntent::NoteOn {
468            pitch,
469            velocity,
470            channel,
471        } => PerformanceIntent::NoteOn {
472            pitch: state.transform_pitch(pitch),
473            velocity,
474            channel,
475        },
476        PerformanceIntent::NoteOff {
477            pitch,
478            velocity,
479            channel,
480        } => PerformanceIntent::NoteOff {
481            pitch: state.transform_pitch(pitch),
482            velocity,
483            channel,
484        },
485        PerformanceIntent::Aftertouch {
486            pitch,
487            pressure,
488            channel,
489        } => PerformanceIntent::Aftertouch {
490            pitch: state.transform_pitch(pitch),
491            pressure,
492            channel,
493        },
494        other => other,
495    }
496}