Skip to content

Commit

Permalink
Merge pull request #468 from endragor/android-support
Browse files Browse the repository at this point in the history
Android support via Oboe
  • Loading branch information
mitchmindtree authored Oct 27, 2020
2 parents 5816483 + 06cf00c commit f8775b1
Show file tree
Hide file tree
Showing 10 changed files with 867 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/cpal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ jobs:
toolchain: stable
override: true
components: clippy
target: armv7-linux-androideabi
- name: Run clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all --all-features
- name: Run clippy for Android target
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all --all-features --target armv7-linux-androideabi

rustfmt-check:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -198,3 +204,23 @@ jobs:
run: cargo test --all --no-default-features --verbose
- name: Run all features
run: cargo test --all --all-features --verbose

android-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install stable
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: armv7-linux-androideabi
- name: Check beep
run: cargo check --example beep --target armv7-linux-androideabi --verbose
- name: Check enumerate
run: cargo check --example enumerate --target armv7-linux-androideabi --verbose
- name: Check feedback
run: cargo check --example feedback --target armv7-linux-androideabi --verbose
- name: Check record_wav
run: cargo check --example record_wav --target armv7-linux-androideabi --verbose
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,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.2.1", features = [ "java-interface" ] }
ndk = "0.2"
ndk-glue = "0.2"
jni = "0.17"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Currently supported hosts include:
- Windows (via WASAPI by default, see ASIO instructions below)
- macOS (via CoreAudio)
- iOS (via CoreAudio)
- Android (via Oboe)
- Emscripten

Note that on Linux, the ALSA development files are required. These are provided
Expand Down
2 changes: 2 additions & 0 deletions src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub(crate) mod emscripten;
))]
pub(crate) mod jack;
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, 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(),
}
}
}
90 changes: 90 additions & 0 deletions src/host/oboe/input_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::marker::PhantomData;
use std::time::Instant;

extern crate oboe;

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

pub struct CpalInputCallback<I, C> {
data_cb: Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>,
error_cb: Box<dyn FnMut(StreamError) + Send + 'static>,
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) -> 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),
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 f8775b1

Please sign in to comment.