Android FFmpeg系列05--音频解码与播放

2022-09-21 14:55:12 浏览数 (1)

引言

在前面的连载系列中,我们分别用FFmpeg的软解和硬解两种方式解码了本地mp4文件的视频流并使用OpenGL渲染上屏

Android FFmpeg系列03--视频解码与渲染

Android FFmpeg系列04--FFmpeg调用MediaCodec进行硬解码

本篇文章将通过音频基础AudioTrackFFmpeg音频解码&重采样三个部分的讲解来完成对Demo中mp4文件内音频流的解码与播放功能

(48kHZ,双声道,fltp格式)

音频基础

关于音频采样率、声道、采样位数等基础可以参考Android FFmpeg系列02--音视频基础

重采样

音频重采样就是通过改变音频的采样率、采样格式、声道数等参数使之按照我们期望的音频参数输出音频数据的过程

为什么需要重采样?

因为音频文件的音频参数是多种多样的,而播放音频的设备不一定支持这些参数,这就需要通过重采样进行转换后才能正常播放;另外比如说我们需要对多段音频进行mix,需要首先确保每段音频具有相同的采样率、采样格式和声道数,这个时候也需要进行重采样

FFmpeg中的音频采样格式

FFmpeg中的音频采样格式分为两种,以P结尾的planar格式和不带P结尾的packed格式

代码语言:javascript复制
enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16, ///< signed 16 bits
    AV_SAMPLE_FMT_S32, ///< signed 32 bits
    AV_SAMPLE_FMT_FLT, ///< float
    AV_SAMPLE_FMT_DBL, ///< double

    AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
    AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
    AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
    AV_SAMPLE_FMT_FLTP, ///< float, planar
    AV_SAMPLE_FMT_DBLP, ///< double, planar
    AV_SAMPLE_FMT_S64, ///< signed 64 bits
    AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar

    AV_SAMPLE_FMT_NB           ///< Number of sample formats. DO NOT USE if linking dynamically
};

以双声道为例,planar格式在存储时,左右声道的数据分开存储,左声道在data[0],右声道数据在data[1],每个声道所占用的字节数为linesize[0]和linesize[1]

packed格式则按照LRLRLR...的格式交错存储在data[0]中,总的数据量为linesize[0]

eg:双声道的AV_SAMPLE_FMT_S16和AV_SAMPLE_FMT_FLTP

FFmpeg音频帧中的nb_samples字段

AVFrame中的nb_samples字段表示音频数据每个通道的采样数量,它与具体的码流类型和编码级别有关

nb_samples和AVCodecContext中的frame_size相同

音频帧的数据量计算

代码语言:javascript复制
// size = nb_samples * channels * bytes_per_sample
// 以双声道,AV_SAMPLE_FMT_S16格式(2字节)为例
// AAC(nb_samples = 1024),
size = 1024 * 2 * 2 = 4096字节

// MP3(nb_samples = 1152)
size = 1152 * 2 * 2 = 4608字节

音频帧的播放时间计算

代码语言:javascript复制
// duration = nb_samples / sample_rate
// 以采样率为44100HZ为例
// AAC(nb_samples = 1024),
duration = 1024 / 44100 = 0.02322s = 23.22ms

// MP3(nb_samples = 1152)
duration = 1152 / 44100 = 0.02612s = 26.12ms

AudioTrack

AudioTrack因为不创建解码器,所以只能用于PCM数据的播放或者播放wav文件,它提供两种播放模式

  • MODE_STATIC:预先将待播放的音频数据全部写入内存,然后进行播放
  • MODE_STREAM:边写入边播放

我们现在的场景是通过FFmpeg实时解码出音频PCM数据并播放,所以选择stream模式

创建AudioTrack

这里我们固定音频参数为双声道,采样率为44100HZ,采样格式为ENCODING_PCM_16BIT

代码语言:javascript复制
// 计算最小buffer size
val bufferSize = AudioTrack.getMinBufferSize(
    44100, 
    AudioFormat.CHANNEL_OUT_STEREO, 
    AudioFormat.ENCODING_PCM_16BIT
)
// 创建AudioTrack实例
mAudioTrack = AudioTrack(
    AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build(),
    AudioFormat.Builder().
        setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setSampleRate(44100)
        .build(),
    bufferSize, 
    AudioTrack.MODE_STREAM,
    AudioManager.AUDIO_SESSION_ID_GENERATE
)

播放

代码语言:javascript复制
mAudioTrack!!.play()

实时写入数据

代码语言:javascript复制
mAudioTrack?.write(audio, 0, size)

释放

代码语言:javascript复制
mAudioTrack?.stop()
mAudioTrack?.release()

FFmpeg音频解码&重采样

音频解码的步骤和视频解码步骤是类似的

解封装&找到音频流index -> 打开解码器 -> 循环解码&重采样 -> 解码结束释放相关资源

详细的解码代码就不贴了,可以查看源码中的AudioDecoder.cpp

这里重点说说重采样的过程

初始化重采样上下文

代码语言:javascript复制
mSwrContext = swr_alloc_set_opts(
    nullptr,
    AV_CH_LAYOUT_STEREO, // 双声道
    AV_SAMPLE_FMT_S16, // 对应到AudioTrack的AudioFormat.ENCODING_PCM_16BIT
    44100,

    mAvFrame->channel_layout, 
    AVSampleFormat(mAvFrame->format), // format = AV_SAMPLE_FMT_FLTP
    mAvFrame->sample_rate, // 48000HZ
    0,
    nullptr
);
swr_init(mSwrContext);

重采样

代码语言:javascript复制
// 重采样后的nb_samples
int out_nb = (int) av_rescale_rnd(mAvFrame->nb_samples, 44100, mAvFrame->sample_rate, AV_ROUND_UP);

// 重采样后的channels数
int out_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);

// 计算重采样后每帧的size
int size = av_samples_get_buffer_size(nullptr, out_channels, out_nb, AV_SAMPLE_FMT_S16, 1);

// 初始化重采样后的数据buffer
if (mAudioBuffer == nullptr) {
    mAudioBuffer = (uint8_t *) av_malloc(size); 
}

// 重采样
int nb = swr_convert(
    mSwrContext,
    &mAudioBuffer,
    size / out_channels, // 每个通道的samples size
    (const uint8_t**)mAvFrame->data,
    mAvFrame->nb_samples
);

mDataSize = nb * out_channels * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);

上面的mAudioBuffer和mDataSize就是透过JNI传给AudioTrack#write的数据,一些细节可以查看提供的Demo源码

https://github.com/sifutang/ffmpeg-demo

~~END~~

0 人点赞