1mod layers;
21mod schedule;
22mod sections;
23
24use crate::dsl::SoundDoc;
25use crate::render;
26use crate::runtime::AudioSource;
27pub use schedule::Quantize;
28
29pub struct LoopBuffer {
31 left: Vec<f32>,
32 right: Vec<f32>,
33 pos: usize,
34}
35
36impl LoopBuffer {
37 pub fn from_doc(doc: &SoundDoc) -> Self {
44 let (left, right) = render_stereo_pair(doc);
45 LoopBuffer::from_stereo(left, right)
46 }
47
48 pub fn from_doc_at(doc: &SoundDoc, sample_rate: u32) -> Self {
52 LoopBuffer::from_doc(&doc_at(doc, sample_rate))
53 }
54
55 pub fn from_stereo(mut left: Vec<f32>, mut right: Vec<f32>) -> Self {
59 let n = left.len().min(right.len());
60 left.truncate(n);
61 right.truncate(n);
62 LoopBuffer {
63 left,
64 right,
65 pos: 0,
66 }
67 }
68
69 pub fn from_doc_len(doc: &SoundDoc, frames: usize) -> Self {
74 let (mut left, mut right) = render_stereo_pair(doc);
75 left.resize(frames, 0.0);
76 right.resize(frames, 0.0);
77 LoopBuffer {
78 left,
79 right,
80 pos: 0,
81 }
82 }
83
84 pub fn from_doc_len_at(doc: &SoundDoc, frames: usize, sample_rate: u32) -> Self {
87 LoopBuffer::from_doc_len(&doc_at(doc, sample_rate), frames)
88 }
89}
90
91fn doc_at(doc: &SoundDoc, sample_rate: u32) -> SoundDoc {
96 let mut doc = doc.clone();
97 doc.sample_rate = sample_rate;
98 doc
99}
100
101impl AudioSource for LoopBuffer {
102 fn fill(&mut self, out: &mut [f32]) -> usize {
103 let frames = out.len() / 2;
104 let n = self.left.len();
105 if n == 0 {
106 out.fill(0.0);
107 return frames;
108 }
109 for f in 0..frames {
110 out[f * 2] = self.left[self.pos];
111 out[f * 2 + 1] = self.right[self.pos];
112 self.pos += 1;
115 if self.pos == n {
116 self.pos = 0;
117 }
118 }
119 frames
120 }
121
122 fn reset(&mut self) {
123 self.pos = 0;
124 }
125}
126
127struct Layer {
128 source: Box<dyn AudioSource + Send>,
129 fade_in_at: f32,
131 gain: f32,
132 target: f32,
133}
134
135struct Stinger {
136 left: Vec<f32>,
137 right: Vec<f32>,
138 pos: usize,
139}
140
141impl Stinger {
142 fn new(mut left: Vec<f32>, mut right: Vec<f32>) -> Self {
145 let n = left.len().min(right.len());
146 left.truncate(n);
147 right.truncate(n);
148 Stinger {
149 left,
150 right,
151 pos: 0,
152 }
153 }
154}
155
156const DUCK_ATTACK_SECS: f32 = 0.002;
159const DUCK_SNAP_EPSILON: f32 = 1e-4;
162
163pub fn render_stereo_pair(doc: &SoundDoc) -> (Vec<f32>, Vec<f32>) {
170 let p = render::render_product(doc);
171 p.stereo.unwrap_or_else(|| (p.mono.clone(), p.mono))
172}
173
174struct Scheduled {
176 fire_at: u64,
177 action: Action,
178}
179
180enum Action {
181 SetIntensity(f32),
182 Stinger(Stinger),
183 Transition { to: usize },
184}
185
186struct Section {
188 name: String,
189 buffer: LoopBuffer,
190}
191
192struct SectionFade {
194 to: usize,
195 from_gain: f32,
198 step: f32,
201 frames_done: usize,
205}
206
207pub struct AdaptiveMusic {
228 layers: Vec<Layer>,
229 stingers: Vec<Stinger>,
230 intensity: f32,
231 fade_coeff: f32,
233 scratch: Vec<f32>,
234 sample_rate: u32,
235 paused: bool,
237 position: u64,
240 duck_gain: f32,
242 duck_target: f32,
245 duck_attack: f32,
247 duck_coeff: f32,
249 bpm: f32,
251 beats_per_bar: u32,
253 pending: Vec<Scheduled>,
255 sections: Vec<Section>,
257 current_section: Option<usize>,
259 section_fade: Option<SectionFade>,
261 section_step: f32,
263}
264
265impl AdaptiveMusic {
266 pub fn new(sample_rate: u32) -> Self {
268 let fade_coeff = 1.0 - (-1.0 / (1.5 * sample_rate as f32)).exp();
269 AdaptiveMusic {
270 layers: Vec::new(),
271 stingers: Vec::new(),
272 intensity: 0.0,
273 fade_coeff,
274 scratch: Vec::new(),
275 sample_rate,
276 paused: false,
277 position: 0,
278 duck_gain: 1.0,
279 duck_target: 1.0,
280 duck_attack: 0.0,
281 duck_coeff: 0.0,
282 bpm: 0.0,
283 beats_per_bar: 4,
284 pending: Vec::new(),
285 sections: Vec::new(),
286 current_section: None,
287 section_fade: None,
288 section_step: 1.0 / (0.06 * sample_rate as f32),
291 }
292 }
293 pub fn pause(&mut self) {
299 self.paused = true;
300 }
301
302 pub fn resume(&mut self) {
304 self.paused = false;
305 }
306
307 pub fn is_paused(&self) -> bool {
309 self.paused
310 }
311
312 pub fn reset(&mut self) {
318 self.position = 0;
319 self.stingers.clear();
320 self.pending.clear();
321 self.section_fade = None;
322 self.duck_gain = 1.0;
324 self.duck_target = 1.0;
325 for l in &mut self.layers {
326 l.source.reset();
327 }
328 for s in &mut self.sections {
329 s.buffer.reset();
330 }
331 }
332}
333
334impl AudioSource for AdaptiveMusic {
335 fn fill(&mut self, out: &mut [f32]) -> usize {
336 let frames = out.len() / 2;
337 out.fill(0.0);
338 if self.paused {
340 return frames;
341 }
342 let mut done = 0usize;
346 while done < frames {
347 self.fire_due();
348 let next = self
349 .pending
350 .iter()
351 .map(|s| s.fire_at)
352 .filter(|&t| t > self.position)
353 .min();
354 let span = match next {
355 Some(t) => ((t - self.position) as usize).min(frames - done),
356 None => frames - done,
357 };
358 self.render_span(&mut out[done * 2..(done + span) * 2], span);
359 done += span;
360 }
361 frames
362 }
363
364 fn reset(&mut self) {
369 AdaptiveMusic::reset(self);
370 }
371}
372
373impl AdaptiveMusic {
374 fn render_span(&mut self, out: &mut [f32], frames: usize) {
378 if self.scratch.len() < frames * 2 {
379 self.scratch.resize(frames * 2, 0.0);
380 }
381 let coeff = self.fade_coeff;
382 let scratch = &mut self.scratch[..frames * 2];
383
384 let fade = self
388 .section_fade
389 .as_ref()
390 .map(|f| (f.to, f.from_gain, f.step, f.frames_done));
391 if let Some((to, start, step, done)) = fade {
392 let g = |f: usize| (start + (done + f) as f32 * step).clamp(0.0, 1.0);
393 if let Some(cur) = self.current_section {
394 self.sections[cur].buffer.fill(scratch);
395 for f in 0..frames {
396 let g = g(f);
397 out[f * 2] += scratch[f * 2] * (1.0 - g);
398 out[f * 2 + 1] += scratch[f * 2 + 1] * (1.0 - g);
399 }
400 }
401 self.sections[to].buffer.fill(scratch);
402 for f in 0..frames {
403 let g = g(f);
404 out[f * 2] += scratch[f * 2] * g;
405 out[f * 2 + 1] += scratch[f * 2 + 1] * g;
406 }
407 let end = g(frames);
408 if end >= 1.0 {
409 self.current_section = Some(to);
410 self.section_fade = None;
411 } else if end <= 0.0 {
412 self.section_fade = None;
414 } else if let Some(fd) = self.section_fade.as_mut() {
415 fd.frames_done = done + frames;
416 }
417 } else if let Some(cur) = self.current_section {
418 self.sections[cur].buffer.fill(scratch);
419 for f in 0..frames {
420 out[f * 2] += scratch[f * 2];
421 out[f * 2 + 1] += scratch[f * 2 + 1];
422 }
423 }
424
425 for layer in &mut self.layers {
426 layer.source.fill(scratch);
427 for f in 0..frames {
428 layer.gain += (layer.target - layer.gain) * coeff;
429 if (layer.target - layer.gain).abs() < DUCK_SNAP_EPSILON {
433 layer.gain = layer.target;
434 }
435 out[f * 2] += scratch[f * 2] * layer.gain;
436 out[f * 2 + 1] += scratch[f * 2 + 1] * layer.gain;
437 }
438 }
439 for st in &mut self.stingers {
440 let n = (st.left.len() - st.pos).min(frames);
443 for f in 0..n {
444 out[f * 2] += st.left[st.pos];
445 out[f * 2 + 1] += st.right[st.pos];
446 st.pos += 1;
447 }
448 }
449 self.stingers.retain(|s| s.pos < s.left.len());
450 if self.duck_gain < 1.0 || self.duck_target < 1.0 {
453 for f in 0..frames {
454 if self.duck_gain > self.duck_target {
455 self.duck_gain += (self.duck_target - self.duck_gain) * self.duck_attack;
457 if self.duck_gain <= self.duck_target + DUCK_SNAP_EPSILON {
458 self.duck_gain = self.duck_target;
459 self.duck_target = 1.0;
461 }
462 } else {
463 self.duck_gain += (1.0 - self.duck_gain) * self.duck_coeff;
466 if self.duck_gain >= 1.0 - DUCK_SNAP_EPSILON {
467 self.duck_gain = 1.0;
468 }
469 }
470 out[f * 2] *= self.duck_gain;
471 out[f * 2 + 1] *= self.duck_gain;
472 }
473 }
474 self.position += frames as u64;
475 }
476}
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 fn doc(json: &str) -> SoundDoc {
482 serde_json::from_str(json).unwrap()
483 }
484 fn peak(s: &[f32]) -> f32 {
485 s.iter().fold(0.0f32, |m, &x| m.max(x.abs()))
486 }
487
488 #[test]
489 fn layers_fade_with_intensity() {
490 let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
491 let hi = doc(r#"{ "name":"h", "duration":0.2, "root": { "type":"sine", "freq":880 } }"#);
492 let mut music = AdaptiveMusic::new(48_000);
493 music.add_layer(LoopBuffer::from_doc(&base), 0.0); music.add_layer(LoopBuffer::from_doc(&hi), 0.5); let mut out = vec![0.0f32; 512 * 2];
497 music.fill(&mut out);
498 assert!(peak(&out) > 0.0, "base layer sounds");
499 assert_eq!(
500 music.layer_gain(1),
501 Some(0.0),
502 "hi layer silent at intensity 0"
503 );
504
505 music.set_intensity(1.0);
506 for _ in 0..400 {
507 music.fill(&mut vec![0.0f32; 512 * 2]); }
509 assert!(
510 music.layer_gain(1).unwrap() > 0.9,
511 "hi layer swelled in with intensity"
512 );
513 }
514
515 #[test]
516 fn stingers_fire_and_finish() {
517 let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
518 let sting =
519 doc(r#"{ "name":"s", "duration":0.05, "root": { "type":"sine", "freq":1320 } }"#);
520 let mut music = AdaptiveMusic::new(48_000);
521 music.add_layer(LoopBuffer::from_doc(&base), 0.0);
522 music.stinger(&sting);
523 assert_eq!(music.active_stingers(), 1);
524 for _ in 0..20 {
526 music.fill(&mut vec![0.0f32; 512 * 2]);
527 }
528 assert_eq!(
529 music.active_stingers(),
530 0,
531 "stinger finished and was culled"
532 );
533 }
534
535 #[test]
536 fn loop_buffer_reset_rewinds_to_head() {
537 let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
538 let mut buf = LoopBuffer::from_doc(&base);
539
540 let mut first = vec![0.0f32; 64 * 2];
541 buf.fill(&mut first);
542 buf.fill(&mut vec![0.0f32; 512 * 2]); buf.reset();
544 let mut again = vec![0.0f32; 64 * 2];
545 buf.fill(&mut again);
546
547 assert_eq!(
548 first, again,
549 "reset replays from the loop head, sample-identical"
550 );
551 }
552
553 #[test]
554 fn position_advances_while_playing_and_holds_when_paused() {
555 let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
556 let mut music = AdaptiveMusic::new(48_000);
557 music.add_layer(LoopBuffer::from_doc(&base), 0.0);
558
559 assert_eq!(music.position_frames(), 0);
560 music.fill(&mut vec![0.0f32; 512 * 2]);
561 assert_eq!(music.position_frames(), 512, "the clock advances by frames");
562
563 music.pause();
564 assert!(music.is_paused());
565 let mut out = vec![0.1f32; 512 * 2];
566 music.fill(&mut out);
567 assert_eq!(peak(&out), 0.0, "paused output is silent");
568 assert_eq!(music.position_frames(), 512, "the clock holds while paused");
569
570 music.resume();
571 music.fill(&mut vec![0.0f32; 512 * 2]);
572 assert_eq!(music.position_frames(), 1024, "resumes advancing");
573 }
574
575 #[test]
576 fn reset_zeroes_the_clock_and_restarts_layers() {
577 let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
578 let mut music = AdaptiveMusic::new(48_000);
579 music.add_layer(LoopBuffer::from_doc(&base), 0.0);
580 music.set_intensity(1.0);
581
582 music.fill(&mut vec![0.0f32; 512 * 2]);
583 assert!(music.position_frames() > 0);
584 music.reset();
585 assert_eq!(music.position_frames(), 0, "the clock is back at sample 0");
586
587 let mut out = vec![0.0f32; 512 * 2];
588 music.fill(&mut out);
589 assert!(peak(&out) > 0.0, "the bed plays again from the top");
590 }
591
592 #[test]
593 fn duck_attenuates_then_recovers() {
594 use std::time::Duration;
595 let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
596
597 let mut plain = AdaptiveMusic::new(48_000);
599 plain.add_layer(LoopBuffer::from_doc(&base), 0.0);
600 let mut plain_out = vec![0.0f32; 512 * 2];
601 plain.fill(&mut plain_out);
602 let reference = peak(&plain_out);
603
604 let mut music = AdaptiveMusic::new(48_000);
606 music.add_layer(LoopBuffer::from_doc(&base), 0.0);
607 music.duck(0.9, Duration::from_millis(180));
608 let mut ducked = vec![0.0f32; 512 * 2];
609 music.fill(&mut ducked);
610 assert!(
611 peak(&ducked) < reference,
612 "ducked block is quieter than the undicked reference"
613 );
614
615 for _ in 0..64 {
617 music.fill(&mut vec![0.0f32; 512 * 2]);
618 }
619 let mut recovered = vec![0.0f32; 512 * 2];
620 music.fill(&mut recovered);
621 assert!(
622 peak(&recovered) > 0.9 * reference,
623 "the duck recovered toward unity"
624 );
625 }
626
627 fn tone(freq: f32) -> SoundDoc {
630 doc(&format!(
631 r#"{{ "name":"t", "duration":0.25, "root": {{ "type":"sine", "freq":{freq} }} }}"#
632 ))
633 }
634
635 fn advance(m: &mut AdaptiveMusic, frames: usize) {
637 m.fill(&mut vec![0.0f32; frames * 2]);
638 }
639
640 #[test]
641 fn clock_counts_beats_and_bars() {
642 let mut m = AdaptiveMusic::new(48_000);
643 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
644 m.set_tempo(120.0, 4); advance(&mut m, 24_000);
646 assert!(
647 (m.beats() - 1.0).abs() < 1e-6,
648 "one beat elapsed: {}",
649 m.beats()
650 );
651 advance(&mut m, 24_000 * 7); assert!(
653 (m.bars() - 2.0).abs() < 1e-6,
654 "two bars elapsed: {}",
655 m.bars()
656 );
657 }
658
659 #[test]
660 fn intensity_change_waits_for_the_bar() {
661 let mut m = AdaptiveMusic::new(48_000);
662 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
663 m.add_layer(LoopBuffer::from_doc(&tone(880.0)), 0.5); m.set_tempo(120.0, 4); m.set_intensity_at(1.0, Quantize::Bar);
666 advance(&mut m, 48_000);
668 assert_eq!(m.intensity(), 0.0, "intensity holds before the bar");
669 advance(&mut m, 48_100);
671 assert_eq!(m.intensity(), 1.0, "intensity applied on the bar");
672 }
673
674 #[test]
675 fn transition_swaps_sections_on_the_bar() {
676 let mut m = AdaptiveMusic::new(48_000);
677 let explore = m.add_section("explore", &tone(330.0));
678 let battle = m.add_section("battle", &tone(660.0));
679 assert_eq!(m.current_section(), Some(explore));
680 assert_eq!(m.section_named("battle"), Some(battle));
681 m.set_tempo(120.0, 4); m.transition_to(battle, Quantize::Bar);
683 let mut out = vec![0.0f32; 512 * 2];
685 m.fill(&mut out);
686 assert!(peak(&out) > 0.0, "section audio plays");
687 assert_eq!(m.current_section(), Some(explore), "holds until the bar");
688 for _ in 0..200 {
690 advance(&mut m, 512);
691 }
692 assert_eq!(m.current_section(), Some(battle), "swapped to battle");
693 }
694
695 #[test]
696 fn buffer_and_doc_section_apis_agree() {
697 let d = tone(220.0);
701 let via_doc = {
702 let mut m = AdaptiveMusic::new(48_000);
703 m.add_section("a", &d);
704 let mut o = vec![0.0f32; 256 * 2];
705 m.fill(&mut o);
706 o
707 };
708 let via_buf = {
709 let mut m = AdaptiveMusic::new(48_000);
710 m.add_section_buffer("a", LoopBuffer::from_doc_at(&d, 48_000));
711 let mut o = vec![0.0f32; 256 * 2];
712 m.fill(&mut o);
713 o
714 };
715 assert_eq!(
716 via_doc, via_buf,
717 "add_section_buffer must match add_section"
718 );
719 }
720
721 #[test]
722 fn doc_apis_render_at_the_engine_rate() {
723 let d = tone(220.0); assert_ne!(d.sample_rate, 48_000, "test needs a mismatched doc");
728
729 let mut at_engine_rate = AdaptiveMusic::new(48_000);
730 at_engine_rate.add_layer_doc(&d, 0.0);
731 let mut a = vec![0.0f32; 512];
732 at_engine_rate.fill(&mut a);
733
734 let mut explicit = AdaptiveMusic::new(48_000);
735 explicit.add_layer(LoopBuffer::from_doc_at(&d, 48_000), 0.0);
736 let mut b = vec![0.0f32; 512];
737 explicit.fill(&mut b);
738 assert_eq!(a, b, "add_layer_doc == from_doc_at at the engine rate");
739
740 let mut wrong = AdaptiveMusic::new(48_000);
741 wrong.add_layer(LoopBuffer::from_doc(&d), 0.0);
742 let mut c = vec![0.0f32; 512];
743 wrong.fill(&mut c);
744 assert_ne!(a, c, "the doc-rate render is a different (detuned) signal");
745 }
746
747 #[test]
748 fn transition_to_the_current_section_is_a_pure_noop() {
749 let plain = {
753 let mut m = AdaptiveMusic::new(48_000);
754 m.add_section("a", &tone(220.0));
755 m.add_section("b", &tone(440.0));
756 let mut o = vec![0.0f32; 256 * 2];
757 m.fill(&mut o);
758 o
759 };
760 let mut m = AdaptiveMusic::new(48_000);
761 let a = m.add_section("a", &tone(220.0));
762 m.add_section("b", &tone(440.0));
763 m.transition_to(a, Quantize::Immediate); let mut o = vec![0.0f32; 256 * 2];
765 m.fill(&mut o);
766 assert_eq!(
767 o, plain,
768 "a transition to the current section changed the mix"
769 );
770 }
771
772 #[test]
773 fn stinger_fires_on_the_beat() {
774 let mut m = AdaptiveMusic::new(48_000);
775 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
776 m.set_tempo(120.0, 4); m.stinger_at(&tone(1320.0), Quantize::Beat);
778 assert_eq!(m.active_stingers(), 0, "not yet — waiting for the beat");
779 for _ in 0..46 {
781 advance(&mut m, 512); }
783 assert_eq!(m.active_stingers(), 0, "still before the beat");
784 advance(&mut m, 512); assert_eq!(m.active_stingers(), 1, "stinger fired on the beat");
786 }
787
788 #[test]
789 fn quantized_schedule_is_deterministic() {
790 let run = || {
791 let mut m = AdaptiveMusic::new(48_000);
792 m.add_section("a", &tone(330.0));
793 let b = m.add_section("b", &tone(660.0));
794 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
795 m.set_tempo(140.0, 4);
796 m.set_intensity_at(0.8, Quantize::Beat);
797 m.transition_to(b, Quantize::Bar);
798 m.stinger_at(&tone(990.0), Quantize::Bars(2));
799 let mut acc = Vec::new();
800 let mut out = vec![0.0f32; 333 * 2]; for _ in 0..300 {
802 m.fill(&mut out);
803 acc.extend_from_slice(&out);
804 }
805 acc
806 };
807 assert_eq!(
808 run(),
809 run(),
810 "a fixed tempo + block schedule replays identically"
811 );
812 }
813
814 #[test]
815 fn immediate_api_unchanged_without_tempo() {
816 let mut m = AdaptiveMusic::new(48_000);
818 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.5);
819 m.set_intensity_at(1.0, Quantize::Bar); assert_eq!(m.intensity(), 1.0, "no tempo → applies immediately");
821 }
822
823 #[test]
824 fn from_doc_len_forces_the_grid_and_loops_at_it() {
825 let grid = 1000;
828 let mut buf = LoopBuffer::from_doc_len(&tone(220.0), grid);
829 let mut first = vec![0.0f32; grid * 2];
830 let mut second = vec![0.0f32; grid * 2];
831 buf.fill(&mut first);
832 buf.fill(&mut second);
833 assert_eq!(first, second, "the buffer loops exactly at the grid length");
834 }
835
836 #[test]
837 fn stem_set_shares_one_beat_grid() {
838 let mut music = AdaptiveMusic::new(48_000);
839 music.set_tempo(120.0, 4); let base = tone(220.0);
842 let hi = tone(880.0);
843 let grid = music.add_stem_set(&[(&base, 0.0), (&hi, 0.5)], 4.0);
844
845 assert_eq!(grid, 96_000, "grid = duration_beats × frames_per_beat");
847 assert_eq!(music.layer_gain(0), Some(1.0), "base always on");
848 assert_eq!(music.layer_gain(1), Some(0.0), "hi silent until intensity");
849 }
852
853 #[test]
854 fn stem_set_without_tempo_falls_back_to_the_first_stem() {
855 let mut music = AdaptiveMusic::new(48_000);
856 let base = tone(220.0);
857 let grid = music.add_stem_set(&[(&base, 0.0)], 4.0);
858 assert!(
859 grid > 0,
860 "no tempo → grid falls back to the first stem's natural length"
861 );
862 }
863
864 #[test]
865 fn scheduled_events_are_block_size_invariant() {
866 let run = |block: usize| {
871 let mut m = AdaptiveMusic::new(48_000);
872 m.add_section("a", &tone(330.0));
873 let b = m.add_section("b", &tone(660.0));
874 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
875 m.add_layer(LoopBuffer::from_doc(&tone(880.0)), 0.5);
876 m.set_tempo(120.0, 4);
877 m.set_intensity_at(1.0, Quantize::Beat);
878 m.stinger_at(&tone(990.0), Quantize::Beat);
879 m.transition_to(b, Quantize::Bar);
880 let mut acc = Vec::new();
881 let mut out = vec![0.0f32; block * 2];
882 for _ in 0..(200_000 / block) {
883 m.fill(&mut out);
884 acc.extend_from_slice(&out);
885 }
886 acc
887 };
888 let a = run(128);
889 let b = run(512);
890 let n = a.len().min(b.len());
891 assert_eq!(
892 a[..n],
893 b[..n],
894 "output must not depend on the host block size"
895 );
896 }
897
898 #[test]
899 fn transition_reversal_mid_fade_cancels_back() {
900 let mut m = AdaptiveMusic::new(48_000);
904 let a = m.add_section("a", &tone(330.0));
905 let b = m.add_section("b", &tone(660.0));
906 m.transition_to(b, Quantize::Immediate);
907 advance(&mut m, 512); m.transition_to(a, Quantize::Immediate);
909 for _ in 0..40 {
910 advance(&mut m, 512);
911 }
912 assert_eq!(m.current_section(), Some(a), "cancelled back to a");
913 }
914
915 #[test]
916 fn re_transition_to_the_fade_target_is_a_noop() {
917 let mut m = AdaptiveMusic::new(48_000);
920 m.add_section("a", &tone(330.0));
921 let b = m.add_section("b", &tone(660.0));
922 m.transition_to(b, Quantize::Immediate);
923 advance(&mut m, 512); m.transition_to(b, Quantize::Immediate);
925 advance(&mut m, 2500); assert_eq!(
927 m.current_section(),
928 Some(b),
929 "the fade completed on its original clock"
930 );
931 }
932
933 #[test]
934 fn audio_source_reset_restarts_the_bed() {
935 let mut m = AdaptiveMusic::new(48_000);
937 m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
938 m.stinger(&tone(990.0));
939 advance(&mut m, 512);
940 assert!(m.position_frames() > 0);
941 let src: &mut dyn AudioSource = &mut m;
942 src.reset();
943 assert_eq!(
944 m.position_frames(),
945 0,
946 "reset through the trait restarts the clock"
947 );
948 assert_eq!(m.active_stingers(), 0, "and clears stingers");
949 }
950
951 #[test]
952 fn third_section_switch_completes_the_fade_first() {
953 let mut m = AdaptiveMusic::new(48_000);
956 m.add_section("a", &tone(330.0));
957 let b = m.add_section("b", &tone(660.0));
958 let c = m.add_section("c", &tone(990.0));
959 m.transition_to(b, Quantize::Immediate);
960 advance(&mut m, 512); m.transition_to(c, Quantize::Immediate);
962 advance(&mut m, 2880); assert_eq!(
964 m.current_section(),
965 Some(b),
966 "the in-flight fade completed before the onward transition"
967 );
968 for _ in 0..20 {
969 advance(&mut m, 512);
970 }
971 assert_eq!(m.current_section(), Some(c), "then c faded in");
972 }
973}