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 { /// Reference to the PCM data being played. pub sample: Option>, /// 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, 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, }, } /// PROMETEU Audio Subsystem. /// /// Models a multi-channel PCM sampler. /// It works like an "Audio CPU": the Game Core sends high-level commands /// every frame, and the Host backend implements the low-level mixer. pub struct Audio { /// Local state of the 16 hardware voices. pub voices: [Channel; MAX_CHANNELS], /// Queue of pending commands to be processed by the Host mixer. pub commands: Vec, } impl Audio { /// Initializes the audio system with empty voices. pub fn new() -> Self { const EMPTY_CHANNEL: Channel = Channel { sample: None, pos: 0.0, pitch: 1.0, volume: 255, pan: 127, loop_mode: LoopMode::Off, priority: 0, }; Self { voices: [EMPTY_CHANNEL; MAX_CHANNELS], commands: Vec::new(), } } pub fn play(&mut self, sample: Arc, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) { if voice_id < MAX_CHANNELS { 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.commands.push(AudioCommand::Stop { voice_id }); } } pub fn set_volume(&mut self, voice_id: usize, volume: u8) { if voice_id < MAX_CHANNELS { 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.commands.push(AudioCommand::SetPan { voice_id, pan }); } } pub fn set_pitch(&mut self, voice_id: usize, pitch: f64) { if voice_id < MAX_CHANNELS { 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].sample.is_some() } else { false } } /// Clears the command queue. The Host should consume this every frame. pub fn clear_commands(&mut self) { self.commands.clear(); } }