diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 80525d56d..76219fcb3 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -18,6 +18,7 @@ import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.yunqiinnovation.ble_service.BleService import com.deep_voice.speech.tts.AudioOutputDevice +import com.deep_voice.speech.tts.AudioDataListener import kotlinx.coroutines.* import com.yunqiinnovation.azure_speech.tools.RecordFile /** AzureSpeechPlugin */ @@ -105,6 +106,70 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } } + + /** + * 从WAV格式音频数据中提取纯PCM数据 + * + * @param wavData WAV格式的音频数据 + * @return 纯PCM音频数据 + */ + private fun extractPcmFromWav(wavData: ByteArray): ByteArray { + try { + // WAV文件头最小长度为44字节 + if (wavData.size < 44) { + FileLogger.e(tag, "WAV数据长度不足,无法解析文件头") + return ByteArray(0) + } + + // 检查RIFF标识符 + val riffHeader = String(wavData, 0, 4, Charsets.US_ASCII) + if (riffHeader != "RIFF") { + FileLogger.e(tag, "不是有效的RIFF WAV文件,标识符: $riffHeader") + return ByteArray(0) + } + + // 检查WAVE标识符 + val waveHeader = String(wavData, 8, 4, Charsets.US_ASCII) + if (waveHeader != "WAVE") { + FileLogger.e(tag, "不是有效的WAVE文件,标识符: $waveHeader") + return ByteArray(0) + } + + // 查找data chunk + var offset = 12 // 跳过RIFF头部 + while (offset < wavData.size - 8) { + val chunkId = String(wavData, offset, 4, Charsets.US_ASCII) + val chunkSize = (wavData[offset + 4].toInt() and 0xFF) or + ((wavData[offset + 5].toInt() and 0xFF) shl 8) or + ((wavData[offset + 6].toInt() and 0xFF) shl 16) or + ((wavData[offset + 7].toInt() and 0xFF) shl 24) + + if (chunkId == "data") { + // 找到data chunk,提取PCM数据 + val dataOffset = offset + 8 + val dataSize = minOf(chunkSize, wavData.size - dataOffset) + + if (dataSize > 0) { + val pcmData = ByteArray(dataSize) + System.arraycopy(wavData, dataOffset, pcmData, 0, dataSize) + FileLogger.d(tag, "成功提取PCM数据: ${pcmData.size}字节") + return pcmData + } + break + } + + // 移动到下一个chunk + offset += 8 + chunkSize + } + + FileLogger.e(tag, "未找到data chunk") + return ByteArray(0) + + } catch (e: Exception) { + FileLogger.e(tag, "解析WAV数据异常: ${e.message}") + return ByteArray(0) + } + } // 音频数据 事件发送方法 private fun sendAudioDataEvent(event: Map) { if (audioDataEventSink == null) { @@ -937,7 +1002,15 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) { FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}") - BleService.writeExternalAudioData(audioData) + // Azure TTS返回的是RIFF WAV格式,需要去除文件头 + val pcmData = extractPcmFromWav(audioData) + if (pcmData.isNotEmpty()) { + FileLogger.d(tag, "提取PCM数据,大小=${pcmData.size}字节") + BleService.writeExternalRightAudioData(pcmData) + } + else { + FileLogger.e(tag, "提取PCM数据失败,音频数据可能不是WAV格式") + } } override fun onSynthesisFailed(text: String, error: String) { // sendAstEvent( diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index f63488d10..a34ee7790 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -264,6 +264,65 @@ private func sendAstEvent(_ event: [String: Any]) { sendAudioDataEvent(eventMap) } + /** + * 从WAV格式音频数据中提取纯PCM数据 + * @param wavData WAV格式的音频数据 + * @return 纯PCM音频数据,如果提取失败则返回空Data + */ + private func extractPcmFromWav(_ wavData: Data) -> Data { + // WAV文件头最小长度为44字节 + guard wavData.count >= 44 else { + os_log("WAV数据长度不足,无法解析文件头", log: log, type: .error) + return Data() + } + + // 检查RIFF标识符 + let riffHeader = String(data: wavData.subdata(in: 0..<4), encoding: .ascii) ?? "" + guard riffHeader == "RIFF" else { + os_log("不是有效的RIFF WAV文件,标识符: %@", log: log, type: .error, riffHeader) + return Data() + } + + // 检查WAVE标识符 + let waveHeader = String(data: wavData.subdata(in: 8..<12), encoding: .ascii) ?? "" + guard waveHeader == "WAVE" else { + os_log("不是有效的WAVE文件,标识符: %@", log: log, type: .error, waveHeader) + return Data() + } + + // 查找data chunk + var offset = 12 // 跳过RIFF头部 + while offset < wavData.count - 8 { + let chunkIdData = wavData.subdata(in: offset..<(offset + 4)) + let chunkId = String(data: chunkIdData, encoding: .ascii) ?? "" + + // 读取chunk大小(小端序) + let chunkSizeData = wavData.subdata(in: (offset + 4)..<(offset + 8)) + let chunkSize = chunkSizeData.withUnsafeBytes { bytes in + return bytes.load(as: UInt32.self).littleEndian + } + + if chunkId == "data" { + // 找到data chunk,提取PCM数据 + let dataOffset = offset + 8 + let dataSize = min(Int(chunkSize), wavData.count - dataOffset) + + if dataSize > 0 { + let pcmData = wavData.subdata(in: dataOffset..<(dataOffset + dataSize)) + os_log("成功提取PCM数据: %d字节", log: log, type: .info, pcmData.count) + return pcmData + } + break + } + + // 移动到下一个chunk + offset += 8 + Int(chunkSize) + } + + os_log("未找到data chunk", log: log, type: .error) + return Data() + } + // MARK: - ASR 方法处理 private func handleAsrMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -950,9 +1009,18 @@ private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventC * @param audioData 音频数据 */ public func onSynthesisAudioGenerated(text: String, audioData: Data) { - print("语音合成音频数据生成回调", text, audioData.count) - // 调用BLE服务写入音频数据 - BleService.shared.writeExternalAudioData(data: audioData) + guard let plugin = plugin else { return } + + plugin.os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count) + + // Azure TTS返回的是RIFF WAV格式,需要去除文件头提取PCM数据 + let pcmData = plugin.extractPcmFromWav(audioData) + if !pcmData.isEmpty { + plugin.os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count) + BleService.shared.writeExternalAudioData(data: pcmData) + } else { + plugin.os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error) + } // plugin?.sendAsrEvent([ // "type": "synthesisAudioGenerated",