mirror of
https://github.com/OpenEmu/OpenEmuKit.git
synced 2025-11-01 11:08:14 +00:00
244 lines
10 KiB
Swift
244 lines
10 KiB
Swift
// Copyright (c) 2022, 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 Foundation
|
|
import AudioToolbox
|
|
import AVFoundation
|
|
import os.log
|
|
|
|
@objc(OEAudioUnit) final public class AudioUnit: AUAudioUnit {
|
|
// swiftlint:disable identifier_name
|
|
@objc public static var kAudioUnitSubType_Emulator = OSType(bitPattern: 0x65_6d_75_21) // emu!
|
|
@objc public static let kAudioUnitManufacturer_OpenEmu = OSType(bitPattern: 0x6f_65_6d_75) // oemu
|
|
|
|
private static var isRegistered: Bool = {
|
|
let desc = AudioComponentDescription(componentType: kAudioUnitType_Generator,
|
|
componentSubType: kAudioUnitSubType_Emulator,
|
|
componentManufacturer: kAudioUnitManufacturer_OpenEmu,
|
|
componentFlags: 0,
|
|
componentFlagsMask: 0)
|
|
|
|
AUAudioUnit.registerSubclass(AudioUnit.self,
|
|
as: desc,
|
|
name: "OEAudioUnit",
|
|
version: .max)
|
|
return true
|
|
}()
|
|
|
|
@objc public static func register() {
|
|
_ = isRegistered
|
|
}
|
|
|
|
private var _outputProvider: AURenderPullInputBlock?
|
|
|
|
@objc public override var outputProvider: AURenderPullInputBlock? {
|
|
get { _outputProvider }
|
|
set { _outputProvider = newValue }
|
|
}
|
|
|
|
private let _inputBus: CustomBus
|
|
private var _inputBusArray: AUAudioUnitBusArray!
|
|
private let _outputBus: CustomBus
|
|
private var _outputBusArray: AUAudioUnitBusArray!
|
|
|
|
@frozen @usableFromInline struct Converter {
|
|
let conv: AudioConverterRef?
|
|
let buffer: UnsafeMutablePointer<UInt8>?
|
|
let inputFrameCount: UInt32
|
|
let inputBytePerFrame: UInt32
|
|
}
|
|
|
|
private var converter: UnsafeMutablePointer<Converter> = .allocate(capacity: 1)
|
|
|
|
override init(componentDescription: AudioComponentDescription, options: AudioComponentInstantiationOptions = []) throws {
|
|
let defaultFormat = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 2)!
|
|
|
|
_inputBus = try CustomBus(format: defaultFormat)
|
|
_outputBus = try CustomBus(format: defaultFormat)
|
|
|
|
try super.init(componentDescription: componentDescription, options: options)
|
|
|
|
_inputBusArray = .init(audioUnit: self, busType: .input, busses: [_inputBus])
|
|
_outputBusArray = .init(audioUnit: self, busType: .output, busses: [_outputBus])
|
|
|
|
maximumFramesToRender = 512
|
|
}
|
|
|
|
deinit {
|
|
converter.deallocate()
|
|
}
|
|
|
|
public override var inputBusses: AUAudioUnitBusArray {
|
|
_inputBusArray
|
|
}
|
|
|
|
public override var outputBusses: AUAudioUnitBusArray {
|
|
_outputBusArray
|
|
}
|
|
|
|
final var requiresConversion: Bool {
|
|
_inputBus.format != _outputBus.format
|
|
}
|
|
|
|
@objc public override func allocateRenderResources() throws {
|
|
try super.allocateRenderResources()
|
|
|
|
guard requiresConversion else { return }
|
|
|
|
let srcDesc = _inputBus.format.streamDescription
|
|
let dstDesc = _outputBus.format.streamDescription
|
|
|
|
var conv: AudioConverterRef?
|
|
let status = AudioConverterNew(srcDesc, dstDesc, &conv)
|
|
if status != noErr {
|
|
os_log(.error, log: .audio, "Unable to create audio converter: %d", status)
|
|
return
|
|
}
|
|
|
|
/* 64 bytes of padding above self.maximumFramesToRender because
|
|
* CoreAudio is stupid and likes to request more bytes than the maximum
|
|
* even though IT TAKES CARE TO SET THE MAXIMUM VALUE ITSELF! */
|
|
let inputFrameCount = maximumFramesToRender + 64
|
|
let inputBytePerFrame = srcDesc.pointee.mBytesPerFrame
|
|
let bufferSize = Int(inputFrameCount * inputBytePerFrame)
|
|
|
|
converter.pointee = .init(conv: conv,
|
|
buffer: .allocate(capacity: bufferSize),
|
|
inputFrameCount: inputFrameCount,
|
|
inputBytePerFrame: inputBytePerFrame)
|
|
|
|
os_log(.info, log: .audio, "Audio converter buffer size = %u bytes", bufferSize)
|
|
}
|
|
|
|
@objc public override func deallocateRenderResources() {
|
|
super.deallocateRenderResources()
|
|
freeResources()
|
|
}
|
|
|
|
private func freeResources() {
|
|
if let buffer = converter.pointee.buffer {
|
|
buffer.deallocate()
|
|
}
|
|
|
|
if let conv = converter.pointee.conv {
|
|
AudioConverterDispose(conv)
|
|
}
|
|
|
|
converter.pointee = .init(conv: nil, buffer: nil, inputFrameCount: 0, inputBytePerFrame: 0)
|
|
}
|
|
|
|
// MARK: - AUAudioUnit (AUAudioUnitImplementation)
|
|
|
|
@frozen @usableFromInline struct InputData {
|
|
let pullInput: AURenderPullInputBlock?
|
|
var timestamp: UnsafePointer<AudioTimeStamp>?
|
|
let converter: UnsafePointer<Converter>
|
|
}
|
|
|
|
@objc public override var internalRenderBlock: AUInternalRenderBlock {
|
|
let pullInput = outputProvider
|
|
|
|
if requiresConversion {
|
|
let inOutDataProc: AudioConverterComplexInputDataProc = { (_, ioNumberDataPackets, ioData, _, inUserData) -> OSStatus in
|
|
let inp = inUserData.unsafelyUnwrapped.assumingMemoryBound(to: InputData.self)
|
|
let conv = inp.pointee.converter.pointee
|
|
|
|
var pullFlags: AudioUnitRenderActionFlags = []
|
|
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer(conv.buffer)
|
|
ioData.pointee.mBuffers.mDataByteSize = conv.inputFrameCount * conv.inputBytePerFrame
|
|
|
|
/* cap the bytes we return to the amount of bytes available to guard
|
|
* against core audio requesting more bytes that they fit in the buffer
|
|
* EVEN THOUGH THE BUFFER IS ALREADY LARGER THAN MAXIMUMFRAMESTORENDER */
|
|
ioNumberDataPackets.pointee = min(conv.inputFrameCount, ioNumberDataPackets.pointee)
|
|
|
|
return inp.pointee.pullInput.unsafelyUnwrapped(&pullFlags,
|
|
inp.pointee.timestamp.unsafelyUnwrapped,
|
|
ioNumberDataPackets.pointee,
|
|
0,
|
|
ioData)
|
|
}
|
|
|
|
let converter = converter
|
|
|
|
var data = InputData(pullInput: pullInput,
|
|
converter: converter)
|
|
// swiftlint:disable closure_parameter_position
|
|
return { (_ actionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
|
|
_ timestamp: UnsafePointer<AudioTimeStamp>,
|
|
_ frameCount: AUAudioFrameCount,
|
|
_ outputBusNumber: Int,
|
|
_ outputData: UnsafeMutablePointer<AudioBufferList>,
|
|
_ realtimeEvenListHead: UnsafePointer<AURenderEvent>?,
|
|
_ pullInputBlock: AURenderPullInputBlock?) -> AUAudioUnitStatus in
|
|
guard pullInput != nil
|
|
else { return kAudioUnitErr_NoConnection }
|
|
|
|
data.timestamp = timestamp
|
|
var packetSize: UInt32 = frameCount
|
|
|
|
let res = AudioConverterFillComplexBuffer(converter.pointee.conv!, inOutDataProc, &data, &packetSize, outputData, nil)
|
|
return res
|
|
}
|
|
}
|
|
|
|
return { (_ actionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
|
|
_ timestamp: UnsafePointer<AudioTimeStamp>,
|
|
_ frameCount: AUAudioFrameCount,
|
|
_ outputBusNumber: Int,
|
|
_ outputData: UnsafeMutablePointer<AudioBufferList>,
|
|
_ realtimeEvenListHead: UnsafePointer<AURenderEvent>?,
|
|
_ pullInputBlock: AURenderPullInputBlock?) -> AUAudioUnitStatus in
|
|
guard let pullInput = pullInput
|
|
else { return kAudioUnitErr_NoConnection }
|
|
|
|
var pullFlags: AudioUnitRenderActionFlags = []
|
|
return pullInput(&pullFlags, timestamp, frameCount, 0, outputData)
|
|
}
|
|
}
|
|
|
|
@objc class CustomBus: AUAudioUnitBus {
|
|
var _format: AVAudioFormat
|
|
|
|
override init(format: AVAudioFormat) throws {
|
|
self._format = format
|
|
try super.init(format: format)
|
|
}
|
|
|
|
@objc override func setFormat(_ format: AVAudioFormat) throws {
|
|
if _format == format {
|
|
return
|
|
}
|
|
|
|
willChangeValue(forKey: "format")
|
|
_format = format
|
|
didChangeValue(forKey: "format")
|
|
}
|
|
|
|
override var format: AVAudioFormat {
|
|
_format
|
|
}
|
|
}
|
|
}
|