diff --git a/CMakeLists.txt b/CMakeLists.txt index ce04ab0..63c979e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 2.8.5) project(libsoundio C) set(CMAKE_MODULE_PATH ${libsoundio_SOURCE_DIR}/cmake ${CMAKE_MODULE_PATH}) +include(CheckCCompilerFlag) if(CMAKE_VERSION VERSION_LESS 3.0.0) set(CMAKE_INSTALL_LIBDIR "lib" CACHE PATH "library install dir (lib)") @@ -36,6 +37,7 @@ option(ENABLE_PULSEAUDIO "Enable PulseAudio backend" ON) option(ENABLE_ALSA "Enable ALSA backend" ON) option(ENABLE_COREAUDIO "Enable CoreAudio backend" ON) option(ENABLE_WASAPI "Enable WASAPI backend" ON) +option(ENABLE_ANDROID "Enable Android OpenSL ES backend" ON) find_package(Threads) if(Threads_FOUND) @@ -138,6 +140,21 @@ else() set(SOUNDIO_HAVE_WASAPI false) endif() +if(ENABLE_ANDROID) + find_package(AndroidOpenSLES) + if(ANDROID_OPENSLES_FOUND) + set(STATUS_ANDROID "OK") + set(SOUNDIO_HAVE_ANDROID true) + else() + set(STATUS_ANDROID "not found") + set(SOUNDIO_HAVE_ANDROID false) + set(ANDROID_OPENSLES_LIBRARY "") + endif() +else() + set(STATUS_ANDROID "disabled") + set(SOUNDIO_HAVE_ANDROID false) + set(ANDROID_OPENSLES_LIBRARY "") +endif() set(LIBSOUNDIO_SOURCES "${libsoundio_SOURCE_DIR}/src/soundio.c" @@ -179,6 +196,11 @@ if(SOUNDIO_HAVE_WASAPI) "${libsoundio_SOURCE_DIR}/src/wasapi.c" ) endif() +if(SOUNDIO_HAVE_ANDROID) + set(LIBSOUNDIO_SOURCES ${LIBSOUNDIO_SOURCES} + "${libsoundio_SOURCE_DIR}/src/android.c" + ) +endif() include_directories( ${libsoundio_SOURCE_DIR} @@ -195,6 +217,7 @@ set(LIBSOUNDIO_LIBS ${COREFOUNDATION_LIBRARY} ${AUDIOUNIT_LIBRARY} ${CMAKE_THREAD_LIBS_INIT} + ${ANDROID_OPENSLES_LIBRARY} ) if(MSVC) @@ -208,8 +231,13 @@ else() set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Werror -pedantic") set(LIB_CFLAGS "-std=c11 -fvisibility=hidden -Wall -Werror=strict-prototypes -Werror=old-style-definition -Werror=missing-prototypes -D_REENTRANT -D_POSIX_C_SOURCE=200809L -Wno-missing-braces") set(EXAMPLE_CFLAGS "-std=c99 -Wall") - set(TEST_CFLAGS "${LIB_CFLAGS} -fprofile-arcs -ftest-coverage") - set(TEST_LDFLAGS "-fprofile-arcs -ftest-coverage") + + set(PROFILING_FLAGS "-fprofile-arcs -ftest-coverage") + check_c_compiler_flag("${PROFILING_FLAGS}" PROFILING_FLAGS_SUPPORTED) + if(PROFILING_FLAGS_SUPPORTED) + set(TEST_CFLAGS "${LIB_CFLAGS} ${PROFILING_FLAGS}") + set(TEST_LDFLAGS "${PROFILING_FLAGS}") + endif() set(LIBM "m") endif() @@ -229,7 +257,7 @@ if(BUILD_DYNAMIC_LIBS) OUTPUT_NAME soundio SOVERSION ${LIBSOUNDIO_VERSION_MAJOR} VERSION ${LIBSOUNDIO_VERSION} - COMPILE_FLAGS ${LIB_CFLAGS} + COMPILE_FLAGS "${LIB_CFLAGS}" LINKER_LANGUAGE C ) target_link_libraries(libsoundio_shared LINK_PUBLIC ${LIBSOUNDIO_LIBS}) @@ -240,7 +268,7 @@ if(BUILD_STATIC_LIBS) add_library(libsoundio_static STATIC ${LIBSOUNDIO_SOURCES}) set_target_properties(libsoundio_static PROPERTIES OUTPUT_NAME ${SOUNDIO_STATIC_LIBNAME} - COMPILE_FLAGS ${LIB_CFLAGS} + COMPILE_FLAGS "${LIB_CFLAGS}" LINKER_LANGUAGE C ) install(TARGETS libsoundio_static DESTINATION ${CMAKE_INSTALL_LIBDIR}) @@ -303,21 +331,21 @@ if(BUILD_TESTS) target_link_libraries(unit_tests LINK_PUBLIC ${LIBSOUNDIO_LIBS}) set_target_properties(unit_tests PROPERTIES LINKER_LANGUAGE C - COMPILE_FLAGS ${TEST_CFLAGS} - LINK_FLAGS ${TEST_LDFLAGS} + COMPILE_FLAGS "${TEST_CFLAGS}" + LINK_FLAGS "${TEST_LDFLAGS}" ) add_executable(latency "${libsoundio_SOURCE_DIR}/test/latency.c" ${LIBSOUNDIO_SOURCES}) target_link_libraries(latency LINK_PUBLIC ${LIBSOUNDIO_LIBS} ${LIBM}) set_target_properties(latency PROPERTIES LINKER_LANGUAGE C - COMPILE_FLAGS ${LIB_CFLAGS} + COMPILE_FLAGS "${LIB_CFLAGS}" ) add_executable(underflow test/underflow.c) set_target_properties(underflow PROPERTIES LINKER_LANGUAGE C - COMPILE_FLAGS ${EXAMPLE_CFLAGS}) + COMPILE_FLAGS "${EXAMPLE_CFLAGS}") if(BUILD_DYNAMIC_LIBS) target_link_libraries(underflow libsoundio_shared ${LIBM}) else() @@ -380,9 +408,10 @@ message( "System Dependencies\n" "-------------------\n" "* threads : ${STATUS_THREADS}\n" - "* JACK (optional) : ${STATUS_JACK}\n" - "* PulseAudio (optional) : ${STATUS_PULSEAUDIO}\n" - "* ALSA (optional) : ${STATUS_ALSA}\n" - "* CoreAudio (optional) : ${STATUS_COREAUDIO}\n" - "* WASAPI (optional) : ${STATUS_WASAPI}\n" + "* JACK (optional) : ${STATUS_JACK}\n" + "* PulseAudio (optional) : ${STATUS_PULSEAUDIO}\n" + "* ALSA (optional) : ${STATUS_ALSA}\n" + "* CoreAudio (optional) : ${STATUS_COREAUDIO}\n" + "* WASAPI (optional) : ${STATUS_WASAPI}\n" + "* Android OpenSL ES (optional) : ${STATUS_ANDROID}\n" ) diff --git a/README.md b/README.md index e08f3cc..d940742 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,24 @@ cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/mxe/usr/x86_64-w64-mingw32.static/share make ``` +### Building for Android + +Install the dependencies: + + * cmake + * Android NDK + +The Android NDK provides a CMake toolchain file for cross-compilation starting in version r13. Run the following, where "ANDROID_NDK_HOME" is the root folder of the NDK: + +``` +mkdir build +cd build +cmake .. -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake" -DANDROID_PLATFORM=android-21 +make +``` + +If you are compiling for an emulator, you probably also want to add the CMake variable `-DANDROID_ARCH=x86`. Further variables are documented in the toolchain file. + ### Testing For each backend, do the following: @@ -254,6 +272,12 @@ For each backend, do the following: 0. Run `./latency` and make sure the printed beeps line up with the beeps that you hear. +### Testing for Android + +Currently when run from the command-line, input streams always fail at +`soundio_instream_open` due to the Android permissions model. Input devices will +only work in full-featured Android apps. + ### Building the Documentation Ensure that [doxygen](http://www.stack.nl/~dimitri/doxygen/) is installed, diff --git a/cmake/FindAndroidOpenSLES.cmake b/cmake/FindAndroidOpenSLES.cmake new file mode 100644 index 0000000..22e406b --- /dev/null +++ b/cmake/FindAndroidOpenSLES.cmake @@ -0,0 +1,16 @@ +# Copyright (c) 2016 libsoundio +# This file is MIT licensed. +# See http://opensource.org/licenses/MIT + +# ANDROID_OPENSLES_FOUND +# ANDROID_OPENSLES_INCLUDE_DIR +# ANDROID_OPENSLES_LIBRARY + +find_path(ANDROID_OPENSLES_INCLUDE_DIR NAMES SLES/OpenSLES_Android.h) + +find_library(ANDROID_OPENSLES_LIBRARY NAMES OpenSLES) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(ANDROID_OPENSLES DEFAULT_MSG ANDROID_OPENSLES_LIBRARY ANDROID_OPENSLES_INCLUDE_DIR) + +mark_as_advanced(ANDROID_OPENSLES_INCLUDE_DIR ANDROID_OPENSLES_LIBRARY) diff --git a/soundio/soundio.h b/soundio/soundio.h index a958fc4..43af008 100644 --- a/soundio/soundio.h +++ b/soundio/soundio.h @@ -223,6 +223,7 @@ enum SoundIoBackend { SoundIoBackendAlsa, SoundIoBackendCoreAudio, SoundIoBackendWasapi, + SoundIoBackendAndroid }; enum SoundIoDeviceAim { diff --git a/src/android.c b/src/android.c new file mode 100644 index 0000000..73da69f --- /dev/null +++ b/src/android.c @@ -0,0 +1,786 @@ +/* + * Copyright (c) 2016 libsoundio + * + * This file is part of libsoundio, which is MIT licensed. + * See http://opensource.org/licenses/MIT + */ + +#include "android.h" +#include "soundio_private.h" +#include "util.h" + +static void destroy_android(struct SoundIoPrivate *si) { + struct SoundIoAndroid *sia = &si->backend_data.android; + + if (sia->cond) { + soundio_os_cond_destroy(sia->cond); + sia->cond = NULL; + } + + if (sia->engineObject) { + (*sia->engineObject)->Destroy(sia->engineObject); + sia->engineObject = NULL; + } +} + +static void flush_events_android(struct SoundIoPrivate *si) { + struct SoundIo *soundio = &si->pub; + struct SoundIoAndroid *sia = &si->backend_data.android; + if (sia->devices_emitted) + return; + sia->devices_emitted = true; + soundio->on_devices_change(soundio); +} + +static void wait_events_android(struct SoundIoPrivate *si) { + struct SoundIoAndroid *sia = &si->backend_data.android; + flush_events_android(si); + soundio_os_cond_wait(sia->cond, NULL); +} + +static void wakeup_android(struct SoundIoPrivate *si) { + struct SoundIoAndroid *sia = &si->backend_data.android; + soundio_os_cond_signal(sia->cond, NULL); +} + +static void force_device_scan_android(struct SoundIoPrivate *si) { + // nothing to do; Android devices never change +} + +static SLAndroidDataFormat_PCM_EX build_format_pcm_android(enum SoundIoFormat format, + int channel_count, int sample_rate) +{ + // SLAndroidDataFormat_PCM_EX is backwards compatible with the deprecated + // SLDataFormat_PCM, but uses a different formatType. This new formatType + // is supported for output starting at API level 21, and for input starting + // at API level 23. It's only required for floating-point data, so for U8 and + // S16 data we use the old way. + SLAndroidDataFormat_PCM_EX format_pcm = { + .formatType = format == SoundIoFormatFloat32LE ? + SL_ANDROID_DATAFORMAT_PCM_EX : SL_DATAFORMAT_PCM, + .numChannels = channel_count, + .sampleRate = sample_rate * 1000, + .bitsPerSample = soundio_get_bytes_per_sample(format) * 8, + .containerSize = soundio_get_bytes_per_sample(format) * 8, + .channelMask = channel_count == 2 ? + SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT : + SL_SPEAKER_FRONT_CENTER, + .endianness = SL_BYTEORDER_LITTLEENDIAN, + + // Only the float representation matters, since otherwise we use SL_DATAFORMAT_PCM, + // but it's nice for correctness. + .representation = format == SoundIoFormatFloat32LE ? + SL_ANDROID_PCM_REPRESENTATION_FLOAT : + format == SoundIoFormatS16LE ? + SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT : + SL_ANDROID_PCM_REPRESENTATION_UNSIGNED_INT + }; + return format_pcm; +} + +static void write_callback_android(SLAndroidSimpleBufferQueueItf bq, void* context) { + struct SoundIoOutStreamPrivate *os = context; + struct SoundIoOutStreamAndroid *osa = &os->backend_data.android; + struct SoundIoOutStream *outstream = &os->pub; + + osa->write_frame_count = 0; + outstream->write_callback(outstream, 1, osa->bytes_per_buffer / + outstream->bytes_per_frame); + + // Sometimes write callbacks return without writing to any buffers. They're + // supposed to honor min_frames, but if they don't, give them a more informative + // error message and keep the stream alive. + if (osa->write_frame_count == 0) { + outstream->error_callback(outstream, SoundIoErrorInvalid); + osa->write_frame_count = 1; + osa->buffers[osa->curBuffer][0] = 0; + } + + if (SL_RESULT_SUCCESS != (*osa->playerBufferQueue)->Enqueue(osa->playerBufferQueue, + osa->buffers[osa->curBuffer], osa->write_frame_count * outstream->bytes_per_frame)) + { + outstream->error_callback(outstream, SoundIoErrorStreaming); + } + + osa->curBuffer ^= 1; +} + +static void outstream_destroy_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os) +{ + struct SoundIoOutStreamAndroid *osa = &os->backend_data.android; + + if (osa->playerObject) + (*osa->playerObject)->Destroy(osa->playerObject); + + if (osa->outputMixObject) + (*osa->outputMixObject)->Destroy(osa->outputMixObject); + + if (osa->buffers[0]) + free(osa->buffers[0]); +} + +static enum SoundIoError outstream_open_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os) +{ + struct SoundIoOutStreamAndroid *osa = &os->backend_data.android; + struct SoundIoAndroid *sia = &si->backend_data.android; + struct SoundIoOutStream *outstream = &os->pub; + struct SoundIoDevice *device = outstream->device; + + if (outstream->software_latency == 0.0) { + outstream->software_latency = soundio_double_clamp( + device->software_latency_min, 1.0, device->software_latency_max); + } + + if (SL_RESULT_SUCCESS != (*sia->engineEngine)->CreateOutputMix(sia->engineEngine, + &osa->outputMixObject, 0, 0, 0)) + { + return SoundIoErrorOpeningDevice; + } + + if(SL_RESULT_SUCCESS != (*osa->outputMixObject)->Realize(osa->outputMixObject, + SL_BOOLEAN_FALSE)) + { + outstream_destroy_android(si, os); + return SoundIoErrorOpeningDevice; + } + + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + // TODO: The minimum buffer count is 1. When would you want to have + // a single buffer? + 2 + }; + + SLAndroidDataFormat_PCM_EX format_pcm = build_format_pcm_android( + outstream->format, + outstream->layout.channel_count, + outstream->sample_rate + ); + + SLDataSource audioSrc = {&loc_bufq, &format_pcm}; + SLDataLocator_OutputMix loc_outmix = { + SL_DATALOCATOR_OUTPUTMIX, + osa->outputMixObject + }; + SLDataSink audioSnk = {&loc_outmix, NULL}; + SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE}; + const SLboolean req[1] = {SL_BOOLEAN_TRUE}; + + if (SL_RESULT_SUCCESS != (*sia->engineEngine)->CreateAudioPlayer(sia->engineEngine, + &osa->playerObject, &audioSrc, &audioSnk, 1, ids, req)) + { + outstream_destroy_android(si, os); + return SoundIoErrorOpeningDevice; + } + if (SL_RESULT_SUCCESS != (*osa->playerObject)->Realize(osa->playerObject, + SL_BOOLEAN_FALSE)) + { + outstream_destroy_android(si, os); + return SoundIoErrorOpeningDevice; + } + + if (SL_RESULT_SUCCESS != (*osa->playerObject)->GetInterface(osa->playerObject, + SL_IID_PLAY, &osa->playerPlay)) + { + outstream_destroy_android(si, os); + return SoundIoErrorOpeningDevice; + } + + if (SL_RESULT_SUCCESS != (*osa->playerObject)->GetInterface(osa->playerObject, + SL_IID_BUFFERQUEUE, &osa->playerBufferQueue)) + { + outstream_destroy_android(si, os); + return SoundIoErrorOpeningDevice; + } + if (SL_RESULT_SUCCESS != (*osa->playerBufferQueue)->RegisterCallback( + osa->playerBufferQueue, write_callback_android, os)) + { + outstream_destroy_android(si, os); + return SoundIoErrorOpeningDevice; + } + + // TODO: An audio stream's latency and jitter can be reduced by using the + // native buffer size. Unfortunately, there is no officially supported way + // to get the native buffer size without access to a JVM. + osa->bytes_per_buffer = outstream->bytes_per_frame * outstream->sample_rate + * outstream->software_latency / 2; + size_t samples_per_buffer = osa->bytes_per_buffer / outstream->bytes_per_sample; + osa->buffers[0] = calloc(samples_per_buffer * 2, outstream->bytes_per_sample); + if (!osa->buffers[0]) { + outstream_destroy_android(si, os); + return SoundIoErrorNoMem; + } + osa->buffers[1] = &osa->buffers[0][osa->bytes_per_buffer]; + + return 0; +} + +static enum SoundIoError outstream_pause_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os, bool pause) +{ + struct SoundIoOutStreamAndroid *osa = &os->backend_data.android; + + if (SL_RESULT_SUCCESS != (*osa->playerPlay)->SetPlayState(osa->playerPlay, + pause ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING)) + { + return SoundIoErrorInvalid; + } + + return 0; +} + +static enum SoundIoError outstream_start_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os) +{ + struct SoundIoOutStreamAndroid *osa = &os->backend_data.android; + + if (SL_RESULT_SUCCESS != (*osa->playerPlay)->SetPlayState(osa->playerPlay, + SL_PLAYSTATE_PLAYING)) + { + return SoundIoErrorInvalid; + } + write_callback_android(osa->playerBufferQueue, os); + write_callback_android(osa->playerBufferQueue, os); + + return 0; +} + +static enum SoundIoError outstream_begin_write_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os, struct SoundIoChannelArea **out_areas, + int *frame_count) +{ + struct SoundIoOutStream *outstream = &os->pub; + struct SoundIoOutStreamAndroid *osa = &os->backend_data.android; + + if (*frame_count > osa->bytes_per_buffer / outstream->bytes_per_frame) + return SoundIoErrorInvalid; + + char *write_ptr = osa->buffers[osa->curBuffer]; + for (int ch = 0; ch < outstream->layout.channel_count; ch += 1) { + osa->areas[ch].ptr = write_ptr + outstream->bytes_per_sample * ch; + osa->areas[ch].step = outstream->bytes_per_frame; + } + osa->write_frame_count = *frame_count; + *out_areas = osa->areas; + + return 0; +} + +static enum SoundIoError outstream_end_write_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os) +{ + return 0; +} + +static enum SoundIoError outstream_clear_buffer_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os) +{ + // TODO: This might be supported, actually. + return SoundIoErrorIncompatibleBackend; +} + +static enum SoundIoError outstream_get_latency_android(struct SoundIoPrivate *si, + struct SoundIoOutStreamPrivate *os, double *out_latency) +{ + struct SoundIoOutStream *outstream = &os->pub; + *out_latency = outstream->software_latency; + return 0; +} + +static void instream_destroy_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is) +{ + struct SoundIoInStreamAndroid *isa = &is->backend_data.android; + + if (isa->audioRecorderObject) + (*isa->audioRecorderObject)->Destroy(isa->audioRecorderObject); + + if (isa->buffers[0]) + free(isa->buffers[0]); +} + +static void read_callback_android(SLAndroidSimpleBufferQueueItf bq, void* context) { + struct SoundIoInStreamPrivate *is = context; + struct SoundIoInStreamAndroid *isa = &is->backend_data.android; + struct SoundIoInStream *instream = &is->pub; + + int frame_count = isa->bytes_per_buffer / instream->bytes_per_frame; + instream->read_callback(instream, frame_count, frame_count); + + if (SL_RESULT_SUCCESS != (*isa->recorderBufferQueue)->Enqueue( + isa->recorderBufferQueue, isa->buffers[isa->curBuffer], isa->bytes_per_buffer)) + { + instream->error_callback(instream, SoundIoErrorStreaming); + return; + } + isa->curBuffer ^= 1; +} + +static enum SoundIoError instream_open_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is) +{ + struct SoundIoInStreamAndroid *isa = &is->backend_data.android; + struct SoundIoAndroid *sia = &si->backend_data.android; + struct SoundIoInStream *instream = &is->pub; + struct SoundIoDevice *device = instream->device; + + if (instream->software_latency == 0.0) { + instream->software_latency = soundio_double_clamp( + device->software_latency_min, 1.0, device->software_latency_max); + } + + SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, + SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL}; + SLDataSource audioSrc = {&loc_dev, NULL}; + + SLDataLocator_AndroidSimpleBufferQueue loc_bq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + // TODO: The minimum buffer count is 1. When would you want to have + // a single buffer? + 2 + }; + SLAndroidDataFormat_PCM_EX format_pcm = build_format_pcm_android( + instream->format, + instream->layout.channel_count, + instream->sample_rate + ); + SLDataSink audioSnk = {&loc_bq, &format_pcm}; + + const SLInterfaceID ids[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; + const SLboolean reqs[1] = {SL_BOOLEAN_TRUE}; + + if (SL_RESULT_SUCCESS != (*sia->engineEngine)->CreateAudioRecorder( + sia->engineEngine, &isa->audioRecorderObject, &audioSrc, &audioSnk, 1, ids, reqs)) + { + instream_destroy_android(si, is); + return SoundIoErrorOpeningDevice; + } + + if (SL_RESULT_SUCCESS != (*isa->audioRecorderObject)->Realize(isa->audioRecorderObject, + SL_BOOLEAN_FALSE)) + { + // TODO: On an LG G4 running Android 6.0 / API 23, the error code is + // SL_RESULT_CONTENT_UNSUPPORTED when a process does not have permission + // to record. Are there any other situations where this error code is seen? + instream_destroy_android(si, is); + return SoundIoErrorOpeningDevice; + } + + if(SL_RESULT_SUCCESS != (*isa->audioRecorderObject)->GetInterface( + isa->audioRecorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &isa->recorderBufferQueue)) + { + instream_destroy_android(si, is); + return SoundIoErrorOpeningDevice; + } + + if(SL_RESULT_SUCCESS != (*isa->recorderBufferQueue)->RegisterCallback( + isa->recorderBufferQueue, read_callback_android, is)) + { + instream_destroy_android(si, is); + return SoundIoErrorOpeningDevice; + } + + if (SL_RESULT_SUCCESS != (*isa->audioRecorderObject)->GetInterface(isa->audioRecorderObject, + SL_IID_RECORD, &isa->recorderRecord)) { + instream_destroy_android(si, is); + return SoundIoErrorOpeningDevice; + } + + // TODO: The documentation seems to imply that the optimal output buffer size + // is also the optimal input buffer size. See the documentation on SoundIoOutStreamAndroid + // for more information. + isa->bytes_per_buffer = instream->bytes_per_frame * instream->sample_rate + * instream->software_latency / 2; + size_t samples_per_buffer = isa->bytes_per_buffer / instream->bytes_per_sample; + isa->buffers[0] = calloc(samples_per_buffer * 2, instream->bytes_per_sample); + if (!isa->buffers[0]) { + instream_destroy_android(si, is); + return SoundIoErrorNoMem; + } + isa->buffers[1] = &isa->buffers[0][isa->bytes_per_buffer]; + + return 0; +} + +static enum SoundIoError instream_pause_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is, bool pause) +{ + struct SoundIoInStreamAndroid *isa = &is->backend_data.android; + + if (SL_RESULT_SUCCESS != (*isa->recorderRecord)->SetRecordState(isa->recorderRecord, + pause ? SL_RECORDSTATE_PAUSED : SL_RECORDSTATE_RECORDING)) + { + return SoundIoErrorInvalid; + } + + return 0; +} + +static enum SoundIoError instream_start_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is) +{ + struct SoundIoInStreamAndroid *isa = &is->backend_data.android; + + if (SL_RESULT_SUCCESS != (*isa->recorderRecord)->SetRecordState(isa->recorderRecord, + SL_RECORDSTATE_RECORDING)) + { + return SoundIoErrorInvalid; + } + + if (SL_RESULT_SUCCESS != (*isa->recorderBufferQueue)->Enqueue( + isa->recorderBufferQueue, isa->buffers[isa->curBuffer], isa->bytes_per_buffer)) + { + return SoundIoErrorOpeningDevice; + } + if (SL_RESULT_SUCCESS != (*isa->recorderBufferQueue)->Enqueue( + isa->recorderBufferQueue, isa->buffers[isa->curBuffer ^ 1], isa->bytes_per_buffer)) + { + return SoundIoErrorOpeningDevice; + } + + return 0; +} + +static enum SoundIoError instream_begin_read_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is, struct SoundIoChannelArea **out_areas, + int *frame_count) +{ + struct SoundIoInStream *instream = &is->pub; + struct SoundIoInStreamAndroid *isa = &is->backend_data.android; + + if (*frame_count != isa->bytes_per_buffer / instream->bytes_per_frame) + return SoundIoErrorInvalid; + + char *read_ptr = isa->buffers[isa->curBuffer]; + for (int ch = 0; ch < instream->layout.channel_count; ch += 1) { + isa->areas[ch].ptr = read_ptr + instream->bytes_per_sample * ch; + isa->areas[ch].step = instream->bytes_per_frame; + } + *out_areas = isa->areas; + + return 0; +} + +static enum SoundIoError instream_end_read_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is) +{ + return 0; +} + +static enum SoundIoError instream_get_latency_android(struct SoundIoPrivate *si, + struct SoundIoInStreamPrivate *is, double *out_latency) +{ + struct SoundIoInStream *instream = &is->pub; + *out_latency = instream->software_latency; + return 0; +} + +static enum SoundIoError check_input_floating_point_support(struct SoundIoPrivate *si) { + struct SoundIoAndroid *sia = &si->backend_data.android; + + SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, NULL}; + SLDataSource audioSrc = {&loc_dev, NULL}; + + SLDataLocator_AndroidSimpleBufferQueue loc_bq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; + + SLAndroidDataFormat_PCM_EX format_pcm = build_format_pcm_android( + SoundIoFormatFloat32LE, + // TODO: do the rest of these parameters always work? + 2, // channel count + 48000 // sample rate + ); + SLDataSink audioSnk = {&loc_bq, &format_pcm}; + + const SLInterfaceID ids[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; + const SLboolean reqs[1] = {SL_BOOLEAN_TRUE}; + + SLObjectItf audioRecorderObject; + SLuint32 status = (*sia->engineEngine)->CreateAudioRecorder( + sia->engineEngine, &audioRecorderObject, &audioSrc, &audioSnk, 1, ids, reqs); + if (audioRecorderObject) + (*audioRecorderObject)->Destroy(audioRecorderObject); + + if (SL_RESULT_SUCCESS == status) + return SoundIoErrorNone; + else if (SL_RESULT_PARAMETER_INVALID == status) + return SoundIoErrorIncompatibleDevice; + else + return SoundIoErrorOpeningDevice; +} + +enum SoundIoError soundio_android_init(struct SoundIoPrivate *si) { + struct SoundIo *soundio = &si->pub; + struct SoundIoAndroid *sia = &si->backend_data.android; + + if (SL_RESULT_SUCCESS != slCreateEngine(&sia->engineObject, 0, NULL, 0, NULL, + NULL)) + { + return SoundIoErrorInitAudioBackend; + } + + if (SL_RESULT_SUCCESS != (*sia->engineObject)->Realize(sia->engineObject, + SL_BOOLEAN_FALSE)) + { + destroy_android(si); + return SoundIoErrorInitAudioBackend; + } + + if (SL_RESULT_SUCCESS != (*sia->engineObject)->GetInterface(sia->engineObject, + SL_IID_ENGINE, &sia->engineEngine)) + { + destroy_android(si); + return SoundIoErrorInitAudioBackend; + } + + sia->cond = soundio_os_cond_create(); + if (!sia->cond) { + destroy_android(si); + return SoundIoErrorNoMem; + } + + si->safe_devices_info = ALLOCATE(struct SoundIoDevicesInfo, 1); + if (!si->safe_devices_info) { + destroy_android(si); + return SoundIoErrorNoMem; + } + + si->destroy = destroy_android; + si->flush_events = flush_events_android; + si->wait_events = wait_events_android; + si->wakeup = wakeup_android; + si->force_device_scan = force_device_scan_android; + + si->outstream_open = outstream_open_android; + si->outstream_destroy = outstream_destroy_android; + si->outstream_start = outstream_start_android; + si->outstream_begin_write = outstream_begin_write_android; + si->outstream_end_write = outstream_end_write_android; + si->outstream_clear_buffer = outstream_clear_buffer_android; + si->outstream_pause = outstream_pause_android; + si->outstream_get_latency = outstream_get_latency_android; + + si->instream_open = instream_open_android; + si->instream_destroy = instream_destroy_android; + si->instream_start = instream_start_android; + si->instream_begin_read = instream_begin_read_android; + si->instream_end_read = instream_end_read_android; + si->instream_pause = instream_pause_android; + si->instream_get_latency = instream_get_latency_android; + + // create output device + si->safe_devices_info->default_output_index = 0; + { + struct SoundIoDevicePrivate *dev = ALLOCATE(struct SoundIoDevicePrivate, 1); + if (!dev) { + destroy_android(si); + return SoundIoErrorNoMem; + } + struct SoundIoDevice *device = &dev->pub; + + device->ref_count = 1; + device->aim = SoundIoDeviceAimOutput; + device->soundio = soundio; + device->id = strdup("android-out"); + device->name = strdup("Android Output"); + if (!device->id || !device->name) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + + // The following information is according to: + // https://developer.android.com/ndk/guides/audio/opensl-for-android.html + + // TODO: could be prealloc'd? + device->layout_count = 2; + device->layouts = ALLOCATE(struct SoundIoChannelLayout, device->layout_count); + if (!device->layouts) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + device->layouts[0] = + *soundio_channel_layout_get_builtin(SoundIoChannelLayoutIdMono); + device->layouts[1] = + *soundio_channel_layout_get_builtin(SoundIoChannelLayoutIdStereo); + + // Might support additional formats as well? But there's no way to query + // for them. + // TODO: Could be prealloc'd? + device->format_count = 3; + device->formats = ALLOCATE(enum SoundIoFormat, device->format_count); + if (!device->formats) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + device->formats[0] = SoundIoFormatU8; + device->formats[1] = SoundIoFormatS16LE; + // Floating-point was added in API level 21 / Android 5.0 Lollipop + device->formats[2] = SoundIoFormatFloat32LE; + + // TODO: Could be prealloc'd? + device->sample_rate_count = 9; + device->sample_rates = ALLOCATE(struct SoundIoSampleRateRange, + device->sample_rate_count); + if (!device->sample_rates) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + device->sample_rates[0].min = device->sample_rates[0].max = 8000; + device->sample_rates[1].min = device->sample_rates[1].max = 11025; + device->sample_rates[2].min = device->sample_rates[2].max = 12000; + device->sample_rates[3].min = device->sample_rates[3].max = 16000; + device->sample_rates[4].min = device->sample_rates[4].max = 22050; + device->sample_rates[5].min = device->sample_rates[5].max = 24000; + device->sample_rates[6].min = device->sample_rates[6].max = 32000; + device->sample_rates[7].min = device->sample_rates[7].max = 44100; + device->sample_rates[8].min = device->sample_rates[8].max = 48000; + + // TODO: An audio stream's latency can be greatly reduced by using the + // native sampling rate (and thus bypassing software remixing in AudioFlinger + // (a.k.a. the "fast track")). Unfortunately, there is no officially + // supported way to get the native sampling rate without access to a JVM. + device->sample_rate_current = 48000; + + // In general, Android makes no guarantees about audio latency. There + // are hardware feature flags that do make guarantees: + // - android.hardware.audio.low_latency: output latency of <=45ms + // - android.hardware.audio.pro: round-trip latency of <=20ms + // There is currently no way to check these flags without access to a + // JVM. The following number is a very optimistic lower-bound. + device->software_latency_min = 0.01; + + // TODO: These numbers are completely made up + device->software_latency_current = 0.1; + device->software_latency_max = 4.0; + + if (SoundIoListDevicePtr_append(&si->safe_devices_info->output_devices, + device)) + { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + } + + // create input device + // TODO: Should an input device still be created if the application doesn't + // have permission to access recording devices? + // TODO: Additional recording profiles, such as CAMCORDER and VOICE_RECOGNITION, + // are described in . Should these be + // exposed? How? + si->safe_devices_info->default_input_index = 0; + { + struct SoundIoDevicePrivate *dev = ALLOCATE(struct SoundIoDevicePrivate, 1); + if (!dev) { + destroy_android(si); + return SoundIoErrorNoMem; + } + struct SoundIoDevice *device = &dev->pub; + + device->ref_count = 1; + device->aim = SoundIoDeviceAimInput; + device->soundio = soundio; + device->id = strdup("android-in"); + device->name = strdup("Android Generic Input"); + if (!device->id || !device->name) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + + // TODO: Is there a case where these don't work? + // TODO: could be prealloc'd? + device->layout_count = 2; + device->layouts = ALLOCATE(struct SoundIoChannelLayout, device->layout_count); + if (!device->layouts) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + device->layouts[0] = + *soundio_channel_layout_get_builtin(SoundIoChannelLayoutIdMono); + device->layouts[1] = + *soundio_channel_layout_get_builtin(SoundIoChannelLayoutIdStereo); + + // TODO: Might support additional formats as well? + // TODO: Could be prealloc'd? + device->format_count = 3; + device->formats = ALLOCATE(enum SoundIoFormat, device->format_count); + if (!device->formats) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + device->formats[0] = SoundIoFormatU8; + device->formats[1] = SoundIoFormatS16LE; + { + // Floating-point was added in API level 23 / Android M + enum SoundIoError err = check_input_floating_point_support(si); + if (SoundIoErrorNone == err) { + device->formats[2] = SoundIoFormatFloat32LE; + } else if (SoundIoErrorIncompatibleDevice == err) { + device->format_count -= 1; + } else { + soundio_device_unref(device); + destroy_android(si); + return err; + } + } + + // TODO: Could be prealloc'd? + // TODO: Supposedly, not all devices support all sample rates. This has + // only been tested on an LG G4 with Android 6.0 / API level 23, where + // all sample rates seem to be supported. + device->sample_rate_count = 9; + device->sample_rates = ALLOCATE(struct SoundIoSampleRateRange, + device->sample_rate_count); + if (!device->sample_rates) { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + device->sample_rates[0].min = device->sample_rates[0].max = 8000; + device->sample_rates[1].min = device->sample_rates[1].max = 11025; + device->sample_rates[2].min = device->sample_rates[2].max = 12000; + device->sample_rates[3].min = device->sample_rates[3].max = 16000; + device->sample_rates[4].min = device->sample_rates[4].max = 22050; + device->sample_rates[5].min = device->sample_rates[5].max = 24000; + device->sample_rates[6].min = device->sample_rates[6].max = 32000; + device->sample_rates[7].min = device->sample_rates[7].max = 44100; + device->sample_rates[8].min = device->sample_rates[8].max = 48000; + + // TODO: The documentation seems to imply that the optimal output sample + // rate is also the optimal input sample rate. See the documentation on + // SoundIoOutStreamAndroid for more information. + device->sample_rate_current = 48000; + + // In general, Android makes no guarantees about audio latency. There + // are hardware feature flags that do make guarantees: + // - android.hardware.audio.low_latency: output latency of <=45ms + // - android.hardware.audio.pro: round-trip latency of <=20ms + // There is currently no way to check these flags without access to a + // JVM. The following number is a very optimistic lower-bound. + device->software_latency_min = 0.01; + + // TODO: These numbers are completely made up + device->software_latency_current = 0.1; + device->software_latency_max = 4.0; + + if (SoundIoListDevicePtr_append(&si->safe_devices_info->input_devices, + device)) + { + soundio_device_unref(device); + destroy_android(si); + return SoundIoErrorNoMem; + } + } + + return SoundIoErrorNone; +} diff --git a/src/android.h b/src/android.h new file mode 100644 index 0000000..5bb910e --- /dev/null +++ b/src/android.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016 libsoundio + * + * This file is part of libsoundio, which is MIT licensed. + * See http://opensource.org/licenses/MIT + */ + +#ifndef SOUNDIO_ANDROID_H +#define SOUNDIO_ANDROID_H + +#include +#include + +#include "soundio_internal.h" +#include "os.h" +#include "soundio_internal.h" + +struct SoundIoPrivate; +enum SoundIoError soundio_android_init(struct SoundIoPrivate *si); + +struct SoundIoAndroid { + struct SoundIoOsCond *cond; + bool devices_emitted; + + SLObjectItf engineObject; + SLEngineItf engineEngine; +}; + +struct SoundIoOutStreamAndroid { + SLObjectItf outputMixObject; + SLObjectItf playerObject; + SLPlayItf playerPlay; + SLAndroidSimpleBufferQueueItf playerBufferQueue; + + int curBuffer; + size_t write_frame_count; + size_t bytes_per_buffer; + char *buffers[2]; + struct SoundIoChannelArea areas[SOUNDIO_MAX_CHANNELS]; +}; + +struct SoundIoInStreamAndroid { + SLObjectItf audioRecorderObject; + SLRecordItf recorderRecord; + SLAndroidSimpleBufferQueueItf recorderBufferQueue; + + int curBuffer; + size_t bytes_per_buffer; + char *buffers[2]; + struct SoundIoChannelArea areas[SOUNDIO_MAX_CHANNELS]; +}; + +#endif diff --git a/src/config.h.in b/src/config.h.in index ff22769..18cf08c 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -18,5 +18,6 @@ #cmakedefine SOUNDIO_HAVE_ALSA #cmakedefine SOUNDIO_HAVE_COREAUDIO #cmakedefine SOUNDIO_HAVE_WASAPI +#cmakedefine SOUNDIO_HAVE_ANDROID #endif diff --git a/src/os.c b/src/os.c index b542ad3..e2949e4 100644 --- a/src/os.c +++ b/src/os.c @@ -78,6 +78,12 @@ #include #endif +#ifdef __ANDROID__ +#include +#include +#include +#endif + struct SoundIoOsThread { #if defined(SOUNDIO_OS_WINDOWS) HANDLE handle; @@ -670,11 +676,24 @@ int soundio_os_init_mirrored_memory(struct SoundIoOsMirroredMemory *mem, size_t break; } #else + + int fd; +#ifdef __ANDROID__ + fd = open("/dev/ashmem", O_RDWR); + if (fd < 0) + return SoundIoErrorSystemResources; + + int ret = ioctl(fd, ASHMEM_SET_SIZE, actual_capacity); + if (ret < 0) { + close(fd); + return SoundIoErrorSystemResources; + } +#else char shm_path[] = "/dev/shm/soundio-XXXXXX"; char tmp_path[] = "/tmp/soundio-XXXXXX"; char *chosen_path; - int fd = mkstemp(shm_path); + fd = mkstemp(shm_path); if (fd < 0) { fd = mkstemp(tmp_path); if (fd < 0) { @@ -695,6 +714,7 @@ int soundio_os_init_mirrored_memory(struct SoundIoOsMirroredMemory *mem, size_t close(fd); return SoundIoErrorSystemResources; } +#endif char *address = (char*)mmap(NULL, actual_capacity * 2, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); if (address == MAP_FAILED) { diff --git a/src/soundio.c b/src/soundio.c index 273b9cf..98fc845 100644 --- a/src/soundio.c +++ b/src/soundio.c @@ -29,6 +29,9 @@ static const enum SoundIoBackend available_backends[] = { #endif #ifdef SOUNDIO_HAVE_WASAPI SoundIoBackendWasapi, +#endif +#ifdef SOUNDIO_HAVE_ANDROID + SoundIoBackendAndroid, #endif SoundIoBackendDummy, }; @@ -67,6 +70,12 @@ static backend_init_t backend_init_fns[] = { NULL, #endif +#ifdef SOUNDIO_HAVE_ANDROID + soundio_android_init, +#else + NULL, +#endif + &soundio_dummy_init, }; @@ -165,6 +174,7 @@ const char *soundio_backend_name(enum SoundIoBackend backend) { case SoundIoBackendAlsa: return "ALSA"; case SoundIoBackendCoreAudio: return "CoreAudio"; case SoundIoBackendWasapi: return "WASAPI"; + case SoundIoBackendAndroid: return "Android OpenSL ES"; case SoundIoBackendDummy: return "Dummy"; } return "(invalid backend)"; diff --git a/src/soundio_private.h b/src/soundio_private.h index 1e1ab4a..6aecf5e 100644 --- a/src/soundio_private.h +++ b/src/soundio_private.h @@ -32,6 +32,10 @@ #include "wasapi.h" #endif +#ifdef SOUNDIO_HAVE_ANDROID +#include "android.h" +#endif + #include "dummy.h" union SoundIoBackendData { @@ -49,6 +53,9 @@ union SoundIoBackendData { #endif #ifdef SOUNDIO_HAVE_WASAPI struct SoundIoWasapi wasapi; +#endif +#ifdef SOUNDIO_HAVE_ANDROID + struct SoundIoAndroid android; #endif struct SoundIoDummy dummy; }; @@ -87,6 +94,9 @@ union SoundIoOutStreamBackendData { #endif #ifdef SOUNDIO_HAVE_WASAPI struct SoundIoOutStreamWasapi wasapi; +#endif +#ifdef SOUNDIO_HAVE_ANDROID + struct SoundIoOutStreamAndroid android; #endif struct SoundIoOutStreamDummy dummy; }; @@ -106,6 +116,9 @@ union SoundIoInStreamBackendData { #endif #ifdef SOUNDIO_HAVE_WASAPI struct SoundIoInStreamWasapi wasapi; +#endif +#ifdef SOUNDIO_HAVE_ANDROID + struct SoundIoInStreamAndroid android; #endif struct SoundIoInStreamDummy dummy; };