Skip to content
Open
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ jobs:
- run: cargo check --tests --lib --no-default-features
# Check alternative decoders.
- run: cargo check --tests --lib --no-default-features --features claxon,hound,minimp3,lewton
# Test 64-bit sample mode
- run: cargo test --all-targets --features 64bit
- run: cargo test --doc --features 64bit
- run: cargo test --all-targets --all-features --features 64bit
# Check examples compile in both modes
- run: cargo check --examples
- run: cargo check --examples --features 64bit
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Four dithering algorithms: `TPDF`, `RPDF`, `GPDF`, and `HighPass`
- `DitherAlgorithm` enum for algorithm selection
- `Source::dither()` function for applying dithering
- Added `64bit` feature to opt-in to 64-bit sample precision (`f64`).

### Fixed
- docs.rs will now document all features, including those that are optional.
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ wav_output = ["dep:hound"]
tracing = ["dep:tracing"]
# Experimental features using atomic floating-point operations
experimental = ["dep:atomic_float"]
# Perform all calculations with 64-bit floats (instead of 32)
64bit = []

# Audio generation features
#
Expand Down
8 changes: 4 additions & 4 deletions benches/conversions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use dasp_sample::{Duplex, Sample};
use dasp_sample::{Duplex, Sample as DaspSample};
use divan::Bencher;
use rodio::conversions::SampleTypeConverter;
use rodio::{conversions::SampleTypeConverter, Sample};

mod shared;

Expand All @@ -9,7 +9,7 @@ fn main() {
}

#[divan::bench(types = [i16, u16, f32])]
fn from_sample<S: Duplex<f32>>(bencher: Bencher) {
fn from_sample<S: Duplex<Sample>>(bencher: Bencher) {
bencher
.with_inputs(|| {
shared::music_wav()
Expand All @@ -18,6 +18,6 @@ fn from_sample<S: Duplex<f32>>(bencher: Bencher) {
.into_iter()
})
.bench_values(|source| {
SampleTypeConverter::<_, rodio::Sample>::new(source).for_each(divan::black_box_drop)
SampleTypeConverter::<_, Sample>::new(source).for_each(divan::black_box_drop)
})
}
6 changes: 2 additions & 4 deletions benches/effects.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::time::Duration;

use divan::Bencher;
use rodio::source::AutomaticGainControlSettings;
use rodio::Source;

mod shared;
Expand Down Expand Up @@ -48,7 +47,7 @@ fn amplify(bencher: Bencher) {
fn agc_enabled(bencher: Bencher) {
bencher.with_inputs(music_wav).bench_values(|source| {
source
.automatic_gain_control(AutomaticGainControlSettings::default())
.automatic_gain_control(Default::default())
.for_each(divan::black_box_drop)
})
}
Expand All @@ -58,8 +57,7 @@ fn agc_enabled(bencher: Bencher) {
fn agc_disabled(bencher: Bencher) {
bencher.with_inputs(music_wav).bench_values(|source| {
// Create the AGC source
let amplified_source =
source.automatic_gain_control(AutomaticGainControlSettings::default());
let amplified_source = source.automatic_gain_control(Default::default());

// Get the control handle and disable AGC
let agc_control = amplified_source.get_agc_control();
Expand Down
3 changes: 1 addition & 2 deletions benches/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::num::NonZero;
use std::time::Duration;

use divan::Bencher;
use rodio::source::AutomaticGainControlSettings;
use rodio::ChannelCount;
use rodio::{source::UniformSourceIterator, Source};

Expand All @@ -20,7 +19,7 @@ fn long(bencher: Bencher) {
.high_pass(300)
.amplify(1.2)
.speed(0.9)
.automatic_gain_control(AutomaticGainControlSettings::default())
.automatic_gain_control(Default::default())
.delay(Duration::from_secs_f32(0.5))
.fade_in(Duration::from_secs_f32(2.0))
.take_duration(Duration::from_secs(10));
Expand Down
4 changes: 2 additions & 2 deletions examples/automatic_gain_control.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rodio::source::{AutomaticGainControlSettings, Source};
use rodio::source::Source;
use rodio::Decoder;
use std::error::Error;
use std::fs::File;
Expand All @@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let source = Decoder::try_from(file)?;

// Apply automatic gain control to the source
let agc_source = source.automatic_gain_control(AutomaticGainControlSettings::default());
let agc_source = source.automatic_gain_control(Default::default());

// Make it so that the source checks if automatic gain control should be
// enabled or disabled every 5 milliseconds. We must clone `agc_enabled`,
Expand Down
29 changes: 15 additions & 14 deletions examples/limit_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! to configure audio limiting parameters.

use rodio::source::{LimitSettings, SineWave, Source};
use rodio::Sample;
use std::time::Duration;

fn main() {
Expand Down Expand Up @@ -39,11 +40,11 @@ fn main() {
let limited_wave = sine_wave.limit(LimitSettings::default());

// Collect some samples to demonstrate
let samples: Vec<f32> = limited_wave.take(100).collect();
let samples: Vec<Sample> = limited_wave.take(100).collect();
println!(" Generated {} limited samples", samples.len());

// Show peak reduction
let max_sample = samples.iter().fold(0.0f32, |acc, &x| acc.max(x.abs()));
let max_sample: Sample = samples.iter().fold(0.0, |acc, &x| acc.max(x.abs()));
println!(" Peak amplitude after limiting: {max_sample:.3}");
println!();

Expand All @@ -56,7 +57,7 @@ fn main() {

// Apply the custom settings from Example 2
let custom_limited = sine_wave2.limit(custom_limiting);
let custom_samples: Vec<f32> = custom_limited.take(50).collect();
let custom_samples: Vec<Sample> = custom_limited.take(50).collect();
println!(
" Generated {} samples with custom settings",
custom_samples.len()
Expand Down Expand Up @@ -101,7 +102,7 @@ fn main() {
println!("Example 6: Limiting with -6dB threshold");

// Create a sine wave that will definitely trigger limiting
const AMPLITUDE: f32 = 2.5; // High amplitude to ensure limiting occurs
const AMPLITUDE: Sample = 2.5; // High amplitude to ensure limiting occurs
let test_sine = SineWave::new(440.0)
.amplify(AMPLITUDE)
.take_duration(Duration::from_millis(100)); // 100ms = ~4410 samples
Expand All @@ -114,24 +115,24 @@ fn main() {
.with_release(Duration::from_millis(12)); // Moderate release

let limited_sine = test_sine.limit(strict_limiting.clone());
let test_samples: Vec<f32> = limited_sine.take(4410).collect();
let test_samples: Vec<Sample> = limited_sine.take(4410).collect();

// Analyze peaks at different time periods
let early_peak = test_samples[0..500]
let early_peak: Sample = test_samples[0..500]
.iter()
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
let mid_peak = test_samples[1000..1500]
.fold(0.0, |acc, &x| acc.max(x.abs()));
let mid_peak: Sample = test_samples[1000..1500]
.iter()
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
let settled_peak = test_samples[2000..]
.fold(0.0, |acc, &x| acc.max(x.abs()));
let settled_peak: Sample = test_samples[2000..]
.iter()
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
.fold(0.0, |acc, &x| acc.max(x.abs()));

// With -6dB threshold, ALL samples are well below 1.0!
let target_linear = 10.0_f32.powf(strict_limiting.threshold / 20.0);
let max_settled = test_samples[2000..]
let target_linear = (10.0 as Sample).powf(strict_limiting.threshold / 20.0);
let max_settled: Sample = test_samples[2000..]
.iter()
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
.fold(0.0, |acc, &x| acc.max(x.abs()));

println!(
" {}dB threshold limiting results:",
Expand Down
20 changes: 12 additions & 8 deletions examples/mix_multiple_sources.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use rodio::mixer;
use rodio::source::{SineWave, Source};
use rodio::Float;
use std::error::Error;
use std::num::NonZero;
use std::time::Duration;

const NOTE_DURATION: Duration = Duration::from_secs(1);
const NOTE_AMPLITUDE: Float = 0.20;

fn main() -> Result<(), Box<dyn Error>> {
// Construct a dynamic controller and mixer, stream_handle, and sink.
let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), NonZero::new(44_100).unwrap());
Expand All @@ -14,17 +18,17 @@ fn main() -> Result<(), Box<dyn Error>> {
// notes in the key of C and in octave 4: C4, or middle C on a piano,
// E4, G4, and A4 respectively.
let source_c = SineWave::new(261.63)
.take_duration(Duration::from_secs_f32(1.))
.amplify(0.20);
.take_duration(NOTE_DURATION)
.amplify(NOTE_AMPLITUDE);
let source_e = SineWave::new(329.63)
.take_duration(Duration::from_secs_f32(1.))
.amplify(0.20);
.take_duration(NOTE_DURATION)
.amplify(NOTE_AMPLITUDE);
let source_g = SineWave::new(392.0)
.take_duration(Duration::from_secs_f32(1.))
.amplify(0.20);
.take_duration(NOTE_DURATION)
.amplify(NOTE_AMPLITUDE);
let source_a = SineWave::new(440.0)
.take_duration(Duration::from_secs_f32(1.))
.amplify(0.20);
.take_duration(NOTE_DURATION)
.amplify(NOTE_AMPLITUDE);

// Add sources C, E, G, and A to the mixer controller.
controller.add(source_c);
Expand Down
10 changes: 6 additions & 4 deletions examples/noise_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

use std::{error::Error, thread::sleep, time::Duration};

use rodio::source::{
noise::{Blue, Brownian, Pink, Velvet, Violet, WhiteGaussian, WhiteTriangular, WhiteUniform},
Source,
use rodio::{
source::noise::{
Blue, Brownian, Pink, Velvet, Violet, WhiteGaussian, WhiteTriangular, WhiteUniform,
},
Sample, Source,
};

fn main() -> Result<(), Box<dyn Error>> {
Expand Down Expand Up @@ -74,7 +76,7 @@ fn main() -> Result<(), Box<dyn Error>> {
/// Helper function to play a noise type with description
fn play_noise<S>(stream_handle: &rodio::OutputStream, source: S, name: &str, description: &str)
where
S: Source<Item = f32> + Send + 'static,
S: Source<Item = Sample> + Send + 'static,
{
println!("{} Noise", name);
println!(" Application: {}", description);
Expand Down
20 changes: 11 additions & 9 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
//!

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

Expand All @@ -40,13 +41,13 @@ impl SamplesBuffer {
where
D: Into<Vec<Sample>>,
{
let data: Arc<[f32]> = data.into().into();
let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap()
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 / 1_000_000_000,
(duration_ns % 1_000_000_000) as u32,
duration_ns / NANOS_PER_SEC,
(duration_ns % NANOS_PER_SEC) as u32,
);

Self {
Expand Down Expand Up @@ -103,8 +104,9 @@ impl Source for SamplesBuffer {
// sample directly.

let curr_channel = self.pos % self.channels().get() as usize;
let new_pos =
pos.as_secs_f32() * self.sample_rate().get() as f32 * self.channels().get() as f32;
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());
Expand Down Expand Up @@ -171,7 +173,7 @@ mod tests {
#[cfg(test)]
mod try_seek {
use super::*;
use crate::common::{ChannelCount, SampleRate};
use crate::common::{ChannelCount, Float, SampleRate};
use crate::Sample;
use std::time::Duration;

Expand All @@ -187,7 +189,7 @@ mod tests {
buf.try_seek(Duration::from_secs(5)).unwrap();
assert_eq!(
buf.next(),
Some(5.0 * SAMPLE_RATE.get() as f32 * CHANNELS.get() as f32)
Some(5.0 * SAMPLE_RATE.get() as Float * CHANNELS.get() as Float)
);

assert!(buf.next().is_some_and(|s| s.trunc() as i32 % 2 == 1));
Expand Down
12 changes: 11 additions & 1 deletion src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ pub type ChannelCount = NonZero<u16>;
/// Number of bits per sample. Can never be zero.
pub type BitDepth = NonZero<u32>;

/// Floating point type used for internal calculations. Can be configured to be
/// either `f32` (default) or `f64` using the `64bit` feature flag.
#[cfg(not(feature = "64bit"))]
pub type Float = f32;

/// Floating point type used for internal calculations. Can be configured to be
/// either `f32` (default) or `f64` using the `64bit` feature flag.
#[cfg(feature = "64bit")]
pub type Float = f64;

/// Represents value of a single sample.
/// Silence corresponds to the value `0.0`. The expected amplitude range is -1.0...1.0.
/// Values below and above this range are clipped in conversion to other sample types.
/// Use conversion traits from [dasp_sample] crate or [crate::conversions::SampleTypeConverter]
/// to convert between sample types if necessary.
pub type Sample = f32;
pub type Sample = Float;

/// Used to test at compile time that a struct/enum implements Send, Sync and
/// is 'static. These are common requirements for dynamic error management
Expand Down
2 changes: 1 addition & 1 deletion src/conversions/sample_rate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ where
.zip(self.next_frame.iter())
.enumerate()
{
let sample = math::lerp(cur, next, numerator, self.to);
let sample = math::lerp(*cur, *next, numerator, self.to);

if off == 0 {
result = Some(sample);
Expand Down
10 changes: 9 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@
//!
//! The "playback" feature adds support for playing audio. This feature requires the "cpal" crate.
//!
//! ### Feature "64bit"
//!
//! The "64bit" feature enables 64-bit sample precision using `f64` for audio samples and most
//! internal calculations. By default, rodio uses 32-bit floats (`f32`), which offers better
//! performance and is sufficient for most use cases. The 64-bit mode addresses precision drift
//! when chaining many audio operations together and in long-running signal generators where
//! phase errors compound over time.
//!
//! ## How it works under the hood
//!
//! Rodio spawns a background thread that is dedicated to reading from the sources and sending
Expand Down Expand Up @@ -189,7 +197,7 @@ pub mod queue;
pub mod source;
pub mod static_buffer;

pub use crate::common::{BitDepth, ChannelCount, Sample, SampleRate};
pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate};
pub use crate::decoder::Decoder;
pub use crate::sink::Sink;
pub use crate::source::Source;
Expand Down
Loading