Browse Source

Merge commit '025ac6b26d6abaeaf9821d451bdcfbd337575673' into new_dev

weicu
lxm 1 year ago
parent
commit
2443b037ce
  1. 42
      lib/modules/settings/views/settings_view.dart
  2. 24
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  3. 11
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  4. 1
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  5. 43
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  6. 4
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  7. 4
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  8. 137
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt
  9. 46
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
  10. 182
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift
  11. 56
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift
  12. 54
      local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift
  13. 59
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  14. 24
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

42
lib/modules/settings/views/settings_view.dart

@ -515,27 +515,27 @@ class SettingsView extends GetView<SettingsController> {
// ? Colors.white.withOpacity(0.1) // ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]), // : Colors.grey[200]),
//BLE测试 //BLE测试
// _buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
// title: 'opus解码测试', title: 'opus解码测试',
// subtitle: '测试opus解码', subtitle: '测试opus解码',
// icon: Icons.bluetooth_searching, icon: Icons.bluetooth_searching,
// iconBgColor: isDarkMode iconBgColor: isDarkMode
// ? Colors.green[900]!.withOpacity(0.3) ? Colors.green[900]!.withOpacity(0.3)
// : Colors.green[100]!, : Colors.green[100]!,
// iconColor: iconColor:
// isDarkMode ? Colors.green[300]! : Colors.green[600]!, isDarkMode ? Colors.green[300]! : Colors.green[600]!,
// titleColor: isDarkMode ? Colors.white : null, titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null, subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () { onTap: () {
// Get.toNamed(Routes.opusTest); Get.toNamed(Routes.opusTest);
// }, },
// isDarkMode: isDarkMode, isDarkMode: isDarkMode,
// ), ),
// Divider( Divider(
// height: 1, height: 1,
// color: isDarkMode color: isDarkMode
// ? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]), : Colors.grey[200]),
// BLE测试 // BLE测试
// _buildSimpleNavigationSetting( // _buildSimpleNavigationSetting(

24
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -130,6 +130,8 @@ object AgentService : CoroutineScope {
//是否播放音乐中 //是否播放音乐中
private var isplaymusic = false; private var isplaymusic = false;
//是否保留最终结果
private var isKeepResult = true;
// 打断防抖相关 // 打断防抖相关
private var lastInterruptTime = 0L private var lastInterruptTime = 0L
@ -265,6 +267,7 @@ object AgentService : CoroutineScope {
*/ */
fun dispose() { fun dispose() {
try { try {
BleService.closeCodec()
stopRecognition() stopRecognition()
stopTts() stopTts()
stopAiStream() stopAiStream()
@ -474,6 +477,7 @@ object AgentService : CoroutineScope {
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
BleService.closeCodec()
stopRecognition() stopRecognition()
sendEvent( sendEvent(
"auto_stop", "auto_stop",
@ -516,13 +520,18 @@ object AgentService : CoroutineScope {
* @param mode 识别模式:normal, ble_wakeup, phone_call, push_to_talk * @param mode 识别模式:normal, ble_wakeup, phone_call, push_to_talk
*/ */
fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean { fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean {
if(isExternalActive){
isKeepResult = false
BleService.openEncoder()//不发指令。只是录制opus
}
if (!isInitialized) { if (!isInitialized) {
Log.d(TAG, "服务未初始化") Log.d(TAG, "服务未初始化")
return false return false
} }
Log.d("TAG", "开始语音识别startRecognition?") Log.d("TAG", "开始语音识别startRecognition?")
stopRecognition() stopRecognition()
BleService.openEncoder()
// if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ // if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){
// Log.d(TAG, "开始语音识别stopRecognition") // Log.d(TAG, "开始语音识别stopRecognition")
// // BleService.closeCodec() // // BleService.closeCodec()
@ -559,6 +568,9 @@ object AgentService : CoroutineScope {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") Log.d("TAG", "azureAsrHelper?.startContinuousRecognition")
if(isExternalActive){
isKeepResult = true
}
azureAsrHelper?.startContinuousRecognition(audioSourceType) azureAsrHelper?.startContinuousRecognition(audioSourceType)
// 根据模式启动相应的空闲检测 // 根据模式启动相应的空闲检测
@ -586,7 +598,7 @@ object AgentService : CoroutineScope {
val success = azureAsrHelper?.setupEventListeners(object : val success = azureAsrHelper?.setupEventListeners(object :
AzureAsrHelper.ContinuousRecognizeCallback { AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()&&isKeepResult==true) {
// 检测到语音,更新状态 // 检测到语音,更新状态
_hasSpeechDetected.set(true) _hasSpeechDetected.set(true)
@ -630,7 +642,7 @@ object AgentService : CoroutineScope {
} }
override fun onResult(result: String, detectedLanguage: String) { override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) { if (result.isNotEmpty()&&isKeepResult==true) {//加入是否结束要输出最后的结果判读
sendEvent( sendEvent(
"recognition_result", mapOf( "recognition_result", mapOf(
"text" to result, "text" to result,
@ -709,11 +721,9 @@ object AgentService : CoroutineScope {
Log.d(TAG, "停止语音识别,当前模式: $currentRecognitionMode") Log.d(TAG, "停止语音识别,当前模式: $currentRecognitionMode")
try { try {
_isRecognitionActive.set(false)
azureAsrHelper?.stopContinuousRecognition() azureAsrHelper?.stopContinuousRecognition()
BleService.closeCodec()
_isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
// 重置识别模式 // 重置识别模式
currentRecognitionMode = "normal" currentRecognitionMode = "normal"
return return
@ -721,7 +731,6 @@ object AgentService : CoroutineScope {
Log.e(TAG, "停止语音识别异常: ${e.message}") Log.e(TAG, "停止语音识别异常: ${e.message}")
_isRecognitionActive.set(false) _isRecognitionActive.set(false)
stopIdleCheck() stopIdleCheck()
// 重置识别模式 // 重置识别模式
currentRecognitionMode = "normal" currentRecognitionMode = "normal"
return return
@ -1055,6 +1064,7 @@ object AgentService : CoroutineScope {
) )
) )
if (name == "exit_interaction") { if (name == "exit_interaction") {
BleService.closeCodec()
stopRecognition() stopRecognition()
} }
} catch (e: Exception) { } catch (e: Exception) {

11
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -14,6 +14,8 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
// 添加BleService的导入语句
import com.yunqiinnovation.ble_service.BleService
class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler,
AgentServiceListener { AgentServiceListener {
@ -124,13 +126,14 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.error("START_ERROR", "启动对话失败: ${e.message}", null) result.error("START_ERROR", "启动对话失败: ${e.message}", null)
} }
} }
"recognizeCallback" -> { "recognizeCallback" -> {
val isStarted = AgentService.recognizeCallback() val isStarted = AgentService.recognizeCallback()
result.success(isStarted) result.success(isStarted)
} }
"stopConversation" -> { "stopConversation" -> {
try { try {
BleService.closeCodec()
AgentService.stopRecognition() AgentService.stopRecognition()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {

1
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -77,6 +77,7 @@ Log.d(TAG, "手动启动语音识别: ")
Log.e(TAG, "语音代理未初始化") Log.e(TAG, "语音代理未初始化")
return return
} }
BleService.closeCodec()
AgentService.stopRecognition() AgentService.stopRecognition()
return return
} }

43
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -69,6 +69,8 @@ class AgentServiceImpl: NSObject {
// 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题 // 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题
private var isStartingRecognition: Bool = false private var isStartingRecognition: Bool = false
private var stopRequestedDuringStart: Bool = false private var stopRequestedDuringStart: Bool = false
//是否保留最终结果
private var isKeepResult = true;
var currsessionId: String = "" var currsessionId: String = ""
var language: String = "" var language: String = ""
@ -354,7 +356,8 @@ class AgentServiceImpl: NSObject {
if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode) os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode)
BleService.shared.closeCodec() // 关闭编码器 (设备侧)
BleService.shared.closeCodec()
self.stopRecognition() self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [ self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout", "reason": "idle_timeout",
@ -378,6 +381,14 @@ class AgentServiceImpl: NSObject {
} }
} }
func startRecognition(useBle: Bool = false, mode: String = "normal") -> Bool { func startRecognition(useBle: Bool = false, mode: String = "normal") -> Bool {
// 接收到唤醒信号,打开编码器 (设备侧)
print("ai启动语音\(useBle))")
var openResult = false // 添加openResult变量定义
if useBle{
isKeepResult = false
// 捕获openEncoder的返回值
openResult = BleService.shared.openEncoder() // 正确捕获返回值
}
if !isInitialized { if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED") sendError("服务未初始化", code: "NOT_INITIALIZED")
return false return false
@ -389,12 +400,11 @@ class AgentServiceImpl: NSObject {
return false return false
} }
} }
stopTts(); stopRecognition()
if isRecognizing { stopTts()
stopRecognition()
}
os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败")
wasMusicPlayingBeforeRecognition = MusicService.shared.isPlaying wasMusicPlayingBeforeRecognition = MusicService.shared.isPlaying
// 如果音乐正在播放,则暂停 // 如果音乐正在播放,则暂停
@ -427,7 +437,9 @@ class AgentServiceImpl: NSObject {
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
return false return false
} }
if(useBle){
isKeepResult = true
}
// 根据模式启动相应的空闲检测(此处 guard isRecognizing,会在 onSessionStarted 中再次启动) // 根据模式启动相应的空闲检测(此处 guard isRecognizing,会在 onSessionStarted 中再次启动)
startIdleCheckForMode(mode: mode) startIdleCheckForMode(mode: mode)
@ -449,6 +461,7 @@ class AgentServiceImpl: NSObject {
/// 特性: /// 特性:
/// 1) 即便处于"启动中"(isStartingRecognition)也会打印日志并尝试停止 /// 1) 即便处于"启动中"(isStartingRecognition)也会打印日志并尝试停止
/// 2) 如果底层尚未完全进入可停止状态,记录一次待停止请求,待会话启动后立即停止 /// 2) 如果底层尚未完全进入可停止状态,记录一次待停止请求,待会话启动后立即停止
print("停止识别语音,isRecognizing=\(isRecognizing),isStartingRecognition=\(isStartingRecognition)")
if !(isRecognizing || isStartingRecognition) { if !(isRecognizing || isStartingRecognition) {
// 既不在识别中,也不在启动中,视为幂等处理 // 既不在识别中,也不在启动中,视为幂等处理
return true return true
@ -472,7 +485,6 @@ class AgentServiceImpl: NSObject {
MusicService.shared.resume() MusicService.shared.resume()
} }
// 重置识别模式 // 重置识别模式
currentRecognitionMode = "normal" currentRecognitionMode = "normal"
@ -490,7 +502,7 @@ class AgentServiceImpl: NSObject {
return false return false
} }
guard let audioStream = azureAsrHelper?.audioStream else { guard let audioStream = azureAsrHelper?.audioStream else {
os_log("音频流未初始化", log: logger,type: .error) os_log("AI音频流未初始化", log: logger,type: .error)
return false return false
} }
audioStream.saveAudioDataTo(data: audioData) audioStream.saveAudioDataTo(data: audioData)
@ -1053,6 +1065,8 @@ private func jsonToString(_ json: [String: Any]) -> String? {
// 如果正在识别,需要先停止 // 如果正在识别,需要先停止
let wasRecognizing = isRecognizing let wasRecognizing = isRecognizing
if wasRecognizing { if wasRecognizing {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
_ = stopRecognition() _ = stopRecognition()
} }
@ -1227,6 +1241,8 @@ private func jsonToString(_ json: [String: Any]) -> String? {
func dispose() -> Bool { func dispose() -> Bool {
print("ai释放资源") print("ai释放资源")
if isRecognizing { if isRecognizing {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
stopRecognition() stopRecognition()
} }
@ -1388,6 +1404,8 @@ class ChatApiStreamCallback: StreamCallback {
print("onFunctionCall=functionCall\(functionCall)") print("onFunctionCall=functionCall\(functionCall)")
if (name == "exit_interaction") { if (name == "exit_interaction") {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
agentService.stopRecognition() agentService.stopRecognition()
} }
@ -1513,9 +1531,6 @@ extension AgentServiceImpl: BleService.Callback {
func onWakeupSignalReceived() { func onWakeupSignalReceived() {
stopTts()
stopRecognition()
startRecognition(useBle: true, mode: "ble_wakeup") startRecognition(useBle: true, mode: "ble_wakeup")
} }
@ -1715,7 +1730,7 @@ class AudioPlayer {
extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
func onResult(_ text: String, _ detectedLanguage: String) { func onResult(_ text: String, _ detectedLanguage: String) {
if !text.isEmpty { if !text.isEmpty && isKeepResult {
var data: [String: Any] = ["text": text] var data: [String: Any] = ["text": text]
if !detectedLanguage.isEmpty { if !detectedLanguage.isEmpty {
data["language"] = detectedLanguage data["language"] = detectedLanguage
@ -1734,7 +1749,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
} }
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) { func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
if !recognizing.isEmpty { if !recognizing.isEmpty && isKeepResult {
let previousHasSpeech = hasSpeechDetected let previousHasSpeech = hasSpeechDetected
hasSpeechDetected = true hasSpeechDetected = true
@ -1770,6 +1785,8 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
// 新增:若在启动期间已经收到停止请求,则立即执行停止 // 新增:若在启动期间已经收到停止请求,则立即执行停止
if stopRequestedDuringStart { if stopRequestedDuringStart {
stopRequestedDuringStart = false stopRequestedDuringStart = false
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
_ = stopRecognition() _ = stopRecognition()
} }
} }

4
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -2,7 +2,7 @@ import Flutter
import UIKit import UIKit
import Foundation import Foundation
import os.log import os.log
import ble_service
/// 代理服务Flutter插件,负责将Flutter调用桥接到原生实现 /// 代理服务Flutter插件,负责将Flutter调用桥接到原生实现
public class AgentServicePlugin: NSObject, FlutterPlugin { public class AgentServicePlugin: NSObject, FlutterPlugin {
@ -90,6 +90,8 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
return return
} }
print("stopConversation") print("stopConversation")
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
impl.stopRecognition() impl.stopRecognition()
result(true) result(true)

4
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -215,7 +215,7 @@ class AzureAsrHelper(private val context: Context) {
if (!audioStream.isInitialized()) { if (!audioStream.isInitialized()) {
audioStream.initAudioRecord() audioStream.initAudioRecord()
} }
audioStream.setupStream()
audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream) audioConfig = AudioConfig.fromStreamInput(audioStream.pushAudioStream)
} catch (e: Exception) { } catch (e: Exception) {
@ -488,7 +488,7 @@ class AzureAsrHelper(private val context: Context) {
if (!audioStream.isInitialized()) { if (!audioStream.isInitialized()) {
audioStream.initAudioRecord() audioStream.initAudioRecord()
} }
audioStream.setupStream()
if (audioStream.recordfile == null) { if (audioStream.recordfile == null) {
audioStream.recordfile = RecordFile() audioStream.recordfile = RecordFile()
} }

137
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt

@ -475,26 +475,57 @@ class IntegratedSpeechTranslationService(
} }
/** /**
* 停止连续语音翻译 * 停止连续语音翻译(非阻塞)
* 在 IO 线程内顺序停止识别器、录音线程与文件,并对翻译/合成收尾增加统一超时保护,避免阻塞主线程与潜在 ANR。
*/ */
fun stopContinuousTranslation() { fun stopContinuousTranslation() {
try { try {
recognizer?.stopContinuousRecognitionAsync() Log.d(TAG, "停止连续语音翻译-开始")
audioProcessor?.stopRecording() // 先标记识别关闭,避免后续流程继续入队处理
recordfile1!!.closeFile(true)
serviceState.isRecognizing.set(false) serviceState.isRecognizing.set(false)
// 等待当前处理完成
while (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) {
Thread.sleep(100)
}
Log.d(TAG, "停止连续语音翻译") // 在 IO 线程中做真实的停止动作,避免阻塞主线程
launch(Dispatchers.IO) {
// 1) 停止识别器:等待短超时,避免卡住
try {
recognizer?.stopContinuousRecognitionAsync()
?.get(1500, java.util.concurrent.TimeUnit.MILLISECONDS)
} catch (t: Throwable) {
Log.w(TAG, "停止识别器超时或失败", t)
}
// 2) 停止录音线程(打断并短等待退出)
runCatching { audioProcessor?.stopRecording() }
.onFailure { Log.w(TAG, "停止录音失败", it) }
// 3) 关闭录音文件(若开启过录音)
runCatching { recordfile1?.closeFile(true) }
.onFailure { Log.w(TAG, "关闭录音文件失败", it) }
// 4) 等待翻译/合成收尾,整体不超过 2s
val start = System.currentTimeMillis()
while ((serviceState.isTranslating.get() || serviceState.isSynthesizing.get())
&& System.currentTimeMillis() - start < 2000
) {
delay(100)
}
if (serviceState.isTranslating.get() || serviceState.isSynthesizing.get()) {
Log.w(TAG, "等待翻译/合成完成超时,强制标记结束")
}
// 5) 统一落幕:确保状态关闭
serviceState.isTranslating.set(false)
serviceState.isSynthesizing.set(false)
Log.d(TAG, "停止连续语音翻译-完成")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止翻译失败", e) Log.e(TAG, "停止翻译失败", e)
eventCallback?.onError("Service", "停止失败: ${e.message}") eventCallback?.onError("Service", "停止失败: ${e.message}")
} }
} }
/** /**
* 处理翻译和合成流程 * 处理翻译和合成流程
*/ */
@ -1184,58 +1215,57 @@ fun getSupportedLanguages(): List<String> {
} }
/** /**
* 改进的资源释放方法 * 安全释放音频处理器资源
* 添加更强的异常处理和超时保护
*/ */
fun dispose() { fun dispose() {
try { try {
Log.d(TAG, "开始释放音频处理器资源...") Log.d(TAG, "开始释放音频处理器资源...")
// 1. 停止运行标志 // 1. 设置停止标志
isRunning.set(false) if (!isRunning.compareAndSet(true, false)) {
Log.d(TAG, "音频处理器已停止,跳过释放")
return
}
// 2. 中断处理线程 // 1) 安全停止处理线程
processingThread?.let { thread -> processingThread?.let { thread ->
if (thread.isAlive) { if (thread.isAlive) {
try {
thread.interrupt() thread.interrupt()
try { thread.join(500)
// 等待线程结束,最多等待2秒 if (thread.isAlive) {
thread.join(2000) Log.w(TAG, "音频处理线程未在500ms内结束")
if (thread.isAlive) { // 不强制杀死,交由系统回收,但继续清理资源
Log.w(TAG, "音频处理线程未能正常结束") } else {
} else { Log.d(TAG, "音频处理线程已正常结束")
Log.d(TAG, "音频处理线程已结束")
}
} catch (e: InterruptedException) {
Log.w(TAG, "等待音频处理线程结束时被中断")
Thread.currentThread().interrupt()
} }
} catch (e: InterruptedException) {
Log.w(TAG, "等待线程结束时被中断", e)
Thread.currentThread().interrupt()
} catch (e: Exception) {
Log.e(TAG, "停止处理线程时发生异常", e)
} }
} }
processingThread = null }
processingThread = null
// 3. 关闭音频流
try {
pushAudioStream?.close()
pushAudioStream = null
Log.d(TAG, "音频流已关闭")
} catch (e: Exception) {
Log.e(TAG, "关闭音频流失败", e)
}
// 4. 清理音频队列 // 2) 关闭输入流
try { try {
val remainingData = audioQueue.size pushAudioStream?.close()
audioQueue.clear() } catch (t: Throwable) {
Log.d(TAG, "音频队列已清理,丢弃 $remainingData 个数据包") Log.w(TAG, "关闭 PushAudioInputStream 时异常", t)
} catch (e: Exception) { } finally {
Log.e(TAG, "清理音频队列失败", e) pushAudioStream = null
} }
Log.d(TAG, "音频处理器资源释放完成") // 3) 清空队列
audioQueue.clear()
} catch (e: Exception) { Log.d(TAG, "音频处理器资源释放完成")
Log.e(TAG, "释放音频处理器资源时发生异常", e) } catch (e: Exception) {
} Log.e(TAG, "释放音频处理器资源失败", e)
}
} }
} }
} }
@ -1315,7 +1345,16 @@ class VolcanoTranslationServiceImpl :
"ms-MY" to "ms", // 马来语(马来西亚) "ms-MY" to "ms", // 马来语(马来西亚)
"id-ID" to "id", // 印尼语(印尼) "id-ID" to "id", // 印尼语(印尼)
"fil-PH" to "tl", // 菲律宾语(菲律宾) "fil-PH" to "tl", // 菲律宾语(菲律宾)
"sv-SE" to "sv", // 瑞典语(瑞典)
"ro-RO" to "ro", // 罗马尼亚语(罗马尼亚)
"hu-HU" to "hu", // 匈牙利语(匈牙利)
"da-DK" to "da", // 丹麦语(丹麦)
"fi-FI" to "fi", // 芬兰语(芬兰)
"nb-NO" to "nb", // 挪威语(挪威)
"hr-HR" to "hr", // 克罗地亚语(克罗地亚)
"ca-ES" to "ca", // 加泰隆语
"mr-IN" to "mr", // 马拉地语(印度)
"ur-IN" to "ur", // 乌尔都语(印度)
// 欧洲 // 欧洲
"en-US" to "en", // 英语(美国) "en-US" to "en", // 英语(美国)
"en-GB" to "en", // 英语(英国) "en-GB" to "en", // 英语(英国)

46
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt

@ -88,8 +88,6 @@ class SimpleAudioReceiver(private val context: Context) {
* 初始化音频录制 * 初始化音频录制
*/ */
fun initAudioRecord() { fun initAudioRecord() {
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
isRunning.set(true) isRunning.set(true)
Log.d(TAG, "initAudioRecord: ${isRunning}") Log.d(TAG, "initAudioRecord: ${isRunning}")
startWriteThread() // 再启动数据读取线程 startWriteThread() // 再启动数据读取线程
@ -97,6 +95,17 @@ class SimpleAudioReceiver(private val context: Context) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
isInitialized = true isInitialized = true
}
/**
* 设置音频流
*/
fun setupStream() {
// 安全关闭并清理之前的流
pushAudioStream?.close()
pushAudioStream = null
val format = getOptimalAudioFormat()
pushAudioStream = AudioInputStream.createPushStream(format)
} }
fun isInitialized(): Boolean = isInitialized fun isInitialized(): Boolean = isInitialized
/** /**
@ -143,7 +152,7 @@ class SimpleAudioReceiver(private val context: Context) {
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data
try { try {
Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}") //Log.d(TAG, "写入数据大小: ${finalData.size},isContinuousRecognitionActive:${isContinuousRecognitionActive},isRecord${isRecord}")
if(audioDataCallback!=null){ if(audioDataCallback!=null){
audioDataCallback?.onAudio(finalData) audioDataCallback?.onAudio(finalData)
} }
@ -183,22 +192,36 @@ class SimpleAudioReceiver(private val context: Context) {
writeQueue.offer(buffer.copyOf()) writeQueue.offer(buffer.copyOf())
} }
/** /**
* 开始音频输入 * 开始音频输入
* @param audioSourceType 音频源类型 * @param audioSourceType 音频源类型
* @param callback 音频数据回调,可为空 * @param callback 音频数据回调,可为空
*/ */
fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) { fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) {
try {
this.audioSourceType = audioSourceType this.audioSourceType = audioSourceType
this.audioDataCallback = callback this.audioDataCallback = callback
writeQueue.clear()
isWriting.set(true) isWriting.set(true)
if(writeQueue.isNotEmpty()){
Log.d("tag", "存下来的数据: ${writeQueue?.size}")
}
when (audioSourceType) { when (audioSourceType) {
AudioSourceType.MICROPHONE -> runMicrophoneCapture() AudioSourceType.MICROPHONE -> runMicrophoneCapture()
AudioSourceType.EXTERNAL -> runExternalCapture() AudioSourceType.EXTERNAL -> runExternalCapture()
} }
} catch (e: Exception) {
Log.e(TAG, "启动音频录制失败: ${e.message}", e)
// 清理资源
pushAudioStream?.close()
pushAudioStream = null
isWriting.set(false)
throw e // 重新抛出异常让调用者处理
} }
}
// ... existing code ...
/** /**
* 配置通话音频模式以优化回声消除 * 配置通话音频模式以优化回声消除
*/ */
@ -367,6 +390,7 @@ class SimpleAudioReceiver(private val context: Context) {
*/ */
fun stopMicrophoneCapture() { fun stopMicrophoneCapture() {
try { try {
writeQueue.clear()
isWriting.set(false) isWriting.set(false)
// 停止录音 // 停止录音

182
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift

@ -605,12 +605,35 @@ import os.log
audioProcessor?.stopMicrophoneCapture() audioProcessor?.stopMicrophoneCapture()
recordFile?.closeFile(isSave: true) recordFile?.closeFile(isSave: true)
// 等待当前处理完成 // 使用后台队列等待当前处理完成,避免阻塞主线程
while serviceState.isTranslating || serviceState.isSynthesizing { DispatchQueue.global(qos: .userInitiated).async { [weak self] in
Thread.sleep(forTimeInterval: 0.1) guard let self = self else { return }
}
// 设置超时时间,避免无限等待
let timeout = DispatchTime.now() + .seconds(5)
while (self.serviceState.isTranslating || self.serviceState.isSynthesizing) && DispatchTime.now() < timeout {
Thread.sleep(forTimeInterval: 0.1)
}
// 强制停止合成器(如果仍在运行)
if self.serviceState.isSynthesizing {
do {
try self.synthesizer?.stopSpeaking()
} catch {
os_log("停止语音合成失败: %@", log: self.log, type: .error, error.localizedDescription)
}
self.serviceState.isSynthesizing = false
os_log("停止连续语音翻译", log: log, type: .info) DispatchQueue.main.async {
self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false)
}
}
DispatchQueue.main.async {
os_log("停止连续语音翻译", log: self.log, type: .info)
}
}
} catch { } catch {
os_log("停止翻译失败: %@", log: log, type: .error, error.localizedDescription) os_log("停止翻译失败: %@", log: log, type: .error, error.localizedDescription)
@ -777,68 +800,93 @@ import os.log
let voice: String let voice: String
switch language { switch language {
// 亚洲地区 // 中文相关
case "zh-CN": case "zh-CN":
voice = "zh-CN-XiaoxiaoNeural" voice = "zh-CN-XiaoxiaoNeural"
case "zh-TW": case "zh-TW":
voice = "zh-TW-HsiaoNeural" voice = "zh-TW-HsiaoChenNeural"
case "zh-HK": case "zh-HK":
voice = "zh-HK-HiuGaaiNeural" voice = "zh-HK-HiuMaanNeural"
case "ja-JP":
voice = "ja-JP-NanamiNeural" // 英语相关
case "ko-KR": case "en-US":
voice = "ko-KR-SunHiNeural" voice = "en-US-AriaNeural"
case "hi-IN":
voice = "hi-IN-KalpanaNeural" // 亚洲语言
case "ta-IN": case "ja-JP":
voice = "ta-IN-SundarNeural" voice = "ja-JP-NanamiNeural"
case "th-TH": case "ko-KR":
voice = "th-TH-PattaraNeural" voice = "ko-KR-SunHiNeural"
case "vi-VN": case "th-TH":
voice = "vi-VN-AnhNeural" voice = "th-TH-PremwadeeNeural"
case "vi-VN":
// 欧洲地区 voice = "vi-VN-HoaiMyNeural"
case "en-US": case "id-ID":
voice = "en-US-AriaNeural" voice = "id-ID-GadisNeural"
case "en-GB": case "hi-IN":
voice = "en-GB-SusanNeural" voice = "hi-IN-SwaraNeural"
case "fr-FR": case "ms-MY":
voice = "fr-FR-DeniseNeural" voice = "ms-MY-YasminNeural"
case "de-DE": case "bn-IN":
voice = "de-DE-KatjaNeural" voice = "bn-IN-TanishaaNeural"
case "es-ES": case "ta-IN":
voice = "es-ES-ElviraNeural" voice = "ta-IN-PallaviNeural"
case "it-IT": case "te-IN":
voice = "it-IT-DonatellaNeural" voice = "te-IN-ShrutiNeural"
case "pl-PL": case "mr-IN":
voice = "pl-PL-JozefinaNeural" voice = "mr-IN-AarohiNeural"
case "ru-RU": case "ur-IN":
voice = "ru-RU-DariaNeural" voice = "ur-IN-GulNeural"
case "tr-TR":
// 中东/非洲 voice = "tr-TR-EmelNeural"
case "ar-SA": case "fa-IR":
voice = "ar-SA-MajdNeural" voice = "fa-IR-DilaraNeural"
case "he-IL":
voice = "he-IL-NoaNeural" // 欧洲语言
case "sw-KE": case "fr-FR":
voice = "sw-KE-SabinaNeural" voice = "fr-FR-DeniseNeural"
case "es-ES":
// 美洲/大洋洲 voice = "es-ES-ElviraNeural"
case "pt-BR": case "pt-BR":
voice = "pt-BR-ManuelaNeural" voice = "pt-BR-FranciscaNeural"
case "es-MX": case "it-IT":
voice = "es-MX-CarlaNeural" voice = "it-IT-ElsaNeural"
case "de-DE":
// 其他重要语言 voice = "de-DE-KatjaNeural"
case "tr-TR": case "ru-RU":
voice = "tr-TR-SerifeNeural" voice = "ru-RU-SvetlanaNeural"
case "uk-UA": case "pl-PL":
voice = "uk-UA-OksanaNeural" voice = "pl-PL-AgnieszkaNeural"
case "fa-IR": case "nl-NL":
voice = "fa-IR-SaraNeural" voice = "nl-NL-ColetteNeural"
case "nl-NL": case "sv-SE":
voice = "nl-NL-LotteNeural" voice = "sv-SE-SofieNeural"
case "cs-CZ":
voice = "cs-CZ-VlastaNeural"
case "el-GR":
voice = "el-GR-AthinaNeural"
case "ro-RO":
voice = "ro-RO-AlinaNeural"
case "hu-HU":
voice = "hu-HU-NoemiNeural"
case "uk-UA":
voice = "uk-UA-PolinaNeural"
case "da-DK":
voice = "da-DK-ChristelNeural"
case "fi-FI":
voice = "fi-FI-NooraNeural"
case "nb-NO":
voice = "nb-NO-IselinNeural"
case "hr-HR":
voice = "hr-HR-GabrijelaNeural"
case "ca-ES":
voice = "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
case "ar-EG":
voice = "ar-EG-SalmaNeural"
case "sw-KE":
voice = "sw-KE-ZuriNeural"
default: default:
voice = "en-US-AriaNeural" // 默认回退英语 voice = "en-US-AriaNeural" // 默认回退英语
} }

56
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift

@ -24,6 +24,8 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
private var region = "cn-north-1" private var region = "cn-north-1"
private var maxRetryAttempts = 3 private var maxRetryAttempts = 3
private var timeout: TimeInterval = 10.0 private var timeout: TimeInterval = 10.0
private var isDisposed = false // 添加释放状态标志
private let disposeLock = NSLock() // 添加线程安全锁
// HTTP客户端 // HTTP客户端
private lazy var urlSession: URLSession = { private lazy var urlSession: URLSession = {
@ -52,7 +54,16 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
"ms-MY": "ms", // 马来语(马来西亚) "ms-MY": "ms", // 马来语(马来西亚)
"id-ID": "id", // 印尼语(印尼) "id-ID": "id", // 印尼语(印尼)
"fil-PH": "tl", // 菲律宾语(菲律宾) "fil-PH": "tl", // 菲律宾语(菲律宾)
"sv-SE": "sv", // 瑞典语(瑞典)
"ro-RO": "ro", // 罗马尼亚语(罗马尼亚)
"hu-HU": "hu", // 匈牙利语(匈牙利)
"da-DK": "da", // 丹麦语(丹麦)
"fi-FI": "fi", // 芬兰语(芬兰)
"nb-NO": "nb", // 挪威语(挪威)
"hr-HR": "hr", // 克罗地亚语(克罗地亚)
"ca-ES": "ca", // 加泰隆语
"mr-IN": "mr", // 马拉地语(印度)
"ur-IN": "ur", // 乌尔都语(印度)
// 欧洲 // 欧洲
"en-US": "en", // 英语(美国) "en-US": "en", // 英语(美国)
"en-GB": "en", // 英语(英国) "en-GB": "en", // 英语(英国)
@ -150,11 +161,25 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
/** /**
* 释放资源 * 释放资源
* 添加线程安全保护,防止重复释放
*/ */
public func dispose() { public func dispose() {
disposeLock.lock()
defer { disposeLock.unlock() }
if isDisposed {
os_log("翻译服务已经释放,跳过重复释放", log: log, type: .info)
return
}
os_log("开始释放翻译服务资源", log: log, type: .info)
isDisposed = true
isInitialized = false isInitialized = false
// 取消并失效URLSession
urlSession.invalidateAndCancel() urlSession.invalidateAndCancel()
os_log("火山翻译服务已释放", log: log, type: .info)
os_log("翻译服务资源释放完成", log: log, type: .info)
} }
// MARK: - Private Methods // MARK: - Private Methods
@ -203,6 +228,7 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
/** /**
* 执行翻译 * 执行翻译
* 添加服务状态检查,防止在已释放的会话中创建任务
*/ */
private func performTranslation( private func performTranslation(
text: String, text: String,
@ -210,6 +236,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
targetLanguage: String targetLanguage: String
) async -> IntegratedSpeechTranslationService.TranslationResult { ) async -> IntegratedSpeechTranslationService.TranslationResult {
// 检查服务是否已被释放
disposeLock.lock()
let disposed = isDisposed
disposeLock.unlock()
if disposed {
os_log("翻译服务已释放,取消翻译请求", log: log, type: .error)
return IntegratedSpeechTranslationService.TranslationResult(
success: false,
error: "翻译服务已释放"
)
}
do { do {
// 转换语言代码 // 转换语言代码
let sourceCode = getLanguageCode(languageCode: sourceLanguage) let sourceCode = getLanguageCode(languageCode: sourceLanguage)
@ -263,6 +302,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T
os_log("发送翻译请求到: %@", log: log, type: .debug, url.absoluteString) os_log("发送翻译请求到: %@", log: log, type: .debug, url.absoluteString)
// 再次检查服务状态,防止在请求构建过程中被释放
disposeLock.lock()
let stillDisposed = isDisposed
disposeLock.unlock()
if stillDisposed {
os_log("翻译服务在请求过程中被释放,取消网络请求", log: log, type: .error)
return IntegratedSpeechTranslationService.TranslationResult(
success: false,
error: "翻译服务已释放"
)
}
// 发送请求 // 发送请求
let (data, response) = try await urlSession.data(for: request) let (data, response) = try await urlSession.data(for: request)

54
local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift

@ -144,11 +144,11 @@ public class SimpleAudioReceiver: NSObject {
while self.isRunning { while self.isRunning {
if !self._isWriting { if !self._isWriting {
// print("startWriteThread 111")
usleep(10_000) usleep(10_000)
continue continue
} }
// print("startWriteThread 222")
guard let dataToWrite = self.writeQueue.take() else { continue } guard let dataToWrite = self.writeQueue.take() else { continue }
do { do {
@ -157,14 +157,12 @@ public class SimpleAudioReceiver: NSObject {
self.audioDataCallback!.onAudio(dataToWrite) self.audioDataCallback!.onAudio(dataToWrite)
} }
if self.pushAudioStream != nil && isContinuousRecognitionActive { if self.pushAudioStream != nil && isContinuousRecognitionActive {
try self.pushAudioStream?.write(dataToWrite) try self.pushAudioStream?.write(dataToWrite)
} }
if self.recordfile != nil && isRecord{ if self.recordfile != nil && isRecord{
// print("写入recordfile数据长度: \(dataToWrite.count)")
recordfile?.saveAudioDataToWav(dataToWrite) recordfile?.saveAudioDataToWav(dataToWrite)
} }
// print("写入数据长度: \(dataToWrite.count)")
} catch { } catch {
print("推送音频数据失败: \(error.localizedDescription)") print("推送音频数据失败: \(error.localizedDescription)")
} }
@ -183,9 +181,8 @@ public class SimpleAudioReceiver: NSObject {
if audioSourceType != .external { if audioSourceType != .external {
return return
} }
// print("外部data=\(data)")
// 放入队列,由写线程写入 // 放入队列,由写线程写入
writeQueue.put(data) self.writeQueue.put(data)
} }
/** /**
@ -220,18 +217,22 @@ public class SimpleAudioReceiver: NSObject {
} }
} }
/**
* 运行外部音频捕获
* 修复OSStatus error -50问题
*/
private func runExternalCapture() { private func runExternalCapture() {
if audioSourceType == .external { if audioSourceType == .external {
do { // do {
try audioSession.setCategory(.playback, // try audioSession.setCategory(.playback,
mode: .videoChat, // mode: .videoChat,
options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路 // options: [ .mixWithOthers,.allowBluetoothA2DP // 允许蓝牙耳机,不占用hfp链路
]) // 添加音频优先级控制 // ]) // 添加音频优先级控制
try audioSession.overrideOutputAudioPort(.none) // try audioSession.overrideOutputAudioPort(.none)
try audioSession.setActive(true) // try audioSession.setActive(true)
} catch { // } catch {
print("设置音频会话失败: \(error.localizedDescription)") // print("设置音频会话失败: \(error.localizedDescription)")
} // }
//pushAudioData(data: Data()) //pushAudioData(data: Data())
micCapture.stopCapture() micCapture.stopCapture()
@ -336,7 +337,7 @@ public func restoreOriginalAudioState() {
public func stopMicrophoneCapture() { public func stopMicrophoneCapture() {
self.writeQueue.clear()
_isWriting=false _isWriting=false
micCapture.stopCapture() micCapture.stopCapture()
print("stopMicrophoneCapture") print("stopMicrophoneCapture")
@ -519,6 +520,23 @@ private class LinkedBlockingQueue<T> {
availableItems.signal() availableItems.signal()
} }
/**
* 清空队列
*/
func clear() {
queueLock.lock()
defer { queueLock.unlock() }
// 检查队列是否已关闭
if isClosed {
return
}
// 添加元素(无容量限制,与Android一致)
queue.removeAll()
}
/** /**
* 关闭队列 * 关闭队列
* 关闭后不能再添加新元素,但可以继续取出已有元素 * 关闭后不能再添加新元素,但可以继续取出已有元素

59
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -190,6 +190,11 @@ object BleService {
// 初始化Handler // 初始化Handler
scanHandler = Handler(Looper.getMainLooper()) scanHandler = Handler(Looper.getMainLooper())
recordfile = RecordingFile(this.context)
recordfile!!.fileName = "不拆分"
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "重新压缩"
// 启动队列处理 // 启动队列处理
startAudioQueueProcessing() startAudioQueueProcessing()
@ -202,13 +207,7 @@ object BleService {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false // 根据需要决定是否因为Opus初始化失败而返回false
} }
recordfile = RecordingFile(this.context)
recordfile!!.fileName = "不拆分"
recordfile1 = RecordingFile(this.context)
recordfile1!!.fileName = "重新压缩"
isInitialized = true isInitialized = true
Log.d(TAG, "BLE服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "BLE服务初始化失败: ${e.message}", e) Log.e(TAG, "BLE服务初始化失败: ${e.message}", e)
@ -838,22 +837,7 @@ startBytesStatistics()
recordfile?.saveAudioDataToWav(data) recordfile?.saveAudioDataToWav(data)
// 将完整的帧加入队列,由专门的线程处理 // 将完整的帧加入队列,由专门的线程处理
audioDataQueue.offer(data) audioDataQueue.offer(data)
// // 将接收到的数据添加到缓存中
// synchronized(audioDataBuffer) {
// audioDataBuffer.addAll(data.toList())
// // 当缓存达到阈值时,提取完整的帧进行解码
// while (audioDataBuffer.size >= AUDIO_BUFFER_SIZE) {
// // 提取一个完整的帧(1280字节)
// val frameData = ByteArray(AUDIO_BUFFER_SIZE)
// for (i in 0 until AUDIO_BUFFER_SIZE) {
// frameData[i] = audioDataBuffer.removeAt(0)
// }
// Log.d(TAG, "缓存达到阈值,提取 ${frameData.size} 字节帧进行解码")
// }
// }
} else { } else {
// Log.d(TAG, "Opus解码流未启动,忽略音频数据") // Log.d(TAG, "Opus解码流未启动,忽略音频数据")
} }
@ -1243,7 +1227,7 @@ startBytesStatistics()
// Opus 解码相关 // Opus 解码相关
// ====================================================================================================== // ======================================================================================================
private fun startOpusStreamDecoding( public fun startOpusStreamDecoding(
hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头 hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头
channel: Int = 1, channel: Int = 1,
sampleRate: Int = 16000, // 确认设备端Opus编码采样率 sampleRate: Int = 16000, // 确认设备端Opus编码采样率
@ -1633,7 +1617,12 @@ startBytesStatistics()
) // 或其他默认声道 ) // 或其他默认声道
) )
} }
fun closeCodec1(): Boolean {
recordfile1!!.closeFile()
recordfile!!.closeFile()
return true
}
/** /**
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
@ -1706,16 +1695,20 @@ startBytesStatistics()
*/ */
fun openEncoder(): Boolean { fun openEncoder(): Boolean {
Log.i(TAG, "打开编码0xB1") Log.i(TAG, "打开编码0xB1")
recordfile1!!.closeFile()
recordfile1!!.creatingFiles()
recordfile!!.closeFile()
recordfile!!.creatingFiles()
startOpusStreamDecoding(false, 1, 16000, 40) startOpusStreamDecoding(false, 1, 16000, 40)
// 目前仅发送命令通知设备开始编码。 // // 目前仅发送命令通知设备开始编码。
return sendCommand( // return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), // BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf( // byteArrayOf(
BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), // BleConst.CODEC_CONTROL_ENCODE_ON.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道 // BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道
) // )
) // )
return true
} }
@ -2101,7 +2094,7 @@ class RecordingFile(private val context: Context) {
} }
/** /**
* 保存音频数据到 WAV 文件(异步) * 保存音频数据到 opus 文件(异步)
*/ */
internal fun saveAudioDataToWav(buffer: ByteArray) { internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return if (fos == null || currentAudioFile == null) return

24
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -586,22 +586,24 @@ private var cmdReplyType: UInt8 = 0
} }
/// 打开编码器 /// 打开编码器
func openEncoder() -> Bool { /// - Returns: 操作是否成功
public func openEncoder() -> Bool {
os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info) os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info)
// 确保Opus解码器处于活跃状态(如果不是则启动) // 确保Opus解码器处于活跃状态(如果不是则启动)
// if opusProcessor?.isDecoding() != true { // if opusProcessor?.isDecoding() != true {
startOpusDecoding() startOpusDecoding()
//} //}
let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) // let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) // let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
if result { // if result {
os_log("✅ 编码器命令发送成功", log: logger, type: .info) // os_log("✅ 编码器命令发送成功", log: logger, type: .info)
} else { // } else {
os_log("❌ 编码器命令发送失败", log: logger, type: .error) // os_log("❌ 编码器命令发送失败", log: logger, type: .error)
} // }
return result //return result
return true
} }
/// 打开解码器 /// 打开解码器
@ -1108,9 +1110,7 @@ private var cmdReplyType: UInt8 = 0
notifyCharacteristic != nil ? "✅" : "❌", notifyCharacteristic != nil ? "✅" : "❌",
audioCharacteristic != nil ? "✅" : "❌") audioCharacteristic != nil ? "✅" : "❌")
// 接收到唤醒信号,打开编码器 (设备侧)
let openResult = openEncoder()
os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败")
notifyWakeupSignalReceived() notifyWakeupSignalReceived()
default: default:

Loading…
Cancel
Save