From 60dec9420896ee19581bf1b3a95298175fc42172 Mon Sep 17 00:00:00 2001 From: mahdi Date: Wed, 12 Mar 2025 22:36:44 +0330 Subject: [PATCH 1/9] Migrate from "oboe" to "ndk::audio" --- Cargo.toml | 6 +- README.md | 6 +- src/host/oboe/convert.rs | 47 ++- src/host/oboe/input_callback.rs | 90 ----- src/host/oboe/java_interface.rs | 7 + .../oboe/java_interface/audio_features.rs | 56 +++ src/host/oboe/java_interface/definitions.rs | 164 +++++++++ src/host/oboe/java_interface/devices_info.rs | 87 +++++ src/host/oboe/java_interface/utils.rs | 181 ++++++++++ src/host/oboe/mod.rs | 332 ++++++++---------- src/host/oboe/output_callback.rs | 90 ----- src/platform/mod.rs | 9 +- 12 files changed, 678 insertions(+), 397 deletions(-) delete mode 100644 src/host/oboe/input_callback.rs create mode 100644 src/host/oboe/java_interface.rs create mode 100644 src/host/oboe/java_interface/audio_features.rs create mode 100644 src/host/oboe/java_interface/definitions.rs create mode 100644 src/host/oboe/java_interface/devices_info.rs create mode 100644 src/host/oboe/java_interface/utils.rs delete mode 100644 src/host/oboe/output_callback.rs diff --git a/Cargo.toml b/Cargo.toml index ecf4cf002..6825970de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ rust-version = "1.70" [features] asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. -oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does. [dependencies] dasp_sample = "0.11" @@ -69,10 +68,11 @@ 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.6", features = [ "java-interface" ] } -ndk = { version = "0.8", default-features = false } +ndk = { version = "0.9", default-features = false, features = ["audio", "api-level-26"]} ndk-context = "0.1" jni = "0.21" +num-derive = "0.4" +num-traits = "0.2" [[example]] name = "android" diff --git a/README.md b/README.md index b5ac67097..b82cbd2aa 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Currently, supported hosts include: - Windows (via WASAPI by default, see ASIO instructions below) - macOS (via CoreAudio) - iOS (via CoreAudio) -- Android (via Oboe) +- Android (via AAudio) - Emscripten Note that on Linux, the ALSA development files are required. These are provided @@ -38,10 +38,6 @@ Some audio backends are optional and will only be compiled with a [feature flag] - JACK (on Linux): `jack` - ASIO (on Windows): `asio` -Oboe can either use a shared or static runtime. The static runtime is used by default, but activating the -`oboe-shared-stdcxx` feature makes it use the shared runtime, which requires `libc++_shared.so` from the Android NDK to -be present during execution. - ## ASIO on Windows [ASIO](https://en.wikipedia.org/wiki/Audio_Stream_Input/Output) is an audio diff --git a/src/host/oboe/convert.rs b/src/host/oboe/convert.rs index 711d2f24c..7a085fa13 100644 --- a/src/host/oboe/convert.rs +++ b/src/host/oboe/convert.rs @@ -1,7 +1,7 @@ use std::convert::TryInto; use std::time::Duration; -extern crate oboe; +extern crate ndk; use crate::{ BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, StreamError, @@ -15,22 +15,21 @@ pub fn to_stream_instant(duration: Duration) -> StreamInstant { ) } -pub fn stream_instant(stream: &mut T) -> StreamInstant { - const CLOCK_MONOTONIC: i32 = 1; +pub fn stream_instant(stream: &ndk::audio::AudioStream) -> StreamInstant { let ts = stream - .get_timestamp(CLOCK_MONOTONIC) - .unwrap_or(oboe::FrameTimestamp { - position: 0, - timestamp: 0, + .timestamp(ndk::audio::Clockid::Monotonic) + .unwrap_or(ndk::audio::Timestamp { + frame_position: 0, + time_nanoseconds: 0, }); - to_stream_instant(Duration::from_nanos(ts.timestamp as u64)) + to_stream_instant(Duration::from_nanos(ts.time_nanoseconds as u64)) } -impl From for StreamError { - fn from(error: oboe::Error) -> Self { - use self::oboe::Error::*; +impl From for StreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; match error { - Disconnected | Unavailable | Closed => Self::DeviceNotAvailable, + Disconnected | Unavailable => Self::DeviceNotAvailable, e => (BackendSpecificError { description: e.to_string(), }) @@ -39,11 +38,11 @@ impl From for StreamError { } } -impl From for PlayStreamError { - fn from(error: oboe::Error) -> Self { - use self::oboe::Error::*; +impl From for PlayStreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; match error { - Disconnected | Unavailable | Closed => Self::DeviceNotAvailable, + Disconnected | Unavailable => Self::DeviceNotAvailable, e => (BackendSpecificError { description: e.to_string(), }) @@ -52,11 +51,11 @@ impl From for PlayStreamError { } } -impl From for PauseStreamError { - fn from(error: oboe::Error) -> Self { - use self::oboe::Error::*; +impl From for PauseStreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; match error { - Disconnected | Unavailable | Closed => Self::DeviceNotAvailable, + Disconnected | Unavailable => Self::DeviceNotAvailable, e => (BackendSpecificError { description: e.to_string(), }) @@ -65,11 +64,11 @@ impl From for PauseStreamError { } } -impl From for BuildStreamError { - fn from(error: oboe::Error) -> Self { - use self::oboe::Error::*; +impl From for BuildStreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; match error { - Disconnected | Unavailable | Closed => Self::DeviceNotAvailable, + Disconnected | Unavailable => Self::DeviceNotAvailable, NoFreeHandles => Self::StreamIdOverflow, InvalidFormat | InvalidRate => Self::StreamConfigNotSupported, IllegalArgument => Self::InvalidArgument, diff --git a/src/host/oboe/input_callback.rs b/src/host/oboe/input_callback.rs deleted file mode 100644 index 0dfb9c881..000000000 --- a/src/host/oboe/input_callback.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::marker::PhantomData; -use std::time::Instant; - -extern crate oboe; - -use super::convert::{stream_instant, to_stream_instant}; -use crate::{Data, InputCallbackInfo, InputStreamTimestamp, SizedSample, StreamError}; - -pub struct CpalInputCallback { - data_cb: Box, - error_cb: Box, - created: Instant, - phantom_channel: PhantomData, - phantom_input: PhantomData, -} - -impl CpalInputCallback { - pub fn new(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::AudioInputStreamSafe, - ) -> InputCallbackInfo { - InputCallbackInfo { - timestamp: InputStreamTimestamp { - callback: to_stream_instant(self.created.elapsed()), - capture: stream_instant(audio_stream), - }, - } - } -} - -impl oboe::AudioInputCallback for CpalInputCallback -where - (T, C): oboe::IsFrameType, -{ - type FrameType = (T, C); - - fn on_error_before_close( - &mut self, - _audio_stream: &mut dyn oboe::AudioInputStreamSafe, - error: oboe::Error, - ) { - (self.error_cb)(StreamError::from(error)) - } - - fn on_error_after_close( - &mut self, - _audio_stream: &mut dyn oboe::AudioInputStreamSafe, - error: oboe::Error, - ) { - (self.error_cb)(StreamError::from(error)) - } - - fn on_audio_ready( - &mut self, - audio_stream: &mut dyn oboe::AudioInputStreamSafe, - audio_data: &[<::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 - } -} diff --git a/src/host/oboe/java_interface.rs b/src/host/oboe/java_interface.rs new file mode 100644 index 000000000..508496f5e --- /dev/null +++ b/src/host/oboe/java_interface.rs @@ -0,0 +1,7 @@ +mod audio_features; +mod definitions; +mod devices_info; +mod utils; + +pub use self::audio_features::*; +pub use self::definitions::*; diff --git a/src/host/oboe/java_interface/audio_features.rs b/src/host/oboe/java_interface/audio_features.rs new file mode 100644 index 000000000..bbd88090c --- /dev/null +++ b/src/host/oboe/java_interface/audio_features.rs @@ -0,0 +1,56 @@ +use super::{ + utils::{ + get_context, get_package_manager, has_system_feature, with_attached, JNIEnv, JObject, + JResult, + }, + PackageManager, +}; + +/** + * The Android audio features + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AudioFeature { + LowLatency, + Output, + Pro, + Microphone, + Midi, +} + +impl From for &'static str { + fn from(feature: AudioFeature) -> Self { + use AudioFeature::*; + match feature { + LowLatency => PackageManager::FEATURE_AUDIO_LOW_LATENCY, + Output => PackageManager::FEATURE_AUDIO_OUTPUT, + Pro => PackageManager::FEATURE_AUDIO_PRO, + Microphone => PackageManager::FEATURE_MICROPHONE, + Midi => PackageManager::FEATURE_MIDI, + } + } +} + +impl AudioFeature { + /** + * Check availability of an audio feature using Android Java API + */ + pub fn has(&self) -> Result { + let context = get_context(); + + with_attached(context, |env, activity| { + try_check_system_feature(env, &activity, (*self).into()) + }) + .map_err(|error| error.to_string()) + } +} + +fn try_check_system_feature<'j>( + env: &mut JNIEnv<'j>, + activity: &JObject<'j>, + feature: &str, +) -> JResult { + let package_manager = get_package_manager(env, activity)?; + + has_system_feature(env, &package_manager, feature) +} diff --git a/src/host/oboe/java_interface/definitions.rs b/src/host/oboe/java_interface/definitions.rs new file mode 100644 index 000000000..a7eaa6047 --- /dev/null +++ b/src/host/oboe/java_interface/definitions.rs @@ -0,0 +1,164 @@ +use num_derive::FromPrimitive; + +use crate::SampleFormat; + +pub(crate) struct Context; + +impl Context { + pub const AUDIO_SERVICE: &'static str = "audio"; +} + +pub(crate) struct PackageManager; + +impl PackageManager { + pub const FEATURE_AUDIO_LOW_LATENCY: &'static str = "android.hardware.audio.low_latency"; + pub const FEATURE_AUDIO_OUTPUT: &'static str = "android.hardware.audio.output"; + pub const FEATURE_AUDIO_PRO: &'static str = "android.hardware.audio.pro"; + pub const FEATURE_MICROPHONE: &'static str = "android.hardware.microphone"; + pub const FEATURE_MIDI: &'static str = "android.software.midi"; +} + +pub(crate) struct AudioManager; + +impl AudioManager { + pub const PROPERTY_OUTPUT_SAMPLE_RATE: &'static str = + "android.media.property.OUTPUT_SAMPLE_RATE"; + pub const PROPERTY_OUTPUT_FRAMES_PER_BUFFER: &'static str = + "android.media.property.OUTPUT_FRAMES_PER_BUFFER"; + + pub const GET_DEVICES_INPUTS: i32 = 1 << 0; + pub const GET_DEVICES_OUTPUTS: i32 = 1 << 1; + pub const GET_DEVICES_ALL: i32 = Self::GET_DEVICES_INPUTS | Self::GET_DEVICES_OUTPUTS; +} + +/** + * The Android audio device info + */ +#[derive(Debug, Clone)] +pub struct AudioDeviceInfo { + /** + * Device identifier + */ + pub id: i32, + + /** + * The type of device + */ + pub device_type: AudioDeviceType, + + /** + * The device can be used for playback and/or capture + */ + pub direction: AudioDeviceDirection, + + /** + * Device address + */ + pub address: String, + + /** + * Device product name + */ + pub product_name: String, + + /** + * Available channel configurations + */ + pub channel_counts: Vec, + + /** + * Supported sample rates + */ + pub sample_rates: Vec, + + /** + * Supported audio formats + */ + pub formats: Vec, +} + +/** + * The type of audio device + */ +#[derive(Debug, Clone, Copy, FromPrimitive)] +#[non_exhaustive] +#[repr(i32)] +pub enum AudioDeviceType { + Unknown = 0, + AuxLine = 19, + BleBroadcast = 30, + BleHeadset = 26, + BleSpeaker = 27, + BluetoothA2DP = 8, + BluetoothSCO = 7, + BuiltinEarpiece = 1, + BuiltinMic = 15, + BuiltinSpeaker = 2, + BuiltinSpeakerSafe = 24, + Bus = 21, + Dock = 13, + Fm = 14, + FmTuner = 16, + Hdmi = 9, + HdmiArc = 10, + HdmiEarc = 29, + HearingAid = 23, + Ip = 20, + LineAnalog = 5, + LineDigital = 6, + RemoteSubmix = 25, + Telephony = 18, + TvTuner = 17, + UsbAccessory = 12, + UsbDevice = 11, + UsbHeadset = 22, + WiredHeadphones = 4, + WiredHeadset = 3, + Unsupported = -1, +} + +/** + * The direction of audio device + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(i32)] +pub enum AudioDeviceDirection { + Dumb = 0, + Input = AudioManager::GET_DEVICES_INPUTS, + Output = AudioManager::GET_DEVICES_OUTPUTS, + InputOutput = AudioManager::GET_DEVICES_ALL, +} + +impl AudioDeviceDirection { + pub fn new(is_input: bool, is_output: bool) -> Self { + use self::AudioDeviceDirection::*; + match (is_input, is_output) { + (true, true) => InputOutput, + (false, true) => Output, + (true, false) => Input, + _ => Dumb, + } + } + + pub fn is_input(&self) -> bool { + 0 < *self as i32 & AudioDeviceDirection::Input as i32 + } + + pub fn is_output(&self) -> bool { + 0 < *self as i32 & AudioDeviceDirection::Output as i32 + } +} + +impl SampleFormat { + pub(crate) const ENCODING_PCM_16BIT: i32 = 2; + //pub(crate) const ENCODING_PCM_8BIT: i32 = 3; + pub(crate) const ENCODING_PCM_FLOAT: i32 = 4; + + pub(crate) fn from_encoding(encoding: i32) -> Option { + match encoding { + SampleFormat::ENCODING_PCM_16BIT => Some(SampleFormat::I16), + SampleFormat::ENCODING_PCM_FLOAT => Some(SampleFormat::F32), + _ => None, + } + } +} diff --git a/src/host/oboe/java_interface/devices_info.rs b/src/host/oboe/java_interface/devices_info.rs new file mode 100644 index 000000000..119f81338 --- /dev/null +++ b/src/host/oboe/java_interface/devices_info.rs @@ -0,0 +1,87 @@ +use num_traits::FromPrimitive; + +use crate::SampleFormat; + +use super::{ + utils::{ + call_method_no_args_ret_bool, call_method_no_args_ret_char_sequence, + call_method_no_args_ret_int, call_method_no_args_ret_int_array, + call_method_no_args_ret_string, get_context, get_devices, get_system_service, + with_attached, JNIEnv, JObject, JResult, + }, + AudioDeviceDirection, AudioDeviceInfo, AudioDeviceType, Context, +}; + +impl AudioDeviceInfo { + /** + * Request audio devices using Android Java API + */ + pub fn request(direction: AudioDeviceDirection) -> Result, String> { + let context = get_context(); + + with_attached(context, |env, context| { + let sdk_version = env + .get_static_field("android/os/Build$VERSION", "SDK_INT", "I")? + .i()?; + + if sdk_version >= 23 { + try_request_devices_info(env, &context, direction) + } else { + Err(jni::errors::Error::MethodNotFound { + name: "".into(), + sig: "".into(), + }) + } + }) + .map_err(|error| error.to_string()) + } +} + +fn try_request_devices_info<'j>( + env: &mut JNIEnv<'j>, + context: &JObject<'j>, + direction: AudioDeviceDirection, +) -> JResult> { + let audio_manager = get_system_service(env, context, Context::AUDIO_SERVICE)?; + + let devices = get_devices(env, &audio_manager, direction as i32)?; + + let length = env.get_array_length(&devices)?; + + (0..length) + .map(|index| { + let device = env.get_object_array_element(&devices, index)?; + let id = call_method_no_args_ret_int(env, &device, "getId")?; + let address = call_method_no_args_ret_string(env, &device, "getAddress")?; + let address = String::from(env.get_string(&address)?); + let product_name = + call_method_no_args_ret_char_sequence(env, &device, "getProductName")?; + let product_name = String::from(env.get_string(&product_name)?); + let device_type = + FromPrimitive::from_i32(call_method_no_args_ret_int(env, &device, "getType")?) + .unwrap_or(AudioDeviceType::Unsupported); + let direction = AudioDeviceDirection::new( + call_method_no_args_ret_bool(env, &device, "isSource")?, + call_method_no_args_ret_bool(env, &device, "isSink")?, + ); + let channel_counts = + call_method_no_args_ret_int_array(env, &device, "getChannelCounts")?; + let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")?; + let formats = call_method_no_args_ret_int_array(env, &device, "getEncodings")? + .into_iter() + .filter_map(SampleFormat::from_encoding) + .collect::>(); + + Ok(AudioDeviceInfo { + id, + address, + product_name, + device_type, + direction, + channel_counts, + sample_rates, + formats, + }) + }) + .collect::, _>>() +} diff --git a/src/host/oboe/java_interface/utils.rs b/src/host/oboe/java_interface/utils.rs new file mode 100644 index 000000000..5671ee6ae --- /dev/null +++ b/src/host/oboe/java_interface/utils.rs @@ -0,0 +1,181 @@ +use jni::sys::jobject; +use ndk_context::AndroidContext; +use std::sync::Arc; + +pub use jni::Executor; + +pub use jni::{ + errors::Result as JResult, + objects::{JIntArray, JObject, JObjectArray, JString}, + JNIEnv, JavaVM, +}; + +pub fn get_context() -> AndroidContext { + ndk_context::android_context() +} + +pub fn with_attached(context: AndroidContext, closure: F) -> JResult +where + for<'j> F: FnOnce(&mut JNIEnv<'j>, JObject<'j>) -> JResult, +{ + let vm = Arc::new(unsafe { JavaVM::from_raw(context.vm().cast())? }); + let context = context.context(); + let context = unsafe { JObject::from_raw(context as jobject) }; + Executor::new(vm).with_attached(|env| closure(env, context)) +} + +pub fn call_method_no_args_ret_int_array<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let array: JIntArray = env.call_method(subject, method, "()[I", &[])?.l()?.into(); + + let length = env.get_array_length(&array)?; + let mut values = Vec::with_capacity(length as usize); + + env.get_int_array_region(array, 0, values.as_mut())?; + + Ok(values) +} + +pub fn call_method_no_args_ret_int<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult { + env.call_method(subject, method, "()I", &[])?.i() +} + +pub fn call_method_no_args_ret_bool<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult { + env.call_method(subject, method, "()Z", &[])?.z() +} + +pub fn call_method_no_args_ret_string<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + Ok(env + .call_method(subject, method, "()Ljava/lang/String;", &[])? + .l()? + .into()) +} + +pub fn call_method_no_args_ret_char_sequence<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let cseq = env + .call_method(subject, method, "()Ljava/lang/CharSequence;", &[])? + .l()?; + + Ok(env + .call_method(&cseq, "toString", "()Ljava/lang/String;", &[])? + .l()? + .into()) +} + +pub fn call_method_string_arg_ret_bool<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, + arg: impl AsRef, +) -> JResult { + env.call_method( + subject, + name, + "(Ljava/lang/String;)Z", + &[(&env.new_string(arg)?).into()], + )? + .z() +} + +pub fn call_method_string_arg_ret_string<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, + arg: impl AsRef, +) -> JResult> { + Ok(env + .call_method( + subject, + name, + "(Ljava/lang/String;)Ljava/lang/String;", + &[(&env.new_string(arg)?).into()], + )? + .l()? + .into()) +} + +pub fn call_method_string_arg_ret_object<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, + arg: &str, +) -> JResult> { + env.call_method( + subject, + method, + "(Ljava/lang/String;)Ljava/lang/Object;", + &[(&env.new_string(arg)?).into()], + )? + .l() +} + +pub fn get_package_manager<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, +) -> JResult> { + env.call_method( + subject, + "getPackageManager", + "()Landroid/content/pm/PackageManager;", + &[], + )? + .l() +} + +pub fn has_system_feature<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult { + call_method_string_arg_ret_bool(env, subject, "hasSystemFeature", name) +} + +pub fn get_system_service<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult> { + call_method_string_arg_ret_object(env, subject, "getSystemService", name) +} + +pub fn get_property<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult> { + call_method_string_arg_ret_string(env, subject, "getProperty", name) +} + +pub fn get_devices<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + flags: i32, +) -> JResult> { + env.call_method( + subject, + "getDevices", + "(I)[Landroid/media/AudioDeviceInfo;", + &[flags.into()], + )? + .l() + .map(From::from) +} diff --git a/src/host/oboe/mod.rs b/src/host/oboe/mod.rs index e2acb5208..bfcd6936c 100644 --- a/src/host/oboe/mod.rs +++ b/src/host/oboe/mod.rs @@ -1,29 +1,29 @@ use std::cell::RefCell; use std::cmp; use std::convert::TryInto; -use std::time::Duration; +use std::time::{Duration, Instant}; use std::vec::IntoIter as VecIntoIter; -extern crate oboe; +extern crate ndk; + +use convert::{stream_instant, to_stream_instant}; +use java_interface::{AudioDeviceDirection, AudioDeviceInfo}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, SampleRate, SizedSample, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + DeviceNameError, DevicesError, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, + OutputStreamTimestamp, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, + SizedSample, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; mod android_media; mod convert; -mod input_callback; -mod output_callback; +mod java_interface; use self::android_media::{get_audio_record_min_buffer_size, get_audio_track_min_buffer_size}; -use self::input_callback::CpalInputCallback; -use self::oboe::{AudioInputStream, AudioOutputStream}; -use self::output_callback::CpalOutputCallback; +use self::ndk::audio::AudioStream; // Android Java API supports up to 8 channels, but oboe API // only exposes mono and stereo. @@ -38,11 +38,17 @@ const SAMPLE_RATES: [i32; 13] = [ pub struct Host; #[derive(Clone)] -pub struct Device(Option); +pub struct Device(Option); pub enum Stream { - Input(Box>), - Output(Box>), + Input(AudioStream), + Output(AudioStream), } + +// AAudioStream is safe to be send, but not sync. +// See https://developer.android.com/ndk/guides/audio/aaudio/aaudio +// TODO: Is this still in-progress? https://github.com/rust-mobile/ndk/pull/497 +unsafe impl Send for Stream {} + pub type SupportedInputConfigs = VecIntoIter; pub type SupportedOutputConfigs = VecIntoIter; pub type Devices = VecIntoIter; @@ -62,8 +68,7 @@ impl HostTrait for Host { } fn devices(&self) -> Result { - if let Ok(devices) = oboe::AudioDeviceInfo::request(oboe::AudioDeviceDirection::InputOutput) - { + if let Ok(devices) = AudioDeviceInfo::request(AudioDeviceDirection::InputOutput) { Ok(devices .into_iter() .map(|d| Device(Some(d))) @@ -140,7 +145,7 @@ fn default_supported_configs(is_output: bool) -> VecIntoIter VecIntoIter { let sample_rates = if !device.sample_rates.is_empty() { @@ -156,7 +161,7 @@ fn device_supported_configs( &ALL_CHANNELS }; - const ALL_FORMATS: [oboe::AudioFormat; 2] = [oboe::AudioFormat::I16, oboe::AudioFormat::F32]; + const ALL_FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32]; let formats = if !device.formats.is_empty() { device.formats.as_slice() } else { @@ -174,12 +179,8 @@ fn device_supported_configs( let channel_mask = CHANNEL_MASKS[*channel_count as usize - 1]; for format in formats { let (android_format, sample_format) = match format { - oboe::AudioFormat::I16 => { - (android_media::ENCODING_PCM_16BIT, SampleFormat::I16) - } - oboe::AudioFormat::F32 => { - (android_media::ENCODING_PCM_FLOAT, SampleFormat::F32) - } + SampleFormat::I16 => (android_media::ENCODING_PCM_16BIT, SampleFormat::I16), + SampleFormat::F32 => (android_media::ENCODING_PCM_FLOAT, SampleFormat::F32), _ => panic!("Unexpected format"), }; let buffer_size = buffer_size_range_for_params( @@ -202,69 +203,105 @@ fn device_supported_configs( output.into_iter() } -fn configure_for_device( - builder: oboe::AudioStreamBuilder, +fn configure_for_device( + builder: ndk::audio::AudioStreamBuilder, device: &Device, config: &StreamConfig, -) -> oboe::AudioStreamBuilder { +) -> ndk::audio::AudioStreamBuilder { let mut builder = if let Some(info) = &device.0 { - builder.set_device_id(info.id) + builder.device_id(info.id) } else { builder }; - builder = builder.set_sample_rate(config.sample_rate.0.try_into().unwrap()); + builder = builder.sample_rate(config.sample_rate.0.try_into().unwrap()); match &config.buffer_size { BufferSize::Default => builder, - BufferSize::Fixed(size) => builder.set_buffer_capacity_in_frames(*size as i32), + BufferSize::Fixed(size) => builder.buffer_capacity_in_frames(*size as i32), } } -fn build_input_stream( +fn build_input_stream( device: &Device, config: &StreamConfig, - data_callback: D, - error_callback: E, - builder: oboe::AudioStreamBuilder, + mut data_callback: D, + mut error_callback: E, + builder: ndk::audio::AudioStreamBuilder, + sample_format: SampleFormat, ) -> Result where - T: SizedSample + oboe::IsFormat + Send + 'static, - C: oboe::IsChannelCount + Send + 'static, - (T, C): oboe::IsFrameType, D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { let builder = configure_for_device(builder, device, config); + let created = Instant::now(); + let channel_count = config.channels as i32; let stream = builder - .set_callback(CpalInputCallback::::new( - data_callback, - error_callback, - )) + .data_callback(Box::new(move |stream, data, num_frames| { + let cb_info = InputCallbackInfo { + timestamp: InputStreamTimestamp { + callback: to_stream_instant(created.elapsed()), + capture: stream_instant(stream), + }, + }; + (data_callback)( + &unsafe { + Data::from_parts( + data as *mut _, + (num_frames * channel_count).try_into().unwrap(), + sample_format, + ) + }, + &cb_info, + ); + ndk::audio::AudioCallbackResult::Continue + })) + .error_callback(Box::new(move |stream, error| { + (error_callback)(StreamError::from(error)) + })) .open_stream()?; - Ok(Stream::Input(Box::new(RefCell::new(stream)))) + Ok(Stream::Input(stream)) } -fn build_output_stream( +fn build_output_stream( device: &Device, config: &StreamConfig, - data_callback: D, - error_callback: E, - builder: oboe::AudioStreamBuilder, + mut data_callback: D, + mut error_callback: E, + builder: ndk::audio::AudioStreamBuilder, + sample_format: SampleFormat, ) -> Result where - T: SizedSample + oboe::IsFormat + Send + 'static, - C: oboe::IsChannelCount + Send + 'static, - (T, C): oboe::IsFrameType, D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { let builder = configure_for_device(builder, device, config); + let created = Instant::now(); + let channel_count = config.channels as i32; let stream = builder - .set_callback(CpalOutputCallback::::new( - data_callback, - error_callback, - )) + .data_callback(Box::new(move |stream, data, num_frames| { + let cb_info = OutputCallbackInfo { + timestamp: OutputStreamTimestamp { + callback: to_stream_instant(created.elapsed()), + playback: stream_instant(stream), + }, + }; + (data_callback)( + &mut unsafe { + Data::from_parts( + data as *mut _, + (num_frames * channel_count).try_into().unwrap(), + sample_format, + ) + }, + &cb_info, + ); + ndk::audio::AudioCallbackResult::Continue + })) + .error_callback(Box::new(move |stream, error| { + (error_callback)(StreamError::from(error)) + })) .open_stream()?; - Ok(Stream::Output(Box::new(RefCell::new(stream)))) + Ok(Stream::Output(stream)) } impl DeviceTrait for Device { @@ -333,66 +370,40 @@ impl DeviceTrait for Device { D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - match sample_format { - SampleFormat::I16 => { - let builder = oboe::AudioStreamBuilder::default() - .set_input() - .set_format::(); - if config.channels == 1 { - build_input_stream( - self, - config, - data_callback, - error_callback, - builder.set_mono(), - ) - } else if config.channels == 2 { - build_input_stream( - self, - config, - data_callback, - error_callback, - builder.set_stereo(), - ) - } else { - Err(BackendSpecificError { - description: "More than 2 channels are not supported by Oboe.".to_owned(), - } - .into()) + let format = match sample_format { + SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, + SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, + sample_format => { + return Err(BackendSpecificError { + description: format!("{} format is not supported on Android.", sample_format), } + .into()) } - SampleFormat::F32 => { - let builder = oboe::AudioStreamBuilder::default() - .set_input() - .set_format::(); - if config.channels == 1 { - build_input_stream( - self, - config, - data_callback, - error_callback, - builder.set_mono(), - ) - } else if config.channels == 2 { - build_input_stream( - self, - config, - data_callback, - error_callback, - builder.set_stereo(), - ) - } else { - Err(BackendSpecificError { - description: "More than 2 channels are not supported by Oboe.".to_owned(), - } - .into()) + }; + let channel_count = match config.channels { + 1 => 1, + 2 => 2, + channels => { + return Err(BackendSpecificError { + description: "More than 2 channels are not supported by Oboe.".to_owned(), } + .into()) } - sample_format => Err(BackendSpecificError { - description: format!("{} format is not supported on Android.", sample_format), - } - .into()), - } + }; + + let builder = ndk::audio::AudioStreamBuilder::new()? + .direction(ndk::audio::AudioDirection::Input) + .channel_count(channel_count) + .format(format); + + build_input_stream( + self, + config, + data_callback, + error_callback, + builder, + sample_format, + ) } fn build_output_stream_raw( @@ -407,80 +418,48 @@ impl DeviceTrait for Device { D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - match sample_format { - SampleFormat::I16 => { - let builder = oboe::AudioStreamBuilder::default() - .set_output() - .set_format::(); - if config.channels == 1 { - build_output_stream( - self, - config, - data_callback, - error_callback, - builder.set_mono(), - ) - } else if config.channels == 2 { - build_output_stream( - self, - config, - data_callback, - error_callback, - builder.set_stereo(), - ) - } else { - Err(BackendSpecificError { - description: "More than 2 channels are not supported by Oboe.".to_owned(), - } - .into()) + let format = match sample_format { + SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, + SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, + sample_format => { + return Err(BackendSpecificError { + description: format!("{} format is not supported on Android.", sample_format), } + .into()) } - SampleFormat::F32 => { - let builder = oboe::AudioStreamBuilder::default() - .set_output() - .set_format::(); - if config.channels == 1 { - build_output_stream( - self, - config, - data_callback, - error_callback, - builder.set_mono(), - ) - } else if config.channels == 2 { - build_output_stream( - self, - config, - data_callback, - error_callback, - builder.set_stereo(), - ) - } else { - Err(BackendSpecificError { - description: "More than 2 channels are not supported by Oboe.".to_owned(), - } - .into()) + }; + let channel_count = match config.channels { + 1 => 1, + 2 => 2, + channels => { + return Err(BackendSpecificError { + description: "More than 2 channels are not supported by Oboe.".to_owned(), } + .into()) } - sample_format => Err(BackendSpecificError { - description: format!("{} format is not supported on Android.", sample_format), - } - .into()), - } + }; + + let builder = ndk::audio::AudioStreamBuilder::new()? + .direction(ndk::audio::AudioDirection::Output) + .channel_count(channel_count) + .format(format); + + build_output_stream( + self, + config, + data_callback, + error_callback, + builder, + sample_format, + ) } } impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { match self { - Self::Input(stream) => stream - .borrow_mut() - .request_start() - .map_err(PlayStreamError::from), - Self::Output(stream) => stream - .borrow_mut() - .request_start() - .map_err(PlayStreamError::from), + Self::Input(stream) => stream.request_start().map_err(PlayStreamError::from), + Self::Output(stream) => stream.request_start().map_err(PlayStreamError::from), } } @@ -490,10 +469,7 @@ impl StreamTrait for Stream { description: "Pause called on the input stream.".to_owned(), } .into()), - Self::Output(stream) => stream - .borrow_mut() - .request_pause() - .map_err(PauseStreamError::from), + Self::Output(stream) => stream.request_pause().map_err(PauseStreamError::from), } } } diff --git a/src/host/oboe/output_callback.rs b/src/host/oboe/output_callback.rs deleted file mode 100644 index 6ab696048..000000000 --- a/src/host/oboe/output_callback.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::marker::PhantomData; -use std::time::Instant; - -extern crate oboe; - -use super::convert::{stream_instant, to_stream_instant}; -use crate::{Data, OutputCallbackInfo, OutputStreamTimestamp, SizedSample, StreamError}; - -pub struct CpalOutputCallback { - data_cb: Box, - error_cb: Box, - created: Instant, - phantom_channel: PhantomData, - phantom_input: PhantomData, -} - -impl CpalOutputCallback { - pub fn new(data_cb: D, error_cb: E) -> Self - where - D: FnMut(&mut Data, &OutputCallbackInfo) + 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::AudioOutputStreamSafe, - ) -> OutputCallbackInfo { - OutputCallbackInfo { - timestamp: OutputStreamTimestamp { - callback: to_stream_instant(self.created.elapsed()), - playback: stream_instant(audio_stream), - }, - } - } -} - -impl oboe::AudioOutputCallback for CpalOutputCallback -where - (T, C): oboe::IsFrameType, -{ - type FrameType = (T, C); - - fn on_error_before_close( - &mut self, - _audio_stream: &mut dyn oboe::AudioOutputStreamSafe, - error: oboe::Error, - ) { - (self.error_cb)(StreamError::from(error)) - } - - fn on_error_after_close( - &mut self, - _audio_stream: &mut dyn oboe::AudioOutputStreamSafe, - error: oboe::Error, - ) { - (self.error_cb)(StreamError::from(error)) - } - - fn on_audio_ready( - &mut self, - audio_stream: &mut dyn oboe::AudioOutputStreamSafe, - audio_data: &mut [<::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)( - &mut unsafe { - Data::from_parts( - audio_data.as_mut_ptr() as *mut _, - audio_data.len() * channel_count, - T::FORMAT, - ) - }, - &cb_info, - ); - oboe::DataCallbackResult::Continue - } -} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 65d77ca40..fed4e08ac 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -64,13 +64,8 @@ macro_rules! impl_platform_host { /// The `Stream` implementation associated with the platform's dynamically dispatched /// [`Host`] type. - // Streams cannot be `Send` or `Sync` if we plan to support Android's AAudio API. This is - // because the stream API is not thread-safe, and the API prohibits calling certain - // functions within the callback. - // - // TODO: Confirm this and add more specific detail and references. #[must_use = "If the stream is not stored it will not play."] - pub struct Stream(StreamInner, crate::platform::NotSendSyncAcrossAllPlatforms); + pub struct Stream(StreamInner); /// The `SupportedInputConfigs` iterator associated with the platform's dynamically /// dispatched [`Host`] type. @@ -526,7 +521,7 @@ macro_rules! impl_platform_host { impl From for Stream { fn from(s: StreamInner) -> Self { - Stream(s, Default::default()) + Stream(s) } } From 6b73ade50e064a06580b28254b59d8a9a79455d8 Mon Sep 17 00:00:00 2001 From: mahdi Date: Wed, 12 Mar 2025 23:23:10 +0330 Subject: [PATCH 2/9] trying to pass github workflow for android packing --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 6825970de..b26d1a497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,11 @@ jni = "0.21" num-derive = "0.4" num-traits = "0.2" +[package.metadata.android] +min_sdk_version = 26 +target_sdk_version = 30 +max_sdk_version = 29 + [[example]] name = "android" path = "examples/android.rs" From 759cef5509b516c6d31b3f2a295f6cd6f7b61a13 Mon Sep 17 00:00:00 2001 From: mahdi Date: Thu, 13 Mar 2025 16:34:44 +0330 Subject: [PATCH 3/9] rename mod oboe -> aaudio --- src/host/{oboe => aaudio}/android_media.rs | 0 src/host/{oboe => aaudio}/convert.rs | 0 src/host/{oboe => aaudio}/java_interface.rs | 0 .../java_interface/audio_features.rs | 0 .../{oboe => aaudio}/java_interface/definitions.rs | 0 .../{oboe => aaudio}/java_interface/devices_info.rs | 0 src/host/{oboe => aaudio}/java_interface/utils.rs | 0 src/host/{oboe => aaudio}/mod.rs | 0 src/host/mod.rs | 2 +- src/platform/mod.rs | 12 ++++++------ 10 files changed, 7 insertions(+), 7 deletions(-) rename src/host/{oboe => aaudio}/android_media.rs (100%) rename src/host/{oboe => aaudio}/convert.rs (100%) rename src/host/{oboe => aaudio}/java_interface.rs (100%) rename src/host/{oboe => aaudio}/java_interface/audio_features.rs (100%) rename src/host/{oboe => aaudio}/java_interface/definitions.rs (100%) rename src/host/{oboe => aaudio}/java_interface/devices_info.rs (100%) rename src/host/{oboe => aaudio}/java_interface/utils.rs (100%) rename src/host/{oboe => aaudio}/mod.rs (100%) diff --git a/src/host/oboe/android_media.rs b/src/host/aaudio/android_media.rs similarity index 100% rename from src/host/oboe/android_media.rs rename to src/host/aaudio/android_media.rs diff --git a/src/host/oboe/convert.rs b/src/host/aaudio/convert.rs similarity index 100% rename from src/host/oboe/convert.rs rename to src/host/aaudio/convert.rs diff --git a/src/host/oboe/java_interface.rs b/src/host/aaudio/java_interface.rs similarity index 100% rename from src/host/oboe/java_interface.rs rename to src/host/aaudio/java_interface.rs diff --git a/src/host/oboe/java_interface/audio_features.rs b/src/host/aaudio/java_interface/audio_features.rs similarity index 100% rename from src/host/oboe/java_interface/audio_features.rs rename to src/host/aaudio/java_interface/audio_features.rs diff --git a/src/host/oboe/java_interface/definitions.rs b/src/host/aaudio/java_interface/definitions.rs similarity index 100% rename from src/host/oboe/java_interface/definitions.rs rename to src/host/aaudio/java_interface/definitions.rs diff --git a/src/host/oboe/java_interface/devices_info.rs b/src/host/aaudio/java_interface/devices_info.rs similarity index 100% rename from src/host/oboe/java_interface/devices_info.rs rename to src/host/aaudio/java_interface/devices_info.rs diff --git a/src/host/oboe/java_interface/utils.rs b/src/host/aaudio/java_interface/utils.rs similarity index 100% rename from src/host/oboe/java_interface/utils.rs rename to src/host/aaudio/java_interface/utils.rs diff --git a/src/host/oboe/mod.rs b/src/host/aaudio/mod.rs similarity index 100% rename from src/host/oboe/mod.rs rename to src/host/aaudio/mod.rs diff --git a/src/host/mod.rs b/src/host/mod.rs index 8de06cbe0..21128bc3f 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -23,7 +23,7 @@ pub(crate) mod emscripten; pub(crate) mod jack; pub(crate) mod null; #[cfg(target_os = "android")] -pub(crate) mod oboe; +pub(crate) mod aaudio; #[cfg(windows)] pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fed4e08ac..60c839fa5 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -693,17 +693,17 @@ mod platform_impl { #[cfg(target_os = "android")] mod platform_impl { - pub use crate::host::oboe::{ - Device as OboeDevice, Devices as OboeDevices, Host as OboeHost, Stream as OboeStream, - SupportedInputConfigs as OboeSupportedInputConfigs, - SupportedOutputConfigs as OboeSupportedOutputConfigs, + pub use crate::host::aaudio::{ + Device as AAudioDevice, Devices as AAudioDevices, Host as AAudioHost, Stream as AAudioStream, + SupportedInputConfigs as AAudioSupportedInputConfigs, + SupportedOutputConfigs as AAudioSupportedOutputConfigs, }; - impl_platform_host!(Oboe oboe "Oboe"); + impl_platform_host!(AAudio aaudio "AAudio"); /// The default host for the current compilation target platform. pub fn default_host() -> Host { - OboeHost::new() + AAudioHost::new() .expect("the default host should always be available") .into() } From bcbce4d1736c8184b9f8d261254189356dfdebd0 Mon Sep 17 00:00:00 2001 From: mahdi Date: Thu, 13 Mar 2025 16:36:34 +0330 Subject: [PATCH 4/9] Clarify Android audio channel support and note AAudio support --- src/host/aaudio/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index bfcd6936c..c3e75ed24 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -25,8 +25,8 @@ mod java_interface; use self::android_media::{get_audio_record_min_buffer_size, get_audio_track_min_buffer_size}; use self::ndk::audio::AudioStream; -// Android Java API supports up to 8 channels, but oboe API -// only exposes mono and stereo. +// Android Java API supports up to 8 channels +// TODO: more channels available in native AAudio const CHANNEL_MASKS: [i32; 2] = [ android_media::CHANNEL_OUT_MONO, android_media::CHANNEL_OUT_STEREO, @@ -173,7 +173,8 @@ fn device_supported_configs( for channel_count in channel_counts { assert!(*channel_count > 0); if *channel_count > 2 { - // could be supported by the device, but oboe does not support more than 2 channels + // could be supported by the device + // TODO: more channels available in native AAudio continue; } let channel_mask = CHANNEL_MASKS[*channel_count as usize - 1]; @@ -384,8 +385,9 @@ impl DeviceTrait for Device { 1 => 1, 2 => 2, channels => { + // TODO: more channels available in native AAudio return Err(BackendSpecificError { - description: "More than 2 channels are not supported by Oboe.".to_owned(), + description: "More than 2 channels are not supported yet.".to_owned(), } .into()) } @@ -432,8 +434,9 @@ impl DeviceTrait for Device { 1 => 1, 2 => 2, channels => { + // TODO: more channels available in native AAudio return Err(BackendSpecificError { - description: "More than 2 channels are not supported by Oboe.".to_owned(), + description: "More than 2 channels are not supported yet.".to_owned(), } .into()) } From 766aa0be2370748e13fbbe8504cd84af3a56d63c Mon Sep 17 00:00:00 2001 From: mahdi Date: Fri, 14 Mar 2025 06:03:22 +0330 Subject: [PATCH 5/9] using newer build-tools --- .github/workflows/cpal.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index db0b51387..17f794667 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -223,7 +223,7 @@ jobs: targets: armv7-linux-androideabi,aarch64-linux-android,i686-linux-android,x86_64-linux-android - name: Set Up Android tools run: | - ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "platforms;android-30" + ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;30.0.2" "platforms;android-30" - name: Install Cargo APK run: cargo install cargo-apk - name: Build APK From caef17d9a6542ebef40fa444b241709ee15b6a98 Mon Sep 17 00:00:00 2001 From: mahdi Date: Fri, 14 Mar 2025 07:23:26 +0330 Subject: [PATCH 6/9] move android example to a dedicated crate --- Cargo.toml | 13 --------- examples/android/.gitignore | 6 +++++ examples/android/Cargo.toml | 30 +++++++++++++++++++++ examples/android/README.md | 14 ++++++++++ examples/{android.rs => android/src/lib.rs} | 0 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 examples/android/.gitignore create mode 100644 examples/android/Cargo.toml create mode 100644 examples/android/README.md rename examples/{android.rs => android/src/lib.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index b26d1a497..130e36edb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,6 @@ hound = "3.5" ringbuf = "0.4.1" clap = { version = "4.0", features = ["derive"] } -[target.'cfg(target_os = "android")'.dev-dependencies] -ndk-glue = "0.7" - [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.54.0", features = [ "Win32_Media_Audio", @@ -74,16 +71,6 @@ jni = "0.21" num-derive = "0.4" num-traits = "0.2" -[package.metadata.android] -min_sdk_version = 26 -target_sdk_version = 30 -max_sdk_version = 29 - -[[example]] -name = "android" -path = "examples/android.rs" -crate-type = ["cdylib"] - [[example]] name = "beep" diff --git a/examples/android/.gitignore b/examples/android/.gitignore new file mode 100644 index 000000000..f6bcf6957 --- /dev/null +++ b/examples/android/.gitignore @@ -0,0 +1,6 @@ +/target +/Cargo.lock +.cargo/ +.DS_Store +recorded.wav +rls*.log diff --git a/examples/android/Cargo.toml b/examples/android/Cargo.toml new file mode 100644 index 000000000..88923696f --- /dev/null +++ b/examples/android/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "android" +version = "0.1.0" +edition = "2024" + +[dependencies] +cpal = { path = "../../" } +anyhow = "1.0" +ndk-glue = "0.7" + +[lib] +name = "android" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[package.metadata.android] +# Specifies the package property of the manifest. +package = "com.foo.bar" + +# Specifies the array of targets to build for. +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android", "i686-linux-android", "x86_64-linux-android" ] + +# Name for final APK file. +# Defaults to package name. +apk_name = "myapp" + +[package.metadata.android.sdk] +min_sdk_version = 26 +target_sdk_version = 30 +max_sdk_version = 29 diff --git a/examples/android/README.md b/examples/android/README.md new file mode 100644 index 000000000..13b0b6d10 --- /dev/null +++ b/examples/android/README.md @@ -0,0 +1,14 @@ +## How to install + +```sh +rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android +``` + +## How to build apk + +```sh +# Builds the project in release mode and places it into a `apk` file. +cargo apk build --release +``` + +more information at: https://github.com/rust-mobile/cargo-apk \ No newline at end of file diff --git a/examples/android.rs b/examples/android/src/lib.rs similarity index 100% rename from examples/android.rs rename to examples/android/src/lib.rs From f1b3d9611519367c359082b7151fbbd6ee91c5fc Mon Sep 17 00:00:00 2001 From: mahdi Date: Fri, 14 Mar 2025 07:26:02 +0330 Subject: [PATCH 7/9] cargo fmt --all --- src/host/aaudio/mod.rs | 4 ++-- src/host/mod.rs | 4 ++-- src/platform/mod.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index c3e75ed24..4372c1dc1 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -389,7 +389,7 @@ impl DeviceTrait for Device { return Err(BackendSpecificError { description: "More than 2 channels are not supported yet.".to_owned(), } - .into()) + .into()); } }; @@ -438,7 +438,7 @@ impl DeviceTrait for Device { return Err(BackendSpecificError { description: "More than 2 channels are not supported yet.".to_owned(), } - .into()) + .into()); } }; diff --git a/src/host/mod.rs b/src/host/mod.rs index 21128bc3f..0c61a5910 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "android")] +pub(crate) mod aaudio; #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -22,8 +24,6 @@ pub(crate) mod emscripten; ))] pub(crate) mod jack; pub(crate) mod null; -#[cfg(target_os = "android")] -pub(crate) mod aaudio; #[cfg(windows)] pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 60c839fa5..6fc35f6f4 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -694,8 +694,8 @@ mod platform_impl { #[cfg(target_os = "android")] mod platform_impl { pub use crate::host::aaudio::{ - Device as AAudioDevice, Devices as AAudioDevices, Host as AAudioHost, Stream as AAudioStream, - SupportedInputConfigs as AAudioSupportedInputConfigs, + Device as AAudioDevice, Devices as AAudioDevices, Host as AAudioHost, + Stream as AAudioStream, SupportedInputConfigs as AAudioSupportedInputConfigs, SupportedOutputConfigs as AAudioSupportedOutputConfigs, }; From aea6c1ab03bb1a4253dac16372749a030e12b8bf Mon Sep 17 00:00:00 2001 From: mahdi Date: Fri, 14 Mar 2025 07:28:54 +0330 Subject: [PATCH 8/9] removing --features "oboe/fetch-prebuilt" from workflow --- .github/workflows/cpal.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index 17f794667..649b94f90 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -22,7 +22,7 @@ jobs: - name: Run clippy run: cargo clippy --all --all-features - name: Run clippy for Android target - run: cargo clippy --all --features asio --features oboe/fetch-prebuilt --target armv7-linux-androideabi + run: cargo clippy --all --features asio --target armv7-linux-androideabi rustfmt-check: runs-on: ubuntu-latest @@ -203,15 +203,15 @@ jobs: with: target: armv7-linux-androideabi - name: Check android - run: cargo check --example android --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose + run: cargo check --example android --target armv7-linux-androideabi --verbose - name: Check beep - run: cargo check --example beep --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose + run: cargo check --example beep --target armv7-linux-androideabi --verbose - name: Check enumerate - run: cargo check --example enumerate --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose + run: cargo check --example enumerate --target armv7-linux-androideabi --verbose - name: Check feedback - run: cargo check --example feedback --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose + run: cargo check --example feedback --target armv7-linux-androideabi --verbose - name: Check record_wav - run: cargo check --example record_wav --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose + run: cargo check --example record_wav --target armv7-linux-androideabi --verbose android-apk-build: runs-on: ubuntu-latest From 734fffb471908607fb0c13bca654591bf0379c12 Mon Sep 17 00:00:00 2001 From: mahdi Date: Fri, 14 Mar 2025 07:34:45 +0330 Subject: [PATCH 9/9] adapt workflow to check new android example --- .github/workflows/cpal.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index 649b94f90..a8130178f 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -203,7 +203,8 @@ jobs: with: target: armv7-linux-androideabi - name: Check android - run: cargo check --example android --target armv7-linux-androideabi --verbose + working-directory: examples/android + run: cargo check --target armv7-linux-androideabi --verbose - name: Check beep run: cargo check --example beep --target armv7-linux-androideabi --verbose - name: Check enumerate @@ -227,7 +228,8 @@ jobs: - name: Install Cargo APK run: cargo install cargo-apk - name: Build APK - run: cargo apk build --example android + working-directory: examples/android + run: cargo apk build ios-build: runs-on: macOS-latest