How to obtain audio from a virtual device with only output channels (no input channels) in macOS

44 Views Asked by At

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!))
0

There are 0 best solutions below