use crate::hardware::memory_banks::SoundBankPoolAccess; use crate::model::Sample; use std::sync::Arc; /// Maximum number of simultaneous audio voices supported by the hardware. pub const MAX_CHANNELS: usize = 16; /// Standard sample rate for the final audio output. pub const OUTPUT_SAMPLE_RATE: u32 = 48000; /// Defines if a sample should stop at the end or repeat indefinitely. #[derive(Clone, Copy, Debug, PartialEq, Default)] pub enum LoopMode { /// Play once and stop. #[default] Off, /// Return to the start (or loop_start) when reaching the end. On, } /// State of a single playback voice (channel). /// /// The Core maintains this state to provide information to the App (e.g., is_playing), /// but the actual real-time mixing is performed by the Host using commands. pub struct Channel { /// The actual sample data being played. pub sample: Option>, /// Whether this channel is currently active. pub active: bool, /// Current playback position within the sample (fractional for pitch shifting). pub pos: f64, /// Playback speed multiplier (1.0 = original speed). pub pitch: f64, /// Voice volume (0-255). pub volume: u8, /// Stereo panning (0=Full Left, 127=Center, 255=Full Right). pub pan: u8, /// Loop configuration for this voice. pub loop_mode: LoopMode, /// Playback priority (used for voice stealing policies). pub priority: u8, } impl Default for Channel { fn default() -> Self { Self { sample: None, active: false, pos: 0.0, pitch: 1.0, volume: 255, pan: 127, loop_mode: LoopMode::Off, priority: 0, } } } /// Commands sent from the Core to the Host audio backend. /// /// Because the Core logic runs at 60Hz and Audio is generated at 48kHz, /// we use an asynchronous command queue to synchronize them. pub enum AudioCommand { /// Start playing a sample on a specific voice. Play { sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode, }, /// Immediately stop playback on a voice. Stop { voice_id: usize, }, /// Update volume of an ongoing playback. SetVolume { voice_id: usize, volume: u8, }, /// Update panning of an ongoing playback. SetPan { voice_id: usize, pan: u8, }, /// Update pitch of an ongoing playback. SetPitch { voice_id: usize, pitch: f64, }, /// Pause all audio processing. MasterPause, /// Resume audio processing. MasterResume, } /// PROMETEU Audio Subsystem. /// /// Models a multi-channel PCM sampler (SPU). /// The audio system in Prometeu is **command-based**. This means the Core /// doesn't generate raw audio samples; instead, it sends high-level commands /// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend /// (e.g., CPAL on desktop) then consumes these commands and performs the /// actual mixing at the native hardware sample rate. /// /// ### Key Features: /// - **16 Simultaneous Voices**: Independent volume, pan, and pitch. /// - **Sample-based Synthesis**: Plays PCM data stored in SoundBanks. /// - **Stereo Output**: 48kHz output target. pub trait AudioBridge { fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) -> (); fn play_sample(&mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) -> (); fn stop(&mut self, voice_id: usize) -> (); fn set_volume(&mut self, voice_id: usize, volume: u8) -> (); fn set_pan(&mut self, voice_id: usize, pan: u8) -> (); fn set_pitch(&mut self, voice_id: usize, pitch: f64) -> (); fn is_playing(&self, voice_id: usize) -> bool; fn clear_commands(&mut self) -> (); } pub struct Audio { /// Local state of the hardware voices. This state is used for logic /// (e.g., checking if a sound is still playing) and is synchronized with the Host. pub voices: [Channel; MAX_CHANNELS], /// Queue of pending commands to be processed by the Host mixer. /// This queue is typically flushed and sent to the Host once per frame. pub commands: Vec, /// Interface to access sound memory banks (ARAM). pub sound_banks: Arc, } impl AudioBridge for Audio { fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, loop_mode) } fn play_sample(&mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { self.play_sample(sample, voice_id, volume, pan, pitch, priority, loop_mode) } fn stop(&mut self, voice_id: usize) { self.stop(voice_id) } fn set_volume(&mut self, voice_id: usize, volume: u8) { self.set_volume(voice_id, volume) } fn set_pan(&mut self, voice_id: usize, pan: u8) { self.set_pan(voice_id, pan) } fn set_pitch(&mut self, voice_id: usize, pitch: f64) { self.set_pitch(voice_id, pitch) } fn is_playing(&self, voice_id: usize) -> bool { self.is_playing(voice_id) } fn clear_commands(&mut self) { self.clear_commands() } } impl Audio { /// Initializes the audio system with empty voices and sound bank access. pub fn new(sound_banks: Arc) -> Self { Self { voices: std::array::from_fn(|_| Channel::default()), commands: Vec::new(), sound_banks, } } pub fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { if voice_id >= MAX_CHANNELS { return; } // Resolve the sample from the hardware pools let sample = self.sound_banks.sound_bank_slot(bank_id as usize) .and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone)); if let Some(s) = sample { println!("[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.", bank_id, sample_id, voice_id); self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode); } else { eprintln!("[Audio] Failed to resolve sample from bank {} sample {}.", bank_id, sample_id); } } pub fn play_sample(&mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { if voice_id >= MAX_CHANNELS { return; } // Update local state self.voices[voice_id] = Channel { sample: Some(Arc::clone(&sample)), active: true, pos: 0.0, pitch, volume, pan, loop_mode, priority, }; // Push command to the host self.commands.push(AudioCommand::Play { sample, voice_id, volume, pan, pitch, priority, loop_mode, }); } pub fn stop(&mut self, voice_id: usize) { if voice_id < MAX_CHANNELS { self.voices[voice_id].active = false; self.voices[voice_id].sample = None; self.commands.push(AudioCommand::Stop { voice_id }); } } pub fn set_volume(&mut self, voice_id: usize, volume: u8) { if voice_id < MAX_CHANNELS { self.voices[voice_id].volume = volume; self.commands.push(AudioCommand::SetVolume { voice_id, volume }); } } pub fn set_pan(&mut self, voice_id: usize, pan: u8) { if voice_id < MAX_CHANNELS { self.voices[voice_id].pan = pan; self.commands.push(AudioCommand::SetPan { voice_id, pan }); } } pub fn set_pitch(&mut self, voice_id: usize, pitch: f64) { if voice_id < MAX_CHANNELS { self.voices[voice_id].pitch = pitch; self.commands.push(AudioCommand::SetPitch { voice_id, pitch }); } } pub fn is_playing(&self, voice_id: usize) -> bool { if voice_id < MAX_CHANNELS { self.voices[voice_id].active } else { false } } /// Clears the command queue. The Host should consume this every frame. pub fn clear_commands(&mut self) { self.commands.clear(); } }