1mod compile;
24mod diagnostics;
25mod estimate;
26mod pattern;
27mod phrase;
28
29pub use compile::{CompileOptions, CompileTarget};
30pub use pattern::{
31 PatternError, concat, euclidean, gate, humanize, layer, probability, quantize, repeat, reverse,
32 rotate, slice, stretch, transpose, tuplet, vel,
33};
34pub use phrase::Phrase;
35
36use serde::{Deserialize, Serialize};
37
38use crate::catalog::{Voice, VoiceParams};
39use crate::dsl::{Adsr, ENGINE_VERSION, Node, SeqNote, SeqWave, Value};
40
41#[non_exhaustive]
44#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct SongTrack {
46 pub name: String,
49 pub wave: SeqWave,
52 pub env: Adsr,
54 #[serde(default = "unit_gain")]
56 pub gain: f32,
57 #[serde(default)]
59 pub pan: f32,
60 #[serde(default)]
62 pub sf2: String,
63 #[serde(default)]
65 pub sf2_preset: u32,
66 #[serde(default)]
68 pub sf2_bank: u32,
69 #[serde(default)]
72 pub notes: Vec<SeqNote>,
73 #[serde(default)]
75 pub voice: VoiceParams,
76 #[serde(default)]
78 pub reverb: f32,
79 #[serde(default)]
81 pub swing: Option<f32>,
82 #[serde(default)]
84 pub humanize: Option<f32>,
85 #[serde(default)]
87 pub mute: bool,
88 #[serde(default)]
92 pub solo: bool,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub automation: Vec<SongLane>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub bus: Option<String>,
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub sends: Vec<crate::dsl::Send>,
105}
106
107#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
110pub struct SongLane {
111 pub target: crate::dsl::AutoTarget,
113 #[serde(default)]
115 pub curve: crate::dsl::AutoCurve,
116 pub points: Vec<SongPoint>,
118}
119
120#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
122pub struct SongPoint {
123 pub at: f32,
126 pub v: f32,
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize)]
133pub struct Pattern {
134 pub name: String,
136 pub bars: u32,
138 pub notes: Vec<SeqNote>,
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct Placement {
145 pub track: String,
147 pub pattern: String,
149 pub bar: u32,
151}
152
153pub use crate::units::MeterPoint;
154
155#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
159pub struct Section {
160 pub name: String,
162 pub bar: u32,
164 pub bars: u32,
166}
167
168#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
171pub struct Marker {
172 pub name: String,
174 pub at: crate::units::Beat,
176}
177
178#[non_exhaustive]
181#[derive(Clone, Debug, Serialize, Deserialize)]
182pub struct Song {
183 pub name: String,
185 pub bpm: f32,
187 #[serde(default = "default_steps_per_beat")]
189 pub steps_per_beat: u32,
190 #[serde(default = "default_beats_per_bar")]
192 pub beats_per_bar: u32,
193 #[serde(default)]
195 pub swing: f32,
196 #[serde(default)]
198 pub humanize: f32,
199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
203 pub tempo_map: Vec<crate::dsl::TempoPoint>,
204 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub meter_map: Vec<MeterPoint>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub pickup: Option<crate::units::Beat>,
212 #[serde(default, skip_serializing_if = "Vec::is_empty")]
215 pub sections: Vec<Section>,
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
218 pub markers: Vec<Marker>,
219 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 pub buses: Vec<crate::dsl::Bus>,
223 pub tracks: Vec<SongTrack>,
225 pub patterns: Vec<Pattern>,
227 pub arrangement: Vec<Placement>,
229 #[serde(default)]
231 pub master: Vec<Node>,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub seed: Option<u64>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub engine: Option<u32>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub version: Option<u32>,
249}
250
251pub fn note(step: u32, len: u32, pitch: &str) -> SeqNote {
254 note_vel(step, len, pitch, 1.0)
255}
256
257pub fn note_vel(step: u32, len: u32, pitch: &str, gain: f32) -> SeqNote {
259 SeqNote {
260 step,
261 len,
262 pitch: Value::Note(pitch.to_string()),
263 gain,
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum SongError {
270 Empty,
272 UnknownTrack(String),
274 UnknownPattern(String),
276 Compile(String),
278}
279
280impl std::fmt::Display for SongError {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match self {
283 SongError::Empty => f.write_str("song has no tracks"),
284 SongError::UnknownTrack(t) => {
285 write!(f, "arrangement references unknown track '{t}'")
286 }
287 SongError::UnknownPattern(p) => {
288 write!(f, "arrangement references unknown pattern '{p}'")
289 }
290 SongError::Compile(e) => f.write_str(e),
291 }
292 }
293}
294
295impl std::error::Error for SongError {}
296
297impl From<SongError> for String {
298 fn from(e: SongError) -> String {
299 e.to_string()
300 }
301}
302
303impl Song {
304 pub fn new(name: impl Into<String>, bpm: f32) -> Self {
306 Song {
307 name: name.into(),
308 bpm,
309 steps_per_beat: default_steps_per_beat(),
310 beats_per_bar: default_beats_per_bar(),
311 swing: 0.0,
312 humanize: 0.0,
313 tempo_map: Vec::new(),
314 meter_map: Vec::new(),
315 pickup: None,
316 sections: Vec::new(),
317 markers: Vec::new(),
318 buses: Vec::new(),
319 tracks: Vec::new(),
320 patterns: Vec::new(),
321 arrangement: Vec::new(),
322 master: Vec::new(),
323 seed: None,
324 engine: Some(ENGINE_VERSION),
325 version: Some(crate::dsl::SCHEMA_VERSION),
326 }
327 }
328
329 pub fn with_seed(mut self, seed: u64) -> Self {
332 self.seed = Some(seed);
333 self
334 }
335
336 pub fn add_track(&mut self, name: impl Into<String>, wave: SeqWave, env: Adsr) -> &mut Self {
341 let name = self.unique_name(&slugify(&name.into()));
342 self.tracks.push(SongTrack {
343 name,
344 wave,
345 env,
346 gain: 1.0,
347 pan: 0.0,
348 sf2: String::new(),
349 sf2_preset: 0,
350 sf2_bank: 0,
351 notes: Vec::new(),
352 voice: VoiceParams::default(),
353 reverb: 0.0,
354 swing: None,
355 humanize: None,
356 mute: false,
357 solo: false,
358 automation: Vec::new(),
359 bus: None,
360 sends: Vec::new(),
361 });
362 self
363 }
364
365 pub fn add(mut self, instrument: Voice, write: impl FnOnce(&mut Phrase)) -> Self {
376 let mut phrase = Phrase::new(self.steps_per_beat);
377 write(&mut phrase);
378 let name = self.unique_name(&slugify(&instrument.name));
381 self.tracks.push(SongTrack {
382 name,
383 wave: instrument.wave,
384 env: instrument.env,
385 gain: instrument.gain,
386 pan: instrument.pan,
387 sf2: String::new(),
388 sf2_preset: 0,
389 sf2_bank: 0,
390 notes: phrase.notes,
391 voice: instrument.voice,
392 reverb: instrument.reverb,
393 swing: instrument.swing,
394 humanize: instrument.humanize,
395 mute: false,
396 solo: false,
397 automation: Vec::new(),
398 bus: None,
399 sends: Vec::new(),
400 });
401 self
402 }
403
404 pub fn add_voice(&mut self, name: impl Into<String>, voice: &Voice) -> &mut Self {
416 let name = self.unique_name(&slugify(&name.into()));
417 self.tracks.push(SongTrack {
418 name,
419 wave: voice.wave,
420 env: voice.env,
421 gain: voice.gain,
422 pan: voice.pan,
423 sf2: String::new(),
424 sf2_preset: 0,
425 sf2_bank: 0,
426 notes: Vec::new(),
427 voice: voice.voice,
428 reverb: voice.reverb,
429 swing: voice.swing,
430 humanize: voice.humanize,
431 mute: false,
432 solo: false,
433 automation: Vec::new(),
434 bus: None,
435 sends: Vec::new(),
436 });
437 self
438 }
439
440 fn unique_name(&self, base: &str) -> String {
443 if !self.tracks.iter().any(|t| t.name == base) {
444 return base.to_string();
445 }
446 (2..)
447 .map(|i| format!("{base}_{i}"))
448 .find(|n| !self.tracks.iter().any(|t| &t.name == n))
449 .expect("an unused suffix always exists")
450 }
451
452 pub fn add_pattern(
454 &mut self,
455 name: impl Into<String>,
456 bars: u32,
457 notes: Vec<SeqNote>,
458 ) -> &mut Self {
459 self.patterns.push(Pattern {
460 name: name.into(),
461 bars: bars.max(1),
462 notes,
463 });
464 self
465 }
466
467 pub fn arrange(&mut self, track: impl Into<String>, pattern: impl Into<String>, bar: u32) {
469 self.arrangement.push(Placement {
470 track: track.into(),
471 pattern: pattern.into(),
472 bar,
473 });
474 }
475
476 pub fn arrange_repeat(&mut self, track: &str, pattern: &str, start_bar: u32, times: u32) {
479 let stride = self
480 .patterns
481 .iter()
482 .find(|p| p.name == pattern)
483 .map(|p| p.bars)
484 .unwrap_or(1);
485 for i in 0..times {
486 self.arrange(track, pattern, start_bar + i * stride);
487 }
488 }
489
490 pub fn with_master(mut self, master: Vec<Node>) -> Self {
492 self.master = master;
493 self
494 }
495}
496
497fn slugify(name: &str) -> String {
501 let mut s = String::with_capacity(name.len());
502 let mut pending_us = false;
503 for c in name.chars() {
504 if c.is_ascii_alphanumeric() {
505 if pending_us && !s.is_empty() {
506 s.push('_');
507 }
508 s.push(c.to_ascii_lowercase());
509 pending_us = false;
510 } else {
511 pending_us = true;
512 }
513 }
514 if s.is_empty() {
515 s.push_str("track");
516 }
517 s
518}
519
520fn unit_gain() -> f32 {
521 1.0
522}
523fn default_steps_per_beat() -> u32 {
524 4
525}
526fn default_beats_per_bar() -> u32 {
527 4
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use crate::render;
534
535 fn amp() -> Adsr {
536 Adsr {
537 a: 0.005,
538 d: 0.1,
539 s: 0.8,
540 r: 0.2,
541 punch: 0.0,
542 }
543 }
544 fn peak(s: &[f32]) -> f32 {
545 s.iter().fold(0.0f32, |m, &x| m.max(x.abs()))
546 }
547
548 #[test]
549 fn compiles_and_renders_a_two_track_song() {
550 let mut song = Song::new("demo", 120.0);
551 song.add_track("bass", SeqWave::Bass, amp());
552 song.add_track("drums", SeqWave::Kit, amp());
553 song.add_pattern("bassline", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
554 song.add_pattern(
555 "beat",
556 1,
557 vec![note(0, 2, "midi:36"), note(8, 2, "midi:38")],
558 );
559 song.arrange_repeat("bass", "bassline", 0, 2);
560 song.arrange_repeat("drums", "beat", 0, 2);
561 assert_eq!(song.length_bars(), 2);
562
563 let doc = song.to_doc().unwrap();
564 assert!(matches!(&doc.root, Node::Tracks { tracks, .. } if tracks.len() == 2));
565 let out = render::render(&doc);
566 assert!(peak(&out) > 0.0, "the song makes sound");
567 assert_eq!(render::render(&song.to_doc().unwrap()), out);
569 }
570
571 #[test]
572 fn pattern_places_at_the_right_bar() {
573 let mut song = Song::new("s", 120.0);
575 song.add_track("lead", SeqWave::Square, amp());
576 song.add_pattern("p", 1, vec![note(0, 1, "C4")]);
577 song.arrange("lead", "p", 0);
578 song.arrange("lead", "p", 2); let doc = song.to_doc().unwrap();
580 let Node::Tracks { tracks, .. } = &doc.root else {
581 panic!("tracks root");
582 };
583 let Node::Seq { notes, .. } = &tracks[0].node else {
584 panic!("seq track");
585 };
586 assert_eq!(
587 notes.iter().map(|n| n.step).collect::<Vec<_>>(),
588 vec![0, 32]
589 );
590 }
591
592 #[test]
593 fn rejects_unknown_references() {
594 let mut a = Song::new("s", 120.0);
595 a.add_track("t", SeqWave::Sine, amp());
596 a.add_pattern("p", 1, vec![note(0, 1, "C4")]);
597 a.arrange("nope", "p", 0);
598 assert_eq!(
599 a.to_doc().unwrap_err(),
600 SongError::UnknownTrack("nope".into())
601 );
602
603 let mut b = Song::new("s", 120.0);
604 b.add_track("t", SeqWave::Sine, amp());
605 b.arrange("t", "ghost", 0);
606 assert_eq!(
607 b.to_doc().unwrap_err(),
608 SongError::UnknownPattern("ghost".into())
609 );
610 }
611
612 #[test]
613 fn round_trips_through_serde() {
614 let mut song = Song::new("s", 128.0);
615 song.add_track("bass", SeqWave::Bass, amp());
616 song.add_pattern("r", 1, vec![note(0, 4, "C2")]);
617 song.arrange("bass", "r", 0);
618 let json = serde_json::to_string(&song).unwrap();
619 let back: Song = serde_json::from_str(&json).unwrap();
620 assert!(back.to_doc().is_ok(), "a saved song reloads and compiles");
621 }
622
623 #[test]
624 fn fluent_add_places_notes_on_the_beat_grid() {
625 use crate::catalog::{Drums, GrandPiano};
626 let song = Song::new("demo", 120.0)
628 .add(GrandPiano::grand(), |t| {
629 t.at(0.0).note("C4", 1.0).at(1.0).note("E4", 1.0);
630 })
631 .add(Drums::acoustic(), |t| {
632 t.at(0.0).kick().at(0.5).hat();
633 });
634 let doc = song.to_doc().unwrap();
635 let Node::Tracks { tracks, .. } = &doc.root else {
636 panic!("tracks root");
637 };
638 assert_eq!(tracks.len(), 2);
639 let Node::Seq { notes, .. } = &tracks[0].node else {
640 panic!("seq");
641 };
642 assert_eq!(notes.iter().map(|n| n.step).collect::<Vec<_>>(), vec![0, 4]);
643 let Node::Seq { notes: drums, .. } = &tracks[1].node else {
644 panic!("seq");
645 };
646 assert_eq!(drums.iter().map(|n| n.step).collect::<Vec<_>>(), vec![0, 2]);
647 }
648
649 #[test]
650 fn fluent_song_renders_deterministically() {
651 use crate::catalog::{Bass, GrandPiano};
652 let build = || {
653 Song::new("tune", 100.0)
654 .add(GrandPiano::grand(), |t| {
655 t.play("C4", 1.0).play("E4", 1.0).play("G4", 1.0);
656 })
657 .add(Bass::finger(), |t| {
658 t.at(0.0).note("C2", 3.0);
659 })
660 .to_doc()
661 .unwrap()
662 };
663 let a = render::render(&build());
664 assert!(peak(&a) > 0.0, "the fluent song makes sound");
665 assert_eq!(render::render(&build()), a, "byte-identical every render");
666 }
667
668 #[test]
669 fn guitar_voice_param_reaches_the_seq() {
670 use crate::catalog::Guitar;
671 let doc = Song::new("g", 120.0)
672 .add(Guitar::steel(), |t| {
673 t.at(0.0).note("E3", 2.0);
674 })
675 .to_doc()
676 .unwrap();
677 let Node::Tracks { tracks, .. } = &doc.root else {
678 panic!("tracks");
679 };
680 let Node::Seq { pluck, .. } = &tracks[0].node else {
681 panic!("seq");
682 };
683 assert!(
684 (pluck.pluck_decay - 0.965).abs() < 1e-6,
685 "steel pluck_decay set"
686 );
687 }
688
689 #[test]
690 fn duplicate_instrument_names_are_disambiguated() {
691 use crate::catalog::GrandPiano;
692 let song = Song::new("two pianos", 120.0)
693 .add(GrandPiano::grand(), |t| {
694 t.at(0.0).note("C4", 1.0);
695 })
696 .add(GrandPiano::grand(), |t| {
697 t.at(0.0).note("E4", 1.0);
698 });
699 assert_eq!(song.tracks[0].name, "grand_piano");
700 assert_eq!(song.tracks[1].name, "grand_piano_2");
701 }
702
703 #[test]
704 fn per_track_reverb_wraps_and_is_dry_by_default() {
705 use crate::catalog::GrandPiano;
706 let dry = Song::new("s", 100.0)
708 .add(GrandPiano::grand(), |t| {
709 t.at(0.0).note("C4", 1.0);
710 })
711 .to_doc()
712 .unwrap();
713 let Node::Tracks { tracks, .. } = &dry.root else {
714 panic!("tracks")
715 };
716 assert!(
717 matches!(&tracks[0].node, Node::Seq { .. }),
718 "dry = bare seq"
719 );
720 let wet = Song::new("s", 100.0)
722 .add(GrandPiano::grand().reverb(0.5), |t| {
723 t.at(0.0).note("C4", 1.0);
724 })
725 .to_doc()
726 .unwrap();
727 let Node::Tracks { tracks, .. } = &wet.root else {
728 panic!("tracks")
729 };
730 let Node::Chain { stages } = &tracks[0].node else {
731 panic!("reverb wraps the seq in a chain")
732 };
733 assert!(matches!(stages[0], Node::Seq { .. }));
734 assert!(matches!(stages[1], Node::Reverb { .. }));
735 assert!(
736 render::render(&wet).iter().any(|&x| x != 0.0),
737 "wet song sounds"
738 );
739 }
740
741 #[test]
742 fn per_track_swing_overrides_the_song_swing() {
743 use crate::catalog::Bass;
744 let doc = Song::new("s", 120.0) .add(Bass::finger().swing(0.6), |t| {
746 t.at(0.0).note("C2", 1.0).at(1.0).note("G1", 1.0);
747 })
748 .to_doc()
749 .unwrap();
750 let Node::Tracks { tracks, .. } = &doc.root else {
751 panic!("tracks")
752 };
753 let Node::Seq { swing, .. } = &tracks[0].node else {
754 panic!("seq")
755 };
756 assert!(
757 (*swing - 0.6).abs() < 1e-6,
758 "track swing overrides the song's"
759 );
760 }
761
762 #[test]
763 fn catalog_names_become_valid_layer_id_slugs() {
764 use crate::catalog::{Drums, Guitar, Strings};
765 let doc = Song::new("s", 100.0)
768 .add(Strings::warm(), |t| {
769 t.at(0.0).chord(&["C4", "E4"], 4.0);
770 })
771 .add(Guitar::steel(), |t| {
772 t.at(0.0).note("E3", 4.0);
773 })
774 .add(Drums::tr808(), |t| {
775 t.at(0.0).kick();
776 })
777 .to_doc()
778 .unwrap();
779 assert!(
780 doc.validate().is_ok(),
781 "catalog song validates: {:?}",
782 doc.validate()
783 );
784 let Node::Tracks { tracks, .. } = &doc.root else {
785 panic!("tracks");
786 };
787 for t in tracks {
788 let id = t.id.as_deref().unwrap();
789 assert!(
790 id.chars()
791 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
792 "layer id '{id}' is a slug"
793 );
794 }
795 }
796
797 #[test]
798 fn song_pins_engine_and_version_at_creation() {
799 let mut song = Song::new("pinned", 120.0);
800 song.add_track("bass", SeqWave::Bass, amp());
801 song.tracks[0].notes.push(note(0, 4, "C2"));
802 assert_eq!(song.engine, Some(ENGINE_VERSION));
803 let doc = song.to_doc().unwrap();
804 assert_eq!(doc.engine, Some(ENGINE_VERSION));
805 assert_eq!(doc.version, Some(crate::dsl::SCHEMA_VERSION));
806
807 song.engine = Some(3);
810 assert_eq!(song.to_doc().unwrap().engine, Some(3));
811
812 let mut v = serde_json::to_value(&song).unwrap();
815 v.as_object_mut().unwrap().remove("engine");
816 v.as_object_mut().unwrap().remove("version");
817 let legacy: Song = serde_json::from_value(v).unwrap();
818 let doc = legacy.to_doc().unwrap();
819 assert_eq!(doc.engine, Some(ENGINE_VERSION));
820 assert_eq!(doc.version, None);
821 }
822
823 #[test]
824 fn length_bars_counts_direct_track_notes() {
825 let mut song = Song::new("fluent", 120.0); song.add_track("bass", SeqWave::Bass, amp());
829 assert_eq!(song.length_bars(), 0);
830 song.tracks[0].notes.push(note(17, 4, "C2")); assert_eq!(song.length_bars(), 2);
832 }
833
834 #[test]
835 fn slugifies_names() {
836 assert_eq!(slugify("Mellow Piano"), "mellow_piano");
837 assert_eq!(slugify("808 drums"), "808_drums");
838 assert_eq!(slugify("steel guitar"), "steel_guitar");
839 assert_eq!(slugify(" !! "), "track");
840 }
841
842 #[test]
843 fn pathological_step_values_saturate_instead_of_wrapping() {
844 let mut song = Song::new("s", 120.0);
848 song.add_track("t", SeqWave::Square, amp());
849 song.tracks[0].notes.push(note(u32::MAX, 4, "C4"));
850 let doc = song.to_doc().unwrap(); assert!(doc.duration.is_finite());
852 let mut song = Song::new("s", 120.0);
854 song.add_track("t", SeqWave::Square, amp());
855 song.add_pattern("p", 1, vec![note(0, 1, "C4")]);
856 song.arrange("t", "p", u32::MAX);
857 let doc = song.to_doc().unwrap();
858 assert!(doc.duration.is_finite());
859 }
860
861 #[test]
862 fn add_track_slugifies_and_dedups_names() {
863 let mut song = Song::new("s", 120.0);
866 song.add_track("My Bass", SeqWave::Bass, amp());
867 song.add_track("My Bass", SeqWave::Bass, amp());
868 assert_eq!(song.tracks[0].name, "my_bass");
869 assert_eq!(song.tracks[1].name, "my_bass_2");
870 }
871
872 #[test]
873 fn add_voice_slugifies_dedups_and_carries_the_voice_fields() {
874 use crate::catalog::{Bass, Drums};
875 let voice = Bass::pick()
876 .gain(0.8)
877 .pan(-0.25)
878 .reverb(0.4)
879 .swing(0.5)
880 .humanize(0.1);
881 let mut song = Song::new("s", 120.0);
882 song.add_voice("My Bass", &voice);
883 song.add_voice("My Bass", &Bass::finger());
884 song.add_voice("drums", &Drums::tr808());
885 assert_eq!(song.tracks[0].name, "my_bass");
886 assert_eq!(song.tracks[1].name, "my_bass_2");
887 assert_eq!(song.tracks[2].name, "drums");
888
889 let t = &song.tracks[0];
890 assert_eq!(t.wave, voice.wave);
891 assert_eq!(t.env, voice.env);
892 assert_eq!(t.gain, 0.8);
893 assert_eq!(t.pan, -0.25);
894 assert_eq!(t.reverb, 0.4);
895 assert_eq!(t.swing, Some(0.5));
896 assert_eq!(t.humanize, Some(0.1));
897 assert_eq!(t.voice, voice.voice, "the pick's bass_* params ride along");
898 assert!(t.notes.is_empty(), "add_voice writes no notes");
899 assert!(t.sf2.is_empty() && t.sf2_preset == 0 && t.sf2_bank == 0);
900 assert!(!t.mute && !t.solo);
901
902 song.add_pattern("p", 1, vec![note(0, 2, "C2")]);
904 song.arrange("my_bass", "p", 0);
905 assert!(song.to_doc().is_ok());
906 }
907
908 #[test]
909 fn degenerate_bpm_keeps_duration_and_placement_consistent() {
910 let mut song = Song::new("s", 0.5);
914 song.add_track("t", SeqWave::Sine, amp());
915 song.tracks[0].notes.push(note(0, 2, "C4"));
916 song.tracks[0].notes.push(note(16, 2, "C4")); let doc = song.to_doc().unwrap();
918 doc.validate().unwrap();
919 let Node::Tracks { tracks, .. } = &doc.root else {
920 panic!("tracks root");
921 };
922 let Node::Seq { bpm, .. } = &tracks[0].node else {
923 panic!("a reverb-less track compiles to a bare seq");
924 };
925 assert_eq!(*bpm, 1.0, "the seq plays at the clamped bpm");
926 let expected = 18.0 * (60.0 / 4.0) + 2.0; assert!(
928 (doc.duration - expected).abs() < 1e-3,
929 "duration matches the clamped bpm: {}",
930 doc.duration
931 );
932 }
933}