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 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}