refrain_adapters/
audio.rs1use rosc::{OscBundle, OscMessage, OscPacket, OscTime, OscType};
7use serde::Serialize;
8
9use refrain_core::Refrain;
10
11use crate::schedule::{schedule, Hap};
12use crate::{AdapterCaps, AdapterErr, EmitCtx, ExtractedRefrain, RefrainAdapter};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum AudioFormat {
16 StrudelJson,
17 Osc,
18}
19
20#[derive(Debug, Clone, Serialize)]
21struct StrudelHap {
22 whole: StrudelSpan,
23 part: StrudelSpan,
24 value: StrudelValue,
25}
26
27#[derive(Debug, Clone, Serialize)]
28struct StrudelSpan {
29 begin: f64,
30 end: f64,
31}
32
33#[derive(Debug, Clone, Serialize)]
34struct StrudelValue {
35 note: Option<String>,
36 raw: String,
37}
38
39pub struct AudioAdapter {
40 pub format: AudioFormat,
41}
42
43impl AudioAdapter {
44 pub fn new(format: AudioFormat) -> Self {
45 Self { format }
46 }
47
48 fn collect_haps(refrain: &Refrain) -> Vec<Hap> {
49 let mut all = Vec::new();
50 let mut t = 0.0;
51 for (_kind, p) in refrain.stages() {
52 let (sub, dur) = schedule(p, t);
53 all.extend(sub);
54 t += dur;
55 }
56 all
57 }
58
59 fn emit_strudel(&self, haps: &[Hap]) -> Result<Vec<u8>, AdapterErr> {
60 let json_haps: Vec<StrudelHap> = haps
61 .iter()
62 .map(|h| StrudelHap {
63 whole: StrudelSpan {
64 begin: h.start,
65 end: h.end,
66 },
67 part: StrudelSpan {
68 begin: h.start,
69 end: h.end,
70 },
71 value: StrudelValue {
72 note: h.pitch.clone(),
73 raw: h.value.clone(),
74 },
75 })
76 .collect();
77 serde_json::to_vec_pretty(&json_haps)
78 .map_err(|e| AdapterErr::Encoding(format!("strudel json: {}", e)))
79 }
80
81 fn emit_osc(&self, haps: &[Hap]) -> Result<Vec<u8>, AdapterErr> {
82 let mut messages: Vec<OscPacket> = Vec::with_capacity(haps.len());
83 for h in haps {
84 let mut args: Vec<OscType> = Vec::new();
85 args.push(OscType::Float(h.start as f32));
86 args.push(OscType::Float(h.duration() as f32));
87 if let Some(p) = &h.pitch {
88 args.push(OscType::String(p.clone()));
89 } else {
90 args.push(OscType::String(h.value.clone()));
91 }
92 messages.push(OscPacket::Message(OscMessage {
93 addr: "/refrain/note".into(),
94 args,
95 }));
96 }
97 let bundle = OscBundle {
98 timetag: OscTime {
99 seconds: 0,
100 fractional: 0,
101 },
102 content: messages,
103 };
104 rosc::encoder::encode(&OscPacket::Bundle(bundle))
105 .map_err(|e| AdapterErr::Encoding(format!("osc: {}", e)))
106 }
107}
108
109impl RefrainAdapter for AudioAdapter {
110 fn name(&self) -> &str {
111 match self.format {
112 AudioFormat::StrudelJson => "audio.strudel-json",
113 AudioFormat::Osc => "audio.osc",
114 }
115 }
116
117 fn emit(&self, refrain: &ExtractedRefrain, _ctx: &EmitCtx) -> Result<Vec<u8>, AdapterErr> {
118 let haps = Self::collect_haps(refrain.refrain);
119 match self.format {
120 AudioFormat::StrudelJson => self.emit_strudel(&haps),
121 AudioFormat::Osc => self.emit_osc(&haps),
122 }
123 }
124
125 fn capabilities(&self) -> AdapterCaps {
126 AdapterCaps {
127 realtime: matches!(self.format, AudioFormat::Osc),
128 differentiable: false,
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use refrain_core::parse;
137
138 #[test]
139 fn strudel_emits_valid_json() {
140 let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
141 let a = AudioAdapter::new(AudioFormat::StrudelJson);
142 let ex = ExtractedRefrain { refrain: &r };
143 let bytes = a.emit(&ex, &EmitCtx::default()).unwrap();
144 let s = std::str::from_utf8(&bytes).unwrap();
145 assert!(s.contains("\"note\""));
146 assert!(s.contains("\"C4\""));
147 let parsed: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
148 let arr = parsed.as_array().unwrap();
149 assert_eq!(arr.len(), 4);
150 }
151
152 #[test]
153 fn osc_emits_non_empty_bundle() {
154 let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
155 let a = AudioAdapter::new(AudioFormat::Osc);
156 let ex = ExtractedRefrain { refrain: &r };
157 let bytes = a.emit(&ex, &EmitCtx::default()).unwrap();
158 assert!(!bytes.is_empty());
159 assert_eq!(&bytes[0..8], b"#bundle\0");
161 }
162
163 #[test]
164 fn schedule_for_loop_four_quarters_spans_one_cycle() {
165 let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
166 let haps = AudioAdapter::collect_haps(&r);
167 assert_eq!(haps.len(), 4);
168 let total: f64 = haps.last().unwrap().end - haps.first().unwrap().start;
169 assert_eq!(total, 1.0);
170 }
171
172 #[test]
173 fn empty_refrain_yields_no_haps() {
174 let r = parse("(refrain empty)").unwrap();
175 let a = AudioAdapter::new(AudioFormat::StrudelJson);
176 let ex = ExtractedRefrain { refrain: &r };
177 let bytes = a.emit(&ex, &EmitCtx::default()).unwrap();
178 let parsed: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
179 assert_eq!(parsed.as_array().unwrap().len(), 0);
180 }
181
182 #[test]
183 fn name_reflects_format() {
184 assert_eq!(
185 AudioAdapter::new(AudioFormat::StrudelJson).name(),
186 "audio.strudel-json"
187 );
188 assert_eq!(AudioAdapter::new(AudioFormat::Osc).name(), "audio.osc");
189 }
190}