Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set BaseAudioContext::state by render thread to signal device readiness #419

Merged
merged 9 commits into from
Jan 4, 2024
13 changes: 7 additions & 6 deletions src/context/concrete_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ struct ConcreteBaseAudioContextInner {
listener_params: Option<AudioListenerParams>,
/// Denotes if this AudioContext is offline or not
offline: bool,
/// Describes the current state of the `ConcreteBaseAudioContext`
state: AtomicU8,
/// Current state of the `ConcreteBaseAudioContext`, shared with the RenderThread
state: Arc<AtomicU8>,
/// Stores the event handlers
event_loop: EventLoop,
/// Sender for events that will be handled by the EventLoop
Expand Down Expand Up @@ -147,9 +147,11 @@ impl BaseAudioContext for ConcreteBaseAudioContext {

impl ConcreteBaseAudioContext {
/// Creates a `BaseAudioContext` instance
#[allow(clippy::too_many_arguments)] // TODO refactor with builder pattern
pub(super) fn new(
sample_rate: f32,
max_channel_count: usize,
state: Arc<AtomicU8>,
frames_played: Arc<AtomicU64>,
render_channel: Sender<ControlMessage>,
event_channel: Option<(Sender<EventDispatch>, Receiver<EventDispatch>)>,
Expand All @@ -175,7 +177,7 @@ impl ConcreteBaseAudioContext {
queued_audio_listener_msgs: Mutex::new(Vec::new()),
listener_params: None,
offline,
state: AtomicU8::new(AudioContextState::Suspended as u8),
state,
event_loop: event_loop.clone(),
event_send,
};
Expand Down Expand Up @@ -263,7 +265,6 @@ impl ConcreteBaseAudioContext {
if self.state() != AudioContextState::Closed {
let result = self.inner.render_channel.read().unwrap().send(msg);
if result.is_err() {
self.set_state(AudioContextState::Closed);
log::warn!("Discarding control message - render thread is closed");
}
}
Expand Down Expand Up @@ -333,14 +334,14 @@ impl ConcreteBaseAudioContext {
/// Returns state of current context
#[must_use]
pub(super) fn state(&self) -> AudioContextState {
self.inner.state.load(Ordering::SeqCst).into()
self.inner.state.load(Ordering::Acquire).into()
}

/// Updates state of current context
pub(super) fn set_state(&self, state: AudioContextState) {
let current_state = self.state();
if current_state != state {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A race condition may occur in concurrent calls to this method (e.g. making a massive amount of suspend calls) causing the statechange event to be sent too many times. This could be fixed with https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU64.html#method.compare_exchange but this method will probably be refactored away once we solve how to make suspend_sync better

self.inner.state.store(state as u8, Ordering::SeqCst);
self.inner.state.store(state as u8, Ordering::Release);
let _ = self.send_event(EventDispatch::state_change());
}
}
Expand Down
41 changes: 34 additions & 7 deletions src/context/offline.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! The `OfflineAudioContext` type

use std::sync::atomic::AtomicU64;
use std::sync::atomic::{AtomicU64, AtomicU8};
use std::sync::{Arc, Mutex};

use crate::buffer::AudioBuffer;
use crate::context::{BaseAudioContext, ConcreteBaseAudioContext};
use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
use crate::render::RenderThread;
use crate::{assert_valid_sample_rate, RENDER_QUANTUM_SIZE};

Expand Down Expand Up @@ -71,19 +71,23 @@ impl OfflineAudioContext {
// track number of frames - synced from render thread to control thread
let frames_played = Arc::new(AtomicU64::new(0));
let frames_played_clone = Arc::clone(&frames_played);
let state = Arc::new(AtomicU8::new(AudioContextState::Suspended as u8));
let state_clone = Arc::clone(&state);

// setup the render 'thread', which will run inside the control thread
let renderer = RenderThread::new(
sample_rate,
number_of_channels,
receiver,
state_clone,
frames_played_clone,
);

// first, setup the base audio context
let base = ConcreteBaseAudioContext::new(
sample_rate,
number_of_channels,
state,
frames_played,
sender,
None,
Expand Down Expand Up @@ -131,7 +135,11 @@ impl OfflineAudioContext {
..
} = renderer;

renderer.render_audiobuffer_sync(self.length, suspend_callbacks, self)
self.base.set_state(AudioContextState::Running);
let result = renderer.render_audiobuffer_sync(self.length, suspend_callbacks, self);
self.base.set_state(AudioContextState::Closed);

result
}

/// Given the current connections and scheduled changes, starts rendering audio.
Expand Down Expand Up @@ -159,9 +167,15 @@ impl OfflineAudioContext {
..
} = renderer;

renderer
self.base.set_state(AudioContextState::Running);

let result = renderer
.render_audiobuffer(self.length, suspend_promises, resume_receiver)
.await
.await;

self.base.set_state(AudioContextState::Closed);

result
}

/// get the length of rendering audio buffer
Expand Down Expand Up @@ -253,7 +267,8 @@ impl OfflineAudioContext {
.insert(insert_pos, (quantum, sender));
} // lock is dropped

receiver.await.unwrap()
receiver.await.unwrap();
self.base().set_state(AudioContextState::Suspended);
}

/// Schedules a suspension of the time progression in the audio context at the specified time
Expand Down Expand Up @@ -313,9 +328,15 @@ impl OfflineAudioContext {
"InvalidStateError: cannot suspend multiple times at the same render quantum",
);

let boxed_callback = Box::new(|ctx: &mut OfflineAudioContext| {
ctx.base().set_state(AudioContextState::Suspended);
(callback)(ctx);
ctx.base().set_state(AudioContextState::Running);
});

renderer
.suspend_callbacks
.insert(insert_pos, (quantum, Box::new(callback)));
.insert(insert_pos, (quantum, boxed_callback));
}

/// Resumes the progression of the OfflineAudioContext's currentTime when it has been suspended
Expand All @@ -324,6 +345,7 @@ impl OfflineAudioContext {
///
/// Panics when the context is closed or rendering has not started
pub async fn resume(&self) {
self.base().set_state(AudioContextState::Running);
self.resume_sender.clone().send(()).await.unwrap()
}
}
Expand All @@ -339,6 +361,7 @@ mod tests {
#[test]
fn render_empty_graph() {
let mut context = OfflineAudioContext::new(2, 555, 44_100.);
assert_eq!(context.state(), AudioContextState::Suspended);
let buffer = context.start_rendering_sync();

assert_eq!(context.length(), 555);
Expand All @@ -347,6 +370,8 @@ mod tests {
assert_eq!(buffer.length(), 555);
assert_float_eq!(buffer.get_channel_data(0), &[0.; 555][..], abs_all <= 0.);
assert_float_eq!(buffer.get_channel_data(1), &[0.; 555][..], abs_all <= 0.);

assert_eq!(context.state(), AudioContextState::Closed);
}

#[test]
Expand All @@ -365,12 +390,14 @@ mod tests {
let mut context = OfflineAudioContext::new(1, len, sample_rate as f32);

context.suspend_sync(RENDER_QUANTUM_SIZE as f64 / sample_rate, |context| {
assert_eq!(context.state(), AudioContextState::Suspended);
let mut src = context.create_constant_source();
src.connect(&context.destination());
src.start();
});

context.suspend_sync((3 * RENDER_QUANTUM_SIZE) as f64 / sample_rate, |context| {
assert_eq!(context.state(), AudioContextState::Suspended);
context.destination().disconnect();
});

Expand Down
Loading