mirror of
https://github.com/OpenEmu/OpenEmuKit.git
synced 2025-11-01 11:08:14 +00:00
275 lines
9.1 KiB
Objective-C
275 lines
9.1 KiB
Objective-C
// Copyright (c) 2019, OpenEmu Team
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// * Neither the name of the OpenEmu Team nor the
|
|
// names of its contributors may be used to endorse or promote products
|
|
// derived from this software without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
// DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
#import "OEGameAudio.h"
|
|
#import "OEAudioUnit.h"
|
|
#import "OELogging.h"
|
|
|
|
@import OpenEmuBase;
|
|
@import AudioToolbox;
|
|
@import AVFoundation;
|
|
@import CoreAudioKit;
|
|
|
|
@implementation OEGameAudio {
|
|
AVAudioEngine *_engine;
|
|
id _token;
|
|
AVAudioUnitGenerator *_gen;
|
|
__weak OEGameCore *_gameCore;
|
|
BOOL _outputDeviceIsDefault;
|
|
BOOL _running; // specifies the expected state of OEGameAudio
|
|
}
|
|
|
|
- (id)initWithCore:(OEGameCore *)core
|
|
{
|
|
if((self = [super init]) == nil)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
[OEAudioUnit registerSelf];
|
|
_gameCore = core;
|
|
_volume = 1.0;
|
|
_running = NO;
|
|
_engine = [AVAudioEngine new];
|
|
_outputDeviceIsDefault = YES;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self stopMonitoringEngineConfiguration];
|
|
}
|
|
|
|
- (void)audioSampleRateDidChange {
|
|
if (_running) {
|
|
[_engine stop];
|
|
[self configureNodes];
|
|
[_engine prepare];
|
|
[self performSelector:@selector(resumeAudio) withObject:nil afterDelay:0.020];
|
|
}
|
|
}
|
|
|
|
- (void)startAudio {
|
|
NSAssert1(_gameCore.audioBufferCount == 1, @"only one buffer supported; got=%lu", _gameCore.audioBufferCount);
|
|
|
|
[self createNodes];
|
|
[self configureNodes];
|
|
[self attachNodes];
|
|
[self setOutputDeviceID:self.outputDeviceID];
|
|
|
|
[_engine prepare];
|
|
// per the following, we need to wait before resuming to allow devices to start 🤦🏻♂️
|
|
// https://github.com/AudioKit/AudioKit/blob/f2a404ff6cf7492b93759d2cd954c8a5387c8b75/Examples/macOS/OutputSplitter/OutputSplitter/Audio/Output.swift#L88-L95
|
|
[self performSelector:@selector(resumeAudio) withObject:nil afterDelay:0.020];
|
|
[self startMonitoringEngineConfiguration];
|
|
}
|
|
|
|
- (void)stopAudio {
|
|
[_engine stop];
|
|
[self detachNodes];
|
|
[self destroyNodes];
|
|
_running = NO;
|
|
}
|
|
|
|
- (void)pauseAudio
|
|
{
|
|
[_engine pause];
|
|
_running = NO;
|
|
}
|
|
|
|
- (void)resumeAudio
|
|
{
|
|
_running = YES;
|
|
NSError *err;
|
|
if (![_engine startAndReturnError:&err]) {
|
|
os_log_error(OE_LOG_AUDIO, "unable to start AVAudioEngine: %{public}s", err.localizedDescription.UTF8String);
|
|
return;
|
|
}
|
|
}
|
|
|
|
- (void)startMonitoringEngineConfiguration
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
_token = [NSNotificationCenter.defaultCenter addObserverForName:AVAudioEngineConfigurationChangeNotification object:_engine queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull note) {
|
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
if (!strongSelf)
|
|
return;
|
|
|
|
os_log_info(OE_LOG_AUDIO, "AVAudioEngine configuration change");
|
|
[self setOutputDeviceID:self.outputDeviceID];
|
|
}];
|
|
}
|
|
|
|
- (void)stopMonitoringEngineConfiguration
|
|
{
|
|
if (_token) {
|
|
[NSNotificationCenter.defaultCenter removeObserver:_token];
|
|
}
|
|
}
|
|
|
|
- (OEAudioBufferReadBlock)readBlockForBuffer:(id<OEAudioBuffer>)buffer {
|
|
if ([buffer respondsToSelector:@selector(readBlock)]) {
|
|
return [buffer readBlock];
|
|
}
|
|
|
|
return ^NSUInteger(void * buf, NSUInteger max) {
|
|
return [buffer read:buf maxLength:max];
|
|
};
|
|
}
|
|
|
|
- (AVAudioFormat *)renderFormat {
|
|
UInt32 channelCount = (UInt32)[_gameCore channelCountForBuffer:0];
|
|
UInt32 bytesPerSample = (UInt32)[_gameCore audioBitDepth] / 8;
|
|
|
|
CAFFormatFlags formatFlags;
|
|
if (bytesPerSample == 4) {
|
|
// assume 32-bit float
|
|
formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsNonInterleaved | kAudioFormatFlagIsPacked;
|
|
} else {
|
|
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian;
|
|
}
|
|
|
|
AudioStreamBasicDescription mDataFormat = {
|
|
.mSampleRate = [_gameCore audioSampleRateForBuffer:0],
|
|
.mFormatID = kAudioFormatLinearPCM,
|
|
.mFormatFlags = formatFlags,
|
|
.mBytesPerPacket = bytesPerSample * channelCount,
|
|
.mFramesPerPacket = 1,
|
|
.mBytesPerFrame = bytesPerSample * channelCount,
|
|
.mChannelsPerFrame = channelCount,
|
|
.mBitsPerChannel = 8 * bytesPerSample,
|
|
};
|
|
|
|
return [[AVAudioFormat alloc] initWithStreamDescription:&mDataFormat];
|
|
}
|
|
|
|
- (AudioDeviceID)defaultAudioOutputDeviceID {
|
|
AudioObjectPropertyAddress addr = {
|
|
.mSelector = kAudioHardwarePropertyDefaultOutputDevice,
|
|
.mScope = kAudioObjectPropertyScopeGlobal,
|
|
.mElement = kAudioObjectPropertyElementMaster,
|
|
};
|
|
|
|
AudioObjectID deviceID = kAudioDeviceUnknown;
|
|
UInt32 size = sizeof(deviceID);
|
|
AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, nil, &size, &deviceID);
|
|
return deviceID;
|
|
}
|
|
|
|
- (void)configureNodes {
|
|
AVAudioFormat *renderFormat = [self renderFormat];
|
|
OEAudioUnit *au = (OEAudioUnit*)_gen.AUAudioUnit;
|
|
NSError *err;
|
|
AUAudioUnitBus *bus = au.inputBusses[0];
|
|
if (![bus setFormat:renderFormat error:&err]) {
|
|
os_log_error(OE_LOG_AUDIO, "unable to set input bus render format %{public}s: %{public}s",
|
|
renderFormat.description.UTF8String,
|
|
err.localizedDescription.UTF8String);
|
|
return;
|
|
}
|
|
|
|
OEAudioBufferReadBlock read = [self readBlockForBuffer:[_gameCore audioBufferAtIndex:0]];
|
|
AudioStreamBasicDescription const *src = renderFormat.streamDescription;
|
|
NSUInteger bytesPerFrame = src->mBytesPerFrame;
|
|
UInt32 channelCount = src->mChannelsPerFrame;
|
|
|
|
au.outputProvider = ^AUAudioUnitStatus(AudioUnitRenderActionFlags *actionFlags, const AudioTimeStamp *timestamp, AUAudioFrameCount frameCount, NSInteger inputBusNumber, AudioBufferList *inputData) {
|
|
NSUInteger bytesRequested = frameCount * bytesPerFrame;
|
|
NSUInteger bytesCopied = read(inputData->mBuffers[0].mData, bytesRequested);
|
|
|
|
inputData->mBuffers[0].mDataByteSize = (UInt32)bytesCopied;
|
|
inputData->mBuffers[0].mNumberChannels = channelCount;
|
|
|
|
return noErr;
|
|
};
|
|
}
|
|
|
|
- (void)createNodes {
|
|
AudioComponentDescription desc = {
|
|
.componentType = kAudioUnitType_Generator,
|
|
.componentSubType = kAudioUnitSubType_Emulator,
|
|
.componentManufacturer = kAudioUnitManufacturer_OpenEmu,
|
|
};
|
|
_gen = [[AVAudioUnitGenerator alloc] initWithAudioComponentDescription:desc];
|
|
}
|
|
|
|
- (void)destroyNodes {
|
|
_gen = nil;
|
|
}
|
|
|
|
- (void)connectNodes {
|
|
[_engine connect:_gen to:_engine.mainMixerNode format:nil];
|
|
_engine.mainMixerNode.outputVolume = _volume;
|
|
}
|
|
|
|
- (void)attachNodes {
|
|
[_engine attachNode:_gen];
|
|
}
|
|
|
|
- (void)detachNodes {
|
|
[_engine detachNode:_gen];
|
|
}
|
|
|
|
- (AudioDeviceID)outputDeviceID
|
|
{
|
|
return _outputDeviceIsDefault ? 0 : _engine.outputNode.AUAudioUnit.deviceID;
|
|
}
|
|
|
|
- (void)setOutputDeviceID:(AudioDeviceID)outputDeviceID
|
|
{
|
|
if (outputDeviceID == 0) {
|
|
outputDeviceID = [self defaultAudioOutputDeviceID];
|
|
os_log_info(OE_LOG_AUDIO, "using default audio device %d", outputDeviceID);
|
|
_outputDeviceIsDefault = YES;
|
|
} else {
|
|
_outputDeviceIsDefault = NO;
|
|
}
|
|
|
|
[_engine stop];
|
|
NSError *err;
|
|
if (![_engine.outputNode.AUAudioUnit setDeviceID:outputDeviceID error:&err]) {
|
|
os_log_error(OE_LOG_AUDIO, "unable to set output device ID %d: %{public}s",
|
|
outputDeviceID,
|
|
err.localizedDescription.UTF8String);
|
|
return;
|
|
}
|
|
[self connectNodes];
|
|
|
|
if (_running && !_engine.isRunning) {
|
|
[_engine prepare];
|
|
[self performSelector:@selector(resumeAudio) withObject:nil afterDelay:0.020];
|
|
}
|
|
}
|
|
|
|
- (void)setVolume:(CGFloat)aVolume
|
|
{
|
|
_volume = aVolume;
|
|
if (_engine) {
|
|
_engine.mainMixerNode.outputVolume = _volume;
|
|
}
|
|
}
|
|
|
|
|
|
@end
|