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