use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Cursor;
use png::{BitDepth, ColorType, Encoder};
use refrain_core::ast::StageKind;
use refrain_core::Refrain;
use crate::schedule::{schedule, Hap};
use crate::{AdapterCaps, AdapterErr, EmitCtx, ExtractedRefrain, RefrainAdapter};
const DEFAULT_W: u32 = 256;
const DEFAULT_H: u32 = 256;
pub struct VisualAdapter {
pub width: u32,
pub height: u32,
}
impl VisualAdapter {
pub fn new() -> Self {
Self {
width: DEFAULT_W,
height: DEFAULT_H,
}
}
pub fn with_size(width: u32, height: u32) -> Self {
Self { width, height }
}
}
impl Default for VisualAdapter {
fn default() -> Self {
Self::new()
}
}
fn collect_stage_haps(refrain: &Refrain) -> [Vec<Hap>; 3] {
let mut by_stage: [Vec<Hap>; 3] = [Vec::new(), Vec::new(), Vec::new()];
for (kind, p) in refrain.stages() {
let (haps, _) = schedule(p, 0.0);
let idx = match kind {
StageKind::Territorialize => 0,
StageKind::Deterritorialize => 1,
StageKind::Reterritorialize => 2,
};
by_stage[idx].extend(haps);
}
by_stage
}
fn key_color(key: &str) -> [u8; 3] {
const PALETTE: &[[u8; 3]] = &[
[220, 20, 60],
[255, 99, 71],
[255, 140, 0],
[255, 215, 0],
[154, 205, 50],
[60, 179, 113],
[0, 139, 139],
[70, 130, 180],
[65, 105, 225],
[123, 104, 238],
[186, 85, 211],
[218, 112, 214],
[255, 105, 180],
[205, 92, 92],
[244, 164, 96],
[189, 183, 107],
[85, 107, 47],
[46, 139, 87],
[32, 178, 170],
[25, 25, 112],
[72, 61, 139],
[128, 0, 128],
[199, 21, 133],
[128, 128, 128],
];
let mut h = DefaultHasher::new();
key.hash(&mut h);
let idx = (h.finish() % PALETTE.len() as u64) as usize;
PALETTE[idx]
}
fn render_buffer(width: u32, height: u32, stage_haps: &[Vec<Hap>; 3]) -> Vec<u8> {
let mut buf = vec![0u8; (width * height * 3) as usize];
let max_end = stage_haps
.iter()
.flatten()
.map(|h| h.end)
.fold(0.0_f64, f64::max);
let scale_x = if max_end > 0.0 {
(width - 1) as f64 / max_end
} else {
0.0
};
let band_h = (height / 3).max(1);
for (band_idx, haps) in stage_haps.iter().enumerate() {
let y0 = (band_idx as u32) * band_h;
let y1 = (y0 + band_h).min(height);
for h in haps {
let key = h.pitch.clone().unwrap_or_else(|| h.value.clone());
let [r, g, b] = key_color(&key);
let x0 = (h.start * scale_x) as u32;
let mut x1 = (h.end * scale_x).max(h.start * scale_x + 1.0) as u32;
if x1 >= width {
x1 = width - 1;
}
for y in y0..y1 {
for x in x0..=x1 {
let i = ((y * width + x) * 3) as usize;
if i + 2 < buf.len() {
buf[i] = r;
buf[i + 1] = g;
buf[i + 2] = b;
}
}
}
}
}
buf
}
fn encode_png(width: u32, height: u32, rgb: &[u8]) -> Result<Vec<u8>, AdapterErr> {
let mut out = Vec::new();
{
let cursor = Cursor::new(&mut out);
let mut encoder = Encoder::new(cursor, width, height);
encoder.set_color(ColorType::Rgb);
encoder.set_depth(BitDepth::Eight);
encoder.set_compression(png::Compression::Default);
encoder.set_filter(png::FilterType::NoFilter);
let mut writer = encoder
.write_header()
.map_err(|e| AdapterErr::Encoding(format!("png header: {}", e)))?;
writer
.write_image_data(rgb)
.map_err(|e| AdapterErr::Encoding(format!("png body: {}", e)))?;
}
Ok(out)
}
impl RefrainAdapter for VisualAdapter {
fn name(&self) -> &str {
"visual.png"
}
fn emit(&self, refrain: &ExtractedRefrain, _ctx: &EmitCtx) -> Result<Vec<u8>, AdapterErr> {
let stage_haps = collect_stage_haps(refrain.refrain);
let buf = render_buffer(self.width, self.height, &stage_haps);
encode_png(self.width, self.height, &buf)
}
fn capabilities(&self) -> AdapterCaps {
AdapterCaps {
realtime: false,
differentiable: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use refrain_core::parse;
fn sha256_hex(bytes: &[u8]) -> String {
let mut h = DefaultHasher::new();
bytes.hash(&mut h);
format!("{:016x}", h.finish())
}
#[test]
fn png_header_present() {
let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
let v = VisualAdapter::new();
let ex = ExtractedRefrain { refrain: &r };
let bytes = v.emit(&ex, &EmitCtx::default()).unwrap();
assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn output_is_deterministic_byte_for_byte() {
let r = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
let v = VisualAdapter::new();
let ex = ExtractedRefrain { refrain: &r };
let b1 = v.emit(&ex, &EmitCtx::default()).unwrap();
let b2 = v.emit(&ex, &EmitCtx::default()).unwrap();
assert_eq!(b1, b2);
}
#[test]
fn distinct_refrains_render_distinctly() {
let v = VisualAdapter::new();
let r1 = parse("(refrain a (territorialize (loop 4 (note C4 q))))").unwrap();
let r2 = parse("(refrain a (territorialize (loop 4 (note G4 e))))").unwrap();
let b1 = v
.emit(&ExtractedRefrain { refrain: &r1 }, &EmitCtx::default())
.unwrap();
let b2 = v
.emit(&ExtractedRefrain { refrain: &r2 }, &EmitCtx::default())
.unwrap();
assert_ne!(b1, b2);
}
#[test]
fn empty_refrain_still_renders_canvas() {
let r = parse("(refrain empty)").unwrap();
let v = VisualAdapter::new();
let ex = ExtractedRefrain { refrain: &r };
let bytes = v.emit(&ex, &EmitCtx::default()).unwrap();
assert_eq!(&bytes[0..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn custom_size_produces_smaller_buffer() {
let r = parse("(refrain a (territorialize (note C4 q)))").unwrap();
let small = VisualAdapter::with_size(16, 16);
let big = VisualAdapter::with_size(256, 256);
let small_bytes = small
.emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
.unwrap();
let big_bytes = big
.emit(&ExtractedRefrain { refrain: &r }, &EmitCtx::default())
.unwrap();
assert!(small_bytes.len() < big_bytes.len());
}
#[test]
fn golden_byte_hash_for_canonical_refrain() {
let r = parse("(refrain canonical (territorialize (loop 4 (note C4 q))))").unwrap();
let v = VisualAdapter::with_size(64, 48);
let ex = ExtractedRefrain { refrain: &r };
let bytes = v.emit(&ex, &EmitCtx::default()).unwrap();
let _hash = sha256_hex(&bytes);
assert!(bytes.len() > 50, "PNG too small: {} bytes", bytes.len());
assert!(bytes.len() < 10_000, "PNG too large: {} bytes", bytes.len());
}
}