From efd821ec0d1a98e692001078d8ba5314878805cb Mon Sep 17 00:00:00 2001 From: kaho Date: Mon, 22 Apr 2024 15:44:06 +0800 Subject: [PATCH 1/2] wip: impl shape instance cache --- Cargo.lock | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 5 +- src/shape.rs | 99 ++++++++++++++++++++++++++++-- 4 files changed, 269 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3d372b..7e4707c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anyhow" version = "1.0.82" @@ -26,6 +44,7 @@ version = "0.0.0-dev" dependencies = [ "anyhow", "bspline", + "cached", "enum_dispatch", "float-cmp", "itertools", @@ -47,6 +66,39 @@ dependencies = [ "trait-set", ] +[[package]] +name = "cached" +version = "0.49.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8e463fceca5674287f32d252fb1d94083758b8709c160efae66d263e5f4eba" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown", + "instant", + "once_cell", + "thiserror", +] + +[[package]] +name = "cached_proc_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9f16c0d84de31a2ab7fdf5f7783c14631f7075cf464eb3bb43119f61c9cb2a" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + [[package]] name = "cc" version = "1.0.94" @@ -59,6 +111,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.11.0" @@ -86,18 +173,49 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indoc" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.12.1" @@ -422,6 +540,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -450,6 +574,26 @@ version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +[[package]] +name = "thiserror" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "trait-set" version = "0.2.0" @@ -473,6 +617,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "windows-targets" version = "0.48.5" @@ -529,3 +679,23 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] diff --git a/Cargo.toml b/Cargo.toml index f9969e0..e48f5f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib"] [dependencies] anyhow = "1.0.82" bspline = "1.1.0" +cached = "0.49.3" enum_dispatch = "0.3.13" float-cmp = "0.9.0" itertools = "0.12.1" diff --git a/src/lib.rs b/src/lib.rs index 785324a..70d6196 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,11 +154,12 @@ impl Shape { } if let Ok(interp) = slf.downcast::() { let interp = interp.get(); - return Ok(shape::Shape::new_interp( + return shape::Shape::new_interp( interp.knots.clone(), interp.controls.clone(), interp.degree, - )); + ) + .map_err(|e| PyValueError::new_err(e.to_string())); } Err(PyTypeError::new_err("Invalid shape type.")) } diff --git a/src/shape.rs b/src/shape.rs index 9f15ec1..0b4f70c 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -1,16 +1,39 @@ +use std::hash::Hash; +use std::sync::Arc; + +use anyhow::{anyhow, Result}; use bspline::BSpline; +use cached::proc_macro::cached; use enum_dispatch::enum_dispatch; +use ordered_float::NotNan; +/// A shape that can be used to modulate the amplitude of a signal. +/// +/// The shape is defined in the range \[-0.5, 0.5\]. +/// +/// Internally, shape instances are cached such that we can compare and hash +/// by instance address. #[derive(Debug, Clone)] -pub struct Shape(ShapeVariant); +pub struct Shape(Arc); impl Shape { pub fn new_hann() -> Self { - Self(ShapeVariant::Hann(Hann)) + Self(get_shape_instance(ShapeKey::Hann)) } - pub fn new_interp(knots: Vec, controls: Vec, degree: usize) -> Self { - Self(ShapeVariant::Interp(Interp::new(knots, controls, degree))) + pub fn new_interp(knots: Vec, controls: Vec, degree: usize) -> Result { + let knots = knots + .into_iter() + .map(NotNan::new) + .collect::>() + .map_err(|_| anyhow!("Nan in knots"))?; + let controls = controls + .into_iter() + .map(NotNan::new) + .collect::>() + .map_err(|_| anyhow!("Nan in controls"))?; + let key = ShapeKey::Interp(knots, controls, degree); + Ok(Self(get_shape_instance(key))) } pub fn sample_array(&self, x0: f64, dx: f64, array: &mut [f64]) { @@ -18,6 +41,41 @@ impl Shape { } } +impl Hash for Shape { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state); + } +} + +impl PartialEq for Shape { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for Shape {} + +type HashableArray = Vec>; + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +enum ShapeKey { + Hann, + Interp(HashableArray, HashableArray, usize), +} + +#[cached(size = 128)] +fn get_shape_instance(a: ShapeKey) -> Arc { + let variant = match a { + ShapeKey::Hann => Hann.into(), + ShapeKey::Interp(t, c, k) => { + let t = t.into_iter().map(|v| v.into()).collect(); + let c = c.into_iter().map(|v| v.into()).collect(); + Interp::new(t, c, k).into() + } + }; + Arc::new(variant) +} + #[enum_dispatch(ShapeTrait)] #[derive(Debug, Clone)] enum ShapeVariant { @@ -149,4 +207,37 @@ mod tests { assert_approx_eq!(f64, interp.sample(x), y); } } + + #[test] + fn test_shape_eq() { + let h1 = Shape::new_hann(); + let h2 = Shape::new_hann(); + assert_eq!(h1, h2); + let knots = vec![ + -0.5, + -0.5, + -0.5, + -0.5, + -0.16666666666666669, + 0.0, + 0.16666666666666663, + 0.5, + 0.5, + 0.5, + 0.5, + ]; + let controls = vec![ + 6.123233995736766e-17, + 0.35338865119588236, + 0.8602099957160162, + 1.0465966680946615, + 0.8602099957160163, + 0.35338865119588264, + 6.123233995736766e-17, + ]; + let i1 = Shape::new_interp(knots.clone(), controls.clone(), 3).unwrap(); + let i2 = Shape::new_interp(knots.clone(), controls.clone(), 3).unwrap(); + assert_eq!(i1, i2); + assert_ne!(h1, i1); + } } From c492107fad978e0101dbead248dc3264ee65cecc Mon Sep 17 00:00:00 2001 From: kaho Date: Mon, 22 Apr 2024 18:19:58 +0800 Subject: [PATCH 2/2] feat: impl simple envelope caching --- src/lib.rs | 3 +- src/sampler.rs | 84 +++++++++++++++++++++++++++++++++----------------- src/time.rs | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 src/time.rs diff --git a/src/lib.rs b/src/lib.rs index 70d6196..411ce35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use crate::sampler::Sampler; mod sampler; mod schedule; mod shape; +mod time; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; @@ -2013,7 +2014,7 @@ fn generate_waveforms( .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; let mut sampler = Sampler::new(); for c in channels.iter() { - sampler.add_channel(c.base_freq, c.sample_rate, c.length, c.delay); + sampler.add_channel(c.base_freq, c.sample_rate, c.length, c.delay, c.align_level); } for s in shapes.iter() { let s = s.bind(py); diff --git a/src/sampler.rs b/src/sampler.rs index b86c453..352bbca 100644 --- a/src/sampler.rs +++ b/src/sampler.rs @@ -1,11 +1,14 @@ -use std::f64::consts::TAU; +use std::{f64::consts::TAU, sync::Arc}; +use cached::proc_macro::cached; use itertools::izip; use numpy::Complex64; +use ordered_float::NotNan; use crate::{ schedule::{self, ArrangedElement, ElementVariant}, shape::Shape, + time::{AlignedIndex, Time}, }; #[derive(Debug, Clone, Default)] @@ -19,9 +22,21 @@ impl Sampler { Self::default() } - pub fn add_channel(&mut self, base_freq: f64, sample_rate: f64, length: usize, delay: f64) { - self.channels - .push(Channel::new(base_freq, sample_rate, length, delay)); + pub fn add_channel( + &mut self, + base_freq: f64, + sample_rate: f64, + length: usize, + delay: f64, + align_level: i32, + ) { + self.channels.push(Channel::new( + base_freq, + sample_rate, + length, + delay, + align_level, + )); } pub fn add_shape(&mut self, shape: Shape) { @@ -62,7 +77,7 @@ impl Sampler { } fn execute_play(&mut self, element: &schedule::Play, time: f64, duration: f64) { - let shape = element.shape_id().map(|id| &self.shapes[id]); + let shape = element.shape_id().map(|id| self.shapes[id].clone()); let width = element.width(); let plateau = if element.flexible() { duration - width @@ -74,6 +89,9 @@ impl Sampler { let freq = element.frequency(); let phase = element.phase(); let channel = &mut self.channels[element.channel_id()]; + let time = Time::new(time).unwrap(); + let width = Time::new(width).unwrap(); + let plateau = Time::new(plateau).unwrap(); channel.sample( shape, time, width, plateau, amplitude, drag_coef, freq, phase, ); @@ -150,18 +168,20 @@ struct Channel { phase: f64, sample_rate: f64, waveform: Vec, - delay: f64, + delay: Time, + align_level: i32, } impl Channel { - fn new(base_freq: f64, sample_rate: f64, length: usize, delay: f64) -> Self { + fn new(base_freq: f64, sample_rate: f64, length: usize, delay: f64, align_level: i32) -> Self { Self { base_freq, delta_freq: 0.0, phase: 0.0, sample_rate, waveform: vec![Complex64::default(); length], - delay, + delay: Time::new(delay).unwrap(), + align_level, } } @@ -200,49 +220,55 @@ impl Channel { fn sample( &mut self, - shape: Option<&Shape>, - time: f64, - width: f64, - plateau: f64, + shape: Option, + time: Time, + width: Time, + plateau: Time, amplitude: f64, drag_coef: f64, freq: f64, phase: f64, ) { let t_start = time + self.delay; - let i_frac_start = t_start * self.sample_rate; - let i_start = i_frac_start.ceil() as usize; - let index_offset = i_start as f64 - i_frac_start; + let i_frac_start = AlignedIndex::new(t_start, self.sample_rate, self.align_level).unwrap(); + let i_start = i_frac_start.ceil(); + let index_offset = i_frac_start.index_offset(); let global_freq = self.total_freq(); let local_freq = freq; let total_freq = global_freq + local_freq; let dt = 1.0 / self.sample_rate; let phase0 = phase + self.phase - + global_freq * (i_start as f64 * dt - self.delay) - + local_freq * index_offset * dt; + + global_freq * (i_start.value() * dt - self.delay.value()) + + local_freq * index_offset.value() * dt; let dphase = total_freq * dt; let phase0 = phase0 * TAU; let dphase = dphase * TAU; - let waveform = &mut self.waveform[i_start..]; + let waveform = &mut self.waveform[i_start.value() as usize..]; if let Some(shape) = shape { - let envelope = get_envelope(shape, width, plateau, self.sample_rate, index_offset); + let sample_rate = NotNan::new(self.sample_rate).unwrap(); + let envelope = get_envelope(shape, width, plateau, index_offset, sample_rate); let drag_coef = drag_coef * self.sample_rate; mix_add_envelope(waveform, &envelope, amplitude, drag_coef, phase0, dphase); } else { - let i_plateau = ((width + plateau) * self.sample_rate).ceil() as usize; + let i_plateau = ((width + plateau).value() * self.sample_rate).ceil() as usize; mix_add_plateau(&mut waveform[..i_plateau], amplitude, phase0, dphase); } } } +#[cached(size = 1024)] fn get_envelope( - shape: &Shape, - width: f64, - plateau: f64, - sample_rate: f64, - index_offset: f64, -) -> Vec { + shape: Shape, + width: Time, + plateau: Time, + index_offset: AlignedIndex, + sample_rate: NotNan, +) -> Arc> { + let width = width.value(); + let plateau = plateau.value(); + let index_offset = index_offset.value(); + let sample_rate = sample_rate.into_inner(); let dt = 1.0 / sample_rate; let t_offset = index_offset * dt; let t1 = width / 2.0 - t_offset; @@ -262,7 +288,7 @@ fn get_envelope( let x2 = (plateau_end_index as f64 * dt - t2) / width; shape.sample_array(x2, dx, &mut envelope[plateau_end_index..]); } - envelope + Arc::new(envelope) } fn mix_add_envelope( @@ -291,10 +317,10 @@ fn mix_add_envelope( } pub fn mix_add_plateau(waveform: &mut [Complex64], amplitude: f64, phase: f64, dphase: f64) { - let mut carrier = Complex64::from_polar(1.0, phase); + let mut carrier = Complex64::from_polar(amplitude, phase); let dcarrier = Complex64::from_polar(1.0, dphase); for y in waveform.iter_mut() { - *y += carrier * amplitude; + *y += carrier; carrier *= dcarrier; } } diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..04b8b96 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,57 @@ +use std::ops::Add; + +use anyhow::{anyhow, Result}; +use ordered_float::NotNan; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Time(NotNan); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct AlignedIndex(NotNan); + +impl Time { + pub fn new(value: f64) -> Result { + Ok(Self( + NotNan::new(value).map_err(|_| anyhow!("NaN in Time value"))?, + )) + } + + pub fn value(&self) -> f64 { + self.0.into_inner() + } +} + +impl AlignedIndex { + pub fn new(time: Time, sample_rate: f64, align_level: i32) -> Result { + let scaled_sr = scaleb(sample_rate, -align_level); + let i = (time.value() * scaled_sr).ceil(); + let aligned_index = scaleb(i, align_level); + Ok(Self( + NotNan::new(aligned_index).map_err(|_| anyhow!("NaN in AlignedIndex value"))?, + )) + } + + pub fn value(&self) -> f64 { + self.0.into_inner() + } + + pub fn ceil(&self) -> Self { + Self(NotNan::new(self.0.ceil()).unwrap()) + } + + pub fn index_offset(&self) -> Self { + Self(NotNan::new(self.0.ceil() - self.0.into_inner()).unwrap()) + } +} + +fn scaleb(x: f64, s: i32) -> f64 { + x * (s as f64).exp2() +} + +impl Add for Time { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +}