use prometeu_hal::{AudioBridge, AudioOpStatus}; 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; use crate::memory_banks::SoundBankPoolAccess; /// Looping mode for samples (re-exported from the hardware contract). pub use prometeu_hal::LoopMode; use prometeu_hal::sample::Sample; /// 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 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: prometeu_hal::LoopMode, ) -> AudioOpStatus { let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On, }; self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm) } fn play_sample( &mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode, ) -> AudioOpStatus { let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On, }; self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm) } 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, } } #[allow(clippy::too_many_arguments)] 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, ) -> AudioOpStatus { if voice_id >= MAX_CHANNELS { return AudioOpStatus::VoiceInvalid; } // 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 { AudioOpStatus::SampleNotFound } } #[allow(clippy::too_many_arguments)] pub fn play_sample( &mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode, ) -> AudioOpStatus { if voice_id >= MAX_CHANNELS { return AudioOpStatus::VoiceInvalid; } // 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, }); AudioOpStatus::Ok } 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(); } }