-
Notifications
You must be signed in to change notification settings - Fork 18
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
Change output device for running AudioContext #232
Changes from 6 commits
52696b6
beb1041
1a9d173
7251bdc
40654a2
aec4877
2022e0d
5e0840d
63c75c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
//! The `AudioContext` type and constructor options | ||
use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext}; | ||
use crate::io::{self, AudioBackend}; | ||
use crate::io::{self, AudioBackend, ControlThreadInit, RenderThreadInit}; | ||
use crate::media::{MediaElement, MediaStream}; | ||
use crate::message::ControlMessage; | ||
use crate::node::{self, ChannelConfigOptions}; | ||
use crate::AudioRenderCapacity; | ||
|
||
use std::sync::atomic::AtomicU64; | ||
use std::sync::Arc; | ||
use std::error::Error; | ||
use std::sync::Mutex; | ||
|
||
/// Identify the type of playback, which affects tradeoffs | ||
/// between audio output latency and power consumption | ||
|
@@ -57,9 +58,11 @@ pub struct AudioContext { | |
/// represents the underlying `BaseAudioContext` | ||
base: ConcreteBaseAudioContext, | ||
/// audio backend (play/pause functionality) | ||
backend: Box<dyn AudioBackend>, | ||
backend: Mutex<Box<dyn AudioBackend>>, | ||
/// Provider for rendering performance metrics | ||
render_capacity: AudioRenderCapacity, | ||
/// Initializer for the render thread (when restart is required) | ||
render_thread_init: RenderThreadInit, | ||
} | ||
|
||
impl BaseAudioContext for AudioContext { | ||
|
@@ -80,7 +83,7 @@ impl AudioContext { | |
/// This will play live audio on the default output device. | ||
/// | ||
/// ```no_run | ||
/// use web_audio_api::context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions}; | ||
/// use web_audio_api::context::{AudioContext, AudioContextOptions}; | ||
/// | ||
/// // Request a sample rate of 44.1 kHz and default latency (buffer size 128, if available) | ||
/// let opts = AudioContextOptions { | ||
|
@@ -94,33 +97,55 @@ impl AudioContext { | |
/// // Alternatively, use the default constructor to get the best settings for your hardware | ||
/// // let context = AudioContext::default(); | ||
/// ``` | ||
/// | ||
/// # Panics | ||
/// | ||
/// The `AudioContext` constructor will panic when an invalid `sinkId` is provided in the | ||
/// `AudioContextOptions`. In a future version, a `try_new` constructor will be introduced that | ||
/// will never panic. | ||
#[allow(clippy::needless_pass_by_value)] | ||
#[must_use] | ||
pub fn new(options: AudioContextOptions) -> Self { | ||
// track number of frames - synced from render thread to control thread | ||
let frames_played = Arc::new(AtomicU64::new(0)); | ||
let frames_played_clone = frames_played.clone(); | ||
if let Some(sink_id) = &options.sink_id { | ||
if !crate::enumerate_devices() | ||
.into_iter() | ||
.any(|d| d.device_id() == sink_id) | ||
{ | ||
panic!("NotFoundError: invalid sinkId"); | ||
} | ||
} | ||
|
||
let (control_thread_init, render_thread_init) = io::thread_init(); | ||
let backend = io::build_output(options, render_thread_init.clone()); | ||
|
||
let ControlThreadInit { | ||
frames_played, | ||
ctrl_msg_send, | ||
load_value_recv, | ||
} = control_thread_init; | ||
|
||
// select backend based on cargo features | ||
let (backend, sender, cap_recv) = io::build_output(options, frames_played_clone); | ||
let graph = crate::render::graph::Graph::new(); | ||
let message = crate::message::ControlMessage::Startup { graph }; | ||
ctrl_msg_send.send(message).unwrap(); | ||
|
||
let base = ConcreteBaseAudioContext::new( | ||
backend.sample_rate(), | ||
backend.number_of_channels(), | ||
frames_played, | ||
sender, | ||
ctrl_msg_send, | ||
false, | ||
); | ||
base.set_state(AudioContextState::Running); | ||
|
||
// setup AudioRenderCapacity for this context | ||
let base_clone = base.clone(); | ||
let render_capacity = AudioRenderCapacity::new(base_clone, cap_recv); | ||
let render_capacity = AudioRenderCapacity::new(base_clone, load_value_recv); | ||
|
||
Self { | ||
base, | ||
backend, | ||
backend: Mutex::new(backend), | ||
render_capacity, | ||
render_thread_init, | ||
} | ||
} | ||
|
||
|
@@ -131,7 +156,7 @@ impl AudioContext { | |
// it to the audio subsystem, so this value is zero. (see Gecko) | ||
#[allow(clippy::unused_self)] | ||
#[must_use] | ||
pub const fn base_latency(&self) -> f64 { | ||
pub fn base_latency(&self) -> f64 { | ||
0. | ||
} | ||
|
||
|
@@ -140,15 +165,73 @@ impl AudioContext { | |
/// the time at which the first sample in the buffer is actually processed | ||
/// by the audio output device. | ||
#[must_use] | ||
#[allow(clippy::missing_panics_doc)] | ||
pub fn output_latency(&self) -> f64 { | ||
self.backend.output_latency() | ||
self.backend.lock().unwrap().output_latency() | ||
} | ||
|
||
/// Identifier or the information of the current audio output device. | ||
/// | ||
/// The initial value is `None`, which means the default audio output device. | ||
pub fn sink_id(&self) -> Option<&str> { | ||
self.backend.sink_id() | ||
#[allow(clippy::missing_panics_doc)] | ||
pub fn sink_id(&self) -> Option<String> { | ||
self.backend.lock().unwrap().sink_id().map(str::to_string) | ||
} | ||
|
||
/// Update the current audio output device. | ||
/// | ||
/// This function operates synchronously and might block the current thread. An async version | ||
/// is currently not implemented. | ||
#[allow(clippy::needless_collect, clippy::missing_panics_doc)] | ||
pub fn set_sink_id_sync(&self, sink_id: String) -> Result<(), Box<dyn Error>> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the meat of the PR.
|
||
if self.sink_id().as_deref() == Some(&sink_id) { | ||
return Ok(()); // sink is already active | ||
} | ||
|
||
if !crate::enumerate_devices() | ||
.into_iter() | ||
.any(|d| d.device_id() == sink_id) | ||
{ | ||
Err("NotFoundError: invalid sinkId")?; | ||
} | ||
|
||
let mut backend_guard = self.backend.lock().unwrap(); | ||
|
||
// acquire exclusive lock on ctrl msg sender | ||
let ctrl_msg_send = self.base.lock_control_msg_sender(); | ||
|
||
// flush out the ctrl msg receiver, cache | ||
let pending_msgs: Vec<_> = self.render_thread_init.ctrl_msg_recv.try_iter().collect(); | ||
|
||
// acquire the audio graph from the current render thread, shutting it down | ||
let (graph_send, graph_recv) = crossbeam_channel::bounded(1); | ||
let message = ControlMessage::Shutdown { sender: graph_send }; | ||
ctrl_msg_send.send(message).unwrap(); | ||
let graph = graph_recv.recv().unwrap(); | ||
|
||
// hotswap the backend | ||
let options = AudioContextOptions { | ||
sample_rate: Some(self.sample_rate()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this sample rate is not available on the new sink, bad things will happen. That is why #183 exists |
||
latency_hint: AudioContextLatencyCategory::default(), // todo reuse existing setting | ||
sink_id: Some(sink_id), | ||
}; | ||
*backend_guard = io::build_output(options, self.render_thread_init.clone()); | ||
|
||
// send the audio graph to the new render thread | ||
let message = ControlMessage::Startup { graph }; | ||
ctrl_msg_send.send(message).unwrap(); | ||
|
||
self.base().set_state(AudioContextState::Running); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the context was suspended maybe we want to keep it suspended? Actually, I didn't read the thing carefully, but the spec seems to say:
https://webaudio.github.io/web-audio-api/#ref-for-dom-audiocontext-setsinkid%E2%91%A0 But as we already differ on the initial state, I don't really know... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, very good point. I will update the PR |
||
|
||
// flush the cached msgs | ||
pending_msgs | ||
.into_iter() | ||
.for_each(|m| self.base().send_control_msg(m).unwrap()); | ||
|
||
// explicitly release the lock to prevent concurrent render threads | ||
drop(backend_guard); | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Suspends the progression of time in the audio context. | ||
|
@@ -167,7 +250,7 @@ impl AudioContext { | |
/// * For a `BackendSpecificError` | ||
#[allow(clippy::missing_const_for_fn, clippy::unused_self)] | ||
pub fn suspend_sync(&self) { | ||
if self.backend.suspend() { | ||
if self.backend.lock().unwrap().suspend() { | ||
self.base().set_state(AudioContextState::Suspended); | ||
} | ||
} | ||
|
@@ -186,7 +269,7 @@ impl AudioContext { | |
/// * For a `BackendSpecificError` | ||
#[allow(clippy::missing_const_for_fn, clippy::unused_self)] | ||
pub fn resume_sync(&self) { | ||
if self.backend.resume() { | ||
if self.backend.lock().unwrap().resume() { | ||
self.base().set_state(AudioContextState::Running); | ||
} | ||
} | ||
|
@@ -204,7 +287,7 @@ impl AudioContext { | |
/// Will panic when this function is called multiple times | ||
#[allow(clippy::missing_const_for_fn, clippy::unused_self)] | ||
pub fn close_sync(&self) { | ||
self.backend.close(); | ||
self.backend.lock().unwrap().close(); | ||
|
||
self.base().set_state(AudioContextState::Closed); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure of the thread layout here, but can't this lock() be a problem regarding the audio thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's something we might want to call periodically during rendering I guess
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize now the
backend
naming is confusing.The
backend
object live in the control thread, so locking is no issue.A better name would be
backend_manager
, because it passes instructions from the control thread (start, pause, close) and collects info from the actual backend in the render thread (latency, sink_id).I will update the PR by renaming this struct