Skip to main content

vst3_host/
audio.rs

1//! Audio types and utilities for VST3 host
2
3/// Audio buffers for plugin processing
4#[derive(Debug)]
5pub struct AudioBuffers {
6    /// Input audio buffers, indexed `[channel][sample]`.
7    pub inputs: Vec<Vec<f32>>,
8    /// Output audio buffers, indexed `[channel][sample]`.
9    pub outputs: Vec<Vec<f32>>,
10    /// Sample rate in Hz
11    pub sample_rate: f64,
12    /// Number of samples per buffer
13    pub block_size: usize,
14}
15
16impl AudioBuffers {
17    /// Create new audio buffers
18    pub fn new(
19        input_channels: usize,
20        output_channels: usize,
21        block_size: usize,
22        sample_rate: f64,
23    ) -> Self {
24        let inputs = vec![vec![0.0; block_size]; input_channels];
25        let outputs = vec![vec![0.0; block_size]; output_channels];
26
27        Self {
28            inputs,
29            outputs,
30            sample_rate,
31            block_size,
32        }
33    }
34
35    /// Clear all buffers to silence
36    pub fn clear(&mut self) {
37        for buffer in &mut self.inputs {
38            buffer.fill(0.0);
39        }
40        for buffer in &mut self.outputs {
41            buffer.fill(0.0);
42        }
43    }
44
45    /// Get the number of input channels
46    pub fn input_channels(&self) -> usize {
47        self.inputs.len()
48    }
49
50    /// Get the number of output channels
51    pub fn output_channels(&self) -> usize {
52        self.outputs.len()
53    }
54}
55
56/// Audio level information for a single channel
57#[derive(Debug, Clone, Copy)]
58pub struct ChannelLevel {
59    /// Peak level (0.0 to 1.0, where 1.0 = 0dB)
60    pub peak: f32,
61    /// RMS level (0.0 to 1.0)
62    pub rms: f32,
63    /// Peak hold level (0.0 to 1.0)
64    pub peak_hold: f32,
65}
66
67impl Default for ChannelLevel {
68    fn default() -> Self {
69        Self {
70            peak: 0.0,
71            rms: 0.0,
72            peak_hold: 0.0,
73        }
74    }
75}
76
77impl ChannelLevel {
78    /// Convert peak level to decibels
79    pub fn peak_db(&self) -> f32 {
80        if self.peak <= 0.0 {
81            -f32::INFINITY
82        } else {
83            20.0 * self.peak.log10()
84        }
85    }
86
87    /// Convert RMS level to decibels
88    pub fn rms_db(&self) -> f32 {
89        if self.rms <= 0.0 {
90            -f32::INFINITY
91        } else {
92            20.0 * self.rms.log10()
93        }
94    }
95
96    /// Check if the signal is clipping (> 0dB)
97    pub fn is_clipping(&self) -> bool {
98        self.peak > 1.0
99    }
100}
101
102/// Audio level information for all channels
103#[derive(Debug, Clone)]
104pub struct AudioLevels {
105    /// Level information for each channel
106    pub channels: Vec<ChannelLevel>,
107}
108
109impl AudioLevels {
110    /// Create new audio levels for the given number of channels
111    pub fn new(channel_count: usize) -> Self {
112        Self {
113            channels: vec![ChannelLevel::default(); channel_count],
114        }
115    }
116
117    /// Update levels from audio buffers
118    pub fn update_from_buffers(&mut self, buffers: &[Vec<f32>]) {
119        for (i, buffer) in buffers.iter().enumerate() {
120            if i >= self.channels.len() {
121                break;
122            }
123
124            // Calculate peak
125            let peak = buffer.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
126
127            // Calculate RMS (guard against a zero-length channel buffer → 0/0 = NaN).
128            let sum_squares: f32 = buffer.iter().map(|&x| x * x).sum();
129            let rms = if buffer.is_empty() {
130                0.0
131            } else {
132                (sum_squares / buffer.len() as f32).sqrt()
133            };
134
135            // Update channel levels
136            let channel = &mut self.channels[i];
137            channel.peak = peak;
138            channel.rms = rms;
139
140            // Update peak hold if necessary
141            if peak > channel.peak_hold {
142                channel.peak_hold = peak;
143            }
144        }
145    }
146
147    /// Reset peak hold values
148    pub fn reset_peak_hold(&mut self) {
149        for channel in &mut self.channels {
150            channel.peak_hold = channel.peak;
151        }
152    }
153
154    /// Check if any channel is clipping
155    pub fn is_clipping(&self) -> bool {
156        self.channels.iter().any(|ch| ch.is_clipping())
157    }
158}
159
160/// A VST3 speaker arrangement: a bitmask where each set bit is one channel (so the channel
161/// count is the number of set bits). Wraps the SDK's `SpeakerArrangement` (a `u64` bitmask);
162/// use the named constants or [`from_raw`](Self::from_raw).
163#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164pub struct SpeakerArrangement(pub u64);
165
166impl SpeakerArrangement {
167    /// No channels (`kEmpty`).
168    pub const EMPTY: Self = Self(0);
169    /// Mono (`kMono` = front-center).
170    pub const MONO: Self = Self(0x0008_0000);
171    /// Stereo L/R (`kStereo`).
172    pub const STEREO: Self = Self(0x3);
173    /// Stereo surround Ls/Rs (`kStereoSurround`).
174    pub const STEREO_SURROUND: Self = Self(0x30);
175
176    /// Wrap a raw VST3 `SpeakerArrangement` bitmask.
177    pub fn from_raw(bits: u64) -> Self {
178        Self(bits)
179    }
180
181    /// The raw VST3 bitmask.
182    pub fn raw(self) -> u64 {
183        self.0
184    }
185
186    /// Number of channels in this arrangement (the count of set bits).
187    pub fn channel_count(self) -> usize {
188        self.0.count_ones() as usize
189    }
190}
191
192/// The speaker arrangements of a plugin's audio input and output buses.
193#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
194pub struct BusArrangements {
195    /// Arrangement of each audio input bus, in bus-index order.
196    pub inputs: Vec<SpeakerArrangement>,
197    /// Arrangement of each audio output bus, in bus-index order.
198    pub outputs: Vec<SpeakerArrangement>,
199}
200
201/// A single-channel peak meter with falling ballistics and a timed peak-hold marker —
202/// the behaviour a level meter UI wants but [`AudioLevels`]'s sticky `peak_hold` doesn't give.
203///
204/// Time is **injected** ([`push`](Self::push) takes `now: Instant`) so the meter is
205/// deterministic and independent of any clock — pass `Instant::now()` from real code, or
206/// synthetic instants in tests. Feed it the per-block peak amplitude; read [`level`](Self::level)
207/// for the falling meter value and [`peak_hold`](Self::peak_hold) for the held marker.
208///
209/// ```
210/// use std::time::{Duration, Instant};
211/// use vst3_host::audio::PeakMeter;
212///
213/// let mut meter = PeakMeter::new(20.0, Duration::from_secs(2)); // 20 dB/s fall, 2 s hold
214/// let t0 = Instant::now();
215/// meter.push(0.8, t0);
216/// assert_eq!(meter.level(), 0.8);
217/// // After silence the displayed level falls but the hold marker stays put (within the window).
218/// meter.push(0.0, t0 + Duration::from_millis(100));
219/// assert!(meter.level() < 0.8 && meter.level() > 0.0);
220/// assert_eq!(meter.peak_hold(), 0.8);
221/// ```
222#[derive(Debug, Clone)]
223pub struct PeakMeter {
224    fall_db_per_sec: f32,
225    hold: std::time::Duration,
226    level: f32,
227    peak_hold: f32,
228    peak_hold_at: Option<std::time::Instant>,
229    last: Option<std::time::Instant>,
230}
231
232impl PeakMeter {
233    /// Below this the level snaps to exactly 0.0 (≈ -100 dB), so a meter fully empties
234    /// instead of asymptotically approaching zero forever.
235    const SILENCE: f32 = 1e-5;
236
237    /// Create a meter that falls at `fall_db_per_sec` decibels per second and holds the peak
238    /// marker for `hold` before it, too, begins to fall. A typical UI meter uses ~20 dB/s and
239    /// a 1–3 second hold.
240    pub fn new(fall_db_per_sec: f32, hold: std::time::Duration) -> Self {
241        Self {
242            fall_db_per_sec: fall_db_per_sec.max(0.0),
243            hold,
244            level: 0.0,
245            peak_hold: 0.0,
246            peak_hold_at: None,
247            last: None,
248        }
249    }
250
251    /// Linear gain after falling for `dt`, e.g. `10^(-(dB/s · dt)/20)`.
252    fn decay(&self, dt: std::time::Duration) -> f32 {
253        let db = self.fall_db_per_sec * dt.as_secs_f32();
254        10f32.powf(-db / 20.0)
255    }
256
257    /// Update with a new block's peak amplitude (`0.0..`) observed at `now`. The displayed
258    /// level rises instantly to a louder peak and decays toward quieter input; the hold marker
259    /// latches the loudest value and only starts falling once `hold` has elapsed since it was set.
260    pub fn push(&mut self, block_peak: f32, now: std::time::Instant) {
261        // Treat non-finite input (NaN/±inf from a misbehaving plugin) as silence so it can't
262        // permanently poison the meter — `inf * decay` stays inf and would never fall.
263        let block_peak = if block_peak.is_finite() {
264            block_peak.max(0.0)
265        } else {
266            0.0
267        };
268        let decay = match self.last {
269            Some(prev) => self.decay(now.saturating_duration_since(prev)),
270            None => 1.0,
271        };
272
273        self.level = (self.level * decay).max(block_peak);
274        if self.level < Self::SILENCE {
275            self.level = 0.0;
276        }
277
278        if block_peak >= self.peak_hold {
279            // New loudest value — latch it and restart the hold timer.
280            self.peak_hold = block_peak;
281            self.peak_hold_at = Some(now);
282        } else if self
283            .peak_hold_at
284            .is_some_and(|at| now.saturating_duration_since(at) > self.hold)
285        {
286            // Hold window expired — the marker falls at the same ballistic, never below `level`.
287            self.peak_hold = (self.peak_hold * decay).max(self.level);
288            if self.peak_hold < Self::SILENCE {
289                self.peak_hold = 0.0;
290            }
291        }
292
293        self.last = Some(now);
294    }
295
296    /// The current falling-meter level (`0.0..`).
297    pub fn level(&self) -> f32 {
298        self.level
299    }
300
301    /// The held peak marker (`0.0..`).
302    pub fn peak_hold(&self) -> f32 {
303        self.peak_hold
304    }
305
306    /// Reset the meter to silence.
307    pub fn reset(&mut self) {
308        self.level = 0.0;
309        self.peak_hold = 0.0;
310        self.peak_hold_at = None;
311        self.last = None;
312    }
313}
314
315/// A moving-window RMS estimator over the most recent `N` samples.
316///
317/// Unlike [`AudioLevels`]'s per-block RMS (which resets every buffer), this gives a smooth
318/// level over a fixed time window regardless of block size — feed it samples or whole blocks
319/// and read [`rms`](Self::rms). The window length in samples is `window_secs · sample_rate`.
320///
321/// ```
322/// use vst3_host::audio::RmsWindow;
323///
324/// let mut rms = RmsWindow::new(4);
325/// for _ in 0..4 { rms.push_sample(0.5); }
326/// assert!((rms.rms() - 0.5).abs() < 1e-6); // constant 0.5 → RMS 0.5
327/// ```
328#[derive(Debug, Clone)]
329pub struct RmsWindow {
330    capacity: usize,
331    squares: std::collections::VecDeque<f32>,
332    // f64 accumulator so a meter running for the lifetime of a stream (millions of
333    // add/subtract cycles) doesn't drift from f32 rounding error.
334    sum: f64,
335}
336
337impl RmsWindow {
338    /// Create a window holding the most recent `window_samples` samples (minimum 1).
339    pub fn new(window_samples: usize) -> Self {
340        let capacity = window_samples.max(1);
341        Self {
342            capacity,
343            squares: std::collections::VecDeque::with_capacity(capacity),
344            sum: 0.0,
345        }
346    }
347
348    /// Create a window sized for `window_secs` of audio at `sample_rate` Hz.
349    pub fn from_duration(window_secs: f32, sample_rate: f64) -> Self {
350        Self::new((window_secs.max(0.0) as f64 * sample_rate).round() as usize)
351    }
352
353    /// Add one sample, evicting the oldest if the window is full.
354    pub fn push_sample(&mut self, sample: f32) {
355        let sq = sample * sample;
356        if self.squares.len() == self.capacity {
357            if let Some(old) = self.squares.pop_front() {
358                self.sum -= old as f64;
359            }
360        }
361        self.squares.push_back(sq);
362        self.sum += sq as f64;
363    }
364
365    /// Add a whole block of samples.
366    pub fn push_block(&mut self, block: &[f32]) {
367        for &s in block {
368            self.push_sample(s);
369        }
370    }
371
372    /// Current RMS over the samples in the window (`0.0` when empty).
373    pub fn rms(&self) -> f32 {
374        if self.squares.is_empty() {
375            return 0.0;
376        }
377        // Guard against tiny negative drift from float subtraction.
378        (self.sum.max(0.0) / self.squares.len() as f64).sqrt() as f32
379    }
380
381    /// Number of samples currently in the window.
382    pub fn len(&self) -> usize {
383        self.squares.len()
384    }
385
386    /// Whether the window holds no samples yet.
387    pub fn is_empty(&self) -> bool {
388        self.squares.is_empty()
389    }
390
391    /// Drop all samples.
392    pub fn clear(&mut self) {
393        self.squares.clear();
394        self.sum = 0.0;
395    }
396}
397
398/// Audio processing configuration
399#[derive(Debug, Clone, Copy)]
400pub struct AudioConfig {
401    /// Sample rate in Hz
402    pub sample_rate: f64,
403    /// Block size in samples
404    pub block_size: usize,
405    /// Number of input channels
406    pub input_channels: usize,
407    /// Number of output channels
408    pub output_channels: usize,
409    /// Transport tempo in beats per minute, advertised to plugins in the host
410    /// `ProcessContext` (drives tempo-synced DSP such as LFOs and synced delays).
411    pub tempo: f64,
412    /// Time signature numerator (beats per bar), advertised in the `ProcessContext`.
413    pub time_sig_numerator: i32,
414    /// Time signature denominator (note value of one beat), advertised in the
415    /// `ProcessContext`.
416    pub time_sig_denominator: i32,
417}
418
419impl Default for AudioConfig {
420    fn default() -> Self {
421        Self {
422            sample_rate: 44100.0,
423            block_size: 512,
424            input_channels: 0,
425            output_channels: 2,
426            tempo: 120.0,
427            time_sig_numerator: 4,
428            time_sig_denominator: 4,
429        }
430    }
431}
432
433/// Audio stream trait for controlling playback
434pub trait AudioStream: Send {
435    /// Start playback
436    fn play(&self) -> Result<(), Box<dyn std::error::Error>>;
437
438    /// Pause playback
439    fn pause(&self) -> Result<(), Box<dyn std::error::Error>>;
440}
441
442/// Audio backend trait for creating audio streams
443#[allow(clippy::type_complexity)] // Box<dyn FnMut...> callbacks are intrinsic to the API
444pub trait AudioBackend: Send + Sync {
445    /// The stream type this backend produces
446    type Stream: AudioStream + Send + 'static;
447    /// The device type this backend uses
448    type Device: Send + Sync;
449    /// The error type this backend returns
450    type Error: std::error::Error + Send + Sync + 'static;
451
452    /// Enumerate available output devices
453    fn enumerate_output_devices(&self) -> Result<Vec<Self::Device>, Self::Error>;
454
455    /// Enumerate available input devices
456    fn enumerate_input_devices(&self) -> Result<Vec<Self::Device>, Self::Error>;
457
458    /// Get the default output device
459    fn default_output_device(&self) -> Option<Self::Device>;
460
461    /// Get the default input device
462    fn default_input_device(&self) -> Option<Self::Device>;
463
464    /// Create an output stream
465    fn create_output_stream(
466        &self,
467        device: &Self::Device,
468        config: AudioConfig,
469        data_callback: Box<dyn FnMut(&mut [f32]) + Send>,
470        error_callback: Box<dyn FnMut(Self::Error) + Send>,
471    ) -> Result<Self::Stream, Self::Error>;
472
473    /// Create an input stream
474    fn create_input_stream(
475        &self,
476        device: &Self::Device,
477        config: AudioConfig,
478        data_callback: Box<dyn FnMut(&[f32]) + Send>,
479        error_callback: Box<dyn FnMut(Self::Error) + Send>,
480    ) -> Result<Self::Stream, Self::Error>;
481}
482
483/// Write deinterleaved channel buffers to a 32-bit float WAV file (`WAVE_FORMAT_IEEE_FLOAT`).
484///
485/// `channels[ch][frame]`; all channels must be the same length. Used by offline rendering
486/// (e.g. [`crate::simple::render_to_wav`]) and audio export. No external dependency.
487pub fn write_wav<P: AsRef<std::path::Path>>(
488    path: P,
489    channels: &[Vec<f32>],
490    sample_rate: u32,
491) -> crate::error::Result<()> {
492    use crate::error::Error;
493    use std::io::Write;
494
495    let num_channels = channels.len().max(1) as u16;
496    let frames = channels.iter().map(|c| c.len()).min().unwrap_or(0);
497    let bits_per_sample: u16 = 32;
498    let block_align = num_channels * (bits_per_sample / 8);
499    let byte_rate = sample_rate * block_align as u32;
500    let data_size = (frames * num_channels as usize * (bits_per_sample / 8) as usize) as u32;
501
502    let mut buf: Vec<u8> = Vec::with_capacity(44 + data_size as usize);
503    buf.extend_from_slice(b"RIFF");
504    buf.extend_from_slice(&(36 + data_size).to_le_bytes());
505    buf.extend_from_slice(b"WAVE");
506    buf.extend_from_slice(b"fmt ");
507    buf.extend_from_slice(&16u32.to_le_bytes());
508    buf.extend_from_slice(&3u16.to_le_bytes()); // IEEE float
509    buf.extend_from_slice(&num_channels.to_le_bytes());
510    buf.extend_from_slice(&sample_rate.to_le_bytes());
511    buf.extend_from_slice(&byte_rate.to_le_bytes());
512    buf.extend_from_slice(&block_align.to_le_bytes());
513    buf.extend_from_slice(&bits_per_sample.to_le_bytes());
514    buf.extend_from_slice(b"data");
515    buf.extend_from_slice(&data_size.to_le_bytes());
516    // Interleave channels frame by frame.
517    for f in 0..frames {
518        for ch in channels {
519            buf.extend_from_slice(&ch[f].to_le_bytes());
520        }
521    }
522
523    let mut file =
524        std::fs::File::create(path).map_err(|e| Error::Other(format!("create wav: {e}")))?;
525    file.write_all(&buf)
526        .map_err(|e| Error::Other(format!("write wav: {e}")))?;
527    Ok(())
528}
529
530/// Read a WAV file written as 32-bit float (`WAVE_FORMAT_IEEE_FLOAT`) or 16-bit PCM, returning
531/// deinterleaved channels (`channels[ch][frame]`) and the sample rate. The inverse of
532/// [`write_wav`]; used to feed a recorded signal into a plugin's input.
533pub fn read_wav<P: AsRef<std::path::Path>>(path: P) -> crate::error::Result<(Vec<Vec<f32>>, u32)> {
534    use crate::error::Error;
535    let data = std::fs::read(path).map_err(|e| Error::Other(format!("read wav: {e}")))?;
536    let err = |m: &str| Error::Other(format!("invalid wav: {m}"));
537    if data.len() < 44 || &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
538        return Err(err("not a RIFF/WAVE file"));
539    }
540    // Walk chunks to find fmt and data (handles extra chunks before data).
541    let (mut fmt_tag, mut channels, mut sample_rate, mut bits) = (0u16, 0u16, 0u32, 0u16);
542    let mut data_range: Option<(usize, usize)> = None;
543    let mut pos = 12;
544    while pos + 8 <= data.len() {
545        let id = &data[pos..pos + 4];
546        let size = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
547            as usize;
548        let body = pos + 8;
549        if id == b"fmt " && body + 16 <= data.len() {
550            fmt_tag = u16::from_le_bytes([data[body], data[body + 1]]);
551            channels = u16::from_le_bytes([data[body + 2], data[body + 3]]);
552            sample_rate = u32::from_le_bytes([
553                data[body + 4],
554                data[body + 5],
555                data[body + 6],
556                data[body + 7],
557            ]);
558            bits = u16::from_le_bytes([data[body + 14], data[body + 15]]);
559        } else if id == b"data" {
560            data_range = Some((body, (body + size).min(data.len())));
561        }
562        pos = body + size + (size & 1); // chunks are word-aligned
563    }
564    let (ds, de) = data_range.ok_or_else(|| err("no data chunk"))?;
565    if channels == 0 {
566        return Err(err("zero channels"));
567    }
568    let nch = channels as usize;
569    let mut out: Vec<Vec<f32>> = vec![Vec::new(); nch];
570    let bytes = &data[ds..de];
571    match (fmt_tag, bits) {
572        (3, 32) => {
573            for (i, frame) in bytes.chunks_exact(4 * nch).enumerate() {
574                let _ = i;
575                for (ch, s) in frame.chunks_exact(4).enumerate() {
576                    out[ch].push(f32::from_le_bytes([s[0], s[1], s[2], s[3]]));
577                }
578            }
579        }
580        (1, 16) => {
581            for frame in bytes.chunks_exact(2 * nch) {
582                for (ch, s) in frame.chunks_exact(2).enumerate() {
583                    let v = i16::from_le_bytes([s[0], s[1]]) as f32 / 32768.0;
584                    out[ch].push(v);
585                }
586            }
587        }
588        _ => return Err(err("unsupported format (need 32-bit float or 16-bit PCM)")),
589    }
590    Ok((out, sample_rate))
591}
592
593/// A source that fills a plugin's input buffers each block — a generated test signal or a
594/// preloaded audio file — so effects can be auditioned/rendered with a known input.
595pub trait InputSource: Send {
596    /// Fill `inputs[ch][..frames]` with the next block of audio at `sample_rate`.
597    fn fill(&mut self, inputs: &mut [Vec<f32>], frames: usize, sample_rate: f64);
598}
599
600/// A host-synthesized input signal (no capture device needed). Carries its own cursor so blocks
601/// are continuous across calls.
602#[derive(Debug, Clone)]
603pub enum SignalSource {
604    /// Silence (all zeros).
605    Silence,
606    /// A sine tone at `freq` Hz and linear `amplitude` (0..1).
607    Sine {
608        /// Frequency in Hz.
609        freq: f32,
610        /// Linear amplitude (0..1).
611        amplitude: f32,
612        /// Running phase in radians (cursor; start at 0.0).
613        phase: f64,
614    },
615    /// White noise with linear `amplitude` (0..1).
616    WhiteNoise {
617        /// Linear amplitude (0..1).
618        amplitude: f32,
619        /// xorshift RNG state (cursor; seed non-zero).
620        rng: u64,
621    },
622    /// A preloaded multi-channel sample (e.g. from [`read_wav`]), played from `pos`.
623    Wav {
624        /// Channel samples (`samples[ch][frame]`).
625        samples: std::sync::Arc<Vec<Vec<f32>>>,
626        /// Playback cursor (frame index).
627        pos: usize,
628        /// Loop back to the start at the end instead of going silent.
629        looping: bool,
630    },
631}
632
633impl SignalSource {
634    /// A sine tone.
635    pub fn sine(freq: f32, amplitude: f32) -> Self {
636        SignalSource::Sine {
637            freq,
638            amplitude,
639            phase: 0.0,
640        }
641    }
642    /// White noise (deterministic from a fixed seed).
643    pub fn white_noise(amplitude: f32) -> Self {
644        SignalSource::WhiteNoise {
645            amplitude,
646            rng: 0x9E37_79B9_7F4A_7C15,
647        }
648    }
649    /// A preloaded WAV/sample buffer.
650    pub fn wav(samples: Vec<Vec<f32>>, looping: bool) -> Self {
651        SignalSource::Wav {
652            samples: std::sync::Arc::new(samples),
653            pos: 0,
654            looping,
655        }
656    }
657}
658
659impl InputSource for SignalSource {
660    fn fill(&mut self, inputs: &mut [Vec<f32>], frames: usize, sample_rate: f64) {
661        for ch in inputs.iter_mut() {
662            if ch.len() < frames {
663                ch.resize(frames, 0.0);
664            }
665        }
666        match self {
667            SignalSource::Silence => {
668                for ch in inputs.iter_mut() {
669                    for s in &mut ch[..frames] {
670                        *s = 0.0;
671                    }
672                }
673            }
674            SignalSource::Sine {
675                freq,
676                amplitude,
677                phase,
678            } => {
679                let step = std::f64::consts::TAU * *freq as f64 / sample_rate.max(1.0);
680                for f in 0..frames {
681                    let v = (phase.sin() as f32) * *amplitude;
682                    for ch in inputs.iter_mut() {
683                        ch[f] = v;
684                    }
685                    *phase = (*phase + step) % std::f64::consts::TAU;
686                }
687            }
688            SignalSource::WhiteNoise { amplitude, rng } => {
689                for f in 0..frames {
690                    // xorshift64
691                    let mut x = *rng;
692                    x ^= x << 13;
693                    x ^= x >> 7;
694                    x ^= x << 17;
695                    *rng = x;
696                    // Map to [-1, 1) then scale.
697                    let unit = ((x >> 11) as f64 / (1u64 << 53) as f64) as f32 * 2.0 - 1.0;
698                    let v = unit * *amplitude;
699                    for ch in inputs.iter_mut() {
700                        ch[f] = v;
701                    }
702                }
703            }
704            SignalSource::Wav {
705                samples,
706                pos,
707                looping,
708            } => {
709                let total = samples.iter().map(|c| c.len()).max().unwrap_or(0);
710                for f in 0..frames {
711                    let p = *pos + f;
712                    let src_idx = if total == 0 {
713                        None
714                    } else if p < total {
715                        Some(p)
716                    } else if *looping {
717                        Some(p % total)
718                    } else {
719                        None
720                    };
721                    for (ci, ch) in inputs.iter_mut().enumerate() {
722                        ch[f] = match src_idx {
723                            Some(i) => samples
724                                .get(ci % samples.len().max(1))
725                                .and_then(|c| c.get(i))
726                                .copied()
727                                .unwrap_or(0.0),
728                            None => 0.0,
729                        };
730                    }
731                }
732                *pos += frames;
733            }
734        }
735    }
736}
737
738#[cfg(test)]
739mod wav_tests {
740    use super::*;
741
742    #[test]
743    fn write_wav_has_correct_header_and_size() {
744        let ch = vec![vec![0.0f32, 0.5, -0.5, 1.0], vec![0.1, 0.2, 0.3, 0.4]];
745        let path = std::env::temp_dir().join("vh_write_wav_test.wav");
746        write_wav(&path, &ch, 48_000).unwrap();
747        let bytes = std::fs::read(&path).unwrap();
748        let _ = std::fs::remove_file(&path);
749
750        assert_eq!(&bytes[0..4], b"RIFF");
751        assert_eq!(&bytes[8..12], b"WAVE");
752        assert_eq!(u16::from_le_bytes([bytes[20], bytes[21]]), 3); // IEEE float
753        assert_eq!(u16::from_le_bytes([bytes[22], bytes[23]]), 2); // channels
754        assert_eq!(
755            u32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]),
756            48_000
757        );
758        // 4 frames * 2 ch * 4 bytes = 32 bytes of data; file = 44-byte header + 32.
759        assert_eq!(bytes.len(), 44 + 32);
760    }
761
762    #[test]
763    fn write_then_read_wav_round_trips() {
764        let ch = vec![vec![0.0f32, 0.5, -0.5, 1.0], vec![0.1, 0.2, 0.3, 0.4]];
765        let path = std::env::temp_dir().join(format!("vh_rw_{}.wav", std::process::id()));
766        write_wav(&path, &ch, 44_100).unwrap();
767        let (back, sr) = read_wav(&path).unwrap();
768        let _ = std::fs::remove_file(&path);
769        assert_eq!(sr, 44_100);
770        assert_eq!(back.len(), 2);
771        for (a, b) in ch.iter().zip(back.iter()) {
772            for (x, y) in a.iter().zip(b.iter()) {
773                assert!((x - y).abs() < 1e-6, "{x} vs {y}");
774            }
775        }
776    }
777}
778
779#[cfg(test)]
780mod signal_tests {
781    use super::*;
782
783    #[test]
784    fn sine_starts_at_zero_and_stays_in_amplitude() {
785        let mut src = SignalSource::sine(1000.0, 0.5);
786        let mut inputs = vec![vec![0.0f32; 256], vec![0.0f32; 256]];
787        src.fill(&mut inputs, 256, 48_000.0);
788        assert!(inputs[0][0].abs() < 1e-6, "sine should start at phase 0");
789        for ch in &inputs {
790            assert!(
791                ch.iter().all(|s| s.abs() <= 0.5 + 1e-6),
792                "exceeds amplitude"
793            );
794        }
795        // Both channels get the same (mono) signal.
796        assert_eq!(inputs[0], inputs[1]);
797        // Non-trivial signal (not all zero).
798        assert!(inputs[0].iter().any(|s| s.abs() > 0.1));
799    }
800
801    #[test]
802    fn noise_is_bounded_and_varied() {
803        let mut src = SignalSource::white_noise(0.25);
804        let mut inputs = vec![vec![0.0f32; 512]];
805        src.fill(&mut inputs, 512, 48_000.0);
806        assert!(inputs[0].iter().all(|s| s.abs() <= 0.25 + 1e-6));
807        let first = inputs[0][0];
808        assert!(inputs[0].iter().any(|&s| s != first), "noise should vary");
809    }
810
811    #[test]
812    fn wav_source_advances_and_zero_pads() {
813        let mut src = SignalSource::wav(vec![vec![1.0, 2.0, 3.0]], false);
814        let mut inputs = vec![vec![0.0f32; 5]];
815        src.fill(&mut inputs, 5, 48_000.0);
816        assert_eq!(inputs[0], vec![1.0, 2.0, 3.0, 0.0, 0.0]); // zero-pads past the end
817    }
818
819    #[test]
820    fn wav_source_loops() {
821        let mut src = SignalSource::wav(vec![vec![1.0, 2.0]], true);
822        let mut inputs = vec![vec![0.0f32; 5]];
823        src.fill(&mut inputs, 5, 48_000.0);
824        assert_eq!(inputs[0], vec![1.0, 2.0, 1.0, 2.0, 1.0]); // wraps
825    }
826}
827
828#[cfg(test)]
829mod speaker_arrangement_tests {
830    use super::*;
831
832    #[test]
833    fn channel_counts_match_bitmask() {
834        assert_eq!(SpeakerArrangement::EMPTY.channel_count(), 0);
835        assert_eq!(SpeakerArrangement::MONO.channel_count(), 1);
836        assert_eq!(SpeakerArrangement::STEREO.channel_count(), 2);
837        assert_eq!(SpeakerArrangement::STEREO_SURROUND.channel_count(), 2);
838    }
839
840    #[test]
841    fn raw_round_trips() {
842        let bits = SpeakerArrangement::STEREO.raw();
843        assert_eq!(bits, 0x3);
844        assert_eq!(
845            SpeakerArrangement::from_raw(bits),
846            SpeakerArrangement::STEREO
847        );
848        // Arbitrary 5.1-ish mask: 6 set bits → 6 channels.
849        assert_eq!(SpeakerArrangement::from_raw(0b111111).channel_count(), 6);
850    }
851}
852
853#[cfg(test)]
854mod meter_tests {
855    use super::*;
856    use std::time::{Duration, Instant};
857
858    #[test]
859    fn peak_meter_rises_instantly_and_holds() {
860        let mut m = PeakMeter::new(20.0, Duration::from_secs(2));
861        let t0 = Instant::now();
862        m.push(0.7, t0);
863        assert_eq!(m.level(), 0.7);
864        assert_eq!(m.peak_hold(), 0.7);
865
866        // A louder block snaps both up immediately.
867        m.push(0.9, t0 + Duration::from_millis(10));
868        assert_eq!(m.level(), 0.9);
869        assert_eq!(m.peak_hold(), 0.9);
870    }
871
872    #[test]
873    fn peak_meter_level_falls_but_hold_latches() {
874        let mut m = PeakMeter::new(20.0, Duration::from_secs(3));
875        let t0 = Instant::now();
876        m.push(1.0, t0);
877
878        // 0.5 s of silence: 20 dB/s → -10 dB ≈ 0.316 linear. Level fell; hold latched.
879        m.push(0.0, t0 + Duration::from_millis(500));
880        let lvl = m.level();
881        assert!(
882            lvl < 1.0 && lvl > 0.0,
883            "level should be mid-fall, got {lvl}"
884        );
885        assert!((lvl - 0.316).abs() < 0.02, "≈-10 dB expected, got {lvl}");
886        assert_eq!(m.peak_hold(), 1.0, "hold must latch within its window");
887    }
888
889    #[test]
890    fn peak_meter_hold_falls_after_window() {
891        let mut m = PeakMeter::new(20.0, Duration::from_secs(1));
892        let t0 = Instant::now();
893        m.push(1.0, t0);
894        // Past the 1 s hold window, with continued silence the marker starts falling too.
895        m.push(0.0, t0 + Duration::from_millis(1500));
896        assert!(
897            m.peak_hold() < 1.0,
898            "hold should fall after the window expired, got {}",
899            m.peak_hold()
900        );
901    }
902
903    #[test]
904    fn peak_meter_reaches_silence_floor() {
905        let mut m = PeakMeter::new(60.0, Duration::from_millis(0));
906        let t0 = Instant::now();
907        m.push(0.5, t0);
908        // A long gap of silence fully empties the meter (snaps to exactly 0).
909        m.push(0.0, t0 + Duration::from_secs(10));
910        assert_eq!(m.level(), 0.0);
911        assert_eq!(m.peak_hold(), 0.0);
912    }
913
914    #[test]
915    fn rms_window_constant_signal() {
916        let mut r = RmsWindow::new(8);
917        for _ in 0..8 {
918            r.push_sample(0.5);
919        }
920        assert!((r.rms() - 0.5).abs() < 1e-6);
921        assert_eq!(r.len(), 8);
922    }
923
924    #[test]
925    fn rms_window_slides_and_evicts() {
926        let mut r = RmsWindow::new(3);
927        r.push_block(&[1.0, 1.0, 1.0]);
928        assert!((r.rms() - 1.0).abs() < 1e-6);
929        // Push three zeros: the loud samples are evicted, RMS returns to 0.
930        r.push_block(&[0.0, 0.0, 0.0]);
931        assert_eq!(r.len(), 3);
932        assert!(
933            r.rms() < 1e-6,
934            "window should have slid to silence, got {}",
935            r.rms()
936        );
937    }
938
939    #[test]
940    fn rms_window_empty_is_zero() {
941        let r = RmsWindow::new(16);
942        assert!(r.is_empty());
943        assert_eq!(r.rms(), 0.0);
944    }
945
946    #[test]
947    fn rms_window_from_duration_sizes_correctly() {
948        // 10 ms at 48 kHz = 480 samples.
949        let r = RmsWindow::from_duration(0.01, 48_000.0);
950        assert_eq!(r.capacity, 480);
951    }
952}