You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 14:54:07