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
相关产品推荐
相关产品推荐

