use sim_kernel::{Error, Expr, Result, Symbol};
use sim_lib_stream_core::{
BufferPolicy, ClockDomain, LatencyClass, StreamDirection, StreamEnvelope, StreamMedia,
StreamMetadata, StreamValue, TransportProfile,
};
use crate::{
AtomRef, LaneDescriptor, LaneId, LaneKind, LaneTarget, Music, MusicObject, NoteEvent,
PlayEvent, TempoMapRef, Time, TimeRange, time_to_tick,
};
pub type PlayStream = StreamValue;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlayContext {
pub transport: Symbol,
pub tempo: TempoMapRef,
pub sample_rate: u32,
pub ppq: u32,
pub range: TimeRange,
pub seed: u64,
pub capabilities: Vec<String>,
pub site: SiteHint,
pub upstream: Vec<PlayEvent>,
}
impl PlayContext {
pub fn new(range: TimeRange) -> Self {
Self {
transport: Symbol::qualified("music/transport", "offline"),
tempo: TempoMapRef::default(),
sample_rate: 48_000,
ppq: range.start.tpq,
range,
seed: 0,
capabilities: Vec::new(),
site: SiteHint::LocalCoroutine,
upstream: Vec::new(),
}
}
pub fn stream_metadata(&self, id: Symbol, item_count: usize) -> Result<StreamMetadata> {
Ok(StreamMetadata::new(
id,
StreamMedia::Data,
StreamDirection::Source,
ClockDomain::MidiTick.symbol(),
BufferPolicy::bounded(item_count.max(1))?,
))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SiteHint {
LocalCoroutine,
Thread,
Process,
BrowserWasm,
AudioWorklet,
Lan,
}
impl SiteHint {
pub fn symbol(self) -> Symbol {
match self {
Self::LocalCoroutine => Symbol::qualified("site", "local-coroutine"),
Self::Thread => Symbol::qualified("site", "thread"),
Self::Process => Symbol::qualified("site", "process"),
Self::BrowserWasm => Symbol::qualified("site", "browser-wasm"),
Self::AudioWorklet => Symbol::qualified("site", "audio-worklet"),
Self::Lan => Symbol::qualified("site", "lan"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlayableDescriptor {
pub id: Symbol,
pub lanes: Vec<LaneDescriptor>,
pub clock_domain: ClockDomain,
pub latency_class: LatencyClass,
pub shape: PlayableShape,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlayableShape {
pub symbol: Symbol,
pub fields: Vec<String>,
}
impl PlayableShape {
pub fn music_object() -> Self {
Self {
symbol: Symbol::qualified("music/shape", "playable"),
fields: vec![
"describe".to_owned(),
"prepare".to_owned(),
"render-range".to_owned(),
"render-preview".to_owned(),
"freeze".to_owned(),
"as-shape".to_owned(),
],
}
}
pub fn to_expr(&self) -> Expr {
Expr::Map(vec![
(
Expr::Symbol(Symbol::new("shape")),
Expr::Symbol(self.symbol.clone()),
),
(
Expr::Symbol(Symbol::new("fields")),
Expr::List(self.fields.iter().cloned().map(Expr::String).collect()),
),
])
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
let Expr::Map(entries) = expr else {
return Err(Error::Eval("playable shape must be a map".to_owned()));
};
let symbol = entries
.iter()
.find_map(|(key, value)| match (key, value) {
(Expr::Symbol(key), Expr::Symbol(symbol)) if key.name.as_ref() == "shape" => {
Some(symbol.clone())
}
_ => None,
})
.ok_or_else(|| Error::Eval("playable shape missing shape field".to_owned()))?;
let fields = entries
.iter()
.find_map(|(key, value)| match (key, value) {
(Expr::Symbol(key), Expr::List(fields)) if key.name.as_ref() == "fields" => {
Some(fields)
}
_ => None,
})
.ok_or_else(|| Error::Eval("playable shape missing fields".to_owned()))?
.iter()
.map(|field| match field {
Expr::String(value) => Ok(value.clone()),
_ => Err(Error::Eval("playable shape field must be text".to_owned())),
})
.collect::<Result<Vec<_>>>()?;
Ok(Self { symbol, fields })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FrozenPlayable {
pub descriptor: PlayableDescriptor,
pub events: Vec<PlayEvent>,
pub content_hash: String,
}
pub trait Playable {
fn describe(&self) -> Result<PlayableDescriptor>;
fn prepare(&mut self, _cx: &PlayContext) -> Result<()> {
Ok(())
}
fn render_range(&self, cx: &PlayContext) -> Result<PlayStream>;
fn render_preview(&self, cx: &PlayContext) -> Result<PlayStream> {
self.render_range(cx)
}
fn freeze(&self, cx: &PlayContext) -> Result<FrozenPlayable>;
fn as_shape(&self) -> PlayableShape {
PlayableShape::music_object()
}
}
impl Playable for Music {
fn describe(&self) -> Result<PlayableDescriptor> {
default_music_descriptor(Symbol::qualified(
"music/playable",
self.kind().to_ascii_lowercase(),
))
}
fn render_range(&self, cx: &PlayContext) -> Result<PlayStream> {
let mut events = render_music_events(self, cx)?;
crate::stable_event_order(&mut events);
let items = events
.iter()
.map(|event| event.to_stream_item(ClockDomain::MidiTick.symbol()))
.collect::<Result<Vec<_>>>()?;
let metadata = cx.stream_metadata(
Symbol::qualified("music/play-stream", self.kind()),
items.len(),
)?;
Ok(StreamValue::pull(metadata, items))
}
fn freeze(&self, cx: &PlayContext) -> Result<FrozenPlayable> {
let mut events = render_music_events(self, cx)?;
crate::stable_event_order(&mut events);
let descriptor = self.describe()?;
let content_hash = stable_content_hash(&events, cx);
Ok(FrozenPlayable {
descriptor,
events,
content_hash,
})
}
}
pub fn render_music_events(object: &dyn MusicObject, cx: &PlayContext) -> Result<Vec<PlayEvent>> {
let mut atoms = Vec::new();
object.voices(Time::from_integer(0), &mut atoms);
let mut events = cx.upstream.clone();
let note_lane = LaneId::new("notes");
for atom in atoms {
if let AtomRef::Note(note) = atom.atom {
let onset = time_to_tick(atom.onset, cx.ppq).map_err(music_err)?;
let duration = time_to_tick(note.duration, cx.ppq).map_err(music_err)?;
let Some((time, duration)) = cx.range.clip_span(onset, duration) else {
continue;
};
events.push(PlayEvent::Note(NoteEvent {
lane_id: note_lane.clone(),
time,
duration,
pitch: note.pitch,
velocity: note.velocity,
channel: note.channel,
}));
}
}
crate::stable_event_order(&mut events);
Ok(events)
}
pub fn stream_envelopes(stream: &PlayStream) -> Result<Vec<StreamEnvelope>> {
let metadata = stream.metadata().clone();
let items = stream.take_packets(usize::MAX)?;
items
.iter()
.enumerate()
.map(|(sequence, item)| {
StreamEnvelope::from_item_with_profile(
&metadata,
sequence as u64,
item,
TransportProfile::memory_local(),
)
})
.collect()
}
fn default_music_descriptor(id: Symbol) -> Result<PlayableDescriptor> {
Ok(PlayableDescriptor {
id,
lanes: vec![
LaneDescriptor::new(
LaneId::new("notes"),
LaneKind::Note,
LaneTarget::Instrument(Symbol::qualified("music/target", "default")),
0,
)
.map_err(music_err)?,
],
clock_domain: ClockDomain::MidiTick,
latency_class: LatencyClass::Interactive,
shape: PlayableShape::music_object(),
})
}
fn stable_content_hash(events: &[PlayEvent], cx: &PlayContext) -> String {
let mut hash = 0xcbf29ce484222325u64;
for byte in format!("{events:?}:{}", cx.seed).bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3);
}
format!("fnv1a64:{hash:016x}")
}
fn music_err(err: crate::MusicError) -> Error {
Error::Eval(err.to_string())
}