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#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct PerformanceTake {
24 pub source_id: Symbol,
26 pub take_id: Symbol,
28 pub events: Vec<PerformanceEvent>,
30 cassette: StreamCassette,
31 content_hash: String,
32}
33
34impl PerformanceTake {
35 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 pub fn cassette(&self) -> &StreamCassette {
50 &self.cassette
51 }
52
53 pub fn content_hash(&self) -> &str {
55 &self.content_hash
56 }
57
58 pub fn replay_events(&self) -> Result<Vec<PerformanceEvent>> {
60 performance_events_from_cassette(&self.cassette)
61 }
62
63 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 pub fn note_events(&self) -> Result<Vec<NoteEvent>> {
72 performance_note_events(&self.events)
73 }
74
75 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 pub fn as_clip(&self) -> Result<Music> {
86 Ok(Music::PianoRoll(PianoRoll::from_performance_take(self)?))
87 }
88
89 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 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
124pub 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}