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

如何在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]  // 基频、一次谐波、二次谐波比例
}

关键修复说明

  1. 音频图结构简化:移除冗余混音器节点,麦克风输入直接进入inputMixer,同时挂载两个Tap,避免节点连接错误。
  2. 格式统一:明确设置主混音器输出为单声道,解决声道数不匹配导致的kAudio_ParamError错误。
  3. Session配置优化:移除.defaultToSpeaker选项,避免播放和录制时的声道冲突,确保耳机输出正常。
  4. Tap逻辑整合:在FFT回调中结合PitchTap的数据,统一通过audioDetectedHandler对外传递完整的音频数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 17:54:52