From 825d975785fedc586d2dce68c75aa5756b1beb6b Mon Sep 17 00:00:00 2001 From: Austin Hicks Date: Sat, 14 Dec 2024 21:51:10 -0800 Subject: [PATCH] Lots of WIP --- Cargo.lock | 31 ++- Cargo.toml | 3 +- crates/synthizer/Cargo.toml | 1 + crates/synthizer/examples/two_sine_waves.rs | 24 ++ crates/synthizer/src/chain.rs | 92 ++++++- crates/synthizer/src/chain_mathops.rs | 90 +++++-- crates/synthizer/src/core_traits.rs | 130 ++++++---- crates/synthizer/src/lib.rs | 4 + crates/synthizer/src/mount_point.rs | 17 ++ crates/synthizer/src/signals/and_then.rs | 18 +- crates/synthizer/src/signals/audio_io.rs | 46 ++++ crates/synthizer/src/signals/clock.rs | 8 +- crates/synthizer/src/signals/consume_input.rs | 60 +++++ crates/synthizer/src/signals/conversion.rs | 34 ++- crates/synthizer/src/signals/mod.rs | 4 + crates/synthizer/src/signals/null.rs | 6 +- crates/synthizer/src/signals/periodic_f64.rs | 14 +- crates/synthizer/src/signals/scalars.rs | 12 +- crates/synthizer/src/signals/trig.rs | 14 +- crates/synthizer/src/synthesizer.rs | 241 ++++++++++++++++++ 20 files changed, 721 insertions(+), 128 deletions(-) create mode 100644 crates/synthizer/examples/two_sine_waves.rs create mode 100644 crates/synthizer/src/mount_point.rs create mode 100644 crates/synthizer/src/signals/audio_io.rs create mode 100644 crates/synthizer/src/signals/consume_input.rs create mode 100644 crates/synthizer/src/synthesizer.rs diff --git a/Cargo.lock b/Cargo.lock index 421864b..c21eba4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -37,9 +37,18 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "archery" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" +dependencies = [ + "triomphe", +] [[package]] name = "arrayvec" @@ -1167,6 +1176,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rpds" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0e15515d3ce3313324d842629ea4905c25a13f81953eadb88f85516f59290a4" +dependencies = [ + "archery", +] + [[package]] name = "rubato" version = "0.14.1" @@ -1560,6 +1578,7 @@ dependencies = [ "rand", "rand_xoshiro", "rayon", + "rpds", "rubato", "sharded-slab", "smallvec", @@ -1741,6 +1760,12 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 11fe200..4bbf70d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.dependencies] ahash = "0.8.3" anyhow = "1.0.79" -arc-swap = "1.6.0" +arc-swap = "1.7.1" audio_synchronization = { path = "crates/audio_synchronization" } arrayvec = "0.7.2" atomic_refcell = "0.1.9" @@ -42,6 +42,7 @@ rand = "0.8.5" rand_xoshiro = "0.6.0" rayon = "1.8.0" reciprocal = "0.1.2" +rpds = "1.1.0" rubato = "0.14.1" sharded-slab = "0.1.4" smallvec = { version = "1.10.0", features = ["write"] } diff --git a/crates/synthizer/Cargo.toml b/crates/synthizer/Cargo.toml index e73a948..f58f55e 100644 --- a/crates/synthizer/Cargo.toml +++ b/crates/synthizer/Cargo.toml @@ -25,6 +25,7 @@ num.workspace = true rand.workspace = true rand_xoshiro.workspace = true rayon.workspace = true +rpds.workspace = true rubato.workspace = true sharded-slab.workspace = true smallvec.workspace = true diff --git a/crates/synthizer/examples/two_sine_waves.rs b/crates/synthizer/examples/two_sine_waves.rs new file mode 100644 index 0000000..ceee25e --- /dev/null +++ b/crates/synthizer/examples/two_sine_waves.rs @@ -0,0 +1,24 @@ +use synthizer::*; + +fn main() { + let pi2 = 2.0f64 * std::f64::consts::PI; + let chain1 = Chain::new(500f64) + .divide_by_sr() + .periodic_sum(pi2, 0.0f64) + .sin(); + let chain2 = Chain::new(600f64) + .divide_by_sr() + .periodic_sum(pi2, 0.0) + .sin(); + let added = chain1 + chain2; + let ready = added * Chain::new(0.5f64); + let to_dev = ready.to_audio_device(); + + let mut synth = Synthesizer::new_audio_defaults(); + let _handle = { + let mut batch = synth.batch(); + batch.mount(to_dev) + }; + + std::thread::sleep(std::time::Duration::from_secs(5)); +} diff --git a/crates/synthizer/src/chain.rs b/crates/synthizer/src/chain.rs index cb8a1c1..93ce6a4 100644 --- a/crates/synthizer/src/chain.rs +++ b/crates/synthizer/src/chain.rs @@ -1,3 +1,4 @@ +use crate::config; use crate::core_traits::*; use crate::signals as sigs; @@ -43,10 +44,97 @@ impl Chain { /// Start a chain. /// /// `initial` can be one of a few things. The two most common are another chain or a constant. - pub fn new(initial: S) -> Self { - Self { inner: initial } + pub fn new(initial: S) -> Chain { + Chain { inner: initial } } + /// Send this chain to the audio device. + pub fn to_audio_device( + self, + ) -> Chain< + impl IntoSignal< + Signal = impl Signal< + Input = IntoSignalInput, + Output = (), + Parameters = IntoSignalParameters, + State = IntoSignalState, + >, + >, + > + where + S::Signal: Signal, + { + Chain { + inner: sigs::AudioOutputSignalConfig::new(self.inner), + } + } + + /// Convert this chain's input type to another type, capping the chain with a signal that will use the `Default` + /// implementation on whatever input type is currently wanted. + /// + /// This annoying function exists because Rust does not have specialization. What we want to be able to do is to + /// combine signals which don't have inputs with signals that do when performing mathematical operations. Ideally, + /// we would specialize the mathematical traits. Unfortunately we cannot do that. The primary use of this method + /// is essentially to say "okay, I know the other side has some bigger input, but this side doesn't need any input, I + /// promise". + /// + /// That is not the only use: sometimes you do legitimately want to feed a signal zeros or some other default value. + pub fn discard_and_default( + self, + ) -> Chain< + impl IntoSignal< + Signal = impl Signal< + Input = NewInputType, + Output = IntoSignalOutput, + State = IntoSignalState, + Parameters = IntoSignalParameters, + >, + >, + > + where + IntoSignalInput: Default, + { + Chain { + inner: sigs::ConsumeInputSignalConfig::<_, NewInputType>::new(self.inner), + } + } + + /// Divide this chain's output by the sample rate of the library. + /// + /// This is mostly used to convert a frequency (HZ) to an increment per sample, e.g. when building sine waves. + pub fn divide_by_sr( + self, + ) -> Chain, Output = f64>>> + where + S::Signal: Signal, + IntoSignalInput: Default, + { + let converted = self.output_into::(); + let sr = Chain::new(config::SR as f64).discard_and_default::>(); + let done = converted / sr; + Chain { inner: done.inner } + } + + /// Convert the output of this chain into a different type. + pub fn output_into( + self, + ) -> Chain< + impl IntoSignal< + Signal = impl Signal< + Input = IntoSignalInput, + Output = T, + State = IntoSignalState, + Parameters = IntoSignalParameters, + >, + >, + > + where + T: From>, + { + Chain { + inner: sigs::ConvertOutputConfig::::new(self.inner), + } + } /// Push a periodic summation onto this chain. /// /// The input will be taken from whatever signal is here already, and the period is specified herer as a constant. diff --git a/crates/synthizer/src/chain_mathops.rs b/crates/synthizer/src/chain_mathops.rs index 57f3b34..3993d9d 100644 --- a/crates/synthizer/src/chain_mathops.rs +++ b/crates/synthizer/src/chain_mathops.rs @@ -4,21 +4,79 @@ use std::ops::*; use crate::chain::Chain; use crate::core_traits::*; -pub struct AddSig(L, R); -pub struct AddSigConfig(L, R); - -impl Add> for Chain -where - A: IntoSignal, - B: IntoSignal, - A::Signal: Signal>, - IntoSignalOutput: Add>, -{ - type Output = Chain>; - - fn add(self, rhs: Chain) -> Self::Output { - Chain { - inner: AddSigConfig(self.inner, rhs.inner), +macro_rules! impl_mathop { + ($trait: ident, $signal_name: ident, $signal_config:ident, $method: ident) => { + pub struct $signal_name(L, R); + pub struct $signal_config(L, R); + + impl $trait> for Chain + where + A: IntoSignal, + B: IntoSignal, + A::Signal: Signal>, + IntoSignalOutput: $trait>, + { + type Output = Chain<$signal_config>; + + fn $method(self, rhs: Chain) -> Self::Output { + Chain { + inner: $signal_config(self.inner, rhs.inner), + } + } + } + + unsafe impl Signal for $signal_name + where + S1: Signal, + S2: Signal>, + SignalSealedOutput: $trait>, + { + type Input = SignalSealedInput; + type Output = as $trait>>::Output; + type Parameters = (SignalSealedParameters, SignalSealedParameters); + type State = (SignalSealedState, SignalSealedState); + + fn tick1>( + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + input: &'_ Self::Input, + destination: D, + ) { + // The complexity here is that we cannot project the context twice. We need the left value first. + let mut left = None; + S1::tick1(&mut ctx.wrap(|s| &mut s.0, |p| &p.0), input, |y| { + left = Some(y) + }); + S2::tick1(&mut ctx.wrap(|s| &mut s.1, |p| &p.1), input, |y| { + destination.send(left.unwrap().$method(y)); + }) + } } - } + + impl IntoSignal for $signal_config + where + S1: IntoSignal, + S2: IntoSignal, + $signal_name: Signal, + { + type Signal = $signal_name; + + fn into_signal(self) -> crate::Result { + let l = self.0.into_signal()?; + let r = self.1.into_signal()?; + Ok($signal_name(l, r)) + } + } + }; } + +impl_mathop!(Add, AddSig, AddSigConfig, add); +impl_mathop!(Sub, SubSig, SubSigConfig, sub); +impl_mathop!(Mul, MulSig, MulSigConfig, mul); +impl_mathop!(Div, DivSig, DivSigConfig, div); +impl_mathop!(BitAnd, BitAndSig, BitAndSigConfig, bitand); +impl_mathop!(BitOr, BitOrSig, BitOrSigConfig, bitor); +impl_mathop!(BitXor, BitXorSig, BitXorSigConfig, bitxor); + +impl_mathop!(Rem, RemSig, RemSigConfig, rem); +impl_mathop!(Shl, ShlSig, ShlSigConfig, shl); +impl_mathop!(Shr, ShrSig, ShrSigConfig, shr); diff --git a/crates/synthizer/src/core_traits.rs b/crates/synthizer/src/core_traits.rs index 90f2a60..22c6fcc 100644 --- a/crates/synthizer/src/core_traits.rs +++ b/crates/synthizer/src/core_traits.rs @@ -1,88 +1,102 @@ +use crate::config; use crate::error::Result; -/// A signal. -/// -/// This is a lot like an iterator, but instead of pulling values out, values are pushed along instead. This allows for -/// "forking" in the middle, e.g. for sidechains and bypasses. -/// -/// All signals have: -/// -/// - A state, which will be materialized on the audio thread. -/// - Some parameters, which are read-only on the audio thread (but interior mutability is allowed). -/// - An input type, which is what the signal processes. -/// - An output type, which is what the signal produces. -/// -/// Users external to this crate are not expected to implement this trait directly, and in fact doing so is impossible -/// because many of the arguments have private fields. Instead, you should build signals up from the smaller pieces -/// this crate provides. -/// -/// Note: this trait is primarily implemented on zero-sized types which describe a control flow graph. The entrypoint -/// to the crate is `SignalBuilder`, which builds signals using types that also contain settings before converting them -/// to their equivalents and pre-allocating various pieces of machinery. Note that being ZST is *not* guaranteed. -pub trait Signal: Sized { - type Input: Sized; - type Output: Sized; - type State: Sized; - type Parameters: Sized; - - /// Tick this signal once. - fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, - input: &'_ Self::Input, - destination: D, - ); -} +pub(crate) mod sealed { + use super::*; -pub trait SignalDestination { - fn send(&mut self, value: Input); + /// This internal trait is the actual magic. + /// + /// # Safety + /// + /// This trait is unsafe because the library relies on it to uphold the contracts documented with the method. In + /// particular, calling `tick1` must always send exactly one value to the destination, as the destination may be + /// writing into uninitialized memory. This lets us get performance out, especially in debug builds where things + /// like immediate unwrapping of options will not be optimized away. + pub unsafe trait Signal: Sized + Send + Sync { + type Input: Sized; + type Output: Sized; + type State: Sized + Send + Sync; + type Parameters: Sized + Send + Sync; + + /// Tick this signal once. + /// + /// Must use the destination to send exactly one value. + fn tick1>( + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + input: &'_ Self::Input, + destination: D, + ); + } + + pub trait SignalDestination { + fn send(self, value: Input); + } } +pub(crate) use sealed::*; + impl SignalDestination for F where Input: Sized, - - F: FnMut(Input), + F: FnOnce(Input), { - fn send(&mut self, value: Input) { - (*self)(value) + fn send(self, value: Input) { + self(value) } } -pub struct SignalExecutionContext<'a, TState, TParameters> { +pub struct SignalExecutionContext<'a, 'shared, TState, TParameters> { pub(crate) state: &'a mut TState, pub(crate) parameters: &'a TParameters, - /// Time in samples from some epoch. + pub(crate) fixed: &'a mut FixedSignalExecutionContext<'shared>, +} + +/// Parts of the execution context which do not contain references that need to be recast. +pub(crate) struct FixedSignalExecutionContext<'a> { pub(crate) time: u64, + pub(crate) audio_destinationh: &'a mut [f64; config::BLOCK_SIZE], } -impl SignalExecutionContext<'_, TState, TParameters> { +impl<'shared, TState, TParameters> SignalExecutionContext<'_, 'shared, TState, TParameters> { /// Convert this context into values usually derived from reborrows of this context's fields. Used to grab parts of /// contexts when moving upstream. pub(crate) fn wrap<'a, NewS, NewP>( &'a mut self, new_state: impl FnOnce(&'a mut TState) -> &'a mut NewS, new_params: impl FnOnce(&'a TParameters) -> &'a NewP, - ) -> SignalExecutionContext<'a, NewS, NewP> { + ) -> SignalExecutionContext<'a, 'shared, NewS, NewP> + where + 'shared: 'a, + { SignalExecutionContext { state: new_state(self.state), parameters: new_params(self.parameters), - time: self.time, + fixed: self.fixed, } } } -/// A helper trait to write bounds over signals which do not have inputs. -/// -/// These are special, because they can be mounted into the audio thread. This generally means that the input is coming -/// from a signal which knows how to produce values "out of thin air", either by performing mathematics or by getting -/// data through some other mechanism. pub trait Generator: Signal {} impl Generator for T where T: Signal {} -/// A mountable signal has no inputs and no outputs. -pub trait Mountable: Generator + Signal {} -impl Mountable for T where T: Generator + Signal {} +/// A mountable signal has no inputs and no outputs, and its state and parameters are 'static. +pub trait Mountable +where + Self: Generator + Send + Sync + 'static, + Self: Signal + Generator, + SignalSealedState: Send + Sync + 'static, + SignalSealedParameters: Send + Sync + 'static, +{ +} + +impl Mountable for T +where + T: Generator + Signal + Send + Sync + 'static, + SignalSealedState: Send + Sync + 'static, + SignalSealedParameters: Send + Sync + 'static, +{ +} /// Something which knows how to convert itself into a signal. /// @@ -95,7 +109,13 @@ pub trait IntoSignal { fn into_signal(self) -> Result; } -/// Workaround for https://github.com/rust-lang/rust/issues/38078: rustc is not always able to determine when a type -/// isn't ambiguous, or at the very least it doesn't tell us what the options are, so we use this instead. -#[allow(type_alias_bounds)] -pub type IntoSignalOutput = ::Output; +// Workarounds for https://github.com/rust-lang/rust/issues/38078: rustc is not always able to determine when a type +// isn't ambiguous, or at the very least it doesn't tell us what the options are, so we use this instead. +pub(crate) type IntoSignalOutput = <::Signal as Signal>::Output; +pub(crate) type IntoSignalInput = <::Signal as Signal>::Input; +pub(crate) type IntoSignalParameters = <::Signal as Signal>::Parameters; +pub(crate) type IntoSignalState = <::Signal as Signal>::State; +pub(crate) type SignalSealedInput = ::Input; +pub(crate) type SignalSealedOutput = ::Output; +pub(crate) type SignalSealedState = ::State; +pub(crate) type SignalSealedParameters = ::Parameters; diff --git a/crates/synthizer/src/lib.rs b/crates/synthizer/src/lib.rs index 5133278..3bf9165 100644 --- a/crates/synthizer/src/lib.rs +++ b/crates/synthizer/src/lib.rs @@ -22,14 +22,18 @@ mod error; pub mod fast_xoroshiro; mod is_audio_thread; mod loop_spec; +mod mount_point; mod option_recycler; pub mod sample_sources; pub mod signals; +pub mod synthesizer; mod unique_id; mod worker_pool; +pub use chain::*; pub use channel_format::*; pub use config::SR; pub use db::DbExt; pub use error::{Error, Result}; pub use loop_spec::*; +pub use synthesizer::Synthesizer; diff --git a/crates/synthizer/src/mount_point.rs b/crates/synthizer/src/mount_point.rs new file mode 100644 index 0000000..69cc143 --- /dev/null +++ b/crates/synthizer/src/mount_point.rs @@ -0,0 +1,17 @@ +use crate::core_traits::*; +use crate::synthesizer::SynthesizerState; +use std::sync::Arc; + +pub(crate) struct MountPoint +where + S::State: Send + Sync + 'static, + S::Parameters: Send + Sync + 'static, +{ + state: S::State, + signal: S, + time: u64, +} + +pub(crate) trait ErasedMountPoint: Send + Sync + 'static { + fn run(&mut self, state: &Arc); +} diff --git a/crates/synthizer/src/signals/and_then.rs b/crates/synthizer/src/signals/and_then.rs index 370ed92..3cdab3a 100644 --- a/crates/synthizer/src/signals/and_then.rs +++ b/crates/synthizer/src/signals/and_then.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData as PD; - use crate::core_traits::*; /// Takes the signal on the left, and feeds its output to the signal on the right. The signal on the left will be @@ -8,9 +6,9 @@ use crate::core_traits::*; /// This allows "filling holes". For example, one might map a set of signals into a struct for later use, then use /// `and_then` to pass it to a signal expecting that struct. This is what allows chains to embed other chains in them, /// and to have recursion. In other words, higher level helpers use this as a building block. -pub struct AndThen(PD<*mut (S1, S2)>); +pub struct AndThen(S1, S2); -impl Signal for AndThen +unsafe impl Signal for AndThen where S1: Signal, S2: Signal, @@ -21,7 +19,7 @@ where type Parameters = (S1::Parameters, S2::Parameters); fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, input: &'_ Self::Input, destination: D, ) { @@ -51,14 +49,14 @@ where type Signal = AndThen; fn into_signal(self) -> crate::Result { - let _s1 = self.left.into_signal()?; - let _s2 = self.right.into_signal()?; - Ok(AndThen::new()) + let s1 = self.left.into_signal()?; + let s2 = self.right.into_signal()?; + Ok(AndThen::new(s1, s2)) } } impl AndThen { - pub(crate) fn new() -> Self { - AndThen(PD) + pub(crate) fn new(s1: S1, s2: S2) -> Self { + AndThen(s1, s2) } } diff --git a/crates/synthizer/src/signals/audio_io.rs b/crates/synthizer/src/signals/audio_io.rs new file mode 100644 index 0000000..dc7d835 --- /dev/null +++ b/crates/synthizer/src/signals/audio_io.rs @@ -0,0 +1,46 @@ +use crate::core_traits::*; + +pub struct AudioOutputSignal(S); +pub struct AudioOutputSignalConfig(S); + +unsafe impl Signal for AudioOutputSignal +where + S: Signal, +{ + type Output = (); + type Input = S::Input; + type State = S::State; + type Parameters = S::Parameters; + + fn tick1>( + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + input: &'_ Self::Input, + destination: D, + ) { + let mut val: Option = None; + S::tick1(ctx, input, |x| val = Some(x)); + + // We output the unit type instead. + destination.send(()); + + // TODO: this is actually going to the audio output buffer. + } +} + +impl IntoSignal for AudioOutputSignalConfig +where + S: IntoSignal, + S::Signal: Signal, +{ + type Signal = AudioOutputSignal; + + fn into_signal(self) -> crate::Result { + Ok(AudioOutputSignal(self.0.into_signal()?)) + } +} + +impl AudioOutputSignalConfig { + pub(crate) fn new(signal: S) -> Self { + Self(signal) + } +} diff --git a/crates/synthizer/src/signals/clock.rs b/crates/synthizer/src/signals/clock.rs index 914214a..e1f31f3 100644 --- a/crates/synthizer/src/signals/clock.rs +++ b/crates/synthizer/src/signals/clock.rs @@ -3,17 +3,17 @@ use crate::core_traits::*; pub struct Clock(()); -impl Signal for Clock { +unsafe impl Signal for Clock { type Input = (); type Output = u64; type State = (); type Parameters = (); fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, _input: &'_ Self::Input, - mut destination: D, + destination: D, ) { - destination.send(ctx.time); + destination.send(ctx.fixed.time); } } diff --git a/crates/synthizer/src/signals/consume_input.rs b/crates/synthizer/src/signals/consume_input.rs new file mode 100644 index 0000000..b2f6e2f --- /dev/null +++ b/crates/synthizer/src/signals/consume_input.rs @@ -0,0 +1,60 @@ +use std::marker::PhantomData as PD; + +use crate::core_traits::*; + +/// Consume the input of this signal. Then replace it with the `Default::default()` value of a new input type. +/// +/// This is basically a no-op signal. It is useful to get from signals which take `()` as input to signals which take +/// some other type of input so that they may be lifted up and joined into other signals using mathematical operators. +/// Because Rust does not have specialization, we cannot write a version of the mathematical traits which understands +/// that signals whose input are `()` may take any input, and concrete casting is sadly required. +pub(crate) struct ConsumeInputSignal( + Wrapped, + PD, +); + +pub(crate) struct ConsumeInputSignalConfig( + Wrapped, + PD, +); + +unsafe impl Send for ConsumeInputSignal where S: Send {} +unsafe impl Sync for ConsumeInputSignal where S: Sync {} + +unsafe impl Signal for ConsumeInputSignal +where + S: Signal, + S::Input: Default, +{ + type Input = I; + type Output = S::Output; + type State = S::State; + type Parameters = S::Parameters; + + fn tick1>( + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + _input: &'_ Self::Input, + destination: D, + ) { + let new_in: S::Input = Default::default(); + S::tick1(ctx, &new_in, destination); + } +} + +impl IntoSignal for ConsumeInputSignalConfig +where + S: IntoSignal, + IntoSignalInput: Default, +{ + type Signal = ConsumeInputSignal; + + fn into_signal(self) -> crate::Result { + Ok(ConsumeInputSignal(self.0.into_signal()?, PD)) + } +} + +impl ConsumeInputSignalConfig { + pub(crate) fn new(wrapped: S) -> Self { + Self(wrapped, PD) + } +} diff --git a/crates/synthizer/src/signals/conversion.rs b/crates/synthizer/src/signals/conversion.rs index ecc2810..c27b98c 100644 --- a/crates/synthizer/src/signals/conversion.rs +++ b/crates/synthizer/src/signals/conversion.rs @@ -6,15 +6,22 @@ use std::marker::PhantomData as PD; use crate::core_traits::*; /// Converts the output of the upstream signal into the input of the downstream signal, if `O::Output: Into`. -pub struct ConvertOutput(PD<*mut OSig>, PD<*mut DType>); +pub struct ConvertOutput(OSig, PD); +pub struct ConvertOutputConfig(OSig, PD); impl ConvertOutput { - pub(crate) fn new() -> Self { - Self(PD, PD) + pub(crate) fn new(sig: Sig) -> Self { + Self(sig, PD) } } -impl Signal for ConvertOutput +impl ConvertOutputConfig { + pub(crate) fn new(sig: Sig) -> Self { + Self(sig, PD) + } +} + +unsafe impl Signal for ConvertOutput where Sig: Signal, Sig::Output: Into, @@ -25,9 +32,9 @@ where type State = Sig::State; fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, input: &'_ Self::Input, - mut destination: D, + destination: D, ) { Sig::tick1(ctx, input, |x: Sig::Output| { let y: DType = x.into(); @@ -36,14 +43,19 @@ where } } -impl IntoSignal for ConvertOutput +// DType is not part of the signal. It is only a record of the signal's output type. Consequently these are actually +// correct. +unsafe impl Send for ConvertOutput {} +unsafe impl Sync for ConvertOutput {} + +impl IntoSignal for ConvertOutputConfig where - Sig: Signal, - Sig::Output: Into, + Sig: IntoSignal, + IntoSignalOutput: Into, { - type Signal = Self; + type Signal = ConvertOutput; fn into_signal(self) -> crate::Result { - Ok(ConvertOutput::new()) + Ok(ConvertOutput::new(self.0.into_signal()?)) } } diff --git a/crates/synthizer/src/signals/mod.rs b/crates/synthizer/src/signals/mod.rs index 1c8c429..185de62 100644 --- a/crates/synthizer/src/signals/mod.rs +++ b/crates/synthizer/src/signals/mod.rs @@ -1,5 +1,7 @@ mod and_then; +mod audio_io; mod clock; +mod consume_input; mod conversion; mod null; mod periodic_f64; @@ -7,7 +9,9 @@ mod scalars; mod trig; pub use and_then::*; +pub use audio_io::*; pub use clock::*; +pub(crate) use consume_input::*; pub use conversion::*; pub use null::*; pub use periodic_f64::*; diff --git a/crates/synthizer/src/signals/null.rs b/crates/synthizer/src/signals/null.rs index c796878..3443ab3 100644 --- a/crates/synthizer/src/signals/null.rs +++ b/crates/synthizer/src/signals/null.rs @@ -6,16 +6,16 @@ use crate::core_traits::*; /// at all. pub struct NullSignal(()); -impl Signal for NullSignal { +unsafe impl Signal for NullSignal { type Input = (); type Output = (); type State = (); type Parameters = (); fn tick1>( - _ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + _ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, _input: &'_ Self::Input, - mut destination: D, + destination: D, ) { destination.send(()); } diff --git a/crates/synthizer/src/signals/periodic_f64.rs b/crates/synthizer/src/signals/periodic_f64.rs index d0d9047..9fc8c22 100644 --- a/crates/synthizer/src/signals/periodic_f64.rs +++ b/crates/synthizer/src/signals/periodic_f64.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData as PD; - use crate::core_traits::*; /// A signal which produces an f64 value in the range (0..period) by summing the value of an input signal. e.g. @@ -13,7 +11,7 @@ pub struct PeriodicF64Config { pub(crate) initial_value: f64, } -pub struct PeriodicF64Signal(PD<*mut SIncr>); +pub struct PeriodicF64Signal(SIncr); pub struct PeriodicF64State { freq_state: SIncr::State, @@ -25,7 +23,7 @@ pub struct PeriodicF64Parameters { freq_params: SIncr::Parameters, } -impl Signal for PeriodicF64Signal +unsafe impl Signal for PeriodicF64Signal where SIncr: Signal, { @@ -35,9 +33,9 @@ where type Parameters = PeriodicF64Parameters; fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, input: &'_ Self::Input, - mut destination: D, + destination: D, ) { let period = ctx.parameters.period; let mut val = ctx.state.cur_val; @@ -66,7 +64,7 @@ where type Signal = PeriodicF64Signal; fn into_signal(self) -> crate::Result { - let _wrapped = self.frequency.into_signal()?; - Ok(PeriodicF64Signal(PD)) + let wrapped = self.frequency.into_signal()?; + Ok(PeriodicF64Signal(wrapped)) } } diff --git a/crates/synthizer/src/signals/scalars.rs b/crates/synthizer/src/signals/scalars.rs index 4a1215f..35d7993 100644 --- a/crates/synthizer/src/signals/scalars.rs +++ b/crates/synthizer/src/signals/scalars.rs @@ -2,30 +2,28 @@ use crate::core_traits::*; use crate::error::Result; -struct ConstConfig(T); - macro_rules! impl_scalar { ($t: ty) => { - impl Signal for $t { + unsafe impl Signal for $t { type Input = (); type Output = $t; type State = (); type Parameters = $t; fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, _input: &'_ Self::Input, - mut destination: D, + destination: D, ) { destination.send(*ctx.parameters); } } - impl IntoSignal for ConstConfig<$t> { + impl IntoSignal for $t { type Signal = $t; fn into_signal(self) -> Result { - Ok(self.0) + Ok(self) } } }; diff --git a/crates/synthizer/src/signals/trig.rs b/crates/synthizer/src/signals/trig.rs index 903a69c..581d2fd 100644 --- a/crates/synthizer/src/signals/trig.rs +++ b/crates/synthizer/src/signals/trig.rs @@ -1,14 +1,12 @@ -use std::marker::PhantomData as PD; - use crate::core_traits::*; pub struct SinSignalConfig { pub(crate) wrapped: S, } -pub struct SinSignal(PD<*mut S>); +pub struct SinSignal(S); -impl Signal for SinSignal +unsafe impl Signal for SinSignal where S: Signal, { @@ -18,9 +16,9 @@ where type Parameters = S::Parameters; fn tick1>( - ctx: &mut SignalExecutionContext<'_, Self::State, Self::Parameters>, + ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, input: &'_ Self::Input, - mut destination: D, + destination: D, ) { S::tick1(ctx, input, |x: f64| destination.send(x.sin())); } @@ -34,7 +32,7 @@ where type Signal = SinSignal; fn into_signal(self) -> crate::Result { - let _wrapped = self.wrapped.into_signal()?; - Ok(SinSignal(PD)) + let wrapped = self.wrapped.into_signal()?; + Ok(SinSignal(wrapped)) } } diff --git a/crates/synthizer/src/synthesizer.rs b/crates/synthizer/src/synthesizer.rs new file mode 100644 index 0000000..eb54833 --- /dev/null +++ b/crates/synthizer/src/synthesizer.rs @@ -0,0 +1,241 @@ +use std::marker::PhantomData as PD; +use std::sync::Arc; + +use arc_swap::{ArcSwap, ArcSwapOption}; +use atomic_refcell::AtomicRefCell; +use rpds::{HashTrieMapSync, VectorSync}; + +use crate::chain::Chain; +use crate::config; +use crate::core_traits::*; +use crate::mount_point::ErasedMountPoint; +use crate::unique_id::UniqueId; + +type SynthMap = HashTrieMapSync; +type SynthVec = VectorSync; + +/// TODO: this is actually private-ish, but we're getting off the ground. +pub struct Synthesizer { + published_state: Arc>, +} + +#[derive(Clone)] +struct MountContainer { + pending_drop: Arc, + slots: SynthMap>>>, + erased_mount: Arc>>, +} + +/// This is the state published to the audio thread. +/// +/// Here's how this works: the state is behind `Arc`. To manipulate it, we `make_mut` that `Arc`, do whatever we're +/// doing, then publish via `ArcSwap`. This is "cheap" to clone in the sense that it's using persistent data +/// structures, but the way things actually work is we have batches and each batch will modify only one copy until the +/// batch ends. On the audio thread, interior mutability is then used to modify the states. +/// +/// That's a lot of pointer chasing. To deal with that, mounts materialize once per block and run the block. +/// +/// To make sure deallocation never happens on the audio thread, we maintain a linked list of these. When a new state +/// is seen by the audio thread, the old state will have been swapped into place. Then, when a batch starts, we drop +/// that linked list if needed. As a result, the final Arc never goes away on the audio thread. There is a kind of +/// trick however: this can indeed be a cycle. We rely on the next batch creation to clear that cycle. +#[derive(Clone)] +pub(crate) struct SynthesizerState { + older_state: Arc>, + + /// The value is an Arc to an interior-mutable erased box. + mounts: SynthMap, + + audio_thred_state: Arc>, +} + +/// Ephemeral state for the audio thread itself. Owned by the audio thread but behind AtomicRefCell to avoid unsafe +/// code. +pub(crate) struct AudioThreadState { + /// Intermediate mono buffer before going to miniaudio. + /// + /// This will be more complex a bit later on. At the moment, it's more "get us off the ground" stuff. + pub(crate) buffer: [f64; config::BLOCK_SIZE], + + pub(crate) buf_remaining: usize, + + /// time in blocks since this state was created. + pub(crate) time_in_blocks: u64, +} + +/// Internal helper type: flips a boolean to true when dropped. +/// +/// This allows dropping outside the batch context. The object(s) are marked dropped and then, on the next batch, they +/// are actually removed. Eventually they drop for real, as states rotate out of the audio thread. +struct MarkDropped(Arc); + +impl MarkDropped { + pub(crate) fn new() -> Self { + MarkDropped(Arc::new(std::sync::atomic::AtomicBool::new(false))) + } +} + +impl Drop for MarkDropped { + fn drop(&mut self) { + self.0.store(true, std::sync::atomic::Ordering::Relaxed); + } +} + +/// A handle which may be used to manipulate some object of type `T`. +/// +/// Handles keep objects alive. When the last handle drops, the object does as well. +/// +/// Handles may alias the same object. The `T` here is allowing you to manipulate some parameter of your signal in some +/// fashion, but signals may have different parameters and "knobs" all of which may have a different handle (TODO: +/// implement `Slot` stuff). +pub struct Handle { + object_id: UniqueId, + mark_drop: Arc, + _phantom: PD, +} + +impl Clone for Handle { + fn clone(&self) -> Self { + Self { + object_id: self.object_id, + mark_drop: self.mark_drop.clone(), + _phantom: PD, + } + } +} + +/// A marker type used as a type parameters to handles which are for an entire mount point. +pub struct MountPointHandleMarker; + +/// A batch of changes for the audio thread. +/// +/// You ask for a batch. Then you manipulate things by asking the batch for their parameters. The states all build up, +/// and are stored with the batch. Then, when the batch drops, they are published to the audio thread +pub struct Batch<'a> { + synthesizer: &'a mut Synthesizer, + new_state: SynthesizerState, +} + +impl Drop for Batch<'_> { + fn drop(&mut self) { + self.handle_pending_drops(); + self.synthesizer + .published_state + .store(Arc::new(self.new_state.clone())); + } +} + +impl Synthesizer { + pub fn new_audio_defaults() -> Self { + Self { + published_state: Arc::new(ArcSwap::new(Arc::new(SynthesizerState::new()))), + } + } + + pub fn batch(&mut self) -> Batch<'_> { + let new_state = Arc::unwrap_or_clone(self.published_state.load_full()); + + let mut ret = Batch { + synthesizer: self, + new_state, + }; + + // Clear the state out. + ret.new_state.older_state = Arc::new(ArcSwapOption::new(None)); + ret.handle_pending_drops(); + + ret + } +} + +impl SynthesizerState { + fn new() -> Self { + Self { + audio_thred_state: Arc::new(AtomicRefCell::new(AudioThreadState { + buf_remaining: 0, + buffer: [0.0f64; config::BLOCK_SIZE], + time_in_blocks: 0, + })), + mounts: SynthMap::new_sync(), + older_state: Arc::new(ArcSwapOption::new(None)), + } + } +} +impl Batch<'_> { + /// Called on batch creation to catch pending drops from last time, then again on batch publish to catch pending + /// drops the user might have made during the batch. + fn handle_pending_drops(&mut self) { + // no retain in rpds. + let mut pending_dropkeys = smallvec::SmallVec::<[UniqueId; 16]>::new(); + + for (id, m) in self.new_state.mounts.iter() { + if m.pending_drop.load(std::sync::atomic::Ordering::Relaxed) { + pending_dropkeys.push(*id); + } + } + + for id in pending_dropkeys { + self.new_state.mounts.remove_mut(&id); + } + } + + pub fn mount(&mut self, _chain: Chain) -> Handle + where + S::Signal: Mountable, + SignalSealedState: Send + Sync + 'static, + SignalSealedParameters: Send + Sync + 'static, + { + let object_id = UniqueId::new(); + let mark_drop = Arc::new(MarkDropped::new()); + Handle { + object_id, + mark_drop, + _phantom: PD, + } + } +} + +/// Run one iteration of the audio thread. +fn at_iter(state: Arc, mut dest: &mut [f64]) { + while !dest.is_empty() { + // Grab the audio thread state and copy out whatever data we can. + { + let mut state = state.audio_thred_state.borrow_mut(); + if state.buf_remaining > 0 { + let remaining = dest.len(); + let will_do = state.buf_remaining.min(remaining); + assert!(will_do > 0); + + let start_ind = state.buffer.len() - state.buf_remaining; + let grabbing = &mut state.buffer[start_ind..(start_ind + will_do)]; + (dest[..will_do]).copy_from_slice(grabbing); + dest = &mut dest[will_do..]; + + if dest.is_empty() { + // This was enough, and we do not need to refill the buffer. + return; + } + } + } + + // Prepare the audio thread state, then release the borrow, allowing mount points to grab it. + { + let mut at_state = state.audio_thred_state.borrow_mut(); + + at_state.buf_remaining = config::BLOCK_SIZE; + // Zero it out for this iteration. + at_state.buffer.fill(0.0f64); + } + + // Mounts may fill the audio buffer. + for (_, m) in state.mounts.iter() { + if m.pending_drop.load(std::sync::atomic::Ordering::Relaxed) { + continue; + } + + m.erased_mount.borrow_mut().run(&state); + } + + state.audio_thred_state.borrow_mut().time_in_blocks += 1; + } +}