13 changed files with 551 additions and 419 deletions
@ -0,0 +1,29 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:typed_data'; |
||||
|
|
||||
|
/// 语音识别服务接口 |
||||
|
abstract class AudioService { |
||||
|
/// 开始录音 |
||||
|
Future<bool> enableRecord(String filePath); |
||||
|
|
||||
|
/// 暂停录音 |
||||
|
Future<bool> pauseRecord(); |
||||
|
|
||||
|
/// 继续录音 |
||||
|
Future<bool> resumeRecord(); |
||||
|
|
||||
|
/// 设置音频配置 |
||||
|
Future<bool> setAudioConfig({ |
||||
|
int sampleRate = 16000, |
||||
|
int channels = 1, |
||||
|
}); |
||||
|
|
||||
|
// /// 移动文件到新路径 |
||||
|
Future<bool> moveFile(String sourcePath, String destPath); |
||||
|
|
||||
|
// /// 重命名指定路径的音频文件 |
||||
|
Future<bool> renameFile(String filePath, String newName); |
||||
|
|
||||
|
/// 结束录音 |
||||
|
Future<bool> stopRecord(bool isSave); |
||||
|
} |
||||
@ -0,0 +1,403 @@ |
|||||
|
package com.yunqiinnovation.azure_speech |
||||
|
|
||||
|
import android.content.Context |
||||
|
import android.media.AudioFormat |
||||
|
import android.media.AudioRecord |
||||
|
import android.media.MediaRecorder |
||||
|
import android.util.Log |
||||
|
import com.microsoft.cognitiveservices.speech.* |
||||
|
import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat |
||||
|
import java.util.concurrent.LinkedBlockingQueue |
||||
|
import com.microsoft.cognitiveservices.speech.audio.AudioInputStream |
||||
|
import com.microsoft.cognitiveservices.speech.audio.PushAudioInputStream |
||||
|
import java.util.concurrent.atomic.AtomicBoolean |
||||
|
import android.media.AudioManager |
||||
|
// 添加缺失的导入 |
||||
|
import android.media.AudioFocusRequest |
||||
|
import android.media.AudioAttributes |
||||
|
import com.yunqiinnovation.azure_speech.tools.RecordFile |
||||
|
|
||||
|
/** |
||||
|
* 简单音频接收器类,用于处理音频录制和流传输 |
||||
|
* @param azureAsrHelper AzureAsrHelper实例的引用,用于访问共享状态 |
||||
|
*/ |
||||
|
class SimpleAudioReceiver(private val context: Context) { |
||||
|
|
||||
|
companion object { |
||||
|
private const val TAG = "SimpleAudioReceiver" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 音频来源类型(使用 AzureAsrHelper 中定义的枚举) |
||||
|
*/ |
||||
|
enum class AudioSourceType { |
||||
|
/** 使用设备麦克风 */ |
||||
|
MICROPHONE, |
||||
|
/** 使用外部提供的音频数据 */ |
||||
|
EXTERNAL |
||||
|
} |
||||
|
|
||||
|
private val bufferSize = 4096 // 可根据需要调整 |
||||
|
var audioRecord: AudioRecord? = null |
||||
|
var pushAudioStream: PushAudioInputStream? = null |
||||
|
// 音频源配置 |
||||
|
var audioSourceType = AudioSourceType.MICROPHONE |
||||
|
// 新增:用于异步写入的队列和线程 |
||||
|
private val writeQueue = LinkedBlockingQueue<ByteArray>() |
||||
|
private val isRunning = AtomicBoolean(false)// 控制线程是否继续存在 |
||||
|
private val isWriting = AtomicBoolean(false) // 控制是否应该写入数据 |
||||
|
private var writeThread: Thread? = null |
||||
|
|
||||
|
// 音频配置 |
||||
|
private val channelConfig = AudioFormat.CHANNEL_IN_MONO |
||||
|
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT |
||||
|
// 音频模式管理 |
||||
|
private var audioManager: AudioManager? = null |
||||
|
private var originalAudioMode = AudioManager.MODE_NORMAL |
||||
|
|
||||
|
// 录音文件处理 |
||||
|
var recordfile: RecordFile? = null |
||||
|
/** |
||||
|
* 获取最优音频格式 |
||||
|
*/ |
||||
|
private fun getOptimalAudioFormat(): AudioStreamFormat { |
||||
|
// 支持的采样率列表(按优先级排序) |
||||
|
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) |
||||
|
|
||||
|
// 查找设备支持的最佳采样率 |
||||
|
val sampleRate = supportedSampleRates.firstOrNull { rate -> |
||||
|
val bufferSize = AudioRecord.getMinBufferSize( |
||||
|
rate, |
||||
|
AudioFormat.CHANNEL_IN_MONO, |
||||
|
AudioFormat.ENCODING_PCM_16BIT |
||||
|
) |
||||
|
bufferSize > 0 // 返回正值表示支持 |
||||
|
} ?: 16000 // 默认回退值 |
||||
|
|
||||
|
Log.i(TAG, "使用采样率: ${sampleRate}Hz") |
||||
|
|
||||
|
// 创建对应的音频格式 |
||||
|
return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化音频录制 |
||||
|
*/ |
||||
|
fun initAudioRecord() { |
||||
|
val format = getOptimalAudioFormat() |
||||
|
pushAudioStream = AudioInputStream.createPushStream(format) |
||||
|
isRunning.set(true) |
||||
|
Log.d(TAG, "initAudioRecord: ${isRunning}") |
||||
|
startWriteThread() // 再启动数据读取线程 |
||||
|
// 初始化音频管理器 |
||||
|
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
||||
|
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 启动写入线程 |
||||
|
*/ |
||||
|
private fun startWriteThread() { |
||||
|
|
||||
|
writeThread = Thread { |
||||
|
try { |
||||
|
while (isRunning.get()) { |
||||
|
// Log.d(TAG, "startWriteThread:isWriting= ${isWriting}") |
||||
|
if (!isWriting.get()) { |
||||
|
Thread.sleep(10) // 短暂休眠避免空转 |
||||
|
continue |
||||
|
} |
||||
|
var data: ByteArray? = null |
||||
|
var bytesToWrite = 0 |
||||
|
//Log.d(TAG, "startWriteThread: ${audioRecord?.recordingState}") |
||||
|
// 情况1:正在录制中 -> 直接从AudioRecord读取 |
||||
|
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { |
||||
|
data = ByteArray(bufferSize) |
||||
|
val bytesRead = audioRecord?.read(data, 0, bufferSize) ?: -1 |
||||
|
|
||||
|
when { |
||||
|
bytesRead < 0 -> { |
||||
|
Log.e("tag", "读取音频失败,错误码: $bytesRead") |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
bytesRead == 0 -> continue // 无数据可读 |
||||
|
else -> bytesToWrite = bytesRead // 有效数据 |
||||
|
} |
||||
|
} |
||||
|
// 情况2:不在录制但队列有数据 -> 从队列获取 |
||||
|
else if (writeQueue.isNotEmpty()) { |
||||
|
data = writeQueue.poll() |
||||
|
Log.d("tag", "写入数据: ${data?.size}") |
||||
|
bytesToWrite = data?.size ?: 0 |
||||
|
} |
||||
|
// 确保有有效数据再写入 |
||||
|
if (data != null && bytesToWrite > 0) { |
||||
|
// 处理实际读取长度 < bufferSize 的情况 |
||||
|
val finalData = |
||||
|
if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data |
||||
|
|
||||
|
try { |
||||
|
Log.d(TAG, "写入数据大小: ${finalData.size}") |
||||
|
pushAudioStream?.write(finalData) |
||||
|
|
||||
|
|
||||
|
recordfile?.saveAudioDataToWav(finalData) |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "写入失败: ${e.message}") |
||||
|
} |
||||
|
} else { |
||||
|
Thread.yield() // 避免空转消耗CPU |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "写入线程异常: ${e.stackTraceToString()}") |
||||
|
} finally { |
||||
|
Log.d(TAG, "音频写入线程退出") |
||||
|
writeQueue.clear() |
||||
|
} |
||||
|
}.apply { |
||||
|
name = "AudioWriteThread" |
||||
|
start() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 外部音频输入 |
||||
|
*/ |
||||
|
fun saveAudioDataTo(buffer: ByteArray) { |
||||
|
if (audioSourceType == AudioSourceType.MICROPHONE) return |
||||
|
// 放入队列,由写线程写入 |
||||
|
writeQueue.offer(buffer.copyOf()) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开始音频输入 |
||||
|
*/ |
||||
|
fun startAudioRecord(audioSourceType: AudioSourceType) { |
||||
|
this.audioSourceType = audioSourceType |
||||
|
writeQueue.clear() |
||||
|
isWriting.set(true) |
||||
|
|
||||
|
when (audioSourceType) { |
||||
|
AudioSourceType.MICROPHONE -> runMicrophoneCapture() |
||||
|
AudioSourceType.EXTERNAL -> runExternalCapture() |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 配置通话音频模式以优化回声消除 |
||||
|
*/ |
||||
|
private fun setupCommunicationAudioMode() { |
||||
|
try { |
||||
|
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
||||
|
|
||||
|
// 保存原始音频模式 |
||||
|
originalAudioMode = audioManager?.mode ?: AudioManager.MODE_NORMAL |
||||
|
|
||||
|
// 设置通话模式 - 这是关键! |
||||
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION |
||||
|
|
||||
|
// 启用扬声器(如果需要外放) |
||||
|
audioManager?.isSpeakerphoneOn = true |
||||
|
|
||||
|
// 请求音频焦点 |
||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { |
||||
|
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) |
||||
|
.setAudioAttributes( |
||||
|
AudioAttributes.Builder() |
||||
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) |
||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) |
||||
|
.build() |
||||
|
) |
||||
|
.build() |
||||
|
|
||||
|
audioManager?.requestAudioFocus(focusRequest) |
||||
|
} |
||||
|
|
||||
|
Log.d("AudioMode", "通话音频模式已设置: MODE_IN_COMMUNICATION") |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioMode", "设置通话音频模式失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 运行麦克风捕获 |
||||
|
*/ |
||||
|
private fun runMicrophoneCapture() { |
||||
|
try { |
||||
|
// 首先设置通话音频模式 |
||||
|
setupCommunicationAudioMode() |
||||
|
|
||||
|
val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) |
||||
|
val sampleRate = supportedSampleRates.firstOrNull { rate -> |
||||
|
val bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat) |
||||
|
bufferSize > 0 |
||||
|
} ?: 16000 |
||||
|
|
||||
|
val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) |
||||
|
|
||||
|
// 使用VOICE_COMMUNICATION音频源(专为VoIP优化) |
||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { |
||||
|
val format = AudioFormat.Builder() |
||||
|
.setSampleRate(sampleRate) |
||||
|
.setEncoding(audioFormat) |
||||
|
.setChannelMask(AudioFormat.CHANNEL_IN_MONO) |
||||
|
.build() |
||||
|
|
||||
|
audioRecord = AudioRecord.Builder() |
||||
|
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) |
||||
|
.setAudioFormat(format) |
||||
|
.setBufferSizeInBytes(minBufferSize * 2) |
||||
|
.build() |
||||
|
} else { |
||||
|
audioRecord = AudioRecord( |
||||
|
MediaRecorder.AudioSource.VOICE_COMMUNICATION, |
||||
|
sampleRate, |
||||
|
channelConfig, |
||||
|
audioFormat, |
||||
|
minBufferSize * 2 |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { |
||||
|
throw IllegalStateException("AudioRecord初始化失败") |
||||
|
} |
||||
|
|
||||
|
audioRecord?.startRecording() |
||||
|
|
||||
|
Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz") |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("TAG", "麦克风捕获失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 运行外部捕获 |
||||
|
*/ |
||||
|
private fun runExternalCapture() { |
||||
|
Log.d("TAG", "外部音频捕获启动") |
||||
|
try { // TODO: 实现外部音频源捕获逻辑 |
||||
|
// 停止录音 |
||||
|
if (audioRecord?.recordingState == AudioRecord.STATE_INITIALIZED) { |
||||
|
Log.d("TAG", "外部音频捕获启动 释放audioRecord") |
||||
|
audioRecord?.stop() |
||||
|
// 释放录音实例 |
||||
|
audioRecord?.release() |
||||
|
audioRecord = null |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("TAG", "外部音频捕获异常: ${e.message}") |
||||
|
} finally { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 继续麦克风捕获 |
||||
|
*/ |
||||
|
fun resumeRecord() { |
||||
|
try { |
||||
|
isWriting.set(true) |
||||
|
// 首先设置通话音频模式 |
||||
|
setupCommunicationAudioMode() |
||||
|
// 停止录音 |
||||
|
audioRecord?.startRecording() |
||||
|
|
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "继续录音失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 停止麦克风捕获 |
||||
|
*/ |
||||
|
fun stopMicrophoneCapture() { |
||||
|
try { |
||||
|
isWriting.set(false) |
||||
|
|
||||
|
// 停止录音 |
||||
|
audioRecord?.stop() |
||||
|
|
||||
|
// 恢复音频模式 |
||||
|
restoreCommunicationAudioMode() |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "停止麦克风捕获失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 释放音频资源 |
||||
|
*/ |
||||
|
fun releaseAudioResources() { |
||||
|
try { |
||||
|
isRunning.set(false) |
||||
|
isWriting.set(false) |
||||
|
|
||||
|
// 中断并等待捕获线程结束 |
||||
|
writeThread?.interrupt() |
||||
|
writeThread?.join(300) // 最多等待300ms |
||||
|
|
||||
|
// 释放录音实例 |
||||
|
audioRecord?.release() |
||||
|
audioRecord = null |
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "释放音频资源失败: ${e.message}") |
||||
|
e.printStackTrace() |
||||
|
} finally { |
||||
|
writeQueue.clear() |
||||
|
writeThread = null |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 禁用蓝牙音频功能,切换回正常音频模式 |
||||
|
*/ |
||||
|
fun disableBluetoothAudio() { |
||||
|
try { |
||||
|
// 关闭蓝牙SCO(Synchronous Connection Oriented)音频路由 |
||||
|
audioManager?.isBluetoothScoOn = false |
||||
|
// 停止蓝牙SCO连接 |
||||
|
audioManager?.stopBluetoothSco() |
||||
|
// 切换回正常音频模式(非通话模式) |
||||
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioConfig", "Failed to disable Bluetooth: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 恢复原始音频设备状态(通常是重新启用蓝牙) |
||||
|
*/ |
||||
|
fun restoreOriginalAudioState() { |
||||
|
try { |
||||
|
// 恢复应用启动时的原始音频模式 |
||||
|
audioManager?.mode = AudioManager.MODE_NORMAL |
||||
|
|
||||
|
// 重新启用蓝牙SCO |
||||
|
audioManager?.isBluetoothScoOn = true |
||||
|
// 启动蓝牙SCO连接(通常在需要蓝牙通话时使用) |
||||
|
audioManager?.startBluetoothSco() |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioConfig", "Failed to restore audio state: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
/** |
||||
|
* 恢复原始音频模式 |
||||
|
*/ |
||||
|
private fun restoreCommunicationAudioMode() { |
||||
|
try { |
||||
|
audioManager?.mode = originalAudioMode |
||||
|
audioManager?.isSpeakerphoneOn = false |
||||
|
// 释放音频焦点 |
||||
|
Log.d("AudioMode", "音频模式已恢复") |
||||
|
} catch (e: Exception) { |
||||
|
Log.e("AudioMode", "恢复音频模式失败: ${e.message}") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue