172 lines
4.8 KiB
Rust

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<Arc<Sample>>,
/// 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<Sample>,
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<AudioCommand>,
}
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<Sample>, 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();
}
}