1use refrain_core::Refrain;
11
12use crate::schedule::{schedule, Hap};
13use crate::{AdapterCaps, AdapterErr, EmitCtx, ExtractedRefrain, RefrainAdapter};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum TextStyle {
17 Prose,
18 Bullets,
19}
20
21pub struct TextAdapter {
22 pub style: TextStyle,
23 pub seed: u64,
24}
25
26impl TextAdapter {
27 pub fn new(style: TextStyle) -> Self {
28 Self { style, seed: 0 }
29 }
30
31 pub fn with_seed(style: TextStyle, seed: u64) -> Self {
32 Self { style, seed }
33 }
34}
35
36fn collect_haps(refrain: &Refrain) -> Vec<Hap> {
37 let mut all = Vec::new();
38 let mut t = 0.0;
39 for (_kind, p) in refrain.stages() {
40 let (sub, dur) = schedule(p, t);
41 all.extend(sub);
42 t += dur;
43 }
44 all
45}
46
47fn lcg(state: &mut u64) -> u64 {
49 *state = state
50 .wrapping_mul(6364136223846793005)
51 .wrapping_add(1442695040888963407);
52 *state
53}
54
55const VERBS: &[&str] = &["sings", "chants", "intones", "voices", "speaks"];
56const SUFFIXES: &[&str] = &[".", " in measured time.", " over the refrain.", " quietly."];
57
58fn render_hap(h: &Hap, state: &mut u64, style: TextStyle) -> String {
59 let verb = VERBS[(lcg(state) as usize) % VERBS.len()];
60 let suffix = SUFFIXES[(lcg(state) as usize) % SUFFIXES.len()];
61 match style {
62 TextStyle::Prose => match &h.pitch {
63 Some(p) => format!(
64 "At cycle {:.4}, the voice {} {} for {:.4} cycles{}",
65 h.start,
66 verb,
67 p,
68 h.duration(),
69 suffix
70 ),
71 None => format!(
72 "At cycle {:.4}, a structural mark: {}{}",
73 h.start, h.value, suffix
74 ),
75 },
76 TextStyle::Bullets => match &h.pitch {
77 Some(p) => format!("- t={:.4} {} {} dur={:.4}", h.start, verb, p, h.duration()),
78 None => format!("- t={:.4} mark={}", h.start, h.value),
79 },
80 }
81}
82
83impl RefrainAdapter for TextAdapter {
84 fn name(&self) -> &str {
85 match self.style {
86 TextStyle::Prose => "text.prose",
87 TextStyle::Bullets => "text.bullets",
88 }
89 }
90
91 fn emit(&self, refrain: &ExtractedRefrain, _ctx: &EmitCtx) -> Result<Vec<u8>, AdapterErr> {
92 let haps = collect_haps(refrain.refrain);
93 let mut state = self.seed.wrapping_add(0x9E3779B97F4A7C15); let mut out = String::new();
95 out.push_str("# Refrain: ");
96 out.push_str(&refrain.refrain.name);
97 out.push('\n');
98 for h in &haps {
99 out.push_str(&render_hap(h, &mut state, self.style));
100 out.push('\n');
101 }
102 Ok(out.into_bytes())
103 }
104
105 fn capabilities(&self) -> AdapterCaps {
106 AdapterCaps {
107 realtime: false,
108 differentiable: false,
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use refrain_core::parse;
117
118 #[test]
119 fn prose_contains_pitch_lines() {
120 let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
121 let a = TextAdapter::new(TextStyle::Prose);
122 let s = String::from_utf8(
123 a.emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
124 .unwrap(),
125 )
126 .unwrap();
127 assert!(s.contains("# Refrain: a"));
128 assert_eq!(s.matches("C4").count(), 4);
129 }
130
131 #[test]
132 fn bullets_uses_dash_prefix() {
133 let r = parse("(refrain b (territorialize (note G4 e)))").unwrap();
134 let a = TextAdapter::new(TextStyle::Bullets);
135 let s = String::from_utf8(
136 a.emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
137 .unwrap(),
138 )
139 .unwrap();
140 assert!(s.contains("- t="));
141 assert!(s.contains("G4"));
142 }
143
144 #[test]
145 fn deterministic_for_same_seed() {
146 let r = parse("(refrain c (territorialize (loop 4 (note C4 q))))").unwrap();
147 let a = TextAdapter::with_seed(TextStyle::Prose, 42);
148 let s1 = a
149 .emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
150 .unwrap();
151 let s2 = a
152 .emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
153 .unwrap();
154 assert_eq!(s1, s2);
155 }
156
157 #[test]
158 fn different_seeds_diverge() {
159 let r = parse("(refrain c (territorialize (loop 4 (note C4 q))))").unwrap();
160 let a = TextAdapter::with_seed(TextStyle::Prose, 1);
161 let b = TextAdapter::with_seed(TextStyle::Prose, 2);
162 let sa = a
163 .emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
164 .unwrap();
165 let sb = b
166 .emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
167 .unwrap();
168 assert_ne!(sa, sb);
169 }
170
171 #[test]
172 fn empty_refrain_emits_just_header() {
173 let r = parse("(refrain e)").unwrap();
174 let a = TextAdapter::new(TextStyle::Prose);
175 let s = String::from_utf8(
176 a.emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
177 .unwrap(),
178 )
179 .unwrap();
180 assert_eq!(s, "# Refrain: e\n");
181 }
182
183 #[test]
184 fn names_distinguish_styles() {
185 assert_eq!(TextAdapter::new(TextStyle::Prose).name(), "text.prose");
186 assert_eq!(TextAdapter::new(TextStyle::Bullets).name(), "text.bullets");
187 }
188}