This commit is contained in:
2026-01-21 17:05:30 +01:00
commit 67322381c3
59 changed files with 10421 additions and 0 deletions

141
src/engine/audio.rs Normal file
View File

@@ -0,0 +1,141 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::Stream;
use crossbeam_channel::Receiver;
use doux::{Engine, EngineMetrics};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use super::AudioCommand;
pub struct ScopeBuffer {
pub samples: [AtomicU32; 64],
peak_left: AtomicU32,
peak_right: AtomicU32,
}
impl ScopeBuffer {
pub fn new() -> Self {
Self {
samples: std::array::from_fn(|_| AtomicU32::new(0)),
peak_left: AtomicU32::new(0),
peak_right: AtomicU32::new(0),
}
}
pub fn write(&self, data: &[f32]) {
let mut peak_l: f32 = 0.0;
let mut peak_r: f32 = 0.0;
for (i, atom) in self.samples.iter().enumerate() {
let idx = i * 2;
let left = data.get(idx).copied().unwrap_or(0.0);
let right = data.get(idx + 1).copied().unwrap_or(0.0);
peak_l = peak_l.max(left.abs());
peak_r = peak_r.max(right.abs());
atom.store(left.to_bits(), Ordering::Relaxed);
}
self.peak_left.store(peak_l.to_bits(), Ordering::Relaxed);
self.peak_right.store(peak_r.to_bits(), Ordering::Relaxed);
}
pub fn read(&self) -> [f32; 64] {
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
}
pub fn peaks(&self) -> (f32, f32) {
let left = f32::from_bits(self.peak_left.load(Ordering::Relaxed));
let right = f32::from_bits(self.peak_right.load(Ordering::Relaxed));
(left, right)
}
}
pub struct AudioStreamConfig {
pub output_device: Option<String>,
pub channels: u16,
pub buffer_size: u32,
}
pub fn build_stream(
config: &AudioStreamConfig,
audio_rx: Receiver<AudioCommand>,
scope_buffer: Arc<ScopeBuffer>,
metrics: Arc<EngineMetrics>,
initial_samples: Vec<doux::sample::SampleEntry>,
) -> Result<(Stream, f32), String> {
let host = cpal::default_host();
let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name)
.ok_or_else(|| format!("Device not found: {name}"))?,
None => host
.default_output_device()
.ok_or("No default output device")?,
};
let default_config = device.default_output_config().map_err(|e| e.to_string())?;
let sample_rate = default_config.sample_rate().0 as f32;
let buffer_size = if config.buffer_size > 0 {
cpal::BufferSize::Fixed(config.buffer_size)
} else {
cpal::BufferSize::Default
};
let stream_config = cpal::StreamConfig {
channels: config.channels,
sample_rate: default_config.sample_rate(),
buffer_size,
};
let sr = sample_rate;
let channels = config.channels as usize;
let metrics_clone = Arc::clone(&metrics);
let mut engine = Engine::new_with_metrics(sample_rate, channels, Arc::clone(&metrics));
engine.sample_index = initial_samples;
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _| {
let buffer_samples = data.len() / channels;
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
while let Ok(cmd) = audio_rx.try_recv() {
match cmd {
AudioCommand::Evaluate(s) => {
engine.evaluate(&s);
}
AudioCommand::Hush => {
engine.hush();
}
AudioCommand::Panic => {
engine.panic();
}
AudioCommand::LoadSamples(samples) => {
engine.sample_index.extend(samples);
}
AudioCommand::ResetEngine => {
let old_samples = std::mem::take(&mut engine.sample_index);
engine =
Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
engine.sample_index = old_samples;
}
}
}
engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(data, &[], &[]);
scope_buffer.write(&engine.output);
},
|err| eprintln!("stream error: {err}"),
None,
)
.map_err(|e| format!("Failed to build stream: {e}"))?;
stream
.play()
.map_err(|e| format!("Failed to play stream: {e}"))?;
Ok((stream, sample_rate))
}