Skip to content

Commit b837a36

Browse files
authored
Merge pull request #500 from orottier/feature/async-decode-buffer
Implement async method decode_audio_data (however still blocking IO)
2 parents ff73b16 + cce4480 commit b837a36

File tree

5 files changed

+151
-60
lines changed

5 files changed

+151
-60
lines changed

.github/workflows/msrv.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
2626
- name: Install Rust toolchain
2727
# Aligned with `rust-version` in `Cargo.toml`
28-
uses: dtolnay/rust-toolchain@1.71
28+
uses: dtolnay/rust-toolchain@1.76
2929

3030
- name: Check out repository
3131
uses: actions/checkout@v3

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ include = [
1515
"LICENSE",
1616
"README.md",
1717
]
18-
rust-version = "1.71"
18+
rust-version = "1.76"
1919

2020
[dependencies]
2121
almost = "0.2.0"

src/context/base.rs

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use crate::param::AudioParamDescriptor;
1212
use crate::periodic_wave::{PeriodicWave, PeriodicWaveOptions};
1313
use crate::{node, AudioListener};
1414

15+
use std::future::Future;
16+
1517
/// The interface representing an audio-processing graph built from audio modules linked together,
1618
/// each represented by an `AudioNode`.
1719
///
@@ -29,11 +31,11 @@ pub trait BaseAudioContext {
2931
///
3032
/// In addition to the official spec, the input parameter can be any byte stream (not just an
3133
/// array). This means you can decode audio data from a file, network stream, or in memory
32-
/// buffer, and any other [`std::io::Read`] implementer. The data if buffered internally so you
34+
/// buffer, and any other [`std::io::Read`] implementer. The data is buffered internally so you
3335
/// should not wrap the source in a `BufReader`.
3436
///
3537
/// This function operates synchronously, which may be undesirable on the control thread. The
36-
/// example shows how to avoid this. An async version is currently not implemented.
38+
/// example shows how to avoid this. See also the async method [`Self::decode_audio_data`].
3739
///
3840
/// # Errors
3941
///
@@ -82,6 +84,50 @@ pub trait BaseAudioContext {
8284
Ok(buffer)
8385
}
8486

87+
/// Decode an [`AudioBuffer`] from a given input stream.
88+
///
89+
/// The current implementation can decode FLAC, Opus, PCM, Vorbis, and Wav.
90+
///
91+
/// In addition to the official spec, the input parameter can be any byte stream (not just an
92+
/// array). This means you can decode audio data from a file, network stream, or in memory
93+
/// buffer, and any other [`std::io::Read`] implementer. The data is buffered internally so you
94+
/// should not wrap the source in a `BufReader`.
95+
///
96+
/// Warning, the current implementation still uses blocking IO so it's best to use Tokio's
97+
/// `spawn_blocking` to run the decoding on a thread dedicated to blocking operations. See also
98+
/// the async method [`Self::decode_audio_data_sync`].
99+
///
100+
/// # Errors
101+
///
102+
/// This method returns an Error in various cases (IO, mime sniffing, decoding).
103+
// Use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified,
104+
// hence we use `-> impl Future + ..` instead.
105+
fn decode_audio_data<R: std::io::Read + Send + Sync + 'static>(
106+
&self,
107+
input: R,
108+
) -> impl Future<Output = Result<AudioBuffer, Box<dyn std::error::Error + Send + Sync>>>
109+
+ Send
110+
+ 'static {
111+
let sample_rate = self.sample_rate();
112+
async move {
113+
// Set up a media decoder, consume the stream in full and construct a single buffer out of it
114+
let mut buffer = MediaDecoder::try_new(input)?
115+
.collect::<Result<Vec<_>, _>>()?
116+
.into_iter()
117+
.reduce(|mut accum, item| {
118+
accum.extend(&item);
119+
accum
120+
})
121+
// if there are no samples decoded, return an empty buffer
122+
.unwrap_or_else(|| AudioBuffer::from(vec![vec![]], sample_rate));
123+
124+
// resample to desired rate (no-op if already matching)
125+
buffer.resample(sample_rate);
126+
127+
Ok(buffer)
128+
}
129+
}
130+
85131
/// Create an new "in-memory" `AudioBuffer` with the given number of channels,
86132
/// length (i.e. number of samples per channel) and sample rate.
87133
///
@@ -339,3 +385,88 @@ pub trait BaseAudioContext {
339385
}
340386
}
341387
}
388+
389+
#[cfg(test)]
390+
mod tests {
391+
use super::*;
392+
use crate::context::OfflineAudioContext;
393+
394+
use float_eq::assert_float_eq;
395+
396+
fn require_send_sync_static<T: Send + Sync + 'static>(_: T) {}
397+
398+
#[test]
399+
fn test_decode_audio_data_sync() {
400+
let context = OfflineAudioContext::new(1, 1, 44100.);
401+
let file = std::fs::File::open("samples/sample.wav").unwrap();
402+
let audio_buffer = context.decode_audio_data_sync(file).unwrap();
403+
404+
assert_eq!(audio_buffer.sample_rate(), 44100.);
405+
assert_eq!(audio_buffer.length(), 142_187);
406+
assert_eq!(audio_buffer.number_of_channels(), 2);
407+
assert_float_eq!(audio_buffer.duration(), 3.224, abs_all <= 0.001);
408+
409+
let left_start = &audio_buffer.get_channel_data(0)[0..100];
410+
let right_start = &audio_buffer.get_channel_data(1)[0..100];
411+
// assert distinct two channel data
412+
assert!(left_start != right_start);
413+
}
414+
415+
#[test]
416+
fn test_decode_audio_data_future_send_static() {
417+
let context = OfflineAudioContext::new(1, 1, 44100.);
418+
let file = std::fs::File::open("samples/sample.wav").unwrap();
419+
let future = context.decode_audio_data(file);
420+
require_send_sync_static(future);
421+
}
422+
423+
#[test]
424+
fn test_decode_audio_data_async() {
425+
use futures::executor;
426+
let context = OfflineAudioContext::new(1, 1, 44100.);
427+
let file = std::fs::File::open("samples/sample.wav").unwrap();
428+
let future = context.decode_audio_data(file);
429+
let audio_buffer = executor::block_on(future).unwrap();
430+
431+
assert_eq!(audio_buffer.sample_rate(), 44100.);
432+
assert_eq!(audio_buffer.length(), 142_187);
433+
assert_eq!(audio_buffer.number_of_channels(), 2);
434+
assert_float_eq!(audio_buffer.duration(), 3.224, abs_all <= 0.001);
435+
436+
let left_start = &audio_buffer.get_channel_data(0)[0..100];
437+
let right_start = &audio_buffer.get_channel_data(1)[0..100];
438+
// assert distinct two channel data
439+
assert!(left_start != right_start);
440+
}
441+
442+
// #[test]
443+
// disabled: symphonia cannot handle empty WAV-files
444+
#[allow(dead_code)]
445+
fn test_decode_audio_data_empty() {
446+
let context = OfflineAudioContext::new(1, 1, 44100.);
447+
let file = std::fs::File::open("samples/empty_2c.wav").unwrap();
448+
let audio_buffer = context.decode_audio_data_sync(file).unwrap();
449+
assert_eq!(audio_buffer.length(), 0);
450+
}
451+
452+
#[test]
453+
fn test_decode_audio_data_decoding_error() {
454+
let context = OfflineAudioContext::new(1, 1, 44100.);
455+
let file = std::fs::File::open("samples/corrupt.wav").unwrap();
456+
assert!(context.decode_audio_data_sync(file).is_err());
457+
}
458+
459+
#[test]
460+
fn test_create_buffer() {
461+
let number_of_channels = 3;
462+
let length = 2000;
463+
let sample_rate = 96_000.;
464+
465+
let context = OfflineAudioContext::new(1, 1, 44100.);
466+
let buffer = context.create_buffer(number_of_channels, length, sample_rate);
467+
468+
assert_eq!(buffer.number_of_channels(), 3);
469+
assert_eq!(buffer.length(), 2000);
470+
assert_float_eq!(buffer.sample_rate(), 96000., abs_all <= 0.);
471+
}
472+
}

src/context/mod.rs

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,6 @@ mod tests {
146146
use super::*;
147147
use crate::node::AudioNode;
148148

149-
use float_eq::assert_float_eq;
150-
151149
fn require_send_sync_static<T: Send + Sync + 'static>(_: T) {}
152150

153151
#[test]
@@ -156,7 +154,7 @@ mod tests {
156154
let registration = context.mock_registration();
157155

158156
// we want to be able to ship AudioNodes to another thread, so the Registration should be
159-
// Send Sync and 'static
157+
// Send, Sync and 'static
160158
require_send_sync_static(registration);
161159
}
162160

@@ -167,62 +165,17 @@ mod tests {
167165
}
168166

169167
#[test]
170-
fn test_sample_rate_length() {
171-
let context = OfflineAudioContext::new(1, 48000, 96000.);
172-
assert_float_eq!(context.sample_rate(), 96000., abs_all <= 0.);
173-
assert_eq!(context.length(), 48000);
174-
}
175-
176-
#[test]
177-
fn test_decode_audio_data() {
178-
let context = OfflineAudioContext::new(1, 1, 44100.);
179-
let file = std::fs::File::open("samples/sample.wav").unwrap();
180-
let audio_buffer = context.decode_audio_data_sync(file).unwrap();
181-
182-
assert_eq!(audio_buffer.sample_rate(), 44100.);
183-
assert_eq!(audio_buffer.length(), 142_187);
184-
assert_eq!(audio_buffer.number_of_channels(), 2);
185-
assert_float_eq!(audio_buffer.duration(), 3.224, abs_all <= 0.001);
186-
187-
let left_start = &audio_buffer.get_channel_data(0)[0..100];
188-
let right_start = &audio_buffer.get_channel_data(1)[0..100];
189-
// assert distinct two channel data
190-
assert!(left_start != right_start);
191-
}
192-
193-
// #[test]
194-
// disabled: symphonia cannot handle empty WAV-files
195-
#[allow(dead_code)]
196-
fn test_decode_audio_data_empty() {
197-
let context = OfflineAudioContext::new(1, 1, 44100.);
198-
let file = std::fs::File::open("samples/empty_2c.wav").unwrap();
199-
let audio_buffer = context.decode_audio_data_sync(file).unwrap();
200-
assert_eq!(audio_buffer.length(), 0);
201-
}
202-
203-
#[test]
204-
fn test_decode_audio_data_decoding_error() {
205-
let context = OfflineAudioContext::new(1, 1, 44100.);
206-
let file = std::fs::File::open("samples/corrupt.wav").unwrap();
207-
assert!(context.decode_audio_data_sync(file).is_err());
208-
}
209-
210-
#[test]
211-
fn test_create_buffer() {
212-
let number_of_channels = 3;
213-
let length = 2000;
214-
let sample_rate = 96_000.;
215-
216-
let context = OfflineAudioContext::new(1, 1, 44100.);
217-
let buffer = context.create_buffer(number_of_channels, length, sample_rate);
218-
219-
assert_eq!(buffer.number_of_channels(), 3);
220-
assert_eq!(buffer.length(), 2000);
221-
assert_float_eq!(buffer.sample_rate(), 96000., abs_all <= 0.);
168+
fn test_online_audio_context_send_sync() {
169+
let options = AudioContextOptions {
170+
sink_id: "none".into(),
171+
..AudioContextOptions::default()
172+
};
173+
let context = AudioContext::new(options);
174+
require_send_sync_static(context);
222175
}
223176

224177
#[test]
225-
fn test_registration() {
178+
fn test_context_equals() {
226179
let context = OfflineAudioContext::new(1, 48000, 96000.);
227180
let dest = context.destination();
228181
assert!(dest.context() == context.base());

src/context/offline.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,13 @@ mod tests {
433433
use crate::node::AudioNode;
434434
use crate::node::AudioScheduledSourceNode;
435435

436+
#[test]
437+
fn test_sample_rate_length() {
438+
let context = OfflineAudioContext::new(1, 48000, 96000.);
439+
assert_float_eq!(context.sample_rate(), 96000., abs_all <= 0.);
440+
assert_eq!(context.length(), 48000);
441+
}
442+
436443
#[test]
437444
fn render_empty_graph() {
438445
let mut context = OfflineAudioContext::new(2, 555, 44_100.);

0 commit comments

Comments
 (0)