Skip to main content

sim_lib_music_core/
player.rs

1use sim_kernel::{Error, Expr, Result, Symbol};
2use sim_lib_stream_core::{ClockDomain, LatencyClass, RateContract};
3use sim_lib_topology::{PlacementNodeProfile, SiteId};
4use sim_value::access;
5
6use crate::{LaneId, LaneTarget, PlayEvent};
7
8/// How a chain device combines its output with the events flowing through it.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum PlayerMode {
11    /// Pass input through and append generated events.
12    Through,
13    /// Drop input and emit only generated events.
14    Replace,
15    /// Filter input by lane, dropping matched events.
16    Filter,
17    /// Route input while generating side events without merging them.
18    Sidechain,
19    /// Drop input and emit self-clocked generated events.
20    SelfClocked,
21}
22
23impl PlayerMode {
24    /// Returns the stable wire label for this mode.
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use sim_lib_music_core::PlayerMode;
30    ///
31    /// assert_eq!(PlayerMode::Through.wire_label(), "through");
32    /// assert_eq!(PlayerMode::SelfClocked.wire_label(), "self_clocked");
33    /// ```
34    pub fn wire_label(self) -> &'static str {
35        match self {
36            Self::Through => "through",
37            Self::Replace => "replace",
38            Self::Filter => "filter",
39            Self::Sidechain => "sidechain",
40            Self::SelfClocked => "self_clocked",
41        }
42    }
43
44    /// Returns the qualified symbol naming this mode.
45    pub fn symbol(self) -> Symbol {
46        Symbol::qualified("music/player-mode", self.wire_label())
47    }
48}
49
50/// Stable identifier for a device within a player chain.
51#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct PlayerDeviceId(pub String);
53
54impl PlayerDeviceId {
55    /// Creates a device id from any string-like value.
56    pub fn new(value: impl Into<String>) -> Self {
57        Self(value.into())
58    }
59}
60
61impl AsRef<str> for PlayerDeviceId {
62    fn as_ref(&self) -> &str {
63        &self.0
64    }
65}
66
67/// Typed value for a player parameter snapshot entry.
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum ParamValue {
70    /// Boolean parameter.
71    Bool(bool),
72    /// Signed 64-bit integer parameter.
73    I64(i64),
74    /// Text parameter.
75    Text(String),
76    /// Symbol parameter.
77    Symbol(Symbol),
78}
79
80impl ParamValue {
81    fn to_expr(&self) -> Expr {
82        match self {
83            Self::Bool(value) => Expr::Bool(*value),
84            Self::I64(value) => Expr::String(value.to_string()),
85            Self::Text(value) => Expr::String(value.clone()),
86            Self::Symbol(value) => Expr::Symbol(value.clone()),
87        }
88    }
89}
90
91/// Sorted, deduplicated snapshot of named player parameters.
92#[derive(Clone, Debug, Default, PartialEq, Eq)]
93pub struct ParamSnapshot {
94    /// Parameter entries sorted by key with duplicates removed.
95    pub entries: Vec<(String, ParamValue)>,
96}
97
98impl ParamSnapshot {
99    /// Builds a snapshot, sorting entries by key and dropping duplicate keys.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use sim_lib_music_core::{ParamSnapshot, ParamValue};
105    ///
106    /// let snapshot = ParamSnapshot::new(vec![
107    ///     ("gain".to_owned(), ParamValue::I64(1)),
108    ///     ("attack".to_owned(), ParamValue::Bool(true)),
109    /// ]);
110    /// assert_eq!(snapshot.entries[0].0, "attack");
111    /// assert_eq!(snapshot.entries[1].0, "gain");
112    /// ```
113    pub fn new(mut entries: Vec<(String, ParamValue)>) -> Self {
114        entries.sort_by(|left, right| left.0.cmp(&right.0));
115        entries.dedup_by(|left, right| left.0 == right.0);
116        Self { entries }
117    }
118
119    /// Encodes the snapshot as an expression map keyed by parameter name.
120    pub fn to_expr(&self) -> Expr {
121        Expr::Map(
122            self.entries
123                .iter()
124                .map(|(key, value)| (Expr::Symbol(Symbol::new(key.clone())), value.to_expr()))
125                .collect(),
126        )
127    }
128}
129
130/// Placement of a chain device: where it runs and under what rate profile.
131#[derive(Clone, Debug, PartialEq, Eq)]
132pub struct ChainPlacement {
133    /// Site the device is placed on.
134    pub site: SiteId,
135    /// Node profile describing the device's rate contract and pinning.
136    pub profile: PlacementNodeProfile,
137}
138
139impl ChainPlacement {
140    /// Creates a placement for `site` with the given node profile.
141    pub fn new(site: impl Into<SiteId>, profile: PlacementNodeProfile) -> Self {
142        Self {
143            site: site.into(),
144            profile,
145        }
146    }
147
148    /// Returns the default local-coroutine placement at MIDI-tick rate.
149    pub fn local_coroutine() -> Self {
150        Self::new(
151            "local-coroutine",
152            PlacementNodeProfile::new(RateContract::midi_tick(), false),
153        )
154    }
155
156    /// Encodes the placement, flattening its rate contract, as an expression map.
157    pub fn to_expr(&self) -> Expr {
158        let rate = self.profile.rate_contract();
159        Expr::Map(vec![
160            (
161                Expr::Symbol(Symbol::new("site")),
162                Expr::Symbol(self.site.as_symbol().clone()),
163            ),
164            (
165                Expr::Symbol(Symbol::new("clock-domain")),
166                Expr::Symbol(rate.clock_domain().symbol()),
167            ),
168            (
169                Expr::Symbol(Symbol::new("latency-class")),
170                Expr::Symbol(rate.latency_class().symbol()),
171            ),
172            (
173                Expr::Symbol(Symbol::new("nominal-rate-hz")),
174                Expr::String(
175                    rate.nominal_rate_hz()
176                        .map(|rate| rate.to_string())
177                        .unwrap_or_else(|| "none".to_owned()),
178                ),
179            ),
180            (
181                Expr::Symbol(Symbol::new("realtime-pin")),
182                Expr::Bool(self.profile.realtime_pin()),
183            ),
184        ])
185    }
186
187    /// Decodes a placement from a map produced by [`ChainPlacement::to_expr`].
188    pub fn from_expr(expr: &Expr) -> Result<Self> {
189        let Expr::Map(entries) = expr else {
190            return Err(Error::Eval("chain placement must be a map".to_owned()));
191        };
192        let site = symbol_field(entries, "site")?.to_string();
193        let clock_domain = ClockDomain::from_symbol(symbol_field(entries, "clock-domain")?)?;
194        let latency_class = LatencyClass::from_symbol(symbol_field(entries, "latency-class")?)?;
195        let nominal_rate_hz =
196            match string_field(entries, "nominal-rate-hz")? {
197                "none" => None,
198                value => Some(value.parse::<u32>().map_err(|err| {
199                    Error::Eval(format!("invalid placement nominal-rate-hz: {err}"))
200                })?),
201            };
202        let realtime_pin = bool_field(entries, "realtime-pin")?;
203        Ok(Self::new(
204            site,
205            PlacementNodeProfile::new(
206                RateContract::new(clock_domain, latency_class, nominal_rate_hz),
207                realtime_pin,
208            ),
209        ))
210    }
211}
212
213/// Pairing of a device id with its resolved placement.
214#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct PlacedChainDevice {
216    /// Identifier of the placed device.
217    pub device_id: PlayerDeviceId,
218    /// Placement assigned to the device.
219    pub placement: ChainPlacement,
220}
221
222/// Plan listing the placement of every device in a chain.
223#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct ChainPlacementPlan {
225    /// Placed devices, sorted by device id.
226    pub devices: Vec<PlacedChainDevice>,
227}
228
229impl ChainPlacementPlan {
230    /// Builds a plan, sorting devices by id for stable output.
231    pub fn new(mut devices: Vec<PlacedChainDevice>) -> Self {
232        devices.sort_by(|left, right| left.device_id.cmp(&right.device_id));
233        Self { devices }
234    }
235
236    /// Encodes the plan as an expression list of device/placement maps.
237    pub fn to_expr(&self) -> Expr {
238        Expr::List(
239            self.devices
240                .iter()
241                .map(|device| {
242                    Expr::Map(vec![
243                        (
244                            Expr::Symbol(Symbol::new("device")),
245                            Expr::String(device.device_id.0.clone()),
246                        ),
247                        (
248                            Expr::Symbol(Symbol::new("placement")),
249                            device.placement.to_expr(),
250                        ),
251                    ])
252                })
253                .collect(),
254        )
255    }
256
257    /// Decodes a plan from a list produced by [`ChainPlacementPlan::to_expr`].
258    pub fn from_expr(expr: &Expr) -> Result<Self> {
259        let Expr::List(items) = expr else {
260            return Err(Error::Eval(
261                "chain placement plan must be a list".to_owned(),
262            ));
263        };
264        let mut devices = Vec::new();
265        for item in items {
266            let Expr::Map(entries) = item else {
267                return Err(Error::Eval(
268                    "chain placement plan item must be a map".to_owned(),
269                ));
270            };
271            devices.push(PlacedChainDevice {
272                device_id: PlayerDeviceId::new(string_field(entries, "device")?),
273                placement: ChainPlacement::from_expr(field(entries, "placement")?)?,
274            });
275        }
276        Ok(Self::new(devices))
277    }
278}
279
280/// Descriptor of a chain's output target and its rate contract.
281#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct PlayerTargetDescriptor {
283    /// Symbol identifying the target.
284    pub id: Symbol,
285    /// Lane target the chain output is routed to.
286    pub target: LaneTarget,
287    /// Rate contract the target runs under.
288    pub rate_contract: RateContract,
289}
290
291impl PlayerTargetDescriptor {
292    /// Builds an instrument target descriptor at MIDI-tick rate.
293    pub fn instrument(id: impl Into<String>) -> Self {
294        let id = Symbol::qualified("music/target", id.into());
295        Self {
296            id: id.clone(),
297            target: LaneTarget::Instrument(id),
298            rate_contract: RateContract::midi_tick(),
299        }
300    }
301
302    /// Returns the target's clock domain from its rate contract.
303    pub fn clock_domain(&self) -> ClockDomain {
304        self.rate_contract.clock_domain()
305    }
306
307    /// Returns the target's latency class from its rate contract.
308    pub fn latency_class(&self) -> LatencyClass {
309        self.rate_contract.latency_class()
310    }
311}
312
313/// A single processing stage in a player chain.
314#[derive(Clone, Debug, PartialEq, Eq)]
315pub struct ChainDevice {
316    /// Identifier of this device.
317    pub id: PlayerDeviceId,
318    /// Symbol of the player backing this device.
319    pub player: Symbol,
320    /// Mode controlling how the device combines with its input.
321    pub mode: PlayerMode,
322    /// Sort order of the device within the chain.
323    pub order: u32,
324    /// Whether the device passes input through untouched.
325    pub bypass: bool,
326    /// Whether the device is muted (excluded from rendering).
327    pub mute: bool,
328    /// Whether the device is soloed.
329    pub solo: bool,
330    /// Whether the device is enabled.
331    pub enabled: bool,
332    /// Parameter snapshot for the device.
333    pub params: ParamSnapshot,
334    /// Events the device contributes to the chain.
335    pub generated: Vec<PlayEvent>,
336    /// Lane ids the device filters out, kept sorted for binary search.
337    pub filter_lanes: Vec<LaneId>,
338    /// Optional lane the device reroutes events onto.
339    pub route_lane: Option<LaneId>,
340    /// Placement of the device on a site.
341    pub placement: ChainPlacement,
342}
343
344impl ChainDevice {
345    /// Creates an enabled device with default flags and local placement.
346    pub fn new(id: impl Into<String>, player: Symbol, mode: PlayerMode, order: u32) -> Self {
347        Self {
348            id: PlayerDeviceId::new(id),
349            player,
350            mode,
351            order,
352            bypass: false,
353            mute: false,
354            solo: false,
355            enabled: true,
356            params: ParamSnapshot::default(),
357            generated: Vec::new(),
358            filter_lanes: Vec::new(),
359            route_lane: None,
360            placement: ChainPlacement::local_coroutine(),
361        }
362    }
363
364    /// Sets the device's generated events.
365    pub fn with_generated(mut self, generated: Vec<PlayEvent>) -> Self {
366        self.generated = generated;
367        self
368    }
369
370    /// Sets the filter lanes, keeping them sorted for lookup.
371    pub fn with_filter_lanes(mut self, lanes: Vec<LaneId>) -> Self {
372        self.filter_lanes = lanes;
373        self.filter_lanes.sort();
374        self
375    }
376
377    /// Sets the lane that events are rerouted onto.
378    pub fn with_route_lane(mut self, lane: LaneId) -> Self {
379        self.route_lane = Some(lane);
380        self
381    }
382
383    /// Sets the device's placement.
384    pub fn with_placement(mut self, placement: ChainPlacement) -> Self {
385        self.placement = placement;
386        self
387    }
388
389    /// Marks the device as bypassed.
390    pub fn bypassed(mut self) -> Self {
391        self.bypass = true;
392        self
393    }
394}
395
396pub(crate) fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
397    entries
398        .iter()
399        .find_map(|(key, value)| match key {
400            Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
401                Some(value)
402            }
403            _ => None,
404        })
405        .ok_or_else(|| Error::Eval(format!("missing {name} field")))
406}
407
408pub(crate) fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
409    access::entry_required_str(entries, name, "string field")
410}
411
412pub(crate) fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
413    access::entry_required_sym(entries, name, "symbol field")
414}
415
416pub(crate) fn bool_field(entries: &[(Expr, Expr)], name: &str) -> Result<bool> {
417    access::entry_required_bool(entries, name, "boolean field")
418}