Skip to main content

sim_lib_music_core/
playable.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2use sim_lib_stream_core::{
3    BufferPolicy, ClockDomain, LatencyClass, StreamDirection, StreamEnvelope, StreamMedia,
4    StreamMetadata, StreamValue, TransportProfile,
5};
6
7use crate::{
8    AtomRef, LaneDescriptor, LaneId, LaneKind, LaneTarget, Music, MusicObject, NoteEvent,
9    PlayEvent, TempoMapRef, Time, TimeRange, time_to_tick,
10};
11
12/// Stream of rendered play items produced by a [`Playable`].
13pub type PlayStream = StreamValue;
14
15/// Rendering context handed to a [`Playable`] during prepare/render/freeze.
16///
17/// Carries the transport identity, tempo map, clock resolution, time window,
18/// and any upstream events that a render run depends on.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct PlayContext {
21    /// Symbol identifying the transport that drives this render.
22    pub transport: Symbol,
23    /// Reference to the tempo map used to relate musical and wall-clock time.
24    pub tempo: TempoMapRef,
25    /// Audio sample rate in hertz.
26    pub sample_rate: u32,
27    /// Pulses (ticks) per quarter note for tick-domain conversion.
28    pub ppq: u32,
29    /// Time window the render is clipped to.
30    pub range: TimeRange,
31    /// Deterministic seed for any randomized rendering.
32    pub seed: u64,
33    /// Capability tokens the render site advertises.
34    pub capabilities: Vec<String>,
35    /// Hint describing where the render is expected to execute.
36    pub site: SiteHint,
37    /// Events fed in from upstream stages of a chain.
38    pub upstream: Vec<PlayEvent>,
39}
40
41impl PlayContext {
42    /// Creates a context over `range` with offline transport defaults.
43    pub fn new(range: TimeRange) -> Self {
44        Self {
45            transport: Symbol::qualified("music/transport", "offline"),
46            tempo: TempoMapRef::default(),
47            sample_rate: 48_000,
48            ppq: range.start.tpq,
49            range,
50            seed: 0,
51            capabilities: Vec::new(),
52            site: SiteHint::LocalCoroutine,
53            upstream: Vec::new(),
54        }
55    }
56
57    /// Builds stream metadata for a source stream of `item_count` data items.
58    pub fn stream_metadata(&self, id: Symbol, item_count: usize) -> Result<StreamMetadata> {
59        Ok(StreamMetadata::new(
60            id,
61            StreamMedia::Data,
62            StreamDirection::Source,
63            ClockDomain::MidiTick.symbol(),
64            BufferPolicy::bounded(item_count.max(1))?,
65        ))
66    }
67}
68
69/// Hint about the execution site a render is expected to run on.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum SiteHint {
72    /// Same-thread cooperative coroutine.
73    LocalCoroutine,
74    /// Dedicated worker thread.
75    Thread,
76    /// Separate operating-system process.
77    Process,
78    /// WebAssembly guest in a browser.
79    BrowserWasm,
80    /// Browser audio worklet thread.
81    AudioWorklet,
82    /// Node reachable over the local network.
83    Lan,
84}
85
86impl SiteHint {
87    /// Returns the qualified symbol naming this site hint.
88    pub fn symbol(self) -> Symbol {
89        match self {
90            Self::LocalCoroutine => Symbol::qualified("site", "local-coroutine"),
91            Self::Thread => Symbol::qualified("site", "thread"),
92            Self::Process => Symbol::qualified("site", "process"),
93            Self::BrowserWasm => Symbol::qualified("site", "browser-wasm"),
94            Self::AudioWorklet => Symbol::qualified("site", "audio-worklet"),
95            Self::Lan => Symbol::qualified("site", "lan"),
96        }
97    }
98}
99
100/// Static description of a [`Playable`]: its identity, lanes, and clocking.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct PlayableDescriptor {
103    /// Symbol identifying the playable.
104    pub id: Symbol,
105    /// Output lanes the playable produces.
106    pub lanes: Vec<LaneDescriptor>,
107    /// Clock domain the rendered events are timed in.
108    pub clock_domain: ClockDomain,
109    /// Latency class the playable targets.
110    pub latency_class: LatencyClass,
111    /// Object shape exposed for protocol dispatch.
112    pub shape: PlayableShape,
113}
114
115/// Shape record describing the playable protocol surface.
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct PlayableShape {
118    /// Symbol naming the shape.
119    pub symbol: Symbol,
120    /// Field/method names the shape exposes.
121    pub fields: Vec<String>,
122}
123
124impl PlayableShape {
125    /// Returns the canonical shape for music playable objects.
126    pub fn music_object() -> Self {
127        Self {
128            symbol: Symbol::qualified("music/shape", "playable"),
129            fields: vec![
130                "describe".to_owned(),
131                "prepare".to_owned(),
132                "render-range".to_owned(),
133                "render-preview".to_owned(),
134                "freeze".to_owned(),
135                "as-shape".to_owned(),
136            ],
137        }
138    }
139
140    /// Encodes the shape as an expression map.
141    pub fn to_expr(&self) -> Expr {
142        Expr::Map(vec![
143            (
144                Expr::Symbol(Symbol::new("shape")),
145                Expr::Symbol(self.symbol.clone()),
146            ),
147            (
148                Expr::Symbol(Symbol::new("fields")),
149                Expr::List(self.fields.iter().cloned().map(Expr::String).collect()),
150            ),
151        ])
152    }
153
154    /// Decodes a shape from an expression map produced by [`PlayableShape::to_expr`].
155    pub fn from_expr(expr: &Expr) -> Result<Self> {
156        let Expr::Map(entries) = expr else {
157            return Err(Error::Eval("playable shape must be a map".to_owned()));
158        };
159        let symbol = entries
160            .iter()
161            .find_map(|(key, value)| match (key, value) {
162                (Expr::Symbol(key), Expr::Symbol(symbol)) if key.name.as_ref() == "shape" => {
163                    Some(symbol.clone())
164                }
165                _ => None,
166            })
167            .ok_or_else(|| Error::Eval("playable shape missing shape field".to_owned()))?;
168        let fields = entries
169            .iter()
170            .find_map(|(key, value)| match (key, value) {
171                (Expr::Symbol(key), Expr::List(fields)) if key.name.as_ref() == "fields" => {
172                    Some(fields)
173                }
174                _ => None,
175            })
176            .ok_or_else(|| Error::Eval("playable shape missing fields".to_owned()))?
177            .iter()
178            .map(|field| match field {
179                Expr::String(value) => Ok(value.clone()),
180                _ => Err(Error::Eval("playable shape field must be text".to_owned())),
181            })
182            .collect::<Result<Vec<_>>>()?;
183        Ok(Self { symbol, fields })
184    }
185}
186
187/// Fully rendered snapshot of a [`Playable`] with a content hash.
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct FrozenPlayable {
190    /// Descriptor of the frozen playable.
191    pub descriptor: PlayableDescriptor,
192    /// Rendered events in stable order.
193    pub events: Vec<PlayEvent>,
194    /// Stable hash of the rendered content.
195    pub content_hash: String,
196}
197
198/// Object that can describe, render, and freeze itself into play events.
199pub trait Playable {
200    /// Returns the static descriptor for this playable.
201    fn describe(&self) -> Result<PlayableDescriptor>;
202
203    /// Prepares the playable for rendering under `cx`; defaults to a no-op.
204    fn prepare(&mut self, _cx: &PlayContext) -> Result<()> {
205        Ok(())
206    }
207
208    /// Renders the playable over the context's time range into a stream.
209    fn render_range(&self, cx: &PlayContext) -> Result<PlayStream>;
210
211    /// Renders a preview stream; defaults to [`Playable::render_range`].
212    fn render_preview(&self, cx: &PlayContext) -> Result<PlayStream> {
213        self.render_range(cx)
214    }
215
216    /// Renders and captures the playable into a [`FrozenPlayable`].
217    fn freeze(&self, cx: &PlayContext) -> Result<FrozenPlayable>;
218
219    /// Returns the object shape; defaults to [`PlayableShape::music_object`].
220    fn as_shape(&self) -> PlayableShape {
221        PlayableShape::music_object()
222    }
223}
224
225impl Playable for Music {
226    fn describe(&self) -> Result<PlayableDescriptor> {
227        default_music_descriptor(Symbol::qualified(
228            "music/playable",
229            self.kind().to_ascii_lowercase(),
230        ))
231    }
232
233    fn render_range(&self, cx: &PlayContext) -> Result<PlayStream> {
234        let mut events = render_music_events(self, cx)?;
235        crate::stable_event_order(&mut events);
236        let items = events
237            .iter()
238            .map(|event| event.to_stream_item(ClockDomain::MidiTick.symbol()))
239            .collect::<Result<Vec<_>>>()?;
240        let metadata = cx.stream_metadata(
241            Symbol::qualified("music/play-stream", self.kind()),
242            items.len(),
243        )?;
244        Ok(StreamValue::pull(metadata, items))
245    }
246
247    fn freeze(&self, cx: &PlayContext) -> Result<FrozenPlayable> {
248        let mut events = render_music_events(self, cx)?;
249        crate::stable_event_order(&mut events);
250        let descriptor = self.describe()?;
251        let content_hash = stable_content_hash(&events, cx);
252        Ok(FrozenPlayable {
253            descriptor,
254            events,
255            content_hash,
256        })
257    }
258}
259
260/// Renders a music object into clipped, stably ordered note play events.
261///
262/// Walks the object's voices, clips each note to the context range, and
263/// prepends any upstream events from `cx`.
264pub fn render_music_events(object: &dyn MusicObject, cx: &PlayContext) -> Result<Vec<PlayEvent>> {
265    let mut atoms = Vec::new();
266    object.voices(Time::from_integer(0), &mut atoms);
267    let mut events = cx.upstream.clone();
268    let note_lane = LaneId::new("notes");
269    for atom in atoms {
270        if let AtomRef::Note(note) = atom.atom {
271            let onset = time_to_tick(atom.onset, cx.ppq).map_err(music_err)?;
272            let duration = time_to_tick(note.duration, cx.ppq).map_err(music_err)?;
273            let Some((time, duration)) = cx.range.clip_span(onset, duration) else {
274                continue;
275            };
276            events.push(PlayEvent::Note(NoteEvent {
277                lane_id: note_lane.clone(),
278                time,
279                duration,
280                pitch: note.pitch,
281                velocity: note.velocity,
282                channel: note.channel,
283            }));
284        }
285    }
286    crate::stable_event_order(&mut events);
287    Ok(events)
288}
289
290/// Drains a play stream into transport envelopes over a memory-local profile.
291pub fn stream_envelopes(stream: &PlayStream) -> Result<Vec<StreamEnvelope>> {
292    let metadata = stream.metadata().clone();
293    let items = stream.take_packets(usize::MAX)?;
294    items
295        .iter()
296        .enumerate()
297        .map(|(sequence, item)| {
298            StreamEnvelope::from_item_with_profile(
299                &metadata,
300                sequence as u64,
301                item,
302                TransportProfile::memory_local(),
303            )
304        })
305        .collect()
306}
307
308fn default_music_descriptor(id: Symbol) -> Result<PlayableDescriptor> {
309    Ok(PlayableDescriptor {
310        id,
311        lanes: vec![
312            LaneDescriptor::new(
313                LaneId::new("notes"),
314                LaneKind::Note,
315                LaneTarget::Instrument(Symbol::qualified("music/target", "default")),
316                0,
317            )
318            .map_err(music_err)?,
319        ],
320        clock_domain: ClockDomain::MidiTick,
321        latency_class: LatencyClass::Interactive,
322        shape: PlayableShape::music_object(),
323    })
324}
325
326fn stable_content_hash(events: &[PlayEvent], cx: &PlayContext) -> String {
327    let mut hash = 0xcbf29ce484222325u64;
328    for byte in format!("{events:?}:{}", cx.seed).bytes() {
329        hash ^= u64::from(byte);
330        hash = hash.wrapping_mul(0x100000001b3);
331    }
332    format!("fnv1a64:{hash:016x}")
333}
334
335fn music_err(err: crate::MusicError) -> Error {
336    Error::Eval(err.to_string())
337}