二、Android 音频框架

Android 音频框架 --- APPLICATION->JNI

摘要

在该章节,主要解释 Android 官方提供的的音频框架图,以及针对 APPLICATION 的应用逻辑进行讲解--- 以 AudioTrack 为例。

官网资料中, 有提供 Android 的音频框架图,这里先解读一下图中的含义。

1. APPLICATION

这里提供一个播放(track)的demo 来展示应用播放的流程,来帮助更好的理解这个流程框图。(完整的代码放到文件的末尾, 只是用来演示播放流程,不一定能跑通哦).

1.1 提取音轨, 解码

AudioTrack 只能够播放原始数据 也就是 pcm 数据. 文件的后缀通常为 .pcm.

而我们平常遇到的 .mp3 通常是经过封装的数据格式。因此,如果要将数据送入 AudioTrack 进行播放,那么就必须对数据进行提取解码。所以一般在播放的第一步为 提取音轨, 解码。

java 复制代码
// 提取音频轨道
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);

// 创建解码器
codec = MediaCodec.createDecoderByType(mime);
codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); // 解码器输入待解码数据
int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs); // 获取解码后的输出缓冲区

在提取完音频数据之后需要对数据进行解码, 解析成 AudioTrack 能够播放的音频数据类型。

1.2 创建 AudioTrack.java

该组件为一个通用的应用层播放组件, 与其具有相似功能的还有MediaPlayer, 但他们最终都会调用到 framework 中的 AudioTrack.cpp.

java 复制代码
AudioTrack audioTrack = new AudioTrack(
    AudioManager.STREAM_MUSIC, 	// StreamType
    sampleRate,	// 采样率
    channelConfig, 	// 通道配置
    AudioFormat.ENCODING_PCM_16BIT, 	// 数据格式, 位深
    minBufferSize, 	// 最小buffeSize
    AudioTrack.MODE_STREAM); // MODE_STREAM (一般用这个其他暂不做详细解释, 见下面的解释).
  • MODE_STATIC 预先将需要播放的音频数据读取到内存中,然后才开始播放。
  • MODE_STREAM 边读边播,不会将数据直接加载到内存
1.3 播放

目前我们做了几件事情, 提取文件的音频轨道、对音轨的数据进行解码为原始数据格式。那么下一步就是播放。播放的流程很简单,就是向 Track 中不断的写数据.

java 复制代码
audioTrack.play(); // 启动AudioTrack,开始播放音频, 其中会触发一系列的 AudioFramework 框架的流程,这里暂不细究. 

// 当文件没有播放完一直向其中写数据。
while (!isEOS) {
  audioTrack.write(buffer, 0, buffer.length);
}
1.4 停止
java 复制代码
codec.stop(); // 停止解码器
codec.release(); // 释放解码器资源
extractor.release(); // 释放MediaExtractor资源
audioTrack.stop(); // 停止AudioTrack
audioTrack.release(); // 释放AudioTrack资源

2. 调用关系

在上面的代码中,我们看到了 APPLICATION 的代码, 以及看到了 APPLICATION 是调用 JNI 的代码,但是对比 Android 官网的架构图中,它具体调用的是哪些 JNI 的方法呢?这里也将其贴出来。这里只贴和AudioTrack 相关的逻辑,提取音频轨道和解码不在该音频框架中

Java -> JNI

java 复制代码
AudioTrack audioTrack = new AudioTrack(
    AudioManager.STREAM_MUSIC, 
    sampleRate,
    channelConfig, 
    AudioFormat.ENCODING_PCM_16BIT, 
    minBufferSize, 
    AudioTrack.MODE_STREAM); // 创建AudioTrack对象

| |

| | 调用 JNI

| |

\ /

cpp 复制代码
...
{"native_setup",
 "(Ljava/lang/Object;Ljava/lang/Object;[IIIIII[ILandroid/os/Parcel;"
 "JZILjava/lang/Object;Ljava/lang/String;)I",
 (void *)android_media_AudioTrack_setup},
...

也就是上层在创建 AudioTrack 的时候会调用到 JNI 的 android_media_AudioTrack_setup 方法,具体的调用逻辑这里不做赘述。感兴趣可自行先看,具体的逻辑放到后面的章节说明。

小结

这里简单介绍了 Android 音频框架图,以AudioTrack 为例,举例说明 APPLICATION 是如何调用 JNI 方法。

下一章,将说明 JNI 到 libmeida 的具体调用。


demo 代码

java 复制代码
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;

import java.io.IOException;
import java.nio.ByteBuffer;

public class AudioTrackDemo {

    private static final String TAG = "AudioTrackDemo";

    public void playMp3(String filePath) {
        MediaExtractor extractor = new MediaExtractor(); // 创建MediaExtractor,用于提取媒体数据
        try {
            extractor.setDataSource(filePath); // 设置数据源为要播放的MP3文件路径
        } catch (IOException e) {
            Log.e(TAG, "Error setting data source", e); // 如果发生错误,打印错误信息
            return; // 出现异常时,直接返回
        }

        int audioTrackIndex = -1; // 初始化音频轨道索引
        MediaFormat format = null; // 初始化媒体格式

        // 遍历媒体文件中的轨道,找到音频轨道
        for (int i = 0; i < extractor.getTrackCount(); i++) {
            format = extractor.getTrackFormat(i); // 获取轨道格式
            String mime = format.getString(MediaFormat.KEY_MIME); // 获取MIME类型
            if (mime.startsWith("audio/")) { // 如果MIME类型以"audio/"开头,表示这是音频轨道
                audioTrackIndex = i; // 记录音频轨道的索引
                break; // 找到音频轨道后跳出循环
            }
        }

        if (audioTrackIndex == -1) {
            Log.e(TAG, "No audio track found in file."); // 如果没有找到音频轨道,打印错误信息
            return; // 没有音频轨道时,直接返回
        }
 
        extractor.selectTrack(audioTrackIndex); // 选择音频轨道进行解码

        String mime = format.getString(MediaFormat.KEY_MIME); // 获取音频轨道的MIME类型
        MediaCodec codec;
        try {
            codec = MediaCodec.createDecoderByType(mime); // 创建解码器,用于解码音频数据
        } catch (IOException e) {
            Log.e(TAG, "Error creating codec", e); // 如果解码器创建失败,打印错误信息
            return; // 发生异常时,直接返回
        }

        codec.configure(format, null, null, 0); // 配置解码器,设置解码格式
        codec.start(); // 启动解码器

        ByteBuffer[] inputBuffers = codec.getInputBuffers(); // 获取输入缓冲区,用于存放解码前的数据
        ByteBuffer[] outputBuffers = codec.getOutputBuffers(); // 获取输出缓冲区,用于存放解码后的PCM数据
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); // 创建BufferInfo对象,用于描述解码后的数据

        int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); // 获取音频的采样率
        int channelConfig = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1 ?
                AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO; // 根据声道数量设置音频输出格式(单声道或立体声)

        int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,
                channelConfig, AudioFormat.ENCODING_PCM_16BIT); // 计算AudioTrack所需的最小缓冲区大小
        AudioTrack audioTrack = new AudioTrack(
            AudioManager.STREAM_MUSIC, 
            sampleRate,
            channelConfig, 
            AudioFormat.ENCODING_PCM_16BIT, 
            minBufferSize, 
            AudioTrack.MODE_STREAM); // 创建AudioTrack对象
            
        audioTrack.play(); // 启动AudioTrack,开始播放音频

        boolean isEOS = false; // 标记是否到达文件末尾
        long timeoutUs = 10000; // 解码超时时间,单位为微秒

        while (!isEOS) {
            int inputBufferIndex = codec.dequeueInputBuffer(timeoutUs); // 从解码器获取可用的输入缓冲区
            if (inputBufferIndex >= 0) {
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; // 获取对应的输入缓冲区
                int sampleSize = extractor.readSampleData(inputBuffer, 0); // 从媒体文件中读取一帧数据到输入缓冲区

                if (sampleSize < 0) {
                    // 如果没有更多数据,表示文件结束
                    codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); // 向解码器发送结束标志
                    isEOS = true; // 设置结束标志为true
                } else {
                    codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0); // 将读取的数据送入解码器
                    extractor.advance(); // 提取下一帧数据
                }
            }

            int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs); // 获取解码后的输出缓冲区
            if (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; // 获取对应的输出缓冲区

                byte[] chunk = new byte[bufferInfo.size]; // 创建字节数组存储解码后的PCM数据
                outputBuffer.get(chunk); // 将输出缓冲区的数据读入字节数组
                outputBuffer.clear(); // 清空输出缓冲区

                audioTrack.write(chunk, 0, chunk.length); // 将PCM数据写入AudioTrack进行播放
                codec.releaseOutputBuffer(outputBufferIndex, false); // 释放输出缓冲区
            }
        }

        codec.stop(); // 停止解码器
        codec.release(); // 释放解码器资源
        extractor.release(); // 释放MediaExtractor资源
        audioTrack.stop(); // 停止AudioTrack
        audioTrack.release(); // 释放AudioTrack资源
    }
}
相关推荐
studyForMokey2 小时前
【Android面试】Java专题 todo
android·java·面试
代码改善世界2 小时前
【MATLAB初阶】矩阵操作(二):矩阵的运算
android·matlab·矩阵
九皇叔叔2 小时前
MySQL实操指南:复制表及数据复制全解析
android·数据库·mysql
梦想不只是梦与想2 小时前
flutter 与 Android iOS 通信?以及实现原理(一)
android·flutter·ios·methodchannel·eventchannel·basicmessage
Lambert_lin04 小时前
Android grade9.0 之后 自定义apk 名称
android·kotlin
fengci.4 小时前
ctfshow其他(web408-web432)
android·开发语言·前端·学习·php
Kapaseker4 小时前
“点击显示全文” — Compose 实现
android·kotlin
lxysbly4 小时前
安卓土星ss模拟器下载(支持中文、金手指)
android
程序员陆业聪5 小时前
异步初始化框架设计:用拓扑排序干掉启动串行瓶颈
android