使用QAudioOutput播放ffmpeg解码出的音频
写在前面,不推荐用QAudioOutput播放媒体音频,因为不够强大,难以控制。推荐使用SDL。
音频数据格式
要想播放一段音频裸流,除了需要数据本身以外,还需要规定这段数据的格式才能正确播放。其中声道数、采样率、采样数据类型是最基本的格式内容。例如,一段声道数为2,采样率为48000Hz,数据类型为8位无符号整形的音频裸流,储存方式为:
声道0的采样点0 | 声道1的采样点0 | 声道0的采样点1 | 声道1的采样点1 | 声道0的采样点2 | 声道1的采样点2 | 以此类推... |
上面,每个格子为1个声道的采样点,48000Hz表示1秒钟一个声道有48000个采样点,2声道则共有2 * 48000 = 96000个采样点,每个采样点为一个8位无符号整形数据。只要能明确知道这样排列的一段数据的声道数、采样率、采样数据类型,就可以播放这段音频数据。
ffmpeg中的音频数据格式
解码上下文中,可以获取音频的数据格式
在结构体AVCodecContext中:
/* audio only */ int sample_rate; ///< samples per second int channels; ///< number of audio channels /** * audio sample format * - encoding: Set by user. * - decoding: Set by libavcodec. */ enum AVSampleFormat sample_fmt; ///< sample format
分别为采样率,声道数,采样数据格式。
其中,数据格式AVSampleFormat如下:
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 };
可以看到,很多相同类型的数据格式有两种,比如AV_SAMPLE_FMT_U8和AV_SAMPLE_FMT_U8P,都是8位无符号整形,但是后者多了一个后缀P,注释中说明planar,这是什么意思呢?
这其实是另一种数据的结构,一般ffmpeg刚刚解码出来的数据并不是像我们之前看到的那样排列的,而是按各个声道分组排列的:
声道0的采样点0 | 声道0的采样点1 | 声道0的采样点2 | ... | 声道1的采样点0 | 声道1的采样点1 | 声道1的采样点2 | ... |
QAudioOutput的音频格式QAudioFormat
setChannelCount(int); // 设置声道数
setSampleSize(int); // 设置采样数据大小(bit位数)
setSampleType(QAudioFormat::SampleType); // 设置采样数据格式
setCodec(QString); // 设置解码器类型,裸流就设置为 "audio/pcm"
enum SampleType { Unknown, SignedInt, UnSignedInt, Float };
有用的就是SignedInt有符号整形、UnSignedInt无符号整形、Float浮点型,可以看到,实际上sampleSize和sampleType两个合起来决定了采样数据类型,比如setSampleSize(8)并且setSampleType(UnSignedInt)就对应于ffmpeg中的AV_SAMPLE_FMT_U8.
ffmpeg中的函数av_get_bytes_per_sample可以获得AVSampleFormat对应的采样数据类型的字节数,这个结果乘以8就可以得到QAudioFormat的sampleSize参数了。
FFmpeg中的音频格式转换
前面说了,ffmpeg中刚刚解码出的数据因为排列方式的原因,不能直接播放,必须要转换,首先根据音频解码上下文设置并初始化转换上下文:
swrCtx = swr_alloc_set_opts(nullptr, audioCodecCtx->channel_layout, AV_SAMPLE_FMT_S16, audioCodecCtx->sample_rate, audioCodecCtx->channel_layout, audioCodecCtx->sample_fmt, audioCodecCtx->sample_rate, 0, nullptr); swr_init(swrCtx);
在解码得到一帧音频后,先转换后计算所需要的内存大小,然后分配内存并进行格式转换:
int bufsize = av_samples_get_buffer_size(nullptr, frame->channels, frame->nb_samples, AV_SAMPLE_FMT_S16, 0); uint8_t *buf = new uint8_t[bufsize]; swr_convert(swrCtx, &buf, frame->nb_samples, (const uint8_t**)(frame->data), frame->nb_samples);
这样得到的buf中的音频数据就可以用于播放了,别忘了使用后要 delete[] buf 哦。
最后解码完成后,要记得释放掉转换上下文:
swr_free(&swrCtx);
QAudioOutput播放音频
先设置QAudioFormat,然后初始化QAudioOutput,并打开音频设备:
QAudioFormat audioFormat; audioFormat.setSampleRate(audioCodecCtx->sample_rate); audioFormat.setChannelCount(audioCodecCtx->channels); audioFormat.setSampleSize(8*av_get_bytes_per_sample(AV_SAMPLE_FMT_S16)); audioFormat.setSampleType(QAudioFormat::SignedInt); audioFormat.setCodec("audio/pcm"); QAudioOutput audioOutput = new QAudioOutput(audioFormat); QIODevice *audioDevice = audioOutput->start();
这样,就可以通过QIODevice::write()方法,写入音频数据,进行播放了。用下面代码播放上节中转换出来的数据:
audioDevice->write((const char*)buf, bufsize); delete[] buf;
播放停止后,别忘了停止并释放QAudioOutput:
audioOutput->stop(); delete audioOutput;