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    released_while_sustained: bool,
211}
212
213#[derive(Clone, Debug, Default)]
214struct PerformanceClipState {
215    sustain_pedal: bool,
216    active: BTreeMap<PerformanceNoteKey, OpenPerformanceNote>,
217    notes: Vec<NoteEvent>,
218}
219
220impl PerformanceClipState {
221    fn observe(&mut self, event: &PerformanceEvent) -> Result<()> {
222        match &event.intent {
223            PerformanceIntent::NoteOn {
224                pitch,
225                velocity,
226                channel,
227            } => {
228                let key = PerformanceNoteKey::new(*channel, *pitch);
229                if self.active.contains_key(&key) {
230                    self.close_note(key, event.time)?;
231                }
232                self.active.insert(
233                    key,
234                    OpenPerformanceNote {
235                        lane_id: event.lane_id.clone(),
236                        pitch: *pitch,
237                        velocity: *velocity,
238                        channel: *channel,
239                        started_at: event.time,
240                        released_while_sustained: false,
241                    },
242                );
243            }
244            PerformanceIntent::NoteOff { pitch, channel, .. } => {
245                let key = PerformanceNoteKey::new(*channel, *pitch);
246                if self.sustain_pedal {
247                    if let Some(note) = self.active.get_mut(&key) {
248                        note.released_while_sustained = true;
249                    }
250                } else {
251                    self.close_note(key, event.time)?;
252                }
253            }
254            PerformanceIntent::Sustain { down, .. } => {
255                self.sustain_pedal = *down;
256                if !down {
257                    let released = self
258                        .active
259                        .iter()
260                        .filter_map(|(key, note)| note.released_while_sustained.then_some(*key))
261                        .collect::<Vec<_>>();
262                    for key in released {
263                        self.close_note(key, event.time)?;
264                    }
265                }
266            }
267            PerformanceIntent::Panic => {
268                let keys = self.active.keys().copied().collect::<Vec<_>>();
269                for key in keys {
270                    self.close_note(key, event.time)?;
271                }
272                self.sustain_pedal = false;
273            }
274            PerformanceIntent::Aftertouch { .. }
275            | PerformanceIntent::PitchBend { .. }
276            | PerformanceIntent::Parameter { .. } => {}
277        }
278        Ok(())
279    }
280
281    fn close_note(&mut self, key: PerformanceNoteKey, end: Tick) -> Result<()> {
282        let Some(note) = self.active.remove(&key) else {
283            return Ok(());
284        };
285        let end = end.quantize(note.started_at.tpq);
286        if end.ticks < note.started_at.ticks {
287            return Err(Error::Eval(
288                "performance note-off precedes note-on".to_owned(),
289            ));
290        }
291        self.notes.push(NoteEvent {
292            lane_id: note.lane_id,
293            time: note.started_at,
294            duration: Tick::new(end.ticks - note.started_at.ticks, note.started_at.tpq)
295                .map_err(music_err)?,
296            pitch: note.pitch,
297            velocity: note.velocity,
298            channel: note.channel,
299        });
300        Ok(())
301    }
302
303    fn finish(mut self) -> Result<Vec<NoteEvent>> {
304        if !self.active.is_empty() {
305            return Err(Error::Eval(
306                "cannot convert performance take with held notes".to_owned(),
307            ));
308        }
309        self.notes.sort_by(|left, right| {
310            left.time
311                .ticks
312                .cmp(&right.time.ticks)
313                .then_with(|| left.lane_id.cmp(&right.lane_id))
314                .then_with(|| left.pitch.cmp(&right.pitch))
315        });
316        Ok(self.notes)
317    }
318}
319
320fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
321    entries
322        .iter()
323        .find_map(|(key, value)| match key {
324            Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
325                Some(value)
326            }
327            _ => None,
328        })
329        .ok_or_else(|| Error::Eval(format!("missing {name} field")))
330}
331
332fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
333    access::entry_required_str(entries, name, "string field")
334}
335
336fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
337    access::entry_required_sym(entries, name, "symbol field")
338}
339
340fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
341    string_field(entries, name)?
342        .parse::<i64>()
343        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
344}
345
346fn u32_field(entries: &[(Expr, Expr)], name: &str) -> Result<u32> {
347    string_field(entries, name)?
348        .parse::<u32>()
349        .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
350}
351
352fn tick_field(entries: &[(Expr, Expr)], name: &str) -> Result<Tick> {
353    let Expr::Map(entries) = field(entries, name)? else {
354        return Err(Error::Eval(format!("{name} field must be a tick map")));
355    };
356    Tick::new(i64_field(entries, "ticks")?, u32_field(entries, "tpq")?).map_err(music_err)
357}
358
359fn music_err(err: impl std::fmt::Display) -> Error {
360    Error::Eval(err.to_string())
361}