1use super::{Song, SongError, SongTrack};
7use crate::diag::{CompileError, Diagnostic};
8use crate::dsl::{ENGINE_VERSION, Node, SeqNote, SoundDoc, Track};
9use crate::ids::TrackId;
10use crate::program::{PROGRAM_VERSION, Program, ProgramMeta, TrackMeta, blocker_warnings};
11use crate::units::Beat;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum CompileTarget {
20 #[default]
22 Offline,
23 Runtime,
25}
26
27#[derive(Debug, Clone, Default)]
30pub struct CompileOptions {
31 pub sample_rate: Option<u32>,
34 pub target: CompileTarget,
36}
37
38const SEND_ROOM: f32 = 0.6;
40const SEND_MIX_MAX: f32 = 0.5;
42
43pub(crate) fn note_end(n: &SeqNote) -> u32 {
48 n.step.saturating_add(n.len.max(1))
49}
50
51impl Song {
52 pub fn length_bars(&self) -> u32 {
55 let steps_per_bar = self.steps_per_bar();
56 let from_patterns = self
57 .arrangement
58 .iter()
59 .map(|pl| {
60 let bars = self
61 .patterns
62 .iter()
63 .find(|p| p.name == pl.pattern)
64 .map(|p| p.bars)
65 .unwrap_or(0);
66 pl.bar.saturating_add(bars)
67 })
68 .max()
69 .unwrap_or(0);
70 let from_notes = if self.plain_meter() {
71 self.tracks
72 .iter()
73 .flat_map(|t| t.notes.iter())
74 .map(|n| note_end(n).div_ceil(steps_per_bar))
75 .max()
76 .unwrap_or(0)
77 } else {
78 self.tracks
79 .iter()
80 .flat_map(|t| t.notes.iter())
81 .map(|n| {
82 let beat = Beat::new(note_end(n) as i64, self.steps_per_beat.max(1));
83 self.bar_count_at_beat(beat)
84 })
85 .max()
86 .unwrap_or(0)
87 };
88 from_patterns.max(from_notes)
89 }
90
91 fn bar_count_at_beat(&self, beat: Beat) -> u32 {
94 crate::units::bar_count_at_beat(&self.meter_map, self.beats_per_bar, self.pickup, beat)
95 }
96
97 fn steps_per_bar(&self) -> u32 {
101 self.beats_per_bar
102 .max(1)
103 .saturating_mul(self.steps_per_beat.max(1))
104 }
105
106 fn checked_steps_per_bar(&self) -> Result<u32, SongError> {
107 self.beats_per_bar
108 .max(1)
109 .checked_mul(self.steps_per_beat.max(1))
110 .ok_or_else(|| {
111 SongError::Compile(format!(
112 "beats_per_bar ({}) × steps_per_beat ({}) exceeds the u32 song grid",
113 self.beats_per_bar, self.steps_per_beat
114 ))
115 })
116 }
117
118 pub(crate) fn plain_meter(&self) -> bool {
121 self.meter_map.is_empty() && self.pickup.is_none()
122 }
123
124 pub fn beat_at_bar(&self, bar: u32) -> Beat {
128 crate::units::beat_at_bar(&self.meter_map, self.beats_per_bar, self.pickup, bar)
129 }
130
131 pub(crate) fn beat_to_step(&self, beat: Beat) -> Result<u32, SongError> {
134 let spb = self.steps_per_beat.max(1) as i128;
135 let num = beat.num as i128 * spb;
136 if num % beat.den as i128 != 0 {
137 return Err(SongError::Compile(format!(
138 "a placement at beat {beat} doesn't land on the {}-steps-per-beat \
139 grid — change steps_per_beat or the meter map/pickup",
140 self.steps_per_beat.max(1)
141 )));
142 }
143 Ok(u32::try_from((num / beat.den as i128).max(0)).unwrap_or(u32::MAX))
144 }
145
146 pub fn to_doc(&self) -> Result<crate::dsl::SoundDoc, SongError> {
150 if self.tracks.is_empty() {
151 return Err(SongError::Empty);
152 }
153 for pl in &self.arrangement {
154 if !self.tracks.iter().any(|t| t.name == pl.track) {
155 return Err(SongError::UnknownTrack(pl.track.clone()));
156 }
157 if !self.patterns.iter().any(|p| p.name == pl.pattern) {
158 return Err(SongError::UnknownPattern(pl.pattern.clone()));
159 }
160 }
161 self.checked_steps_per_bar()?;
162
163 let sec_per_step = 60.0 / (self.bpm.max(1.0) * self.steps_per_beat.max(1) as f32);
164 let any_solo = self.tracks.iter().any(|t| t.solo);
165 let mut end_step = 0u32;
166 let mut doc_tracks = Vec::with_capacity(self.tracks.len());
167 for t in &self.tracks {
168 doc_tracks.push(self.compile_track(t, &mut end_step, any_solo)?);
169 }
170
171 let duration = if self.tempo_map.is_empty() {
174 end_step as f32 * sec_per_step + 2.0 } else {
176 let end_beat = end_step as f64 / self.steps_per_beat.max(1) as f64;
177 crate::dsl::tempo_map_seconds_at(&self.tempo_map, end_beat) as f32 + 2.0
178 };
179 let root = Node::Tracks {
180 tracks: doc_tracks,
181 master: self.master.clone(),
182 buses: self.buses.clone(),
183 };
184 let mut json = serde_json::json!({
189 "name": self.name,
190 "duration": duration,
191 "engine": self.engine.unwrap_or(ENGINE_VERSION),
192 "root": serde_json::to_value(&root).map_err(|e| SongError::Compile(e.to_string()))?,
193 });
194 if let Some(v) = self.version {
195 json["version"] = serde_json::json!(v);
196 }
197 let doc: crate::dsl::SoundDoc = serde_json::from_value(json)
198 .map_err(|e| SongError::Compile(format!("song doc build: {e}")))?;
199 Ok(doc)
200 }
201
202 fn compile_track(
208 &self,
209 t: &SongTrack,
210 end_step: &mut u32,
211 any_solo: bool,
212 ) -> Result<Track, SongError> {
213 let steps_per_bar = self.steps_per_bar();
214 let mut notes: Vec<SeqNote> = t.notes.clone();
215 for n in ¬es {
216 *end_step = (*end_step).max(note_end(n));
217 }
218 for pl in self.arrangement.iter().filter(|p| p.track == t.name) {
219 let pat = self
220 .patterns
221 .iter()
222 .find(|p| p.name == pl.pattern)
223 .expect("pattern existence checked above");
224 let offset = if self.plain_meter() {
227 pl.bar.saturating_mul(steps_per_bar)
228 } else {
229 self.beat_to_step(self.beat_at_bar(pl.bar))?
230 };
231 for n in &pat.notes {
232 let placed = SeqNote {
233 step: n.step.saturating_add(offset),
234 len: n.len,
235 pitch: n.pitch.clone(),
236 gain: n.gain,
237 };
238 *end_step = (*end_step).max(note_end(&placed));
239 notes.push(placed);
240 }
241 }
242 notes.sort_by_key(|n| n.step);
243
244 let mut seq_json = serde_json::json!({
250 "type": "seq",
251 "bpm": self.bpm.max(1.0),
255 "steps_per_beat": self.steps_per_beat.max(1),
256 "wave": serde_json::to_value(t.wave).map_err(|e| SongError::Compile(e.to_string()))?,
257 "env": serde_json::to_value(t.env).map_err(|e| SongError::Compile(e.to_string()))?,
258 "swing": t.swing.unwrap_or(self.swing),
259 "humanize": t.humanize.unwrap_or(self.humanize),
260 "sf2": t.sf2,
261 "sf2_preset": t.sf2_preset,
262 "sf2_bank": t.sf2_bank,
263 "notes": serde_json::to_value(¬es).map_err(|e| SongError::Compile(e.to_string()))?,
264 });
265 if let serde_json::Value::Object(voice) =
266 serde_json::to_value(t.voice).map_err(|e| SongError::Compile(e.to_string()))?
267 {
268 for (key, val) in voice {
269 if !val.is_null() {
270 seq_json[key] = val;
271 }
272 }
273 }
274 if !self.tempo_map.is_empty() {
277 seq_json["tempo_map"] = serde_json::to_value(&self.tempo_map)
278 .map_err(|e| SongError::Compile(e.to_string()))?;
279 }
280 let seq: Node = serde_json::from_value(seq_json)
281 .map_err(|e| SongError::Compile(format!("track '{}' seq build: {e}", t.name)))?;
282
283 let node = if t.reverb > 0.0 {
286 let rv = t.reverb.clamp(0.0, 1.0);
287 Node::Chain {
288 stages: vec![
289 seq,
290 Node::Reverb {
291 room: SEND_ROOM,
292 mix: SEND_MIX_MAX * rv,
293 },
294 ],
295 }
296 } else {
297 seq
298 };
299 Ok(Track {
300 id: Some(t.name.clone()),
301 node,
302 pan: t.pan,
303 gain: t.gain,
304 at: 0.0,
305 mute: t.mute || (any_solo && !t.solo),
306 automation: t
307 .automation
308 .iter()
309 .map(|lane| self.compile_lane(lane))
310 .collect(),
311 sidechain: None,
312 bus: t.bus.clone(),
313 sends: t.sends.clone(),
314 })
315 }
316
317 fn compile_lane(&self, lane: &super::SongLane) -> crate::dsl::AutoLane {
320 crate::dsl::AutoLane {
321 target: lane.target,
322 curve: lane.curve,
323 points: lane
324 .points
325 .iter()
326 .map(|p| crate::dsl::AutoPoint {
327 t: self.seconds_at_beat(p.at),
328 v: p.v,
329 })
330 .collect(),
331 }
332 }
333
334 fn seconds_at_beat(&self, beat: f32) -> f32 {
336 if self.tempo_map.is_empty() {
337 beat * 60.0 / self.bpm.max(1.0)
338 } else {
339 crate::dsl::tempo_map_seconds_at(&self.tempo_map, beat as f64) as f32
340 }
341 }
342}
343
344impl Song {
345 pub fn compile(&self, opts: &CompileOptions) -> Result<Program, CompileError> {
369 let mut diags = CompileError::default();
372 if self.tracks.is_empty() {
373 diags.push(Diagnostic::from(&SongError::Empty));
374 }
375 for (i, pl) in self.arrangement.iter().enumerate() {
376 if !self.tracks.iter().any(|t| t.name == pl.track) {
377 let mut d = Diagnostic::from(&SongError::UnknownTrack(pl.track.clone()));
378 d.path = format!("arrangement[{i}].track");
379 diags.push(d);
380 }
381 if !self.patterns.iter().any(|p| p.name == pl.pattern) {
382 let mut d = Diagnostic::from(&SongError::UnknownPattern(pl.pattern.clone()));
383 d.path = format!("arrangement[{i}].pattern");
384 diags.push(d);
385 }
386 }
387 if diags.has_errors() {
388 return Err(diags);
389 }
390
391 self.validate_maps(&mut diags);
392 if diags.has_errors() {
393 return Err(diags);
394 }
395
396 let mut doc = match self.to_doc() {
397 Ok(doc) => doc,
398 Err(e) => {
399 diags.push(Diagnostic::from(&e));
400 return Err(diags);
401 }
402 };
403 if let Some(rate) = opts.sample_rate {
404 doc.sample_rate = rate;
405 }
406 if let Some(seed) = self.seed {
407 doc.seed = seed;
408 }
409 if let Err(e) = doc.validate() {
410 diags.push(
411 Diagnostic::error("T2000", "doc", e.to_string())
412 .with_remediation("fix the flagged document field and recompile"),
413 );
414 return Err(diags);
415 }
416
417 let warnings = blocker_warnings(&doc);
418 if opts.target == CompileTarget::Runtime && !warnings.is_empty() {
419 for mut blocker in warnings {
420 blocker.severity = crate::diag::Severity::Error;
421 diags.push(blocker);
422 }
423 return Err(diags);
424 }
425 let meta = self.program_meta(&doc);
426 let estimates = super::estimate::program_estimates(&doc);
427 let mut program = Program {
428 program_version: PROGRAM_VERSION,
429 schema_version: doc.effective_version(),
430 engine_version: doc.effective_engine(),
431 hash: 0,
432 target: opts.target,
433 doc,
434 meta,
435 estimates,
436 warnings,
437 };
438 program.hash = program.computed_hash();
439 Ok(program)
440 }
441
442 fn program_meta(&self, doc: &SoundDoc) -> ProgramMeta {
445 let mut sections = self.sections.clone();
446 sections.sort_by_key(|s| s.bar);
447 let mut markers = self.markers.clone();
448 markers.sort_by_key(|m| m.at);
449 ProgramMeta {
450 name: doc.name.clone(),
451 tempo_bpm: self.bpm.max(1.0),
452 beats_per_bar: self.beats_per_bar.max(1),
453 steps_per_beat: self.steps_per_beat.max(1),
454 tempo_map: self.tempo_map.clone(),
455 meter_map: self.meter_map.clone(),
456 pickup: self.pickup,
457 sections,
458 markers,
459 length_bars: self.length_bars(),
460 duration_secs: doc.duration,
461 duration_frames: super::estimate::duration_frames(doc),
462 sample_rate: doc.sample_rate,
463 tracks: self
464 .tracks
465 .iter()
466 .enumerate()
467 .map(|(i, t)| TrackMeta {
468 id: TrackId::from(i as u64 + 1),
469 name: t.name.clone(),
470 wave: t.wave,
471 notes: super::estimate::track_note_count(doc, i),
472 mute: t.mute,
473 solo: t.solo,
474 })
475 .collect(),
476 }
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use crate::dsl::{Adsr, SeqWave};
484 use crate::song::note;
485 use crate::units::MeterPoint;
486
487 fn amp() -> Adsr {
488 Adsr {
489 a: 0.005,
490 d: 0.1,
491 s: 0.8,
492 r: 0.2,
493 punch: 0.0,
494 }
495 }
496
497 fn demo_song() -> Song {
498 let mut song = Song::new("demo", 120.0);
499 song.add_track("bass", SeqWave::Bass, amp());
500 song.add_track("keys", SeqWave::Epiano, amp());
501 song.add_pattern("riff", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
502 song.add_pattern("stab", 1, vec![note(4, 2, "C4")]);
503 song.arrange("bass", "riff", 0);
504 song.arrange("keys", "stab", 0);
505 song
506 }
507
508 #[test]
509 fn compile_collects_every_unknown_reference_in_one_pass() {
510 let mut song = Song::new("s", 120.0);
511 song.add_track("t", SeqWave::Sine, amp());
512 song.arrange("nope", "ghost", 0);
513 song.arrange("alsono", "ghost2", 1);
514 let err = song.compile(&CompileOptions::default()).unwrap_err();
515 let codes: Vec<_> = err.0.iter().map(|d| (d.code, d.path.as_str())).collect();
516 assert_eq!(
517 codes,
518 vec![
519 ("T1001", "arrangement[0].track"),
520 ("T1002", "arrangement[0].pattern"),
521 ("T1001", "arrangement[1].track"),
522 ("T1002", "arrangement[1].pattern"),
523 ],
524 "every bad reference reported, with its exact path"
525 );
526 }
527
528 #[test]
529 fn compile_an_empty_song_is_t1000() {
530 let song = Song::new("s", 120.0);
531 let err = song.compile(&CompileOptions::default()).unwrap_err();
532 assert_eq!(err.0.len(), 1);
533 assert_eq!(err.0[0].code, "T1000");
534 }
535
536 #[test]
537 fn an_overflowing_deserialized_grid_is_a_compile_error() {
538 let mut song = demo_song();
539 song.beats_per_bar = u32::MAX;
540 song.steps_per_beat = u32::MAX;
541 let err = song.compile(&CompileOptions::default()).unwrap_err();
542 assert!(err.to_string().contains("exceeds the u32 song grid"));
543 }
544
545 #[test]
546 fn compile_is_deterministic() {
547 let a = demo_song().compile(&CompileOptions::default()).unwrap();
548 let b = demo_song().compile(&CompileOptions::default()).unwrap();
549 assert_eq!(a.hash, b.hash);
550 assert_eq!(a.render_mono(), b.render_mono());
551 }
552
553 #[test]
554 fn the_seed_stamps_the_doc_and_moves_the_hash() {
555 let plain = demo_song().compile(&CompileOptions::default()).unwrap();
556 assert_eq!(plain.doc.seed, 0);
557 let seeded = demo_song()
558 .with_seed(7)
559 .compile(&CompileOptions::default())
560 .unwrap();
561 assert_eq!(seeded.doc.seed, 7);
562 assert_ne!(plain.hash, seeded.hash, "the seed is part of the artifact");
563 let again = demo_song()
565 .with_seed(7)
566 .compile(&CompileOptions::default())
567 .unwrap();
568 assert_eq!(seeded.hash, again.hash);
569 }
570
571 #[test]
572 fn mute_and_solo_reach_the_mixer() {
573 let mut song = demo_song();
574 song.tracks[1].solo = true;
575 let program = song.compile(&CompileOptions::default()).unwrap();
576 let Node::Tracks { tracks, .. } = &program.doc.root else {
577 panic!("tracks root");
578 };
579 assert!(tracks[0].mute, "the non-solo track is muted");
580 assert!(!tracks[1].mute, "the solo track sounds");
581 let mut song = demo_song();
583 song.tracks[1].solo = true;
584 song.tracks[1].mute = true;
585 let program = song.compile(&CompileOptions::default()).unwrap();
586 let Node::Tracks { tracks, .. } = &program.doc.root else {
587 panic!("tracks root");
588 };
589 assert!(tracks[1].mute);
590 }
591
592 #[test]
593 fn compile_option_sample_rate_stamps_the_program() {
594 let program = demo_song()
595 .compile(&CompileOptions {
596 sample_rate: Some(48_000),
597 ..CompileOptions::default()
598 })
599 .unwrap();
600 assert_eq!(program.doc.sample_rate, 48_000);
601 assert_eq!(program.meta.sample_rate, 48_000);
602 }
603
604 #[test]
605 fn a_streamable_tracks_root_compiles_without_warnings() {
606 let program = demo_song().compile(&CompileOptions::default()).unwrap();
610 assert!(
611 program.is_streamable(),
612 "a plain compiled song streams natively now: {:?}",
613 program.warnings
614 );
615 assert!(program.warnings.is_empty());
616 }
617
618 #[test]
619 fn a_tracks_root_with_an_unstreamable_part_still_warns() {
620 let mut song = demo_song();
623 song.master.push(
624 serde_json::from_value(
625 serde_json::json!({ "type": "convolve", "decay": 0.6, "mix": 0.4 }),
626 )
627 .unwrap(),
628 );
629 let program = song.compile(&CompileOptions::default()).unwrap();
630 assert!(!program.is_streamable());
631 assert!(
632 program
633 .warnings
634 .iter()
635 .any(|d| d.code == "T1508" && d.message.contains("the master chain")),
636 "the master-chain blocker is a warning with its context: {:?}",
637 program.warnings
638 );
639 assert!(
640 program
641 .warnings
642 .iter()
643 .all(|d| d.severity == crate::diag::Severity::Warning),
644 "warnings never fail a compile"
645 );
646 }
647
648 #[test]
649 fn runtime_target_rejects_streaming_blockers() {
650 let mut song = demo_song();
651 song.master.push(
652 serde_json::from_value(
653 serde_json::json!({ "type": "convolve", "decay": 0.6, "mix": 0.4 }),
654 )
655 .unwrap(),
656 );
657 let err = song
658 .compile(&CompileOptions {
659 target: CompileTarget::Runtime,
660 ..CompileOptions::default()
661 })
662 .unwrap_err();
663 assert!(err.has_errors());
664 assert!(
665 err.0
666 .iter()
667 .any(|d| d.code == "T1508" && d.severity == crate::diag::Severity::Error)
668 );
669 }
670
671 #[test]
672 fn an_invalid_resolved_doc_is_t2000() {
673 let mut song = Song::new("s", 120.0);
676 song.add_track("keys", SeqWave::Sampler, amp());
677 song.tracks[0].notes.push(note(0, 4, "C4"));
678 let err = song.compile(&CompileOptions::default()).unwrap_err();
679 assert_eq!(err.0.len(), 1);
680 assert_eq!(err.0[0].code, "T2000");
681 assert_eq!(err.0[0].path, "doc");
682 }
683
684 #[test]
685 fn meta_preserves_the_musical_facts() {
686 let program = demo_song().compile(&CompileOptions::default()).unwrap();
687 assert_eq!(program.meta.name, "demo");
688 assert_eq!(program.meta.tempo_bpm, 120.0);
689 assert_eq!(program.meta.length_bars, 1);
690 assert_eq!(program.meta.tracks.len(), 2);
691 assert_eq!(program.meta.tracks[0].id.get(), 1);
692 assert_eq!(program.meta.tracks[1].id.get(), 2);
693 assert_eq!(program.meta.tracks[0].name, "bass");
694 assert_eq!(program.meta.tracks[0].notes, 2);
695 assert_eq!(
696 program.meta.duration_frames,
697 (program.doc.duration * program.doc.sample_rate as f32).round() as u64
698 );
699 }
700
701 #[test]
702 fn estimates_count_events_and_peak_voices() {
703 let mut song = Song::new("s", 120.0);
704 song.add_track("chords", SeqWave::Organ, amp());
705 song.tracks[0].notes.push(note(0, 8, "C4"));
707 song.tracks[0].notes.push(note(0, 8, "E4"));
708 song.tracks[0].notes.push(note(0, 8, "G4"));
709 song.tracks[0].notes.push(note(8, 4, "A4"));
710 let program = song.compile(&CompileOptions::default()).unwrap();
711 assert_eq!(program.estimates.events, 4);
712 assert_eq!(program.estimates.peak_voices, 3, "the chord is 3 voices");
713 }
714
715 #[test]
716 fn program_renders_stereo_matching_the_doc() {
717 let program = demo_song().compile(&CompileOptions::default()).unwrap();
718 let (l, r) = program.render_stereo();
719 let product = crate::render::render_product(&program.doc);
720 let (el, er) = product.stereo.unwrap();
721 assert_eq!(l, el);
722 assert_eq!(r, er);
723 }
724
725 #[test]
726 fn tempo_map_walk_is_segment_exact() {
727 let map = vec![
729 crate::dsl::TempoPoint {
730 at: Beat::zero(),
731 bpm: 120.0,
732 },
733 crate::dsl::TempoPoint {
734 at: Beat::from_int(4),
735 bpm: 240.0,
736 },
737 ];
738 assert_eq!(crate::dsl::tempo_map_seconds_at(&map, 0.0), 0.0);
739 assert_eq!(crate::dsl::tempo_map_seconds_at(&map, 4.0), 2.0);
740 assert_eq!(crate::dsl::tempo_map_seconds_at(&map, 8.0), 3.0);
741 assert_eq!(crate::dsl::tempo_map_bpm_at(&map, 3.999), 120.0);
742 assert_eq!(crate::dsl::tempo_map_bpm_at(&map, 4.0), 240.0);
743 }
744
745 fn tempo_mapped_song() -> Song {
746 let mut song = demo_song();
747 song.tempo_map = vec![
748 crate::dsl::TempoPoint {
749 at: Beat::zero(),
750 bpm: 120.0,
751 },
752 crate::dsl::TempoPoint {
753 at: Beat::from_int(4),
754 bpm: 240.0,
755 },
756 ];
757 song
758 }
759
760 #[test]
761 fn tempo_map_reaches_the_seq_and_the_meta() {
762 let program = tempo_mapped_song()
763 .compile(&CompileOptions {
764 sample_rate: Some(48_000),
765 ..CompileOptions::default()
766 })
767 .unwrap();
768 let Node::Tracks { tracks, .. } = &program.doc.root else {
769 panic!("tracks root");
770 };
771 let Node::Seq { tempo_map, .. } = &tracks[0].node else {
772 panic!("seq track");
773 };
774 assert_eq!(tempo_map.len(), 2, "the seq carries the map");
775 assert_eq!(program.meta.tempo_map.len(), 2, "the meta preserves it");
776 assert!(
779 (program.doc.duration - 3.5).abs() < 1e-4,
780 "{}",
781 program.doc.duration
782 );
783 }
784
785 #[test]
786 fn tempo_map_places_notes_on_exact_frames() {
787 let json = r#"{ "name": "mapped", "duration": 4.0, "version": 2, "engine": 4,
790 "sample_rate": 48000,
791 "root": { "type": "seq", "bpm": 120, "wave": "sawtooth",
792 "tempo_map": [ { "at": { "num": 0, "den": 1 }, "bpm": 120 },
793 { "at": { "num": 4, "den": 1 }, "bpm": 240 } ],
794 "env": { "a": 0.0, "d": 0.0, "s": 1.0, "r": 0.01 },
795 "notes": [ { "step": 0, "len": 4, "pitch": "A4" },
796 { "step": 32, "len": 4, "pitch": "A4" } ] } }"#;
797 let doc: SoundDoc = serde_json::from_str(json).unwrap();
798 doc.validate().unwrap();
799 let out = crate::render::render(&doc);
800 let onsets: Vec<usize> = {
802 let mut marks = Vec::new();
803 let mut silent = true;
804 for (i, &s) in out.iter().enumerate() {
805 if silent && s != 0.0 {
806 marks.push(i);
807 silent = false;
808 } else if !silent && s == 0.0 {
809 silent = true;
810 }
811 }
812 marks
813 };
814 assert_eq!(onsets.len(), 2, "two notes: {onsets:?}");
815 assert_eq!(
816 onsets[1] - onsets[0],
817 144_000,
818 "the note past the tempo change lands exactly 3.0 s later: {onsets:?}"
819 );
820 }
821
822 #[test]
823 fn a_tempo_mapped_seq_streams_byte_identically() {
824 let json = r#"{ "name": "mapped", "duration": 1.0, "version": 2, "engine": 4,
825 "root": { "type": "seq", "bpm": 120, "wave": "square",
826 "tempo_map": [ { "at": { "num": 0, "den": 1 }, "bpm": 120 },
827 { "at": { "num": 2, "den": 1 }, "bpm": 90 } ],
828 "env": { "a": 0.005, "d": 0.05, "s": 0.6, "r": 0.05 },
829 "notes": [ { "step": 0, "len": 2, "pitch": "C4" },
830 { "step": 6, "len": 2, "pitch": "E4" },
831 { "step": 12, "len": 2, "pitch": "G4" } ] } }"#;
832 let doc: SoundDoc = serde_json::from_str(json).unwrap();
833 doc.validate().unwrap();
834 crate::streaming::tests::assert_byte_identical(&doc);
835 }
836
837 #[test]
838 fn meter_map_and_pickup_place_bars_exactly() {
839 let mut song = Song::new("waltzish", 120.0);
841 song.meter_map = vec![MeterPoint {
842 bar: 0,
843 numerator: 6,
844 denominator: 8,
845 }];
846 song.add_track("t", SeqWave::Sine, amp());
847 song.add_pattern("p", 1, vec![note(0, 1, "C4")]);
848 song.arrange("t", "p", 1);
849 assert_eq!(song.beat_at_bar(1), Beat::from_int(3));
850 assert_eq!(song.beat_at_bar(2), Beat::from_int(6));
851 let program = song.compile(&CompileOptions::default()).unwrap();
852 let Node::Tracks { tracks, .. } = &program.doc.root else {
853 panic!("tracks");
854 };
855 let Node::Seq { notes, .. } = &tracks[0].node else {
856 panic!("seq");
857 };
858 assert_eq!(notes[0].step, 12, "bar 1 of 6/8 is step 12");
859 song.pickup = Some(Beat::from_int(1));
861 let program = song.compile(&CompileOptions::default()).unwrap();
862 let Node::Tracks { tracks, .. } = &program.doc.root else {
863 panic!("tracks");
864 };
865 let Node::Seq { notes, .. } = &tracks[0].node else {
866 panic!("seq");
867 };
868 assert_eq!(notes[0].step, 4, "bar 1 follows the one-beat pickup");
869 }
870
871 #[test]
872 fn off_grid_placements_are_t1005() {
873 let mut song = Song::new("s", 120.0);
874 song.pickup = Some(Beat::new(1, 3)); song.add_track("t", SeqWave::Sine, amp());
876 song.add_pattern("p", 1, vec![note(0, 1, "C4")]);
877 song.arrange("t", "p", 1);
878 let err = song.compile(&CompileOptions::default()).unwrap_err();
879 assert!(
880 err.0
881 .iter()
882 .any(|d| d.code == "T1005" && d.path == "arrangement[0].bar"),
883 "{:?}",
884 err.0
885 );
886 }
887
888 #[test]
889 fn map_and_section_violations_have_their_codes() {
890 let mut song = demo_song();
891 song.tempo_map = vec![crate::dsl::TempoPoint {
892 at: Beat::from_int(2),
893 bpm: 140.0,
894 }];
895 let err = song.compile(&CompileOptions::default()).unwrap_err();
896 assert!(err.0.iter().any(|d| d.code == "T1003"), "{:?}", err.0);
897
898 let mut song = demo_song();
899 song.meter_map = vec![MeterPoint {
900 bar: 0,
901 numerator: 3,
902 denominator: 5,
903 }];
904 let err = song.compile(&CompileOptions::default()).unwrap_err();
905 assert!(err.0.iter().any(|d| d.code == "T1004"), "{:?}", err.0);
906
907 let mut song = demo_song();
908 song.sections.push(crate::song::Section {
909 name: String::new(),
910 bar: 0,
911 bars: 4,
912 });
913 let err = song.compile(&CompileOptions::default()).unwrap_err();
914 assert!(err.0.iter().any(|d| d.code == "T1006"), "{:?}", err.0);
915 }
916
917 #[test]
918 fn sections_and_markers_reach_the_meta_sorted() {
919 let mut song = demo_song();
920 song.sections.push(crate::song::Section {
921 name: "chorus".into(),
922 bar: 4,
923 bars: 4,
924 });
925 song.sections.push(crate::song::Section {
926 name: "verse".into(),
927 bar: 0,
928 bars: 4,
929 });
930 song.markers.push(crate::song::Marker {
931 name: "drop".into(),
932 at: Beat::from_int(16),
933 });
934 let program = song.compile(&CompileOptions::default()).unwrap();
935 assert_eq!(program.meta.sections[0].name, "verse");
936 assert_eq!(program.meta.sections[1].name, "chorus");
937 assert_eq!(program.meta.markers[0].name, "drop");
938 let loaded = crate::program::Program::from_json(&program.to_json()).unwrap();
940 assert_eq!(loaded.meta.sections.len(), 2);
941 }
942
943 #[test]
944 fn length_bars_respects_the_meter_map() {
945 let mut song = Song::new("s", 120.0);
947 song.meter_map = vec![MeterPoint {
948 bar: 0,
949 numerator: 6,
950 denominator: 8,
951 }];
952 song.add_track("t", SeqWave::Sine, amp());
953 song.tracks[0].notes.push(note(0, 24, "C4")); assert_eq!(song.length_bars(), 2);
955 song.meter_map.clear();
957 assert_eq!(song.length_bars(), 2);
958 }
959
960 #[test]
961 fn automation_compiles_beats_to_seconds() {
962 let mut song = Song::new("s", 120.0);
963 song.add_track("t", SeqWave::Sine, amp());
964 song.tracks[0].notes.push(note(0, 4, "C4"));
965 song.tracks[0].automation.push(crate::song::SongLane {
966 target: crate::dsl::AutoTarget::Gain,
967 curve: crate::dsl::AutoCurve::Step,
968 points: vec![
969 crate::song::SongPoint { at: 0.0, v: 0.2 },
970 crate::song::SongPoint { at: 2.0, v: 0.8 },
971 ],
972 });
973 let program = song.compile(&CompileOptions::default()).unwrap();
974 let Node::Tracks { tracks, .. } = &program.doc.root else {
975 panic!("tracks root");
976 };
977 assert_eq!(tracks[0].automation.len(), 1);
978 assert_eq!(tracks[0].automation[0].curve, crate::dsl::AutoCurve::Step);
979 assert_eq!(
980 tracks[0].automation[0].points[1].t, 1.0,
981 "beat 2 at 120 BPM is 1.0 s"
982 );
983
984 song.tempo_map = vec![
986 crate::dsl::TempoPoint {
987 at: Beat::zero(),
988 bpm: 120.0,
989 },
990 crate::dsl::TempoPoint {
991 at: Beat::from_int(2),
992 bpm: 60.0,
993 },
994 ];
995 song.tracks[0].automation[0].points[1].at = 4.0;
996 let program = song.compile(&CompileOptions::default()).unwrap();
997 let Node::Tracks { tracks, .. } = &program.doc.root else {
998 panic!("tracks root");
999 };
1000 assert_eq!(
1001 tracks[0].automation[0].points[1].t, 3.0,
1002 "the lane crosses the tempo map segment-wise"
1003 );
1004 }
1005
1006 #[test]
1007 fn buses_and_sends_pass_through_to_the_document() {
1008 let mut song = demo_song();
1009 song.buses.push(crate::dsl::Bus {
1010 id: "verb".into(),
1011 gain: 0.8,
1012 effects: vec![Node::Reverb {
1013 room: 0.6,
1014 mix: 0.4,
1015 }],
1016 });
1017 song.tracks[1].bus = Some("verb".into());
1018 song.tracks[0].sends.push(crate::dsl::Send {
1019 bus: "verb".into(),
1020 amount: 0.3,
1021 });
1022 let program = song.compile(&CompileOptions::default()).unwrap();
1023 let Node::Tracks { tracks, buses, .. } = &program.doc.root else {
1024 panic!("tracks root");
1025 };
1026 assert_eq!(buses.len(), 1);
1027 assert_eq!(buses[0].id, "verb");
1028 assert_eq!(tracks[1].bus.as_deref(), Some("verb"));
1029 assert_eq!(tracks[0].sends.len(), 1);
1030 assert_eq!(tracks[0].sends[0].bus, "verb");
1031 assert!(program.doc.validate().is_ok(), "the wired mix validates");
1032 assert!(!program.render_mono().is_empty());
1034 }
1035}