Skip to main content

refrain_adapters/
audio.rs

1//! Audio adapter: emits either Strudel-style Hap JSON or OSC bundle bytes.
2//!
3//! Strudel format reference: <https://strudel.tidalcycles.org/>
4//! OSC format reference: <https://opensoundcontrol.stanford.edu/spec-1_0.html>
5
6use 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        // OSC bundles begin with "#bundle\0".
160        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}