fixes PR005

This commit is contained in:
bQUARKz 2026-03-03 08:09:30 +00:00
parent 5ec270dd43
commit 4fb872dde5
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 245 additions and 28 deletions

View File

@ -1,15 +1,96 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use prometeu_drivers::{AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE}; use prometeu_drivers::{AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
use prometeu_hal::LoopMode; use prometeu_hal::LoopMode;
use ringbuf::HeapRb;
use ringbuf::traits::{Consumer, Producer, Split}; use ringbuf::traits::{Consumer, Producer, Split};
use ringbuf::HeapRb;
use std::error::Error;
use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
trait AudioStreamHandle {}
impl AudioStreamHandle for cpal::Stream {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostAudioError {
NoOutputDevice,
BuildStream(String),
PlayStream(String),
}
impl fmt::Display for HostAudioError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HostAudioError::NoOutputDevice => write!(f, "no output device available"),
HostAudioError::BuildStream(err) => write!(f, "failed to build audio stream: {}", err),
HostAudioError::PlayStream(err) => write!(f, "failed to play audio stream: {}", err),
}
}
}
impl Error for HostAudioError {}
trait AudioRuntime {
type Device;
type Stream: AudioStreamHandle + 'static;
fn default_output_device(&self) -> Option<Self::Device>;
fn build_output_stream<D, E>(
&self,
device: &Self::Device,
config: &cpal::StreamConfig,
data_callback: D,
error_callback: E,
) -> Result<Self::Stream, HostAudioError>
where
D: FnMut(&mut [f32]) + Send + 'static,
E: FnMut(String) + Send + 'static;
fn play_stream(&self, stream: &Self::Stream) -> Result<(), HostAudioError>;
}
struct CpalAudioRuntime;
impl AudioRuntime for CpalAudioRuntime {
type Device = cpal::Device;
type Stream = cpal::Stream;
fn default_output_device(&self) -> Option<Self::Device> {
cpal::default_host().default_output_device()
}
fn build_output_stream<D, E>(
&self,
device: &Self::Device,
config: &cpal::StreamConfig,
mut data_callback: D,
mut error_callback: E,
) -> Result<Self::Stream, HostAudioError>
where
D: FnMut(&mut [f32]) + Send + 'static,
E: FnMut(String) + Send + 'static,
{
device
.build_output_stream(
config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| data_callback(data),
move |err| error_callback(err.to_string()),
None,
)
.map_err(|err| HostAudioError::BuildStream(err.to_string()))
}
fn play_stream(&self, stream: &Self::Stream) -> Result<(), HostAudioError> {
stream.play().map_err(|err| HostAudioError::PlayStream(err.to_string()))
}
}
pub struct HostAudio { pub struct HostAudio {
pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>, pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
pub perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>, pub perf_consumer: Option<ringbuf::wrap::CachingCons<Arc<HeapRb<u64>>>>,
_stream: Option<cpal::Stream>, _stream: Option<Box<dyn AudioStreamHandle>>,
} }
impl HostAudio { impl HostAudio {
@ -17,9 +98,16 @@ impl HostAudio {
Self { producer: None, perf_consumer: None, _stream: None } Self { producer: None, perf_consumer: None, _stream: None }
} }
pub fn init(&mut self) { pub fn init(&mut self) -> Result<(), HostAudioError> {
let host = cpal::default_host(); self.init_with_runtime(&CpalAudioRuntime)
let device = host.default_output_device().expect("no output device available"); }
fn init_with_runtime<R: AudioRuntime>(&mut self, runtime: &R) -> Result<(), HostAudioError> {
self.producer = None;
self.perf_consumer = None;
self._stream = None;
let device = runtime.default_output_device().ok_or(HostAudioError::NoOutputDevice)?;
let config = cpal::StreamConfig { let config = cpal::StreamConfig {
channels: 2, channels: 2,
@ -30,18 +118,16 @@ impl HostAudio {
let rb = HeapRb::<AudioCommand>::new(1024); let rb = HeapRb::<AudioCommand>::new(1024);
let (prod, mut cons) = rb.split(); let (prod, mut cons) = rb.split();
self.producer = Some(prod);
let mut mixer = AudioMixer::new(); let mut mixer = AudioMixer::new();
// To pass performance data from the audio thread to the main thread // To pass performance data from the audio thread to the main thread
let audio_perf_rb = HeapRb::<u64>::new(64); let audio_perf_rb = HeapRb::<u64>::new(64);
let (mut perf_prod, perf_cons) = audio_perf_rb.split(); let (mut perf_prod, perf_cons) = audio_perf_rb.split();
let stream = device let stream = runtime.build_output_stream(
.build_output_stream( &device,
&config, &config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { move |data: &mut [f32]| {
// Consumes commands from ringbuffer // Consumes commands from ringbuffer
while let Some(cmd) = cons.try_pop() { while let Some(cmd) = cons.try_pop() {
mixer.process_command(cmd); mixer.process_command(cmd);
@ -52,13 +138,13 @@ impl HostAudio {
let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64); let _ = perf_prod.try_push(mixer.last_processing_time.as_micros() as u64);
}, },
|err| eprintln!("audio stream error: {}", err), |err| eprintln!("audio stream error: {}", err),
None, )?;
)
.expect("failed to build audio stream");
stream.play().expect("failed to play audio stream"); runtime.play_stream(&stream)?;
self._stream = Some(stream); self.producer = Some(prod);
self.perf_consumer = Some(perf_cons); self.perf_consumer = Some(perf_cons);
self._stream = Some(Box::new(stream));
Ok(())
} }
pub fn send_commands(&mut self, commands: &mut Vec<AudioCommand>) { pub fn send_commands(&mut self, commands: &mut Vec<AudioCommand>) {
@ -68,6 +154,8 @@ impl HostAudio {
eprintln!("[HostAudio] Command ringbuffer full, dropping command."); eprintln!("[HostAudio] Command ringbuffer full, dropping command.");
} }
} }
} else {
commands.clear();
} }
} }
@ -80,6 +168,133 @@ impl HostAudio {
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::stats::HostStats;
struct FakeStream;
impl AudioStreamHandle for FakeStream {}
#[derive(Clone, Copy)]
struct FakeDevice;
struct FakeRuntime {
has_device: bool,
build_result: Result<(), HostAudioError>,
play_result: Result<(), HostAudioError>,
}
impl AudioRuntime for FakeRuntime {
type Device = FakeDevice;
type Stream = FakeStream;
fn default_output_device(&self) -> Option<Self::Device> {
self.has_device.then_some(FakeDevice)
}
fn build_output_stream<D, E>(
&self,
_device: &Self::Device,
_config: &cpal::StreamConfig,
_data_callback: D,
_error_callback: E,
) -> Result<Self::Stream, HostAudioError>
where
D: FnMut(&mut [f32]) + Send + 'static,
E: FnMut(String) + Send + 'static,
{
self.build_result.clone().map(|_| FakeStream)
}
fn play_stream(&self, _stream: &Self::Stream) -> Result<(), HostAudioError> {
self.play_result.clone()
}
}
#[test]
fn test_send_commands_without_audio_drops_pending_commands() {
let mut audio = HostAudio::new();
let mut commands = vec![AudioCommand::MasterPause, AudioCommand::MasterResume];
audio.send_commands(&mut commands);
assert!(commands.is_empty());
assert!(audio.producer.is_none());
}
#[test]
fn test_update_stats_without_audio_is_safe() {
let mut audio = HostAudio::new();
let mut stats = HostStats::new();
audio.update_stats(&mut stats);
assert_eq!(stats.audio_load_accum_us, 0);
assert_eq!(stats.audio_load_samples, 0);
}
#[test]
fn test_init_returns_error_when_no_output_device_exists() {
let mut audio = HostAudio::new();
let runtime = FakeRuntime { has_device: false, build_result: Ok(()), play_result: Ok(()) };
let result = audio.init_with_runtime(&runtime);
assert_eq!(result, Err(HostAudioError::NoOutputDevice));
assert!(audio.producer.is_none());
assert!(audio.perf_consumer.is_none());
assert!(audio._stream.is_none());
}
#[test]
fn test_init_returns_error_when_stream_build_fails() {
let mut audio = HostAudio::new();
let runtime = FakeRuntime {
has_device: true,
build_result: Err(HostAudioError::BuildStream("simulated build failure".to_string())),
play_result: Ok(()),
};
let result = audio.init_with_runtime(&runtime);
assert_eq!(result, Err(HostAudioError::BuildStream("simulated build failure".to_string())));
assert!(audio.producer.is_none());
assert!(audio.perf_consumer.is_none());
assert!(audio._stream.is_none());
}
#[test]
fn test_init_returns_error_when_stream_play_fails() {
let mut audio = HostAudio::new();
let runtime = FakeRuntime {
has_device: true,
build_result: Ok(()),
play_result: Err(HostAudioError::PlayStream("simulated play failure".to_string())),
};
let result = audio.init_with_runtime(&runtime);
assert_eq!(result, Err(HostAudioError::PlayStream("simulated play failure".to_string())));
assert!(audio.producer.is_none());
assert!(audio.perf_consumer.is_none());
assert!(audio._stream.is_none());
}
#[test]
fn test_init_populates_audio_state_on_success() {
let mut audio = HostAudio::new();
let runtime = FakeRuntime { has_device: true, build_result: Ok(()), play_result: Ok(()) };
audio.init_with_runtime(&runtime).unwrap();
assert!(audio.producer.is_some());
assert!(audio.perf_consumer.is_some());
assert!(audio._stream.is_some());
}
}
pub struct AudioMixer { pub struct AudioMixer {
voices: [Channel; MAX_CHANNELS], voices: [Channel; MAX_CHANNELS],
pub last_processing_time: Duration, pub last_processing_time: Duration,

View File

@ -7,8 +7,8 @@ use crate::stats::HostStats;
use crate::utilities::draw_rgb565_to_rgba8; use crate::utilities::draw_rgb565_to_rgba8;
use pixels::wgpu::PresentMode; use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture}; use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::AudioCommand;
use prometeu_drivers::hardware::Hardware; use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::AudioCommand;
use prometeu_firmware::{BootTarget, Firmware}; use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::telemetry::CertificationConfig; use prometeu_hal::telemetry::CertificationConfig;
@ -238,7 +238,9 @@ impl ApplicationHandler for HostRunner {
self.pixels = Some(pixels); self.pixels = Some(pixels);
self.audio.init(); if let Err(err) = self.audio.init() {
eprintln!("[HostAudio] Disabled: {}", err);
}
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
} }