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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum PlayerMode {
11 Through,
13 Replace,
15 Filter,
17 Sidechain,
19 SelfClocked,
21}
22
23impl PlayerMode {
24 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 pub fn symbol(self) -> Symbol {
46 Symbol::qualified("music/player-mode", self.wire_label())
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct PlayerDeviceId(pub String);
53
54impl PlayerDeviceId {
55 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#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum ParamValue {
70 Bool(bool),
72 I64(i64),
74 Text(String),
76 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
93pub struct ParamSnapshot {
94 pub entries: Vec<(String, ParamValue)>,
96}
97
98impl ParamSnapshot {
99 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 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#[derive(Clone, Debug, PartialEq, Eq)]
132pub struct ChainPlacement {
133 pub site: SiteId,
135 pub profile: PlacementNodeProfile,
137}
138
139impl ChainPlacement {
140 pub fn new(site: impl Into<SiteId>, profile: PlacementNodeProfile) -> Self {
142 Self {
143 site: site.into(),
144 profile,
145 }
146 }
147
148 pub fn local_coroutine() -> Self {
150 Self::new(
151 "local-coroutine",
152 PlacementNodeProfile::new(RateContract::midi_tick(), false),
153 )
154 }
155
156 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 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#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct PlacedChainDevice {
216 pub device_id: PlayerDeviceId,
218 pub placement: ChainPlacement,
220}
221
222#[derive(Clone, Debug, PartialEq, Eq)]
224pub struct ChainPlacementPlan {
225 pub devices: Vec<PlacedChainDevice>,
227}
228
229impl ChainPlacementPlan {
230 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct PlayerTargetDescriptor {
283 pub id: Symbol,
285 pub target: LaneTarget,
287 pub rate_contract: RateContract,
289}
290
291impl PlayerTargetDescriptor {
292 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 pub fn clock_domain(&self) -> ClockDomain {
304 self.rate_contract.clock_domain()
305 }
306
307 pub fn latency_class(&self) -> LatencyClass {
309 self.rate_contract.latency_class()
310 }
311}
312
313#[derive(Clone, Debug, PartialEq, Eq)]
315pub struct ChainDevice {
316 pub id: PlayerDeviceId,
318 pub player: Symbol,
320 pub mode: PlayerMode,
322 pub order: u32,
324 pub bypass: bool,
326 pub mute: bool,
328 pub solo: bool,
330 pub enabled: bool,
332 pub params: ParamSnapshot,
334 pub generated: Vec<PlayEvent>,
336 pub filter_lanes: Vec<LaneId>,
338 pub route_lane: Option<LaneId>,
340 pub placement: ChainPlacement,
342}
343
344impl ChainDevice {
345 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 pub fn with_generated(mut self, generated: Vec<PlayEvent>) -> Self {
366 self.generated = generated;
367 self
368 }
369
370 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 pub fn with_route_lane(mut self, lane: LaneId) -> Self {
379 self.route_lane = Some(lane);
380 self
381 }
382
383 pub fn with_placement(mut self, placement: ChainPlacement) -> Self {
385 self.placement = placement;
386 self
387 }
388
389 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}