Wip: refacto
This commit is contained in:
@@ -2,9 +2,11 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::Stream;
|
||||
use crossbeam_channel::Receiver;
|
||||
use doux::{Engine, EngineMetrics};
|
||||
use ringbuf::{traits::*, HeapRb};
|
||||
use rustfft::{num_complex::Complex, FftPlanner};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use super::AudioCommand;
|
||||
|
||||
@@ -75,6 +77,7 @@ impl SpectrumBuffer {
|
||||
|
||||
const FFT_SIZE: usize = 512;
|
||||
const NUM_BANDS: usize = 32;
|
||||
const ANALYSIS_RING_SIZE: usize = 4096;
|
||||
|
||||
struct SpectrumAnalyzer {
|
||||
ring: Vec<f32>,
|
||||
@@ -151,6 +154,72 @@ impl SpectrumAnalyzer {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnalysisHandle {
|
||||
running: Arc<AtomicBool>,
|
||||
#[allow(dead_code)]
|
||||
thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl AnalysisHandle {
|
||||
#[allow(dead_code)]
|
||||
pub fn shutdown(mut self) {
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
if let Some(t) = self.thread.take() {
|
||||
let _ = t.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AnalysisHandle {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_analysis_thread(
|
||||
sample_rate: f32,
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
) -> (ringbuf::HeapProd<f32>, AnalysisHandle) {
|
||||
let rb = HeapRb::<f32>::new(ANALYSIS_RING_SIZE);
|
||||
let (producer, consumer) = rb.split();
|
||||
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = Arc::clone(&running);
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("fft-analysis".into())
|
||||
.spawn(move || {
|
||||
analysis_loop(consumer, spectrum_buffer, sample_rate, running_clone);
|
||||
})
|
||||
.expect("Failed to spawn FFT analysis thread");
|
||||
|
||||
let handle = AnalysisHandle {
|
||||
running,
|
||||
thread: Some(thread),
|
||||
};
|
||||
|
||||
(producer, handle)
|
||||
}
|
||||
|
||||
fn analysis_loop(
|
||||
mut consumer: ringbuf::HeapCons<f32>,
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
sample_rate: f32,
|
||||
running: Arc<AtomicBool>,
|
||||
) {
|
||||
let mut analyzer = SpectrumAnalyzer::new(sample_rate);
|
||||
let mut local_buf = [0.0f32; 256];
|
||||
|
||||
while running.load(Ordering::Relaxed) {
|
||||
let count = consumer.pop_slice(&mut local_buf);
|
||||
if count > 0 {
|
||||
analyzer.feed(&local_buf[..count], &spectrum_buffer);
|
||||
} else {
|
||||
thread::sleep(std::time::Duration::from_micros(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioStreamConfig {
|
||||
pub output_device: Option<String>,
|
||||
pub channels: u16,
|
||||
@@ -165,7 +234,7 @@ pub fn build_stream(
|
||||
spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
metrics: Arc<EngineMetrics>,
|
||||
initial_samples: Vec<doux::sample::SampleEntry>,
|
||||
) -> Result<(Stream, f32), String> {
|
||||
) -> Result<(Stream, f32, AnalysisHandle), String> {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = match &config.output_device {
|
||||
@@ -193,12 +262,13 @@ pub fn build_stream(
|
||||
|
||||
let sr = sample_rate;
|
||||
let channels = config.channels as usize;
|
||||
let max_voices = config.max_voices;
|
||||
let metrics_clone = Arc::clone(&metrics);
|
||||
|
||||
let mut engine = Engine::new_with_metrics(sample_rate, channels, Arc::clone(&metrics));
|
||||
let mut engine = Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics));
|
||||
engine.sample_index = initial_samples;
|
||||
|
||||
let mut analyzer = SpectrumAnalyzer::new(sample_rate);
|
||||
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
@@ -224,7 +294,7 @@ pub fn build_stream(
|
||||
AudioCommand::ResetEngine => {
|
||||
let old_samples = std::mem::take(&mut engine.sample_index);
|
||||
engine =
|
||||
Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
|
||||
Engine::new_with_metrics(sr, channels, max_voices, Arc::clone(&metrics_clone));
|
||||
engine.sample_index = old_samples;
|
||||
}
|
||||
}
|
||||
@@ -234,11 +304,11 @@ pub fn build_stream(
|
||||
engine.process_block(data, &[], &[]);
|
||||
scope_buffer.write(&engine.output);
|
||||
|
||||
// Feed mono mix to spectrum analyzer
|
||||
let mono: Vec<f32> = engine.output.chunks(channels)
|
||||
.map(|ch| ch.iter().sum::<f32>() / channels as f32)
|
||||
.collect();
|
||||
analyzer.feed(&mono, &spectrum_buffer);
|
||||
// Feed mono mix to analysis thread via ring buffer (non-blocking)
|
||||
for chunk in engine.output.chunks(channels) {
|
||||
let mono = chunk.iter().sum::<f32>() / channels as f32;
|
||||
let _ = fft_producer.try_push(mono);
|
||||
}
|
||||
},
|
||||
|err| eprintln!("stream error: {err}"),
|
||||
None,
|
||||
@@ -248,5 +318,5 @@ pub fn build_stream(
|
||||
stream
|
||||
.play()
|
||||
.map_err(|e| format!("Failed to play stream: {e}"))?;
|
||||
Ok((stream, sample_rate))
|
||||
Ok((stream, sample_rate, analysis_handle))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user