Implement CoreAudio driver

This commit is contained in:
Andy Vandijck 2025-05-18 07:28:51 +02:00
parent ef85b71444
commit 79d3453544
9 changed files with 659 additions and 3 deletions

View File

@ -8,6 +8,7 @@ include(VbamFunctions)
set(VBAM_WX_COMMON
audio/audio.cpp
audio/audio.h
audio/internal/coreaudio.cpp
audio/internal/sdl.cpp
audio/internal/sdl.h
audio/internal/openal.cpp
@ -159,6 +160,9 @@ if(CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg")
endif()
if(APPLE)
find_library(COREAUDIO CoreAudio)
find_library(COREFOUNDATION CoreFoundation)
find_library(AUDIOTOOLBOX AudioToolbox)
find_library(METAL Metal)
find_library(METALKIT MetalKit)
endif()
@ -333,6 +337,10 @@ function(configure_wx_target target)
# Metal
if(APPLE)
_add_link_libraries(${COREFOUNDATION})
_add_link_libraries(${COREAUDIO})
_add_link_libraries(${AUDIOTOOLBOX})
if(CMAKE_Metal_COMPILER)
_add_link_libraries($<LINK_LIBRARY:WEAK_FRAMEWORK,${METAL}>)
_add_link_libraries($<LINK_LIBRARY:WEAK_FRAMEWORK,${METALKIT}>)

View File

@ -12,6 +12,10 @@
#include "wx/audio/internal/faudio.h"
#endif
#if defined(__WXMAC__)
#include "wx/audio/internal/coreaudio.h"
#endif
#if defined(VBAM_ENABLE_XAUDIO2)
#include "wx/audio/internal/xaudio2.h"
#endif
@ -41,6 +45,11 @@ std::vector<AudioDevice> EnumerateAudioDevices(const config::AudioApi& audio_api
return audio::internal::GetFAudioDevices();
#endif
#if defined(__WXMAC__)
case config::AudioApi::kCoreAudio:
return audio::internal::GetCoreAudioDevices();
#endif
case config::AudioApi::kLast:
default:
VBAM_NOTREACHED();
@ -71,6 +80,11 @@ std::unique_ptr<SoundDriver> CreateSoundDriver(const config::AudioApi& api) {
return audio::internal::CreateFAudioDriver();
#endif
#if defined(__WXMAC__)
case config::AudioApi::kCoreAudio:
return audio::internal::CreateCoreAudioDriver();
#endif
case config::AudioApi::kLast:
default:
VBAM_NOTREACHED();

View File

@ -0,0 +1,589 @@
#ifdef __APPLE__
#include "wx/audio/internal/coreaudio.h"
// === LOGALL writes very detailed informations to vba-trace.log ===
// #define LOGALL
// on win32 and mac, pointer typedefs only happen with AL_NO_PROTOTYPES
// on mac, ALC_NO_PROTOTYPES as well
// #define AL_NO_PROTOTYPES 1
// on mac, alc pointer typedefs ony happen for ALC if ALC_NO_PROTOTYPES
// unfortunately, there is a bug in the system headers (use of ALCvoid when
// void should be used; shame on Apple for introducing this error, and shame
// on Creative for making a typedef to void in the first place)
// #define ALC_NO_PROTOTYPES 1
#include <CoreAudio/CoreAudio.h>
#include <CoreFoundation/CoreFoundation.h>
#include <AudioToolbox/AudioToolbox.h>
#include <stdlib.h>
#include <memory.h>
#include <wx/arrstr.h>
#include <wx/log.h>
#include <wx/translation.h>
#include <wx/utils.h>
#include "core/base/sound_driver.h"
#include "core/base/check.h"
#include "core/gba/gbaGlobals.h"
#include "core/gba/gbaSound.h"
#include "core/base/ringbuffer.h"
#include "wx/config/option-proxy.h"
#ifndef kAudioObjectPropertyElementMain
#define kAudioObjectPropertyElementMain kAudioObjectPropertyElementMaster
#endif
#ifndef LOGALL
// replace logging functions with comments
#ifdef winlog
#undef winlog
#endif
// https://stackoverflow.com/a/1306690/262458
#define winlog(x, ...) \
do { \
} while (0)
#define debugState() //
#endif
extern int emulating;
namespace audio {
namespace internal {
namespace {
static const AudioObjectPropertyAddress devlist_address = {
kAudioHardwarePropertyDevices,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
static const AudioObjectPropertyAddress default_playback_device_address = {
kAudioHardwarePropertyDefaultOutputDevice,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
const AudioObjectPropertyAddress addr = {
kAudioDevicePropertyStreamConfiguration,
kAudioDevicePropertyScopeOutput,
kAudioObjectPropertyElementMain
};
const AudioObjectPropertyAddress nameaddr = {
kAudioObjectPropertyName,
kAudioDevicePropertyScopeOutput,
kAudioObjectPropertyElementMain
};
static const AudioObjectPropertyAddress alive_address = {
kAudioDevicePropertyDeviceIsAlive,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
class CoreAudioAudio : public SoundDriver {
public:
CoreAudioAudio();
~CoreAudioAudio() override;
bool init(long sampleRate) override; // initialize the sound buffer queue
void deinit();
void setThrottle(unsigned short throttle_) override; // set game speed
void pause() override; // pause the secondary sound buffer
void reset() override; // stop and reset the secondary sound buffer
void resume() override; // play/resume the secondary sound buffer
void write(uint16_t* finalWave, int length) override; // write the emulated sound to a sound buffer
AudioStreamBasicDescription description;
AudioQueueBufferRef buffer = NULL;
AudioQueueBufferRef *buffers = NULL;
AudioQueueRef audioQueue = NULL;
AudioDeviceID device = 0;
bool must_wait = false;
uint16_t current_rate = 0;
int current_buffer = 0;
AudioTimeStamp timestamp;
private:
int soundBufferLen = 0;
AudioDeviceID GetCoreAudioDevice(wxString name);
void setBuffer(uint16_t* finalWave, int length);
bool initialized = false;
};
static void PlaybackBufferReadyCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
{
int bufIndex = 0;
CoreAudioAudio *cadevice = (CoreAudioAudio *)inUserData;
for (int i = 0; i < OPTION(kSoundBuffers); i++)
{
if (inBuffer == cadevice->buffers[i])
{
bufIndex = i;
break;
}
}
cadevice->buffer = cadevice->buffers[bufIndex];
// buffer is unexpectedly here? We're probably dying, but try to requeue this buffer with silence.
if (cadevice->buffer) {
memset(cadevice->buffer->mAudioData, 0, cadevice->buffer->mAudioDataBytesCapacity);
cadevice->buffer->mAudioDataByteSize = 0;
}
if ((OPTION(kSoundBuffers) - 1) >= bufIndex) {
cadevice->must_wait = false;
cadevice->current_buffer = 0;
}
}
static OSStatus DeviceAliveNotification(AudioObjectID devid, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data)
{
CoreAudioAudio *cadevice = (CoreAudioAudio *)data;
UInt32 alive = 1;
UInt32 size = sizeof(alive);
const OSStatus error = AudioObjectGetPropertyData(devid, addrs, 0, NULL, &size, &alive);
(void)num_addr;
bool dead = false;
if (error == kAudioHardwareBadDeviceError) {
dead = true; // device was unplugged.
} else if ((error == kAudioHardwareNoError) && (!alive)) {
dead = true; // device died in some other way.
}
if (dead) {
cadevice->reset();
}
return noErr;
}
AudioDeviceID CoreAudioAudio::GetCoreAudioDevice(wxString name)
{
uint32_t size = 0;
AudioDeviceID dev = 0;
AudioDeviceID *devs = NULL;
AudioBufferList *buflist = NULL;
OSStatus result = 0;
CFStringRef cfstr = NULL;
if (name == _("Default device")) {
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &default_playback_device_address, 0, NULL, &size) != kAudioHardwareNoError) {
return 0;
} else if ((devs = (AudioDeviceID *)malloc(size)) == NULL) {
return 0;
} else if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &default_playback_device_address, 0, NULL, &size, devs) != kAudioHardwareNoError) {
free(devs);
return 0;
}
dev = devs[0];
free(devs);
return dev;
} else {
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size) != kAudioHardwareNoError) {
return 0;
} else if ((devs = (AudioDeviceID *)malloc(size)) == NULL) {
return 0;
} else if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size, devs) != kAudioHardwareNoError) {
free(devs);
return 0;
}
const UInt32 total_devices = (UInt32) (size / sizeof(AudioDeviceID));
for (UInt32 i = 0; i < total_devices; i++)
{
if (AudioObjectGetPropertyDataSize(devs[i], &addr, 0, NULL, &size) != noErr) {
continue;
} else if ((buflist = (AudioBufferList *)malloc(size)) == NULL) {
continue;
}
result = AudioObjectGetPropertyData(devs[i], &addr, 0, NULL, &size, buflist);
if (result != noErr) {
free(buflist);
continue;
}
if (buflist->mNumberBuffers == 0) {
free(buflist);
continue;
}
size = sizeof(CFStringRef);
if (AudioObjectGetPropertyData(devs[i], &nameaddr, 0, NULL, &size, &cfstr) != kAudioHardwareNoError) {
free(buflist);
continue;
}
CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8);
const char *name = (const char *)malloc(len + 1);
CFStringGetCString(cfstr, (char *)name, len + 1, kCFStringEncodingUTF8);
if (name != NULL)
{
const wxString device_name(name, wxConvLibc);
if (device_name == name) {
dev = devs[i];
free(devs);
free(buflist);
free((void *)name);
return dev;
}
}
free(buflist);
free((void *)name);
}
}
return 0;
}
CoreAudioAudio::CoreAudioAudio():
current_rate(static_cast<unsigned short>(coreOptions.throttle)),
initialized(false)
{}
void CoreAudioAudio::deinit() {
if (!initialized)
return;
AudioObjectRemovePropertyListener(device, &alive_address, DeviceAliveNotification, this);
for (int i = 0; i < OPTION(kSoundBuffers); i++) {
AudioQueueFreeBuffer(audioQueue, buffers[i]);
}
buffer = NULL;
AudioQueueStop(audioQueue, TRUE);
device = 0;
initialized = false;
}
CoreAudioAudio::~CoreAudioAudio() {
deinit();
}
static bool AssignDeviceToAudioQueue(CoreAudioAudio *cadevice)
{
const AudioObjectPropertyAddress prop = {
kAudioDevicePropertyDeviceUID,
kAudioDevicePropertyScopeOutput,
kAudioObjectPropertyElementMain
};
OSStatus result;
CFStringRef devuid;
UInt32 devuidsize = sizeof(devuid);
result = AudioObjectGetPropertyData(cadevice->device, &prop, 0, NULL, &devuidsize, &devuid);
if (result != noErr) {
return false;
}
result = AudioQueueSetProperty(cadevice->audioQueue, kAudioQueueProperty_CurrentDevice, &devuid, devuidsize);
if (result != noErr) {
return false;
}
CFRelease(devuid); // Release devuid; we're done with it and AudioQueueSetProperty should have retained if it wants to keep it.
return (bool)(result == noErr);
}
static bool PrepareDevice(CoreAudioAudio *cadevice)
{
const AudioDeviceID devid = cadevice->device;
OSStatus result = noErr;
UInt32 size = 0;
AudioObjectPropertyAddress addr = {
0,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
UInt32 alive = 0;
size = sizeof(alive);
addr.mSelector = kAudioDevicePropertyDeviceIsAlive;
addr.mScope = kAudioDevicePropertyScopeOutput;
result = AudioObjectGetPropertyData(devid, &addr, 0, NULL, &size, &alive);
if (result != noErr) {
return false;
}
if (!alive) {
return false;
}
// some devices don't support this property, so errors are fine here.
pid_t pid = 0;
size = sizeof(pid);
addr.mSelector = kAudioDevicePropertyHogMode;
result = AudioObjectGetPropertyData(devid, &addr, 0, NULL, &size, &pid);
if ((result == noErr) && (pid != -1)) {
return false;
}
return true;
}
bool CoreAudioAudio::init(long sampleRate) {
OSStatus result = 0;
if (initialized) {
deinit();
}
AudioChannelLayout layout;
memset(&layout, 0, sizeof(layout));
device = GetCoreAudioDevice(OPTION(kSoundAudioDevice));
if (device == 0) {
fprintf(stderr, "Couldn't get Core Audio device\n");
return false;
}
description.mFormatID = kAudioFormatLinearPCM;
description.mFormatFlags = kLinearPCMFormatFlagIsPacked;
description.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
description.mChannelsPerFrame = 2;
description.mBitsPerChannel = 16;
description.mSampleRate = current_rate ? static_cast<UInt32>(sampleRate * (current_rate / 100.0)) : static_cast<UInt32>(sampleRate);
description.mFramesPerPacket = 1;
description.mBytesPerFrame = description.mChannelsPerFrame * (description.mBitsPerChannel / 8);
description.mBytesPerPacket = description.mBytesPerFrame * description.mFramesPerPacket;
soundBufferLen = (sampleRate / 60) * description.mBytesPerPacket;
PrepareDevice(this);
AudioObjectAddPropertyListener(device, &alive_address, DeviceAliveNotification, this);
result = AudioQueueNewOutput(&description, PlaybackBufferReadyCallback, this, NULL, NULL, 0, &audioQueue);
if (result != noErr) {
return false;
}
AssignDeviceToAudioQueue(this);
layout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
result = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &layout, sizeof(layout));
if (result != noErr) {
return false;
}
buffers = (AudioQueueBufferRef *)calloc(OPTION(kSoundBuffers), sizeof(AudioQueueBufferRef));
for (int i = 0; i < OPTION(kSoundBuffers); i++) {
result = AudioQueueAllocateBuffer(audioQueue, soundBufferLen, &buffers[i]);
if (result != noErr) {
return false;
}
memset(buffers[i]->mAudioData, 0x00, buffers[i]->mAudioDataBytesCapacity);
buffers[i]->mAudioDataByteSize = buffers[i]->mAudioDataBytesCapacity;
PlaybackBufferReadyCallback(this, audioQueue, buffers[i]);
buffers[i]->mAudioDataByteSize = 0;
}
result = AudioQueueStart(audioQueue, NULL);
return initialized = true;
}
void CoreAudioAudio::setThrottle(unsigned short throttle_) {
if (!initialized)
return;
if (throttle_ == 0)
throttle_ = 200;
current_rate = throttle_;
reset();
}
void CoreAudioAudio::resume() {
if (!initialized)
return;
AudioQueueDeviceGetNearestStartTime(audioQueue, &timestamp, 0);
AudioQueueStart(audioQueue, NULL);
winlog("CoreAudioAudio::resume\n");
}
void CoreAudioAudio::pause() {
if (!initialized)
return;
AudioQueuePause(audioQueue);
winlog("CoreAudioAudio::pause\n");
}
void CoreAudioAudio::reset() {
if (!initialized)
return;
winlog("CoreAudioAudio::reset\n");
init(soundGetSampleRate());
}
void CoreAudioAudio::setBuffer(uint16_t* finalWave, int length) {
AudioQueueBufferRef this_buf = NULL;
this_buf = buffers[current_buffer];
if (this_buf == NULL) {
fprintf(stderr, "Current buffer is NULL\n");
return;
}
memcpy((uint8_t *)this_buf->mAudioData + this_buf->mAudioDataByteSize, finalWave, length);
this_buf->mAudioDataByteSize += (UInt32)length;
if (this_buf->mAudioDataByteSize == this_buf->mAudioDataBytesCapacity) {
AudioQueueEnqueueBufferWithParameters(audioQueue, this_buf, 0, NULL, 0, 0, 0, NULL, NULL, &timestamp);
}
must_wait = true;
}
void CoreAudioAudio::write(uint16_t* finalWave, int length) {
std::size_t samples = length / (description.mBitsPerChannel / 8);
std::size_t avail = 0;
if (!initialized)
return;
while ((avail = ((buffers[current_buffer]->mAudioDataBytesCapacity - buffers[current_buffer]->mAudioDataByteSize) / (description.mBitsPerChannel / 8))) < samples)
{
setBuffer(finalWave, (avail * (description.mBitsPerChannel / 8)));
finalWave += (avail * (description.mBitsPerChannel / 8));
samples -= avail;
if (buffers[current_buffer]->mAudioDataByteSize == buffers[current_buffer]->mAudioDataBytesCapacity) {
current_buffer++;
}
while ((current_buffer >= (OPTION(kSoundBuffers) - 1)) && must_wait) {
wxMilliSleep(1);
}
}
setBuffer(finalWave, samples * (description.mBitsPerChannel / 8));
if (buffers[current_buffer]->mAudioDataByteSize == buffers[current_buffer]->mAudioDataBytesCapacity) {
current_buffer++;
}
while ((current_buffer >= (OPTION(kSoundBuffers) - 1)) && must_wait) {
wxMilliSleep(1);
}
}
} // namespace
std::vector<AudioDevice> GetCoreAudioDevices() {
std::vector<AudioDevice> devices;
uint32_t size = 0;
AudioDeviceID *devs = NULL;
AudioBufferList *buflist = NULL;
OSStatus result = 0;
CFStringRef cfstr = NULL;
devices.push_back({_("Default device"), wxEmptyString});
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size) != kAudioHardwareNoError) {
return devices;
} else if ((devs = (AudioDeviceID *)malloc(size)) == NULL) {
return devices;
} else if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size, devs) != kAudioHardwareNoError) {
free(devs);
return devices;
}
const UInt32 total_devices = (UInt32) (size / sizeof(AudioDeviceID));
for (UInt32 i = 0; i < total_devices; i++)
{
if (AudioObjectGetPropertyDataSize(devs[i], &addr, 0, NULL, &size) != noErr) {
continue;
} else if ((buflist = (AudioBufferList *)malloc(size)) == NULL) {
continue;
}
result = AudioObjectGetPropertyData(devs[i], &addr, 0, NULL, &size, buflist);
if (result != noErr) {
free(buflist);
continue;
}
if (buflist->mNumberBuffers == 0) {
free(buflist);
continue;
}
size = sizeof(CFStringRef);
if (AudioObjectGetPropertyData(devs[i], &nameaddr, 0, NULL, &size, &cfstr) != kAudioHardwareNoError) {
free(buflist);
continue;
}
CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8);
const char *name = (const char *)malloc(len + 1);
CFStringGetCString(cfstr, (char *)name, len + 1, kCFStringEncodingUTF8);
if (name != NULL)
{
const wxString device_name(name, wxConvLibc);
devices.push_back({device_name, device_name});
}
free(buflist);
free((void *)name);
}
return devices;
}
std::unique_ptr<SoundDriver> CreateCoreAudioDriver() {
winlog("newCoreAudio\n");
return std::make_unique<CoreAudioAudio>();
}
} // namespace internal
} // namespace audio
#endif

View File

@ -0,0 +1,18 @@
#ifndef WX_AUDIO_INTERNAL_COREAUDIO_H_
#define WX_AUDIO_INTERNAL_COREAUDIO_H_
#include "wx/audio/audio.h"
namespace audio {
namespace internal {
// Returns the set of OpenAL devices.
std::vector<AudioDevice> GetCoreAudioDevices();
// Creates an OpenAL sound driver.
std::unique_ptr<SoundDriver> CreateCoreAudioDriver();
} // namespace internal
} // namespace audio
#endif // WX_AUDIO_INTERNAL_COREAUDIO_H_

View File

@ -98,6 +98,9 @@ static const std::array<wxString, kNbAudioApis> kAudioApiStrings = {
#if defined(VBAM_ENABLE_FAUDIO)
"faudio",
#endif
#if defined(__WXMAC__)
"coreaudio",
#endif
};
// These MUST follow the same order as the definitions of the enum above.
@ -222,8 +225,12 @@ std::array<Option, kNbOptions>& Option::All() {
/// Sound
#if defined(VBAM_ENABLE_XAUDIO2)
AudioApi audio_api = AudioApi::kXAudio2;
#else
#if defined(__WXMAC__)
AudioApi audio_api = AudioApi::kCoreAudio;
#else
AudioApi audio_api = AudioApi::kOpenAL;
#endif
#endif
wxString audio_dev;
// 10 fixes stuttering on mac with openal, as opposed to 5

View File

@ -94,6 +94,9 @@ enum class AudioApi {
#if defined(VBAM_ENABLE_FAUDIO)
kFAudio,
#endif // VBAM_ENABLE_FAUDIO
#if defined(__WXMAC__)
kCoreAudio,
#endif
// Do not add anything under here.
kLast,

View File

@ -194,6 +194,16 @@ SoundConfig::SoundConfig(wxWindow* parent) : BaseDialog(parent, "SoundConfig") {
audio_api_button->Hide();
#endif
audio_api_button = GetValidatedChild("CoreAudio");
#if defined(__WXMAC__)
audio_api_button->SetValidator(AudioApiValidator(config::AudioApi::kCoreAudio));
audio_api_button->Bind(wxEVT_RADIOBUTTON,
std::bind(&SoundConfig::OnAudioApiChanged, this, std::placeholders::_1,
config::AudioApi::kCoreAudio));
#else
audio_api_button->Hide();
#endif
// Upmix configuration.
upmix_checkbox_ = GetValidatedChild<wxCheckBox>("Upmix");
#if defined(VBAM_ENABLE_XAUDIO2) || defined(VBAM_ENABLE_FAUDIO)

View File

@ -64,9 +64,9 @@ protected:
void DrawingPanelInit() override;
private:
SDL_Window *sdlwindow;
SDL_Texture *texture;
SDL_Renderer *renderer;
SDL_Window *sdlwindow = NULL;
SDL_Texture *texture = NULL;
SDL_Renderer *renderer = NULL;
};
#if defined(__WXMSW__) && !defined(NO_D3D)

View File

@ -135,6 +135,13 @@
<flag>wxALL|wxEXPAND</flag>
<border>5</border>
</object>
<object class="sizeritem">
<object class="wxRadioButton" name="CoreAudio">
<label translate="0">CoreAudio</label>
</object>
<flag>wxALL|wxEXPAND</flag>
<border>5</border>
</object>
<orient>wxHORIZONTAL</orient>
</object>
<flag>wxEXPAND</flag>