Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added `FixedSource` which mirrors `Source` but does not allow the sample rate or
channel count to change.
- Added `ConstSource` which is like `FixedSource` except the sample rate and
channel count are fixed at compile time.
- Added `Skippable::skipped` function to check if the inner source was skipped.
- All sources now implement `ExactSizeIterator` when their inner source does.
- All sources now implement `Iterator::size_hint()`.
Expand All @@ -21,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Breaking: `Microphone` now implements `FixedSource`
- Breaking: `Done` now calls a callback instead of decrementing an `Arc<AtomicUsize>`.
- Updated `cpal` to v0.18.
- Clarified `Source::current_span_len()` documentation to specify it returns total span length.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ required-features = ["playback", "wav"]

[[example]]
name = "microphone"
required-features = ["playback", "recording", "wav_output"]
required-features = ["playback", "recording"]

[[example]]
name = "mix_multiple_sources"
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
[![Crates.io Downloads](https://img.shields.io/crates/d/rodio.svg)](https://crates.io/crates/rodio)
[![Build Status](https://github.com/RustAudio/rodio/workflows/CI/badge.svg)](https://github.com/RustAudio/rodio/actions)

> [!WARNING]
> We are currently rewriting Rodio's core audio engine. The current state should
> be working but under documented. This is expected to take at least a month.
> For the latest unreleased work pre engine rewrite see [this
> checkout](https://github.com/RustAudio/rodio/tree/c5f1e94290922fe4ee963ce6f85754ea6ba36243).
> To follow our progress see the [tracking
> issue](https://github.com/RustAudio/rodio/issues/901)

Rust playback library.

Playback is handled by [cpal](https://github.com/RustAudio/cpal). Format decoding is handled by [Symphonia](https://github.com/pdeljanov/Symphonia) by default, or by optional format-specific decoders:
Expand Down
4 changes: 2 additions & 2 deletions examples/microphone.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use inquire::Select;
use rodio::microphone::{self, MicrophoneBuilder};
use rodio::Source;
use rodio::FixedSource;
use std::error::Error;
use std::thread;
use std::time::Duration;
Expand All @@ -22,7 +22,7 @@ fn main() -> Result<(), Box<dyn Error>> {

println!("Playing the recording");
let mut output = rodio::DeviceSinkBuilder::open_default_sink()?;
output.mixer().add(recording);
output.mixer().add(recording.into_dynamic_source());

thread::sleep(Duration::from_secs(5));

Expand Down
66 changes: 4 additions & 62 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
//!

use crate::common::{ChannelCount, SampleRate};
use crate::math::{duration_to_float, NANOS_PER_SEC};
use crate::source::{SeekError, UniformSourceIterator};
use crate::{Float, Sample, Source};
use crate::{Sample, Source};
use std::sync::Arc;
use std::time::Duration;

Expand All @@ -25,37 +24,19 @@ pub struct SamplesBuffer {
pos: usize,
channels: ChannelCount,
sample_rate: SampleRate,
duration: Duration,
}

impl SamplesBuffer {
/// Builds a new `SamplesBuffer`.
///
/// # Panics
///
/// - Panics if the samples rate is zero.
/// - Panics if the length of the buffer is larger than approximately 16 billion elements.
/// This is because the calculation of the duration would overflow.
///
pub fn new<D>(channels: ChannelCount, sample_rate: SampleRate, data: D) -> Self
where
D: Into<Vec<Sample>>,
{
let data: Arc<[Sample]> = data.into().into();
let duration_ns = NANOS_PER_SEC.checked_mul(data.len() as u64).unwrap()
/ sample_rate.get() as u64
/ channels.get() as u64;
let duration = Duration::new(
duration_ns / NANOS_PER_SEC,
(duration_ns % NANOS_PER_SEC) as u32,
);

Self {
data,
data: data.into().into(),
pos: 0,
channels,
sample_rate,
duration,
}
}

Expand Down Expand Up @@ -91,50 +72,11 @@ impl Source for SamplesBuffer {
self.sample_rate
}

#[inline]
fn total_duration(&self) -> Option<Duration> {
Some(self.duration)
}

/// This jumps in memory till the sample for `pos`.
#[inline]
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
// This is fast because all the samples are in memory already
// and due to the constant sample_rate we can jump to the right
// sample directly.

let curr_channel = self.pos % self.channels().get() as usize;
let new_pos = duration_to_float(pos)
* self.sample_rate().get() as Float
* self.channels().get() as Float;
// saturate pos at the end of the source
let new_pos = new_pos as usize;
let new_pos = new_pos.min(self.data.len());

// make sure the next sample is for the right channel
let new_pos = new_pos.next_multiple_of(self.channels().get() as usize);
let new_pos = new_pos - curr_channel;

self.pos = new_pos;
Ok(())
}
crate::common::source::buffer::source_impl! {}
}

impl Iterator for SamplesBuffer {
type Item = Sample;

#[inline]
fn next(&mut self) -> Option<Self::Item> {
let sample = self.data.get(self.pos)?;
self.pos += 1;
Some(*sample)
}

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.data.len() - self.pos;
(remaining, Some(remaining))
}
crate::common::source::buffer::iter_impl! {}
}

impl ExactSizeIterator for SamplesBuffer {}
Expand Down
2 changes: 2 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::num::NonZero;

use crate::math::nz;

pub(crate) mod source;

/// Sample rate (a frame rate or samples per second per channel).
pub type SampleRate = NonZero<u32>;

Expand Down
44 changes: 44 additions & 0 deletions src/common/source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Code shared between source types.
//!
//! Since we have three source types there is a lot of code duplication. We
//! combat that by placing whatever can be shared here.
//!
//! This can be:
//! - shared types like error enums
//! - shared free functions
//! - shared members / impl blocks through macro_rules
//!
//! # Note
//! Effects are defined through a macro and do not need this kind of
//! deduplication
//!
//! This modules structure mirrors that of what it deduplicates. For example
//! the code shared between [fixed_source::chain] and [const_source::chain] is in
//! common/source/chain.rs

pub(crate) mod buffer;
pub(crate) mod chain;

macro_rules! add_inner_accessors {
($inner:ident) => {
/// Get immutable access to the input to this source
#[inline]
pub fn inner(&self) -> &S {
&self.$inner
}

/// Returns a mutable reference to the inner source.
#[inline]
pub fn inner_mut(&mut self) -> &mut S {
&mut self.$inner
}

/// Returns the inner source.
#[inline]
pub fn into_inner(self) -> S {
self.$inner
}
};
}

pub(crate) use add_inner_accessors;
66 changes: 66 additions & 0 deletions src/common/source/buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
macro_rules! source_impl {
() => {
/// # Panics
/// If the length of the buffer is larger than approximately 16 billion elements.
/// This is because the calculation of the duration would overflow.
#[inline]
fn total_duration(&self) -> Option<Duration> {
use crate::math::NANOS_PER_SEC;

let duration_ns = NANOS_PER_SEC
.checked_mul(self.data.len() as u64)
.expect("slices longer then 16 billion elements are not supported")
/ self.sample_rate().get() as u64
/ self.channels().get() as u64;
let duration = Duration::new(
duration_ns / NANOS_PER_SEC,
(duration_ns % NANOS_PER_SEC) as u32,
);

Some(duration)
}

/// This jumps in memory to the sample corresponding to `pos`.
#[inline]
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
// This is fast because all the samples are in memory already
// and due to the constant sample_rate we can jump to the right
// sample directly.

let curr_channel = self.pos % self.channels().get() as usize;
let new_pos = crate::math::duration_to_float(pos)
* self.sample_rate().get() as crate::Float
* self.channels().get() as crate::Float;
// saturate pos at the end of the source
let new_pos = new_pos as usize;
let new_pos = new_pos.min(self.data.len());

// make sure the next sample is for the right channel
let new_pos = new_pos.next_multiple_of(self.channels().get() as usize);
let new_pos = new_pos - curr_channel;

self.pos = new_pos;
Ok(())
}
};
}

macro_rules! iter_impl {
() => {
type Item = Sample;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let sample = self.data.get(self.pos)?;
self.pos += 1;
Some(*sample)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.data.len() - self.pos;
(remaining, Some(remaining))
}
};
}

pub(crate) use iter_impl;
pub(crate) use source_impl;
94 changes: 94 additions & 0 deletions src/common/source/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::source::SeekError;

#[derive(Debug, thiserror::Error)]
pub enum ChainSeekError {
#[error("Could not get duration of first source ({ty}")]
NoTotalDurationForFirst { ty: &'static str },
#[error("Could not seek in first source")]
FailedToSeekInFirst {
ty: &'static str,
#[source]
error: SeekError,
},
#[error("Could not seek in second source")]
FailedToSeekInSecond {
ty: &'static str,
#[source]
error: SeekError,
},
}

macro_rules! source_impl {
() => {
fn channels(&self) -> crate::ChannelCount {
self.first.channels()
}

fn sample_rate(&self) -> crate::SampleRate {
self.first.sample_rate()
}

fn total_duration(&self) -> Option<std::time::Duration> {
self.first
.total_duration()
.and_then(|d| self.second.total_duration().map(|d2| d2 + d))
}

fn try_seek(&mut self, pos: std::time::Duration) -> Result<(), crate::source::SeekError> {
use crate::source::SeekError;
use std::any::type_name_of_val;
use std::sync::Arc;

let Some(first) = self.first.total_duration() else {
return Err(ChainSeekError::NoTotalDurationForFirst {
ty: type_name_of_val(&self.first),
})
.map_err(Arc::new)
.map_err(|e| SeekError::Other(e));
};

if pos < first {
self.first
.try_seek(pos)
.map_err(|error| ChainSeekError::FailedToSeekInFirst {
ty: type_name_of_val(&self.first),
error,
})
.map_err(Arc::new)
.map_err(|e| SeekError::Other(e))
} else {
self.second
.try_seek(pos)
.map_err(|error| ChainSeekError::FailedToSeekInSecond {
ty: type_name_of_val(&self.second),
error,
})
.map_err(Arc::new)
.map_err(|e| SeekError::Other(e))
}
}
};
}

macro_rules! iter_impl {
() => {
type Item = Sample;

fn next(&mut self) -> Option<Self::Item> {
if self.playing_inner {
match self.first.next() {
Some(sample) => Some(sample),
None => {
self.playing_inner = false;
self.second.next()
}
}
} else {
self.second.next()
}
}
};
}

pub(crate) use iter_impl;
pub(crate) use source_impl;
Loading
Loading