1use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use serde_json::Value as Json;
14
15use crate::dsl::{SoundDoc, ValidateError};
16
17enum Seg {
19 Key(String),
20 Index(usize),
21}
22
23fn parse_path(path: &str) -> Result<Vec<Seg>, String> {
26 let mut segs = Vec::new();
27 for raw in path.split('.') {
28 if raw.is_empty() {
29 continue;
30 }
31 if let Some(br) = raw.find('[') {
32 let key = &raw[..br];
33 if !key.is_empty() {
34 segs.push(Seg::Key(key.to_string()));
35 }
36 let mut s = &raw[br..];
37 while let Some(rest) = s.strip_prefix('[') {
38 let end = rest.find(']').ok_or("unclosed '[' in path")?;
39 let num = &rest[..end];
40 let idx: usize = num
41 .parse()
42 .map_err(|_| format!("bad array index '{num}' in path"))?;
43 segs.push(Seg::Index(idx));
44 s = &rest[end + 1..];
45 }
46 if !s.is_empty() {
47 return Err(format!("trailing '{s}' after index in path"));
48 }
49 } else if let Ok(idx) = raw.parse::<usize>() {
50 segs.push(Seg::Index(idx));
51 } else {
52 segs.push(Seg::Key(raw.to_string()));
53 }
54 }
55 Ok(segs)
56}
57
58fn nav_mut<'a>(root: &'a mut Json, segs: &[Seg]) -> Result<&'a mut Json, String> {
60 let mut cur = root;
61 for seg in segs {
62 cur = match seg {
63 Seg::Key(k) => cur
64 .get_mut(k)
65 .ok_or_else(|| format!("no field '{k}' at this path"))?,
66 Seg::Index(i) => cur
67 .get_mut(*i)
68 .ok_or_else(|| format!("no array index {i} at this path"))?,
69 };
70 }
71 Ok(cur)
72}
73
74#[non_exhaustive]
77#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
78#[serde(tag = "op", rename_all = "snake_case")]
79pub enum EditOp {
80 Set {
85 path: String,
87 value: Json,
89 },
90 Insert {
93 path: String,
95 #[serde(default)]
97 index: Option<usize>,
98 node: Json,
100 },
101 Remove {
104 path: String,
106 #[serde(default)]
108 index: Option<usize>,
109 },
110}
111
112fn apply_one(json: &mut Json, op: &EditOp) -> Result<(), String> {
113 match op {
114 EditOp::Set { path, value } => {
115 let segs = parse_path(path)?;
116 if segs.is_empty() {
117 return Err("set: path must not be empty".into());
118 }
119 let target = nav_mut(json, &segs)?;
120 *target = value.clone();
121 Ok(())
122 }
123 EditOp::Insert { path, index, node } => {
124 let segs = parse_path(path)?;
125 let target = nav_mut(json, &segs)?;
126 let arr = target
127 .as_array_mut()
128 .ok_or("insert: path does not point to an array (use a chain's `stages` or a mix/mul `inputs`)")?;
129 let i = index.unwrap_or(arr.len()).min(arr.len());
130 arr.insert(i, node.clone());
131 Ok(())
132 }
133 EditOp::Remove { path, index } => {
134 let segs = parse_path(path)?;
135 match index {
136 Some(i) => {
137 let arr = nav_mut(json, &segs)?
138 .as_array_mut()
139 .ok_or("remove: path does not point to an array")?;
140 if *i >= arr.len() {
141 return Err(format!(
142 "remove: index {i} out of range (len {})",
143 arr.len()
144 ));
145 }
146 arr.remove(*i);
147 Ok(())
148 }
149 None => match segs.last() {
150 Some(Seg::Index(i)) => {
151 let i = *i;
152 let parent = nav_mut(json, &segs[..segs.len() - 1])?
153 .as_array_mut()
154 .ok_or("remove: parent of the indexed element is not an array")?;
155 if i >= parent.len() {
156 return Err(format!(
157 "remove: index {i} out of range (len {})",
158 parent.len()
159 ));
160 }
161 parent.remove(i);
162 Ok(())
163 }
164 _ => Err("remove: provide `index`, or a path ending in `[n]`".into()),
165 },
166 }
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq)]
173pub enum EditError {
174 Op {
177 index: usize,
179 reason: String,
181 },
182 InvalidGraph(String),
184 Invalid(ValidateError),
186 Morph(String),
189}
190
191impl std::fmt::Display for EditError {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 EditError::Op { index, reason } => write!(f, "op[{index}]: {reason}"),
195 EditError::InvalidGraph(e) => write!(f, "edited graph is invalid: {e}"),
196 EditError::Invalid(e) => write!(f, "edited document fails validation: {e}"),
197 EditError::Morph(e) => f.write_str(e),
198 }
199 }
200}
201
202impl std::error::Error for EditError {}
203
204impl From<ValidateError> for EditError {
205 fn from(e: ValidateError) -> Self {
206 EditError::Invalid(e)
207 }
208}
209
210pub fn apply_ops(doc: &SoundDoc, ops: &[EditOp]) -> Result<SoundDoc, EditError> {
214 let mut json = serde_json::to_value(doc).map_err(|e| EditError::InvalidGraph(e.to_string()))?;
215 for (index, op) in ops.iter().enumerate() {
216 apply_one(&mut json, op).map_err(|reason| EditError::Op { index, reason })?;
217 }
218 let edited: SoundDoc =
219 serde_json::from_value(json).map_err(|e| EditError::InvalidGraph(e.to_string()))?;
220 edited.validate()?;
221 Ok(edited)
222}
223
224#[derive(Debug, Clone, Serialize, JsonSchema)]
227pub struct NodeInfo {
228 pub path: String,
230 #[serde(rename = "type")]
232 pub node_type: String,
233 pub params: Json,
236}
237
238fn is_child_array(key: &str) -> bool {
240 matches!(key, "inputs" | "stages" | "notes" | "modes")
241}
242
243fn join(prefix: &str, seg: &str) -> String {
246 if prefix.is_empty() {
247 seg.to_string()
248 } else {
249 format!("{prefix}.{seg}")
250 }
251}
252
253fn walk(json: &Json, path: &str, out: &mut Vec<NodeInfo>) {
254 let Some(obj) = json.as_object() else { return };
255 if let Some(t) = obj.get("type").and_then(|v| v.as_str()) {
256 let mut params = serde_json::Map::new();
257 for (k, v) in obj {
258 if k == "type" || is_child_array(k) || k == "tracks" || k == "master" {
261 continue;
262 }
263 params.insert(k.clone(), v.clone());
264 }
265 out.push(NodeInfo {
266 path: path.to_string(),
267 node_type: t.to_string(),
268 params: Json::Object(params),
269 });
270 }
271 for (k, v) in obj {
273 if k == "tracks" {
274 if let Some(arr) = v.as_array() {
276 for (i, ch) in arr.iter().enumerate() {
277 if let Some(node) = ch.get("node") {
278 walk(node, &join(path, &format!("tracks[{i}].node")), out);
279 }
280 }
281 }
282 continue;
283 }
284 if k == "trigger" || k == "node" {
285 walk(v, &join(path, k), out);
286 continue;
287 }
288 if k == "modes" {
289 if let Some(arr) = v.as_array() {
292 for (i, mode) in arr.iter().enumerate() {
293 out.push(NodeInfo {
294 path: join(path, &format!("modes[{i}]")),
295 node_type: "mode".to_string(),
296 params: mode.clone(),
297 });
298 }
299 }
300 continue;
301 }
302 if k == "notes" {
303 if let Some(arr) = v.as_array() {
306 for (i, note) in arr.iter().enumerate() {
307 out.push(NodeInfo {
308 path: join(path, &format!("notes[{i}]")),
309 node_type: "note".to_string(),
310 params: note.clone(),
311 });
312 }
313 }
314 continue;
315 }
316 if !is_child_array(k) {
317 continue;
318 }
319 if let Some(arr) = v.as_array() {
320 for (i, child) in arr.iter().enumerate() {
321 walk(child, &join(path, &format!("{k}[{i}]")), out);
322 }
323 }
324 }
325}
326
327#[derive(Debug, Clone, Serialize, JsonSchema)]
331pub struct LayerInfo {
332 pub id: String,
334 pub pan: f32,
336 pub gain: f32,
338 pub at: f32,
340 pub mute: bool,
342 pub nodes: Vec<NodeInfo>,
344}
345
346#[derive(Debug, Clone, Serialize, JsonSchema)]
348pub struct DescribeMap {
349 pub nodes: Vec<NodeInfo>,
351 pub layers: Vec<LayerInfo>,
353 pub master: Vec<NodeInfo>,
356}
357
358pub fn describe(doc: &SoundDoc) -> DescribeMap {
361 let mut map = DescribeMap {
362 nodes: Vec::new(),
363 layers: Vec::new(),
364 master: Vec::new(),
365 };
366 let json = serde_json::to_value(doc).expect("SoundDoc serializes");
370 if let crate::dsl::Node::Tracks { tracks, master } = &doc.root {
371 for (i, t) in tracks.iter().enumerate() {
372 let mut nodes = Vec::new();
373 if let Some(node_json) = json["root"]["tracks"][i].get("node") {
374 walk(node_json, "", &mut nodes);
375 }
376 map.layers.push(LayerInfo {
377 id: t.id.clone().unwrap_or_else(|| format!("layer_{i}")),
378 pan: t.pan,
379 gain: t.gain,
380 at: t.at,
381 mute: t.mute,
382 nodes,
383 });
384 }
385 for (i, _) in master.iter().enumerate() {
386 if let Some(m) = json["root"]["master"].get(i) {
387 walk(m, &format!("root.master[{i}]"), &mut map.master);
388 }
389 }
390 return map;
391 }
392 if let Some(root) = json.get("root") {
393 walk(root, "root", &mut map.nodes);
394 }
395 map
396}
397
398pub fn morph(a: &SoundDoc, b: &SoundDoc, t: f32) -> Result<SoundDoc, EditError> {
403 let mut ja = serde_json::to_value(a).map_err(|e| EditError::InvalidGraph(e.to_string()))?;
404 let mut jb = serde_json::to_value(b).map_err(|e| EditError::InvalidGraph(e.to_string()))?;
405 jb["name"] = ja["name"].clone();
411 jb["seed"] = ja["seed"].clone();
412 jb["sample_rate"] = ja["sample_rate"].clone();
413 if let Some(v) = ja.get("version") {
416 jb["version"] = v.clone();
417 } else if let Some(o) = jb.as_object_mut() {
418 o.remove("version");
419 }
420 let engine = a.effective_engine().max(b.effective_engine());
423 for j in [&mut ja, &mut jb] {
424 if let Some(o) = j.as_object_mut() {
425 if engine > 0 {
426 o.insert("engine".into(), serde_json::json!(engine));
427 } else {
428 o.remove("engine");
429 }
430 }
431 }
432 if let Some(ta) = ja["root"].get("tracks").and_then(|v| v.as_array()).cloned()
436 && let Some(tb) = jb["root"].get_mut("tracks").and_then(|v| v.as_array_mut())
437 {
438 for (i, track_b) in tb.iter_mut().enumerate() {
439 let Some(track_a) = ta.get(i) else { break };
440 for key in ["id", "mute"] {
441 match track_a.get(key) {
442 Some(v) => track_b[key] = v.clone(),
443 None => {
444 if let Some(o) = track_b.as_object_mut() {
445 o.remove(key);
446 }
447 }
448 }
449 }
450 }
451 }
452 let merged = lerp_json(&ja, &jb, t, "$").map_err(EditError::Morph)?;
453 let doc: SoundDoc = serde_json::from_value(merged)
454 .map_err(|e| EditError::InvalidGraph(format!("morphed graph invalid: {e}")))?;
455 doc.validate()?;
456 Ok(doc)
457}
458
459fn lerp_json(a: &Json, b: &Json, t: f32, path: &str) -> Result<Json, String> {
460 match (a, b) {
461 (Json::Number(x), Json::Number(y)) => {
462 let (fx, fy) = (x.as_f64().unwrap_or(0.0), y.as_f64().unwrap_or(0.0));
463 let v = fx + (fy - fx) * t as f64;
464 if (x.is_i64() || x.is_u64()) && (y.is_i64() || y.is_u64()) {
465 Ok(Json::from(v.round() as i64))
466 } else {
467 Ok(serde_json::Number::from_f64(v)
468 .map(Json::Number)
469 .unwrap_or_else(|| Json::from(0)))
470 }
471 }
472 (Json::String(x), Json::String(y)) => {
473 if x == y {
474 Ok(a.clone())
475 } else if let (Some(fa), Some(fb)) =
476 (crate::dsl::note_to_hz(x), crate::dsl::note_to_hz(y))
477 {
478 let hz = fa + (fb - fa) * t;
480 Ok(serde_json::Number::from_f64(hz as f64)
481 .map(Json::Number)
482 .unwrap_or_else(|| Json::from(0)))
483 } else {
484 Err(format!(
485 "{path}: cannot morph between '{x}' and '{y}' — node types / enum choices must match"
486 ))
487 }
488 }
489 (Json::Array(x), Json::Array(y)) => {
490 if x.len() != y.len() {
491 return Err(format!(
492 "{path}: array lengths differ ({} vs {}) — morph needs identical structure",
493 x.len(),
494 y.len()
495 ));
496 }
497 x.iter()
498 .zip(y)
499 .enumerate()
500 .map(|(i, (xa, xb))| lerp_json(xa, xb, t, &format!("{path}[{i}]")))
501 .collect::<Result<Vec<_>, _>>()
502 .map(Json::Array)
503 }
504 (Json::Object(x), Json::Object(y)) => {
505 if x.len() != y.len() || x.keys().any(|k| !y.contains_key(k)) {
506 return Err(format!(
507 "{path}: object fields differ — morph needs graphs with identical shape"
508 ));
509 }
510 let mut out = serde_json::Map::new();
511 for (k, va) in x {
512 out.insert(k.clone(), lerp_json(va, &y[k], t, &format!("{path}.{k}"))?);
513 }
514 Ok(Json::Object(out))
515 }
516 (Json::Bool(x), Json::Bool(y)) if x == y => Ok(a.clone()),
517 (Json::Null, Json::Null) => Ok(Json::Null),
518 _ => Err(format!(
519 "{path}: structure mismatch — morph interpolates parameters of two same-shaped graphs"
520 )),
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 fn laser() -> SoundDoc {
529 serde_json::from_str(
530 r#"{ "name": "laser", "duration": 0.2, "root": { "type": "mix", "inputs": [
531 { "type": "mul", "inputs": [
532 { "type": "square", "freq": 880, "duty": 0.25 },
533 { "type": "env", "d": 0.18 } ] },
534 { "type": "noise" }
535 ] } }"#,
536 )
537 .unwrap()
538 }
539
540 fn op(json: &str) -> EditOp {
541 serde_json::from_str(json).unwrap()
542 }
543
544 #[test]
545 fn set_changes_a_nested_param() {
546 let edited = apply_ops(
547 &laser(),
548 &[op(
549 r#"{ "op": "set", "path": "root.inputs[0].inputs[0].freq", "value": 440 }"#,
550 )],
551 )
552 .unwrap();
553 let v = serde_json::to_value(&edited).unwrap();
554 assert_eq!(v["root"]["inputs"][0]["inputs"][0]["freq"], 440.0);
555 }
556
557 #[test]
558 fn insert_and_remove_reshape_arrays() {
559 let edited = apply_ops(
560 &laser(),
561 &[
562 op(r#"{ "op": "insert", "path": "root.inputs",
563 "node": { "type": "sine", "freq": 220 } }"#),
564 op(r#"{ "op": "remove", "path": "root.inputs[1]" }"#),
565 ],
566 )
567 .unwrap();
568 let v = serde_json::to_value(&edited).unwrap();
569 let inputs = v["root"]["inputs"].as_array().unwrap();
570 assert_eq!(inputs.len(), 2); assert_eq!(inputs[1]["type"], "sine");
572 }
573
574 #[test]
575 fn bad_edits_fail_with_op_index_and_reason() {
576 let err = apply_ops(
577 &laser(),
578 &[op(
579 r#"{ "op": "set", "path": "root.nope.freq", "value": 1 }"#,
580 )],
581 )
582 .unwrap_err();
583 assert!(err.to_string().contains("op[0]"), "{err}");
584 assert!(err.to_string().contains("no field 'nope'"), "{err}");
585 let err = apply_ops(
587 &laser(),
588 &[op(r#"{ "op": "set", "path": "duration", "value": -1 }"#)],
589 )
590 .unwrap_err();
591 assert!(err.to_string().contains("duration"), "{err}");
592 }
593
594 #[test]
595 fn describe_maps_layers_notes_and_master() {
596 let doc: SoundDoc = serde_json::from_str(
597 r#"{ "name": "song", "duration": 1.0, "root": { "type": "tracks",
598 "tracks": [
599 { "id": "lead", "node": { "type": "seq", "bpm": 120, "wave": "sine",
600 "env": { "d": 0.1 },
601 "notes": [ { "step": 0, "len": 2, "pitch": "C4" } ] } },
602 { "id": "pad", "node": { "type": "chain", "stages": [
603 { "type": "sine", "freq": 220 },
604 { "type": "lowpass", "cutoff": 900 } ] }, "pan": -0.3 }
605 ],
606 "master": [ { "type": "compress", "threshold": -12, "ratio": 4 } ] } }"#,
607 )
608 .unwrap();
609 let map = describe(&doc);
610 assert!(map.nodes.is_empty()); assert_eq!(map.layers.len(), 2);
612 let lead = &map.layers[0];
613 assert_eq!(lead.id, "lead");
614 assert_eq!(lead.nodes[0].path, "");
616 assert_eq!(lead.nodes[0].node_type, "seq");
617 assert!(
618 !lead.nodes[0]
619 .params
620 .as_object()
621 .unwrap()
622 .contains_key("notes")
623 );
624 assert_eq!(lead.nodes[1].path, "notes[0]");
625 assert_eq!(lead.nodes[1].node_type, "note");
626 assert_eq!(lead.nodes[1].params["pitch"], "C4");
627 let pad = &map.layers[1];
628 assert_eq!(pad.nodes[1].path, "stages[0]");
629 assert_eq!(pad.pan, -0.3);
630 assert_eq!(map.master[0].path, "root.master[0]");
632 assert_eq!(map.master[0].node_type, "compress");
633 }
634
635 #[test]
636 fn describe_lists_every_node_with_paths() {
637 let infos = describe(&laser()).nodes;
638 let paths: Vec<&str> = infos.iter().map(|i| i.path.as_str()).collect();
639 assert_eq!(
640 paths,
641 vec![
642 "root",
643 "root.inputs[0]",
644 "root.inputs[0].inputs[0]",
645 "root.inputs[0].inputs[1]",
646 "root.inputs[1]",
647 ]
648 );
649 assert_eq!(infos[2].node_type, "square");
650 assert_eq!(infos[2].params["freq"], 880.0);
651 }
652
653 #[test]
654 fn morph_unifies_layer_identity() {
655 let mk = |id1: &str, id2: &str, f: f32| -> SoundDoc {
656 serde_json::from_str(&format!(
657 r#"{{ "name": "m", "duration": 0.2, "version": 2,
658 "root": {{ "type": "tracks", "tracks": [
659 {{ "id": "{id1}", "node": {{ "type": "sine", "freq": {f} }} }},
660 {{ "id": "{id2}", "node": {{ "type": "noise" }}, "mute": true }}
661 ] }} }}"#
662 ))
663 .unwrap()
664 };
665 let a = mk("crack", "tail_a", 200.0);
667 let b = mk("snap", "tail_b", 400.0);
668 let mid = morph(&a, &b, 0.5).unwrap();
669 let v = serde_json::to_value(&mid).unwrap();
670 assert_eq!(v["root"]["tracks"][0]["id"], "crack"); assert_eq!(v["root"]["tracks"][0]["node"]["freq"], 300.0);
672 assert_eq!(v["root"]["tracks"][1]["mute"], true);
673 }
674
675 #[test]
676 fn morph_midpoint_lerps_numbers_and_notes() {
677 let a: SoundDoc = serde_json::from_str(
678 r#"{ "name": "a", "duration": 0.2, "root": { "type": "sine", "freq": 200 } }"#,
679 )
680 .unwrap();
681 let b: SoundDoc = serde_json::from_str(
682 r#"{ "name": "b", "duration": 0.4, "root": { "type": "sine", "freq": 400 } }"#,
683 )
684 .unwrap();
685 let mid = morph(&a, &b, 0.5).unwrap();
686 let v = serde_json::to_value(&mid).unwrap();
687 assert_eq!(v["root"]["freq"], 300.0);
688 assert!((mid.duration - 0.3).abs() < 1e-6);
689 let c: SoundDoc =
691 serde_json::from_str(r#"{ "name": "c", "root": { "type": "noise" } }"#).unwrap();
692 assert!(morph(&a, &c, 0.5).is_err());
693 }
694
695 #[test]
696 fn morph_unifies_engine_seed_and_rate_instead_of_lerping() {
697 let a: SoundDoc = serde_json::from_str(
701 r#"{ "name": "a", "duration": 0.2, "seed": 7,
702 "root": { "type": "sine", "freq": 200 } }"#,
703 )
704 .unwrap();
705 let b: SoundDoc = serde_json::from_str(
706 r#"{ "name": "b", "duration": 0.2, "seed": 99, "engine": 3,
707 "root": { "type": "sine", "freq": 400 } }"#,
708 )
709 .unwrap();
710 let mid = morph(&a, &b, 0.5).unwrap();
711 assert_eq!(mid.engine, Some(3), "max engine wins, never a lerp");
712 assert_eq!(mid.seed, 7, "a's seed is identity, not a parameter");
713 let c: SoundDoc = serde_json::from_str(
715 r#"{ "name": "c", "duration": 0.2, "root": { "type": "sine", "freq": 300 } }"#,
716 )
717 .unwrap();
718 assert_eq!(morph(&a, &c, 0.5).unwrap().engine, None);
719 }
720}