Skip to content

Commit

Permalink
Android support via Oboe
Browse files Browse the repository at this point in the history
  • Loading branch information
endragor committed Sep 6, 2020
1 parent 6558403 commit bd08f86
Show file tree
Hide file tree
Showing 8 changed files with 847 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ stdweb = { version = "0.1.3", default-features = false }
wasm-bindgen = { version = "0.2.58", optional = true }
js-sys = { version = "0.3.35" }
web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] }

[target.'cfg(target_os = "android")'.dependencies]
oboe = { version = "0.1", features = [ "java-interface" ] }
ndk = "0.1"
ndk-glue = "0.1"
jni = "0.17"
2 changes: 2 additions & 0 deletions src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub(crate) mod coreaudio;
#[cfg(target_os = "emscripten")]
pub(crate) mod emscripten;
pub(crate) mod null;
#[cfg(target_os = "android")]
pub(crate) mod oboe;
#[cfg(windows)]
pub(crate) mod wasapi;
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
Expand Down
64 changes: 64 additions & 0 deletions src/host/oboe/android_media.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::sync::Arc;

extern crate jni;
extern crate ndk_glue;

use self::jni::Executor;
use self::jni::{errors::Result as JResult, objects::JObject, JNIEnv, JavaVM};

// constants from android.media.AudioFormat
pub const ENCODING_PCM_16BIT: i32 = 2;
pub const ENCODING_PCM_FLOAT: i32 = 4;
pub const CHANNEL_OUT_MONO: i32 = 4;
pub const CHANNEL_OUT_STEREO: i32 = 12;

fn with_attached<F, R>(closure: F) -> JResult<R>
where
F: FnOnce(&JNIEnv, JObject) -> JResult<R>,
{
let activity = ndk_glue::native_activity();
let vm = Arc::new(unsafe { JavaVM::from_raw(activity.vm())? });
let activity = activity.activity();
Executor::new(vm).with_attached(|env| closure(env, activity.into()))
}

fn get_min_buffer_size(
class: &'static str,
sample_rate: i32,
channel_mask: i32,
format: i32,
) -> i32 {
// Unwrapping everything because these operations are not expected to fail
// or throw exceptions. Android returns negative values for invalid parameters,
// which is what we expect.
with_attached(|env, _activity| {
let class = env.find_class(class).unwrap();
env.call_static_method(
class,
"getMinBufferSize",
"(III)I",
&[sample_rate.into(), channel_mask.into(), format.into()],
)
.unwrap()
.i()
})
.unwrap()
}

pub fn get_audio_track_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 {
get_min_buffer_size(
"android/media/AudioTrack",
sample_rate,
channel_mask,
format,
)
}

pub fn get_audio_record_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 {
get_min_buffer_size(
"android/media/AudioRecord",
sample_rate,
channel_mask,
format,
)
}
82 changes: 82 additions & 0 deletions src/host/oboe/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::convert::TryInto;
use std::time::Duration;

extern crate oboe;

use crate::{
BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, SampleRate,
StreamError, StreamInstant,
};

pub fn to_stream_instant(duration: Duration) -> StreamInstant {
StreamInstant::new(
duration.as_secs().try_into().unwrap(),
duration.subsec_nanos(),
)
}

pub fn stream_instant<T: oboe::AudioStream + ?Sized>(stream: &mut T) -> StreamInstant {
const CLOCK_MONOTONIC: i32 = 1;
let ts = stream
.get_timestamp(CLOCK_MONOTONIC)
.unwrap_or(oboe::FrameTimestamp {
position: 0,
timestamp: 0,
});
to_stream_instant(Duration::from_nanos(ts.timestamp as u64))
}

impl From<oboe::Error> for StreamError {
fn from(error: oboe::Error) -> Self {
use self::oboe::Error::*;
match error {
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
e => (BackendSpecificError {
description: e.to_string(),
})
.into(),
}
}
}

impl From<oboe::Error> for PlayStreamError {
fn from(error: oboe::Error) -> Self {
use self::oboe::Error::*;
match error {
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
e => (BackendSpecificError {
description: e.to_string(),
})
.into(),
}
}
}

impl From<oboe::Error> for PauseStreamError {
fn from(error: oboe::Error) -> Self {
use self::oboe::Error::*;
match error {
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
e => (BackendSpecificError {
description: e.to_string(),
})
.into(),
}
}
}

impl From<oboe::Error> for BuildStreamError {
fn from(error: oboe::Error) -> Self {
use self::oboe::Error::*;
match error {
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
NoFreeHandles => Self::StreamIdOverflow,
InvalidFormat | InvalidRate => Self::StreamConfigNotSupported,
IllegalArgument => Self::InvalidArgument,
e => (BackendSpecificError {
description: e.to_string(),
})
.into(),
}
}
}
92 changes: 92 additions & 0 deletions src/host/oboe/input_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::marker::PhantomData;
use std::time::{Duration, Instant};

extern crate oboe;

use super::convert::{stream_instant, to_stream_instant};
use crate::{Data, InputCallbackInfo, InputStreamTimestamp, Sample, SampleRate, StreamError};

pub struct CpalInputCallback<I, C> {
data_cb: Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>,
error_cb: Box<dyn FnMut(StreamError) + Send + 'static>,
sample_rate: SampleRate,
created: Instant,
phantom_channel: PhantomData<C>,
phantom_input: PhantomData<I>,
}

impl<I, C> CpalInputCallback<I, C> {
pub fn new<D, E>(data_cb: D, error_cb: E, sample_rate: SampleRate) -> Self
where
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
E: FnMut(StreamError) + Send + 'static,
{
Self {
data_cb: Box::new(data_cb),
error_cb: Box::new(error_cb),
sample_rate,
created: Instant::now(),
phantom_channel: PhantomData,
phantom_input: PhantomData,
}
}

fn make_callback_info(
&self,
audio_stream: &mut dyn oboe::AudioInputStream,
) -> InputCallbackInfo {
InputCallbackInfo {
timestamp: InputStreamTimestamp {
callback: to_stream_instant(self.created.elapsed()),
capture: stream_instant(audio_stream),
},
}
}
}

impl<T: Sample, C: oboe::IsChannelCount> oboe::AudioInputCallback for CpalInputCallback<T, C>
where
(T, C): oboe::IsFrameType,
{
type FrameType = (T, C);

fn on_audio_ready(
&mut self,
audio_stream: &mut dyn oboe::AudioInputStream,
audio_data: &[<<Self as oboe::AudioInputCallback>::FrameType as oboe::IsFrameType>::Type],
) -> oboe::DataCallbackResult {
let cb_info = self.make_callback_info(audio_stream);
let channel_count = if C::CHANNEL_COUNT == oboe::ChannelCount::Mono {
1
} else {
2
};
(self.data_cb)(
&unsafe {
Data::from_parts(
audio_data.as_ptr() as *mut _,
audio_data.len() * channel_count,
T::FORMAT,
)
},
&cb_info,
);
oboe::DataCallbackResult::Continue
}

fn on_error_before_close(
&mut self,
_audio_stream: &mut dyn oboe::AudioInputStream,
error: oboe::Error,
) {
(self.error_cb)(StreamError::from(error))
}

fn on_error_after_close(
&mut self,
_audio_stream: &mut dyn oboe::AudioInputStream,
error: oboe::Error,
) {
(self.error_cb)(StreamError::from(error))
}
}
Loading

0 comments on commit bd08f86

Please sign in to comment.