fixes PR005
This commit is contained in:
parent
5ec270dd43
commit
4fb872dde5
@ -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,35 +118,33 @@ 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);
|
||||||
}
|
}
|
||||||
// Mixes audio
|
// Mixes audio
|
||||||
mixer.fill_buffer(data);
|
mixer.fill_buffer(data);
|
||||||
// Sends processing time in microseconds
|
// Sends processing time in microseconds
|
||||||
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,
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user