Skip to main content

sim_lib_music_core/
performance_take.rs

1use std::collections::BTreeMap;
2
3use sim_kernel::{Error, Expr, Result, Symbol};
4use sim_value::access;
5
6use sim_lib_stream_core::{
7    BufferPolicy, ClockDomain, StreamCassette, StreamDirection, StreamItem, StreamMedia,
8    StreamMetadata, StreamPacket, StreamStats, TransportProfile,
9};
10
11use crate::freeze::stable_hash;
12use crate::{
13    Channel, LaneId, Music, NoteEvent, PerformanceEvent, PerformanceIntent, PerformanceNoteKey,
14    PianoRoll, Pitch, PlayContext, PlayEvent, Tick, stable_event_order, tick_to_kernel_tick,
15};
16
17/// A captured recording of performance events with a content-addressed cassette.
18///
19/// Stores the raw [`PerformanceEvent`](crate::PerformanceEvent)s plus a derived
20/// stream cassette and a stable content hash, and can replay, convert to notes, or
21/// render to [`Music`](crate::Music).
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct PerformanceTake {
24    /// Symbol of the source that captured the take.
25    pub source_id: Symbol,
26    /// Symbol identifying the take.
27    pub take_id: Symbol,
28    /// Captured performance events in order.
29    pub events: Vec<PerformanceEvent>,
30    cassette: StreamCassette,
31    content_hash: String,
32}
33
34impl PerformanceTake {
35    /// Builds a take from events, deriving its cassette and content hash.
36    pub fn new(source_id: Symbol, take_id: Symbol, events: Vec<PerformanceEvent>) -> Result<Self> {
37        let cassette = performance_cassette_from_events(&source_id, &take_id, &events)?;
38        let content_hash = performance_cassette_hash(&cassette);
39        Ok(Self {
40            source_id,
41            take_id,
42            events,
43            cassette,
44            content_hash,
45        })
46    }
47
48    /// Returns the stream cassette derived from the take's events.
49    pub fn cassette(&self) -> &StreamCassette {
50        &self.cassette
51    }
52
53    /// Returns the stable content hash of the take's cassette.
54    pub fn content_hash(&self) -> &str {
55        &self.content_hash
56    }
57
58    /// Decodes the take's events back from its cassette.
59    pub fn replay_events(&self) -> Result<Vec<PerformanceEvent>> {
60        performance_events_from_cassette(&self.cassette)
61    }
62
63    /// Recomputes the content hash from the cassette-replayed events.
64    pub fn replay_content_hash(&self) -> Result<String> {
65        let events = self.replay_events()?;
66        let cassette = performance_cassette_from_events(&self.source_id, &self.take_id, &events)?;
67        Ok(performance_cassette_hash(&cassette))
68    }
69
70    /// Pairs note-on/note-off events into timed [`NoteEvent`](crate::NoteEvent)s.
71    pub fn note_events(&self) -> Result<Vec<NoteEvent>> {
72        performance_note_events(&self.events)
73    }
74
75    /// Returns the take's notes wrapped as [`PlayEvent`](crate::PlayEvent)s.
76    pub fn play_events(&self) -> Result<Vec<PlayEvent>> {
77        Ok(self
78            .note_events()?
79            .into_iter()
80            .map(PlayEvent::Note)
81            .collect())
82    }
83
84    /// Renders the take into a piano-roll [`Music`](crate::Music) clip.
85    pub fn as_clip(&self) -> Result<Music> {
86        Ok(Music::PianoRoll(PianoRoll::from_performance_take(self)?))
87    }
88
89    /// Returns `cx` with the take's play events merged into its upstream, ordered.
90    pub fn player_chain_context(&self, cx: &PlayContext) -> Result<PlayContext> {
91        let mut next = cx.clone();
92        next.upstream.extend(self.play_events()?);
93        stable_event_order(&mut next.upstream);
94        Ok(next)
95    }
96}
97
98impl PerformanceEvent {
99    /// Decodes a performance event from an [`Expr`] map.
100    ///
101    /// Validates the `event` kind symbol and reads the lane, source, input time,
102    /// time, and intent fields.
103    pub fn from_expr(expr: &Expr) -> Result<Self> {
104        let Expr::Map(entries) = expr else {
105            return Err(Error::Eval("performance event must be a map".to_owned()));
106        };
107        let kind = symbol_field(entries, "event")?;
108        if *kind != crate::LaneKind::Performance.symbol() {
109            return Err(Error::Eval(format!(
110                "performance event has wrong kind {}",
111                kind.as_qualified_str()
112            )));
113        }
114        Ok(Self {
115            lane_id: LaneId::new(string_field(entries, "lane")?),
116            source_id: symbol_field(entries, "source")?.clone(),
117            input_time: tick_field(entries, "input-time")?,
118            time: tick_field(entries, "time")?,
119            intent: PerformanceIntent::from_expr(field(entries, "intent")?)?,
120        })
121    }
122}
123
124/// Returns the qualified data-kind symbol for performance-event stream packets.
125pub fn performance_event_data_kind() -> Symbol {
126    Symbol::qualified("music/performance", "event")
127}
128
129fn performance_cassette_from_events(
130    source_id: &Symbol,
131    take_id: &Symbol,
132    events: &[PerformanceEvent],
133) -> Result<StreamCassette> {
134    let metadata = performance_metadata(source_id, take_id, events.len())?;
135    let items = events
136        .iter()
137        .map(|event| {
138            StreamItem::with_ticks(
139                StreamPacket::data(performance_event_data_kind(), event.to_expr()),
140                vec![tick_to_kernel_tick(
141                    event.time,
142                    ClockDomain::MidiTick.symbol(),
143                )],
144            )
145        })
146        .collect::<Result<Vec<_>>>()?;
147    StreamCassette::from_items(
148        metadata,
149        items,
150        TransportProfile::memory_local(),
151        StreamStats {
152            yielded: events.len() as u64,
153            closed: true,
154            ..StreamStats::default()
155        },
156    )
157}
158
159fn performance_metadata(
160    source_id: &Symbol,
161    take_id: &Symbol,
162    event_count: usize,
163) -> Result<StreamMetadata> {
164    Ok(StreamMetadata::new(
165        Symbol::qualified(
166            "music/performance-cassette",
167            format!("{}:{}", source_id.name, take_id.name),
168        ),
169        StreamMedia::Data,
170        StreamDirection::Source,
171        ClockDomain::MidiTick.symbol(),
172        BufferPolicy::bounded(event_count.max(1))?,
173    ))
174}
175
176fn performance_events_from_cassette(cassette: &StreamCassette) -> Result<Vec<PerformanceEvent>> {
177    cassette
178        .items()?
179        .into_iter()
180        .map(|item| match item.packet() {
181            StreamPacket::Data(packet) if packet.kind == performance_event_data_kind() => {
182                PerformanceEvent::from_expr(&packet.payload)
183            }
184            _ => Err(Error::Eval(
185                "stream cassette item is not a performance event".to_owned(),
186            )),
187        })
188        .collect()
189}
190
191fn performance_cassette_hash(cassette: &StreamCassette) -> String {
192    stable_hash("performance-cassette", &cassette.to_expr())
193}
194
195fn performance_note_events(events: &[PerformanceEvent]) -> Result<Vec<NoteEvent>> {
196    let mut state = PerformanceClipState::default();
197    for event in events {
198        state.observe(event)?;
199    }
200    state.finish()
201}
202
203#[derive(Clone, Debug)]
204struct OpenPerformanceNote {
205    lane_id: LaneId,
206    pitch: Pitch,
207    velocity: u8,
208    channel: Channel,
209    started_at: Tick,
210    key_released: bool,
211    sostenuto_captured: bool,
212}
213
214#[derive(Clone, Debug, Default)]
215struct PerformanceClipState {
216    sustain_pedals: [bool; 16],
217    sostenuto_pedals: [bool; 16],
218    active: BTreeMap<PerformanceNoteKey, OpenPerformanceNote>,
219    notes: Vec<NoteEvent>,
220}
221
222impl PerformanceClipState {
223    fn observe(&mut self, event: &PerformanceEvent) -> Result<()> {
224        match &event.intent {
225            PerformanceIntent::NoteOn {
226                pitch,
227                velocity,
228                channel,
229            } => {
230                let key = PerformanceNoteKey::new(*channel, *pitch);
231                if self.active.contains_key(&key) {
232                    self.close_note(key, event.time)?;
233                }
234                self.active.insert(
235                    key,
236                    OpenPerformanceNote {
237                        lane_id: event.lane_id.clone(),
238                        pitch: *pitch,
239                        velocity: *velocity,
240                        channel: *channel,
241                        started_at: event.time,
242                        key_released: false,
243                        sostenuto_captured: false,
244                    },
245                );
246            }
247            PerformanceIntent::NoteOff { pitch, channel, .. } => {
248                let key = PerformanceNoteKey::new(*channel, *pitch);
249                let held = if let Some(note) = self.active.get_mut(&key) {
250                    note.key_released = true;
251                    self.sustain_pedals[channel.0 as usize]
252                        || (self.sostenuto_pedals[channel.0 as usize] && note.sostenuto_captured)
253                } else {
254                    false
255                };
256                if !held {
257                    self.close_note(key, event.time)?;
258                }
259            }
260            PerformanceIntent::Sustain { down, channel } => {
261                self.sustain_pedals[channel.0 as usize] = *down;
262                if !down {
263                    let released = self
264                        .active
265                        .iter()
266                        .filter_map(|(key, note)| {
267                            (key.channel == channel.0
268                                && note.key_released
269                                && !(self.sostenuto_pedals[channel.0 as usize]
270                                    && note.sostenuto_captured))
271                                .then_some(*key)
272                        })
273                        .collect::<Vec<_>>();
274                    for key in released {
275                        self.close_note(key, event.time)?;
276                    }
277                }
278            }
279            PerformanceIntent::Sostenuto { down, channel } => {
280                let index = channel.0 as usize;
281                if *down && !self.sostenuto_pedals[index] {
282                    for (key, note) in &mut self.active {
283                        if key.channel == channel.0 {
284                            note.sostenuto_captured = true;
285                        }
286                    }
287                }
288                self.sostenuto_pedals[index] = *down;
289                if !down {
290                    let released = self
291                        .active
292                        .iter()
293                        .filter_map(|(key, note)| {
294                            (key.channel == channel.0
295                                && note.key_released
296                                && !self.sustain_pedals[index])
297                                .then_some(*key)
298                        })
299                        .collect::<Vec<_>>();
300                    for key in released {
301                        self.close_note(key, event.time)?;
302                    }
303                    for (key, note) in &mut self.active {
304                        if key.channel == channel.0 {
305                            note.sostenuto_captured = false;
306                        }
307                    }
308                }
309            }
310            PerformanceIntent::AllNotesOff { channel } => {
311                let index = channel.0 as usize;
312                let keys = self
313                    .active
314                    .iter_mut()
315                    .filter_map(|(key, note)| {
316                        if key.channel != channel.0 {
317                            return None;
318                        }
319                        note.key_released = true;
320                        (!(self.sustain_pedals[index]
321                            || self.sostenuto_pedals[index] && note.sostenuto_captured))
322                            .then_some(*key)
323                    })
324                    .collect::<Vec<_>>();
325                for key in keys {
326                    self.close_note(key, event.time)?;
327                }
328            }
329            PerformanceIntent::AllSoundOff { channel } => {
330                let keys = self
331                    .active
332                    .keys()
333                    .filter(|key| key.channel == channel.0)
334                    .copied()
335                    .collect::<Vec<_>>();
336                for key in keys {
337                    self.close_note(key, event.time)?;
338                }
339            }
340            PerformanceIntent::ResetControllers { channel } => {
341                let index = channel.0 as usize;
342                self.sustain_pedals[index] = false;
343                self.sostenuto_pedals[index] = false;
344                let released = self
345                    .active
346                    .iter()
347                    .filter_map(|(key, note)| {
348                        (key.channel == channel.0 && note.key_released).then_some(*key)
349                    })
350                    .collect::<Vec<_>>();
351                for key in released {
352                    self.close_note(key, event.time)?;
353                }
354                for (key, note) in &mut self.active {
355                    if key.channel == channel.0 {
356                        note.sostenuto_captured = false;
357                    }
358                }
359            }
360            PerformanceIntent::Panic => {
361                let keys = self.active.keys().copied().collect::<Vec<_>>();
362                for key in keys {
363                    self.close_note(key, event.time)?;
364                }
365                self.sustain_pedals = [false; 16];
366                self.sostenuto_pedals = [false; 16];
367            }
368            PerformanceIntent::Aftertouch { .. }
369            | PerformanceIntent::PitchBend { .. }
370            | PerformanceIntent::Parameter { .. } => {}
371        }
372        Ok(())
373    }
374
375    fn close_note(&mut self, key: PerformanceNoteKey, end: Tick) -> Result<()> {
376        let Some(note) = self.active.remove(&key) else {
377            return Ok(());
378        };
379        let end = end.quantize(note.started_at.tpq);
380        if end.ticks < note.started_at.ticks {
381            return Err(Error::Eval(
382                "performance note-off precedes note-on".to_owned(),
383            ));
384        }
385        self.notes.push(NoteEvent {
386            lane_id: note.lane_id,
387            time: note.started_at,
388            duration: Tick::new(end.ticks - note.started_at.ticks, note.started_at.tpq)
389                .map_err(music_err)?,
390            pitch: note.pitch,
391            velocity: note.velocity,
392            channel: note.channel,
393        });
394        Ok(())
395    }
396
397    fn finish(mut self) -> Result<Vec<NoteEvent>> {
398        if !self.active.is_empty() {
399            return Err(Error::Eval(
400                "cannot convert performance take with held notes".to_owned(),
401            ));
402        }
403        self.notes.sort_by(|left, right| {
404            left.time
405                .ticks
406                .cmp(&right.time.ticks)
407                .then_with(|| left.lane_id.cmp(&right.lane_id))
408                .then_with(|| left.pitch.cmp(&right.pitch))
409        });
410        Ok(self.notes)
411    }
412}
413
414fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
415    entries
416        .iter()
417        .find_map(|(key, value)| match key {
418            Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
419                Some(value)
420            }
421            _ => None,
422        })
423        .ok_or_else(|| Error::Eval(format!("missing {name} field")))
424}
425
426fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
427    access::entry_required_str(entries, name, "string field")
428}
429
430fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
431    access::entry_required_sym(entries, name, "symbol field")
432}
433
434fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
435    string_field(entries, name)?
436        .parse::<i64>()
437        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
438}
439
440fn u32_field(entries: &[(Expr, Expr)], name: &str) -> Result<u32> {
441    string_field(entries, name)?
442        .parse::<u32>()
443        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
444}
445
446fn tick_field(entries: &[(Expr, Expr)], name: &str) -> Result<Tick> {
447    let Expr::Map(entries) = field(entries, name)? else {
448        return Err(Error::Eval(format!("{name} field must be a tick map")));
449    };
450    Tick::new(i64_field(entries, "ticks")?, u32_field(entries, "tpq")?).map_err(music_err)
451}
452
453fn music_err(err: impl std::fmt::Display) -> Error {
454    Error::Eval(err.to_string())
455}