use serde::{Deserialize, Serialize};
use crate::diag::Diagnostic;
use crate::dsl::{SeqWave, SoundDoc};
use crate::ids::TrackId;
use crate::render;
use crate::streaming::StreamGraph;
pub const PROGRAM_VERSION: u32 = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Program {
pub program_version: u32,
pub schema_version: u32,
pub engine_version: u32,
pub hash: u64,
#[serde(default)]
pub target: crate::song::CompileTarget,
pub doc: SoundDoc,
pub meta: ProgramMeta,
pub estimates: ResourceEstimates,
#[serde(skip)]
pub warnings: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProgramMeta {
pub name: String,
pub tempo_bpm: f32,
pub beats_per_bar: u32,
pub steps_per_beat: u32,
#[serde(default)]
pub tempo_map: Vec<crate::dsl::TempoPoint>,
#[serde(default)]
pub meter_map: Vec<crate::song::MeterPoint>,
#[serde(default)]
pub pickup: Option<crate::units::Beat>,
#[serde(default)]
pub sections: Vec<crate::song::Section>,
#[serde(default)]
pub markers: Vec<crate::song::Marker>,
pub length_bars: u32,
pub duration_secs: f32,
pub duration_frames: u64,
pub sample_rate: u32,
pub tracks: Vec<TrackMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackMeta {
pub id: TrackId,
pub name: String,
pub wave: SeqWave,
pub notes: u32,
pub mute: bool,
pub solo: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceEstimates {
pub frames: u64,
pub events: u64,
pub peak_voices: u32,
pub memory_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProgramError {
Json(String),
TooNew {
found: u32,
supported: u32,
},
HashMismatch {
stored: u64,
computed: u64,
},
}
impl std::fmt::Display for ProgramError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProgramError::Json(e) => write!(f, "program JSON: {e}"),
ProgramError::TooNew { found, supported } => write!(
f,
"T3001: program version {found} is newer than this binary supports ({supported})"
),
ProgramError::HashMismatch { stored, computed } => write!(
f,
"T3002: program hash mismatch (stored {stored:#018x}, computed {computed:#018x}) — \
the bundle was edited or corrupted; recompile the song"
),
}
}
}
impl std::error::Error for ProgramError {}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xCBF2_9CE4_8422_2325;
for b in bytes {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01B3);
}
h
}
fn canonical_json<T: Serialize>(value: &T) -> Vec<u8> {
let value = serde_json::to_value(value).expect("canonical content serializes");
serde_json::to_string(&value)
.expect("a resolved document serializes")
.into_bytes()
}
pub fn content_hash(doc: &SoundDoc) -> u64 {
fnv1a(&canonical_json(doc))
}
impl Program {
pub(crate) fn computed_hash(&self) -> u64 {
if self.program_version <= 1 {
return content_hash(&self.doc);
}
let mut value = serde_json::to_value(self).expect("a program serializes");
value
.as_object_mut()
.expect("a program serializes as an object")
.remove("hash");
fnv1a(&canonical_json(&value))
}
pub fn render_mono(&self) -> Vec<f32> {
render::render(&self.doc)
}
pub fn render_stereo(&self) -> (Vec<f32>, Vec<f32>) {
let product = render::render_product(&self.doc);
product.stereo.unwrap_or_else(|| {
let m = product.mono;
(m.clone(), m)
})
}
pub fn render_range_frames(&self, start: u64, end: u64) -> (Vec<f32>, Vec<f32>) {
let (l, r) = self.render_stereo();
let start = (start as usize).min(l.len());
let end = (end as usize).min(l.len()).max(start);
(l[start..end].to_vec(), r[start..end].to_vec())
}
pub fn render_range_bars(&self, start_bar: u32, end_bar: u32) -> (Vec<f32>, Vec<f32>) {
let transport = crate::runtime::Transport::for_program(&self.meta);
self.render_range_frames(
transport.frame_at_bar(start_bar),
transport.frame_at_bar(end_bar),
)
}
pub fn render_stems(&self) -> Vec<render::Stem> {
render::render_stems(&self.doc).unwrap_or_default()
}
pub fn is_streamable(&self) -> bool {
self.warnings.is_empty()
}
pub fn capabilities(&self) -> Vec<&'static str> {
let mut caps = vec!["offline-render", "stems"];
if self.is_streamable() {
caps.push("streaming");
}
caps
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("a program serializes")
}
pub fn from_json(json: &str) -> Result<Program, ProgramError> {
let mut program: Program =
serde_json::from_str(json).map_err(|e| ProgramError::Json(e.to_string()))?;
if program.program_version > PROGRAM_VERSION {
return Err(ProgramError::TooNew {
found: program.program_version,
supported: PROGRAM_VERSION,
});
}
let computed = program.computed_hash();
if computed != program.hash {
return Err(ProgramError::HashMismatch {
stored: program.hash,
computed,
});
}
program.warnings = blocker_warnings(&program.doc);
Ok(program)
}
}
pub(crate) fn blocker_warnings(doc: &SoundDoc) -> Vec<Diagnostic> {
fn code(b: &crate::streaming::StreamBlocker) -> &'static str {
use crate::streaming::StreamBlocker as B;
match b {
B::Normalize => "T1501",
B::LoopPlayback => "T1502",
B::StereoTreatment => "T1503",
B::TracksRoot => "T1504",
B::LegacyRng { .. } => "T1505",
B::Sampler => "T1506",
B::ModulatedFilter => "T1507",
B::OfflineEffect { .. } => "T1508",
B::TracksPart { cause, .. } => code(cause),
}
}
StreamGraph::blockers(doc)
.into_iter()
.map(|b| {
Diagnostic::warning(code(&b), "doc", b.to_string()).with_remediation(
"the offline render is unaffected; live playback uses the buffer-backed Player",
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::song::{CompileOptions, Song, note};
fn two_track_program() -> Program {
let mut song = Song::new("prog", 120.0);
song.add_track(
"bass",
crate::dsl::SeqWave::Bass,
crate::dsl::Adsr {
a: 0.005,
d: 0.1,
s: 0.8,
r: 0.2,
punch: 0.0,
},
);
song.add_pattern("riff", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
song.arrange("bass", "riff", 0);
song.compile(&CompileOptions::default()).expect("compiles")
}
#[test]
fn hash_is_canonical_regardless_of_field_order() {
let program = two_track_program();
let json = serde_json::to_string(&program.doc).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
let obj = value.as_object_mut().unwrap();
let mut entries: Vec<_> = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
entries.reverse();
*obj = entries.into_iter().collect();
let reparsed: SoundDoc = serde_json::from_value(value).unwrap();
assert_eq!(content_hash(&reparsed), content_hash(&program.doc));
assert_eq!(program.computed_hash(), program.hash);
}
#[test]
fn program_round_trips_through_json() {
let program = two_track_program();
let loaded = Program::from_json(&program.to_json()).expect("loads");
assert_eq!(loaded.hash, program.hash);
assert_eq!(loaded.warnings.len(), program.warnings.len());
assert_eq!(loaded.target, program.target);
assert_eq!(loaded.render_mono(), program.render_mono());
assert!(loaded.capabilities().contains(&"streaming"));
}
#[test]
fn render_range_is_a_slice_of_the_full_render() {
let program = two_track_program();
let (l, r) = program.render_stereo();
let (sl, sr) = program.render_range_frames(1000, 5000);
assert_eq!(sl, l[1000..5000].to_vec());
assert_eq!(sr, r[1000..5000].to_vec());
let (bl, _) = program.render_range_bars(0, 1);
let transport = crate::runtime::Transport::for_program(&program.meta);
assert_eq!(bl.len(), transport.frame_at_bar(1) as usize);
let (cl, _) = program.render_range_frames(u64::MAX - 1, u64::MAX);
assert!(cl.is_empty());
}
#[test]
fn from_json_rejects_a_newer_revision() {
let program = two_track_program();
let mut value: serde_json::Value = serde_json::from_str(&program.to_json()).unwrap();
value["program_version"] = serde_json::json!(PROGRAM_VERSION + 1);
let err = Program::from_json(&serde_json::to_string(&value).unwrap()).unwrap_err();
assert_eq!(
err,
ProgramError::TooNew {
found: PROGRAM_VERSION + 1,
supported: PROGRAM_VERSION,
}
);
}
#[test]
fn from_json_catches_a_hand_edited_bundle() {
let program = two_track_program();
let mut value: serde_json::Value = serde_json::from_str(&program.to_json()).unwrap();
value["doc"]["duration"] = serde_json::json!(9.0);
let err = Program::from_json(&serde_json::to_string(&value).unwrap()).unwrap_err();
assert!(matches!(err, ProgramError::HashMismatch { .. }));
let mut value: serde_json::Value = serde_json::from_str(&program.to_json()).unwrap();
value["meta"]["sample_rate"] = serde_json::json!(48_000);
let err = Program::from_json(&serde_json::to_string(&value).unwrap()).unwrap_err();
assert!(
matches!(err, ProgramError::HashMismatch { .. }),
"runtime metadata is part of a v2 bundle's integrity boundary"
);
}
#[test]
fn estimates_bound_the_render() {
let program = two_track_program();
assert_eq!(program.estimates.events, 2);
assert_eq!(program.estimates.peak_voices, 1);
assert_eq!(
program.estimates.frames,
(program.doc.duration * program.doc.sample_rate as f32).round() as u64
);
assert_eq!(program.estimates.memory_bytes, program.estimates.frames * 8);
}
}