diff --git a/crates/synthizer/examples/two_sine_waves.rs b/crates/synthizer/examples/two_sine_waves.rs index 0e9a9d7..fcdf4e4 100644 --- a/crates/synthizer/examples/two_sine_waves.rs +++ b/crates/synthizer/examples/two_sine_waves.rs @@ -1,26 +1,61 @@ +use anyhow::Result; use synthizer::*; -fn main() { +const C_FREQ: f64 = 261.63; +const E_FREQ: f64 = 329.63; +const G_FREQ: f64 = 392.00; + +fn main() -> Result<()> { + let mut synth = Synthesizer::new_default_output().unwrap(); + let pi2 = 2.0f64 * std::f64::consts::PI; - let chain1 = Chain::new(500f64) + + let freq1; + let freq2; + let freq3; + + { + let mut b = synth.batch(); + freq1 = b.allocate_slot::(); + freq2 = b.allocate_slot::(); + freq3 = b.allocate_slot::(); + } + + let note1 = read_slot(&freq1, C_FREQ) .divide_by_sr() .periodic_sum(1.0f64, 0.0f64) .inline_mul(Chain::new(pi2)) .sin(); - let chain2 = Chain::new(600f64) + let note2 = read_slot(&freq2, E_FREQ) + .divide_by_sr() + .periodic_sum(1.0f64, 0.0) + .inline_mul(Chain::new(pi2)) + .sin(); + let note3 = read_slot(&freq3, G_FREQ) .divide_by_sr() .periodic_sum(1.0f64, 0.0) .inline_mul(Chain::new(pi2)) .sin(); - let added = chain1 + chain2; + + let added = note1 + note2 + note3; let ready = added * Chain::new(0.1f64); let to_dev = ready.to_audio_device(); - let mut synth = Synthesizer::new_default_output().unwrap(); - let _handle = { + let handle = { let mut batch = synth.batch(); - batch.mount(to_dev) + batch.mount(to_dev)? }; - std::thread::sleep(std::time::Duration::from_secs(5)); + std::thread::sleep(std::time::Duration::from_secs(1)); + + { + let mut batch = synth.batch(); + batch.replace_slot_value(&handle, &freq1, C_FREQ * 2.0)?; + batch.replace_slot_value(&handle, &freq2, E_FREQ * 2.0)?; + batch.replace_slot_value(&handle, &freq3, G_FREQ * 2.0)?; + } + + std::thread::sleep(std::time::Duration::from_secs(1)); + + Ok(()) } diff --git a/crates/synthizer/src/chain.rs b/crates/synthizer/src/chain.rs index cf5400f..347fbdf 100644 --- a/crates/synthizer/src/chain.rs +++ b/crates/synthizer/src/chain.rs @@ -40,6 +40,34 @@ where } } +/// Start a chain which reads from a slot. +pub fn read_slot( + slot: &sigs::Slot, + initial_value: T, +) -> Chain>> +where + T: Clone + Send + Sync + 'static, +{ + Chain { + inner: slot.read_signal(initial_value), + } +} + +/// Start a chain which reads from a slot, then includes whether or not the slot changed this block. +/// +/// Returns `(T, bool)`. +pub fn read_slot_and_changed( + slot: &sigs::Slot, + initial_value: T, +) -> Chain>> +where + T: Send + Sync + Clone + 'static, +{ + Chain { + inner: slot.read_signal_and_change_flag(initial_value), + } +} + impl Chain { /// Start a chain. /// diff --git a/crates/synthizer/src/signals/map.rs b/crates/synthizer/src/signals/map.rs new file mode 100644 index 0000000..b25579a --- /dev/null +++ b/crates/synthizer/src/signals/map.rs @@ -0,0 +1,126 @@ +use std::marker::PhantomData as PD; +use std::mem::MaybeUninit; + +use crate::config; +use crate::core_traits::*; + +pub struct MapSignal(PD<*const (ParSig, F, O)>); +unsafe impl Send for MapSignal {} +unsafe impl Sync for MapSignal {} + +pub struct MapSignalConfig { + parent: ParSigCfg, + closure: F, + _phantom: PD, +} + +pub struct MapSignalState { + closure: F, + + parent_state: SignalState, +} + +unsafe impl Signal for MapSignal +where + ParSig: Signal, + F: FnMut(&SignalOutput) -> O + Send + Sync + 'static, + O: Send, +{ + type Input = SignalInput; + type Output = O; + type Parameters = ParSig::Parameters; + type State = MapSignalState; + + fn on_block_start( + ctx: &mut crate::context::SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + ) { + ParSig::on_block_start(&mut ctx.wrap(|s| &mut s.parent_state, |p| p)); + } + + fn tick1>( + ctx: &mut crate::context::SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + input: &'_ Self::Input, + destination: D, + ) { + let mut par_in = MaybeUninit::uninit(); + ParSig::tick1(&mut ctx.wrap(|s| &mut s.parent_state, |p| p), input, |x| { + par_in.write(x); + }); + + destination.send((ctx.state.closure)(unsafe { par_in.assume_init_ref() })); + } + + fn tick_block< + 'a, + I: FnMut(usize) -> &'a Self::Input, + D: ReusableSignalDestination, + >( + ctx: &'_ mut crate::context::SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + input: I, + mut destination: D, + ) where + Self::Input: 'a, + { + let mut outs: [MaybeUninit>; config::BLOCK_SIZE] = + [const { MaybeUninit::uninit() }; config::BLOCK_SIZE]; + let mut i = 0; + ParSig::tick_block(&mut ctx.wrap(|s| &mut s.parent_state, |p| p), input, |x| { + outs[i].write(x); + i += 1; + }); + + outs.iter().for_each(|i| { + destination.send_reusable((ctx.state.closure)(unsafe { i.assume_init_ref() })) + }); + + // The mapping closure gets references, so we must drop this ourselves. + unsafe { + crate::unsafe_utils::drop_initialized_array(outs); + } + } + + fn trace_slots< + Tracer: FnMut( + crate::unique_id::UniqueId, + std::sync::Arc, + ), + >( + state: &Self::State, + parameters: &Self::Parameters, + inserter: &mut Tracer, + ) { + ParSig::trace_slots(&state.parent_state, parameters, inserter); + } +} + +impl IntoSignal for MapSignalConfig +where + F: FnMut(&IntoSignalOutput) -> O + Send + Sync + 'static, + ParSig: IntoSignal, + O: Send + 'static, +{ + type Signal = MapSignal; + + fn into_signal(self) -> IntoSignalResult { + let par = self.parent.into_signal()?; + + Ok(ReadySignal { + parameters: par.parameters, + state: MapSignalState { + closure: self.closure, + parent_state: par.state, + }, + signal: MapSignal(PD), + }) + } +} + +impl MapSignalConfig { + pub(crate) fn new(parent: ParSig, closure: F) -> Self { + Self { + closure, + parent, + _phantom: PD, + } + } +} diff --git a/crates/synthizer/src/signals/mod.rs b/crates/synthizer/src/signals/mod.rs index 6cd4884..936cc1e 100644 --- a/crates/synthizer/src/signals/mod.rs +++ b/crates/synthizer/src/signals/mod.rs @@ -2,6 +2,7 @@ mod and_then; mod audio_io; mod consume_input; mod conversion; +mod map; mod null; mod periodic_f64; mod scalars; @@ -12,6 +13,7 @@ pub use and_then::*; pub use audio_io::*; pub(crate) use consume_input::*; pub use conversion::*; +pub use map::*; pub use null::*; pub use periodic_f64::*; pub use slots::*; diff --git a/crates/synthizer/src/signals/slots.rs b/crates/synthizer/src/signals/slots.rs new file mode 100644 index 0000000..f626dac --- /dev/null +++ b/crates/synthizer/src/signals/slots.rs @@ -0,0 +1,263 @@ +use std::any::Any; +use std::marker::PhantomData as PD; +use std::sync::Arc; + +use rpds::HashTrieMapSync; + +use crate::config; +use crate::core_traits::*; +use crate::unique_id::UniqueId; + +pub(crate) type SlotMap = HashTrieMapSync; + +/// Reference to a "knob" on your algorithm. +/// +/// In order to get information to the audio thread, you must have a way to communicate with it. Slots solve that +/// problem. You may resolve a slot against a synthesizer and a mount handle, and then replace or (in some cases) +/// mutate the value. +/// +/// Mutation is only available if `T` is Clone. This is because the library is internally using persistent data +/// structures. In some but not all cases, therefore, a clone happens first. In general this happens for the first +/// access in a batch, so it is still worthwhile to use mutation if you can. +/// +/// Slots are unique and tied to their mounts. If you mix up a mount handle and a slot for that mount, an error +/// results. You cannot use a slot without a mount handle. +/// +/// Slots are one way. You can't send data out from the audio thread. You can read the last set value but that's it. +/// Note that reading the value outside the audio thread is slow, on the order of a couple tree traversals and some +/// dynamic casting, and it may get slower in the future. All optimization effort goes toward writing values. +/// +/// Slots are signals in the same way that scalars are, and so you can then pass the slot to `Chain::new`. +/// +/// If you mix up slots on the audio thread such that a chain is mounted in a different mount than the chain from which +/// you got the slot, mounting will error. +/// +/// You get a slot from a batch. The slot outlives the batch without a problem, but do note that creation is expensive +/// if you create only one slot at a time. +pub struct Slot { + pub(crate) slot_id: UniqueId, + pub(crate) _phantom: PD, +} + +/// Internal state for a slot's value. +/// +/// These get a unique id for the update, plus the value. When mutating, the unique id is changed if needed. This can +/// then be used to intelligently detect changes on the audio thread. What actually happens is that we introduce one +/// more level of redirection. `Arc` can tell us whether or not any given value made it to the audio thread; if it +/// didn't we can avoid allocating more ids. +#[derive(Clone)] +pub(crate) struct SlotValueContainer { + value: Arc, + update_id: UniqueId, +} + +/// The audio thread state of a slot. +/// +/// This can tell you the block at which a change last happened in addition to the value. +/// +/// The signal resolves these at block start. +/// +/// This is also the state for the signal: each signal is one slot, and so can directly hold this without having to use +/// erasure. +pub struct SlotAudioThreadState { + /// Same `Arc` as the SlotValueContainer. + value: Arc, + last_update_id: UniqueId, + + /// Set when a new id is found. Cleared on the next `on_block_start` call. + changed_this_block: bool, +} + +/// Subset of the synthesizer's state needed to resolve a slot. +pub(crate) struct SlotUpdateContext<'a> { + /// Slots on this mount. + /// + /// The value is `SlotValueContainer`. + pub(crate) mount_slots: &'a SlotMap>, +} + +impl SlotUpdateContext<'_> { + fn resolve(&'_ self, id: &'_ UniqueId) -> Option<&'_ SlotValueContainer> { + self.mount_slots + .get(id)? + .downcast_ref::>() + } +} + +pub struct SlotSignalParams { + id: UniqueId, + _phantom: PD, +} + +pub struct SlotSignalConfig { + slot_id: UniqueId, + + initial_value: T, +} + +pub struct SlotSignal(PD); + +/// Output from a slot's signal. +/// +/// Every slot is reading from a fixed place in memory, but it has other metadata it wishes to hand out, primarily +/// change notifications. +#[derive(Clone)] // TODO: we can lift the Clone requirement in a bit. +pub struct SlotSignalOutput { + value: T, + changed_this_block: bool, +} + +impl SlotSignalOutput { + pub fn get_value(&self) -> &T { + &self.value + } + + /// Did the value change at the beginning of this block? + /// + /// Note that block intervals are subject to change. Primarily this is useful for crossfades or one-off triggers of + /// other logic. + pub fn changed_this_block(&self) -> bool { + self.changed_this_block + } +} + +unsafe impl Signal for SlotSignal +where + T: Clone, +{ + type Input = (); + type Output = SlotSignalOutput; + type State = SlotAudioThreadState; + type Parameters = SlotSignalParams; + + fn on_block_start( + ctx: &mut crate::context::SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + ) { + let slot_container = ctx + .fixed + .slots + .resolve::(&ctx.parameters.id) + .expect("If a slot is created, it should have previously been allocated"); + let is_change = ctx.state.last_update_id != slot_container.update_id; + + // If no change has occurred, optimize out doing anything. + if !is_change { + ctx.state.changed_this_block = false; + return; + } + + ctx.state.value = slot_container.value.clone(); + ctx.state.last_update_id = slot_container.update_id; + ctx.state.changed_this_block = true; + } + + fn tick1>( + ctx: &mut crate::context::SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + _input: &'_ Self::Input, + destination: D, + ) { + destination.send(SlotSignalOutput { + value: (*ctx.state.value).clone(), + changed_this_block: ctx.state.changed_this_block, + }); + } + + fn tick_block< + 'a, + I: FnMut(usize) -> &'a Self::Input, + D: ReusableSignalDestination, + >( + ctx: &'_ mut crate::context::SignalExecutionContext<'_, '_, Self::State, Self::Parameters>, + _input: I, + mut destination: D, + ) where + Self::Input: 'a, + { + let val = SlotSignalOutput { + value: (*ctx.state.value).clone(), + changed_this_block: ctx.state.changed_this_block, + }; + + for _ in 0..config::BLOCK_SIZE { + destination.send_reusable(val.clone()); + } + } + + fn trace_slots)>( + state: &Self::State, + parameters: &Self::Parameters, + inserter: &mut F, + ) { + let ns = SlotValueContainer { + update_id: UniqueId::new(), + value: state.value.clone(), + }; + + inserter(parameters.id, Arc::new(ns)); + } +} + +impl IntoSignal for SlotSignalConfig +where + T: 'static + Send + Sync + Clone, +{ + type Signal = SlotSignal; + + fn into_signal(self) -> IntoSignalResult { + Ok(ReadySignal { + signal: SlotSignal(PD), + parameters: SlotSignalParams { + id: self.slot_id, + _phantom: PD, + }, + state: SlotAudioThreadState { + changed_this_block: false, + last_update_id: UniqueId::new(), + value: Arc::new(self.initial_value), + }, + }) + } +} + +impl Slot +where + T: Send + Sync + Clone + 'static, +{ + /// Get a signal which will read this slot. + pub(crate) fn read_signal( + &self, + initial_value: T, + ) -> impl IntoSignal> { + crate::signals::MapSignalConfig::new( + SlotSignalConfig { + initial_value, + slot_id: self.slot_id, + }, + |x: &SlotSignalOutput| -> T { x.value.clone() }, + ) + } + + /// Get a signal which will read this slot, then tack on a boolean indicating whether the value changed this block. + pub(crate) fn read_signal_and_change_flag( + &self, + initial_value: T, + ) -> impl IntoSignal> { + crate::signals::MapSignalConfig::new( + SlotSignalConfig { + initial_value, + slot_id: self.slot_id, + }, + |x: &SlotSignalOutput| -> (T, bool) { (x.value.clone(), x.changed_this_block) }, + ) + } +} + +impl SlotValueContainer { + #[must_use = "This is an immutable type in a persistent data structure"] + pub(crate) fn replace(&self, newval: T) -> Self { + Self { + value: Arc::new(newval), + update_id: UniqueId::new(), + } + } +} diff --git a/crates/synthizer/src/signals/trig.rs b/crates/synthizer/src/signals/trig.rs index ef02324..6aa8904 100644 --- a/crates/synthizer/src/signals/trig.rs +++ b/crates/synthizer/src/signals/trig.rs @@ -24,7 +24,9 @@ where S::tick1(ctx, input, |x: f64| destination.send(x.sin())); } - fn on_block_start(_ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>) {} + fn on_block_start(ctx: &mut SignalExecutionContext<'_, '_, Self::State, Self::Parameters>) { + S::on_block_start(ctx); + } fn tick_block< 'a, diff --git a/crates/synthizer/src/synthesizer.rs b/crates/synthizer/src/synthesizer.rs index c949e91..38a5a27 100644 --- a/crates/synthizer/src/synthesizer.rs +++ b/crates/synthizer/src/synthesizer.rs @@ -9,9 +9,9 @@ use rpds::{HashTrieMapSync, VectorSync}; use crate::chain::Chain; use crate::config; use crate::core_traits::*; -use crate::error::Result; +use crate::error::{Error, Result}; use crate::mount_point::ErasedMountPoint; -use crate::signals::{SlotMap, SlotUpdateContext}; +use crate::signals::{Slot, SlotMap, SlotUpdateContext, SlotValueContainer}; use crate::unique_id::UniqueId; type SynthMap = HashTrieMapSync; @@ -95,18 +95,16 @@ impl Drop for MarkDropped { /// 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 { +pub struct Handle { object_id: UniqueId, mark_drop: Arc, - _phantom: PD, } -impl Clone for Handle { +impl Clone for Handle { fn clone(&self) -> Self { Self { object_id: self.object_id, mark_drop: self.mark_drop.clone(), - _phantom: PD, } } } @@ -185,6 +183,7 @@ impl SynthesizerState { } } } + 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. @@ -203,10 +202,7 @@ impl Batch<'_> { } } - pub fn mount( - &mut self, - chain: Chain, - ) -> Result> + pub fn mount(&mut self, chain: Chain) -> Result where S::Signal: Mountable, SignalState: Send + Sync + 'static, @@ -216,6 +212,13 @@ impl Batch<'_> { let pending_drop = MarkDropped::new(); let ready = chain.into_signal()?; + + let mut slots: SlotMap> = Default::default(); + + S::Signal::trace_slots(&ready.state, &ready.parameters, &mut |id, s| { + slots.insert_mut(id, s); + }); + let mp = crate::mount_point::MountPoint { signal: ready.signal, state: ready.state, @@ -225,7 +228,7 @@ impl Batch<'_> { erased_mount: Arc::new(AtomicRefCell::new(Box::new(mp))), pending_drop: pending_drop.0.clone(), parameters: Arc::new(ready.parameters), - slots: Default::default(), + slots, }; self.new_state.mounts.insert_mut(object_id, inserting); @@ -233,9 +236,44 @@ impl Batch<'_> { Ok(Handle { object_id, mark_drop: Arc::new(pending_drop), - _phantom: PD, }) } + + /// Allocate a slot. + /// + /// This slot cannot be used until a chain which uses it is mounted. To use a slot, call [Slot::signal()] or + /// variations, specifying an initial value. Mounting activates the slot by allocating the necessary internal data + /// structures. + pub fn allocate_slot(&mut self) -> Slot { + Slot { + slot_id: UniqueId::new(), + _phantom: PD, + } + } + + pub fn replace_slot_value( + &mut self, + handle: &Handle, + slot: &Slot, + new_val: T, + ) -> Result<()> + where + T: Send + Sync + Clone + 'static, + { + let slot = self.new_state + .mounts.get_mut(&handle.object_id) + .expect("We give out handles, so the user shouldn't be able to get one to objects that don't exist") + .slots + .get_mut(&slot.slot_id) + .ok_or_else(||Error::new_validation_cow("Slot does not match this mount"))?; + let newslot = slot + .downcast_ref::>() + .unwrap() + .replace(new_val); + *slot = Arc::new(newslot); + + Ok(()) + } } /// Run one iteration of the audio thread.