Skip to content

Commit 07c6d0f

Browse files
authored
Merge pull request #428 from orottier/feature/audio-param-numerical-values
Fix AudioParam automationRate does not instantly update on the control thread
2 parents 0b77741 + 40f4fcc commit 07c6d0f

File tree

2 files changed

+85
-73
lines changed

2 files changed

+85
-73
lines changed

src/param.rs

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! AudioParam interface
22
use std::any::Any;
33
use std::slice::{Iter, IterMut};
4-
use std::sync::atomic::{AtomicBool, Ordering};
5-
use std::sync::{Arc, OnceLock};
4+
use std::sync::atomic::Ordering;
5+
use std::sync::{Arc, Mutex, OnceLock};
66

77
use arrayvec::ArrayVec;
88

@@ -265,18 +265,18 @@ impl AudioParamEventTimeline {
265265
#[derive(Clone)] // for the node bindings, see #378
266266
pub struct AudioParam {
267267
registration: Arc<AudioContextRegistration>,
268-
raw_parts: AudioParamRaw,
268+
raw_parts: AudioParamInner,
269269
}
270270

271271
// helper struct to attach / detach to context (for borrow reasons)
272272
#[derive(Clone)]
273-
pub(crate) struct AudioParamRaw {
274-
default_value: f32, // immutable
275-
min_value: f32, // immutable
276-
max_value: f32, // immutable
277-
automation_rate_constrained: bool,
278-
// TODO Use `Weak` instead of `Arc`. The `AudioParamProcessor` is the owner.
279-
shared_parts: Arc<AudioParamShared>,
273+
pub(crate) struct AudioParamInner {
274+
default_value: f32, // immutable
275+
min_value: f32, // immutable
276+
max_value: f32, // immutable
277+
automation_rate_constrained: bool, // effectively immutable
278+
automation_rate: Arc<Mutex<AutomationRate>>, // shared with clones
279+
current_value: Arc<AtomicF32>, // shared with clones and with render thread
280280
}
281281

282282
impl AudioNode for AudioParam {
@@ -317,8 +317,9 @@ impl AudioNode for AudioParam {
317317

318318
impl AudioParam {
319319
/// Current value of the automation rate of the AudioParam
320+
#[allow(clippy::missing_panics_doc)]
320321
pub fn automation_rate(&self) -> AutomationRate {
321-
self.raw_parts.shared_parts.load_automation_rate()
322+
*self.raw_parts.automation_rate.lock().unwrap()
322323
}
323324

324325
/// Update the current value of the automation rate of the AudioParam
@@ -331,7 +332,11 @@ impl AudioParam {
331332
panic!("InvalidStateError: automation rate cannot be changed for this param");
332333
}
333334

335+
let mut guard = self.raw_parts.automation_rate.lock().unwrap();
336+
*guard = value;
334337
self.registration().post_message(value);
338+
drop(guard); // drop guard after sending message to prevent out of order arrivals on
339+
// concurrent access
335340
}
336341

337342
pub(crate) fn set_automation_rate_constrained(&mut self, value: bool) {
@@ -362,7 +367,7 @@ impl AudioParam {
362367
// test_exponential_ramp_a_rate_multiple_blocks
363368
// test_exponential_ramp_k_rate_multiple_blocks
364369
pub fn value(&self) -> f32 {
365-
self.raw_parts.shared_parts.load_current_value()
370+
self.raw_parts.current_value.load(Ordering::Acquire)
366371
}
367372

368373
/// Set the value of the `AudioParam`.
@@ -384,7 +389,9 @@ impl AudioParam {
384389
assert_is_finite(value);
385390
// current_value should always be clamped
386391
let clamped = value.clamp(self.raw_parts.min_value, self.raw_parts.max_value);
387-
self.raw_parts.shared_parts.store_current_value(clamped);
392+
self.raw_parts
393+
.current_value
394+
.store(clamped, Ordering::Release);
388395

389396
// this event is meant to update param intrinsic value before any calculation
390397
// is done, will behave as SetValueAtTime with `time == block_timestamp`
@@ -610,7 +617,7 @@ impl AudioParam {
610617
}
611618

612619
// helper function to detach from context (for borrow reasons)
613-
pub(crate) fn into_raw_parts(self) -> AudioParamRaw {
620+
pub(crate) fn into_raw_parts(self) -> AudioParamInner {
614621
let Self {
615622
registration: _,
616623
raw_parts,
@@ -621,7 +628,7 @@ impl AudioParam {
621628
// helper function to attach to context (for borrow reasons)
622629
pub(crate) fn from_raw_parts(
623630
registration: AudioContextRegistration,
624-
raw_parts: AudioParamRaw,
631+
raw_parts: AudioParamInner,
625632
) -> Self {
626633
Self {
627634
registration: registration.into(),
@@ -635,47 +642,6 @@ impl AudioParam {
635642
}
636643
}
637644

638-
// Atomic fields of `AudioParam` that could be safely shared between threads
639-
// when wrapped into an `Arc`.
640-
//
641-
// Uses the canonical ordering for handover of values, i.e. `Acquire` on load
642-
// and `Release` on store.
643-
#[derive(Debug)]
644-
pub(crate) struct AudioParamShared {
645-
current_value: AtomicF32,
646-
is_a_rate: AtomicBool,
647-
}
648-
649-
impl AudioParamShared {
650-
pub(crate) fn new(current_value: f32, automation_rate: AutomationRate) -> Self {
651-
Self {
652-
current_value: AtomicF32::new(current_value),
653-
is_a_rate: AtomicBool::new(automation_rate.is_a_rate()),
654-
}
655-
}
656-
657-
pub(crate) fn load_current_value(&self) -> f32 {
658-
self.current_value.load(Ordering::Acquire)
659-
}
660-
661-
pub(crate) fn store_current_value(&self, value: f32) {
662-
self.current_value.store(value, Ordering::Release);
663-
}
664-
665-
pub(crate) fn load_automation_rate(&self) -> AutomationRate {
666-
if self.is_a_rate.load(Ordering::Acquire) {
667-
AutomationRate::A
668-
} else {
669-
AutomationRate::K
670-
}
671-
}
672-
673-
pub(crate) fn store_automation_rate(&self, automation_rate: AutomationRate) {
674-
self.is_a_rate
675-
.store(automation_rate.is_a_rate(), Ordering::Release);
676-
}
677-
}
678-
679645
struct BlockInfos {
680646
block_time: f64,
681647
dt: f64,
@@ -691,7 +657,7 @@ pub(crate) struct AudioParamProcessor {
691657
max_value: f32, // immutable
692658
intrinsic_value: f32,
693659
automation_rate: AutomationRate,
694-
shared_parts: Arc<AudioParamShared>,
660+
current_value: Arc<AtomicF32>,
695661
event_timeline: AudioParamEventTimeline,
696662
last_event: Option<AudioParamEvent>,
697663
buffer: ArrayVec<f32, RENDER_QUANTUM_SIZE>,
@@ -719,7 +685,6 @@ impl AudioProcessor for AudioParamProcessor {
719685
fn onmessage(&mut self, msg: &mut dyn Any) {
720686
if let Some(automation_rate) = msg.downcast_ref::<AutomationRate>() {
721687
self.automation_rate = *automation_rate;
722-
self.shared_parts.store_automation_rate(*automation_rate);
723688
return;
724689
}
725690

@@ -1509,7 +1474,7 @@ impl AudioParamProcessor {
15091474
// Set [[current value]] to the value of paramIntrinsicValue at the
15101475
// beginning of this render quantum.
15111476
let clamped = self.intrinsic_value.clamp(self.min_value, self.max_value);
1512-
self.shared_parts.store_current_value(clamped);
1477+
self.current_value.store(clamped, Ordering::Release);
15131478

15141479
// clear the buffer for this block
15151480
self.buffer.clear();
@@ -1614,26 +1579,27 @@ pub(crate) fn audio_param_pair(
16141579
..
16151580
} = descriptor;
16161581

1617-
let shared_parts = Arc::new(AudioParamShared::new(default_value, automation_rate));
1582+
let current_value = Arc::new(AtomicF32::new(default_value));
16181583

16191584
let param = AudioParam {
16201585
registration: registration.into(),
1621-
raw_parts: AudioParamRaw {
1586+
raw_parts: AudioParamInner {
16221587
default_value,
16231588
max_value,
16241589
min_value,
16251590
automation_rate_constrained: false,
1626-
shared_parts: Arc::clone(&shared_parts),
1591+
automation_rate: Arc::new(Mutex::new(automation_rate)),
1592+
current_value: Arc::clone(&current_value),
16271593
},
16281594
};
16291595

16301596
let processor = AudioParamProcessor {
16311597
intrinsic_value: default_value,
1598+
current_value,
16321599
default_value,
16331600
min_value,
16341601
max_value,
16351602
automation_rate,
1636-
shared_parts,
16371603
event_timeline: AudioParamEventTimeline::new(),
16381604
last_event: None,
16391605
buffer: ArrayVec::new(),
@@ -1705,6 +1671,52 @@ mod tests {
17051671
assert_float_eq!(param.value(), 0., abs_all <= 0.);
17061672
}
17071673

1674+
#[test]
1675+
fn test_automation_rate_synchronicity_on_control_thread() {
1676+
let context = OfflineAudioContext::new(1, 0, 48000.);
1677+
1678+
let opts = AudioParamDescriptor {
1679+
name: String::new(),
1680+
automation_rate: AutomationRate::A,
1681+
default_value: 0.,
1682+
min_value: 0.,
1683+
max_value: 1.,
1684+
};
1685+
let (param, _render) = audio_param_pair(opts, context.mock_registration());
1686+
1687+
param.set_automation_rate(AutomationRate::K);
1688+
assert_eq!(param.automation_rate(), AutomationRate::K);
1689+
}
1690+
1691+
#[test]
1692+
fn test_audioparam_clones_in_sync() {
1693+
let context = OfflineAudioContext::new(1, 0, 48000.);
1694+
1695+
let opts = AudioParamDescriptor {
1696+
name: String::new(),
1697+
automation_rate: AutomationRate::A,
1698+
default_value: 0.,
1699+
min_value: -10.,
1700+
max_value: 10.,
1701+
};
1702+
let (param1, mut render) = audio_param_pair(opts, context.mock_registration());
1703+
let param2 = param1.clone();
1704+
1705+
// changing automation rate on param1 should reflect in param2
1706+
param1.set_automation_rate(AutomationRate::K);
1707+
assert_eq!(param2.automation_rate(), AutomationRate::K);
1708+
1709+
// setting value on param1 should reflect in param2
1710+
render.handle_incoming_event(param1.set_value_raw(2.));
1711+
assert_float_eq!(param1.value(), 2., abs_all <= 0.);
1712+
assert_float_eq!(param2.value(), 2., abs_all <= 0.);
1713+
1714+
// setting value on param2 should reflect in param1
1715+
render.handle_incoming_event(param2.set_value_raw(3.));
1716+
assert_float_eq!(param1.value(), 3., abs_all <= 0.);
1717+
assert_float_eq!(param2.value(), 3., abs_all <= 0.);
1718+
}
1719+
17081720
#[test]
17091721
fn test_set_value() {
17101722
{

src/spatial.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::context::{AudioContextRegistration, BaseAudioContext};
66
use crate::node::{
77
AudioNode, ChannelConfig, ChannelConfigOptions, ChannelCountMode, ChannelInterpretation,
88
};
9-
use crate::param::{AudioParam, AudioParamDescriptor, AudioParamRaw, AutomationRate};
9+
use crate::param::{AudioParam, AudioParamDescriptor, AudioParamInner, AutomationRate};
1010
use crate::render::{AudioParamValues, AudioProcessor, AudioRenderQuantum, RenderScope};
1111

1212
use std::f32::consts::PI;
@@ -183,15 +183,15 @@ impl AudioProcessor for ListenerRenderer {
183183

184184
/// Data holder for the BaseAudioContext so it can reconstruct the AudioListener on request
185185
pub(crate) struct AudioListenerParams {
186-
pub position_x: AudioParamRaw,
187-
pub position_y: AudioParamRaw,
188-
pub position_z: AudioParamRaw,
189-
pub forward_x: AudioParamRaw,
190-
pub forward_y: AudioParamRaw,
191-
pub forward_z: AudioParamRaw,
192-
pub up_x: AudioParamRaw,
193-
pub up_y: AudioParamRaw,
194-
pub up_z: AudioParamRaw,
186+
pub position_x: AudioParamInner,
187+
pub position_y: AudioParamInner,
188+
pub position_z: AudioParamInner,
189+
pub forward_x: AudioParamInner,
190+
pub forward_y: AudioParamInner,
191+
pub forward_z: AudioParamInner,
192+
pub up_x: AudioParamInner,
193+
pub up_y: AudioParamInner,
194+
pub up_z: AudioParamInner,
195195
}
196196

197197
use vecmath::{

0 commit comments

Comments
 (0)