如何在AudioKit 5中同时为麦克风输入添加PitchTap与FFTTap
Swift SpriteKit AudioEngine 同时配置 FFTTap、PitchTap 和振荡器的修复方案
问题背景
在Swift SpriteKit游戏中,尝试为AudioEngine同时配置FFTTap和PitchTap,搭配振荡器实现:
- 通过
PitchTap处理麦克风输入的音高与振幅 - 通过
FFTTap处理谐波数据 - 振荡器为用户提供耳机音频,要求精准实时
现有配置能正常处理PitchTap数据,但添加FFTTap后出现音频图错误,报错信息:
ioData.mNumberBuffers=2, ASBD::NumberChannelStreams(output.GetStreamFormat())=1; kAudio_ParamError from AU (0x102a12600): auou/rioc/appl, render err: -50
错误原因分析
- 音频图连接逻辑混乱:
PitchTracker中错误将audioEngine.output重置为inputMixer,覆盖了AudioEngineManager中原本连接的主混音器(包含振荡器输出),导致音频流格式不匹配。 - 冗余混音器节点:
PitchTracker中创建的fftInput和pitchInput混音器完全多余,直接在麦克风输入的混音器上添加两个Tap即可。 - 格式不匹配:输入输出的声道数、采样格式未统一,触发AudioUnit参数无效错误(-50)。
修复方案
核心调整点
- 移除
PitchTracker中冗余的混音器节点,直接在inputMixer上挂载FFTTap和PitchTap - 保留
AudioEngineManager中正确的音频图链路:麦克风输入→inputMixer→主混音器(整合振荡器)→音频输出 - 统一音频格式,确保输入输出的声道数、采样率一致
修复后的完整代码
AudioEngineManager 类
import AVFoundation import AudioKit class AudioEngineManager { static let shared = AudioEngineManager() var audioEngine = AudioEngine() var mainMixer = Mixer() // 主混音器:整合振荡器和输入音频 var oscillator = Oscillator() // 音频生成振荡器 var inputMixer = Mixer() // 麦克风输入混音器 private init() { setupAudioComponents() } func configureAudioSession() { do { let audioSession = AVAudioSession.sharedInstance() try audioSession.setPreferredSampleRate(44100) try audioSession.setPreferredIOBufferDuration(0.005) try audioSession.setCategory(.playAndRecord, mode: .measurement, options: [.allowBluetoothA2DP]) // 移除.defaultToSpeaker,避免播放/录制时的声道冲突 try audioSession.setActive(true) print("Audio session configured.") } catch { print("Failed to configure audio session: \(error)") } } private func setupAudioComponents() { oscillator.amplitude = 0 oscillator.start() // 连接麦克风输入到inputMixer if let input = audioEngine.input { inputMixer.addInput(input) } // 主混音器整合振荡器和输入音频 mainMixer.addInput(oscillator) mainMixer.addInput(inputMixer) // 明确设置主混音器输出格式为单声道,避免格式不匹配 let outputFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)! mainMixer.avAudioNode.outputFormat(forBus: 0) audioEngine.output = mainMixer } func startAudioEngine() { do { inputMixer.start() mainMixer.start() try audioEngine.start() let mixerFormat = mainMixer.avAudioNode.outputFormat(forBus: 0) print("Number of channels: \(mixerFormat.channelCount)") print("Audio engine started successfully.") } catch { print("Failed to start audio engine: \(error)") } } func stopAudioEngine() { audioEngine.stop() print("Audio engine stopped.") } func playTone(frequency: Double, amplitude: Double, duration: TimeInterval) { oscillator.frequency = AUValue(frequency) oscillator.amplitude = AUValue(amplitude) DispatchQueue.main.asyncAfter(deadline: .now() + duration) { self.stopTone() } } func stopTone() { oscillator.amplitude = 0 } }
PitchTracker 类
import AudioKit class PitchTracker { static let shared = PitchTracker() private var inputMixer: Mixer var pitchTap: PitchTap? var fftTap: FFTTap? var audioDetectedHandler: ((AudioData) -> Void)? init() { self.inputMixer = AudioEngineManager.shared.inputMixer setupTaps() } private func setupTaps() { // 直接在inputMixer上添加FFTTap,使用2048缓冲区平衡实时性和精度 fftTap = FFTTap(inputMixer, bufferSize: 2048) { fftData in print("FFT Data received.") // 处理FFT数据,提取谐波比例等 let harmonics = self.calculateHarmonics(from: fftData) if let pitchTap = self.pitchTap, let pitch = pitchTap.lastPitch, let amp = pitchTap.lastAmplitude { self.audioDetectedHandler?(AudioData(pitch: CGFloat(pitch[0]), amplitude: CGFloat(amp[0]), harmonicsRatios: harmonics)) } } // 直接在inputMixer上添加PitchTap pitchTap = PitchTap(inputMixer) { pitch, amp in print("Pitch: \(pitch[0]), Amplitude: \(amp[0])") } } private func calculateHarmonics(from fftData: FFTData) -> [CGFloat] { // 示例:计算基频、一次谐波、二次谐波的比例 var ratios = [CGFloat]() ratios.append(CGFloat(fftData.maxMagnitude)) ratios.append(CGFloat(fftData.magnitudes[Int(fftData.maxIndex * 2)] / fftData.maxMagnitude)) ratios.append(CGFloat(fftData.magnitudes[Int(fftData.maxIndex * 3)] / fftData.maxMagnitude)) return ratios } func startTracking() { do { fftTap?.start() pitchTap?.start() print("Audio tracking started.") } catch { print("Failed to start taps: \(error)") } } func stopTracking() { fftTap?.stop() pitchTap?.stop() print("Audio tracking stopped.") } func differenceInCents(frequency1: Float, frequency2: Float) -> Float { return 1200 * log2(frequency1 / frequency2) } } struct AudioData { var pitch: CGFloat var amplitude: CGFloat var harmonicsRatios: [CGFloat] // 基频、一次谐波、二次谐波比例 }
关键修复说明
- 音频图结构简化:移除冗余混音器节点,麦克风输入直接进入
inputMixer,同时挂载两个Tap,避免节点连接错误。 - 格式统一:明确设置主混音器输出为单声道,解决声道数不匹配导致的
kAudio_ParamError错误。 - Session配置优化:移除
.defaultToSpeaker选项,避免播放和录制时的声道冲突,确保耳机输出正常。 - Tap逻辑整合:在FFT回调中结合PitchTap的数据,统一通过
audioDetectedHandler对外传递完整的音频数据。
内容的提问来源于stack exchange,提问作者rtardif
相关产品推荐
相关产品推荐

