You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mac可视化录音App开发:AudioKit无法切换输入设备及功能实现求助

Mac音频App开发:设备选择、波形显示、音频捕获与输出解决方案

你需求的所有功能均可以实现,以下针对你尝试的两个框架给出具体实现方案:


一、AudioKit:实现App内输入设备切换

AudioKit支持在App内切换输入设备,核心是切换时重启AudioEngine并重新绑定输入节点,并非只能使用系统默认设备。修改你的setInputDevice方法如下:

func setInputDevice(to device: Device) {
    // 停止当前引擎
    engine.stop()
    
    // 移除旧输入节点的连接
    if let existingInput = engine.input {
        engine.disconnectNodeInput(existingInput)
    }
    
    do {
        // 为目标设备创建新输入节点
        let inputNode = try AudioEngine.inputNode(for: device)
        engine.input = inputNode
        
        // 安装Tap获取实时音频数据,用于波形显示和录制
        inputNode.installTap(bus: 0, bufferSize: 1024, format: inputNode.outputFormat(forBus: 0)) { buffer, time in
            self.processAudioBuffer(buffer)
        }
        
        // 重启引擎
        try engine.start()
    } catch {
        print("切换输入设备失败: \(error.localizedDescription)")
    }
}

// 处理音频缓冲(用于波形生成、录制)
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer) {
    // 提取PCM样本数据
    let samples = buffer.floatChannelData![0]
    let sampleCount = Int(buffer.frameLength)
    
    // 1. 生成波形数据:计算每个样本的振幅绝对值
    let amplitudes = (0..<sampleCount).map { CGFloat(abs(samples[$0])) }
    DispatchQueue.main.async {
        self.waveformData = amplitudes // 传递给UI层绘制波形
    }
    
    // 2. 保存录制数据:将buffer写入AVAudioFile
    if let audioFile = self.recordingFile {
        try? audioFile.write(from: buffer)
    }
}

额外说明

  • 设备切换必须重启AudioEngine,这是Core Audio音频流的特性要求
  • 若要将音频输出为其他App的输入,可结合虚拟音频设备(如基于Core Audio开发的虚拟驱动),将AudioEngine的输出路由到虚拟设备,其他App即可选择该设备作为输入源

二、AVFoundation:处理音频数据实现全功能

针对AVFoundation代理回调的音频数据,可按以下步骤处理:

1. 保存音频用于回放

将CMSampleBuffer转换为PCM数据,写入文件或缓存到内存:

private var recordingFile: AVAudioFile?

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    guard CMSampleBufferIsValid(sampleBuffer),
          let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer),
          let audioBufferList = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
            sampleBuffer,
            blockBufferAllocator: nil,
            blockBufferMemoryAllocator: nil,
            flags: 0,
            audioBufferListOut: nil
          ) else { return }
    
    // 转换为PCM数据并写入文件
    let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList)
    for buffer in ablPointer {
        let data = UnsafeRawBufferPointer(buffer.mData!, count: Int(buffer.mDataByteSize))
        if let file = recordingFile {
            try? file.write(from: data)
        }
    }
    
    // 更新波形数据
    updateWaveform(sampleBuffer: sampleBuffer)
}

2. 实时波形图生成

提取音频样本的振幅值,传递给UI层绘制:

@Published var waveformData: [CGFloat] = []

private func updateWaveform(sampleBuffer: CMSampleBuffer) {
    guard let format = CMSampleBufferGetFormatDescription(sampleBuffer),
          let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format),
          let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return }
    
    let sampleCount = CMSampleBufferGetNumSamples(sampleBuffer)
    var dataPointer: UnsafeMutableRawPointer?
    var dataLength: Int = 0
    CMBlockBufferGetDataPointer(blockBuffer, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: &dataLength, dataPointerOut: &dataPointer)
    
    guard let pointer = dataPointer else { return }
    let samples = pointer.bindMemory(to: Float32.self, capacity: sampleCount)
    
    // 计算每个样本的振幅(简化处理)
    let amplitudes = (0..<sampleCount).map { CGFloat(abs(samples[$0])) }
    DispatchQueue.main.async {
        self.waveformData = amplitudes
    }
}

SwiftUI波形绘制示例:

struct WaveformView: View {
    var waveformData: [CGFloat]
    
    var body: some View {
        GeometryReader { geo in
            Path { path in
                guard !waveformData.isEmpty else { return }
                let width = geo.size.width / CGFloat(waveformData.count)
                for (index, amplitude) in waveformData.enumerated() {
                    let x = CGFloat(index) * width
                    let y = geo.size.height / 2 * (1 - amplitude)
                    index == 0 ? path.move(to: CGPoint(x: x, y: y)) : path.addLine(to: CGPoint(x: x, y: y))
                }
            }
            .stroke(Color.blue, lineWidth: 2)
        }
    }
}

3. 音频输出与虚拟输入

  • 回放音频:使用AVAudioPlayer加载录制好的文件,或用AVAudioEngine播放缓存的PCM数据
  • 作为其他App输入:AVFoundation本身不支持虚拟音频设备,需基于Core Audio开发虚拟驱动(可参考开源项目BlackHole的实现逻辑),将捕获的音频输出到虚拟设备,其他App即可选择该设备作为输入

方案选择建议

  • 优先选择AudioKit:封装了Core Audio的复杂逻辑,自带波形分析、录制、播放组件,设备切换逻辑成熟,开发效率更高
  • 若需底层控制:选择AVFoundation+Core Audio组合,需自行处理更多音频数据细节,但灵活性更强

内容的提问来源于stack exchange,提问作者user499846

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 15:55:25