use std::collections::BTreeMap;
use sim_kernel::{Error, Expr, Result, Symbol};
use sim_lib_stream_core::{
BufferPolicy, ClockDomain, StreamCassette, StreamDirection, StreamItem, StreamMedia,
StreamMetadata, StreamPacket, StreamStats, TransportProfile,
};
use crate::freeze::stable_hash;
use crate::{
Channel, LaneId, Music, NoteEvent, PerformanceEvent, PerformanceIntent, PerformanceNoteKey,
PianoRoll, Pitch, PlayContext, PlayEvent, Tick, stable_event_order, tick_to_kernel_tick,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PerformanceTake {
pub source_id: Symbol,
pub take_id: Symbol,
pub events: Vec<PerformanceEvent>,
cassette: StreamCassette,
content_hash: String,
}
impl PerformanceTake {
pub fn new(source_id: Symbol, take_id: Symbol, events: Vec<PerformanceEvent>) -> Result<Self> {
let cassette = performance_cassette_from_events(&source_id, &take_id, &events)?;
let content_hash = performance_cassette_hash(&cassette);
Ok(Self {
source_id,
take_id,
events,
cassette,
content_hash,
})
}
pub fn cassette(&self) -> &StreamCassette {
&self.cassette
}
pub fn content_hash(&self) -> &str {
&self.content_hash
}
pub fn replay_events(&self) -> Result<Vec<PerformanceEvent>> {
performance_events_from_cassette(&self.cassette)
}
pub fn replay_content_hash(&self) -> Result<String> {
let events = self.replay_events()?;
let cassette = performance_cassette_from_events(&self.source_id, &self.take_id, &events)?;
Ok(performance_cassette_hash(&cassette))
}
pub fn note_events(&self) -> Result<Vec<NoteEvent>> {
performance_note_events(&self.events)
}
pub fn play_events(&self) -> Result<Vec<PlayEvent>> {
Ok(self
.note_events()?
.into_iter()
.map(PlayEvent::Note)
.collect())
}
pub fn as_clip(&self) -> Result<Music> {
Ok(Music::PianoRoll(PianoRoll::from_performance_take(self)?))
}
pub fn player_chain_context(&self, cx: &PlayContext) -> Result<PlayContext> {
let mut next = cx.clone();
next.upstream.extend(self.play_events()?);
stable_event_order(&mut next.upstream);
Ok(next)
}
}
impl PerformanceEvent {
pub fn from_expr(expr: &Expr) -> Result<Self> {
let Expr::Map(entries) = expr else {
return Err(Error::Eval("performance event must be a map".to_owned()));
};
let kind = symbol_field(entries, "event")?;
if *kind != crate::LaneKind::Performance.symbol() {
return Err(Error::Eval(format!(
"performance event has wrong kind {}",
kind.as_qualified_str()
)));
}
Ok(Self {
lane_id: LaneId::new(string_field(entries, "lane")?),
source_id: symbol_field(entries, "source")?.clone(),
input_time: tick_field(entries, "input-time")?,
time: tick_field(entries, "time")?,
intent: PerformanceIntent::from_expr(field(entries, "intent")?)?,
})
}
}
pub fn performance_event_data_kind() -> Symbol {
Symbol::qualified("music/performance", "event")
}
fn performance_cassette_from_events(
source_id: &Symbol,
take_id: &Symbol,
events: &[PerformanceEvent],
) -> Result<StreamCassette> {
let metadata = performance_metadata(source_id, take_id, events.len())?;
let items = events
.iter()
.map(|event| {
StreamItem::with_ticks(
StreamPacket::data(performance_event_data_kind(), event.to_expr()),
vec![tick_to_kernel_tick(
event.time,
ClockDomain::MidiTick.symbol(),
)],
)
})
.collect::<Result<Vec<_>>>()?;
StreamCassette::from_items(
metadata,
items,
TransportProfile::memory_local(),
StreamStats {
yielded: events.len() as u64,
closed: true,
..StreamStats::default()
},
)
}
fn performance_metadata(
source_id: &Symbol,
take_id: &Symbol,
event_count: usize,
) -> Result<StreamMetadata> {
Ok(StreamMetadata::new(
Symbol::qualified(
"music/performance-cassette",
format!("{}:{}", source_id.name, take_id.name),
),
StreamMedia::Data,
StreamDirection::Source,
ClockDomain::MidiTick.symbol(),
BufferPolicy::bounded(event_count.max(1))?,
))
}
fn performance_events_from_cassette(cassette: &StreamCassette) -> Result<Vec<PerformanceEvent>> {
cassette
.items()?
.into_iter()
.map(|item| match item.packet() {
StreamPacket::Data(packet) if packet.kind == performance_event_data_kind() => {
PerformanceEvent::from_expr(&packet.payload)
}
_ => Err(Error::Eval(
"stream cassette item is not a performance event".to_owned(),
)),
})
.collect()
}
fn performance_cassette_hash(cassette: &StreamCassette) -> String {
stable_hash("performance-cassette", &cassette.to_expr())
}
fn performance_note_events(events: &[PerformanceEvent]) -> Result<Vec<NoteEvent>> {
let mut state = PerformanceClipState::default();
for event in events {
state.observe(event)?;
}
state.finish()
}
#[derive(Clone, Debug)]
struct OpenPerformanceNote {
lane_id: LaneId,
pitch: Pitch,
velocity: u8,
channel: Channel,
started_at: Tick,
released_while_sustained: bool,
}
#[derive(Clone, Debug, Default)]
struct PerformanceClipState {
sustain_pedal: bool,
active: BTreeMap<PerformanceNoteKey, OpenPerformanceNote>,
notes: Vec<NoteEvent>,
}
impl PerformanceClipState {
fn observe(&mut self, event: &PerformanceEvent) -> Result<()> {
match &event.intent {
PerformanceIntent::NoteOn {
pitch,
velocity,
channel,
} => {
let key = PerformanceNoteKey::new(*channel, *pitch);
if self.active.contains_key(&key) {
self.close_note(key, event.time)?;
}
self.active.insert(
key,
OpenPerformanceNote {
lane_id: event.lane_id.clone(),
pitch: *pitch,
velocity: *velocity,
channel: *channel,
started_at: event.time,
released_while_sustained: false,
},
);
}
PerformanceIntent::NoteOff { pitch, channel, .. } => {
let key = PerformanceNoteKey::new(*channel, *pitch);
if self.sustain_pedal {
if let Some(note) = self.active.get_mut(&key) {
note.released_while_sustained = true;
}
} else {
self.close_note(key, event.time)?;
}
}
PerformanceIntent::Sustain { down, .. } => {
self.sustain_pedal = *down;
if !down {
let released = self
.active
.iter()
.filter_map(|(key, note)| note.released_while_sustained.then_some(*key))
.collect::<Vec<_>>();
for key in released {
self.close_note(key, event.time)?;
}
}
}
PerformanceIntent::Panic => {
let keys = self.active.keys().copied().collect::<Vec<_>>();
for key in keys {
self.close_note(key, event.time)?;
}
self.sustain_pedal = false;
}
PerformanceIntent::Aftertouch { .. }
| PerformanceIntent::PitchBend { .. }
| PerformanceIntent::Parameter { .. } => {}
}
Ok(())
}
fn close_note(&mut self, key: PerformanceNoteKey, end: Tick) -> Result<()> {
let Some(note) = self.active.remove(&key) else {
return Ok(());
};
let end = end.quantize(note.started_at.tpq);
if end.ticks < note.started_at.ticks {
return Err(Error::Eval(
"performance note-off precedes note-on".to_owned(),
));
}
self.notes.push(NoteEvent {
lane_id: note.lane_id,
time: note.started_at,
duration: Tick::new(end.ticks - note.started_at.ticks, note.started_at.tpq)
.map_err(music_err)?,
pitch: note.pitch,
velocity: note.velocity,
channel: note.channel,
});
Ok(())
}
fn finish(mut self) -> Result<Vec<NoteEvent>> {
if !self.active.is_empty() {
return Err(Error::Eval(
"cannot convert performance take with held notes".to_owned(),
));
}
self.notes.sort_by(|left, right| {
left.time
.ticks
.cmp(&right.time.ticks)
.then_with(|| left.lane_id.cmp(&right.lane_id))
.then_with(|| left.pitch.cmp(&right.pitch))
});
Ok(self.notes)
}
}
fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
entries
.iter()
.find_map(|(key, value)| match key {
Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
Some(value)
}
_ => None,
})
.ok_or_else(|| Error::Eval(format!("missing {name} field")))
}
fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
match field(entries, name)? {
Expr::String(value) => Ok(value),
_ => Err(Error::Eval(format!("{name} field must be text"))),
}
}
fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
match field(entries, name)? {
Expr::Symbol(value) => Ok(value),
_ => Err(Error::Eval(format!("{name} field must be a symbol"))),
}
}
fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
string_field(entries, name)?
.parse::<i64>()
.map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
}
fn u32_field(entries: &[(Expr, Expr)], name: &str) -> Result<u32> {
string_field(entries, name)?
.parse::<u32>()
.map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
}
fn tick_field(entries: &[(Expr, Expr)], name: &str) -> Result<Tick> {
let Expr::Map(entries) = field(entries, name)? else {
return Err(Error::Eval(format!("{name} field must be a tick map")));
};
Tick::new(i64_field(entries, "ticks")?, u32_field(entries, "tpq")?).map_err(music_err)
}
fn music_err(err: impl std::fmt::Display) -> Error {
Error::Eval(err.to_string())
}