/* * Copyright (C) 2024 Patrick Gaskin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include // for development, can cross-compile with $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -target aarch64-linux-android26 -shared -o aaudio.so -fPIC -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability -Wall -std=gnu11 op/aaudio.c -laaudio // also see https://github.com/google/oboe/blob/main/docs/AndroidAudioHistory.md // also see https://android.googlesource.com/platform/frameworks/av/+/master/media/libaaudio/examples/utils/AAudioSimplePlayer.h #ifndef __ANDROID__ // make ide autocomplete work without using a full ndk toolchain #define __INTRODUCED_IN(api_level) #endif // https://developer.android.com/ndk/guides/using-newer-apis #define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x))) #define API_AT_LEAST(x) __builtin_available(android x, *) #define AAUDIO_MINIMUM_API 26 #include #include "../op.h" #include "../mixer.h" #include "../sf.h" #include "../utils.h" #include "../xmalloc.h" // mapping from AAUDIO_CHANNEL_* enum values to cmus channel_position_t values // // cat "$(find ${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk} -wholename '*/AAudio.h' | sort -n | tail -n1)" | // grep AAUDIO_CHANNEL | tr -d ' \n' | tr '|,' ' \n' | grep -F '<<' | cut -d '_' -f3- | // cut -d '=' -f1 | xargs printf '#define A2C__%s\tCHANNEL_POSITION_INVALID\n' | // sed -E $(printf " -e s/(A2C__%s\\\t)CHANNEL_POSITION_INVALID/\\\1CHANNEL_POSITION_%s/" \ // FRONT_LEFT FRONT_LEFT \ // FRONT_RIGHT FRONT_RIGHT \ // FRONT_CENTER FRONT_CENTER \ // LOW_FREQUENCY LFE \ // BACK_LEFT REAR_LEFT \ // BACK_RIGHT REAR_RIGHT \ // FRONT_LEFT_OF_CENTER FRONT_LEFT_OF_CENTER \ // FRONT_RIGHT_OF_CENTER FRONT_RIGHT_OF_CENTER \ // BACK_CENTER REAR_CENTER \ // SIDE_LEFT SIDE_LEFT \ // SIDE_RIGHT SIDE_RIGHT \ // TOP_CENTER TOP_CENTER \ // TOP_FRONT_LEFT TOP_FRONT_LEFT \ // TOP_FRONT_CENTER TOP_FRONT_CENTER \ // TOP_FRONT_RIGHT TOP_FRONT_RIGHT \ // TOP_BACK_LEFT TOP_REAR_LEFT \ // TOP_BACK_CENTER TOP_REAR_CENTER \ // TOP_BACK_RIGHT TOP_REAR_RIGHT \ // ) | // column -s $'\t' -t | tee /dev/stderr | cut -d ' ' -f2 | cut -d '_' -f3- | // xargs printf ' X(%s)' | xargs -0 printf '#define A2C_CHANNELS%s\n' #define A2C__FRONT_LEFT CHANNEL_POSITION_FRONT_LEFT #define A2C__FRONT_RIGHT CHANNEL_POSITION_FRONT_RIGHT #define A2C__FRONT_CENTER CHANNEL_POSITION_FRONT_CENTER #define A2C__LOW_FREQUENCY CHANNEL_POSITION_LFE #define A2C__BACK_LEFT CHANNEL_POSITION_REAR_LEFT #define A2C__BACK_RIGHT CHANNEL_POSITION_REAR_RIGHT #define A2C__FRONT_LEFT_OF_CENTER CHANNEL_POSITION_FRONT_LEFT_OF_CENTER #define A2C__FRONT_RIGHT_OF_CENTER CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER #define A2C__BACK_CENTER CHANNEL_POSITION_REAR_CENTER #define A2C__SIDE_LEFT CHANNEL_POSITION_SIDE_LEFT #define A2C__SIDE_RIGHT CHANNEL_POSITION_SIDE_RIGHT #define A2C__TOP_CENTER CHANNEL_POSITION_TOP_CENTER #define A2C__TOP_FRONT_LEFT CHANNEL_POSITION_TOP_FRONT_LEFT #define A2C__TOP_FRONT_CENTER CHANNEL_POSITION_TOP_FRONT_CENTER #define A2C__TOP_FRONT_RIGHT CHANNEL_POSITION_TOP_FRONT_RIGHT #define A2C__TOP_BACK_LEFT CHANNEL_POSITION_TOP_REAR_LEFT #define A2C__TOP_BACK_CENTER CHANNEL_POSITION_TOP_REAR_CENTER #define A2C__TOP_BACK_RIGHT CHANNEL_POSITION_TOP_REAR_RIGHT #define A2C__TOP_SIDE_LEFT CHANNEL_POSITION_INVALID #define A2C__TOP_SIDE_RIGHT CHANNEL_POSITION_INVALID #define A2C__BOTTOM_FRONT_LEFT CHANNEL_POSITION_INVALID #define A2C__BOTTOM_FRONT_CENTER CHANNEL_POSITION_INVALID #define A2C__BOTTOM_FRONT_RIGHT CHANNEL_POSITION_INVALID #define A2C__LOW_FREQUENCY_2 CHANNEL_POSITION_INVALID #define A2C__FRONT_WIDE_LEFT CHANNEL_POSITION_INVALID #define A2C__FRONT_WIDE_RIGHT CHANNEL_POSITION_INVALID #define A2C_CHANNELS X(FRONT_LEFT) X(FRONT_RIGHT) X(FRONT_CENTER) X(LOW_FREQUENCY) X(BACK_LEFT) X(BACK_RIGHT) X(FRONT_LEFT_OF_CENTER) X(FRONT_RIGHT_OF_CENTER) X(BACK_CENTER) X(SIDE_LEFT) X(SIDE_RIGHT) X(TOP_CENTER) X(TOP_FRONT_LEFT) X(TOP_FRONT_CENTER) X(TOP_FRONT_RIGHT) X(TOP_BACK_LEFT) X(TOP_BACK_CENTER) X(TOP_BACK_RIGHT) X(TOP_SIDE_LEFT) X(TOP_SIDE_RIGHT) X(BOTTOM_FRONT_LEFT) X(BOTTOM_FRONT_CENTER) X(BOTTOM_FRONT_RIGHT) X(LOW_FREQUENCY_2) X(FRONT_WIDE_LEFT) X(FRONT_WIDE_RIGHT) // mapping from AAUDIO_CHANNEL_* masks to cmus channel_position_t lists // // cat "$(find ${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk} -wholename '*/aaudio/AAudio.h' | sort -n | tail -n1)" | // grep AAUDIO_CHANNEL | tr -d ' \n' | tr '|,' ',\n' | grep -Fve '<<' -e '-1' | cut -d '_' -f3- | // xargs printf '#define A2C__%s\n' | tr '=' '\t' | sed -E 's/AAUDIO_CHANNEL_([A-Z0-9_]+)/A2C__\1/g' | // column -s $'\t' -t | tee /dev/stderr | cut -d ' ' -f2 | cut -d '_' -f3- | // xargs printf ' X(%s)' | xargs -0 printf '#define A2C_LAYOUTS%s\n' #define A2C__MONO A2C__FRONT_LEFT #define A2C__STEREO A2C__FRONT_LEFT,A2C__FRONT_RIGHT #define A2C__2POINT1 A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__LOW_FREQUENCY #define A2C__TRI A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER #define A2C__TRI_BACK A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__BACK_CENTER #define A2C__3POINT1 A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER,A2C__LOW_FREQUENCY #define A2C__2POINT0POINT2 A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__TOP_SIDE_LEFT,A2C__TOP_SIDE_RIGHT #define A2C__2POINT1POINT2 A2C__2POINT0POINT2,A2C__LOW_FREQUENCY #define A2C__3POINT0POINT2 A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER,A2C__TOP_SIDE_LEFT,A2C__TOP_SIDE_RIGHT #define A2C__3POINT1POINT2 A2C__3POINT0POINT2,A2C__LOW_FREQUENCY #define A2C__QUAD A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__BACK_LEFT,A2C__BACK_RIGHT #define A2C__QUAD_SIDE A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__SIDE_LEFT,A2C__SIDE_RIGHT #define A2C__SURROUND A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER,A2C__BACK_CENTER #define A2C__PENTA A2C__QUAD,A2C__FRONT_CENTER #define A2C__5POINT1 A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER,A2C__LOW_FREQUENCY,A2C__BACK_LEFT,A2C__BACK_RIGHT #define A2C__5POINT1_SIDE A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER,A2C__LOW_FREQUENCY,A2C__SIDE_LEFT,A2C__SIDE_RIGHT #define A2C__6POINT1 A2C__FRONT_LEFT,A2C__FRONT_RIGHT,A2C__FRONT_CENTER,A2C__LOW_FREQUENCY,A2C__BACK_LEFT,A2C__BACK_RIGHT,A2C__BACK_CENTER #define A2C__7POINT1 A2C__5POINT1,A2C__SIDE_LEFT,A2C__SIDE_RIGHT #define A2C__5POINT1POINT2 A2C__5POINT1,A2C__TOP_SIDE_LEFT,A2C__TOP_SIDE_RIGHT #define A2C__5POINT1POINT4 A2C__5POINT1,A2C__TOP_FRONT_LEFT,A2C__TOP_FRONT_RIGHT,A2C__TOP_BACK_LEFT,A2C__TOP_BACK_RIGHT #define A2C__7POINT1POINT2 A2C__7POINT1,A2C__TOP_SIDE_LEFT,A2C__TOP_SIDE_RIGHT #define A2C__7POINT1POINT4 A2C__7POINT1,A2C__TOP_FRONT_LEFT,A2C__TOP_FRONT_RIGHT,A2C__TOP_BACK_LEFT,A2C__TOP_BACK_RIGHT #define A2C__9POINT1POINT4 A2C__7POINT1POINT4,A2C__FRONT_WIDE_LEFT,A2C__FRONT_WIDE_RIGHT #define A2C__9POINT1POINT6 A2C__9POINT1POINT4,A2C__TOP_SIDE_LEFT,A2C__TOP_SIDE_RIGHT #define A2C__FRONT_BACK A2C__FRONT_CENTER,A2C__BACK_CENTER #define A2C_LAYOUTS X(MONO) X(STEREO) X(2POINT1) X(TRI) X(TRI_BACK) X(3POINT1) X(2POINT0POINT2) X(2POINT1POINT2) X(3POINT0POINT2) X(3POINT1POINT2) X(QUAD) X(QUAD_SIDE) X(SURROUND) X(PENTA) X(5POINT1) X(5POINT1_SIDE) X(6POINT1) X(7POINT1) X(5POINT1POINT2) X(5POINT1POINT4) X(7POINT1POINT2) X(7POINT1POINT4) X(9POINT1POINT4) X(9POINT1POINT6) X(FRONT_BACK) // convert a cmus channel map to an equivalent aaudio channel mask (the returned // value will either be invalid or have the same number of bits set as the // number of channels) static aaudio_channel_mask_t cmus_channel_map_to_aaudio_mask(int channels, const channel_position_t *channel_map) { aaudio_channel_mask_t mask = 0; // we can only convert a valid channel map if (channels >= CHANNELS_MAX || !channel_map || !channel_map_valid(channel_map)) { return AAUDIO_CHANNEL_INVALID; } // special case for mono since cmus defines a separate channel position // for it if (channels == 1 && channel_map[0] == CHANNEL_POSITION_MONO) { return AAUDIO_CHANNEL_FRONT_LEFT; } // fill the mask, returning invalid if it has duplicates or no mapping for (int i = 0; i < channels; i++) { #define X(aaudio) \ if (A2C__##aaudio != CHANNEL_POSITION_INVALID && channel_map[i] == A2C__##aaudio) { \ if (mask & AAUDIO_CHANNEL_##aaudio) \ return AAUDIO_CHANNEL_INVALID; \ mask |= AAUDIO_CHANNEL_##aaudio; \ } A2C_CHANNELS #undef X } return mask; } // get the expected cmus channel order for the specified aaudio channel mask static bool channel_map_init_aaudio(aaudio_channel_mask_t mask, channel_position_t *map) { switch (mask) { #define X(aaudio) \ case AAUDIO_CHANNEL_##aaudio: channel_map_copy(map, (channel_position_t[CHANNELS_MAX]){ A2C__##aaudio }); return true; A2C_LAYOUTS #undef X } return false; } // get the name of a known aaudio channel mask static const char *aaudio_channel_to_string(aaudio_channel_mask_t mask) { switch (mask) { #define X(aaudio) \ case AAUDIO_CHANNEL_##aaudio: return #aaudio; A2C_CHANNELS #undef X } switch (mask) { #define X(aaudio) \ case AAUDIO_CHANNEL_##aaudio: return #aaudio; A2C_LAYOUTS #undef X } return NULL; } // allocate a map of output frame byte indexes to input frame byte indexes (or // -1 to zero) to remap channels (map must be sf_get_frame_size elements) static ssize_t *make_channel_remap(const channel_position_t *channel_map_out, const channel_position_t *channel_map_in, sample_format_t sf) { int byte, channel_out, channel_in; ssize_t *map; map = xnew(ssize_t, sf_get_frame_size(sf)); if (!channel_map_out || !channel_map_valid(channel_map_out) || !channel_map_in || !channel_map_valid(channel_map_in)) { for (byte = 0; byte < sf_get_frame_size(sf); byte++) { map[byte] = byte; } } else { for (byte = 0; byte < sf_get_frame_size(sf); byte++) { map[byte] = -1; } for (channel_out = 0; channel_out < sf_get_channels(sf); channel_out++) { if (channel_map_out[channel_out] != CHANNEL_POSITION_INVALID) { for (channel_in = 0; channel_in < sf_get_channels(sf); channel_in++) { if (channel_map_in[channel_in] == channel_map_out[channel_out]) { for (byte = 0; byte < sf_get_sample_size(sf); byte++) { map[sf_get_sample_size(sf) * channel_out + byte] = (ssize_t) sf_get_sample_size(sf) * channel_in + byte; } break; } } } } } d_print("remap bytes"); for (byte = 0; byte < sf_get_frame_size(sf); byte++) { d_print(" %03zd", map[byte]); } d_print("\n"); return map; } // if remap is non-null, applies it and returns dst, otherwise returns src static const uint8_t *apply_channel_remap(uint8_t *dst, const uint8_t *src, size_t n, sample_format_t sf, const ssize_t *remap) { size_t off_frame, off; BUG_ON(!src || dst == src); if (remap) { BUG_ON(!dst); for (off_frame = 0; off_frame < n; off_frame += (size_t) sf_get_frame_size(sf)) { for (off = 0; off < (size_t) sf_get_frame_size(sf); off++) { if (remap[off] != -1) { dst[off_frame+off] = src[off_frame+remap[off]]; } else { dst[off_frame+off] = 0; } } } return dst; } return src; } // configure builder to use the specified sample format (and on a best-effort // basis, channel_map, if provided) // // successful if the sample format is valid and was configured // // on success, out_remap will be set to NULL, or if a channel map was configured // and requires remapping, an allocated map of target frame byte indexes from // the source index REQUIRES_API(AAUDIO_MINIMUM_API) static aaudio_result_t configure_aaudio_sf(AAudioStreamBuilder *builder, sample_format_t sf, const channel_position_t *channel_map, ssize_t **out_remap) { aaudio_format_t format; aaudio_channel_mask_t mask; channel_position_t mask_expected_channels[CHANNELS_MAX]; // apply the sample format if (!sf_get_signed(sf)) { d_print("aaudio does not support unsigned samples\n"); return AAUDIO_ERROR_INVALID_FORMAT; } if (sf_get_bigendian(sf)) { d_print("aaudio does not support big-endian samples\n"); return AAUDIO_ERROR_INVALID_FORMAT; } switch (sf_get_bits(sf)) { case 16: format = AAUDIO_FORMAT_PCM_I16; break; case 24: format = AAUDIO_FORMAT_PCM_I24_PACKED; break; case 32: format = AAUDIO_FORMAT_PCM_I32; break; default: d_print("unsupported sample format bits\n"); return AAUDIO_ERROR_INVALID_FORMAT; } AAudioStreamBuilder_setFormat(builder, format); // apply the sample rate AAudioStreamBuilder_setSampleRate(builder, sf_get_rate(sf)); // set the channel count // // note: if no channel mask is set, aaudio will treat the first two // channels as left/right (duplicating mono to stereo if required), and // leave the rest up to the device, dropping them if the device doesn't // have that many channels AAudioStreamBuilder_setChannelCount(builder, sf_get_channels(sf)); // if we have a channel map, apply it on a best-effort basis *out_remap = NULL; if (channel_map && channel_map_valid(channel_map)) { if (API_AT_LEAST(32)) { mask = cmus_channel_map_to_aaudio_mask(sf_get_channels(sf), channel_map); d_print("channel map aaudio mask %d (%s)\n", mask, aaudio_channel_to_string(mask) ? aaudio_channel_to_string(mask) : "(null)"); if (mask == AAUDIO_CHANNEL_INVALID) { d_print("not applying channel map since it contains duplicates or not all channels have an aaudio equivalent\n"); } else { if (!channel_map_init_aaudio(mask, mask_expected_channels)) { d_print("not applying channel map since there isn't a valid cmus channel mapping for the aaudio mask\n"); } else { if (!channel_map_equal(channel_map, mask_expected_channels, sf_get_channels(sf))) { d_print("will remap channels since the input channel_map order doesn't match the order expected by aaudio\n"); *out_remap = make_channel_remap(mask_expected_channels, channel_map, sf); if (!*out_remap) return AAUDIO_ERROR_NO_MEMORY; } d_print("applying channel mask\n"); AAudioStreamBuilder_setChannelMask(builder, mask); } } } } return AAUDIO_OK; } // maps an res to a suitable error code static int OP_ERROR_AAUDIO(aaudio_result_t res) { // see https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/private/bionic_errdefs.h switch (res) { case AAUDIO_OK: return 0; case AAUDIO_ERROR_INTERNAL: return OP_ERROR_INTERNAL; case AAUDIO_ERROR_NO_SERVICE: return OP_ERROR_NOT_SUPPORTED; case AAUDIO_ERROR_INVALID_FORMAT: return OP_ERROR_SAMPLE_FORMAT; case AAUDIO_ERROR_INVALID_RATE: return OP_ERROR_SAMPLE_FORMAT; case AAUDIO_ERROR_UNAVAILABLE: errno = ECONNREFUSED; return OP_ERROR_ERRNO; // Connection refused case AAUDIO_ERROR_DISCONNECTED: errno = ECONNRESET; return OP_ERROR_ERRNO; // Connection reset by peer case AAUDIO_ERROR_TIMEOUT: errno = ETIMEDOUT; return OP_ERROR_ERRNO; // Connection timed out case AAUDIO_ERROR_WOULD_BLOCK: errno = ENOBUFS; return OP_ERROR_ERRNO; // No buffer space available case AAUDIO_ERROR_UNIMPLEMENTED: errno = ENOSYS; return OP_ERROR_ERRNO; // Function not implemented case AAUDIO_ERROR_NO_FREE_HANDLES: errno = EMFILE; return OP_ERROR_ERRNO; // Too many open files case AAUDIO_ERROR_NO_MEMORY: errno = ENOMEM; return OP_ERROR_ERRNO; // Out of memory case AAUDIO_ERROR_NULL: errno = EFAULT; return OP_ERROR_ERRNO; // Bad address case AAUDIO_ERROR_OUT_OF_RANGE: errno = EINVAL; return OP_ERROR_ERRNO; // Invalid argument case AAUDIO_ERROR_INVALID_HANDLE: errno = EBADF; return OP_ERROR_ERRNO; // Bad file descriptor case AAUDIO_ERROR_INVALID_STATE: errno = EBADFD; return OP_ERROR_ERRNO; // File descriptor in bad state case AAUDIO_ERROR_ILLEGAL_ARGUMENT: errno = EINVAL; return OP_ERROR_ERRNO; // Invalid argument default: return OP_ERROR_INTERNAL; } } // note: all options require restarting the output stream to apply static aaudio_performance_mode_t op_aaudio_opt_performance_mode = AAUDIO_PERFORMANCE_MODE_POWER_SAVING; static aaudio_allowed_capture_policy_t op_aaudio_opt_allowed_capture = AAUDIO_ALLOW_CAPTURE_BY_ALL; static aaudio_sharing_mode_t op_aaudio_opt_sharing_mode = AAUDIO_SHARING_MODE_SHARED; static bool op_aaudio_opt_disable_spatialization = false; static int op_aaudio_opt_min_buffer_capacity_ms = 0; // if we ever decide to support AAUDIO_PERFORMANCE_MODE_LOW_LATENCY streams, // note that disconnection is broken for shared low-latency streams on RQ1A // (this doesn't affect us right now since we don't use low-latency shared mmap // streams) // // https://issuetracker.google.com/issues/173928197 static int op_aaudio_set_performance_mode(const char *val) { if (!strcmp(val, "none")) { op_aaudio_opt_performance_mode = AAUDIO_PERFORMANCE_MODE_NONE; return OP_ERROR_SUCCESS; } if (!strcmp(val, "power_saving")) { op_aaudio_opt_performance_mode = AAUDIO_PERFORMANCE_MODE_POWER_SAVING; return OP_ERROR_SUCCESS; } errno = EINVAL; return -OP_ERROR_ERRNO; } static int op_aaudio_get_performance_mode(char **val) { switch (op_aaudio_opt_performance_mode) { default: __attribute__((fallthrough)); case AAUDIO_PERFORMANCE_MODE_NONE: *val = xstrdup("none"); break; case AAUDIO_PERFORMANCE_MODE_POWER_SAVING: *val = xstrdup("power_saving"); break; } return OP_ERROR_SUCCESS; } static int op_aaudio_set_allowed_capture(const char *val) { if (!strcmp(val, "all")) { op_aaudio_opt_allowed_capture = AAUDIO_ALLOW_CAPTURE_BY_ALL; return OP_ERROR_SUCCESS; } if (!strcmp(val, "none")) { op_aaudio_opt_allowed_capture = AAUDIO_ALLOW_CAPTURE_BY_NONE; return OP_ERROR_SUCCESS; } if (!strcmp(val, "system")) { op_aaudio_opt_allowed_capture = AAUDIO_ALLOW_CAPTURE_BY_SYSTEM; return OP_ERROR_SUCCESS; } errno = EINVAL; return -OP_ERROR_ERRNO; } static int op_aaudio_get_allowed_capture(char **val) { switch (op_aaudio_opt_allowed_capture) { default: __attribute__((fallthrough)); case AAUDIO_ALLOW_CAPTURE_BY_ALL: *val = xstrdup("all"); break; case AAUDIO_ALLOW_CAPTURE_BY_NONE: *val = xstrdup("none"); break; case AAUDIO_ALLOW_CAPTURE_BY_SYSTEM: *val = xstrdup("system"); break; } return OP_ERROR_SUCCESS; } static int op_aaudio_set_sharing_mode(const char *val) { if (!strcmp(val, "shared")) { op_aaudio_opt_sharing_mode = AAUDIO_SHARING_MODE_SHARED; return OP_ERROR_SUCCESS; } if (!strcmp(val, "exclusive")) { op_aaudio_opt_sharing_mode = AAUDIO_SHARING_MODE_EXCLUSIVE; return OP_ERROR_SUCCESS; } errno = EINVAL; return -OP_ERROR_ERRNO; } static int op_aaudio_get_sharing_mode(char **val) { switch (op_aaudio_opt_performance_mode) { default: __attribute__((fallthrough)); case AAUDIO_SHARING_MODE_SHARED: *val = xstrdup("shared"); break; case AAUDIO_SHARING_MODE_EXCLUSIVE: *val = xstrdup("exclusive"); break; } return OP_ERROR_SUCCESS; } static int op_aaudio_set_disable_spatialization(const char *val) { op_aaudio_opt_disable_spatialization = strcmp(val, "true") ? false : true; return OP_ERROR_SUCCESS; } static int op_aaudio_get_disable_spatialization(char **val) { *val = xstrdup(op_aaudio_opt_disable_spatialization ? "true" : "false"); return OP_ERROR_SUCCESS; } static int op_aaudio_set_min_buffer_capacity_ms(const char *val) { long tmp; if (str_to_int(val, &tmp) == -1 || tmp < 0 || tmp > 1000) { errno = EINVAL; return -OP_ERROR_ERRNO; } op_aaudio_opt_min_buffer_capacity_ms = (int) tmp; return OP_ERROR_SUCCESS; } static int op_aaudio_get_min_buffer_capacity_ms(char **val) { char tmp[5]; snprintf(tmp, sizeof(tmp), "%d", op_aaudio_opt_min_buffer_capacity_ms); *val = xstrdup(tmp); return OP_ERROR_SUCCESS; } static bool aaudio_supported() { if (API_AT_LEAST(27)) {} else { // don't use AAudio on API 26 due to bug causing crash on some // devices when closing stream // // https://github.com/google/oboe/issues/40 return -OP_ERROR_NOT_SUPPORTED; } if (API_AT_LEAST(AAUDIO_MINIMUM_API)) { return !!&AAudio_createStreamBuilder; } return false; } static struct { AAudioStream *stream; int32_t device; aaudio_result_t error; sample_format_t sf; ssize_t *remap; char *remap_buf; } op; int mixer_notify_output_in, mixer_notify_output_out; static int op_aaudio_init(void) { if (!aaudio_supported()) { // skip the output plugin (see op_select_any) return -OP_ERROR_NOT_SUPPORTED; } init_pipes(&mixer_notify_output_out, &mixer_notify_output_in); return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_exit(void) { close(mixer_notify_output_out); close(mixer_notify_output_in); return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static void handle_error(AAudioStream *stream, void *userData, aaudio_result_t error) { if (error == AAUDIO_ERROR_DISCONNECTED) { notify_via_pipe(mixer_notify_output_in); } d_print("stream errored (%d - %s)\n", error, AAudio_convertResultToText(error)); op.error = error; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_open(sample_format_t sf, const channel_position_t *channel_map) { aaudio_result_t rc; AAudioStreamBuilder *bld; // create the stream builder rc = AAudio_createStreamBuilder(&bld); if (rc) { d_print("create stream builder failed (%d - %s)\n", rc, AAudio_convertResultToText(rc)); return -OP_ERROR_AAUDIO(rc); } // set the error callback AAudioStreamBuilder_setErrorCallback(bld, handle_error, NULL); // apply the options AAudioStreamBuilder_setSharingMode(bld, op_aaudio_opt_sharing_mode); AAudioStreamBuilder_setPerformanceMode(bld, op_aaudio_opt_performance_mode); if (API_AT_LEAST(28)) AAudioStreamBuilder_setContentType(bld, AAUDIO_CONTENT_TYPE_MUSIC); if (API_AT_LEAST(28)) AAudioStreamBuilder_setUsage(bld, AAUDIO_USAGE_MEDIA); if (API_AT_LEAST(29)) AAudioStreamBuilder_setAllowedCapturePolicy(bld, op_aaudio_opt_allowed_capture); if (API_AT_LEAST(31)) AAudioStreamBuilder_setAttributionTag(bld, "cmus"); if (API_AT_LEAST(32)) AAudioStreamBuilder_setSpatializationBehavior(bld, op_aaudio_opt_disable_spatialization ? AAUDIO_SPATIALIZATION_BEHAVIOR_NEVER : AAUDIO_SPATIALIZATION_BEHAVIOR_AUTO); // ensure the buffer holds at least the requested amount of audio (default 80ms) AAudioStreamBuilder_setBufferCapacityInFrames(bld, sf_get_rate(sf) / (1000 / (op_aaudio_opt_min_buffer_capacity_ms ? op_aaudio_opt_min_buffer_capacity_ms : 80))); // configure the sample format and channel map rc = configure_aaudio_sf(bld, sf, channel_map, &op.remap); if (rc) { d_print("configure format failed (%d - %s)\n", rc, AAudio_convertResultToText(rc)); return -OP_ERROR_AAUDIO(rc); } if (op.remap) { d_print("allocating %zu bytes for remap buffer\n", (size_t) AAudioStream_getBufferCapacityInFrames(op.stream) * (size_t) sf_get_frame_size(sf)); op.remap_buf = xmalloc((size_t) AAudioStream_getBufferCapacityInFrames(op.stream) * (size_t) sf_get_frame_size(sf)); } op.sf = sf; // open the stream op.error = 0; rc = AAudioStreamBuilder_openStream(bld, &op.stream); if (rc) { d_print("open stream failed (%d - %s)\n", rc, AAudio_convertResultToText(rc)); if (op.remap_buf) { free(op.remap_buf); op.remap_buf = NULL; } if (op.remap) { free(op.remap); op.remap = NULL; } AAudioStreamBuilder_delete(bld); return -OP_ERROR_AAUDIO(rc); } op.device = AAudioStream_getDeviceId(op.stream); d_print("optimal buffer frames = %d\n", AAudioStream_getFramesPerBurst(op.stream)); d_print("buffer capacity frames = %d\n", AAudioStream_getBufferCapacityInFrames(op.stream)); // cleanup the stream builder rc = AAudioStreamBuilder_delete(bld); if (rc) { d_print("delete stream builder failed (%d - %s)\n", rc, AAudio_convertResultToText(rc)); if (op.remap_buf) { free(op.remap_buf); op.remap_buf = NULL; } if (op.remap) { free(op.remap); op.remap = NULL; } AAudioStream_close(op.stream); return -OP_ERROR_AAUDIO(rc); } // done (we don't actually start the stream until the first write) return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_close(void) { if (op.remap_buf) { free(op.remap_buf); op.remap_buf = NULL; } if (op.remap) { free(op.remap); op.remap = NULL; } if (op.stream) { AAudioStream_close(op.stream); op.stream = NULL; } return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static aaudio_result_t do_state_change(aaudio_result_t (*request)(AAudioStream *strm), aaudio_stream_state_t state, aaudio_stream_state_t state2) { aaudio_result_t rc; if (op.error) { rc = op.error; return rc; } if (request) { d_print("request state change\n"); rc = request(op.stream); if (rc) { return rc; } } d_print("wait state change (%d:%s || %d:%s)\n", state, AAudio_convertStreamStateToText(state), state2, AAudio_convertStreamStateToText(state2)); aaudio_stream_state_t currentState = AAUDIO_STREAM_STATE_UNKNOWN; aaudio_stream_state_t inputState = currentState; rc = AAUDIO_OK; while (rc == AAUDIO_OK && currentState != state && (state2 == 0 || currentState != state2)) { // this is required to prevent hanging during pause_on_output_change if (op.error) { rc = op.error; break; } if (currentState == AAUDIO_STREAM_STATE_CLOSING || currentState == AAUDIO_STREAM_STATE_CLOSED || currentState == AAUDIO_STREAM_STATE_DISCONNECTED) { rc = AAUDIO_ERROR_DISCONNECTED; break; } d_print("current state change %d\r\n", currentState); rc = AAudioStream_waitForStateChange(op.stream, inputState, ¤tState, INT64_MAX); inputState = currentState; } if (rc) { d_print("failed state change (%d - %s) [current=%d:%s]\n", rc, AAudio_convertResultToText(rc), currentState, AAudio_convertStreamStateToText(currentState)); } else { d_print("done state change [current=%d:%s]\n", currentState, AAudio_convertStreamStateToText(currentState)); } return rc; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_drop(void) { aaudio_result_t rc; aaudio_stream_state_t orig_state = AAudioStream_getState(op.stream); // we can't flush if it's closing if (orig_state == AAUDIO_STREAM_STATE_CLOSING || orig_state == AAUDIO_STREAM_STATE_CLOSED) { return -OP_ERROR_NOT_OPEN; } // only flush if it isn't already flushed or closed if (orig_state != AAUDIO_STREAM_STATE_FLUSHED) { // the stream must be paused to be flushed if (orig_state == AAUDIO_STREAM_STATE_STARTED || orig_state == AAUDIO_STREAM_STATE_STARTING) { rc = do_state_change(AAudioStream_requestPause, AAUDIO_STREAM_STATE_PAUSED, 0); if (rc) { return -OP_ERROR_AAUDIO(rc); } // the stream will be started again on the first write } // flush the stream rc = do_state_change(AAudioStream_requestFlush, AAUDIO_STREAM_STATE_FLUSHED, 0); if (rc) { return -OP_ERROR_AAUDIO(rc); } } return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_write(const char *buf, int count) { int32_t device; aaudio_result_t rc; aaudio_stream_state_t state; // if the stream errored, return an error so cmus restarts the output // plugin // // note that this isn't strictly required since AAudioStream_write will // return an error on stream disconnection, which will cause cmus to // reopen the output plugin // // https://github.com/google/oboe/wiki/TechNote_Disconnect if (op.error) { return -OP_ERROR_AAUDIO(op.error); } // note: this is cheap; it's just a field getter internally device = AAudioStream_getDeviceId(op.stream); if (op.device != device) { if (op.device != -1) { notify_via_pipe(mixer_notify_output_in); } op.device = device; } // start the stream on the first write (rather than after opening or // flushing since cmus may not always use the stream and starting a // stream is somewhat expensive) // // note: this is cheap; it's just a atomic field getter internally state = AAudioStream_getState(op.stream); if (state == AAUDIO_STREAM_STATE_CLOSING || state == AAUDIO_STREAM_STATE_CLOSED) { return -OP_ERROR_NOT_OPEN; } if (state != AAUDIO_STREAM_STATE_STARTING && state != AAUDIO_STREAM_STATE_STARTED) { rc = do_state_change(AAudioStream_requestStart, AAUDIO_STREAM_STATE_STARTED, AAUDIO_STREAM_STATE_STARTING); if (rc) { return -OP_ERROR_AAUDIO(rc); } } // this should never happen since op_aaudio_buffer_space should always // be less than AAudioStream_getBufferCapacityInFrames, and cmus // determines how much to write using it BUG_ON(count >= AAudioStream_getBufferCapacityInFrames(op.stream) * sf_get_frame_size(op.sf)); // remap if necessary buf = (char *) apply_channel_remap((uint8_t *) op.remap_buf, (uint8_t *) buf, count, op.sf, op.remap); // synchronously write the samples to the buffer rc = AAudioStream_write(op.stream, buf, count / (int) sf_get_frame_size(op.sf), INT64_MAX); if (rc < 0) { d_print("write %d = error %d - %s [device=%d] [state=%d]\n", count / (int) sf_get_frame_size(op.sf), rc, AAudio_convertResultToText(rc), device, state); return -OP_ERROR_AAUDIO(rc); } d_print("write %d = %d (* %d bytes) [device=%d] [state=%d]\n", count / (int) sf_get_frame_size(op.sf), rc, (int) sf_get_frame_size(op.sf), device, state); // return the number of bytes we write return (int) sf_get_frame_size(op.sf) * rc; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_pause(void) { // request stream pause, wait until it completes return -OP_ERROR_AAUDIO(do_state_change(AAudioStream_requestPause, AAUDIO_STREAM_STATE_PAUSED, 0)); } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_unpause(void) { // request stream start, wait until it starts to start (i.e., will start // consuming frames written to it) return -OP_ERROR_AAUDIO(do_state_change(AAudioStream_requestStart, AAUDIO_STREAM_STATE_STARTED, AAUDIO_STREAM_STATE_STARTING)); } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_buffer_space(void) { int32_t optimal, nonblock; // optimal buffer amount (anecdotally, this generally seems to be less // than half the buffer capacity) optimal = AAudioStream_getFramesPerBurst(op.stream) * (int32_t) sf_get_frame_size(op.sf); // max buffer amount (without blocking) nonblock = AAudioStream_getBufferSizeInFrames(op.stream) * (int32_t) sf_get_frame_size(op.sf); // want to write the optimal amount (up to the nonblock amount) return optimal < nonblock ? optimal : nonblock; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_init(void) { if (!aaudio_supported()) { // skip the output plugin (see op_select_any) return -OP_ERROR_NOT_SUPPORTED; } return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_exit(void) { return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_open(int *volume_max) { *volume_max = UINT16_MAX; return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_close(void) { return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_get_fds(int what, int *fds) { switch (what) { case MIXER_FDS_OUTPUT: fds[0] = mixer_notify_output_out; return 1; default: return 0; } } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_set_volume(int l, int r) { return -OP_ERROR_NOT_SUPPORTED; } REQUIRES_API(AAUDIO_MINIMUM_API) static int op_aaudio_mixer_get_volume(int *l, int *r) { // aaudio doesn't support volume control, so say the volume is 100% *l = *r = UINT16_MAX; return OP_ERROR_SUCCESS; } REQUIRES_API(AAUDIO_MINIMUM_API) const struct output_plugin_ops op_pcm_ops = { .init = op_aaudio_init, .exit = op_aaudio_exit, .open = op_aaudio_open, .close = op_aaudio_close, .drop = op_aaudio_drop, .write = op_aaudio_write, .pause = op_aaudio_pause, .unpause = op_aaudio_unpause, .buffer_space = op_aaudio_buffer_space, }; REQUIRES_API(AAUDIO_MINIMUM_API) const struct mixer_plugin_ops op_mixer_ops = { .init = op_aaudio_mixer_init, .exit = op_aaudio_mixer_exit, .open = op_aaudio_mixer_open, .close = op_aaudio_mixer_close, .get_fds.abi_2 = op_aaudio_mixer_get_fds, .set_volume = op_aaudio_mixer_set_volume, .get_volume = op_aaudio_mixer_get_volume, }; const struct output_plugin_opt op_pcm_options[] = { OPT(op_aaudio, performance_mode), OPT(op_aaudio, allowed_capture), OPT(op_aaudio, sharing_mode), OPT(op_aaudio, disable_spatialization), OPT(op_aaudio, min_buffer_capacity_ms), { NULL }, }; const struct mixer_plugin_opt op_mixer_options[] = { { NULL }, }; const int op_priority = -3; // higher priority than pulse (-2) const unsigned op_abi_version = OP_ABI_VERSION;