Skip to content

Commit fb973ff

Browse files
committed
Move device and audio stream stuff into submodule
1 parent 178ed21 commit fb973ff

File tree

3 files changed

+169
-154
lines changed

3 files changed

+169
-154
lines changed

toybox-audio/src/device.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use cpal::traits::*;
2+
use anyhow::Context as AnyhowContext;
3+
use tracing::instrument;
4+
5+
use std::sync::{Arc, Mutex};
6+
use std::sync::atomic::{AtomicBool, Ordering};
7+
use std::thread::{JoinHandle};
8+
9+
use super::{Configuration, Provider};
10+
11+
12+
// should be able to close and reopen streams dynamically, potentially on different devices
13+
// any non-device state should be maintained
14+
// should be able to cope with different sample rates
15+
16+
pub struct SharedStreamState {
17+
pub provider: Mutex<Option<Box<dyn Provider>>>,
18+
pub device_lost: AtomicBool,
19+
}
20+
21+
22+
23+
24+
pub struct ActiveStream {
25+
_stream: cpal::platform::StreamInner,
26+
configuration: Configuration,
27+
}
28+
29+
pub enum StreamState {
30+
Pending(Option<JoinHandle<anyhow::Result<ActiveStream>>>),
31+
Active(ActiveStream),
32+
InitFailure,
33+
}
34+
35+
impl StreamState {
36+
fn as_active_stream(&self) -> Option<&ActiveStream> {
37+
match self {
38+
StreamState::Active(active_stream) => Some(active_stream),
39+
_ => None
40+
}
41+
}
42+
43+
pub fn current_configuration(&self) -> Option<Configuration> {
44+
self.as_active_stream()
45+
.map(|active_stream| active_stream.configuration)
46+
}
47+
}
48+
49+
50+
pub fn start_stream_build(stream_shared: Arc<SharedStreamState>) -> JoinHandle<anyhow::Result<ActiveStream>> {
51+
std::thread::spawn(move || {
52+
let host = cpal::default_host();
53+
build_output_stream(&host, stream_shared.clone())
54+
})
55+
}
56+
57+
58+
#[instrument(skip_all, name="audio build_output_stream")]
59+
fn build_output_stream(host: &cpal::Host, stream_shared: Arc<SharedStreamState>) -> anyhow::Result<ActiveStream> {
60+
let device = host.default_output_device().context("no output device available")?;
61+
62+
log::info!("Selected audio device: {}", device.name().unwrap_or_else(|_| String::from("<no name>")));
63+
64+
let supported_configs_range = device.supported_output_configs()
65+
.context("error while querying configs")?;
66+
67+
// TODO(pat.m): support different sample formats
68+
let supported_config = supported_configs_range
69+
.filter(|config| config.sample_format().is_float())
70+
.max_by(cpal::SupportedStreamConfigRange::cmp_default_heuristics)
71+
.context("couldn't find a supported configuration")?;
72+
73+
let desired_sample_rate = 48000.clamp(supported_config.min_sample_rate().0, supported_config.max_sample_rate().0);
74+
let supported_config = supported_config
75+
.with_sample_rate(cpal::SampleRate(desired_sample_rate));
76+
77+
let config = supported_config.into();
78+
79+
log::info!("Selected audio device config: {config:#?}");
80+
81+
let stream = device.build_output_stream(
82+
&config,
83+
{
84+
let stream_shared = Arc::clone(&stream_shared);
85+
86+
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
87+
let _span = tracing::trace_span!("audio provider callback").entered();
88+
89+
let mut provider_maybe = stream_shared.provider.lock().unwrap();
90+
if let Some(provider) = &mut *provider_maybe {
91+
provider.fill_buffer(data);
92+
} else {
93+
data.fill(0.0);
94+
}
95+
}
96+
},
97+
{
98+
move |err| {
99+
// react to errors here.
100+
log::warn!("audio device lost! {err}");
101+
stream_shared.device_lost.store(true, Ordering::Relaxed);
102+
}
103+
},
104+
None // None=blocking, Some(Duration)=timeout
105+
)?;
106+
107+
stream.play()?;
108+
109+
let configuration = Configuration {
110+
sample_rate: config.sample_rate.0 as u32,
111+
channels: config.channels as usize,
112+
};
113+
114+
Ok(ActiveStream {
115+
// We pass around the StreamInner so that we can avoid the !Sync/!Send bounds on Stream.
116+
// Send/Sync are disabled because of android, but we don't care about that.
117+
// https://docs.rs/cpal/latest/x86_64-pc-windows-msvc/src/cpal/platform/mod.rs.html#67
118+
_stream: stream.into_inner(),
119+
configuration
120+
})
121+
}
122+
123+
#[instrument]
124+
pub fn enumerate_audio_devices() -> anyhow::Result<()> {
125+
let host = cpal::default_host();
126+
127+
log::trace!("vvvvvvv Available audio devices vvvvvvvv");
128+
129+
for device in host.output_devices()? {
130+
log::trace!(" => {}", device.name()?);
131+
log::trace!(" => default output config: {:?}", device.default_output_config()?);
132+
log::trace!(" => supported configs:");
133+
for config in device.supported_output_configs()? {
134+
log::trace!(" => {config:?}");
135+
}
136+
log::trace!("");
137+
}
138+
139+
log::trace!("^^^^^^ Available audio devices ^^^^^^^");
140+
141+
Ok(())
142+
}

toybox-audio/src/lib.rs

Lines changed: 26 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -9,63 +9,42 @@ use std::sync::{Arc, Mutex};
99
use std::sync::atomic::{AtomicBool, Ordering};
1010
use std::thread::{JoinHandle};
1111

12+
mod device;
13+
use device::*;
1214

1315
pub mod prelude {
1416
pub use super::Provider;
1517
}
1618

1719

18-
#[instrument(skip_all, name="audio init")]
19-
pub fn init() -> System {
20-
if false {
21-
std::thread::spawn(enumerate_audio_devices);
22-
}
23-
24-
let shared = Arc::new(SharedState {
25-
provider: Mutex::new(None),
26-
device_lost: AtomicBool::new(false),
27-
});
28-
29-
System {
30-
stream_state: StreamState::Pending(Some(start_stream_build(shared.clone()))),
31-
shared,
32-
}
20+
pub struct System {
21+
stream_shared: Arc<SharedStreamState>,
22+
stream_state: StreamState,
3323
}
3424

35-
#[instrument]
36-
fn enumerate_audio_devices() -> anyhow::Result<()> {
37-
let host = cpal::default_host();
25+
impl System {
26+
#[instrument(skip_all, name="audio init")]
27+
pub fn init() -> System {
28+
if false {
29+
std::thread::spawn(enumerate_audio_devices);
30+
}
3831

39-
log::trace!("vvvvvvv Available audio devices vvvvvvvv");
32+
let stream_shared = Arc::new(SharedStreamState {
33+
provider: Mutex::new(None),
34+
device_lost: AtomicBool::new(false),
35+
});
4036

41-
for device in host.output_devices()? {
42-
log::trace!(" => {}", device.name()?);
43-
log::trace!(" => default output config: {:?}", device.default_output_config()?);
44-
log::trace!(" => supported configs:");
45-
for config in device.supported_output_configs()? {
46-
log::trace!(" => {config:?}");
37+
System {
38+
stream_state: StreamState::Pending(Some(start_stream_build(stream_shared.clone()))),
39+
stream_shared,
4740
}
48-
log::trace!("");
4941
}
5042

51-
log::trace!("^^^^^^ Available audio devices ^^^^^^^");
52-
53-
Ok(())
54-
}
55-
56-
57-
pub struct System {
58-
shared: Arc<SharedState>,
59-
60-
stream_state: StreamState,
61-
}
62-
63-
impl System {
6443
pub fn update(&mut self) {
6544
match &mut self.stream_state {
6645
StreamState::Active(_) => {
67-
if self.shared.device_lost.load(Ordering::Relaxed) {
68-
self.stream_state = StreamState::Pending(Some(start_stream_build(self.shared.clone())));
46+
if self.stream_shared.device_lost.load(Ordering::Relaxed) {
47+
self.stream_state = StreamState::Pending(Some(start_stream_build(self.stream_shared.clone())));
6948
}
7049
}
7150

@@ -79,7 +58,7 @@ impl System {
7958
log::info!("Output stream active");
8059

8160
self.stream_state = StreamState::Active(new_stream);
82-
self.shared.device_lost.store(false, Ordering::Relaxed);
61+
self.stream_shared.device_lost.store(false, Ordering::Relaxed);
8362
}
8463

8564
Ok(Err(error)) => {
@@ -108,10 +87,10 @@ impl System {
10887
pub fn set_provider<P>(&mut self, provider: impl Into<Option<P>>) -> anyhow::Result<()>
10988
where P : Provider + Send
11089
{
111-
let mut shared_provider = self.shared.provider.lock().unwrap();
90+
let mut shared_provider = self.stream_shared.provider.lock().unwrap();
11291

11392
if let Some(mut provider) = provider.into() {
114-
let configuration = self.stream_state.as_active_stream().map(|active_stream| active_stream.configuration);
93+
let configuration = self.stream_state.current_configuration();
11594

11695
log::info!("Setting initial provider configuration: {configuration:?}");
11796
provider.on_configuration_changed(configuration);
@@ -126,12 +105,11 @@ impl System {
126105
}
127106

128107
fn try_update_provider_config(&mut self) {
129-
if let Ok(mut guard) = self.shared.provider.lock()
108+
let configuration = self.stream_state.current_configuration();
109+
110+
if let Ok(mut guard) = self.stream_shared.provider.lock()
130111
&& let Some(provider) = &mut *guard
131112
{
132-
let configuration = self.stream_state.as_active_stream()
133-
.map(|active_stream| active_stream.configuration);
134-
135113
log::info!("Update provider configuration: {configuration:?}");
136114
provider.on_configuration_changed(configuration);
137115
}
@@ -148,108 +126,3 @@ pub trait Provider : Send + 'static {
148126
fn on_configuration_changed(&mut self, _: Option<Configuration>);
149127
fn fill_buffer(&mut self, buffer: &mut [f32]);
150128
}
151-
152-
153-
struct SharedState {
154-
provider: Mutex<Option<Box<dyn Provider>>>,
155-
device_lost: AtomicBool,
156-
}
157-
158-
159-
// should be able to close and reopen streams dynamically, potentially on different devices
160-
// any non-device state should be maintained
161-
// should be able to cope with different sample rates
162-
163-
164-
fn start_stream_build(shared: Arc<SharedState>) -> JoinHandle<anyhow::Result<ActiveStream>> {
165-
std::thread::spawn(move || {
166-
let host = cpal::default_host();
167-
build_output_stream(&host, shared.clone())
168-
})
169-
}
170-
171-
172-
#[instrument(skip_all, name="audio build_output_stream")]
173-
fn build_output_stream(host: &cpal::Host, shared: Arc<SharedState>) -> anyhow::Result<ActiveStream> {
174-
let device = host.default_output_device().context("no output device available")?;
175-
176-
log::info!("Selected audio device: {}", device.name().unwrap_or_else(|_| String::from("<no name>")));
177-
178-
let supported_configs_range = device.supported_output_configs()
179-
.context("error while querying configs")?;
180-
181-
// TODO(pat.m): support different sample formats
182-
let supported_config = supported_configs_range
183-
.filter(|config| config.sample_format().is_float())
184-
.max_by(cpal::SupportedStreamConfigRange::cmp_default_heuristics)
185-
.context("couldn't find a supported configuration")?;
186-
187-
let desired_sample_rate = 48000.clamp(supported_config.min_sample_rate().0, supported_config.max_sample_rate().0);
188-
let supported_config = supported_config
189-
.with_sample_rate(cpal::SampleRate(desired_sample_rate));
190-
191-
let config = supported_config.into();
192-
193-
log::info!("Selected audio device config: {config:#?}");
194-
195-
let stream = device.build_output_stream(
196-
&config,
197-
{
198-
let shared = Arc::clone(&shared);
199-
200-
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
201-
let _span = tracing::trace_span!("audio provider callback").entered();
202-
203-
let mut provider_maybe = shared.provider.lock().unwrap();
204-
if let Some(provider) = &mut *provider_maybe {
205-
provider.fill_buffer(data);
206-
} else {
207-
data.fill(0.0);
208-
}
209-
}
210-
},
211-
{
212-
move |err| {
213-
// react to errors here.
214-
log::warn!("audio device lost! {err}");
215-
shared.device_lost.store(true, Ordering::Relaxed);
216-
}
217-
},
218-
None // None=blocking, Some(Duration)=timeout
219-
)?;
220-
221-
stream.play()?;
222-
223-
let configuration = Configuration {
224-
sample_rate: config.sample_rate.0 as u32,
225-
channels: config.channels as usize,
226-
};
227-
228-
Ok(ActiveStream {
229-
// We pass around the StreamInner so that we can avoid the !Sync/!Send bounds on Stream.
230-
// Send/Sync are disabled because of android, but we don't care about that.
231-
// https://docs.rs/cpal/latest/x86_64-pc-windows-msvc/src/cpal/platform/mod.rs.html#67
232-
_stream: stream.into_inner(),
233-
configuration
234-
})
235-
}
236-
237-
struct ActiveStream {
238-
_stream: cpal::platform::StreamInner,
239-
configuration: Configuration,
240-
}
241-
242-
enum StreamState {
243-
Pending(Option<JoinHandle<anyhow::Result<ActiveStream>>>),
244-
Active(ActiveStream),
245-
InitFailure,
246-
}
247-
248-
impl StreamState {
249-
fn as_active_stream(&self) -> Option<&ActiveStream> {
250-
match self {
251-
StreamState::Active(active_stream) => Some(active_stream),
252-
_ => None
253-
}
254-
}
255-
}

toybox/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub fn run_with_settings<F, A>(settings: host::Settings<'_>, start_app: F) -> an
3636
.context("Initialising Vfs")?;
3737

3838
let cfg = cfg::Config::from_vfs(&vfs)?;
39-
let audio = audio::init();
39+
let audio = audio::System::init();
4040

4141
_span.exit();
4242

0 commit comments

Comments
 (0)