From d29fb6d61a64b3145653e4785c961ca2711cfad5 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Sat, 28 Jan 2023 19:59:33 +0100 Subject: [PATCH] WIP resampling --- rhubarb/rhubarb-audio/build.rs | 26 +++ rhubarb/rhubarb-audio/src/audio_filters.rs | 14 +- rhubarb/rhubarb-audio/src/lib.rs | 1 + .../resampled_audio_clip/libsamplerate_raw.rs | 53 ++++++ .../src/resampled_audio_clip/mod.rs | 4 + .../resampled_audio_clip.rs | 175 ++++++++++++++++++ 6 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 rhubarb/rhubarb-audio/src/resampled_audio_clip/libsamplerate_raw.rs create mode 100644 rhubarb/rhubarb-audio/src/resampled_audio_clip/mod.rs create mode 100644 rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs diff --git a/rhubarb/rhubarb-audio/build.rs b/rhubarb/rhubarb-audio/build.rs index 65580d5..4189cfd 100644 --- a/rhubarb/rhubarb-audio/build.rs +++ b/rhubarb/rhubarb-audio/build.rs @@ -121,12 +121,38 @@ fn build_vorbis( VorbisBuildResult { vorbis_utils_path } } +fn build_libsamplerate(parent_dir: impl AsRef) { + let version = "0.2.2"; + let repo_dir = checkout( + &parent_dir, + "https://github.com/libsndfile/libsamplerate.git", + version, + "libsamplerate", + ); + let include_dir = repo_dir.join("include"); + let src_dir = repo_dir.join("src"); + cc::Build::new() + .define("PACKAGE", "\"libsamplerate\"") + .define("VERSION", format!("\"{version}\"").as_str()) + .define("HAVE_STDBOOL_H", "1") + .define("ENABLE_SINC_MEDIUM_CONVERTER", "1") + .include(&include_dir) + .files( + ["samplerate.c", "src_linear.c", "src_sinc.c", "src_zoh.c"] + .map(|name| src_dir.join(name)), + ) + .compile("libsamplerate"); + + println!("cargo:rustc-link-lib=static=libsamplerate"); +} + fn main() { let out_dir = Path::new(&var_os("OUT_DIR").unwrap()).to_path_buf(); println!("cargo:rustc-link-search=native={}", out_dir.display()); let OggBuildResult { ogg_include_dir } = build_ogg(&out_dir); let VorbisBuildResult { vorbis_utils_path } = build_vorbis(&out_dir, ogg_include_dir); + build_libsamplerate(out_dir); println!("cargo:rerun-if-changed={}", vorbis_utils_path.display()); } diff --git a/rhubarb/rhubarb-audio/src/audio_filters.rs b/rhubarb/rhubarb-audio/src/audio_filters.rs index 6f27864..1fb31d0 100644 --- a/rhubarb/rhubarb-audio/src/audio_filters.rs +++ b/rhubarb/rhubarb-audio/src/audio_filters.rs @@ -1,15 +1,22 @@ -use crate::{AudioClip, Segment}; +use crate::{resampled_audio_clip::ResampledAudioClip, AudioClip, Segment}; /// Blanket implementations of audio filters for audio clips. pub trait AudioFilters { /// Returns a new audio clip containing a segment of the specified clip. fn segment(self, start: u64, end: u64) -> Box; + + /// Returns a new audio clip resampled to the specified sampling rate. + fn resampled(self, sampling_rate: u32) -> Box; } impl AudioFilters for Box { fn segment(self, start: u64, end: u64) -> Box { Box::new(Segment::new(self, start, end)) } + + fn resampled(self, sampling_rate: u32) -> Box { + Box::new(ResampledAudioClip::new(self, sampling_rate)) + } } impl AudioFilters for TAudioClip @@ -20,4 +27,9 @@ where let boxed_audio_clip: Box = Box::new(self); boxed_audio_clip.segment(start, end) } + + fn resampled(self, sampling_rate: u32) -> Box { + let boxed_audio_clip: Box = Box::new(self); + boxed_audio_clip.resampled(sampling_rate) + } } diff --git a/rhubarb/rhubarb-audio/src/lib.rs b/rhubarb/rhubarb-audio/src/lib.rs index 1287955..5cdc584 100644 --- a/rhubarb/rhubarb-audio/src/lib.rs +++ b/rhubarb/rhubarb-audio/src/lib.rs @@ -19,6 +19,7 @@ mod memory_audio_clip; mod ogg_audio_clip; mod open_audio_file; mod read_and_seek; +mod resampled_audio_clip; mod sample_reader_assertions; mod segment; mod wave_audio_clip; diff --git a/rhubarb/rhubarb-audio/src/resampled_audio_clip/libsamplerate_raw.rs b/rhubarb/rhubarb-audio/src/resampled_audio_clip/libsamplerate_raw.rs new file mode 100644 index 0000000..f823f29 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/resampled_audio_clip/libsamplerate_raw.rs @@ -0,0 +1,53 @@ +// Raw FFI for libsamplerate + +use std::os::raw::{c_int, c_long, c_void}; + +use crate::Sample; + +#[link(name = "libsamplerate")] +extern "C" { + pub fn src_callback_new( + read_func: unsafe extern "C" fn( + callback_data: *mut c_void, + buffer: *mut *const Sample, + ) -> c_long, + converter_type: ConverterType, + channel_count: c_int, + error: *mut c_int, + callback_data: *mut c_void, + ) -> *mut State; + + pub fn src_callback_read( + state: *mut State, + ratio: f64, + frame_count: c_long, + buffer: *mut Sample, + ) -> c_long; + + pub fn src_error(state: *mut State) -> c_int; + + pub fn src_reset(state: *mut State) -> c_int; + + // Always returns null + pub fn src_delete(state: *mut State) -> *const c_void; +} + +#[repr(C)] +#[allow(dead_code)] +pub enum ConverterType { + /// Band-limited sinc interpolation, best quality, 144 dB SNR, 96% BW. + SincBestQuality = 0, + /// Band-limited sinc interpolation, medium quality, 121 dB SNR, 90% BW. + SincMediumQuality = 1, + /// Band-limited sinc interpolation, low quality, 97 dB SNR, 80% BW. + SincFastest = 2, + /// Zero order hold interpolator, very fast, poor quality. + ZeroOrderHold = 3, + /// Linear interpolator, blindingly fast, poor quality. + Linear = 4, +} + +#[repr(C)] +pub struct State { + private: [u8; 0], +} diff --git a/rhubarb/rhubarb-audio/src/resampled_audio_clip/mod.rs b/rhubarb/rhubarb-audio/src/resampled_audio_clip/mod.rs new file mode 100644 index 0000000..6a962d3 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/resampled_audio_clip/mod.rs @@ -0,0 +1,4 @@ +mod libsamplerate_raw; +mod resampled_audio_clip; + +pub use resampled_audio_clip::ResampledAudioClip; diff --git a/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs b/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs new file mode 100644 index 0000000..39d2662 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs @@ -0,0 +1,175 @@ +use crate::{ + resampled_audio_clip::libsamplerate_raw::src_error, + sample_reader_assertions::SampleReaderAssertions, AudioClip, AudioError, Sample, SampleReader, +}; +use std::{ + cmp::min, + mem::MaybeUninit, + os::raw::{c_long, c_void}, + pin::Pin, + sync::Arc, +}; + +use super::libsamplerate_raw::{ + src_callback_new, src_callback_read, src_delete, src_reset, ConverterType, State, +}; + +/// A resampled representation of another audio clip. +#[derive(Debug, Clone)] +pub struct ResampledAudioClip { + inner_clip: Arc, + sampling_rate: u32, +} + +impl ResampledAudioClip { + pub fn new(inner_clip: Box, sampling_rate: u32) -> Self { + Self { + inner_clip: Arc::from(inner_clip), + sampling_rate, + } + } + + fn factor(&self) -> f64 { + f64::from(self.sampling_rate) / f64::from(self.inner_clip.sampling_rate()) + } +} + +impl AudioClip for ResampledAudioClip { + fn len(&self) -> u64 { + (self.inner_clip.len() as f64 * self.factor()).round() as u64 + } + + fn sampling_rate(&self) -> u32 { + self.sampling_rate + } + + fn create_sample_reader(&self) -> Result, AudioError> { + Ok(Box::new(ResampledSampleReader::new(self)?)) + } +} + +#[derive(Debug)] +struct ResampledSampleReader { + callback_data: Pin>, + factor: f64, + len: u64, + position: u64, + position_changed: bool, + libsamplerate_state: *mut State, +} + +const INPUT_BUFFER_SIZE: usize = 500; + +#[derive(Debug)] +struct CallbackData { + inner_sample_reader: Box, + input_buffer: [Sample; INPUT_BUFFER_SIZE], + last_error: Option, +} + +impl ResampledSampleReader { + fn new(clip: &ResampledAudioClip) -> Result { + let callback_data = Box::pin(CallbackData { + inner_sample_reader: clip.inner_clip.create_sample_reader()?, + input_buffer: [0.0; INPUT_BUFFER_SIZE], + last_error: None, + }); + let channel_count = 1; + let mut error = MaybeUninit::uninit(); + let libsamplerate_state = unsafe { + src_callback_new( + read, + ConverterType::SincMediumQuality, + channel_count, + error.as_mut_ptr(), + &*callback_data as *const CallbackData as *mut c_void, + ) + }; + assert_eq!(unsafe { error.assume_init() }, 0); + Ok(Self { + callback_data, + factor: clip.factor(), + len: clip.len(), + position: 0, + position_changed: false, + libsamplerate_state, + }) + } +} + +impl Drop for ResampledSampleReader { + fn drop(&mut self) { + unsafe { + src_delete(self.libsamplerate_state); + } + } +} + +impl SampleReader for ResampledSampleReader { + fn len(&self) -> u64 { + self.len + } + + fn position(&self) -> u64 { + self.position + } + + fn set_position(&mut self, position: u64) { + if position == self.position { + return; + } + + self.assert_valid_seek_position(position); + self.position = position; + self.position_changed = true; + } + + fn read(&mut self, buffer: &mut [Sample]) -> Result<(), AudioError> { + self.assert_valid_read_size(buffer); + if self.position_changed { + self.callback_data + .inner_sample_reader + .set_position((self.position as f64 / self.factor) as u64); + let error = unsafe { src_reset(self.libsamplerate_state) }; + assert_eq!(error, 0); + self.position_changed = false; + } + + let samples_read = unsafe { + src_callback_read( + self.libsamplerate_state, + self.factor, + buffer.len() as i32, + buffer.as_mut_ptr(), + ) as usize + }; + if let Some(error) = self.callback_data.last_error.take() { + return Err(error); + } + if samples_read < buffer.len() { + assert_eq!(unsafe { src_error(self.libsamplerate_state) }, 0); + panic!("Expected {} samples, got {}.", buffer.len(), samples_read); + } + self.position += buffer.len() as u64; + + Ok(()) + } +} + +unsafe extern "C" fn read(callback_data: *mut c_void, buffer: *mut *const Sample) -> c_long { + let callback_data = callback_data.cast::(); + let inner_sample_reader = &mut (*callback_data).inner_sample_reader; + let sample_count = min(inner_sample_reader.remainder(), INPUT_BUFFER_SIZE as u64) as usize; + + // Allow reading beyond the end of the audio clip by filling with zeros + let (sample_buffer, zero_buffer) = (*callback_data).input_buffer.split_at_mut(sample_count); + let read_result = inner_sample_reader.read(sample_buffer); + if let Err(error) = read_result { + (*callback_data).last_error = Some(error); + return 0; + } + zero_buffer.fill(0.0); + + *buffer = sample_buffer.as_ptr(); + sample_buffer.len() as c_long +}