引言
在前面的连载系列中,我们分别用FFmpeg的软解和硬解两种方式解码了本地mp4文件的视频流并使用OpenGL渲染上屏
Android FFmpeg系列03--视频解码与渲染
Android FFmpeg系列04--FFmpeg调用MediaCodec进行硬解码
本篇文章将通过音频基础、AudioTrack、FFmpeg音频解码&重采样三个部分的讲解来完成对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~~