尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

阿里小云KWS模型Android集成指南:低延迟音频采集实现

阿里小云KWS模型Android集成指南:低延迟音频采集实现 阿里小云KWS模型Android集成指南低延迟音频采集实现你是不是也想给自己的Android应用加上“语音唤醒”功能就像智能音箱那样喊一声“小云小云”应用就能立刻响应。听起来挺酷但真动手做起来发现坑还真不少——音频采集延迟高、权限管理复杂、模型集成麻烦搞不好还会遇到各种奇怪的兼容性问题。别担心这篇文章就是来帮你填坑的。我会手把手带你把阿里小云KWS关键词检测模型集成到你的Android应用里重点解决最让人头疼的低延迟音频采集问题。整个过程我会尽量用大白话讲清楚就算你之前没怎么接触过音频开发跟着做也能跑起来。1. 准备工作环境与依赖在开始敲代码之前咱们先把“地基”打好。这一步主要是准备好开发环境和必要的库文件避免后面编译的时候一堆报错。1.1 开发环境要求首先确保你的电脑上已经装好了下面这些东西Android Studio建议用比较新的稳定版比如Flamingo (2022.2.1)或更高版本。老版本可能对新的Gradle插件支持不太好。Android SDKAPI Level至少要到21 (Android 5.0)因为我们要用一些比较现代的API。建议直接装到最新的稳定版。NDK这是编译原生代码C/C必需的。在Android Studio的SDK Manager里找到“SDK Tools”标签页勾选安装NDK (Side by side)版本选一个稳定的比如r25c就行。JavaJDK 8 或 11 都可以Android Studio一般会自带。1.2 获取阿里云NUI SDK阿里小云KWS模型是打包在阿里云的NUINatural User InterfaceSDK里的。这个SDK提供了语音唤醒、识别等一整套能力。官方渠道最靠谱的方式是去阿里云官网找到“智能语音交互”产品页面里面应该有SDK下载和文档。不过有时候找起来有点费劲。备用方案如果你暂时没有阿里云的账号或者觉得申请流程麻烦也可以先关注一下ModelScope魔搭社区。社区里经常有开发者分享一些开源或示例性的语音模型和工具可以作为学习和原型开发的参考。但要注意用于正式产品的话还是建议通过官方渠道获取授权和完整的SDK。拿到SDK后通常是一个.aar文件比如nuisdk-release.aar和一堆文档、示例代码。我们把.aar文件先放一边等下要用。1.3 创建Android项目并配置Gradle打开Android Studio新建一个项目选“Empty Activity”模板就行名字你随便起比如KwsDemo。然后打开项目根目录下的build.gradle文件确保buildscript里的gradle插件版本不要太旧。// 项目根目录的 build.gradle buildscript { dependencies { classpath com.android.tools.build:gradle:8.1.0 // 建议用这个或更高版本 } }接着打开你App模块下的build.gradle文件通常是app/build.gradle我们需要添加一些配置。// app/build.gradle android { compileSdk 34 // 建议用33或34 defaultConfig { applicationId com.yourcompany.kwsdemo minSdk 21 // 最低支持到5.0 targetSdk 34 versionCode 1 versionName 1.0 // 重点启用CMake来编译我们可能需要的原生代码 externalNativeBuild { cmake { cppFlags -stdc17 // 使用C17标准 // 如果你只针对ARM架构设备可以指定abiFilters来减小包体积 // abiFilters armeabi-v7a, arm64-v8a } } } // 配置原生构建 externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt // CMake配置文件路径 version 3.22.1 } } // 建议开启数据绑定方便点可选 buildFeatures { dataBinding true } } dependencies { // 核心依赖把刚才下载的nuisdk-release.aar文件放到app/libs目录下 implementation fileTree(dir: libs, include: [*.jar, *.aar]) // 其他常用依赖 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 // 用于JSON解析阿里SDK回调经常用JSON implementation com.alibaba:fastjson:1.2.83 }把nuisdk-release.aar文件复制到app/libs/文件夹下。如果没有libs文件夹自己新建一个。2. 权限申请与音频采集基础语音唤醒首先得让手机能听到你说话。这一步我们要搞定Android的录音权限并初始化最基础的音频录制对象。2.1 在AndroidManifest.xml中声明权限打开app/src/main/AndroidManifest.xml文件在application标签外面添加录音权限声明。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.kwsdemo !-- 最重要的录音权限 -- uses-permission android:nameandroid.permission.RECORD_AUDIO / !-- 如果需要将模型文件放在外部存储还需要这个可选 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- Android 10及以上需要作用域存储方式不同 -- application ... ... /application /manifest2.2 动态权限申请从Android 6.0开始敏感权限比如录音需要在运行时动态申请。我们在主Activity里做这个事。// MainActivity.kt import android.Manifest import android.content.pm.PackageManager import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat class MainActivity : AppCompatActivity() { companion object { private const val PERMISSION_REQUEST_RECORD_AUDIO 1001 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) checkAndRequestPermissions() } private fun checkAndRequestPermissions() { val permission Manifest.permission.RECORD_AUDIO if (ContextCompat.checkSelfPermission(this, permission) ! PackageManager.PERMISSION_GRANTED) { // 权限还没被授予向用户解释为什么需要可选 if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { // 可以弹个对话框告诉用户为啥要麦克风权限 showPermissionExplanationDialog() } else { // 直接请求权限 ActivityCompat.requestPermissions( this, arrayOf(permission), PERMISSION_REQUEST_RECORD_AUDIO ) } } else { // 权限已经有了可以开始初始化音频相关模块 initAudioAndKws() } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { PERMISSION_REQUEST_RECORD_AUDIO - { if (grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { // 用户同意了开搞 initAudioAndKws() } else { // 用户拒绝了可以提示一下功能无法使用 Toast.makeText(this, 需要麦克风权限才能使用语音唤醒功能, Toast.LENGTH_LONG).show() } } } } private fun showPermissionExplanationDialog() { // 简单实现一个解释对话框 AlertDialog.Builder(this) .setTitle(需要麦克风权限) .setMessage(语音唤醒功能需要使用麦克风来听取您的指令。) .setPositiveButton(确定) { _, _ - ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.RECORD_AUDIO), PERMISSION_REQUEST_RECORD_AUDIO ) } .setNegativeButton(取消, null) .show() } private fun initAudioAndKws() { // 这里先留空我们下一步来填充 Log.d(MainActivity, 权限OK准备初始化音频和KWS) } }2.3 初始化AudioRecordAudioRecord是Android提供的底层音频采集类我们可以控制采样率、声道、音频格式等参数这对于实现低延迟至关重要。我们先在Activity里定义一个AudioRecord变量和相关的音频参数。// 在MainActivity类内部添加这些成员变量 import android.media.AudioFormat import android.media.AudioRecord import android.media.MediaRecorder class MainActivity : AppCompatActivity() { // 音频参数 - 必须和KWS模型匹配 private val sampleRate 16000 // 采样率16kHz这是很多语音模型的标配 private val channelConfig AudioFormat.CHANNEL_IN_MONO // 单声道 private val audioFormat AudioFormat.ENCODING_PCM_16BIT // 16位PCM // 计算20ms音频帧的大小字节数采样率 * 声道数 * 位深 / 8 * 时长(秒) private val frameSizeInBytes (sampleRate * 2 * 0.02).toInt() // 20ms帧16000 * 2 * 0.02 640字节 private var audioRecord: AudioRecord? null private var isRecording false // ... 之前的权限代码 ... private fun initAudioAndKws() { initAudioRecorder() // 接下来会初始化KWS引擎 } private fun initAudioRecorder() { // 获取最小缓冲区大小通常比我们的一帧要大 val minBufferSize AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) // 我们实际使用的缓冲区大小取计算出的帧大小和最小缓冲区的最大值再稍微放大一点比如4倍保证稳定 val bufferSize maxOf(minBufferSize, frameSizeInBytes) * 4 try { audioRecord AudioRecord( MediaRecorder.AudioSource.VOICE_RECOGNITION, // 音源语音识别系统会做一定优化 sampleRate, channelConfig, audioFormat, bufferSize ) if (audioRecord?.state ! AudioRecord.STATE_INITIALIZED) { Log.e(MainActivity, AudioRecord 初始化失败) audioRecord?.release() audioRecord null return } Log.d(MainActivity, AudioRecord 初始化成功缓冲区大小: $bufferSize) } catch (e: Exception) { Log.e(MainActivity, 创建AudioRecord时出错, e) audioRecord null } } override fun onDestroy() { super.onDestroy() stopRecording() audioRecord?.release() audioRecord null } }关键点解释MediaRecorder.AudioSource.VOICE_RECOGNITION这个音源是专门为语音识别优化的系统可能会尝试降低一些环境噪音比用DEFAULT要好。缓冲区大小这里是个平衡艺术。缓冲区太小容易因为来不及处理导致音频丢失欠载缓冲区太大延迟就高了。我们先用“最小所需缓冲区”和“20ms帧大小”里较大的那个再乘以4给系统留点余量。后面性能优化时可以根据实际情况调整。3. 集成阿里小云KWS模型现在音频采集的架子搭好了该请出主角——阿里小云KWS模型了。我们需要初始化SDK设置回调并把采集到的音频数据“喂”给它。3.1 初始化NUI SDK并设置回调根据阿里云SDK的文档我们需要先初始化NativeNui对象并设置一个回调接口来接收唤醒结果。// 在MainActivity中继续添加 import com.alibaba.idst.nui.* import com.alibaba.idst.nui.Constants class MainActivity : AppCompatActivity() { private lateinit var nuiEngine: NativeNui private var isKwsInitialized false // 在 initAudioAndKws 方法中调用初始化 private fun initAudioAndKws() { initAudioRecorder() initKwsEngine() } private fun initKwsEngine() { // 1. 拷贝SDK内置的资源文件模型、配置等到应用私有目录 val context applicationContext if (!CommonUtils.copyAssetsData(context)) { Log.e(MainActivity, 拷贝SDK资源文件失败) return } Log.d(MainActivity, SDK资源文件拷贝完成) // 2. 获取工作目录路径资源文件被拷贝到的地方 val workspace CommonUtils.getModelPath(context) val debugPath ${externalCacheDir?.absolutePath}/debug.also { File(it).mkdirs() } // 3. 创建NUI引擎实例 nuiEngine NativeNui() // 4. 设置回调 val callback object : INativeNuiCallback { override fun onNuiEventCallback( event: Constants.NuiEvent, resultCode: Int, arg2: Int, kwsResult: KwsResult?, asrResult: AsrResult? ) { // 处理各种事件我们最关心唤醒事件 when (event) { Constants.NuiEvent.EVENT_WUW_TRUSTED - { // 唤醒成功 kwsResult?.let { Log.d(KWS_Callback, 唤醒词检测到: ${it.kws}) runOnUiThread { // 在主线程更新UI比如显示一个提示 Toast.makeText(thisMainActivity, 唤醒成功, Toast.LENGTH_SHORT).show() // 你可以在这里触发后续动作比如打开语音识别 } } } Constants.NuiEvent.EVENT_ASR_RESULT - { // 如果开启了后续识别会收到识别结果这部分我们先不展开 Log.d(KWS_Callback, 识别结果: ${asrResult?.asrResult}) } Constants.NuiEvent.EVENT_ASR_ERROR, Constants.NuiEvent.EVENT_MIC_ERROR - { Log.e(KWS_Callback, 发生错误: ${asrResult?.asrResult}, code: $resultCode) } else - { // 其他事件暂时忽略或按需处理 } } } override fun onNuiNeedAudioData(dataArray: ByteArray, requiredSize: Int): Int { // **核心方法**SDK需要音频数据时回调这里 // 我们需要从AudioRecord中读取数据填充到dataArray里 return readAudioData(dataArray, requiredSize) } override fun onNuiAudioStateChanged(state: Constants.AudioState) { // 音频状态变化比如SDK开始/停止要数据了 when (state) { Constants.AudioState.STATE_OPEN - { Log.d(KWS_Callback, SDK开始请求音频启动录音) startRecording() } Constants.AudioState.STATE_CLOSE - { Log.d(KWS_Callback, SDK停止请求音频停止录音) stopRecording() } Constants.AudioState.STATE_PAUSE - { // 暂停状态根据需求处理 stopRecording() } } } override fun onNuiAudioRMSChanged(volume: Float) { // 可以在这里获取音量大小用于做UI上的音量动画 // Log.d(KWS_Callback, 当前音量: $volume) } override fun onNuiVprEventCallback(event: Constants.NuiVprEvent) { // 声纹事件一般用不到 } } // 5. 准备初始化参数JSON格式 val initParams buildInitParams(workspace, debugPath) // 6. 初始化引擎 val ret nuiEngine.initialize( callback, initParams, Constants.LogLevel.LOG_LEVEL_INFO, // 日志级别调试时可以开VERBOSE true // 是否在独立线程运行回调 ) if (ret Constants.NuiResultCode.SUCCESS) { isKwsInitialized true Log.d(MainActivity, KWS引擎初始化成功) // 初始化成功后可以开始对话等待唤醒 startKwsDialog() } else { Log.e(MainActivity, KWS引擎初始化失败错误码: $ret) } } private fun readAudioData(buffer: ByteArray, requiredSize: Int): Int { if (!isRecording || audioRecord null) { return -1 } // 从AudioRecord中读取数据填满SDK要求的缓冲区 // 注意这个方法是阻塞的直到读满requiredSize或出错 return audioRecord?.read(buffer, 0, requiredSize) ?: -1 } private fun startRecording() { if (isRecording) return try { audioRecord?.startRecording() isRecording true Log.d(MainActivity, 音频录制开始) } catch (e: IllegalStateException) { Log.e(MainActivity, 启动录音失败, e) } } private fun stopRecording() { if (!isRecording) return try { audioRecord?.stop() isRecording false Log.d(MainActivity, 音频录制停止) } catch (e: IllegalStateException) { Log.e(MainActivity, 停止录音失败, e) } } }3.2 构建初始化参数与启动唤醒初始化参数buildInitParams是关键它包含了鉴权信息、工作路径、设备ID等。这里面的ak_id,ak_secret,app_key,token等都需要你替换成自己在阿里云控制台申请的真实信息。// 继续在MainActivity中添加 import com.alibaba.fastjson.JSONObject import com.alibaba.fastjson.JSONException private fun buildInitParams(workspace: String, debugPath: String): String { val params JSONObject() try { // --- 鉴权信息必须替换成你自己的--- // 这些信息需要在阿里云智能语音交互控制台创建项目和应用后获取 params[ak_id] your_access_key_id params[ak_secret] your_access_key_secret params[app_key] your_app_key // Token有时可以动态获取这里先简单传入生产环境建议动态刷新 params[token] your_token_or_leave_empty_if_using_ak // --- 鉴权信息结束 --- params[url] wss://nls-gateway.cn-shanghai.aliyuncs.com:443/ws/v1 // 服务地址 params[workspace] workspace // 必填资源文件路径 params[debug_path] debugPath // 调试日志路径 // 服务模式FullMix表示混合模式本地唤醒云端识别如果只用本地唤醒可以尝试FullLocal params[service_mode] Constants.ModeFullMix // 设备ID非常重要用于鉴权计数。请使用能唯一标识设备的ID不要用随机数或空值。 // 可以使用Android ID、UUID等。这里示例用了一个简单方法仅用于演示生产环境需要更稳定的方案 val deviceId Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: default_device_${System.currentTimeMillis()} params[device_id] deviceId // 离线唤醒功能需要的SDK代码 params[sdk_code] software_nls_tts_offline_standard // 标准版离线SDK // 日志级别 params[log_track_level] Constants.LogLevel.LOG_LEVEL_INFO.toString() } catch (e: JSONException) { Log.e(MainActivity, 构建初始化参数JSON失败, e) } // 注意这个字符串包含了敏感信息不要在日志中明文输出 return params.toString() } private fun startKwsDialog() { if (!isKwsInitialized) return // 可以设置一些对话参数比如动态唤醒词 val dialogParams buildDialogParams() nuiEngine.setParams(dialogParams) // 启动对话模式设为KWS关键词检测 val ret nuiEngine.startDialog(Constants.VadMode.TYPE_KWS, dialogParams) if (ret Constants.NuiResultCode.SUCCESS) { Log.d(MainActivity, KWS对话启动成功等待唤醒...) } else { Log.e(MainActivity, 启动KWS对话失败错误码: $ret) } } private fun buildDialogParams(): String { val params JSONObject() try { // 这里可以动态设置一些参数比如临时token刷新 // 也可以设置动态唤醒词列表如果支持的话 // 示例设置一个动态唤醒词“小云小云” /* val wuwArray JSONArray() val wuwObj JSONObject() wuwObj[name] 小云小云 wuwObj[type] main wuwArray.add(wuwObj) params[wuw] wuwArray */ } catch (e: JSONException) { Log.e(MainActivity, 构建对话参数失败, e) } return params.toString() }4. 低延迟优化实战代码写到这里基本功能应该能跑了。但你可能发现唤醒反应有点“迟钝”这就是延迟问题。下面我们针对移动端的特点进行几项关键的优化。4.1 优化AudioRecord缓冲区与读取策略我们之前用了一个固定大小的缓冲区。但SDK每次回调onNuiNeedAudioData时请求的数据量 (requiredSize) 可能和我们的帧大小不完全匹配。为了更精细地控制延迟我们可以实现一个环形缓冲区Ring Buffer作为中间层。// 添加一个简单的环形缓冲区类 class AudioRingBuffer(private val capacity: Int) { private val buffer ByteArray(capacity) private var readIndex 0 private var writeIndex 0 private var available 0 private val lock Object() fun write(data: ByteArray, offset: Int, length: Int): Int { synchronized(lock) { val writeable capacity - available val toWrite minOf(length, writeable) if (toWrite 0) return 0 // 分两段写从writeIndex到末尾如果不够再从开头写 val firstPart minOf(toWrite, capacity - writeIndex) System.arraycopy(data, offset, buffer, writeIndex, firstPart) writeIndex (writeIndex firstPart) % capacity val secondPart toWrite - firstPart if (secondPart 0) { System.arraycopy(data, offset firstPart, buffer, writeIndex, secondPart) writeIndex (writeIndex secondPart) % capacity } available toWrite return toWrite } } fun read(data: ByteArray, offset: Int, length: Int): Int { synchronized(lock) { val toRead minOf(length, available) if (toRead 0) return 0 val firstPart minOf(toRead, capacity - readIndex) System.arraycopy(buffer, readIndex, data, offset, firstPart) readIndex (readIndex firstPart) % capacity val secondPart toRead - firstPart if (secondPart 0) { System.arraycopy(buffer, readIndex, data, offset firstPart, secondPart) readIndex (readIndex secondPart) % capacity } available - toRead return toRead } } fun clear() { synchronized(lock) { readIndex 0 writeIndex 0 available 0 } } } // 在MainActivity中使用环形缓冲区 class MainActivity : AppCompatActivity() { // ... 其他成员变量 ... private lateinit var audioRingBuffer: AudioRingBuffer private val bufferCapacity 1024 * 20 // 20KB的环形缓冲区可以根据需要调整 private fun initAudioRecorder() { // ... 原有的AudioRecord初始化代码 ... audioRingBuffer AudioRingBuffer(bufferCapacity) // 启动一个单独的线程持续从AudioRecord读取数据放入环形缓冲区 startAudioReadingThread() } private fun startAudioReadingThread() { Thread { val tempBuffer ByteArray(frameSizeInBytes) // 每次读一帧 while (isRecording) { val bytesRead audioRecord?.read(tempBuffer, 0, tempBuffer.size) ?: -1 if (bytesRead 0) { audioRingBuffer.write(tempBuffer, 0, bytesRead) } else { // 读取错误或结束 Log.w(AudioThread, 从AudioRecord读取数据失败或结束: $bytesRead) break } } }.start() } // 修改 readAudioData 方法从环形缓冲区读取 private fun readAudioData(buffer: ByteArray, requiredSize: Int): Int { if (!isRecording) return -1 // 尝试从环形缓冲区读取所需大小的数据 // 如果缓冲区里数据不够可能返回小于requiredSize的值SDK应该能处理 return audioRingBuffer.read(buffer, 0, requiredSize) } }这样做的好处是采集线程和SDK消费线程解耦了。采集线程以固定频率比如每20ms抓取音频放入缓冲区SDK需要时随时来取减少了因等待而产生的延迟。4.2 选择合适的AudioSource与避免AGC我们之前用了VOICE_RECOGNITION这通常是好的。但在某些极端低延迟要求的场景可以尝试CAMCORDER如果设备有的话它通常连接到一个物理上离用户嘴更近的麦克风延迟可能更低。另外Android系统有时会自动应用自动增益控制AGC这可能会扭曲音频信号影响唤醒模型的准确性。虽然AudioRecord本身不直接提供关闭AGC的API但使用VOICE_RECOGNITION源时系统通常会为语音识别优化可能已经减少了AGC的干扰。如果问题严重可能需要更底层的OpenSL ES或AAudioAPI来获得完全控制。4.3 线程模型优化与避免UI阻塞确保onNuiNeedAudioData回调方法执行得非常快。我们的环形缓冲区设计就是为了这个。千万不要在这个回调里做任何耗时的操作比如文件IO、网络请求或者复杂的计算。所有UI更新都应该通过runOnUiThread或Handler抛到主线程去执行就像我们之前在收到唤醒事件时做的那样。5. 常见问题与调试技巧集成过程中你肯定会遇到一些“坑”。这里列举几个常见的并给出排查思路。问题初始化失败错误码非SUCCESS。检查鉴权信息ak_id,ak_secret,app_key,token,sdk_code是否全部正确是否对应同一个阿里云项目。检查网络首次初始化可能需要联网验证。确保设备可以访问阿里云的服务地址。检查资源文件确认CommonUtils.copyAssetsData(context)返回true并且workspace路径有效。问题能初始化但永远唤不醒。检查音频参数确认AudioRecord的采样率16000、声道单声道、格式16位PCM与KWS模型要求完全一致。检查音频通路确保没有其他应用独占麦克风。可以尝试先运行一个系统录音机App看是否能录到声音。检查唤醒词默认的唤醒词是什么可能是“小云小云”。你喊对了吗在安静环境下用正常语速和音量试试。开启详细日志初始化时把日志级别设为LOG_LEVEL_VERBOSE查看SDK内部是否有错误或警告信息。问题唤醒延迟很高1秒。检查缓冲区AudioRecord的缓冲区是否设置得过大尝试逐步减小bufferSize观察延迟变化和稳定性。检查环形缓冲区如果用了环形缓冲区查看其容量是否合理是否经常满或空。性能分析使用Android Studio的Profiler工具监控音频线程的CPU使用率和调度情况看是否存在线程阻塞。问题在部分设备上崩溃或不工作。检查权限确保动态权限申请成功并且用户没有在设置中手动关闭App的麦克风权限。检查设备兼容性某些设备或定制ROM的音频实现可能有bug。尝试在更多设备上测试。检查SDK版本确保使用的nuisdk-release.aar版本与你的编译环境、目标API级别兼容。调试小技巧你可以把onNuiNeedAudioData里读取到的原始PCM数据临时保存到文件里然后用电脑上的音频软件如Audacity打开播放确认采集到的声音是否清晰、有无杂音、音量是否正常。这是定位音频问题最直接的方法。6. 总结走完这一趟你应该已经把阿里小云KWS模型成功地集成到你的Android应用里了并且对如何实现低延迟的音频采集有了更深的体会。回顾一下核心其实就是几个点权限管理要到位、音频参数要对得上、SDK初始化和回调要绑对地方最后再用环形缓冲区这类技巧把采集和消费的节奏协调好延迟自然就下来了。当然这只是一个起点。在实际产品中你可能还需要处理更多细节比如唤醒后的业务逻辑衔接、在后台保活、优化耗电、适配海量设备等等。但有了这个可工作的基础后续的扩展就有了坚实的支撑。建议你先在几种不同型号的手机上充分测试观察效果再根据实际情况做进一步的微调和优化。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
返回列表