Skip to content

Commit bd08f86

Browse files
committed
Android support via Oboe
1 parent 6558403 commit bd08f86

File tree

8 files changed

+847
-0
lines changed

8 files changed

+847
-0
lines changed

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,9 @@ stdweb = { version = "0.1.3", default-features = false }
4343
wasm-bindgen = { version = "0.2.58", optional = true }
4444
js-sys = { version = "0.3.35" }
4545
web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] }
46+
47+
[target.'cfg(target_os = "android")'.dependencies]
48+
oboe = { version = "0.1", features = [ "java-interface" ] }
49+
ndk = "0.1"
50+
ndk-glue = "0.1"
51+
jni = "0.17"

src/host/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ pub(crate) mod coreaudio;
77
#[cfg(target_os = "emscripten")]
88
pub(crate) mod emscripten;
99
pub(crate) mod null;
10+
#[cfg(target_os = "android")]
11+
pub(crate) mod oboe;
1012
#[cfg(windows)]
1113
pub(crate) mod wasapi;
1214
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]

src/host/oboe/android_media.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::sync::Arc;
2+
3+
extern crate jni;
4+
extern crate ndk_glue;
5+
6+
use self::jni::Executor;
7+
use self::jni::{errors::Result as JResult, objects::JObject, JNIEnv, JavaVM};
8+
9+
// constants from android.media.AudioFormat
10+
pub const ENCODING_PCM_16BIT: i32 = 2;
11+
pub const ENCODING_PCM_FLOAT: i32 = 4;
12+
pub const CHANNEL_OUT_MONO: i32 = 4;
13+
pub const CHANNEL_OUT_STEREO: i32 = 12;
14+
15+
fn with_attached<F, R>(closure: F) -> JResult<R>
16+
where
17+
F: FnOnce(&JNIEnv, JObject) -> JResult<R>,
18+
{
19+
let activity = ndk_glue::native_activity();
20+
let vm = Arc::new(unsafe { JavaVM::from_raw(activity.vm())? });
21+
let activity = activity.activity();
22+
Executor::new(vm).with_attached(|env| closure(env, activity.into()))
23+
}
24+
25+
fn get_min_buffer_size(
26+
class: &'static str,
27+
sample_rate: i32,
28+
channel_mask: i32,
29+
format: i32,
30+
) -> i32 {
31+
// Unwrapping everything because these operations are not expected to fail
32+
// or throw exceptions. Android returns negative values for invalid parameters,
33+
// which is what we expect.
34+
with_attached(|env, _activity| {
35+
let class = env.find_class(class).unwrap();
36+
env.call_static_method(
37+
class,
38+
"getMinBufferSize",
39+
"(III)I",
40+
&[sample_rate.into(), channel_mask.into(), format.into()],
41+
)
42+
.unwrap()
43+
.i()
44+
})
45+
.unwrap()
46+
}
47+
48+
pub fn get_audio_track_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 {
49+
get_min_buffer_size(
50+
"android/media/AudioTrack",
51+
sample_rate,
52+
channel_mask,
53+
format,
54+
)
55+
}
56+
57+
pub fn get_audio_record_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 {
58+
get_min_buffer_size(
59+
"android/media/AudioRecord",
60+
sample_rate,
61+
channel_mask,
62+
format,
63+
)
64+
}

src/host/oboe/convert.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use std::convert::TryInto;
2+
use std::time::Duration;
3+
4+
extern crate oboe;
5+
6+
use crate::{
7+
BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, SampleRate,
8+
StreamError, StreamInstant,
9+
};
10+
11+
pub fn to_stream_instant(duration: Duration) -> StreamInstant {
12+
StreamInstant::new(
13+
duration.as_secs().try_into().unwrap(),
14+
duration.subsec_nanos(),
15+
)
16+
}
17+
18+
pub fn stream_instant<T: oboe::AudioStream + ?Sized>(stream: &mut T) -> StreamInstant {
19+
const CLOCK_MONOTONIC: i32 = 1;
20+
let ts = stream
21+
.get_timestamp(CLOCK_MONOTONIC)
22+
.unwrap_or(oboe::FrameTimestamp {
23+
position: 0,
24+
timestamp: 0,
25+
});
26+
to_stream_instant(Duration::from_nanos(ts.timestamp as u64))
27+
}
28+
29+
impl From<oboe::Error> for StreamError {
30+
fn from(error: oboe::Error) -> Self {
31+
use self::oboe::Error::*;
32+
match error {
33+
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
34+
e => (BackendSpecificError {
35+
description: e.to_string(),
36+
})
37+
.into(),
38+
}
39+
}
40+
}
41+
42+
impl From<oboe::Error> for PlayStreamError {
43+
fn from(error: oboe::Error) -> Self {
44+
use self::oboe::Error::*;
45+
match error {
46+
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
47+
e => (BackendSpecificError {
48+
description: e.to_string(),
49+
})
50+
.into(),
51+
}
52+
}
53+
}
54+
55+
impl From<oboe::Error> for PauseStreamError {
56+
fn from(error: oboe::Error) -> Self {
57+
use self::oboe::Error::*;
58+
match error {
59+
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
60+
e => (BackendSpecificError {
61+
description: e.to_string(),
62+
})
63+
.into(),
64+
}
65+
}
66+
}
67+
68+
impl From<oboe::Error> for BuildStreamError {
69+
fn from(error: oboe::Error) -> Self {
70+
use self::oboe::Error::*;
71+
match error {
72+
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
73+
NoFreeHandles => Self::StreamIdOverflow,
74+
InvalidFormat | InvalidRate => Self::StreamConfigNotSupported,
75+
IllegalArgument => Self::InvalidArgument,
76+
e => (BackendSpecificError {
77+
description: e.to_string(),
78+
})
79+
.into(),
80+
}
81+
}
82+
}

src/host/oboe/input_callback.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use std::marker::PhantomData;
2+
use std::time::{Duration, Instant};
3+
4+
extern crate oboe;
5+
6+
use super::convert::{stream_instant, to_stream_instant};
7+
use crate::{Data, InputCallbackInfo, InputStreamTimestamp, Sample, SampleRate, StreamError};
8+
9+
pub struct CpalInputCallback<I, C> {
10+
data_cb: Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>,
11+
error_cb: Box<dyn FnMut(StreamError) + Send + 'static>,
12+
sample_rate: SampleRate,
13+
created: Instant,
14+
phantom_channel: PhantomData<C>,
15+
phantom_input: PhantomData<I>,
16+
}
17+
18+
impl<I, C> CpalInputCallback<I, C> {
19+
pub fn new<D, E>(data_cb: D, error_cb: E, sample_rate: SampleRate) -> Self
20+
where
21+
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
22+
E: FnMut(StreamError) + Send + 'static,
23+
{
24+
Self {
25+
data_cb: Box::new(data_cb),
26+
error_cb: Box::new(error_cb),
27+
sample_rate,
28+
created: Instant::now(),
29+
phantom_channel: PhantomData,
30+
phantom_input: PhantomData,
31+
}
32+
}
33+
34+
fn make_callback_info(
35+
&self,
36+
audio_stream: &mut dyn oboe::AudioInputStream,
37+
) -> InputCallbackInfo {
38+
InputCallbackInfo {
39+
timestamp: InputStreamTimestamp {
40+
callback: to_stream_instant(self.created.elapsed()),
41+
capture: stream_instant(audio_stream),
42+
},
43+
}
44+
}
45+
}
46+
47+
impl<T: Sample, C: oboe::IsChannelCount> oboe::AudioInputCallback for CpalInputCallback<T, C>
48+
where
49+
(T, C): oboe::IsFrameType,
50+
{
51+
type FrameType = (T, C);
52+
53+
fn on_audio_ready(
54+
&mut self,
55+
audio_stream: &mut dyn oboe::AudioInputStream,
56+
audio_data: &[<<Self as oboe::AudioInputCallback>::FrameType as oboe::IsFrameType>::Type],
57+
) -> oboe::DataCallbackResult {
58+
let cb_info = self.make_callback_info(audio_stream);
59+
let channel_count = if C::CHANNEL_COUNT == oboe::ChannelCount::Mono {
60+
1
61+
} else {
62+
2
63+
};
64+
(self.data_cb)(
65+
&unsafe {
66+
Data::from_parts(
67+
audio_data.as_ptr() as *mut _,
68+
audio_data.len() * channel_count,
69+
T::FORMAT,
70+
)
71+
},
72+
&cb_info,
73+
);
74+
oboe::DataCallbackResult::Continue
75+
}
76+
77+
fn on_error_before_close(
78+
&mut self,
79+
_audio_stream: &mut dyn oboe::AudioInputStream,
80+
error: oboe::Error,
81+
) {
82+
(self.error_cb)(StreamError::from(error))
83+
}
84+
85+
fn on_error_after_close(
86+
&mut self,
87+
_audio_stream: &mut dyn oboe::AudioInputStream,
88+
error: oboe::Error,
89+
) {
90+
(self.error_cb)(StreamError::from(error))
91+
}
92+
}

0 commit comments

Comments
 (0)