Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-Channel Audio playback #96

Open
wants to merge 38 commits into
base: stable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f83fe57
Update Audio API to support surround sound audio playback. Only effec…
dvijayak Jan 9, 2024
0d6be6c
TEST: Replace IMMDevice_OpenPropertyStore with IAudioClient_GetMixFor…
dvijayak Jan 9, 2024
3cbcba7
Windows WASAPI device enumeration should handle empty string device I…
dvijayak Jan 12, 2024
f7fa9bc
MTY_AudioCreate now takes a new struct MTY_AudioFormat which is the s…
dvijayak Jan 13, 2024
df0b5a0
Broke PS5 haptics. Trying to fix it.
dvijayak Jan 22, 2024
ca24407
MacOS playback of multichannel audio.
dvijayak Jan 25, 2024
abb3bed
Make sure multichannel audio changes work properly for Linux and Andr…
dvijayak Feb 23, 2024
2a74565
Merge remote-tracking branch 'parsec/stable' into multi-channel-audio
dvijayak May 9, 2024
1d5f23d
how did this compile before, i wonder
bmcnett Jun 10, 2024
d69fbf9
Merge remote-tracking branch 'origin/stable' into multi-channel-audio
bmcnett Jul 15, 2024
57fecba
fix android build
bmcnett Jul 15, 2024
ccaa7e0
Merge remote-tracking branch 'parsec/stable' into multi-channel-audio
dvijayak Sep 13, 2024
4404366
Code style changes from code review
dvijayak Sep 13, 2024
6de5d37
Consistent name style
dvijayak Sep 13, 2024
be910bf
Include channel mask for android audio init.
dvijayak Sep 13, 2024
2ec81d9
Added note for linux that channelsMask is insufficient to define chan…
dvijayak Sep 13, 2024
39066f6
Merge remote-tracking branch 'parsec/stable' into multi-channel-audio
dvijayak Oct 9, 2024
d047c19
Fix android build failure...leaving comment for Ronald.
dvijayak Oct 9, 2024
378fe21
Merge remote-tracking branch 'parsec/stable' into multi-channel-audio
dvijayak Oct 15, 2024
196e81f
Sundry code review comments
dvijayak Oct 15, 2024
efebea0
Whoops compiler error
dvijayak Oct 15, 2024
88409c4
Formatting.
RonaldH-Parsec Oct 24, 2024
58bc760
Important renaming from frames to samples.
dvijayak Oct 30, 2024
ff37e64
Address Callum's code review comments
dvijayak Nov 1, 2024
2c4b889
Refactored common audio module init code.
dvijayak Nov 6, 2024
ade1148
For consistency with external literature, rename "channelsMask" to "c…
dvijayak Nov 6, 2024
6e883d4
Address code review comments
dvijayak Nov 27, 2024
07a7899
Rename stats -> computed
dvijayak Nov 27, 2024
1190e83
Include orders
dvijayak Nov 27, 2024
89378c1
Reorder a few things
dvijayak Dec 2, 2024
2eaefcc
Fixed one bug in macos audio.c and made some style changes.
dvijayak Dec 2, 2024
d973a82
A little more cleanup
dvijayak Dec 3, 2024
29f1f8f
Got rid of strictly unnecessary diffs to make reviewing easier.
dvijayak Dec 6, 2024
3dda3f7
MTY CI
dvijayak Dec 6, 2024
0bc0af9
Remove some more strictly unnecessary diffs
dvijayak Dec 10, 2024
53d48be
Merge remote-tracking branch 'origin/stable' into multi-channel-audio
dvijayak Dec 17, 2024
fdaeca4
It makes much more sense (and was trivial) to have the audio channel …
dvijayak Dec 17, 2024
6341fb1
Fix magic constant
dvijayak Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions src/matoya.h
Original file line number Diff line number Diff line change
Expand Up @@ -1444,30 +1444,48 @@ 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_FLOAT = 1, ///< 32-bit floating point.
MTY_AUDIO_SAMPLE_FORMAT_INT16 = 2, ///< 16-bit signed integer.
} MTY_AudioSampleFormat;
dvijayak marked this conversation as resolved.
Show resolved Hide resolved

/// @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 channelsMask; ///< Opaque bitmask that defines which channels are present in the audio data.
///< Follows standard surround sound speaker ids (see
///< https://en.wikipedia.org/wiki/Surround_sound#Standard_speaker_channels).
///< For `channels` > 2, specify this value correctly in order
///< to yield accurate audio playback.
} 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 supported, the function will fail and return NULL.
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
/// This parameter is a required argument and MUST NOT be 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
/// false to cause this function to fail and return NULL. Windows only.
/// @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,
const char *deviceID, bool fallback);
MTY_AudioCreate(const MTY_AudioFormat *format, uint32_t minBuffer, uint32_t maxBuffer, const char *deviceID, bool fallback);

/// @brief Destroy an MTY_Audio context.
/// @param audio Passed by reference and set to NULL after being destroyed.
Expand All @@ -1489,11 +1507,12 @@ 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);

Expand Down
202 changes: 176 additions & 26 deletions src/unix/apple/audio.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@

#include "matoya.h"

#include <CoreAudio/CoreAudio.h>
#include <AudioToolbox/AudioToolbox.h>

#define AUDIO_SAMPLE_SIZE sizeof(int16_t)
#define AUDIO_SAMPLE_SIZE(fmt) ((fmt) == MTY_AUDIO_SAMPLE_FORMAT_FLOAT ? sizeof(float) : sizeof(int16_t))
#define AUDIO_BUFS 64

#define AUDIO_BUF_SIZE(ctx) \
((ctx)->sample_rate * (ctx)->channels * AUDIO_SAMPLE_SIZE)
((ctx)->sample_rate * (ctx)->channels * AUDIO_SAMPLE_SIZE((ctx)->sample_format))

#define AUDIO_HW_ERROR(e) ((e) != kAudioHardwareNoError)
#define AUDIO_SV_ERROR(e) ((e) != kAudioServicesNoError)

struct MTY_Audio {
AudioQueueRef q;
AudioQueueBufferRef audio_buf[AUDIO_BUFS];
MTY_Atomic32 in_use[AUDIO_BUFS];
MTY_AudioSampleFormat sample_format;
uint32_t sample_rate;
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
uint32_t min_buffer;
uint32_t max_buffer;
uint8_t channels;
uint32_t channelsMask;
bool playing;
};

Expand All @@ -31,46 +37,190 @@ 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?
OSStatus e = kAudioHardwareNoError;
char *uid = NULL;
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
CFStringRef uid_cf = NULL;

UInt32 data_size = sizeof(CFStringRef);
AudioObjectPropertyAddress propAddr = {
.mSelector = kAudioDevicePropertyDeviceUID,
.mScope = prop_scope,
.mElement = prop_element,
};

e = AudioObjectGetPropertyData(device, &propAddr, 0, NULL, &data_size, &uid_cf);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
if (AUDIO_HW_ERROR(e))
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);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
if (!CFStringGetCString(uid_cf, uid, prop_size + 1, kCFStringEncodingUTF8)) {
e = kAudioHardwareBadDeviceError;
goto except;
}

uint32_t frames_per_ms = lrint((float) sampleRate / 1000.0f);
ctx->min_buffer = minBuffer * frames_per_ms;
ctx->max_buffer = maxBuffer * frames_per_ms;
// OK
*out_uid = uid;
*out_uid_cf = uid_cf;

except:

if (AUDIO_HW_ERROR(e)) {
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;
OSStatus e = kAudioHardwareNoError;
dvijayak marked this conversation as resolved.
Show resolved Hide resolved

// 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;
e = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propAddr, 0, NULL, &data_size);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
if (AUDIO_HW_ERROR(e))
goto except;
dvijayak marked this conversation as resolved.
Show resolved Hide resolved

device_ids = calloc(1, data_size);
e = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propAddr, 0, NULL, &data_size, device_ids);
if (AUDIO_HW_ERROR(e))
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;

if (AUDIO_HW_ERROR(audio_object_get_device_uid(device_ids[i], propAddr.mScope, propAddr.mElement, &uid, &uid_cf)))
continue;

if (!strcmp(deviceID, uid)) {
selected_device = &device_ids[i];
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
selected_device_uid = uid_cf;
uid_cf = NULL;
}

if (uid_cf)
CFRelease(uid_cf);
free(uid);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
}
}

if (!selected_device) {
e = kAudioHardwareBadDeviceError;
goto except;
}

// Initialize the selected device

AudioStreamBasicDescription format = {0};
format.mSampleRate = sampleRate;
format.mSampleRate = ctx->sample_rate;
format.mFormatID = kAudioFormatLinearPCM;
format.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
format.mFormatFlags = (ctx->sample_format == MTY_AUDIO_SAMPLE_FORMAT_FLOAT ? kAudioFormatFlagIsFloat : kAudioFormatFlagIsSignedInteger) | kAudioFormatFlagIsPacked;
format.mFramesPerPacket = 1;
format.mChannelsPerFrame = channels;
format.mBitsPerChannel = AUDIO_SAMPLE_SIZE * 8;
format.mBytesPerPacket = AUDIO_SAMPLE_SIZE * format.mChannelsPerFrame;
format.mChannelsPerFrame = ctx->channels;
format.mBitsPerChannel = AUDIO_SAMPLE_SIZE(ctx->sample_format) * 8;
format.mBytesPerPacket = AUDIO_SAMPLE_SIZE(ctx->sample_format) * format.mChannelsPerFrame;
format.mBytesPerFrame = format.mBytesPerPacket;

OSStatus e = AudioQueueNewOutput(&format, audio_queue_callback, ctx, NULL, NULL, 0, &ctx->q);
if (e != kAudioServicesNoError) {
// 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 (AUDIO_SV_ERROR(e)) {
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 (AUDIO_SV_ERROR(e)) {
MTY_Log("'AudioQueueSetProperty(kAudioQueueProperty_CurrentDevice)' failed with error 0x%X", e);
goto except;
}
}

// Specify channel configuration
if (ctx->channelsMask) {
AudioChannelLayout channel_layout = {
.mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelBitmap,
.mChannelBitmap = ctx->channelsMask, // Core Audio channel bitmap follows the spec that the WAVE format follows, so we can simply pass in the mask as-is
};

e = AudioQueueSetProperty(ctx->q, kAudioQueueProperty_ChannelLayout, (const void *) &channel_layout, sizeof(AudioChannelLayout));
if (AUDIO_SV_ERROR(e)) {
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]);
if (e != kAudioServicesNoError) {
if (AUDIO_SV_ERROR(e)) {
MTY_Log("'AudioQueueAllocateBuffer' failed with error 0x%X", e);
goto except;
}
}

except:

if (e != kAudioServicesNoError)
if (selected_device_uid)
CFRelease(selected_device_uid);
free(device_ids);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved

return e;
}

MTY_Audio *MTY_AudioCreate(const 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));
ctx->sample_format = format_in->sampleFormat;
ctx->sample_rate = format_in->sampleRate;
ctx->channels = format_in->channels;
ctx->channelsMask = format_in->channelsMask;

uint32_t frames_per_ms = lrint((float) format_in->sampleRate / 1000.0f);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved
ctx->min_buffer = minBuffer * frames_per_ms;
ctx->max_buffer = maxBuffer * frames_per_ms;

OSStatus e = audio_device_create(ctx, deviceID);

// Upon failure initializing the given device, try again with the system default device
if (AUDIO_SV_ERROR(e))
if (deviceID && deviceID[0])
e = audio_device_create(ctx, NULL);

if (AUDIO_SV_ERROR(e))
MTY_AudioDestroy(&ctx);
dvijayak marked this conversation as resolved.
Show resolved Hide resolved

return ctx;
Expand All @@ -85,7 +235,7 @@ void MTY_AudioDestroy(MTY_Audio **audio)

if (ctx->q) {
OSStatus e = AudioQueueDispose(ctx->q, true);
if (e != kAudioServicesNoError)
if (AUDIO_SV_ERROR(e))
MTY_Log("'AudioQueueDispose' failed with error 0x%X", e);
}

Expand All @@ -104,15 +254,15 @@ static uint32_t audio_get_queued_frames(MTY_Audio *ctx)
}
}

return queued / (ctx->channels * AUDIO_SAMPLE_SIZE);
return queued / (ctx->channels * AUDIO_SAMPLE_SIZE(ctx->sample_format));
}

static void audio_play(MTY_Audio *ctx)
{
if (ctx->playing)
return;

if (AudioQueueStart(ctx->q, NULL) == kAudioServicesNoError)
if (!AUDIO_SV_ERROR(AudioQueueStart(ctx->q, NULL)))
ctx->playing = true;
}

Expand All @@ -121,7 +271,7 @@ void MTY_AudioReset(MTY_Audio *ctx)
if (!ctx->playing)
return;

if (AudioQueueStop(ctx->q, true) == kAudioServicesNoError)
if (!AUDIO_SV_ERROR(AudioQueueStop(ctx->q, true)))
ctx->playing = false;
}

Expand All @@ -132,7 +282,7 @@ uint32_t MTY_AudioGetQueued(MTY_Audio *ctx)

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->channels * AUDIO_SAMPLE_SIZE(ctx->sample_format);
uint32_t queued = audio_get_queued_frames(ctx);

// Stop playing and flush if we've exceeded the maximum buffer or underrun
Expand All @@ -149,7 +299,7 @@ void MTY_AudioQueue(MTY_Audio *ctx, const int16_t *frames, uint32_t count)
buf->mUserData = (void *) (uintptr_t) x;

OSStatus e = AudioQueueEnqueueBuffer(ctx->q, buf, 0, NULL);
if (e == kAudioServicesNoError) {
if (!AUDIO_SV_ERROR(kAudioServicesNoError)) {
MTY_Atomic32Set(&ctx->in_use[x], 1);

} else {
Expand Down
Loading