Skip to main content

proof_engine/audio/
music_engine.rs

1//! Procedural music engine — generates MathAudioSources in real time.
2//!
3//! The music engine converts a high-level `MusicVibe` into a living, evolving
4//! stream of synthesized notes using music theory primitives (Scale, Chord,
5//! Rhythm, Melody).  It ticks every frame and queues AudioEvents for the
6//! audio thread.
7
8use glam::Vec3;
9
10// ── Music theory primitives ───────────────────────────────────────────────────
11
12/// Western equal-temperament scale types.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ScaleType {
15    Major,
16    NaturalMinor,
17    HarmonicMinor,
18    MelodicMinor,
19    Dorian,
20    Phrygian,
21    Lydian,
22    Mixolydian,
23    Locrian,
24    WholeTone,
25    Diminished,
26    Chromatic,
27    Pentatonic,
28    PentatonicMinor,
29    Blues,
30}
31
32impl ScaleType {
33    /// Semitone intervals from root (in order).
34    pub fn intervals(self) -> &'static [u8] {
35        match self {
36            ScaleType::Major          => &[0, 2, 4, 5, 7, 9, 11],
37            ScaleType::NaturalMinor   => &[0, 2, 3, 5, 7, 8, 10],
38            ScaleType::HarmonicMinor  => &[0, 2, 3, 5, 7, 8, 11],
39            ScaleType::MelodicMinor   => &[0, 2, 3, 5, 7, 9, 11],
40            ScaleType::Dorian         => &[0, 2, 3, 5, 7, 9, 10],
41            ScaleType::Phrygian       => &[0, 1, 3, 5, 7, 8, 10],
42            ScaleType::Lydian         => &[0, 2, 4, 6, 7, 9, 11],
43            ScaleType::Mixolydian     => &[0, 2, 4, 5, 7, 9, 10],
44            ScaleType::Locrian        => &[0, 1, 3, 5, 6, 8, 10],
45            ScaleType::WholeTone      => &[0, 2, 4, 6, 8, 10],
46            ScaleType::Diminished     => &[0, 2, 3, 5, 6, 8, 9, 11],
47            ScaleType::Chromatic      => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
48            ScaleType::Pentatonic     => &[0, 2, 4, 7, 9],
49            ScaleType::PentatonicMinor=> &[0, 3, 5, 7, 10],
50            ScaleType::Blues          => &[0, 3, 5, 6, 7, 10],
51        }
52    }
53}
54
55/// A key and scale combination.
56#[derive(Clone, Copy, Debug)]
57pub struct Scale {
58    /// MIDI note of the root (e.g. 60 = C4).
59    pub root:  u8,
60    pub scale: ScaleType,
61}
62
63impl Scale {
64    pub fn new(root: u8, scale: ScaleType) -> Self { Self { root, scale } }
65
66    /// Return the MIDI note of scale degree `degree` (0-indexed) at octave offset.
67    pub fn degree(&self, degree: i32, octave_offset: i32) -> u8 {
68        let intervals = self.scale.intervals();
69        let n = intervals.len() as i32;
70        let oct_shift = degree.div_euclid(n);
71        let idx       = degree.rem_euclid(n) as usize;
72        let semitone  = intervals[idx] as i32 + oct_shift * 12 + octave_offset * 12;
73        ((self.root as i32 + semitone).clamp(0, 127)) as u8
74    }
75
76    /// Convert MIDI note to frequency in Hz.
77    pub fn midi_to_hz(midi: u8) -> f32 {
78        440.0 * 2f32.powf((midi as f32 - 69.0) / 12.0)
79    }
80
81    /// Frequency of scale degree.
82    pub fn freq(&self, degree: i32, octave: i32) -> f32 {
83        Self::midi_to_hz(self.degree(degree, octave))
84    }
85
86    /// All frequencies in one octave.
87    pub fn octave_freqs(&self, octave: i32) -> Vec<f32> {
88        (0..self.scale.intervals().len() as i32)
89            .map(|d| self.freq(d, octave))
90            .collect()
91    }
92}
93
94// ── Chord ─────────────────────────────────────────────────────────────────────
95
96/// A set of simultaneously played scale degrees.
97#[derive(Clone, Debug)]
98pub struct Chord {
99    pub degrees: Vec<i32>,  // scale degrees (0-indexed)
100    pub octave:  i32,
101    pub name:    String,
102}
103
104impl Chord {
105    pub fn new(degrees: Vec<i32>, octave: i32, name: impl Into<String>) -> Self {
106        Self { degrees, octave, name: name.into() }
107    }
108
109    // Common chords (scale-relative degrees)
110    pub fn triad_major(octave: i32)  -> Self { Self::new(vec![0, 2, 4], octave, "Maj") }
111    pub fn triad_minor(octave: i32)  -> Self { Self::new(vec![0, 2, 4], octave, "min") }
112    pub fn seventh(octave: i32)      -> Self { Self::new(vec![0, 2, 4, 6], octave, "7th") }
113    pub fn sus2(octave: i32)         -> Self { Self::new(vec![0, 1, 4], octave, "sus2") }
114    pub fn sus4(octave: i32)         -> Self { Self::new(vec![0, 3, 4], octave, "sus4") }
115    pub fn power(octave: i32)        -> Self { Self::new(vec![0, 4], octave, "5th") }
116    pub fn diminished(octave: i32)   -> Self { Self::new(vec![0, 2, 5], octave, "dim") }
117    pub fn augmented(octave: i32)    -> Self { Self::new(vec![0, 2, 5], octave, "aug") } // approx
118    pub fn add9(octave: i32)         -> Self { Self::new(vec![0, 2, 4, 8], octave, "add9") }
119
120    /// Frequencies of all notes given a scale.
121    pub fn frequencies(&self, scale: &Scale) -> Vec<f32> {
122        self.degrees.iter().map(|&d| scale.freq(d + 0, self.octave)).collect()
123    }
124}
125
126// ── Chord progression ─────────────────────────────────────────────────────────
127
128/// A sequence of chords with durations (in beats).
129#[derive(Clone, Debug)]
130pub struct Progression {
131    pub chords:     Vec<(Chord, f32)>,  // (chord, duration_in_beats)
132    pub current:    usize,
133    pub beat_clock: f32,
134}
135
136impl Progression {
137    pub fn new(chords: Vec<(Chord, f32)>) -> Self {
138        Self { chords, current: 0, beat_clock: 0.0 }
139    }
140
141    /// I–V–vi–IV in major (pop progression).
142    pub fn one_five_six_four(octave: i32) -> Self {
143        Self::new(vec![
144            (Chord::new(vec![0, 2, 4], octave, "I"),   4.0),
145            (Chord::new(vec![4, 6, 1], octave, "V"),   4.0),
146            (Chord::new(vec![5, 0, 2], octave, "vi"),  4.0),
147            (Chord::new(vec![3, 5, 0], octave, "IV"),  4.0),
148        ])
149    }
150
151    /// i–VI–III–VII (minor pop).
152    pub fn minor_pop(octave: i32) -> Self {
153        Self::new(vec![
154            (Chord::new(vec![0, 2, 4], octave, "i"),   4.0),
155            (Chord::new(vec![5, 0, 2], octave, "VI"),  4.0),
156            (Chord::new(vec![2, 4, 6], octave, "III"), 4.0),
157            (Chord::new(vec![6, 1, 3], octave, "VII"), 4.0),
158        ])
159    }
160
161    /// ii–V–I (jazz).
162    pub fn two_five_one(octave: i32) -> Self {
163        Self::new(vec![
164            (Chord::new(vec![1, 3, 5, 0], octave, "ii7"),  4.0),
165            (Chord::new(vec![4, 6, 1, 3], octave, "V7"),   4.0),
166            (Chord::new(vec![0, 2, 4, 6], octave, "Imaj7"),8.0),
167        ])
168    }
169
170    /// Advance the progression clock by beat_delta. Returns Some(chord) when a new chord starts.
171    pub fn tick(&mut self, beat_delta: f32) -> Option<&Chord> {
172        if self.chords.is_empty() { return None; }
173        self.beat_clock += beat_delta;
174        let current_dur = self.chords[self.current].1;
175        if self.beat_clock >= current_dur {
176            self.beat_clock -= current_dur;
177            self.current    = (self.current + 1) % self.chords.len();
178            return Some(&self.chords[self.current].0);
179        }
180        None
181    }
182
183    pub fn current_chord(&self) -> Option<&Chord> {
184        self.chords.get(self.current).map(|(c, _)| c)
185    }
186
187    pub fn progress_in_chord(&self) -> f32 {
188        let dur = self.chords.get(self.current).map(|(_, d)| *d).unwrap_or(1.0);
189        (self.beat_clock / dur).clamp(0.0, 1.0)
190    }
191}
192
193// ── Rhythm ────────────────────────────────────────────────────────────────────
194
195/// A repeating rhythmic pattern as beat positions.
196#[derive(Clone, Debug)]
197pub struct RhythmPattern {
198    pub hits:  Vec<f32>,   // beat positions within one measure
199    pub length: f32,       // length of measure in beats
200    pub cursor: f32,       // current beat position
201    pub next:   usize,     // next hit index
202}
203
204impl RhythmPattern {
205    pub fn new(hits: Vec<f32>, length: f32) -> Self {
206        let next = 0;
207        Self { hits, length, cursor: 0.0, next }
208    }
209
210    pub fn four_on_floor() -> Self {
211        Self::new(vec![0.0, 1.0, 2.0, 3.0], 4.0)
212    }
213
214    pub fn eighth_notes() -> Self {
215        Self::new(vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5], 4.0)
216    }
217
218    pub fn syncopated() -> Self {
219        Self::new(vec![0.0, 0.75, 1.5, 2.25, 3.0, 3.5], 4.0)
220    }
221
222    pub fn offbeat() -> Self {
223        Self::new(vec![0.5, 1.5, 2.5, 3.5], 4.0)
224    }
225
226    pub fn clave_son() -> Self {
227        // 3-2 son clave
228        Self::new(vec![0.0, 0.75, 1.5, 2.5, 3.5], 4.0)
229    }
230
231    pub fn waltz() -> Self {
232        Self::new(vec![0.0, 1.0, 2.0], 3.0)
233    }
234
235    /// Advance by beat_delta. Returns how many hits occurred.
236    pub fn tick(&mut self, beat_delta: f32) -> u32 {
237        if self.hits.is_empty() { return 0; }
238        self.cursor += beat_delta;
239        if self.cursor >= self.length {
240            self.cursor -= self.length;
241            self.next = 0;
242        }
243        let mut count = 0;
244        while self.next < self.hits.len() && self.cursor >= self.hits[self.next] {
245            count    += 1;
246            self.next += 1;
247        }
248        count
249    }
250}
251
252// ── Melody generator ──────────────────────────────────────────────────────────
253
254/// Generates melodic lines that follow a scale and chord progression.
255#[derive(Clone, Debug)]
256pub struct MelodyGenerator {
257    pub scale:        Scale,
258    pub octave:       i32,
259    /// Probability of stepwise motion vs leap.
260    pub step_bias:    f32,
261    /// Probability of resting (silence) per note.
262    pub rest_prob:    f32,
263    /// How much to lean toward chord tones.
264    pub chord_weight: f32,
265    last_degree:      i32,
266    /// Pseudo-random state.
267    rng:              u64,
268}
269
270impl MelodyGenerator {
271    pub fn new(scale: Scale, octave: i32) -> Self {
272        Self {
273            scale,
274            octave,
275            step_bias:    0.7,
276            rest_prob:    0.15,
277            chord_weight: 0.6,
278            last_degree:  0,
279            rng:          12345,
280        }
281    }
282
283    fn rand_f32(&mut self) -> f32 {
284        // xorshift64
285        self.rng ^= self.rng << 13;
286        self.rng ^= self.rng >> 7;
287        self.rng ^= self.rng << 17;
288        (self.rng & 0xFFFF) as f32 / 65535.0
289    }
290
291    fn rand_range(&mut self, lo: i32, hi: i32) -> i32 {
292        if hi <= lo { return lo; }
293        lo + (self.rand_f32() * (hi - lo) as f32) as i32
294    }
295
296    /// Generate the next note. Returns None for a rest, Some(freq) for a note.
297    pub fn next_note(&mut self, chord: &Chord) -> Option<f32> {
298        if self.rand_f32() < self.rest_prob { return None; }
299
300        let n = self.scale.scale.intervals().len() as i32;
301
302        // Bias toward chord tones
303        let degree = if self.rand_f32() < self.chord_weight && !chord.degrees.is_empty() {
304            let idx = self.rand_range(0, chord.degrees.len() as i32) as usize;
305            chord.degrees[idx]
306        } else if self.rand_f32() < self.step_bias {
307            // Step motion
308            let step = if self.rand_f32() < 0.5 { 1 } else { -1 };
309            self.last_degree + step
310        } else {
311            // Leap
312            self.rand_range(-3, 8)
313        };
314
315        // Clamp within reasonable range
316        let degree = degree.clamp(-2, n + 3);
317        self.last_degree = degree;
318
319        Some(self.scale.freq(degree, self.octave))
320    }
321
322    /// Generate a phrase of n notes, returning (frequency, duration) pairs.
323    pub fn phrase(&mut self, n: usize, chord: &Chord, beat_dur: f32) -> Vec<(Option<f32>, f32)> {
324        (0..n).map(|_| (self.next_note(chord), beat_dur)).collect()
325    }
326}
327
328// ── Vibe configuration ────────────────────────────────────────────────────────
329
330/// Parameters for a music vibe.
331#[derive(Clone, Debug)]
332pub struct VibeConfig {
333    pub scale:        Scale,
334    pub bpm:          f32,
335    pub progression:  Progression,
336    pub rhythm:       RhythmPattern,
337    pub bass_enabled: bool,
338    pub melody_enabled: bool,
339    pub pad_enabled:  bool,
340    pub arp_enabled:  bool,
341    /// Overall volume for this vibe.
342    pub volume:       f32,
343    /// Reverb/spaciousness [0, 1].
344    pub spaciousness: f32,
345}
346
347impl VibeConfig {
348    pub fn silence() -> Self {
349        Self {
350            scale:          Scale::new(60, ScaleType::Major),
351            bpm:            80.0,
352            progression:    Progression::new(vec![]),
353            rhythm:         RhythmPattern::new(vec![], 4.0),
354            bass_enabled:   false,
355            melody_enabled: false,
356            pad_enabled:    false,
357            arp_enabled:    false,
358            volume:         0.0,
359            spaciousness:   0.0,
360        }
361    }
362
363    pub fn ambient() -> Self {
364        Self {
365            scale:          Scale::new(57, ScaleType::NaturalMinor),  // A minor
366            bpm:            60.0,
367            progression:    Progression::minor_pop(3),
368            rhythm:         RhythmPattern::eighth_notes(),
369            bass_enabled:   true,
370            melody_enabled: false,
371            pad_enabled:    true,
372            arp_enabled:    false,
373            volume:         0.5,
374            spaciousness:   0.8,
375        }
376    }
377
378    pub fn combat() -> Self {
379        Self {
380            scale:          Scale::new(57, ScaleType::HarmonicMinor), // A harmonic minor
381            bpm:            140.0,
382            progression:    Progression::minor_pop(3),
383            rhythm:         RhythmPattern::four_on_floor(),
384            bass_enabled:   true,
385            melody_enabled: true,
386            pad_enabled:    false,
387            arp_enabled:    true,
388            volume:         0.8,
389            spaciousness:   0.3,
390        }
391    }
392
393    pub fn boss() -> Self {
394        Self {
395            scale:          Scale::new(45, ScaleType::Diminished),  // A2 diminished
396            bpm:            160.0,
397            progression:    Progression::two_five_one(2),
398            rhythm:         RhythmPattern::syncopated(),
399            bass_enabled:   true,
400            melody_enabled: true,
401            pad_enabled:    true,
402            arp_enabled:    true,
403            volume:         1.0,
404            spaciousness:   0.2,
405        }
406    }
407
408    pub fn victory() -> Self {
409        Self {
410            scale:          Scale::new(60, ScaleType::Major),  // C major
411            bpm:            120.0,
412            progression:    Progression::one_five_six_four(4),
413            rhythm:         RhythmPattern::eighth_notes(),
414            bass_enabled:   true,
415            melody_enabled: true,
416            pad_enabled:    false,
417            arp_enabled:    false,
418            volume:         0.9,
419            spaciousness:   0.5,
420        }
421    }
422
423    pub fn exploration() -> Self {
424        Self {
425            scale:          Scale::new(60, ScaleType::Lydian),
426            bpm:            85.0,
427            progression:    Progression::one_five_six_four(3),
428            rhythm:         RhythmPattern::waltz(),
429            bass_enabled:   true,
430            melody_enabled: true,
431            pad_enabled:    true,
432            arp_enabled:    false,
433            volume:         0.6,
434            spaciousness:   0.7,
435        }
436    }
437
438    pub fn tension() -> Self {
439        Self {
440            scale:          Scale::new(60, ScaleType::Phrygian),
441            bpm:            100.0,
442            progression:    Progression::minor_pop(3),
443            rhythm:         RhythmPattern::offbeat(),
444            bass_enabled:   true,
445            melody_enabled: false,
446            pad_enabled:    true,
447            arp_enabled:    false,
448            volume:         0.65,
449            spaciousness:   0.4,
450        }
451    }
452}
453
454// ── Note event ────────────────────────────────────────────────────────────────
455
456/// A note produced by the music engine for the audio thread.
457#[derive(Clone, Debug)]
458pub struct NoteEvent {
459    pub frequency: f32,
460    pub amplitude: f32,
461    pub duration:  f32,
462    pub pan:       f32,   // -1..1
463    pub voice:     NoteVoice,
464}
465
466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
467pub enum NoteVoice {
468    Bass,
469    Melody,
470    Pad,
471    Arp,
472    Chord,
473}
474
475// ── MusicEngine ───────────────────────────────────────────────────────────────
476
477/// Drives procedural music generation each frame.
478pub struct MusicEngine {
479    pub vibe:         VibeConfig,
480    /// Time elapsed in seconds.
481    time:             f32,
482    beat_clock:       f32,
483    /// Beats per second (derived from BPM).
484    beats_per_second: f32,
485    melody_gen:       MelodyGenerator,
486    arp_gen:          MelodyGenerator,
487    /// Buffer of pending note events.
488    pending_notes:    Vec<NoteEvent>,
489    /// Arpeggiator state.
490    arp_index:        usize,
491    arp_chord_cache:  Vec<f32>,
492    /// Transition target vibe.
493    next_vibe:        Option<VibeConfig>,
494    /// Transition progress [0, 1].
495    transition:       f32,
496    pub master_volume: f32,
497}
498
499impl MusicEngine {
500    pub fn new() -> Self {
501        let vibe = VibeConfig::silence();
502        let scale = vibe.scale;
503        Self {
504            vibe: vibe.clone(),
505            time: 0.0,
506            beat_clock: 0.0,
507            beats_per_second: 0.0,
508            melody_gen:  MelodyGenerator::new(scale, 5),
509            arp_gen:     MelodyGenerator::new(scale, 4),
510            pending_notes: Vec::new(),
511            arp_index:   0,
512            arp_chord_cache: Vec::new(),
513            next_vibe:   None,
514            transition:  1.0,
515            master_volume: 1.0,
516        }
517    }
518
519    /// Immediately switch to a new vibe config.
520    pub fn set_vibe(&mut self, config: VibeConfig) {
521        let scale = config.scale;
522        self.vibe = config;
523        self.beats_per_second = self.vibe.bpm / 60.0;
524        self.melody_gen = MelodyGenerator::new(scale, 5);
525        self.arp_gen    = MelodyGenerator::new(scale, 4);
526        self.arp_chord_cache.clear();
527        self.arp_index  = 0;
528    }
529
530    /// Transition to a new vibe over `duration` beats.
531    pub fn transition_to(&mut self, config: VibeConfig, _duration_beats: f32) {
532        self.next_vibe  = Some(config);
533        self.transition = 0.0;
534    }
535
536    /// Load a vibe by name.
537    pub fn set_vibe_by_name(&mut self, name: &str) {
538        let config = match name {
539            "silence"     => VibeConfig::silence(),
540            "ambient"     => VibeConfig::ambient(),
541            "combat"      => VibeConfig::combat(),
542            "boss"        => VibeConfig::boss(),
543            "victory"     => VibeConfig::victory(),
544            "exploration" => VibeConfig::exploration(),
545            "tension"     => VibeConfig::tension(),
546            _ => {
547                log::warn!("MusicEngine: unknown vibe '{}'", name);
548                return;
549            }
550        };
551        self.set_vibe(config);
552    }
553
554    /// Advance the music engine by `dt` seconds.
555    /// Returns note events that should be sent to the audio thread.
556    pub fn tick(&mut self, dt: f32) -> Vec<NoteEvent> {
557        self.pending_notes.clear();
558        self.time += dt;
559
560        if self.beats_per_second < 0.001 { return Vec::new(); }
561
562        let beat_delta = dt * self.beats_per_second;
563        self.beat_clock += beat_delta;
564
565        // Advance chord progression
566        let _chord_changed = self.vibe.progression.tick(beat_delta);
567        let chord_changed = _chord_changed.is_some();
568
569        // Get current chord
570        let chord = match self.vibe.progression.current_chord() {
571            Some(c) => c.clone(),
572            None    => Chord::triad_major(3),
573        };
574
575        let chord_freqs: Vec<f32> = chord.frequencies(&self.vibe.scale);
576
577        // Bass voice — plays root on beat
578        if self.vibe.bass_enabled {
579            let hits = self.vibe.rhythm.tick(beat_delta);
580            for _ in 0..hits {
581                let root_freq = self.vibe.scale.freq(chord.degrees.first().copied().unwrap_or(0), 2);
582                let vol = self.vibe.volume * 0.7 * self.master_volume;
583                self.pending_notes.push(NoteEvent {
584                    frequency: root_freq,
585                    amplitude: vol,
586                    duration:  0.18,
587                    pan:       0.0,
588                    voice:     NoteVoice::Bass,
589                });
590            }
591        }
592
593        // Pad voice — sustains chord tones
594        if self.vibe.pad_enabled && chord_changed {
595            for (i, &freq) in chord_freqs.iter().enumerate() {
596                let pan = (i as f32 - chord_freqs.len() as f32 * 0.5) * 0.3;
597                self.pending_notes.push(NoteEvent {
598                    frequency: freq * 2.0,  // up an octave for pads
599                    amplitude: self.vibe.volume * 0.3 * self.master_volume,
600                    duration:  60.0 / self.vibe.bpm * 4.0,  // one measure
601                    pan,
602                    voice: NoteVoice::Pad,
603                });
604            }
605        }
606
607        // Melody voice — generates notes on eighth beats
608        if self.vibe.melody_enabled {
609            let eighth_beats = (self.beat_clock * 2.0).floor();
610            let prev_eighth  = ((self.beat_clock - beat_delta) * 2.0).floor();
611            if eighth_beats > prev_eighth {
612                if let Some(freq) = self.melody_gen.next_note(&chord) {
613                    self.pending_notes.push(NoteEvent {
614                        frequency: freq,
615                        amplitude: self.vibe.volume * 0.5 * self.master_volume,
616                        duration:  0.12,
617                        pan:       0.2,
618                        voice:     NoteVoice::Melody,
619                    });
620                }
621            }
622        }
623
624        // Arpeggio voice — cycles through chord tones on 16th notes
625        if self.vibe.arp_enabled {
626            if chord_changed || self.arp_chord_cache.is_empty() {
627                self.arp_chord_cache = chord_freqs.clone();
628                // Add some octave doublings
629                for &f in &chord_freqs { self.arp_chord_cache.push(f * 2.0); }
630                self.arp_index = 0;
631            }
632            let sixteenth_beats = (self.beat_clock * 4.0).floor();
633            let prev_sixteenth  = ((self.beat_clock - beat_delta) * 4.0).floor();
634            if sixteenth_beats > prev_sixteenth && !self.arp_chord_cache.is_empty() {
635                let freq = self.arp_chord_cache[self.arp_index % self.arp_chord_cache.len()];
636                self.arp_index += 1;
637                self.pending_notes.push(NoteEvent {
638                    frequency: freq * 4.0,  // two octaves up for arp brightness
639                    amplitude: self.vibe.volume * 0.25 * self.master_volume,
640                    duration:  0.06,
641                    pan:       -0.3,
642                    voice:     NoteVoice::Arp,
643                });
644            }
645        }
646
647        self.pending_notes.clone()
648    }
649
650    pub fn current_bpm(&self) -> f32 { self.vibe.bpm }
651    pub fn current_beat(&self) -> f32 { self.beat_clock }
652    pub fn current_bar(&self) -> u32 { (self.beat_clock / 4.0) as u32 }
653}
654
655impl Default for MusicEngine {
656    fn default() -> Self { Self::new() }
657}
658
659// ── Tests ─────────────────────────────────────────────────────────────────────
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn scale_major_intervals() {
667        let s = Scale::new(60, ScaleType::Major); // C major
668        let f0 = s.freq(0, 4); // C4
669        let f1 = s.freq(1, 4); // D4
670        let f4 = s.freq(4, 4); // G4
671        // C4 ≈ 261.63, D4 ≈ 293.66, G4 ≈ 392.00
672        assert!((f0 - 261.63).abs() < 0.5);
673        assert!((f1 - 293.66).abs() < 0.5);
674        assert!((f4 - 392.00).abs() < 0.5);
675    }
676
677    #[test]
678    fn scale_midi_to_hz_a4() {
679        let hz = Scale::midi_to_hz(69);
680        assert!((hz - 440.0).abs() < 0.01);
681    }
682
683    #[test]
684    fn chord_frequencies_non_empty() {
685        let scale = Scale::new(60, ScaleType::Major);
686        let chord = Chord::triad_major(4);
687        let freqs = chord.frequencies(&scale);
688        assert_eq!(freqs.len(), 3);
689        assert!(freqs.iter().all(|&f| f > 0.0));
690    }
691
692    #[test]
693    fn progression_advances() {
694        let mut prog = Progression::one_five_six_four(4);
695        let first = prog.current_chord().unwrap().name.clone();
696        prog.tick(4.0); // one full bar
697        let second = prog.current_chord().unwrap().name.clone();
698        assert_ne!(first, second);
699    }
700
701    #[test]
702    fn rhythm_fires_hits() {
703        let mut r = RhythmPattern::four_on_floor();
704        let hits  = r.tick(1.0); // one beat
705        assert_eq!(hits, 1);
706    }
707
708    #[test]
709    fn rhythm_full_measure() {
710        let mut r = RhythmPattern::four_on_floor();
711        let total: u32 = (0..4).map(|_| r.tick(1.0)).sum();
712        assert_eq!(total, 4);
713    }
714
715    #[test]
716    fn melody_gen_produces_notes() {
717        let scale = Scale::new(60, ScaleType::Major);
718        let mut gen = MelodyGenerator::new(scale, 4);
719        let chord = Chord::triad_major(4);
720        let phrase = gen.phrase(8, &chord, 0.5);
721        let non_rests = phrase.iter().filter(|(f, _)| f.is_some()).count();
722        // At 15% rest probability, out of 8 notes we expect at least a few notes
723        assert!(non_rests > 0);
724    }
725
726    #[test]
727    fn engine_silence_no_notes() {
728        let mut engine = MusicEngine::new();
729        engine.set_vibe(VibeConfig::silence());
730        let notes = engine.tick(1.0 / 60.0);
731        assert!(notes.is_empty());
732    }
733
734    #[test]
735    fn engine_combat_produces_notes() {
736        let mut engine = MusicEngine::new();
737        engine.set_vibe(VibeConfig::combat());
738        // Tick for 2 full seconds
739        let mut all_notes = Vec::new();
740        for _ in 0..120 {
741            all_notes.extend(engine.tick(1.0 / 60.0));
742        }
743        assert!(!all_notes.is_empty(), "Expected notes in combat vibe");
744    }
745
746    #[test]
747    fn vibe_config_by_name() {
748        let mut engine = MusicEngine::new();
749        engine.set_vibe_by_name("ambient");
750        assert!((engine.current_bpm() - 60.0).abs() < 0.1);
751        engine.set_vibe_by_name("boss");
752        assert!((engine.current_bpm() - 160.0).abs() < 0.1);
753    }
754}