1use std::collections::hash_map::DefaultHasher;
13use std::hash::{Hash, Hasher};
14use std::io::Cursor;
15
16use png::{BitDepth, ColorType, Encoder};
17
18use refrain_core::ast::StageKind;
19use refrain_core::Refrain;
20
21use crate::schedule::{schedule, Hap};
22use crate::{AdapterCaps, AdapterErr, EmitCtx, ExtractedRefrain, RefrainAdapter};
23
24const DEFAULT_W: u32 = 256;
25const DEFAULT_H: u32 = 256;
26
27pub struct VisualAdapter {
28 pub width: u32,
29 pub height: u32,
30}
31
32impl VisualAdapter {
33 pub fn new() -> Self {
34 Self {
35 width: DEFAULT_W,
36 height: DEFAULT_H,
37 }
38 }
39
40 pub fn with_size(width: u32, height: u32) -> Self {
41 Self { width, height }
42 }
43}
44
45impl Default for VisualAdapter {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51fn collect_stage_haps(refrain: &Refrain) -> [Vec<Hap>; 3] {
52 let mut by_stage: [Vec<Hap>; 3] = [Vec::new(), Vec::new(), Vec::new()];
53 for (kind, p) in refrain.stages() {
54 let (haps, _) = schedule(p, 0.0);
55 let idx = match kind {
56 StageKind::Territorialize => 0,
57 StageKind::Deterritorialize => 1,
58 StageKind::Reterritorialize => 2,
59 };
60 by_stage[idx].extend(haps);
61 }
62 by_stage
63}
64
65fn key_color(key: &str) -> [u8; 3] {
66 const PALETTE: &[[u8; 3]] = &[
68 [220, 20, 60],
69 [255, 99, 71],
70 [255, 140, 0],
71 [255, 215, 0],
72 [154, 205, 50],
73 [60, 179, 113],
74 [0, 139, 139],
75 [70, 130, 180],
76 [65, 105, 225],
77 [123, 104, 238],
78 [186, 85, 211],
79 [218, 112, 214],
80 [255, 105, 180],
81 [205, 92, 92],
82 [244, 164, 96],
83 [189, 183, 107],
84 [85, 107, 47],
85 [46, 139, 87],
86 [32, 178, 170],
87 [25, 25, 112],
88 [72, 61, 139],
89 [128, 0, 128],
90 [199, 21, 133],
91 [128, 128, 128],
92 ];
93 let mut h = DefaultHasher::new();
94 key.hash(&mut h);
95 let idx = (h.finish() % PALETTE.len() as u64) as usize;
96 PALETTE[idx]
97}
98
99fn render_buffer(width: u32, height: u32, stage_haps: &[Vec<Hap>; 3]) -> Vec<u8> {
100 let mut buf = vec![0u8; (width * height * 3) as usize];
101
102 let max_end = stage_haps
104 .iter()
105 .flatten()
106 .map(|h| h.end)
107 .fold(0.0_f64, f64::max);
108 let scale_x = if max_end > 0.0 {
109 (width - 1) as f64 / max_end
110 } else {
111 0.0
112 };
113
114 let band_h = (height / 3).max(1);
115
116 for (band_idx, haps) in stage_haps.iter().enumerate() {
117 let y0 = (band_idx as u32) * band_h;
118 let y1 = (y0 + band_h).min(height);
119 for h in haps {
120 let key = h.pitch.clone().unwrap_or_else(|| h.value.clone());
121 let [r, g, b] = key_color(&key);
122 let x0 = (h.start * scale_x) as u32;
123 let mut x1 = (h.end * scale_x).max(h.start * scale_x + 1.0) as u32;
124 if x1 >= width {
125 x1 = width - 1;
126 }
127 for y in y0..y1 {
128 for x in x0..=x1 {
129 let i = ((y * width + x) * 3) as usize;
130 if i + 2 < buf.len() {
131 buf[i] = r;
132 buf[i + 1] = g;
133 buf[i + 2] = b;
134 }
135 }
136 }
137 }
138 }
139 buf
140}
141
142fn encode_png(width: u32, height: u32, rgb: &[u8]) -> Result<Vec<u8>, AdapterErr> {
143 let mut out = Vec::new();
144 {
145 let cursor = Cursor::new(&mut out);
146 let mut encoder = Encoder::new(cursor, width, height);
147 encoder.set_color(ColorType::Rgb);
148 encoder.set_depth(BitDepth::Eight);
149 encoder.set_compression(png::Compression::Default);
151 encoder.set_filter(png::FilterType::NoFilter);
152 let mut writer = encoder
153 .write_header()
154 .map_err(|e| AdapterErr::Encoding(format!("png header: {}", e)))?;
155 writer
156 .write_image_data(rgb)
157 .map_err(|e| AdapterErr::Encoding(format!("png body: {}", e)))?;
158 }
159 Ok(out)
160}
161
162impl RefrainAdapter for VisualAdapter {
163 fn name(&self) -> &str {
164 "visual.png"
165 }
166
167 fn emit(&self, refrain: &ExtractedRefrain, _ctx: &EmitCtx) -> Result<Vec<u8>, AdapterErr> {
168 let stage_haps = collect_stage_haps(refrain.refrain);
169 let buf = render_buffer(self.width, self.height, &stage_haps);
170 encode_png(self.width, self.height, &buf)
171 }
172
173 fn capabilities(&self) -> AdapterCaps {
174 AdapterCaps {
175 realtime: false,
176 differentiable: false,
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use refrain_core::parse;
185
186 fn sha256_hex(bytes: &[u8]) -> String {
187 let mut h = DefaultHasher::new();
192 bytes.hash(&mut h);
193 format!("{:016x}", h.finish())
194 }
195
196 #[test]
197 fn png_header_present() {
198 let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
199 let v = VisualAdapter::new();
200 let ex = ExtractedRefrain { refrain: &r };
201 let bytes = v.emit(&ex, &EmitCtx::default()).unwrap();
202 assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
204 }
205
206 #[test]
207 fn output_is_deterministic_byte_for_byte() {
208 let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
209 let v = VisualAdapter::new();
210 let ex = ExtractedRefrain { refrain: &r };
211 let b1 = v.emit(&ex, &EmitCtx::default()).unwrap();
212 let b2 = v.emit(&ex, &EmitCtx::default()).unwrap();
213 assert_eq!(b1, b2);
214 }
215
216 #[test]
217 fn distinct_refrains_render_distinctly() {
218 let v = VisualAdapter::new();
219 let r1 = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
220 let r2 = parse("(refrain a (territorialize (loop 4 (note G4 e))))").unwrap();
221 let b1 = v
222 .emit(&ExtractedRefrain { refrain: &r1 }, &EmitCtx::default())
223 .unwrap();
224 let b2 = v
225 .emit(&ExtractedRefrain { refrain: &r2 }, &EmitCtx::default())
226 .unwrap();
227 assert_ne!(b1, b2);
228 }
229
230 #[test]
231 fn empty_refrain_still_renders_canvas() {
232 let r = parse("(refrain empty)").unwrap();
233 let v = VisualAdapter::new();
234 let ex = ExtractedRefrain { refrain: &r };
235 let bytes = v.emit(&ex, &EmitCtx::default()).unwrap();
236 assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
237 }
238
239 #[test]
240 fn custom_size_produces_smaller_buffer() {
241 let r = parse("(refrain a (territorialize (note C4 q)))").unwrap();
242 let small = VisualAdapter::with_size(16, 16);
243 let big = VisualAdapter::with_size(256, 256);
244 let small_bytes = small
245 .emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
246 .unwrap();
247 let big_bytes = big
248 .emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
249 .unwrap();
250 assert!(small_bytes.len() < big_bytes.len());
251 }
252
253 #[test]
254 fn golden_byte_hash_for_canonical_refrain() {
255 let r = parse("(refrain canonical (territorialize (loop 4 (note C4 q))))").unwrap();
256 let v = VisualAdapter::with_size(64, 48);
257 let ex = ExtractedRefrain { refrain: &r };
258 let bytes = v.emit(&ex, &EmitCtx::default()).unwrap();
259 let _hash = sha256_hex(&bytes);
260 assert!(bytes.len() > 50, "PNG too small: {} bytes", bytes.len());
264 assert!(bytes.len() < 10_000, "PNG too large: {} bytes", bytes.len());
265 }
266}