I was able to capture the audio stream and forward it to headphones using the following code when the virtual device had both input and output channels, but I cannot capture the audio stream when I set the virtual device to output-only:
init() {
let allDevices = simply.allDevices
for item in allDevices {
if item.name == loopback_name{
loopbackDevice = item
}
if item.name == m2_output_name{
M2OutDevice = item
}
}
var inputAcd = AudioComponentDescription()
inputAcd.componentType = kAudioUnitType_Output;
inputAcd.componentSubType = kAudioUnitSubType_HALOutput;
inputAcd.componentManufacturer = kAudioUnitManufacturer_Apple;
let comp = AudioComponentFindNext(nil, &inputAcd);
AudioComponentInstanceNew(comp!, &inputUnit)
AudioUnitInitialize(inputUnit!)
var enableFlag = UInt32(1)
var disableFlag = UInt32(0)
let inputBus:UInt32 = 1
let outputBus:UInt32 = 0
checkErr(inputUnit!.setPropertyEnableIO(&enableFlag, .input,inputBus))
checkErr(inputUnit!.setPropertyEnableIO(&disableFlag, .output,outputBus))
var deviceid = loopbackDevice!.id
checkErr(inputUnit!.setPropertyCurrentDevice(&deviceid,.output))
var streamFormat1 = AudioStreamBasicDescription()
checkErr(inputUnit!.getPropertyStreamFormat(&streamFormat1,.output,inputBus))
var streamFormat2 = AudioStreamBasicDescription()
checkErr(inputUnit!.getPropertyStreamFormat(&streamFormat2,.input,inputBus))
debugPrint(streamFormat1,"===\n",streamFormat2)
inputUnit!.setPropertyStreamFormat(&streamFormat1, .output, inputBus)
inputUnit!.setPropertyStreamFormat(&streamFormat1, .input, inputBus)
var bufferSizeFrames : UInt32 = 0
checkErr(inputUnit!.getPropertyBufferFrameSize(&bufferSizeFrames, .global))
inputUnit!.setInputBuffer(&inputBuffer, streamFormat1, bufferSizeFrames)
ringBuffer = CircularBuffer<Int32>(channelCount: Int(streamFormat2.mChannelsPerFrame),
capacity: Int(bufferSizeFrames) * 2048)
var callback = AURenderCallbackStruct()
callback.inputProc = inputRenderCallback
callback.inputProcRefCon = Unmanaged.passUnretained(self).toOpaque()
AudioUnitSetProperty(inputUnit!,kAudioOutputUnitProperty_SetInputCallback,kAudioUnitScope_Input, inputBus,
&callback,UInt32(MemoryLayout<AURenderCallbackStruct>.size))
checkErr(NewAUGraph(&graph))
checkErr(AUGraphInitialize(graph!))
var outNode :AUNode = 0
checkErr(graph!.getNode(&outNode))
var mixerNode :AUNode = 0
checkErr(graph!.getNode(&mixerNode,.mixer,.stereoMixer))
checkErr(AUGraphOpen(graph!))
checkErr(graph!.getUnit(outNode , &outputUnit))
checkErr(graph!.getUnit(mixerNode, &mixerUnit))
checkErr(AUGraphConnectNodeInput(graph!, mixerNode, 0, outNode, 0))
var outputCallback = AURenderCallbackStruct()
outputCallback.inputProc = outputRenderCallBack
outputCallback.inputProcRefCon = Unmanaged.passUnretained(self).toOpaque()
AUGraphSetNodeInputCallback(graph!, mixerNode, 0, &outputCallback)
deviceid = M2OutDevice!.id
outputUnit!.setPropertyCurrentDevice(&deviceid,.output)
AudioOutputUnitStart(inputUnit!)
checkErr(AUGraphStart(graph!))
}
// MARK: - inputUnit单元获取音频写入环形缓冲区 -
let inputRenderCallback: AURenderCallback = {
(inRefCon: UnsafeMutableRawPointer,
ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
inTimeStamp: UnsafePointer<AudioTimeStamp>,
inBusNumber: UInt32,
inNumberFrames: UInt32,
ioData: UnsafeMutablePointer<AudioBufferList>?) -> OSStatus in
let inSelf = Unmanaged<IXIAudioTestOutputHelper>.fromOpaque(inRefCon).takeUnretainedValue()
AudioUnitRender(inSelf.inputUnit!,
ioActionFlags,
inTimeStamp,
inBusNumber,
inNumberFrames,
inSelf.inputBuffer!.unsafeMutablePointer);
let start = inTimeStamp.pointee.mSampleTime.int64Value
let end = start + Int64(inNumberFrames)
if (inSelf.inputLatestSampleTime == -1) {
inSelf.inputLatestSampleTime = start
return noErr
}
inSelf.inputLatestSampleTime = start
_ = inSelf.ringBuffer!.write(from: inSelf.inputBuffer!.unsafeMutablePointer, start: start, end: end)
debugPrint("inputRenderCallback")
return noErr
}
let outputRenderCallBack: AURenderCallback = {
(inRefCon: UnsafeMutableRawPointer,
ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
inTimeStamp: UnsafePointer<AudioTimeStamp>,
inBusNumber: UInt32,
inNumberFrames: UInt32,
ioData: UnsafeMutablePointer<AudioBufferList>?) -> OSStatus in
let inSelf = Unmanaged<IXIAudioTestOutputHelper>.fromOpaque(inRefCon).takeUnretainedValue()
let sampleTime = inTimeStamp.pointee.mSampleTime
if (inSelf.outputLatestSampleTime == -1) {
inSelf.outputLatestSampleTime = sampleTime.int64Value
inSelf.computeOffset()
return noErr
} else {
inSelf.outputLatestSampleTime = sampleTime.int64Value
}
let from = Int64(sampleTime + inSelf.sampleOffset - inSelf.safetyOffset)
let to = from + Int64(inNumberFrames)
_ = inSelf.ringBuffer!.read(into: ioData!, from: from, to: to)
return noErr
}
I tried disabling the input of the inputUnit and enabling the output. And I used kAudioUnitProperty_SetRenderCallback to capture the audio stream, but it didn't work. Moreover, I got an error in the kAudioUnitProperty_SetRenderCallback callback saying ioData.mNumberBuffers=2, ASBD::NumberChannelStreams(output.GetStreamFormat())=1; indicating that the captured audio is interleaved. However, I set the inputBuffer to be non-interleaved. When I set the inputBuffer to be non-interleaved, the captured audio stream is empty. Here is the code: enter image description here
var enableFlag = UInt32(1)
var disableFlag = UInt32(0)
let inputBus:UInt32 = 1
let outputBus:UInt32 = 0
checkErr(inputUnit!.setPropertyEnableIO(&disableFlag, .input,inputBus))
checkErr(inputUnit!.setPropertyEnableIO(&enableFlag, .output,outputBus))
var deviceid = loopbackDevice!.id
checkErr(inputUnit!.setPropertyCurrentDevice(&deviceid,.output))
var streamFormat1 = AudioStreamBasicDescription()
checkErr(inputUnit!.getPropertyStreamFormat(&streamFormat1,.output,outputBus))
var streamFormat2 = AudioStreamBasicDescription()
checkErr(inputUnit!.getPropertyStreamFormat(&streamFormat2,.input,outputBus))
debugPrint(streamFormat1,"===\n",streamFormat2)
inputUnit!.setPropertyStreamFormat(&streamFormat2, .output, outputBus)
inputUnit!.setPropertyStreamFormat(&streamFormat2, .input, outputBus)
var bufferSizeFrames : UInt32 = 0
checkErr(inputUnit!.getPropertyBufferFrameSize(&bufferSizeFrames, .global))
inputUnit!.setInputBuffer(&inputBuffer, streamFormat2, bufferSizeFrames)
ringBuffer = CircularBuffer<Int32>(channelCount: Int(streamFormat2.mChannelsPerFrame),
capacity: Int(bufferSizeFrames) * 2048)
checkErr(NewAUGraph(&graph))
checkErr(AUGraphInitialize(graph!))
var outNode :AUNode = 0
checkErr(graph!.getNode(&outNode))
var mixerNode :AUNode = 0
checkErr(graph!.getNode(&mixerNode,.mixer,.stereoMixer))
checkErr(AUGraphOpen(graph!))
checkErr(graph!.getUnit(outNode , &outputUnit))
checkErr(graph!.getUnit(mixerNode, &mixerUnit))
checkErr(AUGraphConnectNodeInput(graph!, mixerNode, 0, outNode, 0))
var callback = AURenderCallbackStruct()
callback.inputProc = inputRenderCallback
callback.inputProcRefCon = Unmanaged.passUnretained(self).toOpaque()
AudioUnitSetProperty(mixerUnit!,kAudioUnitProperty_SetRenderCallback,kAudioUnitScope_Output, 0,
&callback,UInt32(MemoryLayout<AURenderCallbackStruct>.size))
deviceid = M2OutDevice!.id
outputUnit!.setPropertyCurrentDevice(&deviceid,.output)
AudioOutputUnitStart(inputUnit!)
checkErr(AUGraphStart(graph!))