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