写在前面,不推荐用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位无符号整形数据。只要能明确知道这样排列的一段数据的声道数、采样率、采样数据类型,就可以播放这段音频数据。
解码上下文中,可以获取音频的数据格式
在结构体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 | ... |
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中刚刚解码出的数据因为排列方式的原因,不能直接播放,必须要转换,首先根据音频解码上下文设置并初始化转换上下文:
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);
先设置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;