【Qt5】使用QAudioOutput播放ffmpeg解码出的音频

时间:2023-02-01 19:43:25

使用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 ...
这样的数据不能直接播放,必须按照之前那样按照采样点顺序排列才能播放,所以在播放之前,必须先转换为非planar形式。

QAudioOutput的音频格式QAudioFormat

要初始化一个QAudioOutput,必须先设置好QAudioFormat,需要以下几个属性:
setSampleRate(int);        // 设置采样率
setChannelCount(int);    // 设置声道数
setSampleSize(int);         // 设置采样数据大小(bit位数)
setSampleType(QAudioFormat::SampleType);    // 设置采样数据格式
setCodec(QString);         // 设置解码器类型,裸流就设置为 "audio/pcm"
其中QAudioFormat::SampleType采样数据格式有下面几种:
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;