diff --git a/fyrox-impl/src/engine/mod.rs b/fyrox-impl/src/engine/mod.rs index 694845650..da6346309 100644 --- a/fyrox-impl/src/engine/mod.rs +++ b/fyrox-impl/src/engine/mod.rs @@ -315,7 +315,6 @@ pub struct Engine { model_events_receiver: Receiver, - #[allow(dead_code)] // Keep engine instance alive. sound_engine: SoundEngine, // A set of plugins used by the engine. @@ -1501,6 +1500,17 @@ impl Engine { } } + /// Returns current sample rate of the sound engine. + pub fn sound_sample_rate(&self) -> u32 { + self.sound_engine.sample_rate() + } + + /// Normalizes given frequency using sampling rate of the sound output device. Normalized frequency + /// then can be used to create filters. + pub fn normalize_sound_frequency(&self, f: f32) -> f32 { + self.sound_engine.normalize_frequency(f) + } + /// Tries to destroy current graphics context. It will succeed only if the `graphics_context` is fully initialized. /// The method will try to save all possible runtime changes of the window, so the next [`Engine::initialize_graphics_context`] /// will result in the almost exact copy of the context that was made before destruction. diff --git a/fyrox-impl/src/scene/sound/context.rs b/fyrox-impl/src/scene/sound/context.rs index cdf5442f5..12bdaa6aa 100644 --- a/fyrox-impl/src/scene/sound/context.rs +++ b/fyrox-impl/src/scene/sound/context.rs @@ -80,12 +80,6 @@ impl SoundContextGuard<'_> { self.guard.distance_model() } - /// Normalizes given frequency using context's sampling rate. Normalized frequency then can be used - /// to create filters. - pub fn normalize_frequency(&self, f: f32) -> f32 { - self.guard.normalize_frequency(f) - } - /// Returns amount of time context spent on rendering all sound sources. pub fn full_render_duration(&self) -> Duration { self.guard.full_render_duration() diff --git a/fyrox-impl/src/scene/sound/mod.rs b/fyrox-impl/src/scene/sound/mod.rs index 750611fd5..fad08247c 100644 --- a/fyrox-impl/src/scene/sound/mod.rs +++ b/fyrox-impl/src/scene/sound/mod.rs @@ -47,7 +47,7 @@ pub use fyrox_sound::{ DataSource, SoundBuffer, SoundBufferResource, SoundBufferResourceLoadError, }, bus::*, - context::{DistanceModel, SAMPLE_RATE}, + context::DistanceModel, dsp::{filters::*, DelayLine}, effects::*, engine::SoundEngine, diff --git a/fyrox-sound/examples/hrtf.rs b/fyrox-sound/examples/hrtf.rs index 9f5fb25ed..5a34bbe2f 100644 --- a/fyrox-sound/examples/hrtf.rs +++ b/fyrox-sound/examples/hrtf.rs @@ -26,7 +26,7 @@ use fyrox_sound::renderer::hrtf::{HrirSphereResource, HrirSphereResourceExt}; use fyrox_sound::{ algebra::{UnitQuaternion, Vector3}, buffer::{DataSource, SoundBufferResource}, - context::{self, SoundContext}, + context::SoundContext, engine::SoundEngine, futures::executor::block_on, hrtf::HrirSphere, @@ -44,7 +44,7 @@ fn main() { let engine = SoundEngine::new().unwrap(); let hrir_path = PathBuf::from("examples/data/IRC_1002_C.bin"); - let hrir_sphere = HrirSphere::from_file(&hrir_path, context::SAMPLE_RATE).unwrap(); + let hrir_sphere = HrirSphere::from_file(&hrir_path, engine.sample_rate()).unwrap(); // Initialize new sound context with default output device. let context = SoundContext::new(); @@ -55,6 +55,7 @@ fn main() { context .state() .set_renderer(Renderer::HrtfRenderer(HrtfRenderer::new( + engine.sample_rate(), HrirSphereResource::from_hrir_sphere(hrir_sphere, ResourceKind::External), ))); diff --git a/fyrox-sound/examples/reverb.rs b/fyrox-sound/examples/reverb.rs index 2a365b7c5..3ecbf4f5a 100644 --- a/fyrox-sound/examples/reverb.rs +++ b/fyrox-sound/examples/reverb.rs @@ -25,7 +25,7 @@ use fyrox_sound::renderer::hrtf::{HrirSphereResource, HrirSphereResourceExt}; use fyrox_sound::{ algebra::{Point3, UnitQuaternion, Vector3}, buffer::{DataSource, SoundBufferResource}, - context::{self, SoundContext}, + context::SoundContext, effects::{reverb::Reverb, Effect}, engine::SoundEngine, futures::executor::block_on, @@ -40,12 +40,12 @@ use std::{ }; fn main() { - let hrir_path = PathBuf::from("examples/data/IRC_1002_C.bin"); - let hrir_sphere = HrirSphere::from_file(&hrir_path, context::SAMPLE_RATE).unwrap(); - // Initialize sound engine with default output device. let engine = SoundEngine::new().unwrap(); + let hrir_path = PathBuf::from("examples/data/IRC_1002_C.bin"); + let hrir_sphere = HrirSphere::from_file(&hrir_path, engine.sample_rate()).unwrap(); + // Initialize new sound context. let context = SoundContext::new(); @@ -55,6 +55,7 @@ fn main() { context .state() .set_renderer(Renderer::HrtfRenderer(HrtfRenderer::new( + engine.sample_rate(), HrirSphereResource::from_hrir_sphere(hrir_sphere, ResourceKind::External), ))); diff --git a/fyrox-sound/examples/write_wav.rs b/fyrox-sound/examples/write_wav.rs index 46c912b83..678b55e0f 100644 --- a/fyrox-sound/examples/write_wav.rs +++ b/fyrox-sound/examples/write_wav.rs @@ -64,7 +64,7 @@ fn main() { // Create output wav file. The sample rate is currently fixed. let wav_spec = hound::WavSpec { channels: 2, - sample_rate: fyrox_sound::context::SAMPLE_RATE, + sample_rate: engine.sample_rate(), bits_per_sample: 32, sample_format: hound::SampleFormat::Float, }; @@ -76,7 +76,7 @@ fn main() { let mut samples_written = 0; // Wait until sound will play completely. - while samples_written < 3 * fyrox_sound::context::SAMPLE_RATE { + while samples_written < 3 * engine.sample_rate() { engine.state().render(&mut buf); for &(l, r) in buf.iter() { wav_writer.write_sample(l).unwrap(); diff --git a/fyrox-sound/output.wav b/fyrox-sound/output.wav new file mode 100644 index 000000000..8fc7e4ae4 Binary files /dev/null and b/fyrox-sound/output.wav differ diff --git a/fyrox-sound/src/bus.rs b/fyrox-sound/src/bus.rs index c8eec4dff..fa1127488 100644 --- a/fyrox-sound/src/bus.rs +++ b/fyrox-sound/src/bus.rs @@ -185,11 +185,11 @@ impl AudioBus { } } - fn apply_effects(&mut self) { + fn apply_effects(&mut self, sample_rate: u32) { // Pass through the chain of effects. for effect in self.effects.iter_mut() { let (input, output) = self.ping_pong_buffer.input_output_buffers(); - effect.render(input, output); + effect.render(sample_rate, input, output); self.ping_pong_buffer.swap(); } } @@ -487,10 +487,10 @@ impl AudioBusGraph { } } - pub(crate) fn end_render(&mut self, output_device_buffer: &mut [(f32, f32)]) { + pub(crate) fn end_render(&mut self, sample_rate: u32, output_device_buffer: &mut [(f32, f32)]) { let mut leafs = Vec::new(); for (handle, bus) in self.buses.pair_iter_mut() { - bus.apply_effects(); + bus.apply_effects(sample_rate); if bus.child_buses.is_empty() { leafs.push(handle); @@ -531,6 +531,8 @@ mod test { effects::{Attenuate, Effect}, }; + const SAMPLE_RATE: u32 = 44100; + #[test] fn test_multi_bus_data_flow() { let mut output_buffer = [(0.0f32, 0.0f32)]; @@ -553,7 +555,7 @@ mod test { *right = 1.0; } - graph.end_render(&mut output_buffer); + graph.end_render(SAMPLE_RATE, &mut output_buffer); assert_eq!(output_buffer[0], (2.0, 2.0)); } @@ -572,7 +574,7 @@ mod test { *right = 1.0; } - graph.end_render(&mut output_buffer); + graph.end_render(SAMPLE_RATE, &mut output_buffer); assert_eq!(output_buffer[0], (1.0, 1.0)); } @@ -606,7 +608,7 @@ mod test { *right = 1.0; } - graph.end_render(&mut output_buffer); + graph.end_render(SAMPLE_RATE, &mut output_buffer); assert_eq!(output_buffer[0], (0.75, 0.75)); } diff --git a/fyrox-sound/src/context.rs b/fyrox-sound/src/context.rs index 0ede44012..4e7c7f980 100644 --- a/fyrox-sound/src/context.rs +++ b/fyrox-sound/src/context.rs @@ -51,10 +51,6 @@ use std::{ }; use strum_macros::{AsRefStr, EnumString, VariantNames}; -/// Sample rate for output device. -/// TODO: Make this configurable, for now its set to most commonly used sample rate of 44100 Hz. -pub const SAMPLE_RATE: u32 = 44100; - /// Distance model defines how volume of sound will decay when distance to listener changes. #[derive(Copy, Clone, Debug, Eq, PartialEq, Reflect, Visit, AsRefStr, EnumString, VariantNames)] #[repr(u32)] @@ -186,12 +182,6 @@ impl State { self.distance_model } - /// Normalizes given frequency using context's sampling rate. Normalized frequency then can be used - /// to create filters. - pub fn normalize_frequency(&self, f: f32) -> f32 { - f / SAMPLE_RATE as f32 - } - /// Returns amount of time context spent on rendering all sound sources. pub fn full_render_duration(&self) -> Duration { self.render_duration @@ -275,7 +265,7 @@ impl State { &mut self.bus_graph } - pub(crate) fn render(&mut self, output_device_buffer: &mut [(f32, f32)]) { + pub(crate) fn render(&mut self, sample_rate: u32, output_device_buffer: &mut [(f32, f32)]) { let last_time = fyrox_core::instant::Instant::now(); if !self.paused { @@ -294,7 +284,7 @@ impl State { { if let Some(bus_input_buffer) = self.bus_graph.try_get_bus_input_buffer(&source.bus) { - source.render(output_device_buffer.len()); + source.render(sample_rate, output_device_buffer.len()); match self.renderer { Renderer::Default => { @@ -308,6 +298,7 @@ impl State { } Renderer::HrtfRenderer(ref mut hrtf_renderer) => { hrtf_renderer.render_source( + sample_rate, source, &self.listener, self.distance_model, @@ -318,7 +309,7 @@ impl State { } } - self.bus_graph.end_render(output_device_buffer); + self.bus_graph.end_render(sample_rate, output_device_buffer); } self.render_duration = fyrox_core::instant::Instant::now() - last_time; diff --git a/fyrox-sound/src/effects/filter.rs b/fyrox-sound/src/effects/filter.rs index 671f1f952..af014b169 100644 --- a/fyrox-sound/src/effects/filter.rs +++ b/fyrox-sound/src/effects/filter.rs @@ -21,7 +21,6 @@ //! Contains various filter effects. For example, lowpass filter could be used to muffle sounds. use crate::{ - context::SAMPLE_RATE, dsp::{filters::Biquad, filters::BiquadKind}, effects::EffectRenderTrait, }; @@ -44,6 +43,10 @@ macro_rules! define_filter_effect { #[reflect(setter = "set_quality")] quality: f32, + #[reflect(hidden)] + #[visit(skip)] + sample_rate: u32, + #[reflect(hidden)] left: Biquad, #[reflect(hidden)] @@ -52,20 +55,37 @@ macro_rules! define_filter_effect { impl Default for $name { fn default() -> Self { - let mut filter = Self { + Self { cutoff_frequency_hz: 2200.0, gain: 1.0, quality: 0.5, left: Default::default(), right: Default::default(), - }; - filter.update(); - filter + sample_rate: 0 + } } } impl EffectRenderTrait for $name { - fn render(&mut self, input: &[(f32, f32)], output: &mut [(f32, f32)]) { + fn render(&mut self, sample_rate: u32, input: &[(f32, f32)], output: &mut [(f32, f32)]) { + if self.sample_rate != sample_rate { + self.sample_rate = sample_rate; + + self.left.tune( + $kind, + self.cutoff_frequency_hz / self.sample_rate as f32, + self.gain, + self.quality + ); + + self.right.tune( + $kind, + self.cutoff_frequency_hz / self.sample_rate as f32, + self.gain, + self.quality + ); + } + for ((input_left, input_right), (output_left, output_right)) in input.iter().zip(output) { *output_left = self.left.feed(*input_left); *output_right = self.right.feed(*input_right); @@ -78,7 +98,7 @@ macro_rules! define_filter_effect { #[inline] pub fn set_gain(&mut self, gain: f32) { self.gain = gain; - self.update(); + self.sample_rate = 0; } /// Returns filter's gain coefficient. @@ -92,7 +112,7 @@ macro_rules! define_filter_effect { #[inline] pub fn set_cutoff_frequency_hz(&mut self, freq: f32) { self.cutoff_frequency_hz = freq; - self.update(); + self.sample_rate = 0; } /// Returns a cutoff frequency of the filter in Hertz. @@ -106,7 +126,7 @@ macro_rules! define_filter_effect { #[inline] pub fn set_quality(&mut self, quality: f32) { self.quality = quality; - self.update(); + self.sample_rate = 0; } /// Returns the quality of the filter. @@ -114,22 +134,6 @@ macro_rules! define_filter_effect { pub fn quality(&self) -> f32 { self.quality } - - fn update(&mut self) { - self.left.tune( - $kind, - self.cutoff_frequency_hz / SAMPLE_RATE as f32, - self.gain, - self.quality - ); - - self.right.tune( - $kind, - self.cutoff_frequency_hz / SAMPLE_RATE as f32, - self.gain, - self.quality - ) - } } }; } diff --git a/fyrox-sound/src/effects/mod.rs b/fyrox-sound/src/effects/mod.rs index d025190d0..4857f3fb8 100644 --- a/fyrox-sound/src/effects/mod.rs +++ b/fyrox-sound/src/effects/mod.rs @@ -55,7 +55,7 @@ impl Attenuate { } impl EffectRenderTrait for Attenuate { - fn render(&mut self, input: &[(f32, f32)], output: &mut [(f32, f32)]) { + fn render(&mut self, _sample_rate: u32, input: &[(f32, f32)], output: &mut [(f32, f32)]) { for ((input_left, input_right), (output_left, output_right)) in input.iter().zip(output.iter_mut()) { @@ -97,7 +97,7 @@ impl Default for Effect { } pub(crate) trait EffectRenderTrait { - fn render(&mut self, input: &[(f32, f32)], output: &mut [(f32, f32)]); + fn render(&mut self, sample_rate: u32, input: &[(f32, f32)], output: &mut [(f32, f32)]); } macro_rules! static_dispatch { @@ -116,7 +116,7 @@ macro_rules! static_dispatch { } impl EffectRenderTrait for Effect { - fn render(&mut self, input: &[(f32, f32)], output: &mut [(f32, f32)]) { - static_dispatch!(self, render, input, output) + fn render(&mut self, sample_rate: u32, input: &[(f32, f32)], output: &mut [(f32, f32)]) { + static_dispatch!(self, render, sample_rate, input, output) } } diff --git a/fyrox-sound/src/effects/reverb.rs b/fyrox-sound/src/effects/reverb.rs index 68ae10426..4f096f241 100644 --- a/fyrox-sound/src/effects/reverb.rs +++ b/fyrox-sound/src/effects/reverb.rs @@ -275,7 +275,7 @@ impl Reverb { /// # Notes /// /// This method uses normalized frequency as input, this means that you should divide your desired - /// frequency in hertz by sample rate of sound context. Context has `normalize_frequency` method + /// frequency in hertz by sample rate of sound context. Sound engine has `normalize_frequency` method /// exactly for this purpose. pub fn set_fc(&mut self, fc: f32) { self.fc = fc; @@ -290,7 +290,7 @@ impl Reverb { } impl EffectRenderTrait for Reverb { - fn render(&mut self, input: &[(f32, f32)], mix_buf: &mut [(f32, f32)]) { + fn render(&mut self, _sample_rate: u32, input: &[(f32, f32)], mix_buf: &mut [(f32, f32)]) { let wet = self.wet; let dry = 1.0 - self.wet; diff --git a/fyrox-sound/src/engine.rs b/fyrox-sound/src/engine.rs index fbdde8bcf..b9647f20b 100644 --- a/fyrox-sound/src/engine.rs +++ b/fyrox-sound/src/engine.rs @@ -24,7 +24,7 @@ //! //! Sound engine manages contexts, feeds output device with data. -use crate::context::{SoundContext, SAMPLE_RATE}; +use crate::context::SoundContext; use fyrox_core::visitor::{Visit, VisitResult, Visitor}; use fyrox_core::SafeLock; use std::error::Error; @@ -43,6 +43,7 @@ impl Default for SoundEngine { /// Internal state of the sound engine. pub struct State { + sample_rate: u32, contexts: Vec, output_device: Option, } @@ -62,18 +63,37 @@ impl SoundEngine { /// method to an output device (or a file, etc.). pub fn without_device() -> Self { Self(Arc::new(Mutex::new(State { + sample_rate: 44100, contexts: Default::default(), output_device: None, }))) } + /// Sets the sample rate for the sound engine and recreates the output device. + pub fn set_sample_rate(&mut self, sample_rate: u32) -> Result<(), Box> { + self.state().sample_rate = sample_rate; + self.initialize_audio_output_device() + } + + /// Returns current sample rate of the sound engine. + pub fn sample_rate(&self) -> u32 { + self.state().sample_rate + } + + /// Normalizes given frequency using context's sampling rate. Normalized frequency then can be used + /// to create filters. + pub fn normalize_frequency(&self, f: f32) -> f32 { + f / self.sample_rate() as f32 + } + /// Tries to initialize default audio output device. pub fn initialize_audio_output_device(&self) -> Result<(), Box> { - let state = self.clone(); + let sample_rate = self.sample_rate() as usize; + let this = self.clone(); let device = tinyaudio::run_output_device( tinyaudio::OutputDeviceParameters { - sample_rate: SAMPLE_RATE as usize, + sample_rate, channels_count: 2, channel_sample_count: SoundContext::SAMPLES_PER_CHANNEL, }, @@ -87,7 +107,7 @@ impl SoundEngine { ) }; - state.state().render(data); + this.state().render(data); } }, )?; @@ -159,7 +179,7 @@ impl State { fn render_inner(&mut self, buf: &mut [(f32, f32)]) { for context in self.contexts.iter_mut() { - context.state().render(buf); + context.state().render(self.sample_rate, buf); } } } diff --git a/fyrox-sound/src/renderer/hrtf.rs b/fyrox-sound/src/renderer/hrtf.rs index b044119e5..7bc30a803 100644 --- a/fyrox-sound/src/renderer/hrtf.rs +++ b/fyrox-sound/src/renderer/hrtf.rs @@ -74,7 +74,7 @@ //! Clicks can be reproduced by using clean sine wave of 440 Hz on some source moving around listener. use crate::{ - context::{self, DistanceModel, SoundContext}, + context::{DistanceModel, SoundContext}, listener::Listener, renderer::render_source_2d_only, source::SoundSource, @@ -94,6 +94,7 @@ use fyrox_resource::{ Resource, ResourceData, }; use hrtf::HrirSphere; +use std::io::Cursor; use std::{error::Error, ops::Deref}; use std::{fmt::Debug, fmt::Formatter, path::PathBuf, sync::Arc}; use std::{fmt::Display, path::Path}; @@ -159,16 +160,12 @@ impl Visit for HrtfRenderer { impl HrtfRenderer { /// Creates new HRTF renderer using specified HRTF sphere. See module docs for more info. - pub fn new(hrir_sphere_resource: HrirSphereResource) -> Self { + pub fn new(sample_rate: u32, hrir_sphere_resource: HrirSphereResource) -> Self { + let processor = hrir_sphere_resource + .data_ref() + .make_hrtf_processor(sample_rate); Self { - processor: Some(hrtf::HrtfProcessor::new( - { - let sphere = hrir_sphere_resource.data_ref().hrir_sphere.clone().unwrap(); - sphere - }, - SoundContext::HRTF_INTERPOLATION_STEPS, - SoundContext::HRTF_BLOCK_LEN, - )), + processor, hrir_resource: Some(hrir_sphere_resource), } } @@ -187,6 +184,7 @@ impl HrtfRenderer { pub(crate) fn render_source( &mut self, + sample_rate: u32, source: &mut SoundSource, listener: &Listener, distance_model: DistanceModel, @@ -198,11 +196,7 @@ impl HrtfRenderer { if let Some(resource) = self.hrir_resource.as_ref() { let mut header = resource.state(); if let Some(hrir) = header.data() { - self.processor = Some(hrtf::HrtfProcessor::new( - hrir.hrir_sphere.clone().unwrap(), - SoundContext::HRTF_INTERPOLATION_STEPS, - SoundContext::HRTF_BLOCK_LEN, - )); + self.processor = hrir.make_hrtf_processor(sample_rate); } } } @@ -242,13 +236,41 @@ impl HrtfRenderer { } } +#[derive(Clone)] +enum HrirSource { + Preloaded(HrirSphere), + RawData(Vec), +} + +impl Default for HrirSource { + fn default() -> Self { + Self::RawData(Default::default()) + } +} + /// Wrapper for [`HrirSphere`] to be able to use it in the resource manager, that will handle async resource /// loading automatically. #[derive(Reflect, Default, Clone, Visit)] pub struct HrirSphereResourceData { #[reflect(hidden)] #[visit(skip)] - hrir_sphere: Option, + source: HrirSource, +} + +impl HrirSphereResourceData { + fn make_hrtf_processor(&self, sample_rate: u32) -> Option { + let hrir_sphere = match self.source { + HrirSource::Preloaded(ref sphere) => sphere.clone(), + HrirSource::RawData(ref raw_data) => { + HrirSphere::new(Cursor::new(raw_data), sample_rate).ok()? + } + }; + Some(hrtf::HrtfProcessor::new( + hrir_sphere, + SoundContext::HRTF_INTERPOLATION_STEPS, + SoundContext::HRTF_BLOCK_LEN, + )) + } } impl Debug for HrirSphereResourceData { @@ -295,12 +317,8 @@ impl ResourceLoader for HrirSphereLoader { fn load(&self, path: PathBuf, io: Arc) -> BoxedLoaderFuture { Box::pin(async move { - let reader = io.file_reader(&path).await.map_err(LoadError::new)?; - let hrir_sphere = HrirSphere::new(reader, context::SAMPLE_RATE) - .map_err(HrtfError::from) - .map_err(LoadError::new)?; Ok(LoaderPayload::new(HrirSphereResourceData { - hrir_sphere: Some(hrir_sphere), + source: HrirSource::RawData(io.load_file(&path).await.map_err(LoadError::new)?), })) }) } @@ -322,7 +340,7 @@ impl HrirSphereResourceExt for HrirSphereResource { Uuid::new_v4(), kind, HrirSphereResourceData { - hrir_sphere: Some(hrir_sphere), + source: HrirSource::Preloaded(hrir_sphere), }, ) } diff --git a/fyrox-sound/src/source.rs b/fyrox-sound/src/source.rs index eeb347dae..85573057d 100644 --- a/fyrox-sound/src/source.rs +++ b/fyrox-sound/src/source.rs @@ -106,16 +106,6 @@ pub struct SoundSource { looping: bool, #[reflect(min_value = 0.0, max_value = 1.0, step = 0.05)] spatial_blend: f32, - // Important coefficient for runtime resampling. It is used to modify playback speed - // of a source in order to match output device sampling rate. PCM data can be stored - // in various sampling rates (22050 Hz, 44100 Hz, 88200 Hz, etc.) but output device - // is running at fixed sampling rate (usually 44100 Hz). For example if we we'll feed - // data to device with rate of 22050 Hz but device is running at 44100 Hz then we'll - // hear that sound will have high pitch (2.0), to fix that we'll just pre-multiply - // playback speed by 0.5. - // However such auto-resampling has poor quality, but it is fast. - #[reflect(read_only)] - resampling_multiplier: f64, status: Status, #[visit(optional)] pub(crate) bus: String, @@ -173,7 +163,6 @@ impl Default for SoundSource { gain: 1.0, spatial_blend: 1.0, looping: false, - resampling_multiplier: 1.0, status: Status::Stopped, bus: "Master".to_string(), play_once: false, @@ -252,11 +241,6 @@ impl SoundSource { } streaming.use_count += 1; } - - // Make sure to recalculate resampling multiplier, otherwise sound will play incorrectly. - let device_sample_rate = f64::from(crate::context::SAMPLE_RATE); - let sample_rate = locked_buffer.sample_rate() as f64; - self.resampling_multiplier = sample_rate / device_sample_rate; } } } @@ -518,7 +502,7 @@ impl SoundSource { } } - pub(crate) fn render(&mut self, amount: usize) { + pub(crate) fn render(&mut self, sample_rate: u32, amount: usize) { if self.frame_samples.capacity() < amount { self.frame_samples = Vec::with_capacity(amount); } @@ -529,7 +513,7 @@ impl SoundSource { let mut state = buffer.state(); if let Some(buffer) = state.data() { if self.status == Status::Playing && !buffer.is_empty() { - self.render_playing(buffer, amount); + self.render_playing(sample_rate, buffer, amount); } } } @@ -537,10 +521,10 @@ impl SoundSource { self.frame_samples.resize(amount, (0.0, 0.0)); } - fn render_playing(&mut self, buffer: &mut SoundBuffer, amount: usize) { + fn render_playing(&mut self, sample_rate: u32, buffer: &mut SoundBuffer, amount: usize) { let mut count = 0; loop { - count += self.render_until_block_end(buffer, amount - count); + count += self.render_until_block_end(sample_rate, buffer, amount - count); if count == amount { break; } @@ -573,8 +557,23 @@ impl SoundSource { // Renders until the end of the block or until amount samples is written and returns // the number of written samples. - fn render_until_block_end(&mut self, buffer: &mut SoundBuffer, mut amount: usize) -> usize { - let step = self.pitch * self.resampling_multiplier; + fn render_until_block_end( + &mut self, + sample_rate: u32, + buffer: &mut SoundBuffer, + mut amount: usize, + ) -> usize { + // Important coefficient for runtime resampling. It is used to modify playback speed + // of a source in order to match output device sampling rate. PCM data can be stored + // in various sampling rates (22050 Hz, 44100 Hz, 88200 Hz, etc.) but output device + // is running at fixed sampling rate (usually 44100 Hz). For example if we we'll feed + // data to device with rate of 22050 Hz but device is running at 44100 Hz then we'll + // hear that sound will have high pitch (2.0), to fix that we'll just pre-multiply + // playback speed by 0.5. + // However such auto-resampling has poor quality, but it is fast. + let resampling_multiplier = buffer.sample_rate as f64 / f64::from(sample_rate); + + let step = self.pitch * resampling_multiplier; if step == 1.0 { if self.buf_read_pos < 0.0 { // This can theoretically happen if we change pitch on the fly.