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) } }
问题根源
- 采样率不兼容:设置的
44800Hz不是iOS音频硬件支持的标准采样率,AVAudioEngine只支持如44100Hz、48000Hz这类标准值,导致格式校验失败。 - 音频会话重复配置:
startListening方法中重复设置AVAudioSession的category,覆盖了之前的完整配置。 - 重复attach节点:
configureAudio中每次调用都会attach mixerNode,可能导致音频引擎状态异常。 - 未定义的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
相关产品推荐
相关产品推荐

