237 lines
7.8 KiB
Rust
237 lines
7.8 KiB
Rust
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
use prometeu_drivers::{AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
|
use ringbuf::traits::{Consumer, Producer, Split};
|
|
use ringbuf::HeapRb;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use prometeu_hal::LoopMode;
|
|
|
|
pub struct HostAudio {
|
|
pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
|
|
pub perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
|
|
_stream: Option<cpal::Stream>,
|
|
}
|
|
|
|
impl HostAudio {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
producer: None,
|
|
perf_consumer: None,
|
|
_stream: None,
|
|
}
|
|
}
|
|
|
|
pub fn init(&mut self) {
|
|
let host = cpal::default_host();
|
|
let device = host
|
|
.default_output_device()
|
|
.expect("no output device available");
|
|
|
|
let config = cpal::StreamConfig {
|
|
channels: 2,
|
|
sample_rate: cpal::SampleRate(OUTPUT_SAMPLE_RATE),
|
|
buffer_size: cpal::BufferSize::Default,
|
|
};
|
|
|
|
let rb = HeapRb::<AudioCommand>::new(1024);
|
|
let (prod, mut cons) = rb.split();
|
|
|
|
self.producer = Some(prod);
|
|
|
|
let mut mixer = AudioMixer::new();
|
|
|
|
// To pass performance data from the audio thread to the main thread
|
|
let audio_perf_rb = HeapRb::<u64>::new(64);
|
|
let (mut perf_prod, perf_cons) = audio_perf_rb.split();
|
|
|
|
let stream = device
|
|
.build_output_stream(
|
|
&config,
|
|
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
|
// Consumes commands from ringbuffer
|
|
while let Some(cmd) = cons.try_pop() {
|
|
mixer.process_command(cmd);
|
|
}
|
|
// Mixes audio
|
|
mixer.fill_buffer(data);
|
|
// Sends processing time in microseconds
|
|
let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64);
|
|
},
|
|
|err| eprintln!("audio stream error: {}", err),
|
|
None,
|
|
)
|
|
.expect("failed to build audio stream");
|
|
|
|
stream.play().expect("failed to play audio stream");
|
|
self._stream = Some(stream);
|
|
self.perf_consumer = Some(perf_cons);
|
|
}
|
|
|
|
pub fn send_commands(&mut self, commands: &mut Vec<AudioCommand>) {
|
|
if let Some(producer) = &mut self.producer {
|
|
for cmd in commands.drain(..) {
|
|
if let Err(_) = producer.try_push(cmd) {
|
|
eprintln!("[HostAudio] Command ringbuffer full, dropping command.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_stats(&mut self, stats: &mut crate::stats::HostStats) {
|
|
if let Some(cons) = &mut self.perf_consumer {
|
|
while let Some(us) = cons.try_pop() {
|
|
stats.record_audio_perf(us);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct AudioMixer {
|
|
voices: [Channel; MAX_CHANNELS],
|
|
pub last_processing_time: Duration,
|
|
paused: bool,
|
|
}
|
|
|
|
impl AudioMixer {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
voices: Default::default(),
|
|
last_processing_time: Duration::ZERO,
|
|
paused: false,
|
|
}
|
|
}
|
|
|
|
pub fn process_command(&mut self, cmd: AudioCommand) {
|
|
match cmd {
|
|
AudioCommand::Play {
|
|
sample,
|
|
voice_id,
|
|
volume,
|
|
pan,
|
|
pitch,
|
|
priority,
|
|
loop_mode,
|
|
} => {
|
|
if voice_id < MAX_CHANNELS {
|
|
println!("[AudioMixer] Playing voice {}: vol={}, pitch={}, loop={:?}", voice_id, volume, pitch, loop_mode);
|
|
self.voices[voice_id] = Channel {
|
|
sample: Some(sample),
|
|
active: true,
|
|
pos: 0.0,
|
|
pitch,
|
|
volume,
|
|
pan,
|
|
loop_mode,
|
|
priority,
|
|
};
|
|
}
|
|
}
|
|
AudioCommand::Stop { voice_id } => {
|
|
if voice_id < MAX_CHANNELS {
|
|
self.voices[voice_id].active = false;
|
|
self.voices[voice_id].sample = None;
|
|
}
|
|
}
|
|
AudioCommand::SetVolume { voice_id, volume } => {
|
|
if voice_id < MAX_CHANNELS {
|
|
self.voices[voice_id].volume = volume;
|
|
}
|
|
}
|
|
AudioCommand::SetPan { voice_id, pan } => {
|
|
if voice_id < MAX_CHANNELS {
|
|
self.voices[voice_id].pan = pan;
|
|
}
|
|
}
|
|
AudioCommand::SetPitch { voice_id, pitch } => {
|
|
if voice_id < MAX_CHANNELS {
|
|
self.voices[voice_id].pitch = pitch;
|
|
}
|
|
}
|
|
AudioCommand::MasterPause => {
|
|
println!("[AudioMixer] Master Pause");
|
|
self.paused = true;
|
|
}
|
|
AudioCommand::MasterResume => {
|
|
println!("[AudioMixer] Master Resume");
|
|
self.paused = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
|
|
let start = std::time::Instant::now();
|
|
// Zeroes the buffer (stereo)
|
|
for sample in buffer.iter_mut() {
|
|
*sample = 0.0;
|
|
}
|
|
|
|
if self.paused {
|
|
self.last_processing_time = start.elapsed();
|
|
return;
|
|
}
|
|
|
|
for voice in self.voices.iter_mut() {
|
|
let sample_data = match &voice.sample {
|
|
Some(s) => s,
|
|
None => continue,
|
|
};
|
|
|
|
let pitch_ratio = sample_data.sample_rate as f64 / OUTPUT_SAMPLE_RATE as f64;
|
|
let step = voice.pitch * pitch_ratio;
|
|
|
|
let vol_f = voice.volume as f32 / 255.0;
|
|
let pan_f = voice.pan as f32 / 255.0;
|
|
let vol_l = vol_f * (1.0 - pan_f).sqrt();
|
|
let vol_r = vol_f * pan_f.sqrt();
|
|
|
|
for frame in buffer.chunks_exact_mut(2) {
|
|
let pos_int = voice.pos as usize;
|
|
let pos_fract = voice.pos - pos_int as f64;
|
|
|
|
if pos_int >= sample_data.data.len() {
|
|
voice.active = false;
|
|
voice.sample = None;
|
|
break;
|
|
}
|
|
|
|
// Linear Interpolation
|
|
let s1 = sample_data.data[pos_int] as f32 / 32768.0;
|
|
let s2 = if pos_int + 1 < sample_data.data.len() {
|
|
sample_data.data[pos_int + 1] as f32 / 32768.0
|
|
} else if voice.loop_mode == LoopMode::On {
|
|
let loop_start = sample_data.loop_start.unwrap_or(0) as usize;
|
|
sample_data.data[loop_start] as f32 / 32768.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let sample_val = s1 + (s2 - s1) * pos_fract as f32;
|
|
|
|
frame[0] += sample_val * vol_l;
|
|
frame[1] += sample_val * vol_r;
|
|
|
|
voice.pos += step;
|
|
|
|
let end_pos = sample_data.loop_end.map(|e| e as f64).unwrap_or(sample_data.data.len() as f64);
|
|
|
|
if voice.pos >= end_pos {
|
|
if voice.loop_mode == LoopMode::On {
|
|
let loop_start = sample_data.loop_start.unwrap_or(0) as f64;
|
|
voice.pos = loop_start + (voice.pos - end_pos);
|
|
} else {
|
|
voice.active = false;
|
|
voice.sample = None;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final clamp to avoid clipping (optional if using f32, but good for fidelity)
|
|
for sample in buffer.iter_mut() {
|
|
*sample = sample.clamp(-1.0, 1.0);
|
|
}
|
|
self.last_processing_time = start.elapsed();
|
|
}
|
|
}
|