diff --git a/src/audio-common.h b/src/audio-common.h new file mode 100644 index 00000000..fffe3c62 --- /dev/null +++ b/src/audio-common.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "matoya.h" + +struct audio_common { + MTY_AudioFormat format; + + // Values derived from `format` + struct { + uint32_t sample_size; ///< Size in bytes of 1 sample of audio from 1 channel + uint32_t frame_size; ///< Size in bytes of 1 frame of audio, i.e. 1 sample of audio from each and every channel combined + uint32_t buffer_size; ///< Size in bytes of a 1-second buffer + uint32_t min_buffer; ///< Minimum number of frames of audio that must be enqueued before playback + uint32_t max_buffer; ///< Maximum number of frames of audio that can be enqueued for playback + } computed; +}; + +static void audio_common_init(struct audio_common *ctx, MTY_AudioFormat fmt, uint32_t min_buffer_ms, + uint32_t max_buffer_ms) +{ + ctx->format = fmt; + + ctx->computed.sample_size = fmt.sampleFormat == MTY_AUDIO_SAMPLE_FORMAT_FLOAT + ? sizeof(float) : sizeof(int16_t); + ctx->computed.frame_size = fmt.channels * ctx->computed.sample_size; + ctx->computed.buffer_size = fmt.sampleRate * ctx->computed.frame_size; + + uint32_t samples_per_ms = lrintf(fmt.sampleRate / 1000.0f); + ctx->computed.min_buffer = min_buffer_ms * samples_per_ms; + ctx->computed.max_buffer = max_buffer_ms * samples_per_ms; +} diff --git a/src/matoya.h b/src/matoya.h index 5dbac616..0705ca28 100644 --- a/src/matoya.h +++ b/src/matoya.h @@ -1453,21 +1453,89 @@ MTY_WaitPtr(int32_t *sync); //- #module Audio //- #mbrief Simple audio playback and resampling. -//- #mdetails This is a very minimal interface that assumes 2-channel, 16-bit signed PCM +//- #mdetails This is a very minimal interface that supports multi-channel PCM audio //- submitted by pushing to a queue. This module also includes a straightforward -//- resampler. +//- resampler for 2-channel, 16-bit signed PCM audio. typedef struct MTY_Audio MTY_Audio; typedef struct MTY_Resampler MTY_Resampler; +/// @brief Audio sample formats. Currently only PCM formats. +typedef enum { + MTY_AUDIO_SAMPLE_FORMAT_UNKNOWN = 0, ///< Unknown format. + MTY_AUDIO_SAMPLE_FORMAT_FLOAT = 1, ///< 32-bit floating point. + MTY_AUDIO_SAMPLE_FORMAT_INT16 = 2, ///< 16-bit signed integer. +} MTY_AudioSampleFormat; + +/// @brief Surround sound audio channel IDs. +/// @details Used in ::MTY_AudioFormat. Follows standard surround sound speaker ids +/// (see https://en.wikipedia.org/wiki/Surround_sound#Standard_speaker_channels). +typedef enum { + MTY_AUDIO_CHANNEL_FL = 0x00000001, + MTY_AUDIO_CHANNEL_FR = 0x00000002, + MTY_AUDIO_CHANNEL_FC = 0x00000004, + MTY_AUDIO_CHANNEL_LFE = 0x00000008, + MTY_AUDIO_CHANNEL_BL = 0x00000010, + MTY_AUDIO_CHANNEL_BR = 0x00000020, + MTY_AUDIO_CHANNEL_FLC = 0x00000040, + MTY_AUDIO_CHANNEL_FRC = 0x00000080, + MTY_AUDIO_CHANNEL_BC = 0x00000100, + MTY_AUDIO_CHANNEL_SL = 0x00000200, + MTY_AUDIO_CHANNEL_SR = 0x00000400, + MTY_AUDIO_CHANNEL_TC = 0x00000800, + MTY_AUDIO_CHANNEL_TFL = 0x00001000, + MTY_AUDIO_CHANNEL_TFC = 0x00002000, + MTY_AUDIO_CHANNEL_TFR = 0x00004000, + MTY_AUDIO_CHANNEL_TBL = 0x00008000, + MTY_AUDIO_CHANNEL_TBC = 0x00010000, + MTY_AUDIO_CHANNEL_TBR = 0x00020000, + + // Some common configurations + MTY_AUDIO_CHANNEL_CFG_UNKNOWN = 0, + MTY_AUDIO_CHANNEL_CFG_MONO = MTY_AUDIO_CHANNEL_FL, + MTY_AUDIO_CHANNEL_CFG_STEREO = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR, + MTY_AUDIO_CHANNEL_CFG_5_1_SURROUND = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR | MTY_AUDIO_CHANNEL_FC | + MTY_AUDIO_CHANNEL_LFE | MTY_AUDIO_CHANNEL_SL | MTY_AUDIO_CHANNEL_SR, + MTY_AUDIO_CHANNEL_CFG_5_1_REAR = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR | MTY_AUDIO_CHANNEL_FC | + MTY_AUDIO_CHANNEL_LFE | MTY_AUDIO_CHANNEL_BL | MTY_AUDIO_CHANNEL_BR, + MTY_AUDIO_CHANNEL_CFG_7_1_SURROUND = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR | MTY_AUDIO_CHANNEL_FC | + MTY_AUDIO_CHANNEL_LFE | MTY_AUDIO_CHANNEL_BL | MTY_AUDIO_CHANNEL_BR | + MTY_AUDIO_CHANNEL_SL | MTY_AUDIO_CHANNEL_SR, + MTY_AUDIO_CHANNEL_CFG_7_1_WIDE = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR | MTY_AUDIO_CHANNEL_FC | + MTY_AUDIO_CHANNEL_LFE | MTY_AUDIO_CHANNEL_BL | MTY_AUDIO_CHANNEL_BR | + MTY_AUDIO_CHANNEL_FLC | MTY_AUDIO_CHANNEL_FRC, + MTY_AUDIO_CHANNEL_CFG_7_1_SIDE = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR | MTY_AUDIO_CHANNEL_FC | + MTY_AUDIO_CHANNEL_LFE | MTY_AUDIO_CHANNEL_SL | MTY_AUDIO_CHANNEL_SR | + MTY_AUDIO_CHANNEL_FLC | MTY_AUDIO_CHANNEL_FRC, + MTY_AUDIO_CHANNEL_CFG_7_1_4_ATMOS = MTY_AUDIO_CHANNEL_FL | MTY_AUDIO_CHANNEL_FR | MTY_AUDIO_CHANNEL_FC | + MTY_AUDIO_CHANNEL_LFE | MTY_AUDIO_CHANNEL_BL | MTY_AUDIO_CHANNEL_BR | + MTY_AUDIO_CHANNEL_SL | MTY_AUDIO_CHANNEL_SR | MTY_AUDIO_CHANNEL_TFL | + MTY_AUDIO_CHANNEL_TFR | MTY_AUDIO_CHANNEL_TBL | MTY_AUDIO_CHANNEL_TBR, +} MTY_AudioChannelID; + +/// @brief Format description for an audio device. +typedef struct { + MTY_AudioSampleFormat sampleFormat; ///< Format of audio samples. + uint32_t sampleRate; ///< Number of audio samples per second. Usually set to 48000. + uint32_t channels; ///< Number of audio channels. + uint32_t channelMask; ///< Bitmask that defines which channels are present in the audio data. + ///< Should contain only values defined in ::MTY_AudioChannelID. + ///< For `channels` > 2, specify this value correctly in order + ///< to yield accurate audio playback. + ///< Currently only supported by Windows and macOS. All other platforms + ///< will ignore this field. + ///< If set to MTY_AUDIO_CHANNEL_CFG_UNKNOWN, the platform will + ///< attempt to use reasonable defaults based on `channels`. +} MTY_AudioFormat; + /// @brief Create an MTY_Audio context for playback. -/// @param sampleRate Audio sample rate in KHz. +/// @param format Format that the audio device will attempt to initialize. If the +/// provided format is not supported, the function will fail and return NULL. /// @param minBuffer The minimum amount of audio in milliseconds that must be queued /// before playback begins. /// @param maxBuffer The maximum amount of audio in milliseconds that can be queued /// before audio begins getting dropped. The queue will flush to zero, then begin /// building back towards `minBuffer` again before playback resumes. -/// @param channels Number of audio channels. /// @param deviceID Specify a specific audio device for playback, or NULL for the default /// device. Windows only. /// @param fallback If `deviceID` is not NULL, set to true to fallback to the default device, or @@ -1475,7 +1543,7 @@ typedef struct MTY_Resampler MTY_Resampler; /// @returns On failure, NULL is returned. Call MTY_GetLog for details.\n\n /// The returned MTY_Audio context must be destroyed with MTY_AudioDestroy. MTY_EXPORT MTY_Audio * -MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t maxBuffer, uint8_t channels, +MTY_AudioCreate(MTY_AudioFormat format, uint32_t minBuffer, uint32_t maxBuffer, const char *deviceID, bool fallback); /// @brief Destroy an MTY_Audio context. @@ -1498,11 +1566,13 @@ MTY_AudioGetQueued(MTY_Audio *ctx); /// @brief Queue 16-bit signed PCM audio for playback. /// @param ctx An MTY_Audio context. -/// @param frames Buffer containing 16-bit signed PCM audio frames. The number of audio channels -/// is specified during MTY_AudioCreate. In the case of 2-channel PCM, one audio frame is two -/// samples, each sample being one channel. +/// @param frames Buffer containing PCM audio frames as per the format (channels, sample rate, etc) +/// that was specified during MTY_AudioCreate. A frame is an interleaving of 1 sample per channel. +/// For example, in the case of 2-channel/stereo audio, one audio frame is two samples, each +/// sample being one channel. /// @param count The number of frames contained in `frames`. The number of frames would -/// be the size of `frames` in bytes divided by 2 * `channels` specified during MTY_AudioCreate. +/// be the size of `frames` in bytes divided by sample size * `channels` specified +/// during MTY_AudioCreate. MTY_EXPORT void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count); diff --git a/src/unix/apple/audio.c b/src/unix/apple/audio.c index 314e6870..113c875f 100644 --- a/src/unix/apple/audio.c +++ b/src/unix/apple/audio.c @@ -5,21 +5,17 @@ #include "matoya.h" #include +#include -#define AUDIO_SAMPLE_SIZE sizeof(int16_t) -#define AUDIO_BUFS 64 +#include "audio-common.h" -#define AUDIO_BUF_SIZE(ctx) \ - ((ctx)->sample_rate * (ctx)->channels * AUDIO_SAMPLE_SIZE) +#define AUDIO_BUFS 64 struct MTY_Audio { + struct audio_common cmn; AudioQueueRef q; AudioQueueBufferRef audio_buf[AUDIO_BUFS]; MTY_Atomic32 in_use[AUDIO_BUFS]; - uint32_t sample_rate; - uint32_t min_buffer; - uint32_t max_buffer; - uint8_t channels; bool playing; }; @@ -31,37 +27,171 @@ static void audio_queue_callback(void *opaque, AudioQueueRef q, AudioQueueBuffer buf->mAudioDataByteSize = 0; } -MTY_Audio *MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t maxBuffer, uint8_t channels, - const char *deviceID, bool fallback) +static OSStatus audio_object_get_device_uid(AudioObjectID device, AudioObjectPropertyScope prop_scope, + AudioObjectPropertyElement prop_element, char **out_uid, CFStringRef *out_uid_cf) { - // TODO Should this use the current run loop rather than internal threading? + char *uid = NULL; + CFStringRef uid_cf = NULL; + + AudioObjectPropertyAddress propAddr = { + .mSelector = kAudioDevicePropertyDeviceUID, + .mScope = prop_scope, + .mElement = prop_element, + }; + + UInt32 data_size = sizeof(CFStringRef); + OSStatus e = AudioObjectGetPropertyData(device, &propAddr, 0, NULL, &data_size, &uid_cf); + if (e != kAudioHardwareNoError) + goto except; - MTY_Audio *ctx = MTY_Alloc(1, sizeof(MTY_Audio)); - ctx->sample_rate = sampleRate; - ctx->channels = channels; + UInt32 prop_size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(uid_cf), kCFStringEncodingUTF8); + uid = calloc(1, prop_size + 1); + if (!CFStringGetCString(uid_cf, uid, prop_size + 1, kCFStringEncodingUTF8)) { + e = kAudioHardwareBadDeviceError; + goto except; + } + + // OK + *out_uid = uid; + *out_uid_cf = uid_cf; + + except: + + if (e != 0) { + if (uid_cf) + CFRelease(uid_cf); + free(uid); + } + + return e; +} + +static OSStatus audio_device_create(MTY_Audio *ctx, const char *deviceID) +{ + AudioObjectID *selected_device = NULL; + CFStringRef selected_device_uid = NULL; + + // Enumerate all output devices and identify the given device + AudioObjectID *device_ids = NULL; + + bool default_dev = !deviceID || !deviceID[0]; + + AudioObjectPropertyAddress propAddr = { + .mSelector = default_dev ? kAudioHardwarePropertyDefaultOutputDevice : kAudioHardwarePropertyDevices, + .mScope = kAudioObjectPropertyScopeOutput, + .mElement = kAudioObjectPropertyElementWildcard, + }; + + UInt32 data_size = 0; + OSStatus e = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propAddr, 0, NULL, &data_size); + if (e != kAudioHardwareNoError) + goto except; + + if (data_size == 0) { + e = kAudioHardwareBadDeviceError; + goto except; + } + + device_ids = calloc(1, data_size); + e = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propAddr, 0, NULL, &data_size, device_ids); + if (e != kAudioHardwareNoError) + goto except; + + uint32_t n = (uint32_t) (data_size / sizeof(AudioObjectID)); + + if (default_dev) { + if (n != 1) { + e = kAudioHardwareBadDeviceError; + goto except; + } + + selected_device = device_ids; + + } else { + // Goal: find the AudioObjectID of the device whose unique string ID matches the given `deviceID` parameter + for (uint32_t i = 0; !selected_device && i < n; i++) { + char *uid = NULL; + CFStringRef uid_cf = NULL; + + OSStatus e_uid = audio_object_get_device_uid(device_ids[i], propAddr.mScope, + propAddr.mElement, &uid, &uid_cf); + if (e_uid != kAudioHardwareNoError) + continue; + + if (!strcmp(deviceID, uid)) { + selected_device = &device_ids[i]; + selected_device_uid = uid_cf; + uid_cf = NULL; + } + + if (uid_cf) + CFRelease(uid_cf); + + free(uid); + } + } + + if (!selected_device) { + e = kAudioHardwareBadDeviceError; + goto except; + } + + // Initialize the selected device - uint32_t frames_per_ms = lrint((float) sampleRate / 1000.0f); - ctx->min_buffer = minBuffer * frames_per_ms; - ctx->max_buffer = maxBuffer * frames_per_ms; + AudioFormatFlags format_flags = kAudioFormatFlagIsPacked; + if (ctx->cmn.format.sampleFormat == MTY_AUDIO_SAMPLE_FORMAT_FLOAT) { + format_flags |= kAudioFormatFlagIsFloat; + + } else { + format_flags |= kAudioFormatFlagIsSignedInteger; + } AudioStreamBasicDescription format = {0}; - format.mSampleRate = sampleRate; + format.mSampleRate = ctx->cmn.format.sampleRate; format.mFormatID = kAudioFormatLinearPCM; - format.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + format.mFormatFlags = format_flags; format.mFramesPerPacket = 1; - format.mChannelsPerFrame = channels; - format.mBitsPerChannel = AUDIO_SAMPLE_SIZE * 8; - format.mBytesPerPacket = AUDIO_SAMPLE_SIZE * format.mChannelsPerFrame; + format.mChannelsPerFrame = ctx->cmn.format.channels; + format.mBitsPerChannel = ctx->cmn.computed.sample_size * 8; + format.mBytesPerPacket = ctx->cmn.computed.frame_size; format.mBytesPerFrame = format.mBytesPerPacket; - OSStatus e = AudioQueueNewOutput(&format, audio_queue_callback, ctx, NULL, NULL, 0, &ctx->q); + // Create a new audio queue, which by default chooses the device's default device + e = AudioQueueNewOutput(&format, audio_queue_callback, ctx, NULL, NULL, 0, &ctx->q); if (e != kAudioServicesNoError) { MTY_Log("'AudioQueueNewOutput' failed with error 0x%X", e); goto except; } + // Change the audio queue to be associated with the selected audio device + if (selected_device_uid) { + e = AudioQueueSetProperty(ctx->q, kAudioQueueProperty_CurrentDevice, + (const void *) &selected_device_uid, sizeof(CFStringRef)); + if (e != kAudioServicesNoError) { + MTY_Log("'AudioQueueSetProperty(kAudioQueueProperty_CurrentDevice)' failed with error 0x%X", e); + goto except; + } + } + + // Specify channel configuration + if (ctx->cmn.format.channelMask != 0) { + AudioChannelLayout channel_layout = { + .mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelBitmap, + + // Core Audio channel bitmap follows the same spec as WAVE format so we can pass in the mask as-is + .mChannelBitmap = ctx->cmn.format.channelMask, + }; + + e = AudioQueueSetProperty(ctx->q, kAudioQueueProperty_ChannelLayout, + (const void *) &channel_layout, sizeof(AudioChannelLayout)); + if (e != kAudioServicesNoError) { + MTY_Log("'AudioQueueSetProperty(kAudioQueueProperty_ChannelLayout)' failed with error 0x%X", e); + goto except; + } + } + for (int32_t x = 0; x < AUDIO_BUFS; x++) { - e = AudioQueueAllocateBuffer(ctx->q, AUDIO_BUF_SIZE(ctx), &ctx->audio_buf[x]); + e = AudioQueueAllocateBuffer(ctx->q, ctx->cmn.computed.buffer_size, &ctx->audio_buf[x]); if (e != kAudioServicesNoError) { MTY_Log("'AudioQueueAllocateBuffer' failed with error 0x%X", e); goto except; @@ -70,6 +200,28 @@ MTY_Audio *MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t max except: + if (selected_device_uid) + CFRelease(selected_device_uid); + + free(device_ids); + + return e; +} + +MTY_Audio *MTY_AudioCreate(MTY_AudioFormat format_in, uint32_t minBuffer, uint32_t maxBuffer, + const char *deviceID, bool fallback) +{ + // TODO Should this use the current run loop rather than internal threading? + + MTY_Audio *ctx = MTY_Alloc(1, sizeof(MTY_Audio)); + audio_common_init(&ctx->cmn, format_in, minBuffer, maxBuffer); + + OSStatus e = audio_device_create(ctx, deviceID); + + // Upon failure initializing the given device, try again with the system default device + if (e != kAudioServicesNoError && deviceID && deviceID[0] != 0) + e = audio_device_create(ctx, NULL); + if (e != kAudioServicesNoError) MTY_AudioDestroy(&ctx); @@ -104,7 +256,7 @@ static uint32_t audio_get_queued_frames(MTY_Audio *ctx) } } - return queued / (ctx->channels * AUDIO_SAMPLE_SIZE); + return queued / ctx->cmn.computed.frame_size; } static void audio_play(MTY_Audio *ctx) @@ -127,19 +279,19 @@ void MTY_AudioReset(MTY_Audio *ctx) uint32_t MTY_AudioGetQueued(MTY_Audio *ctx) { - return lrint((float) audio_get_queued_frames(ctx) / ((float) ctx->sample_rate / 1000.0f)); + return lrint((float) audio_get_queued_frames(ctx) / ((float) ctx->cmn.format.sampleRate / 1000.0f)); } void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) { - size_t size = count * ctx->channels * AUDIO_SAMPLE_SIZE; + size_t size = count * ctx->cmn.computed.frame_size; uint32_t queued = audio_get_queued_frames(ctx); // Stop playing and flush if we've exceeded the maximum buffer or underrun - if (ctx->playing && (queued > ctx->max_buffer || queued == 0)) + if (ctx->playing && (queued > ctx->cmn.computed.max_buffer || queued == 0)) MTY_AudioReset(ctx); - if (size <= AUDIO_BUF_SIZE(ctx)) { + if (size <= ctx->cmn.computed.buffer_size) { for (uint8_t x = 0; x < AUDIO_BUFS; x++) { if (MTY_Atomic32Get(&ctx->in_use[x]) == 0) { AudioQueueBufferRef buf = ctx->audio_buf[x]; @@ -160,7 +312,7 @@ void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) } // Begin playing again when the minimum buffer has been reached - if (!ctx->playing && queued + count >= ctx->min_buffer) + if (!ctx->playing && queued + count >= ctx->cmn.computed.min_buffer) audio_play(ctx); } } diff --git a/src/unix/linux/android/audio.c b/src/unix/linux/android/audio.c index bfad1d4e..5592aa94 100644 --- a/src/unix/linux/android/audio.c +++ b/src/unix/linux/android/audio.c @@ -10,23 +10,20 @@ #include -#define AUDIO_SAMPLE_SIZE sizeof(int16_t) - -#define AUDIO_BUF_SIZE(ctx) \ - ((ctx)->sample_rate * (ctx)->channels * AUDIO_SAMPLE_SIZE) +#include "audio-common.h" struct MTY_Audio { + struct audio_common cmn; + AAudioStreamBuilder *builder; AAudioStream *stream; MTY_Mutex *mutex; - uint32_t sample_rate; - uint32_t min_buffer; - uint32_t max_buffer; - uint8_t channels; bool flushing; bool playing; + uint32_t min_buffer_size; + uint32_t max_buffer_size; uint8_t *buffer; size_t size; }; @@ -43,7 +40,7 @@ static aaudio_data_callback_result_t audio_callback(AAudioStream *stream, void * MTY_MutexLock(ctx->mutex); - size_t want_size = numFrames * ctx->channels * AUDIO_SAMPLE_SIZE; + size_t want_size = numFrames * ctx->cmn.computed.frame_size; if (ctx->playing && ctx->size >= want_size) { memcpy(audioData, ctx->buffer, want_size); @@ -60,18 +57,16 @@ static aaudio_data_callback_result_t audio_callback(AAudioStream *stream, void * return AAUDIO_CALLBACK_RESULT_CONTINUE; } -MTY_Audio *MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t maxBuffer, uint8_t channels, - const char *deviceID, bool fallback) +MTY_Audio *MTY_AudioCreate(MTY_AudioFormat format, uint32_t minBuffer, + uint32_t maxBuffer, const char *deviceID, bool fallback) { MTY_Audio *ctx = MTY_Alloc(1, sizeof(MTY_Audio)); - ctx->channels = channels; - ctx->sample_rate = sampleRate; - ctx->mutex = MTY_MutexCreate(); - ctx->buffer = MTY_Alloc(AUDIO_BUF_SIZE(ctx), 1); + audio_common_init(&ctx->cmn, format, minBuffer, maxBuffer); + ctx->min_buffer_size = ctx->cmn.computed.min_buffer * ctx->cmn.computed.frame_size; + ctx->max_buffer_size = ctx->cmn.computed.max_buffer * ctx->cmn.computed.frame_size; - uint32_t frames_per_ms = lrint((float) sampleRate / 1000.0f); - ctx->min_buffer = minBuffer * frames_per_ms * ctx->channels * AUDIO_SAMPLE_SIZE; - ctx->max_buffer = maxBuffer * frames_per_ms * ctx->channels * AUDIO_SAMPLE_SIZE; + ctx->mutex = MTY_MutexCreate(); + ctx->buffer = MTY_Alloc(ctx->cmn.computed.buffer_size, 1); return ctx; } @@ -116,7 +111,7 @@ void MTY_AudioReset(MTY_Audio *ctx) uint32_t MTY_AudioGetQueued(MTY_Audio *ctx) { - return (ctx->size / (ctx->channels * AUDIO_SAMPLE_SIZE)) / ctx->sample_rate * 1000; + return (ctx->size / ctx->cmn.computed.frame_size) / ctx->cmn.format.sampleRate * 1000; } static void audio_start(MTY_Audio *ctx) @@ -124,9 +119,12 @@ static void audio_start(MTY_Audio *ctx) if (!ctx->builder) { AAudio_createStreamBuilder(&ctx->builder); AAudioStreamBuilder_setDeviceId(ctx->builder, AAUDIO_UNSPECIFIED); - AAudioStreamBuilder_setSampleRate(ctx->builder, ctx->sample_rate); - AAudioStreamBuilder_setChannelCount(ctx->builder, ctx->channels); - AAudioStreamBuilder_setFormat(ctx->builder, AAUDIO_FORMAT_PCM_I16); + AAudioStreamBuilder_setSampleRate(ctx->builder, ctx->cmn.format.sampleRate); + AAudioStreamBuilder_setChannelCount(ctx->builder, ctx->cmn.format.channels); + // XXX: Setting channel mask via AAudioStreamBuilder_setChannelMask requires bumping up android platform from 28 to 32. + // We have decided not to do this as of 11/06/2024 so as not to exclude a significant portion of users. + AAudioStreamBuilder_setFormat(ctx->builder, ctx->cmn.format.sampleFormat == MTY_AUDIO_SAMPLE_FORMAT_FLOAT + ? AAUDIO_FORMAT_PCM_FLOAT : AAUDIO_FORMAT_PCM_I16); AAudioStreamBuilder_setPerformanceMode(ctx->builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); AAudioStreamBuilder_setErrorCallback(ctx->builder, audio_error, ctx); AAudioStreamBuilder_setDataCallback(ctx->builder, audio_callback, ctx); @@ -140,16 +138,16 @@ static void audio_start(MTY_Audio *ctx) void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) { - size_t data_size = count * ctx->channels * AUDIO_SAMPLE_SIZE; + size_t data_size = count * ctx->cmn.computed.frame_size; audio_start(ctx); MTY_MutexLock(ctx->mutex); - if (ctx->size + data_size >= ctx->max_buffer) + if (ctx->size + data_size >= ctx->max_buffer_size) ctx->flushing = true; - size_t minimum_request = AAudioStream_getFramesPerBurst(ctx->stream) * ctx->channels * AUDIO_SAMPLE_SIZE; + size_t minimum_request = AAudioStream_getFramesPerBurst(ctx->stream) * ctx->cmn.computed.frame_size; if (ctx->flushing && ctx->size < minimum_request) { memset(ctx->buffer, 0, ctx->size); ctx->size = 0; @@ -160,12 +158,12 @@ void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) ctx->flushing = false; } - if (!ctx->flushing && data_size + ctx->size <= AUDIO_BUF_SIZE(ctx)) { + if (!ctx->flushing && data_size + ctx->size <= ctx->cmn.computed.buffer_size) { memcpy(ctx->buffer + ctx->size, frames, data_size); ctx->size += data_size; } - if (ctx->size >= ctx->min_buffer) + if (ctx->size >= ctx->min_buffer_size) ctx->playing = true; MTY_MutexUnlock(ctx->mutex); diff --git a/src/unix/linux/x11/audio.c b/src/unix/linux/x11/audio.c index 927f1a20..8c7ad240 100644 --- a/src/unix/linux/x11/audio.c +++ b/src/unix/linux/x11/audio.c @@ -9,36 +9,24 @@ #include "dl/libasound.h" -#define AUDIO_SAMPLE_SIZE sizeof(int16_t) - -#define AUDIO_BUF_SIZE(ctx) \ - ((ctx)->sample_rate * (ctx)->channels * AUDIO_SAMPLE_SIZE) +#include "audio-common.h" struct MTY_Audio { + struct audio_common cmn; snd_pcm_t *pcm; - bool playing; - uint32_t sample_rate; - uint32_t min_buffer; - uint32_t max_buffer; - uint8_t channels; uint8_t *buf; size_t pos; }; -MTY_Audio *MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t maxBuffer, uint8_t channels, - const char *deviceID, bool fallback) +MTY_Audio *MTY_AudioCreate(MTY_AudioFormat format, uint32_t minBuffer, + uint32_t maxBuffer, const char *deviceID, bool fallback) { if (!libasound_global_init()) return NULL; MTY_Audio *ctx = MTY_Alloc(1, sizeof(MTY_Audio)); - ctx->sample_rate = sampleRate; - ctx->channels = channels; - - uint32_t frames_per_ms = lrint((float) sampleRate / 1000.0f); - ctx->min_buffer = minBuffer * frames_per_ms; - ctx->max_buffer = maxBuffer * frames_per_ms; + audio_common_init(&ctx->cmn, format, minBuffer, maxBuffer); bool r = true; @@ -54,13 +42,19 @@ MTY_Audio *MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t max snd_pcm_hw_params_any(ctx->pcm, params); snd_pcm_hw_params_set_access(ctx->pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED); - snd_pcm_hw_params_set_format(ctx->pcm, params, SND_PCM_FORMAT_S16); - snd_pcm_hw_params_set_channels(ctx->pcm, params, channels); - snd_pcm_hw_params_set_rate(ctx->pcm, params, sampleRate, 0); + snd_pcm_hw_params_set_format(ctx->pcm, params, format.sampleFormat == MTY_AUDIO_SAMPLE_FORMAT_FLOAT + ? SND_PCM_FORMAT_FLOAT : SND_PCM_FORMAT_S16); + snd_pcm_hw_params_set_channels(ctx->pcm, params, format.channels); + // XXX: Channel config for ALSA can't be specified via the opaque `format.channelMask` + // Instead, an explicit channel mapping array is required. To be implemented in the future. + // See `snd_pcm_set_chmap`: + // 1. https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html#ga60ee7d2c2555e21dbc844a1b73839085 + // 2. https://gist.github.com/raydudu/5590a196b9446c709c58a03eff1f38bc + snd_pcm_hw_params_set_rate(ctx->pcm, params, format.sampleRate, 0); snd_pcm_hw_params(ctx->pcm, params); snd_pcm_nonblock(ctx->pcm, 1); - ctx->buf = MTY_Alloc(AUDIO_BUF_SIZE(ctx), 1); + ctx->buf = MTY_Alloc(ctx->cmn.computed.buffer_size, 1); except: @@ -87,7 +81,7 @@ void MTY_AudioDestroy(MTY_Audio **audio) static uint32_t audio_get_queued_frames(MTY_Audio *ctx) { - uint32_t queued = ctx->pos / (ctx->channels * AUDIO_SAMPLE_SIZE); + uint32_t queued = ctx->pos / ctx->cmn.computed.frame_size; if (ctx->playing) { snd_pcm_status_t *status = NULL; @@ -121,30 +115,30 @@ void MTY_AudioReset(MTY_Audio *ctx) uint32_t MTY_AudioGetQueued(MTY_Audio *ctx) { - return lrint((float) audio_get_queued_frames(ctx) / ((float) ctx->sample_rate / 1000.0f)); + return lrint((float) audio_get_queued_frames(ctx) / ((float) ctx->cmn.format.sampleRate / 1000.0f)); } void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) { - size_t size = count * ctx->channels * AUDIO_SAMPLE_SIZE; + size_t size = count * ctx->cmn.computed.frame_size; uint32_t queued = audio_get_queued_frames(ctx); // Stop playing and flush if we've exceeded the maximum buffer or underrun - if (ctx->playing && (queued > ctx->max_buffer || queued == 0)) + if (ctx->playing && (queued > ctx->cmn.computed.max_buffer || queued == 0)) MTY_AudioReset(ctx); - if (ctx->pos + size <= AUDIO_BUF_SIZE(ctx)) { - memcpy(ctx->buf + ctx->pos, frames, count * ctx->channels * AUDIO_SAMPLE_SIZE); + if (ctx->pos + size <= ctx->cmn.computed.buffer_size) { + memcpy(ctx->buf + ctx->pos, frames, count * ctx->cmn.computed.frame_size); ctx->pos += size; } // Begin playing again when the minimum buffer has been reached - if (!ctx->playing && queued + count >= ctx->min_buffer) + if (!ctx->playing && queued + count >= ctx->cmn.computed.min_buffer) audio_play(ctx); if (ctx->playing) { - int32_t e = snd_pcm_writei(ctx->pcm, ctx->buf, ctx->pos / (ctx->channels * AUDIO_SAMPLE_SIZE)); + int32_t e = snd_pcm_writei(ctx->pcm, ctx->buf, ctx->pos / ctx->cmn.computed.frame_size); if (e >= 0) { ctx->pos = 0; diff --git a/src/unix/web/matoya-worker.js b/src/unix/web/matoya-worker.js index 1dff330e..db1b241a 100644 --- a/src/unix/web/matoya-worker.js +++ b/src/unix/web/matoya-worker.js @@ -313,12 +313,12 @@ function mty_mutex_unlock(mutex, index, notify) { } const MTY_AUDIO_API = { - MTY_AudioCreate: function (sampleRate, minBuffer, maxBuffer, channels, deviceID, fallback) { + MTY_AudioCreate: function (format, minBuffer, maxBuffer, deviceID, fallback) { MTY.audio = { - sampleRate, + sampleRate: format.sampleRate, minBuffer, maxBuffer, - channels, + channels: format.channels, }; return 0xCDD; diff --git a/src/windows/audio.c b/src/windows/audio.c index 8bae7a7a..0ca5024d 100644 --- a/src/windows/audio.c +++ b/src/windows/audio.c @@ -9,30 +9,31 @@ #include #include -DEFINE_GUID(CLSID_MMDeviceEnumerator, 0xBCDE0395, 0xE52F, 0x467C, 0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E); -DEFINE_GUID(IID_IMMDeviceEnumerator, 0xA95664D2, 0x9614, 0x4F35, 0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6); -DEFINE_GUID(IID_IAudioClient, 0x1CB9AD4C, 0xDBFA, 0x4C32, 0xB1, 0x78, 0xC2, 0xF5, 0x68, 0xA7, 0x03, 0xB2); -DEFINE_GUID(IID_IAudioRenderClient, 0xF294ACFC, 0x3146, 0x4483, 0xA7, 0xBF, 0xAD, 0xDC, 0xA7, 0xC2, 0x60, 0xE2); -DEFINE_GUID(IID_IMMNotificationClient, 0x7991EEC9, 0x7E89, 0x4D85, 0x83, 0x90, 0x6C, 0x70, 0x3C, 0xEC, 0x60, 0xC0); +DEFINE_GUID(CLSID_MMDeviceEnumerator, 0xBCDE0395, 0xE52F, 0x467C, 0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E); +DEFINE_GUID(IID_IMMDeviceEnumerator, 0xA95664D2, 0x9614, 0x4F35, 0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6); +DEFINE_GUID(IID_IAudioClient, 0x1CB9AD4C, 0xDBFA, 0x4C32, 0xB1, 0x78, 0xC2, 0xF5, 0x68, 0xA7, 0x03, 0xB2); +DEFINE_GUID(IID_IAudioRenderClient, 0xF294ACFC, 0x3146, 0x4483, 0xA7, 0xBF, 0xAD, 0xDC, 0xA7, 0xC2, 0x60, 0xE2); +DEFINE_GUID(IID_IMMNotificationClient, 0x7991EEC9, 0x7E89, 0x4D85, 0x83, 0x90, 0x6C, 0x70, 0x3C, 0xEC, 0x60, 0xC0); +DEFINE_GUID(OWN_KSDATAFORMAT_SUBTYPE_PCM, 0x00000001, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); +DEFINE_GUID(OWN_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); #define COBJMACROS #include #include -#define AUDIO_SAMPLE_SIZE sizeof(int16_t) +#include "audio-common.h" + #define AUDIO_BUFFER_SIZE ((1 * 1000 * 1000 * 1000) / 100) // 1 second #define AUDIO_REINIT_MAX_TRIES 5 #define AUDIO_REINIT_DELAY_MS 100 struct MTY_Audio { + struct audio_common cmn; bool playing; bool notification_init; bool fallback; - uint32_t sample_rate; - uint32_t min_buffer; - uint32_t max_buffer; - uint8_t channels; + WORD bytes_per_frame; WCHAR *device_id; UINT32 buffer_size; IMMDeviceEnumerator *enumerator; @@ -181,7 +182,7 @@ static HRESULT audio_device_create(MTY_Audio *ctx) HRESULT e = S_OK; IMMDevice *device = NULL; - if (ctx->device_id) { + if (ctx->device_id && ctx->device_id[0]) { e = IMMDeviceEnumerator_GetDevice(ctx->enumerator, ctx->device_id, &device); if (e != S_OK && !ctx->fallback) { @@ -206,18 +207,28 @@ static HRESULT audio_device_create(MTY_Audio *ctx) WAVEFORMATEXTENSIBLE pwfx = { .Format.wFormatTag = WAVE_FORMAT_PCM, - .Format.nChannels = ctx->channels, - .Format.nSamplesPerSec = ctx->sample_rate, - .Format.wBitsPerSample = AUDIO_SAMPLE_SIZE * 8, - .Format.nBlockAlign = ctx->channels * AUDIO_SAMPLE_SIZE, - .Format.nAvgBytesPerSec = ctx->sample_rate * ctx->channels * AUDIO_SAMPLE_SIZE, + .Format.nChannels = (WORD) ctx->cmn.format.channels, + .Format.nSamplesPerSec = ctx->cmn.format.sampleRate, + .Format.wBitsPerSample = (WORD) ctx->cmn.computed.sample_size * 8, + .Format.nBlockAlign = (WORD) ctx->cmn.computed.frame_size, + .Format.nAvgBytesPerSec = (DWORD) ctx->cmn.computed.buffer_size, }; - // We must query extended data for greater than two channels - if (ctx->channels > 2) { - e = audio_get_extended_format(device, &pwfx); - if (e != S_OK) - goto except; + if (ctx->cmn.format.channels > 2) { + if (ctx->cmn.format.channelMask == 0) { + e = audio_get_extended_format(device, &pwfx); + if (e != S_OK) + goto except; + + } else { + pwfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + pwfx.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); + + pwfx.Samples.wValidBitsPerSample = pwfx.Format.wBitsPerSample; + pwfx.dwChannelMask = ctx->cmn.format.channelMask; + pwfx.SubFormat = ctx->cmn.format.sampleFormat == MTY_AUDIO_SAMPLE_FORMAT_FLOAT + ? OWN_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : OWN_KSDATAFORMAT_SUBTYPE_PCM; + } } e = IAudioClient_Initialize(ctx->client, AUDCLNT_SHAREMODE_SHARED, @@ -229,6 +240,8 @@ static HRESULT audio_device_create(MTY_Audio *ctx) goto except; } + ctx->bytes_per_frame = pwfx.Format.nBlockAlign; + e = IAudioClient_GetBufferSize(ctx->client, &ctx->buffer_size); if (e != S_OK) { MTY_Log("'IAudioClient_GetBufferSize' failed with HRESULT 0x%X", e); @@ -252,18 +265,13 @@ static HRESULT audio_device_create(MTY_Audio *ctx) return e; } -MTY_Audio *MTY_AudioCreate(uint32_t sampleRate, uint32_t minBuffer, uint32_t maxBuffer, uint8_t channels, - const char *deviceID, bool fallback) +MTY_Audio *MTY_AudioCreate(MTY_AudioFormat format, uint32_t minBuffer, + uint32_t maxBuffer, const char *deviceID, bool fallback) { MTY_Audio *ctx = MTY_Alloc(1, sizeof(MTY_Audio)); - ctx->sample_rate = sampleRate; - ctx->channels = channels; + audio_common_init(&ctx->cmn, format, minBuffer, maxBuffer); ctx->fallback = fallback; - uint32_t frames_per_ms = lrint((float) sampleRate / 1000.0f); - ctx->min_buffer = minBuffer * frames_per_ms; - ctx->max_buffer = maxBuffer * frames_per_ms; - if (deviceID) ctx->device_id = MTY_MultiToWideD(deviceID); @@ -422,7 +430,7 @@ uint32_t MTY_AudioGetQueued(MTY_Audio *ctx) { uint32_t queued = 0; audio_get_queued_frames(ctx, &queued); - return lrint((float) queued / ((float) ctx->sample_rate / 1000.0f)); + return lrint((float) queued / ((float) ctx->cmn.format.sampleRate / 1000.0f)); } void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) @@ -445,7 +453,7 @@ void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) } // Stop playing and flush if we've exceeded the maximum buffer or underrun - if (ctx->playing && (queued > ctx->max_buffer || queued == 0)) + if (ctx->playing && (queued > ctx->cmn.computed.max_buffer || queued == 0)) MTY_AudioReset(ctx); if (ctx->buffer_size - queued >= count) { @@ -453,12 +461,12 @@ void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count) e = IAudioRenderClient_GetBuffer(ctx->render, count, &buffer); if (e == S_OK) { - memcpy(buffer, frames, count * ctx->channels * AUDIO_SAMPLE_SIZE); + memcpy(buffer, frames, count * ctx->bytes_per_frame); IAudioRenderClient_ReleaseBuffer(ctx->render, count, 0); } // Begin playing again when the minimum buffer has been reached - if (!ctx->playing && queued + count >= ctx->min_buffer) + if (!ctx->playing && queued + count >= ctx->cmn.computed.min_buffer) audio_play(ctx); } }