Android基于TFLite的音频分类如何传入本地存储音频文件?
实现本地音频文件分类的解决方案
一、权限配置修改
首先修改AndroidManifest.xml中的权限配置,新增存储读取权限,不需要实时录音功能的话可以移除原有录音权限:
<!-- 可选:不需要实时录音可移除 --> <!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> --> <!-- Android 12及以下存储读取权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <!-- Android 13及以上音频读取权限 --> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
代码中需要动态申请对应的存储权限,申请逻辑和原有录音权限申请逻辑一致。
二、布局文件修改
在原有activity_main.xml中新增文件选择按钮,方便触发本地文件选择操作:
<!-- 在原有ConstraintLayout节点下添加如下按钮 --> <Button android:id="@+id/btn_select_audio" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="选择音频文件" app:layout_constraintTop_toBottomOf="@+id/output" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="32dp"/>
三、核心逻辑修改
1. 新增音频文件解码方法
需要把本地音频文件解码为模型要求的PCM数据,通常基于YAMNet的音频分类模型要求输入为16kHz采样率、单声道、16位整型PCM数据,解码逻辑如下:
// 新增需要导入的类 import android.content.Intent import android.media.MediaExtractor import android.media.MediaFormat import android.media.MediaCodec import android.net.Uri import android.os.Build import android.widget.Button import java.nio.ByteOrder // 解码音频文件为16kHz单声道16位PCM数据 private fun decodeAudio(uri: Uri): ShortArray? { val extractor = MediaExtractor() extractor.setDataSource(this, uri, null) // 定位音频轨道 var audioTrackIndex = -1 var format: MediaFormat? = null for (i in 0 until extractor.trackCount) { val trackFormat = extractor.getTrackFormat(i) if (trackFormat.getString(MediaFormat.KEY_MIME)?.startsWith("audio/") == true) { audioTrackIndex = i format = trackFormat break } } if (audioTrackIndex == -1 || format == null) return null extractor.selectTrack(audioTrackIndex) // 初始化解码器 val codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!) codec.configure(format, null, null, 0) codec.start() val bufferInfo = MediaCodec.BufferInfo() val outputBuffers = mutableListOf<Short>() val timeoutUs = 10000L var isEOS = false while (!isEOS) { val inputBufferIndex = codec.dequeueInputBuffer(timeoutUs) if (inputBufferIndex >= 0) { val inputBuffer = codec.getInputBuffer(inputBufferIndex)!! val sampleSize = extractor.readSampleData(inputBuffer, 0) if (sampleSize < 0) { codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) isEOS = true } else { codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.sampleTime, 0) extractor.advance() } } var outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs) while (outputBufferIndex >= 0) { val outputBuffer = codec.getOutputBuffer(outputBufferIndex)!! // 转换为Short数组 val shortBuffer = outputBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer() val shortArray = ShortArray(shortBuffer.remaining()) shortBuffer.get(shortArray) outputBuffers.addAll(shortArray.toList()) codec.releaseOutputBuffer(outputBufferIndex, false) outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs) } } // 释放资源 codec.stop() codec.release() extractor.release() return outputBuffers.toShortArray() }
2. 替换原有MainActivity分类逻辑
移除实时录音相关逻辑,改为读取解码后的音频数据逐块分类:
class MainActivity : AppCompatActivity() { var probabilityThreshold: Float = 0.3f val REQUEST_STORAGE_PERMISSION = 1338 val REQUEST_SELECT_AUDIO = 1339 lateinit var audioClassifier: AudioClassifier lateinit var showResult: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) showResult = findViewById(R.id.output) val selectBtn = findViewById<Button>(R.id.btn_select_audio) // 申请存储权限 val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { arrayOf(Manifest.permission.READ_MEDIA_AUDIO) } else { arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) } requestPermissions(permissions, REQUEST_STORAGE_PERMISSION) // 加载TFLite模型 audioClassifier = AudioClassifier.createFromFile(this, "my_birds_model.tflite") // 文件选择按钮点击事件 selectBtn.setOnClickListener { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { type = "audio/*" addCategory(Intent.CATEGORY_OPENABLE) } startActivityForResult(intent, REQUEST_SELECT_AUDIO) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_SELECT_AUDIO && resultCode == RESULT_OK) { data?.data?.let { uri -> Thread { // 解码选中的音频文件 val pcmData = decodeAudio(uri) ?: return@Thread val tensorAudio = audioClassifier.createInputTensorAudio() // 获取模型单次推理需要的采样点数量 val bufferSize = tensorAudio.bufferSize.remaining() var offset = 0 // 逐块读取音频数据进行分类 while (offset + bufferSize < pcmData.size) { tensorAudio.load(pcmData, offset, bufferSize) // 原有分类逻辑不变 val output = audioClassifier.classify(tensorAudio) var filteredModelOutput = output[0].categories.filter { it.label.contains("Bird") && it.score > probabilityThreshold } if (filteredModelOutput.isNotEmpty()) { Log.i("Yamnet", "bird sound detected!") filteredModelOutput = output[1].categories.filter { it.score > probabilityThreshold } } val outputStr = filteredModelOutput.sortedBy { -it.score } .joinToString(separator = "\n") { "${it.label} -> ${it.score} " } if (outputStr.isNotEmpty()) { runOnUiThread { showResult.text = outputStr } } offset += bufferSize // 模拟原有每秒分类的间隔,不需要可删除 Thread.sleep(1000) } }.start() } } } // 此处放入上文中的decodeAudio解码方法 private fun decodeAudio(uri: Uri): ShortArray? { // 解码逻辑 } }
注意事项
- 如果原音频采样率不是16kHz,需要额外添加重采样逻辑,否则会导致分类准确率下降
- 大文件解码和推理全部放在后台线程执行,避免阻塞UI
- 模型要求的输入长度也可以通过
audioClassifier.requiredInputBufferSize获取,不需要硬编码
内容的提问来源于stack exchange,提问作者SK SABBIR
相关产品推荐
相关产品推荐

