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

Flutter iOS端使用ShazamKit包崩溃,报音频格式验证错误

iOS端ShazamKit声音识别崩溃解决方案

问题描述

在Flutter中使用shazamkit包实现声音识别,Android版本运行正常,但iOS版本启动识别时直接崩溃,错误信息如下:

ERROR: [0x190bf000] >avae> AVAudioNode.mm:568: CreateRecordingTap: required condition is false: IsFormatSampleRateAndChannelCountValid(format)
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'required condition is false: IsFormatSampleRateAndChannelCountValid(format)'

相关Swift代码:

import Flutter
import UIKit
import ShazamKit
import AudioToolbox

public class SwiftFlutterShazamKitPlugin: NSObject, FlutterPlugin {
    private var session: SHSession?
    private let audioEngine = AVAudioEngine()
    private let playerNode = AVAudioPlayerNode()
    private let mixerNode = AVAudioMixerNode()
    private var callbackChannel: FlutterMethodChannel?
    private var sampleRate = 44800

    
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "flutter_shazam_kit", binaryMessenger: registrar.messenger())
        let instance = SwiftFlutterShazamKitPlugin(callbackChannel: FlutterMethodChannel(name: "flutter_shazam_kit_callback", binaryMessenger: registrar.messenger()))
        registrar.addMethodCallDelegate(instance, channel: channel)
    }
    
    init(callbackChannel: FlutterMethodChannel? = nil) {
        self.callbackChannel = callbackChannel
    }
    
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "configureShazamKitSession":
            let args = call.arguments as! Dictionary<String, Any>
            configureShazamKitSession(
                customCatalogPath: args["customCatalogPath"] as? String,
                sampleRate: args["sampleRate"] as! Int
            )
            result(nil)
        case "startDetectionWithMicrophone":
            do{
                configureAudio()
                try startListening(result: result)
            }catch{
                callbackChannel?.invokeMethod("didHasError", arguments: error.localizedDescription)
            }
        case "endDetectionWithMicrophone":
            stopListening()
            result(nil)
        case "endSession":
            session = nil
            result(nil)
        default:
            result(nil)
        }
    }
}

//MARK: ShazamKit session delegation here
//MARK: Methods for AVAudio
extension SwiftFlutterShazamKitPlugin {
    func configureShazamKitSession(customCatalogPath: String?, sampleRate: Int) {
        self.sampleRate = sampleRate
        do {
            if session == nil {
                if (customCatalogPath == nil) {
                    session = SHSession()
                } else {
                    let documentsUrl = FileManager.default.urls(
                        for: .documentDirectory,
                        in: .userDomainMask
                    ).first!
                    let catalog = SHCustomCatalog()
                    try catalog.add(from: URL(fileURLWithPath: customCatalogPath!))
                    session = SHSession(catalog: catalog)
                }
                session?.delegate = self
            }
        } catch let error {
            callbackChannel?.invokeMethod("didHasError",
                arguments: "configureShazamKitSession() failed")
        }
    }
    
    func addAudio(buffer: AVAudioPCMBuffer, audioTime: AVAudioTime) {
        // Add the audio to the current match request
        session?.matchStreamingBuffer(buffer, at: audioTime)
    }
    
    func configureAudio() {
        playerNode.stop()
        audioEngine.stop()


        let inputFormat = audioEngine.inputNode.inputFormat(forBus: 0)
        
        // Set an output format compatible with ShazamKit.
        let outputFormat = AVAudioFormat(standardFormatWithSampleRate: Double(sampleRate), channels: 1)
        
        // Create a mixer node to convert the input.
        audioEngine.attach(mixerNode)
        
        // Attach the mixer to the microphone input and the output of the audio engine.
        audioEngine.connect(audioEngine.inputNode, to: mixerNode, format: inputFormat)
        // audioEngine.connect(mixerNode, to: audioEngine.outputNode, format: outputFormat)
        
        // Install a tap on the mixer node to capture the microphone audio.
        mixerNode.installTap(onBus: 0,
                             bufferSize: 8192,
                             format: outputFormat) { buffer, audioTime in
            // Add captured audio to the buffer used for making a match.
            self.addAudio(buffer: buffer, audioTime: audioTime)
        }
    }
    
    func startListening(result: FlutterResult) throws {
        guard session != nil else{
            callbackChannel?.invokeMethod("didHasError", arguments: "ShazamSession not found, please call configureShazamKitSession() first to initialize it.")
            result(nil)
            return
        }
        callbackChannel?.invokeMethod("detectStateChanged", arguments: 1)
        // Throw an error if the audio engine is already running.
        guard !audioEngine.isRunning else {
            callbackChannel?.invokeMethod("didHasError", arguments: "Audio engine is currently running, please stop the audio engine first and then try again")
            return
        }
        let audioSession = AVAudioSession.sharedInstance()
                   do {
                try audioSession.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.measurement, options: AVAudioSession.CategoryOptions.defaultToSpeaker)
                try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
            } catch {
                print("audioSession properties weren't set because of an error.")
                delegate?.showFeedbackError(title: "Sorry", message: "Mic is busy")
                return false
            }
        
        // Ask the user for permission to use the mic if required then start the engine.
        try audioSession.setCategory(.playAndRecord)
        audioSession.requestRecordPermission { [weak self] success in
            guard success else {
                self?.callbackChannel?.invokeMethod("didHasError", arguments: "Recording permission not found, please allow permission first and then try again")
                return
            }
            do{
                self?.audioEngine.prepare()
                try self?.audioEngine.start()
            }catch{
                self?.callbackChannel?.invokeMethod("didHasError", arguments: "Can't start the audio engine")
            }
        }
        result(nil)
    }
    
    func stopListening() {
        callbackChannel?.invokeMethod("detectStateChanged", arguments: 0)
        // Check if the audio engine is already recording.
        mixerNode.removeTap(onBus: 0)
        audioEngine.stop()
    }
}

//MARK: Delegate methods for SHSession
extension SwiftFlutterShazamKitPlugin: SHSessionDelegate{
    public func session(_ session: SHSession, didFind match: SHMatch) {
        var mediaItems: [[String: Any]] = []
        match.mediaItems.forEach { rawItem in
            var item: [String : Any] = [:]
            var count: UInt32 = 0
            let properties = class_copyPropertyList(class_getSuperclass(rawItem.classForCoder), &count)
            for i in 0..<count {
                guard let property = properties?[Int(i)] else { continue }
                let name = String(cString: property_getName(property))
                if (name == "properties") {
                    let props = rawItem.value(forKey: name) as! NSDictionary
                    for property in props.allKeys {
                        let prop = property as! String
                        var val = props.value(forKey: prop)!
                        if (String(describing: type(of: val)) == "__NSTaggedDate") {
                            let dateFormatter = DateFormatter()
                            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
                            val = dateFormatter.string(from: val as! Date)
                        }
                        item[prop] = val
                    }
                }
            }
            mediaItems.append(item)
            free(properties)
        
        }
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: mediaItems)
            let jsonString = String(data: jsonData, encoding: .utf8)
            self.callbackChannel?.invokeMethod("matchFound", arguments: jsonString)
        } catch {
            callbackChannel?.invokeMethod("didHasError", arguments: "Error when trying to format data, please try again")
        }
    }
    
    public func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) {
        callbackChannel?.invokeMethod("notFound", arguments: nil)
        callbackChannel?.invokeMethod("didHasError", arguments: error?.localizedDescription)
    }
}

问题根源

  1. 采样率不兼容:设置的44800Hz不是iOS音频硬件支持的标准采样率,AVAudioEngine只支持如44100Hz、48000Hz这类标准值,导致格式校验失败。
  2. 音频会话重复配置:startListening方法中重复设置AVAudioSession的category,覆盖了之前的完整配置。
  3. 重复attach节点:configureAudio中每次调用都会attach mixerNode,可能导致音频引擎状态异常。
  4. 未定义的delegate调用:代码中delegate?.showFeedbackError没有对应的delegate实现,会引发编译或运行错误。

修复步骤

1. 修正采样率

将默认采样率改为iOS支持的标准值,同时确保Flutter端传递的采样率也符合要求:

private var sampleRate = 44100 // 替换44800为44100或48000

2. 清理音频会话重复配置

删除startListening中重复的category设置,保留第一次完整配置,并修复未定义的delegate调用:

func startListening(result: FlutterResult) throws {
    guard session != nil else{
        callbackChannel?.invokeMethod("didHasError", arguments: "ShazamSession not found, please call configureShazamKitSession() first to initialize it.")
        result(nil)
        return
    }
    callbackChannel?.invokeMethod("detectStateChanged", arguments: 1)
    guard !audioEngine.isRunning else {
        callbackChannel?.invokeMethod("didHasError", arguments: "Audio engine is currently running, please stop the audio engine first and then try again")
        return
    }
    let audioSession = AVAudioSession.sharedInstance()
    do {
        try audioSession.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.measurement, options: AVAudioSession.CategoryOptions.defaultToSpeaker)
        try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
    } catch {
        print("audioSession properties weren't set because of an error.")
        callbackChannel?.invokeMethod("didHasError", arguments: "Mic is busy")
        result(nil)
        return
    }
    
    audioSession.requestRecordPermission { [weak self] success in
        guard success else {
            self?.callbackChannel?.invokeMethod("didHasError", arguments: "Recording permission not found, please allow permission first and then try again")
            return
        }
        do{
            self?.audioEngine.prepare()
            try self?.audioEngine.start()
        }catch{
            self?.callbackChannel?.invokeMethod("didHasError", arguments: "Can't start the audio engine")
        }
    }
    result(nil)
}

3. 避免重复attach mixerNode

在configureAudio中先检查并移除已attach的mixerNode:

func configureAudio() {
    playerNode.stop()
    audioEngine.stop()
    
    // 先移除已存在的mixerNode
    if mixerNode.engine != nil {
        audioEngine.detach(mixerNode)
    }
    
    let inputFormat = audioEngine.inputNode.inputFormat(forBus: 0)
    let outputFormat = AVAudioFormat(standardFormatWithSampleRate: Double(sampleRate), channels: 1)
    
    audioEngine.attach(mixerNode)
    audioEngine.connect(audioEngine.inputNode, to: mixerNode, format: inputFormat)
    
    mixerNode.installTap(onBus: 0,
                         bufferSize: 8192,
                         format: outputFormat) { buffer, audioTime in
        self.addAudio(buffer: buffer, audioTime: audioTime)
    }
}

验证修复

修改完成后重新编译iOS项目,启动声音识别功能,采样率格式校验将通过,不会再出现崩溃,同时音频会话配置更规范。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 04:03:19