Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `Chirp` now implements `Iterator::size_hint` and `ExactSizeIterator`.
- `Chirp` and `Empty` now implement `Iterator::size_hint` and `ExactSizeIterator`.
- `SamplesBuffer` now implements `ExactSizeIterator`.
- `Zero` now implements `try_seek`, `total_duration` and `Copy`.
- Added `Source::is_exhausted()` helper method to check if a source has no more samples.
- Added `Red` noise generator that is more practical than `Brownian` noise.
- Added `std_dev()` to `WhiteUniform` and `WhiteTriangular`.
Expand All @@ -38,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved precision in `SkipDuration` to avoid off-by-a-few-samples errors.
- Fixed channel misalignment in queue with non-power-of-2 channel counts (e.g., 6 channels) by ensuring frame-aligned span lengths.
- Fixed channel misalignment when sources end before their promised span length by padding with silence to complete frames.
- Fixed `Empty` source to properly report exhaustion.
- Fixed `Zero::current_span_len` returning remaining samples instead of span length.

### Changed
- `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`.
Expand Down
68 changes: 55 additions & 13 deletions src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use crate::math::nz;
use crate::source::{Empty, SeekError, Source, Zero};
use crate::Sample;

Expand Down Expand Up @@ -121,9 +120,6 @@ pub struct SourcesQueueOutput {
padding_samples_remaining: usize,
}

const SILENCE_SAMPLE_RATE: SampleRate = nz!(44100);
const SILENCE_CHANNELS: ChannelCount = nz!(1);

/// Returns a threshold span length that ensures frame alignment.
///
/// Spans must end on frame boundaries (multiples of channel count) to prevent
Expand Down Expand Up @@ -173,27 +169,38 @@ impl Source for SourcesQueueOutput {

#[inline]
fn channels(&self) -> ChannelCount {
// When current source is exhausted, peek at the next source's metadata
if !self.current.is_exhausted() {
// Current source is active (producing samples)
// - Initially: never (Empty is exhausted immediately)
// - After append: the appended source while playing
// - With keep_alive: Zero (silence) while playing
self.current.channels()
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() {
// Current source exhausted, peek at next queued source
// This is critical: UniformSourceIterator queries metadata during append,
// before any samples are pulled. We must report the next source's metadata.
next.channels()
} else {
// Queue is empty - return silence metadata
SILENCE_CHANNELS
// Queue is empty, no sources queued
// - Initially: Empty
// - With keep_alive: exhausted Zero between silence chunks (matches Empty)
// - Without keep_alive: Empty (will end on next())
self.current.channels()
}
}

#[inline]
fn sample_rate(&self) -> SampleRate {
// When current source is exhausted, peek at the next source's metadata
if !self.current.is_exhausted() {
// Current source is active (producing samples)
self.current.sample_rate()
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() {
// Current source exhausted, peek at next queued source
// This prevents wrong resampling setup in UniformSourceIterator
next.sample_rate()
} else {
// Queue is empty - return silence metadata
SILENCE_SAMPLE_RATE
// Queue is empty, no sources queued
self.current.sample_rate()
}
}

Expand Down Expand Up @@ -276,10 +283,11 @@ impl SourcesQueueOutput {
let mut next = self.input.next_sounds.lock().unwrap();

if next.is_empty() {
let channels = self.current.channels();
let silence = Box::new(Zero::new_samples(
SILENCE_CHANNELS,
SILENCE_SAMPLE_RATE,
threshold(SILENCE_CHANNELS),
channels,
self.current.sample_rate(),
threshold(channels),
)) as Box<_>;
if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
// Play a short silence in order to avoid spinlocking.
Expand Down Expand Up @@ -381,6 +389,40 @@ mod tests {
assert_eq!(rx.next(), Some(-10.0));
}

#[test]
fn append_updates_metadata() {
for keep_alive in [false, true] {
let (tx, rx) = queue::queue(keep_alive);
assert_eq!(
rx.channels(),
nz!(1),
"Initial channels should be 1 (keep_alive={keep_alive})"
);
assert_eq!(
rx.sample_rate(),
nz!(48000),
"Initial sample rate should be 48000 (keep_alive={keep_alive})"
);

tx.append(SamplesBuffer::new(
nz!(2),
nz!(44100),
vec![0.1, 0.2, 0.3, 0.4],
));

assert_eq!(
rx.channels(),
nz!(2),
"Channels should update to 2 (keep_alive={keep_alive})"
);
assert_eq!(
rx.sample_rate(),
nz!(44100),
"Sample rate should update to 44100 (keep_alive={keep_alive})"
);
}
}

#[test]
fn span_ending_mid_frame() {
let mut test_source1 = TestSource::new(&[0.1, 0.2, 0.1, 0.2, 0.1])
Expand Down
26 changes: 13 additions & 13 deletions src/source/empty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@ use crate::math::nz;
use crate::{Sample, Source};

/// An empty source.
#[derive(Debug, Copy, Clone)]
pub struct Empty();

impl Default for Empty {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct Empty;

impl Empty {
/// An empty source that immediately ends without ever returning a sample to
/// play
#[inline]
pub fn new() -> Empty {
Empty()
pub fn new() -> Self {
Self
}
}

Expand All @@ -32,12 +25,19 @@ impl Iterator for Empty {
fn next(&mut self) -> Option<Self::Item> {
None
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(0))
}
}

impl ExactSizeIterator for Empty {}

impl Source for Empty {
#[inline]
fn current_span_len(&self) -> Option<usize> {
None
Some(0)
}

#[inline]
Expand All @@ -52,7 +52,7 @@ impl Source for Empty {

#[inline]
fn total_duration(&self) -> Option<Duration> {
Some(Duration::new(0, 0))
Some(Duration::ZERO)
}

#[inline]
Expand Down
76 changes: 56 additions & 20 deletions src/source/zero.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
use std::time::Duration;

use dasp_sample::Sample as DaspSample;

use super::SeekError;
use crate::common::{ChannelCount, SampleRate};
use crate::{Sample, Source};

/// An source that produces samples with value zero (silence). Depending on if
/// it where created with [`Zero::new`] or [`Zero::new_samples`] it can be never
/// ending or finite.
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug)]
pub struct Zero {
channels: ChannelCount,
sample_rate: SampleRate,
num_samples: Option<usize>,
total_samples: Option<usize>,
position: usize,
}

impl Zero {
/// Create a new source that never ends and produces total silence.
#[inline]
pub fn new(channels: ChannelCount, sample_rate: SampleRate) -> Zero {
Zero {
pub fn new(channels: ChannelCount, sample_rate: SampleRate) -> Self {
Self {
channels,
sample_rate,
num_samples: None,
total_samples: None,
position: 0,
}
}

/// Create a new source that never ends and produces total silence.
#[inline]
pub fn new_samples(
channels: ChannelCount,
sample_rate: SampleRate,
num_samples: usize,
) -> Zero {
Zero {
) -> Self {
Self {
channels,
sample_rate,
num_samples: Some(num_samples),
total_samples: Some(num_samples),
position: 0,
}
}
}
Expand All @@ -44,23 +50,33 @@ impl Iterator for Zero {

#[inline]
fn next(&mut self) -> Option<Self::Item> {
if let Some(num_samples) = self.num_samples {
if num_samples > 0 {
self.num_samples = Some(num_samples - 1);
Some(0.0)
if let Some(total_samples) = self.total_samples {
if self.position < total_samples {
self.position += 1;
} else {
None
return None;
}
}

Some(Sample::EQUILIBRIUM)
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
match self.total_samples {
Some(total_samples) => {
let remaining = total_samples - self.position;
(remaining, Some(remaining))
}
} else {
Some(0.0)
None => (usize::MAX, None),
}
}
}

impl Source for Zero {
#[inline]
fn current_span_len(&self) -> Option<usize> {
self.num_samples
self.total_samples
}

#[inline]
Expand All @@ -73,13 +89,33 @@ impl Source for Zero {
self.sample_rate
}

#[inline]
fn total_duration(&self) -> Option<Duration> {
None
self.total_samples.map(|total| {
let sample_rate = self.sample_rate.get() as u64;
let frames = total / self.channels.get() as usize;
let secs = frames as u64 / sample_rate;
let nanos = ((frames as u64 % sample_rate) * 1_000_000_000) / sample_rate;
Duration::new(secs, nanos as u32)
})
}

#[inline]
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
if let (Some(total_samples), Some(total_duration)) =
(self.total_samples, self.total_duration())
{
let mut target = pos;
if target > total_duration {
target = total_duration;
}

let target_samples = (target.as_secs_f32()
* self.sample_rate.get() as f32
* self.channels.get() as f32) as usize;
let target_samples = target_samples.min(total_samples);

self.position = target_samples;
}

Ok(())
}
}