1use std::f32::consts::{PI, TAU};
10
11use crate::audio::music_engine::{
12 Chord, MelodyGenerator, MusicEngine, NoteEvent, NoteVoice, Progression,
13 RhythmPattern, Scale, ScaleType, VibeConfig,
14};
15
16const SAMPLE_RATE: f32 = 48_000.0;
22
23const DEFAULT_CROSSFADE_SECS: f32 = 0.75;
25
26const MAX_LAYERS: usize = 4;
28
29const FFT_SIZE: usize = 1024;
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum GameVibe {
39 TitleScreen,
40 Exploration,
41 Combat,
42 Boss,
43 Shop,
44 Shrine,
45 ChaosRift,
46 LowHP,
47 Death,
48 Victory,
49}
50
51impl GameVibe {
52 pub fn config(self) -> GameVibeConfig {
54 match self {
55 GameVibe::TitleScreen => VIBE_CONFIGS[0].clone(),
56 GameVibe::Exploration => VIBE_CONFIGS[1].clone(),
57 GameVibe::Combat => VIBE_CONFIGS[2].clone(),
58 GameVibe::Boss => VIBE_CONFIGS[3].clone(),
59 GameVibe::Shop => VIBE_CONFIGS[4].clone(),
60 GameVibe::Shrine => VIBE_CONFIGS[5].clone(),
61 GameVibe::ChaosRift => VIBE_CONFIGS[6].clone(),
62 GameVibe::LowHP => VIBE_CONFIGS[7].clone(),
63 GameVibe::Death => VIBE_CONFIGS[8].clone(),
64 GameVibe::Victory => VIBE_CONFIGS[9].clone(),
65 }
66 }
67
68 pub fn to_engine_vibe(self) -> VibeConfig {
70 let gc = self.config();
71 let root_midi = note_name_to_midi(gc.key_root);
72 let scale = Scale::new(root_midi, gc.scale_type);
73
74 let progression = match self {
75 GameVibe::TitleScreen | GameVibe::Shrine | GameVibe::Death => {
76 Progression::new(vec![
77 (Chord::triad_major(3), 8.0),
78 (Chord::sus2(3), 8.0),
79 ])
80 }
81 GameVibe::Exploration | GameVibe::Shop | GameVibe::Victory => {
82 Progression::one_five_six_four(3)
83 }
84 GameVibe::Combat | GameVibe::LowHP => Progression::minor_pop(3),
85 GameVibe::Boss => Progression::two_five_one(2),
86 GameVibe::ChaosRift => Progression::new(vec![
87 (Chord::diminished(3), 4.0),
88 (Chord::augmented(3), 4.0),
89 (Chord::seventh(3), 4.0),
90 (Chord::sus4(3), 4.0),
91 ]),
92 };
93
94 let rhythm = match self {
95 GameVibe::TitleScreen | GameVibe::Shrine | GameVibe::Death => {
96 RhythmPattern::new(vec![0.0, 2.0], 4.0)
97 }
98 GameVibe::Exploration | GameVibe::Shop => RhythmPattern::waltz(),
99 GameVibe::Combat | GameVibe::LowHP => RhythmPattern::four_on_floor(),
100 GameVibe::Boss => RhythmPattern::syncopated(),
101 GameVibe::ChaosRift => RhythmPattern::clave_son(),
102 GameVibe::Victory => RhythmPattern::eighth_notes(),
103 };
104
105 let (bass, melody, pad, arp) = match self {
106 GameVibe::TitleScreen => (false, false, true, false),
107 GameVibe::Exploration => (true, true, true, false),
108 GameVibe::Combat => (true, true, false, true),
109 GameVibe::Boss => (true, true, true, true),
110 GameVibe::Shop => (true, true, true, false),
111 GameVibe::Shrine => (false, false, true, false),
112 GameVibe::ChaosRift => (true, true, false, true),
113 GameVibe::LowHP => (false, true, false, false),
114 GameVibe::Death => (false, false, true, false),
115 GameVibe::Victory => (true, true, false, false),
116 };
117
118 VibeConfig {
119 scale,
120 bpm: gc.tempo_bpm,
121 progression,
122 rhythm,
123 bass_enabled: bass,
124 melody_enabled: melody,
125 pad_enabled: pad,
126 arp_enabled: arp,
127 volume: gc.volume,
128 spaciousness: gc.reverb_amount,
129 }
130 }
131}
132
133#[derive(Clone, Debug)]
139pub struct GameVibeConfig {
140 pub scale_type: ScaleType,
141 pub key_root: &'static str,
142 pub tempo_bpm: f32,
143 pub time_signature: (u8, u8),
144 pub instrument_set: InstrumentSet,
145 pub reverb_amount: f32,
146 pub filter_cutoff: f32,
147 pub volume: f32,
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub enum InstrumentSet {
153 EtherealPads,
154 Melodic,
155 PercussionHeavy,
156 HeavyBass,
157 WarmGentle,
158 Ethereal,
159 RandomChaos,
160 ThinArrangement,
161 Minimal,
162 Triumphant,
163}
164
165static VIBE_CONFIGS: &[GameVibeConfig] = &[
169 GameVibeConfig {
171 scale_type: ScaleType::Pentatonic,
172 key_root: "C",
173 tempo_bpm: 72.0,
174 time_signature: (4, 4),
175 instrument_set: InstrumentSet::EtherealPads,
176 reverb_amount: 0.85,
177 filter_cutoff: 2000.0,
178 volume: 0.55,
179 },
180 GameVibeConfig {
182 scale_type: ScaleType::Major,
183 key_root: "G",
184 tempo_bpm: 110.0,
185 time_signature: (4, 4),
186 instrument_set: InstrumentSet::Melodic,
187 reverb_amount: 0.6,
188 filter_cutoff: 8000.0,
189 volume: 0.65,
190 },
191 GameVibeConfig {
193 scale_type: ScaleType::NaturalMinor,
194 key_root: "D",
195 tempo_bpm: 140.0,
196 time_signature: (4, 4),
197 instrument_set: InstrumentSet::PercussionHeavy,
198 reverb_amount: 0.25,
199 filter_cutoff: 12000.0,
200 volume: 0.80,
201 },
202 GameVibeConfig {
204 scale_type: ScaleType::Diminished,
205 key_root: "Bb",
206 tempo_bpm: 160.0,
207 time_signature: (4, 4),
208 instrument_set: InstrumentSet::HeavyBass,
209 reverb_amount: 0.20,
210 filter_cutoff: 14000.0,
211 volume: 1.0,
212 },
213 GameVibeConfig {
215 scale_type: ScaleType::Major,
216 key_root: "F",
217 tempo_bpm: 90.0,
218 time_signature: (4, 4),
219 instrument_set: InstrumentSet::WarmGentle,
220 reverb_amount: 0.5,
221 filter_cutoff: 5000.0,
222 volume: 0.5,
223 },
224 GameVibeConfig {
226 scale_type: ScaleType::WholeTone,
227 key_root: "E",
228 tempo_bpm: 60.0,
229 time_signature: (4, 4),
230 instrument_set: InstrumentSet::Ethereal,
231 reverb_amount: 0.95,
232 filter_cutoff: 1500.0,
233 volume: 0.45,
234 },
235 GameVibeConfig {
237 scale_type: ScaleType::Chromatic,
238 key_root: "C", tempo_bpm: 120.0,
240 time_signature: (4, 4),
241 instrument_set: InstrumentSet::RandomChaos,
242 reverb_amount: 0.4,
243 filter_cutoff: 10000.0,
244 volume: 0.7,
245 },
246 GameVibeConfig {
248 scale_type: ScaleType::NaturalMinor,
249 key_root: "D", tempo_bpm: 119.0, time_signature: (4, 4),
252 instrument_set: InstrumentSet::ThinArrangement,
253 reverb_amount: 0.3,
254 filter_cutoff: 3000.0,
255 volume: 0.5,
256 },
257 GameVibeConfig {
259 scale_type: ScaleType::Phrygian,
260 key_root: "A",
261 tempo_bpm: 50.0,
262 time_signature: (4, 4),
263 instrument_set: InstrumentSet::Minimal,
264 reverb_amount: 0.9,
265 filter_cutoff: 1000.0,
266 volume: 0.35,
267 },
268 GameVibeConfig {
270 scale_type: ScaleType::Major,
271 key_root: "C",
272 tempo_bpm: 130.0,
273 time_signature: (4, 4),
274 instrument_set: InstrumentSet::Triumphant,
275 reverb_amount: 0.45,
276 filter_cutoff: 10000.0,
277 volume: 0.85,
278 },
279];
280
281fn note_name_to_midi(name: &str) -> u8 {
288 let base = match name.chars().next().unwrap_or('C') {
289 'C' => 0,
290 'D' => 2,
291 'E' => 4,
292 'F' => 5,
293 'G' => 7,
294 'A' => 9,
295 'B' => 11,
296 _ => 0,
297 };
298 let modifier: i8 = if name.contains('#') {
299 1
300 } else if name.contains('b') {
301 -1
302 } else {
303 0
304 };
305 ((60 + base) as i8 + modifier).clamp(0, 127) as u8
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum LayerType {
315 BassDrone,
316 Melody,
317 Percussion,
318 FullArrangement,
319 Ambient,
320 Tension,
321}
322
323#[derive(Clone, Debug)]
325pub struct MusicLayer {
326 pub layer_type: LayerType,
327 pub volume: f32,
328 pub target_volume: f32,
329 pub crossfade_rate: f32,
330 pub active: bool,
331 pub base_freq: f32,
333 pub pattern: Vec<i32>,
335 pub pattern_cursor: usize,
337 pub beat_accumulator: f32,
339 pub step_beats: f32,
341}
342
343impl MusicLayer {
344 pub fn new(layer_type: LayerType) -> Self {
345 Self {
346 layer_type,
347 volume: 0.0,
348 target_volume: 0.0,
349 crossfade_rate: 2.0, active: false,
351 base_freq: 65.41, pattern: Vec::new(),
353 pattern_cursor: 0,
354 beat_accumulator: 0.0,
355 step_beats: 1.0,
356 }
357 }
358
359 pub fn update(&mut self, dt: f32) {
361 if (self.volume - self.target_volume).abs() < 0.001 {
362 self.volume = self.target_volume;
363 } else if self.volume < self.target_volume {
364 self.volume = (self.volume + self.crossfade_rate * dt).min(self.target_volume);
365 } else {
366 self.volume = (self.volume - self.crossfade_rate * dt).max(self.target_volume);
367 }
368 if self.volume < 0.001 && self.target_volume < 0.001 {
369 self.active = false;
370 }
371 }
372
373 pub fn fade_in(&mut self, secs: f32) {
375 self.active = true;
376 self.target_volume = 1.0;
377 self.crossfade_rate = 1.0 / secs.max(0.01);
378 }
379
380 pub fn fade_out(&mut self, secs: f32) {
382 self.target_volume = 0.0;
383 self.crossfade_rate = 1.0 / secs.max(0.01);
384 }
385
386 pub fn tick_pattern(&mut self, beat_delta: f32, scale: &Scale) -> Vec<NoteEvent> {
388 let mut events = Vec::new();
389 if !self.active || self.pattern.is_empty() {
390 return events;
391 }
392 self.beat_accumulator += beat_delta;
393 while self.beat_accumulator >= self.step_beats {
394 self.beat_accumulator -= self.step_beats;
395 let degree = self.pattern[self.pattern_cursor % self.pattern.len()];
396 self.pattern_cursor = (self.pattern_cursor + 1) % self.pattern.len();
397
398 let octave = match self.layer_type {
399 LayerType::BassDrone => 2,
400 LayerType::Melody => 5,
401 LayerType::Percussion => 3,
402 LayerType::FullArrangement => 4,
403 LayerType::Ambient => 4,
404 LayerType::Tension => 3,
405 };
406
407 let voice = match self.layer_type {
408 LayerType::BassDrone => NoteVoice::Bass,
409 LayerType::Melody => NoteVoice::Melody,
410 LayerType::Percussion => NoteVoice::Chord,
411 LayerType::FullArrangement => NoteVoice::Pad,
412 LayerType::Ambient => NoteVoice::Pad,
413 LayerType::Tension => NoteVoice::Arp,
414 };
415
416 events.push(NoteEvent {
417 frequency: scale.freq(degree, octave),
418 amplitude: self.volume * 0.6,
419 duration: self.step_beats * 0.8,
420 pan: match self.layer_type {
421 LayerType::BassDrone => 0.0,
422 LayerType::Melody => 0.2,
423 LayerType::Percussion => -0.1,
424 LayerType::FullArrangement => 0.0,
425 LayerType::Ambient => -0.3,
426 LayerType::Tension => 0.4,
427 },
428 voice,
429 });
430 }
431 events
432 }
433}
434
435#[derive(Clone, Debug)]
446pub struct MusicLayerStack {
447 pub layers: [MusicLayer; MAX_LAYERS],
448 pub current_vibe: GameVibe,
449 pub current_scale: Scale,
450 pub beats_per_second: f32,
451}
452
453impl MusicLayerStack {
454 pub fn new() -> Self {
455 let mut layers = [
456 MusicLayer::new(LayerType::BassDrone),
457 MusicLayer::new(LayerType::Melody),
458 MusicLayer::new(LayerType::Percussion),
459 MusicLayer::new(LayerType::FullArrangement),
460 ];
461
462 layers[0].pattern = vec![0, 0, 4, 0];
464 layers[0].step_beats = 2.0;
465
466 layers[1].pattern = vec![0, 2, 4, 5, 7, 5, 4, 2];
468 layers[1].step_beats = 0.5;
469
470 layers[2].pattern = vec![0, 0, 4, 0, 0, 4, 0, 4];
472 layers[2].step_beats = 0.25;
473
474 layers[3].pattern = vec![0, 2, 4, 7, 4, 2, 0, -1];
476 layers[3].step_beats = 0.5;
477
478 Self {
479 layers,
480 current_vibe: GameVibe::TitleScreen,
481 current_scale: Scale::new(60, ScaleType::Pentatonic),
482 beats_per_second: 72.0 / 60.0,
483 }
484 }
485
486 pub fn transition_to(&mut self, vibe: GameVibe, crossfade_secs: f32) {
488 let cfg = vibe.config();
489 self.current_vibe = vibe;
490 self.current_scale = Scale::new(note_name_to_midi(cfg.key_root), cfg.scale_type);
491 self.beats_per_second = cfg.tempo_bpm / 60.0;
492
493 let secs = crossfade_secs.max(0.05);
494
495 match vibe {
496 GameVibe::TitleScreen | GameVibe::Shrine | GameVibe::Death => {
497 self.layers[0].fade_in(secs);
498 self.layers[1].fade_out(secs);
499 self.layers[2].fade_out(secs);
500 self.layers[3].fade_out(secs);
501 }
502 GameVibe::Exploration | GameVibe::Shop | GameVibe::Victory => {
503 self.layers[0].fade_in(secs);
504 self.layers[1].fade_in(secs);
505 self.layers[2].fade_out(secs);
506 self.layers[3].fade_out(secs);
507 }
508 GameVibe::Combat | GameVibe::LowHP | GameVibe::ChaosRift => {
509 self.layers[0].fade_in(secs);
510 self.layers[1].fade_in(secs);
511 self.layers[2].fade_in(secs);
512 self.layers[3].fade_out(secs);
513 }
514 GameVibe::Boss => {
515 self.layers[0].fade_in(secs);
516 self.layers[1].fade_in(secs);
517 self.layers[2].fade_in(secs);
518 self.layers[3].fade_in(secs);
519 }
520 }
521 }
522
523 pub fn set_floor_depth(&mut self, floor: u32) {
526 let floor_clamped = (floor as f32).clamp(1.0, 100.0);
527 let midi = 36.0 - (floor_clamped - 1.0) / 99.0 * 24.0;
529 self.layers[0].base_freq = Scale::midi_to_hz(midi.clamp(12.0, 36.0) as u8);
530 }
531
532 pub fn update(&mut self, dt: f32) -> Vec<NoteEvent> {
534 let beat_delta = dt * self.beats_per_second;
535 let mut events = Vec::new();
536 for layer in &mut self.layers {
537 layer.update(dt);
538 events.extend(layer.tick_pattern(beat_delta, &self.current_scale));
539 }
540 events
541 }
542}
543
544impl Default for MusicLayerStack {
545 fn default() -> Self {
546 Self::new()
547 }
548}
549
550#[derive(Clone, Copy, Debug, PartialEq, Eq)]
556pub enum CorruptionTier {
557 Clean,
558 PitchWobble,
559 RhythmDrift,
560 FilterModulation,
561 GranularArtifacts,
562}
563
564impl CorruptionTier {
565 pub fn from_level(level: u32) -> Self {
566 match level {
567 0..=100 => CorruptionTier::Clean,
568 101..=200 => CorruptionTier::PitchWobble,
569 201..=300 => CorruptionTier::RhythmDrift,
570 301..=400 => CorruptionTier::FilterModulation,
571 _ => CorruptionTier::GranularArtifacts,
572 }
573 }
574}
575
576#[derive(Clone, Debug)]
578pub struct PitchWobble {
579 pub max_cents: f32,
580 pub probability: f32,
581 phase: f32,
582 rng_state: u64,
583 active_offset: f32,
584}
585
586impl PitchWobble {
587 pub fn new() -> Self {
588 Self {
589 max_cents: 20.0,
590 probability: 0.0,
591 phase: 0.0,
592 rng_state: 0xDEAD_BEEF,
593 active_offset: 0.0,
594 }
595 }
596
597 pub fn set_intensity(&mut self, t: f32) {
598 self.probability = t.clamp(0.0, 1.0) * 0.3;
600 self.max_cents = 20.0 * t.clamp(0.0, 1.0);
601 }
602
603 fn xorshift(&mut self) -> f32 {
604 self.rng_state ^= self.rng_state << 13;
605 self.rng_state ^= self.rng_state >> 7;
606 self.rng_state ^= self.rng_state << 17;
607 (self.rng_state & 0xFFFF) as f32 / 65535.0
608 }
609
610 pub fn apply(&mut self, sample: f32) -> f32 {
612 self.phase += 1.0 / SAMPLE_RATE;
613 if self.phase > 1.0 {
614 self.phase -= 1.0;
615 if self.xorshift() < self.probability {
617 self.active_offset = (self.xorshift() * 2.0 - 1.0) * self.max_cents;
618 } else {
619 self.active_offset *= 0.95; }
621 }
622 let shift_ratio = 2.0f32.powf(self.active_offset / 1200.0);
624 sample * shift_ratio
625 }
626}
627
628impl Default for PitchWobble {
629 fn default() -> Self {
630 Self::new()
631 }
632}
633
634#[derive(Clone, Debug)]
636pub struct RhythmDrift {
637 pub swing_amount: f32,
638 pub jitter_amount: f32,
639 rng_state: u64,
640}
641
642impl RhythmDrift {
643 pub fn new() -> Self {
644 Self {
645 swing_amount: 0.0,
646 jitter_amount: 0.0,
647 rng_state: 0xCAFE_BABE,
648 }
649 }
650
651 pub fn set_intensity(&mut self, t: f32) {
652 self.swing_amount = t.clamp(0.0, 1.0) * 0.3;
653 self.jitter_amount = t.clamp(0.0, 1.0) * 0.05;
654 }
655
656 fn xorshift(&mut self) -> f32 {
657 self.rng_state ^= self.rng_state << 13;
658 self.rng_state ^= self.rng_state >> 7;
659 self.rng_state ^= self.rng_state << 17;
660 (self.rng_state & 0xFFFF) as f32 / 65535.0
661 }
662
663 pub fn beat_offset(&mut self, beat_index: u32) -> f32 {
665 let swing = if beat_index % 2 == 1 {
666 self.swing_amount
667 } else {
668 0.0
669 };
670 let jitter = (self.xorshift() * 2.0 - 1.0) * self.jitter_amount;
671 swing + jitter
672 }
673
674 pub fn apply(&self, sample: f32) -> f32 {
676 sample
677 }
678}
679
680impl Default for RhythmDrift {
681 fn default() -> Self {
682 Self::new()
683 }
684}
685
686#[derive(Clone, Debug)]
688pub struct FilterModulationEffect {
689 pub lfo_rate: f32,
690 pub lfo_depth: f32,
691 pub base_cutoff: f32,
692 phase: f32,
693 prev_output: f32,
695}
696
697impl FilterModulationEffect {
698 pub fn new() -> Self {
699 Self {
700 lfo_rate: 1.0,
701 lfo_depth: 0.0,
702 base_cutoff: 8000.0,
703 phase: 0.0,
704 prev_output: 0.0,
705 }
706 }
707
708 pub fn set_intensity(&mut self, t: f32, rng_seed: u64) {
709 let pseudo = ((rng_seed & 0xFFFF) as f32) / 65535.0;
711 self.lfo_rate = 0.1 + pseudo * 4.9;
712 self.lfo_depth = t.clamp(0.0, 1.0) * 6000.0;
713 }
714
715 pub fn apply(&mut self, sample: f32) -> f32 {
717 self.phase += self.lfo_rate / SAMPLE_RATE;
718 if self.phase >= 1.0 {
719 self.phase -= 1.0;
720 }
721 let lfo_val = (self.phase * TAU).sin();
722 let cutoff = (self.base_cutoff + lfo_val * self.lfo_depth).clamp(200.0, 20000.0);
723
724 let alpha = (TAU * cutoff / SAMPLE_RATE).min(1.0);
726 self.prev_output += alpha * (sample - self.prev_output);
727 self.prev_output
728 }
729}
730
731impl Default for FilterModulationEffect {
732 fn default() -> Self {
733 Self::new()
734 }
735}
736
737#[derive(Clone, Debug)]
739pub struct GranularArtifacts {
740 pub stutter_probability: f32,
741 pub bit_depth: f32,
742 pub time_stretch_factor: f32,
743 last_sample: f32,
744 rng_state: u64,
745 stutter_counter: u32,
746 stutter_length: u32,
747}
748
749impl GranularArtifacts {
750 pub fn new() -> Self {
751 Self {
752 stutter_probability: 0.0,
753 bit_depth: 16.0,
754 time_stretch_factor: 1.0,
755 last_sample: 0.0,
756 rng_state: 0xBAAD_F00D,
757 stutter_counter: 0,
758 stutter_length: 0,
759 }
760 }
761
762 pub fn set_intensity(&mut self, t: f32) {
763 let clamped = t.clamp(0.0, 2.0);
765 self.stutter_probability = 0.1 + clamped * 0.1; self.bit_depth = (16.0 - clamped * 6.0).clamp(4.0, 16.0);
768 self.time_stretch_factor = 1.0 + clamped * 0.3;
769 }
770
771 fn xorshift(&mut self) -> f32 {
772 self.rng_state ^= self.rng_state << 13;
773 self.rng_state ^= self.rng_state >> 7;
774 self.rng_state ^= self.rng_state << 17;
775 (self.rng_state & 0xFFFF) as f32 / 65535.0
776 }
777
778 pub fn apply(&mut self, sample: f32) -> f32 {
780 let mut out = sample;
781
782 if self.stutter_counter > 0 {
784 self.stutter_counter -= 1;
785 out = self.last_sample;
786 } else if self.xorshift() < self.stutter_probability {
787 self.stutter_length = (self.xorshift() * 2000.0) as u32 + 100;
788 self.stutter_counter = self.stutter_length;
789 self.last_sample = sample;
790 out = sample;
791 }
792
793 let levels = 2.0f32.powf(self.bit_depth);
795 out = (out * levels).round() / levels;
796
797 out
798 }
799}
800
801impl Default for GranularArtifacts {
802 fn default() -> Self {
803 Self::new()
804 }
805}
806
807#[derive(Clone, Debug)]
809pub struct CorruptionAudioProcessor {
810 pub corruption_level: f32,
811 pub tier: CorruptionTier,
812 pub pitch_wobble: PitchWobble,
813 pub rhythm_drift: RhythmDrift,
814 pub filter_mod: FilterModulationEffect,
815 pub granular: GranularArtifacts,
816}
817
818impl CorruptionAudioProcessor {
819 pub fn new() -> Self {
820 Self {
821 corruption_level: 0.0,
822 tier: CorruptionTier::Clean,
823 pitch_wobble: PitchWobble::new(),
824 rhythm_drift: RhythmDrift::new(),
825 filter_mod: FilterModulationEffect::new(),
826 granular: GranularArtifacts::new(),
827 }
828 }
829
830 pub fn process_corruption(&mut self, corruption: u32) {
832 self.corruption_level = corruption as f32;
833 self.tier = CorruptionTier::from_level(corruption);
834
835 match self.tier {
836 CorruptionTier::Clean => {
837 self.pitch_wobble.set_intensity(0.0);
838 self.rhythm_drift.set_intensity(0.0);
839 self.filter_mod.set_intensity(0.0, 0);
840 self.granular.set_intensity(0.0);
841 }
842 CorruptionTier::PitchWobble => {
843 let t = (corruption as f32 - 100.0) / 100.0;
844 self.pitch_wobble.set_intensity(t);
845 self.rhythm_drift.set_intensity(0.0);
846 self.filter_mod.set_intensity(0.0, 0);
847 self.granular.set_intensity(0.0);
848 }
849 CorruptionTier::RhythmDrift => {
850 let t = (corruption as f32 - 200.0) / 100.0;
851 self.pitch_wobble.set_intensity(1.0);
852 self.rhythm_drift.set_intensity(t);
853 self.filter_mod.set_intensity(0.0, 0);
854 self.granular.set_intensity(0.0);
855 }
856 CorruptionTier::FilterModulation => {
857 let t = (corruption as f32 - 300.0) / 100.0;
858 self.pitch_wobble.set_intensity(1.0);
859 self.rhythm_drift.set_intensity(1.0);
860 self.filter_mod.set_intensity(t, corruption as u64);
861 self.granular.set_intensity(0.0);
862 }
863 CorruptionTier::GranularArtifacts => {
864 let t = (corruption as f32 - 400.0) / 100.0;
865 self.pitch_wobble.set_intensity(1.0);
866 self.rhythm_drift.set_intensity(1.0);
867 self.filter_mod.set_intensity(1.0, corruption as u64);
868 self.granular.set_intensity(t);
869 }
870 }
871 }
872
873 pub fn apply(&mut self, sample: f32) -> f32 {
875 let mut s = sample;
876 if self.tier >= CorruptionTier::PitchWobble {
877 s = self.pitch_wobble.apply(s);
878 }
879 if self.tier >= CorruptionTier::RhythmDrift {
880 s = self.rhythm_drift.apply(s);
881 }
882 if self.tier >= CorruptionTier::FilterModulation {
883 s = self.filter_mod.apply(s);
884 }
885 if self.tier >= CorruptionTier::GranularArtifacts {
886 s = self.granular.apply(s);
887 }
888 s
889 }
890
891 pub fn beat_offset(&mut self, beat_index: u32) -> f32 {
893 if self.tier >= CorruptionTier::RhythmDrift {
894 self.rhythm_drift.beat_offset(beat_index)
895 } else {
896 0.0
897 }
898 }
899}
900
901impl Default for CorruptionAudioProcessor {
902 fn default() -> Self {
903 Self::new()
904 }
905}
906
907impl PartialOrd for CorruptionTier {
909 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
910 Some(self.cmp(other))
911 }
912}
913
914impl Ord for CorruptionTier {
915 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
916 let rank = |t: &CorruptionTier| -> u8 {
917 match t {
918 CorruptionTier::Clean => 0,
919 CorruptionTier::PitchWobble => 1,
920 CorruptionTier::RhythmDrift => 2,
921 CorruptionTier::FilterModulation => 3,
922 CorruptionTier::GranularArtifacts => 4,
923 }
924 };
925 rank(self).cmp(&rank(other))
926 }
927}
928
929#[derive(Clone, Copy, Debug, PartialEq, Eq)]
935pub enum ChordType {
936 Major,
937 Minor,
938 Diminished,
939 Augmented,
940 Suspended,
941 Power,
942 Seventh,
943}
944
945#[derive(Clone, Debug)]
947pub struct FloorMusicProfile {
948 pub scale: ScaleType,
949 pub chord_types: Vec<ChordType>,
950 pub arrangement_density: f32,
951 pub tempo_modifier: f32,
952 pub reverb: f32,
953 pub special_notes: &'static str,
954}
955
956pub fn floor_music_profile(floor: u32) -> FloorMusicProfile {
958 match floor {
959 1..=10 => FloorMusicProfile {
960 scale: ScaleType::Major,
961 chord_types: vec![ChordType::Major, ChordType::Minor, ChordType::Suspended],
962 arrangement_density: 1.0,
963 tempo_modifier: 1.0,
964 reverb: 0.35,
965 special_notes: "Warm tones, full arrangement",
966 },
967 11..=25 => FloorMusicProfile {
968 scale: ScaleType::Dorian,
969 chord_types: vec![ChordType::Minor, ChordType::Seventh, ChordType::Suspended],
970 arrangement_density: 0.85,
971 tempo_modifier: 1.0,
972 reverb: 0.45,
973 special_notes: "Dorian mode, slightly cooler, steady tempo",
974 },
975 26..=50 => FloorMusicProfile {
976 scale: ScaleType::NaturalMinor,
977 chord_types: vec![ChordType::Minor, ChordType::Power],
978 arrangement_density: 0.6,
979 tempo_modifier: 0.95,
980 reverb: 0.55,
981 special_notes: "Minor, thinner, sparse percussion",
982 },
983 51..=75 => FloorMusicProfile {
984 scale: ScaleType::Diminished,
985 chord_types: vec![ChordType::Diminished, ChordType::Minor, ChordType::Augmented],
986 arrangement_density: 0.4,
987 tempo_modifier: 0.8,
988 reverb: 0.8,
989 special_notes: "Diminished chords appear, tempo drops 0.8x, long reverb",
990 },
991 76..=99 => FloorMusicProfile {
992 scale: ScaleType::Chromatic,
993 chord_types: vec![ChordType::Power],
994 arrangement_density: 0.15,
995 tempo_modifier: 0.7,
996 reverb: 0.9,
997 special_notes: "Atonal, percussion = heartbeat only (sine 60 BPM), minimal melody",
998 },
999 _ => FloorMusicProfile {
1000 scale: ScaleType::WholeTone,
1002 chord_types: vec![],
1003 arrangement_density: 0.02,
1004 tempo_modifier: 0.5,
1005 reverb: 0.99,
1006 special_notes: "Near silence, single breathing sine drone, calm before The Algorithm",
1007 },
1008 }
1009}
1010
1011pub fn apply_floor_profile(
1013 stack: &mut MusicLayerStack,
1014 engine: &mut MusicEngine,
1015 floor: u32,
1016) {
1017 let profile = floor_music_profile(floor);
1018 stack.set_floor_depth(floor);
1019
1020 let base_bpm = engine.current_bpm();
1022 let adjusted_bpm = base_bpm * profile.tempo_modifier;
1023 stack.beats_per_second = adjusted_bpm / 60.0;
1024
1025 if floor >= 76 && floor <= 99 {
1027 stack.layers[1].fade_out(1.0); stack.layers[2].fade_out(1.0); stack.layers[3].fade_out(1.0); stack.layers[0].pattern = vec![0];
1033 stack.layers[0].step_beats = 1.0;
1034 } else if floor >= 100 {
1035 stack.layers[0].pattern = vec![0];
1037 stack.layers[0].step_beats = 4.0;
1038 stack.layers[1].fade_out(2.0);
1039 stack.layers[2].fade_out(2.0);
1040 stack.layers[3].fade_out(2.0);
1041 }
1042}
1043
1044#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1050pub enum BossMusic {
1051 Mirror,
1052 Null,
1053 Committee,
1054 AlgorithmReborn,
1055}
1056
1057#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1059pub enum PlayerActionType {
1060 Melee,
1061 Magic,
1062 Defense,
1063}
1064
1065#[derive(Clone, Debug)]
1067pub struct BossMusicController {
1068 pub boss: Option<BossMusic>,
1069 pub note_buffer: Vec<f32>,
1071 pub buffer_capacity: usize,
1073 pub reverse_cursor: usize,
1075 pub active_layer_count: u32,
1077 pub last_strip_hp: f32,
1079 pub time_sig_numerator: u8,
1081 pub algorithm_phase: u8,
1083 pub dominant_action: PlayerActionType,
1085 pub action_counts: [u32; 3],
1087}
1088
1089impl BossMusicController {
1090 pub fn new() -> Self {
1091 Self {
1092 boss: None,
1093 note_buffer: Vec::with_capacity(256),
1094 buffer_capacity: 256,
1095 reverse_cursor: 0,
1096 active_layer_count: 4,
1097 last_strip_hp: 1.0,
1098 time_sig_numerator: 4,
1099 algorithm_phase: 1,
1100 dominant_action: PlayerActionType::Melee,
1101 action_counts: [0; 3],
1102 }
1103 }
1104
1105 pub fn activate(&mut self, boss: BossMusic, stack: &mut MusicLayerStack) {
1107 self.boss = Some(boss);
1108 self.note_buffer.clear();
1109 self.reverse_cursor = 0;
1110 self.active_layer_count = 4;
1111 self.last_strip_hp = 1.0;
1112 self.algorithm_phase = 1;
1113 self.action_counts = [0; 3];
1114
1115 match boss {
1116 BossMusic::Mirror => {
1117 }
1119 BossMusic::Null => {
1120 for layer in &mut stack.layers {
1122 layer.fade_in(0.5);
1123 }
1124 self.active_layer_count = 4;
1125 }
1126 BossMusic::Committee => {
1127 self.time_sig_numerator = 5;
1129 for layer in &mut stack.layers {
1130 layer.step_beats = layer.step_beats * 5.0 / 4.0;
1132 }
1133 }
1134 BossMusic::AlgorithmReborn => {
1135 self.algorithm_phase = 1;
1137 }
1138 }
1139 }
1140
1141 pub fn deactivate(&mut self) {
1143 self.boss = None;
1144 self.time_sig_numerator = 4;
1145 }
1146
1147 pub fn mirror_buffer_note(&mut self, freq: f32) {
1149 if self.boss != Some(BossMusic::Mirror) {
1150 return;
1151 }
1152 if self.note_buffer.len() >= self.buffer_capacity {
1153 self.note_buffer.remove(0);
1154 }
1155 self.note_buffer.push(freq);
1156 }
1157
1158 pub fn mirror_next_reversed(&mut self) -> Option<f32> {
1160 if self.boss != Some(BossMusic::Mirror) || self.note_buffer.is_empty() {
1161 return None;
1162 }
1163 let idx = self.note_buffer.len() - 1 - (self.reverse_cursor % self.note_buffer.len());
1164 self.reverse_cursor += 1;
1165 Some(self.note_buffer[idx])
1166 }
1167
1168 pub fn null_update_hp(&mut self, hp_fraction: f32, stack: &mut MusicLayerStack) {
1170 if self.boss != Some(BossMusic::Null) {
1171 return;
1172 }
1173 let threshold = self.last_strip_hp - 0.1;
1175 if hp_fraction < threshold && self.active_layer_count > 0 {
1176 self.last_strip_hp = hp_fraction;
1177 let layer_idx = (self.active_layer_count as usize).min(MAX_LAYERS) - 1;
1179 stack.layers[layer_idx].fade_out(0.8);
1180 self.active_layer_count = self.active_layer_count.saturating_sub(1);
1181 }
1182 }
1183
1184 pub fn record_action(&mut self, action: PlayerActionType) {
1186 let idx = match action {
1187 PlayerActionType::Melee => 0,
1188 PlayerActionType::Magic => 1,
1189 PlayerActionType::Defense => 2,
1190 };
1191 self.action_counts[idx] += 1;
1192
1193 let max_idx = self
1195 .action_counts
1196 .iter()
1197 .enumerate()
1198 .max_by_key(|(_, &c)| c)
1199 .map(|(i, _)| i)
1200 .unwrap_or(0);
1201 self.dominant_action = match max_idx {
1202 0 => PlayerActionType::Melee,
1203 1 => PlayerActionType::Magic,
1204 _ => PlayerActionType::Defense,
1205 };
1206 }
1207
1208 pub fn algorithm_advance_phase(&mut self, stack: &mut MusicLayerStack) {
1210 if self.boss != Some(BossMusic::AlgorithmReborn) {
1211 return;
1212 }
1213 self.algorithm_phase = (self.algorithm_phase + 1).min(3);
1214 match self.algorithm_phase {
1215 2 => {
1216 match self.dominant_action {
1218 PlayerActionType::Melee => {
1219 stack.layers[2].fade_in(0.3);
1221 stack.layers[2].step_beats = 0.125; }
1223 PlayerActionType::Magic => {
1224 stack.layers[1].pattern =
1226 vec![0, 2, 4, 7, 9, 11, 9, 7, 4, 2];
1227 stack.layers[1].step_beats = 0.125;
1228 stack.layers[1].fade_in(0.3);
1229 }
1230 PlayerActionType::Defense => {
1231 stack.layers[1].fade_out(0.5);
1233 stack.layers[3].fade_out(0.5);
1234 }
1235 }
1236 }
1237 3 => {
1238 stack.current_scale = Scale::new(
1240 stack.current_scale.root,
1241 ScaleType::Chromatic,
1242 );
1243 for layer in &mut stack.layers {
1245 layer.pattern = vec![0, 1, 6, 7, 1, 11, 5, 6];
1246 layer.fade_in(0.2);
1247 }
1248 }
1249 _ => {}
1250 }
1251 }
1252
1253 pub fn process_notes(
1255 &mut self,
1256 notes: &mut Vec<NoteEvent>,
1257 stack: &mut MusicLayerStack,
1258 ) {
1259 let boss = match self.boss {
1260 Some(b) => b,
1261 None => return,
1262 };
1263
1264 match boss {
1265 BossMusic::Mirror => {
1266 let melody_notes: Vec<f32> = notes
1268 .iter()
1269 .filter(|n| n.voice == NoteVoice::Melody)
1270 .map(|n| n.frequency)
1271 .collect();
1272 for freq in &melody_notes {
1273 self.mirror_buffer_note(*freq);
1274 }
1275 for note in notes.iter_mut() {
1277 if note.voice == NoteVoice::Melody {
1278 if let Some(rev_freq) = self.mirror_next_reversed() {
1279 note.frequency = rev_freq;
1280 }
1281 }
1282 }
1283 }
1284 BossMusic::Committee => {
1285 }
1288 BossMusic::AlgorithmReborn if self.algorithm_phase == 3 => {
1289 let mut toggle = false;
1291 for note in notes.iter_mut() {
1292 if toggle {
1293 note.frequency *= 2.0f32.powf(6.0 / 12.0); }
1295 toggle = !toggle;
1296 }
1297 }
1298 _ => {}
1299 }
1300 }
1301}
1302
1303impl Default for BossMusicController {
1304 fn default() -> Self {
1305 Self::new()
1306 }
1307}
1308
1309#[derive(Clone, Debug, Default)]
1315pub struct AudioAnalysis {
1316 pub bass_energy: f32,
1317 pub mid_energy: f32,
1318 pub high_energy: f32,
1319 pub beat_detected: bool,
1320 pub envelope: f32,
1321 pub spectral_centroid: f32,
1322}
1323
1324#[derive(Clone, Debug)]
1326pub struct GameVisuals {
1327 pub chaos_particle_speed_mult: f32,
1328 pub camera_fov_offset: f32,
1329 pub force_field_strength: f32,
1330 pub entity_emission_pulse: f32,
1331 pub vignette_intensity: f32,
1332}
1333
1334impl Default for GameVisuals {
1335 fn default() -> Self {
1336 Self {
1337 chaos_particle_speed_mult: 1.0,
1338 camera_fov_offset: 0.0,
1339 force_field_strength: 0.0,
1340 entity_emission_pulse: 0.0,
1341 vignette_intensity: 0.5,
1342 }
1343 }
1344}
1345
1346#[derive(Clone, Debug)]
1348pub struct AudioVisualBridge {
1349 smoothed_bass: f32,
1351 beat_pulse_timer: f32,
1353 prev_envelope: f32,
1355 beat_threshold: f32,
1357 prev_band_energies: [f32; 3],
1359}
1360
1361impl AudioVisualBridge {
1362 pub fn new() -> Self {
1363 Self {
1364 smoothed_bass: 0.0,
1365 beat_pulse_timer: 0.0,
1366 prev_envelope: 0.0,
1367 beat_threshold: 0.15,
1368 prev_band_energies: [0.0; 3],
1369 }
1370 }
1371
1372 pub fn compute_analysis(&mut self, audio_buffer: &[f32], sample_rate: u32) -> AudioAnalysis {
1378 if audio_buffer.is_empty() {
1379 return AudioAnalysis::default();
1380 }
1381
1382 let sr = sample_rate as f32;
1383 let n = audio_buffer.len();
1384
1385 let bass = band_energy(audio_buffer, sr, 20.0, 250.0);
1391 let mid = band_energy(audio_buffer, sr, 250.0, 4000.0);
1392 let high = band_energy(audio_buffer, sr, 4000.0, 16000.0);
1393
1394 let rms = (audio_buffer.iter().map(|s| s * s).sum::<f32>() / n as f32).sqrt();
1396
1397 let flux = (bass - self.prev_band_energies[0]).max(0.0)
1399 + (mid - self.prev_band_energies[1]).max(0.0);
1400 let beat_detected = flux > self.beat_threshold;
1401 self.prev_band_energies = [bass, mid, high];
1402
1403 let total_e = bass + mid + high + 1e-10;
1405 let centroid = (bass * 135.0 + mid * 2125.0 + high * 10000.0) / total_e;
1406
1407 self.prev_envelope = rms;
1408
1409 AudioAnalysis {
1410 bass_energy: bass,
1411 mid_energy: mid,
1412 high_energy: high,
1413 beat_detected,
1414 envelope: rms,
1415 spectral_centroid: centroid,
1416 }
1417 }
1418
1419 pub fn apply_to_visuals(
1421 &mut self,
1422 analysis: &AudioAnalysis,
1423 visuals: &mut GameVisuals,
1424 dt: f32,
1425 ) {
1426 self.smoothed_bass += (analysis.bass_energy - self.smoothed_bass) * (dt * 8.0).min(1.0);
1428 visuals.chaos_particle_speed_mult = 1.0 + self.smoothed_bass * 2.0;
1429
1430 if analysis.beat_detected {
1432 self.beat_pulse_timer = 0.1;
1433 }
1434 if self.beat_pulse_timer > 0.0 {
1435 visuals.camera_fov_offset = -0.005; self.beat_pulse_timer -= dt;
1437 } else {
1438 visuals.camera_fov_offset = 0.0;
1439 }
1440
1441 visuals.force_field_strength = analysis.mid_energy * 1.5;
1443
1444 visuals.entity_emission_pulse = analysis.high_energy * 2.0;
1446
1447 visuals.vignette_intensity = (0.6 - analysis.envelope).clamp(0.1, 0.8);
1449 }
1450}
1451
1452impl Default for AudioVisualBridge {
1453 fn default() -> Self {
1454 Self::new()
1455 }
1456}
1457
1458fn band_energy(buf: &[f32], sample_rate: f32, lo_hz: f32, hi_hz: f32) -> f32 {
1462 let n = buf.len() as f32;
1463 let num_probes = 4u32;
1464 let mut total = 0.0f32;
1465 for i in 0..num_probes {
1466 let freq = lo_hz + (hi_hz - lo_hz) * (i as f32 + 0.5) / num_probes as f32;
1467 let k = (freq * n / sample_rate).round();
1468 let w = TAU * k / n;
1469 let mut s0 = 0.0f32;
1471 let mut s1 = 0.0f32;
1472 let mut s2: f32;
1473 let coeff = 2.0 * w.cos();
1474 for &x in buf {
1475 s2 = s1;
1476 s1 = s0;
1477 s0 = x + coeff * s1 - s2;
1478 }
1479 let power = s0 * s0 + s1 * s1 - coeff * s0 * s1;
1480 total += power.abs();
1481 }
1482 (total / (num_probes as f32 * n)).sqrt()
1483}
1484
1485#[derive(Clone, Debug)]
1491pub struct ChaosRiftTracker {
1492 pub bar_count: u32,
1493 pub last_change_bar: u32,
1494 rng_state: u64,
1495}
1496
1497impl ChaosRiftTracker {
1498 pub fn new() -> Self {
1499 Self {
1500 bar_count: 0,
1501 last_change_bar: 0,
1502 rng_state: 0xC0FFEE,
1503 }
1504 }
1505
1506 fn xorshift(&mut self) -> u64 {
1507 self.rng_state ^= self.rng_state << 13;
1508 self.rng_state ^= self.rng_state >> 7;
1509 self.rng_state ^= self.rng_state << 17;
1510 self.rng_state
1511 }
1512
1513 pub fn tick_bar(&mut self) -> Option<u8> {
1515 self.bar_count += 1;
1516 if self.bar_count - self.last_change_bar >= 4 {
1517 self.last_change_bar = self.bar_count;
1518 let root = (self.xorshift() % 12) as u8 + 48; Some(root)
1520 } else {
1521 None
1522 }
1523 }
1524}
1525
1526impl Default for ChaosRiftTracker {
1527 fn default() -> Self {
1528 Self::new()
1529 }
1530}
1531
1532#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1538pub enum RoomType {
1539 Normal,
1540 Shop,
1541 Shrine,
1542 ChaosRift,
1543 BossArena,
1544}
1545
1546#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1548pub enum EnemyTier {
1549 Fodder,
1550 Standard,
1551 Elite,
1552 MiniBoss,
1553}
1554
1555pub struct MusicDirector {
1565 pub engine: MusicEngine,
1566 pub layer_stack: MusicLayerStack,
1567 pub corruption: CorruptionAudioProcessor,
1568 pub boss_controller: BossMusicController,
1569 pub audio_visual_bridge: AudioVisualBridge,
1570 pub chaos_tracker: ChaosRiftTracker,
1571 pub visuals: GameVisuals,
1572 pub current_vibe: GameVibe,
1573 pub current_floor: u32,
1574 beat_counter: u32,
1576 prev_bar: u32,
1578}
1579
1580impl std::fmt::Debug for MusicDirector {
1581 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1582 f.debug_struct("MusicDirector")
1583 .field("current_vibe", &self.current_vibe)
1584 .field("current_floor", &self.current_floor)
1585 .field("layer_stack", &self.layer_stack)
1586 .field("corruption", &self.corruption)
1587 .field("boss_controller", &self.boss_controller)
1588 .finish()
1589 }
1590}
1591
1592impl MusicDirector {
1593 pub fn new() -> Self {
1594 let mut engine = MusicEngine::new();
1595 engine.set_vibe(GameVibe::TitleScreen.to_engine_vibe());
1596
1597 Self {
1598 engine,
1599 layer_stack: MusicLayerStack::new(),
1600 corruption: CorruptionAudioProcessor::new(),
1601 boss_controller: BossMusicController::new(),
1602 audio_visual_bridge: AudioVisualBridge::new(),
1603 chaos_tracker: ChaosRiftTracker::new(),
1604 visuals: GameVisuals::default(),
1605 current_vibe: GameVibe::TitleScreen,
1606 current_floor: 1,
1607 beat_counter: 0,
1608 prev_bar: 0,
1609 }
1610 }
1611
1612 pub fn on_enter_room(&mut self, room_type: RoomType, floor: u32) {
1616 self.current_floor = floor;
1617 let vibe = match room_type {
1618 RoomType::Normal => GameVibe::Exploration,
1619 RoomType::Shop => GameVibe::Shop,
1620 RoomType::Shrine => GameVibe::Shrine,
1621 RoomType::ChaosRift => GameVibe::ChaosRift,
1622 RoomType::BossArena => GameVibe::Boss,
1623 };
1624 self.transition_vibe(vibe);
1625 apply_floor_profile(&mut self.layer_stack, &mut self.engine, floor);
1626 }
1627
1628 pub fn on_combat_start(&mut self, enemy_tier: EnemyTier) {
1630 let vibe = match enemy_tier {
1631 EnemyTier::Fodder | EnemyTier::Standard => GameVibe::Combat,
1632 EnemyTier::Elite | EnemyTier::MiniBoss => GameVibe::Combat,
1633 };
1634 self.transition_vibe(vibe);
1635
1636 if enemy_tier == EnemyTier::Elite || enemy_tier == EnemyTier::MiniBoss {
1638 self.engine.master_volume = 0.9;
1639 }
1640 }
1641
1642 pub fn on_boss_encounter(&mut self, boss_type: BossMusic) {
1644 self.transition_vibe(GameVibe::Boss);
1645 self.boss_controller.activate(boss_type, &mut self.layer_stack);
1646 }
1647
1648 pub fn on_combat_end(&mut self) {
1650 self.boss_controller.deactivate();
1651 self.engine.master_volume = 1.0;
1652 self.transition_vibe(GameVibe::Exploration);
1653 }
1654
1655 pub fn on_player_low_hp(&mut self) {
1657 self.transition_vibe(GameVibe::LowHP);
1658 let current = self.engine.current_bpm();
1660 let reduced = current * 0.85;
1661 self.layer_stack.beats_per_second = reduced / 60.0;
1662 }
1663
1664 pub fn on_player_death(&mut self) {
1666 self.boss_controller.deactivate();
1667 self.transition_vibe(GameVibe::Death);
1668 }
1669
1670 pub fn on_corruption_change(&mut self, level: u32) {
1672 self.corruption.process_corruption(level);
1673 }
1674
1675 pub fn on_floor_change(&mut self, floor: u32) {
1677 self.current_floor = floor;
1678 self.layer_stack.set_floor_depth(floor);
1679 apply_floor_profile(&mut self.layer_stack, &mut self.engine, floor);
1680 }
1681
1682 pub fn on_victory(&mut self) {
1684 self.boss_controller.deactivate();
1685 self.transition_vibe(GameVibe::Victory);
1686 }
1687
1688 fn transition_vibe(&mut self, vibe: GameVibe) {
1691 if self.current_vibe == vibe {
1692 return;
1693 }
1694 self.current_vibe = vibe;
1695 self.engine.set_vibe(vibe.to_engine_vibe());
1696 self.layer_stack.transition_to(vibe, DEFAULT_CROSSFADE_SECS);
1697
1698 if vibe == GameVibe::ChaosRift {
1699 self.chaos_tracker = ChaosRiftTracker::new();
1700 }
1701 }
1702
1703 pub fn update(&mut self, dt: f32, audio_buffer: &[f32], sample_rate: u32) {
1707 let mut notes = self.engine.tick(dt);
1709
1710 let layer_notes = self.layer_stack.update(dt);
1712 notes.extend(layer_notes);
1713
1714 self.boss_controller.process_notes(&mut notes, &mut self.layer_stack);
1716
1717 if self.current_vibe == GameVibe::ChaosRift {
1719 let bar = self.engine.current_bar();
1720 if bar != self.prev_bar {
1721 self.prev_bar = bar;
1722 if let Some(new_root) = self.chaos_tracker.tick_bar() {
1723 self.layer_stack.current_scale = Scale::new(
1724 new_root,
1725 ScaleType::Chromatic,
1726 );
1727 }
1728 }
1729 }
1730
1731 self.beat_counter = self.beat_counter.wrapping_add(1);
1733
1734 let analysis =
1736 self.audio_visual_bridge.compute_analysis(audio_buffer, sample_rate);
1737 self.audio_visual_bridge
1738 .apply_to_visuals(&analysis, &mut self.visuals, dt);
1739 }
1740
1741 pub fn visuals(&self) -> &GameVisuals {
1743 &self.visuals
1744 }
1745}
1746
1747impl Default for MusicDirector {
1748 fn default() -> Self {
1749 Self::new()
1750 }
1751}
1752
1753#[cfg(test)]
1758mod tests {
1759 use super::*;
1760
1761 #[test]
1764 fn vibe_configs_have_correct_count() {
1765 assert_eq!(VIBE_CONFIGS.len(), 10);
1766 }
1767
1768 #[test]
1769 fn title_screen_config_values() {
1770 let cfg = GameVibe::TitleScreen.config();
1771 assert_eq!(cfg.scale_type, ScaleType::Pentatonic);
1772 assert!((cfg.tempo_bpm - 72.0).abs() < 0.01);
1773 assert_eq!(cfg.instrument_set, InstrumentSet::EtherealPads);
1774 }
1775
1776 #[test]
1777 fn exploration_config_values() {
1778 let cfg = GameVibe::Exploration.config();
1779 assert_eq!(cfg.scale_type, ScaleType::Major);
1780 assert_eq!(cfg.key_root, "G");
1781 assert!((cfg.tempo_bpm - 110.0).abs() < 0.01);
1782 }
1783
1784 #[test]
1785 fn combat_config_values() {
1786 let cfg = GameVibe::Combat.config();
1787 assert_eq!(cfg.scale_type, ScaleType::NaturalMinor);
1788 assert!((cfg.tempo_bpm - 140.0).abs() < 0.01);
1789 }
1790
1791 #[test]
1792 fn boss_config_values() {
1793 let cfg = GameVibe::Boss.config();
1794 assert_eq!(cfg.scale_type, ScaleType::Diminished);
1795 assert_eq!(cfg.key_root, "Bb");
1796 assert!((cfg.tempo_bpm - 160.0).abs() < 0.01);
1797 }
1798
1799 #[test]
1800 fn vibe_to_engine_vibe_produces_valid_config() {
1801 let vc = GameVibe::Combat.to_engine_vibe();
1802 assert!(vc.bpm > 100.0);
1803 assert!(vc.bass_enabled);
1804 assert!(vc.melody_enabled);
1805 }
1806
1807 #[test]
1808 fn vibe_transition_changes_layers() {
1809 let mut stack = MusicLayerStack::new();
1810 stack.transition_to(GameVibe::Boss, 0.5);
1811 assert!(stack.layers[0].target_volume > 0.0);
1813 assert!(stack.layers[1].target_volume > 0.0);
1814 assert!(stack.layers[2].target_volume > 0.0);
1815 assert!(stack.layers[3].target_volume > 0.0);
1816 }
1817
1818 #[test]
1819 fn vibe_transition_exploration_disables_percussion_and_arrangement() {
1820 let mut stack = MusicLayerStack::new();
1821 stack.transition_to(GameVibe::Exploration, 0.5);
1822 assert!(stack.layers[0].target_volume > 0.0); assert!(stack.layers[1].target_volume > 0.0); assert!(stack.layers[2].target_volume < 0.01); assert!(stack.layers[3].target_volume < 0.01); }
1827
1828 #[test]
1831 fn corruption_tier_from_level() {
1832 assert_eq!(CorruptionTier::from_level(0), CorruptionTier::Clean);
1833 assert_eq!(CorruptionTier::from_level(50), CorruptionTier::Clean);
1834 assert_eq!(CorruptionTier::from_level(150), CorruptionTier::PitchWobble);
1835 assert_eq!(CorruptionTier::from_level(250), CorruptionTier::RhythmDrift);
1836 assert_eq!(CorruptionTier::from_level(350), CorruptionTier::FilterModulation);
1837 assert_eq!(CorruptionTier::from_level(500), CorruptionTier::GranularArtifacts);
1838 }
1839
1840 #[test]
1841 fn corruption_processor_clean_passthrough() {
1842 let mut proc = CorruptionAudioProcessor::new();
1843 proc.process_corruption(0);
1844 let out = proc.apply(0.5);
1845 assert!((out - 0.5).abs() < 0.01);
1846 }
1847
1848 #[test]
1849 fn corruption_processor_high_level_modifies_signal() {
1850 let mut proc = CorruptionAudioProcessor::new();
1851 proc.process_corruption(450);
1852 let mut changed = false;
1854 for i in 0..1000 {
1855 let input = (i as f32 * 0.1).sin() * 0.5;
1856 let out = proc.apply(input);
1857 if (out - input).abs() > 0.01 {
1858 changed = true;
1859 break;
1860 }
1861 }
1862 assert!(changed, "Expected corruption to modify the signal");
1863 }
1864
1865 #[test]
1866 fn pitch_wobble_default_is_clean() {
1867 let mut pw = PitchWobble::new();
1868 pw.set_intensity(0.0);
1869 let out = pw.apply(1.0);
1870 assert!((out - 1.0).abs() < 0.01);
1871 }
1872
1873 #[test]
1874 fn granular_bit_crush_reduces_precision() {
1875 let mut ga = GranularArtifacts::new();
1876 ga.bit_depth = 4.0;
1877 ga.stutter_probability = 0.0; let out = ga.apply(0.123456);
1879 let levels = 2.0f32.powf(4.0);
1881 let expected = (0.123456 * levels).round() / levels;
1882 assert!((out - expected).abs() < 0.001);
1883 }
1884
1885 #[test]
1888 fn floor_profile_early_floors_are_major() {
1889 let profile = floor_music_profile(1);
1890 assert_eq!(profile.scale, ScaleType::Major);
1891 assert!((profile.tempo_modifier - 1.0).abs() < 0.01);
1892 }
1893
1894 #[test]
1895 fn floor_profile_deep_floors_are_sparse() {
1896 let profile = floor_music_profile(80);
1897 assert_eq!(profile.scale, ScaleType::Chromatic);
1898 assert!(profile.arrangement_density < 0.2);
1899 }
1900
1901 #[test]
1902 fn floor_profile_100_plus_near_silence() {
1903 let profile = floor_music_profile(100);
1904 assert!(profile.arrangement_density < 0.05);
1905 assert!(profile.tempo_modifier < 0.6);
1906 }
1907
1908 #[test]
1909 fn floor_depth_adjusts_bass_drone() {
1910 let mut stack = MusicLayerStack::new();
1911 stack.set_floor_depth(1);
1912 let freq_1 = stack.layers[0].base_freq;
1913 stack.set_floor_depth(100);
1914 let freq_100 = stack.layers[0].base_freq;
1915 assert!(freq_1 > freq_100, "Floor 1 freq {freq_1} should be > floor 100 freq {freq_100}");
1917 }
1918
1919 #[test]
1922 fn boss_mirror_reverses_melody() {
1923 let mut ctrl = BossMusicController::new();
1924 let mut stack = MusicLayerStack::new();
1925 ctrl.activate(BossMusic::Mirror, &mut stack);
1926
1927 ctrl.mirror_buffer_note(440.0);
1929 ctrl.mirror_buffer_note(550.0);
1930 ctrl.mirror_buffer_note(660.0);
1931
1932 let n1 = ctrl.mirror_next_reversed().unwrap();
1934 let n2 = ctrl.mirror_next_reversed().unwrap();
1935 let n3 = ctrl.mirror_next_reversed().unwrap();
1936 assert!((n1 - 660.0).abs() < 0.01);
1937 assert!((n2 - 550.0).abs() < 0.01);
1938 assert!((n3 - 440.0).abs() < 0.01);
1939 }
1940
1941 #[test]
1942 fn boss_null_strips_layers_on_hp_loss() {
1943 let mut ctrl = BossMusicController::new();
1944 let mut stack = MusicLayerStack::new();
1945 ctrl.activate(BossMusic::Null, &mut stack);
1946 assert_eq!(ctrl.active_layer_count, 4);
1947
1948 ctrl.null_update_hp(0.85, &mut stack);
1950 assert_eq!(ctrl.active_layer_count, 3);
1951
1952 ctrl.null_update_hp(0.70, &mut stack);
1954 assert_eq!(ctrl.active_layer_count, 2);
1955 }
1956
1957 #[test]
1958 fn boss_committee_sets_5_4_time() {
1959 let mut ctrl = BossMusicController::new();
1960 let mut stack = MusicLayerStack::new();
1961 ctrl.activate(BossMusic::Committee, &mut stack);
1962 assert_eq!(ctrl.time_sig_numerator, 5);
1963 }
1964
1965 #[test]
1966 fn boss_algorithm_records_actions() {
1967 let mut ctrl = BossMusicController::new();
1968 let mut stack = MusicLayerStack::new();
1969 ctrl.activate(BossMusic::AlgorithmReborn, &mut stack);
1970
1971 ctrl.record_action(PlayerActionType::Magic);
1972 ctrl.record_action(PlayerActionType::Magic);
1973 ctrl.record_action(PlayerActionType::Melee);
1974
1975 assert_eq!(ctrl.dominant_action, PlayerActionType::Magic);
1976 }
1977
1978 #[test]
1979 fn boss_algorithm_phase_advance() {
1980 let mut ctrl = BossMusicController::new();
1981 let mut stack = MusicLayerStack::new();
1982 ctrl.activate(BossMusic::AlgorithmReborn, &mut stack);
1983 assert_eq!(ctrl.algorithm_phase, 1);
1984
1985 ctrl.algorithm_advance_phase(&mut stack);
1986 assert_eq!(ctrl.algorithm_phase, 2);
1987
1988 ctrl.algorithm_advance_phase(&mut stack);
1989 assert_eq!(ctrl.algorithm_phase, 3);
1990
1991 ctrl.algorithm_advance_phase(&mut stack);
1993 assert_eq!(ctrl.algorithm_phase, 3);
1994 }
1995
1996 #[test]
1999 fn audio_analysis_empty_buffer() {
2000 let mut bridge = AudioVisualBridge::new();
2001 let analysis = bridge.compute_analysis(&[], 48000);
2002 assert!(!analysis.beat_detected);
2003 assert!(analysis.envelope < 0.001);
2004 }
2005
2006 #[test]
2007 fn audio_analysis_sine_has_energy() {
2008 let mut bridge = AudioVisualBridge::new();
2009 let sr = 48000u32;
2010 let buf: Vec<f32> = (0..1024)
2012 .map(|i| (TAU * 200.0 * i as f32 / sr as f32).sin() * 0.8)
2013 .collect();
2014 let analysis = bridge.compute_analysis(&buf, sr);
2015 assert!(analysis.bass_energy > 0.0, "Expected bass energy from 200 Hz sine");
2016 assert!(analysis.envelope > 0.1, "Expected non-trivial envelope");
2017 }
2018
2019 #[test]
2020 fn audio_visual_bridge_beat_pulse() {
2021 let mut bridge = AudioVisualBridge::new();
2022 let mut visuals = GameVisuals::default();
2023 let analysis = AudioAnalysis {
2024 bass_energy: 0.5,
2025 mid_energy: 0.3,
2026 high_energy: 0.1,
2027 beat_detected: true,
2028 envelope: 0.4,
2029 spectral_centroid: 2000.0,
2030 };
2031 bridge.apply_to_visuals(&analysis, &mut visuals, 1.0 / 60.0);
2032 assert!(visuals.camera_fov_offset < 0.0);
2034 assert!(visuals.chaos_particle_speed_mult > 1.0);
2036 }
2037
2038 #[test]
2041 fn director_initializes_to_title_screen() {
2042 let dir = MusicDirector::new();
2043 assert_eq!(dir.current_vibe, GameVibe::TitleScreen);
2044 }
2045
2046 #[test]
2047 fn director_room_transitions() {
2048 let mut dir = MusicDirector::new();
2049 dir.on_enter_room(RoomType::Shop, 5);
2050 assert_eq!(dir.current_vibe, GameVibe::Shop);
2051
2052 dir.on_enter_room(RoomType::ChaosRift, 10);
2053 assert_eq!(dir.current_vibe, GameVibe::ChaosRift);
2054 }
2055
2056 #[test]
2057 fn director_combat_flow() {
2058 let mut dir = MusicDirector::new();
2059 dir.on_combat_start(EnemyTier::Standard);
2060 assert_eq!(dir.current_vibe, GameVibe::Combat);
2061
2062 dir.on_combat_end();
2063 assert_eq!(dir.current_vibe, GameVibe::Exploration);
2064 }
2065
2066 #[test]
2067 fn director_boss_encounter() {
2068 let mut dir = MusicDirector::new();
2069 dir.on_boss_encounter(BossMusic::Mirror);
2070 assert_eq!(dir.current_vibe, GameVibe::Boss);
2071 assert_eq!(dir.boss_controller.boss, Some(BossMusic::Mirror));
2072 }
2073
2074 #[test]
2075 fn director_low_hp_reduces_tempo() {
2076 let mut dir = MusicDirector::new();
2077 dir.on_enter_room(RoomType::Normal, 1);
2078 let bpm_before = dir.engine.current_bpm();
2079 dir.on_player_low_hp();
2080 let bps_after = dir.layer_stack.beats_per_second;
2081 assert!(bps_after < bpm_before / 60.0);
2083 }
2084
2085 #[test]
2086 fn director_corruption_propagates() {
2087 let mut dir = MusicDirector::new();
2088 dir.on_corruption_change(250);
2089 assert_eq!(dir.corruption.tier, CorruptionTier::RhythmDrift);
2090 }
2091
2092 #[test]
2093 fn director_floor_change() {
2094 let mut dir = MusicDirector::new();
2095 dir.on_floor_change(50);
2096 assert_eq!(dir.current_floor, 50);
2097 }
2098
2099 #[test]
2100 fn director_update_runs_without_panic() {
2101 let mut dir = MusicDirector::new();
2102 dir.on_enter_room(RoomType::Normal, 1);
2103 let buf = vec![0.0f32; 1024];
2105 for _ in 0..60 {
2106 dir.update(1.0 / 60.0, &buf, 48000);
2107 }
2108 }
2109
2110 #[test]
2111 fn director_victory_flow() {
2112 let mut dir = MusicDirector::new();
2113 dir.on_boss_encounter(BossMusic::Null);
2114 assert_eq!(dir.current_vibe, GameVibe::Boss);
2115 dir.on_victory();
2116 assert_eq!(dir.current_vibe, GameVibe::Victory);
2117 assert_eq!(dir.boss_controller.boss, None);
2118 }
2119
2120 #[test]
2123 fn chaos_rift_tracker_changes_key_every_4_bars() {
2124 let mut tracker = ChaosRiftTracker::new();
2125 assert!(tracker.tick_bar().is_none());
2127 assert!(tracker.tick_bar().is_none());
2128 assert!(tracker.tick_bar().is_none());
2129 assert!(tracker.tick_bar().is_some());
2131 assert!(tracker.tick_bar().is_none());
2133 assert!(tracker.tick_bar().is_none());
2134 assert!(tracker.tick_bar().is_none());
2135 assert!(tracker.tick_bar().is_some());
2137 }
2138
2139 #[test]
2142 fn layer_crossfade_reaches_target() {
2143 let mut layer = MusicLayer::new(LayerType::Melody);
2144 layer.fade_in(0.5);
2145 for _ in 0..60 {
2147 layer.update(1.0 / 60.0);
2148 }
2149 assert!(
2150 (layer.volume - 1.0).abs() < 0.05,
2151 "Expected volume ~1.0, got {}",
2152 layer.volume,
2153 );
2154 }
2155
2156 #[test]
2157 fn layer_fade_out_deactivates() {
2158 let mut layer = MusicLayer::new(LayerType::Percussion);
2159 layer.active = true;
2160 layer.volume = 1.0;
2161 layer.fade_out(0.5);
2162 for _ in 0..120 {
2163 layer.update(1.0 / 60.0);
2164 }
2165 assert!(!layer.active);
2166 assert!(layer.volume < 0.01);
2167 }
2168
2169 #[test]
2172 fn note_name_c_is_60() {
2173 assert_eq!(note_name_to_midi("C"), 60);
2174 }
2175
2176 #[test]
2177 fn note_name_bb_is_70() {
2178 assert_eq!(note_name_to_midi("Bb"), 70);
2179 }
2180
2181 #[test]
2182 fn note_name_f_sharp_is_66() {
2183 assert_eq!(note_name_to_midi("F#"), 66);
2184 }
2185}