ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FFmpeg + qt 音频播放

FFmpeg + qt 音频播放

1. 读取 文件名

avformat_open_input(&fmt_ctx, filePath.toStdString().c_str(),nullptr, nullptr);

2. 读取完整文件流信息

avformat_find_stream_info(fmt_ctx,nullptr);

3. 获取音频流索引

int audioStreamIndex = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, 0 ,nullptr);

4. 获取音频流结构体

AVStream m_audioStream = fmt_ctx->streams[audioStreamIndex];

5. 获取解码器静态指针

const AVCodec* codec = avcodec_find_decoder(fmt_ctx->codecper->codec_id);

6. 获取解码器上下文

decoder_ctx = avcodec_alloc_context3(codec);

7. 复制解码参数

avcodec_parameters_to_context(decoder_ctx, m_audioStream->codecper);

8. 打开解码器

avcodec_open2(decoder_ctx,codec, nullptr);

9. 创建音频帧

AVFrame m_audioFrame = av_frame_alloc();

10. 创建并初始化音频转换器

SwrContext *m_swrContext;

AVChannelLayout *ch_layout;

//int av_channel_layout_from_mask(AVChannelLayout *ch_layout, uint64_t mask);

av_channel_layout_form_mask(ch_layout, AV_CH_LAYOUT_STEREO);

swr_alloc_set_opts2(&m_swrContext

ch_layout, AV_SAMPLE_S16,44100,

&decoder_ctx->ch_layout,

deocder_ctx->sample_fmt,

decoder_ctx->sample_rate,

0, nullptr //日志偏移量 自定义日志上下文

);

swr_init(m_swrContext);

11. 创建音频缓冲区(根据输入的采样率和样本数, 获取输出的样本数以及可能积压的延迟样本数计算做大输出样本数,根据最大输出样本数和采样率计算缓冲区大小,申请缓冲区大小的内存)

int inRate = decoder_ctx->sample_rate;

int inSamples = decoder_ctx->frame_size > 0 ? decoder_ctx->frame_size : 1024;

int outSamples = av_rescle_rnd(inSamples, 44100, inRate, AV_ROUND) *2;

// 按两秒估算

int delaySample = av_rescale_rnd(2 * inRate, 44100, inRate, AV_ROUND);

int maxOutSamples = outSamples + delaySample;

int bufferSize = av_sample_get_buffer_size(nullptr, 2, maxOutSamples, AV_SAMPLE_S16, 0);

uint8_t *m_audioBuffer = (uint8_t*)av_malloc(bufferSize);

12. 创建压缩包

AVPacket* m_packet = av_packet_alloc();

13. 循环解码 (1. 从fmt_ctx读取完整数据包 2. 将数据包发送到解码器 3. 从解码器中读取解码后的音频帧 4. 计算需要输出的样本数,包括swr内延迟的 5. 重采样 6. 将数据发送到主线程)

while(!isInterruptionRequested()){

av_read_frame(fmt_ctx, m_packet);

avcodec_send_packet(decoder_ctx, m_packet);

while(avcodec_receive_frame(decoder_ctx, m_audioFrame) >= 0){

int delay = swr_get_delay(m_swrContext, decoder_ctx->sample_rate);

int outSample = av_rescale_rnd(delay + m_audioFrame->nb_samples, 44100, m_audioFrame->sample_rate, AV_ROUND_UP);

int conver = swr_conver(m_swrContext, &m_audioBuffer, outSample,

(const uint8_t **)m_audioFrame->data, m_audioFrame->nb_samples

);

if(conver > 0){

int outSize = conver * 2 * sizeof(int16_t);

QByteArray pcm((const char*)m_audioBuffer, outSize);

emit sendPcm(pcm);

}

av_frame_unref(m_audioFrame);

}

av_packet_unref(&m_packet)

}

14. 读取解码器中残留的帧(13上述中的 3-6)

int ret = 0;

while(ret >= 0){

ret = av_read_frame(decoder_ctx, m_audioFrame);

if(ret == AVERROR_EOF || ret == AVERROR(EAGAIN))

break;

int delay = swr_get_delay(m_swrContext, m_audioFrame->nb_samples);

int outSamples = av_rescale_rnd(delay + m_audioFrame->nb_samples, 44100, m_audioFrame->sample_rate, AV_ROUND_UP);

int conver = swr_conver(&m_swrContext,

m_audioBuffer, outSamples,

(const uint8_t**)m_audioFrame->data,

m_audioFrame->nb_samples

);

if(conver > 0){

int outSize = conver *2 * sizeof(int16_t);

QByteArray pcm((const char*)m_audioBuffer, outSize);

emit sendPcm(pcm);

}

av_frame_unref(m_audioFrame);

}

av_packet_unref(&m_packet);

15. 释放

void release(){

if(m_swrContext){

swr_free(&m_swrContext);

m_swrContext = nullptr;

}

if(m_audioFrame){

av_frame_free(&m_audioFrame);

m_audioFrame = nullptr;

}

if(m_audioBuffer){

av_freep(&m_audioBuffer);

m_audioBuffer = nullptr;

}

if(decoder_ctx){

avcodec_free_context(&decoder_ctx);

decoder_ctx = nullptr;

}

if(fmt_ctx){

avformat_free_context(&fmt_ctx);

fmt_ctx = nullptr;

}

}

主线程

1. 设置音频格式

QAudioFormat m_audioFormat;

m_audioFormat.setSampleRate(44100);
m_audioFormat.setChannelCount(2);

m_audioFormat.setSampleSize(16);

m_audioFormat.setSampleType(QAudioSample::SignedInt);

m_audioFormat.setByteOrder(QAudioSample::LittleEndian);

2. 设置音频播放设备

QAudioDeviceInfo m_audioDeviceInfo = QAudioDeviceInfo::defaultOutputDevice();

3. 检查设备是否支持,不支持就用最接近的格式

if(!m_audioDeviceInfo.isFormatSupported(m_audioFormat)){

qDebug() << "格式不支持,使用最接近的格式";

m_audioFormat = m_audioDeviceInfo.nearestFormat(m_audioForamt);

qDeubg() << "实际支持格式:" << m_audioFormat.sampleRate() << "Hz"

<< m_audioFormat.sampleSize() << "bit" << m_audioFormat_channelCount() << "ch";

}

4. 创建并设置输出格式,缓冲区大小 创建并初始化设备IO

QAudioOutput *m_audioOutput = new QAudioOutput(m_audioFormat, this);

// 44100Hz * 2ch * 2(字节) * 0.2s

m_audioOutput->setBufferSize(static_cast<int> 44100 *2 *2 *0.2);

QIODevice m_audioIODevice = m_audioOutput->start();

5. 接收pcm 并加入输出设备

connect(m_audioPlay, &AudioPlay, this, &MainWindow::playAudio);

m_audioPlay->start();

void playAudio(QByteArray pcm){

if(pcm.isEmpty())

return;

QMutexLocker locker(&m_pcmMutex);

m_pcmQueue.append(pcm);

}

6. 定时器:每10ms从队列取数据写入设备

m_feedTimer = new QTimer(this);

connect(m_feedTimer, &QTimer::timeout, this, &MainWindow::feedAudio);

m_feedTimer->start(10);

void feedAudio(){

if(!m_audioIODevice || !m_audioOutput)

return;

QMutextLocker locker(&m_pcmMutext);

while(!m_pcmQueue.empty()){

// 获取音频设备缓冲区当前剩余可写入字节数

int free = m_audioOutput.bytesFree();

if(free < 0) break;

// 获取 剩余字节数 和 缓冲队列剩余字节数 中的最小值

int minBytes = qMin(free, m_pcmQueue.size());

int written = m_audioIODevice->write(m_pcmQueue.constData, minBytes);

if(written > 0)

// 将写入的字节从缓冲队列移除

m_pcmQueue.remove(0, minBytes);

else

break;

}

}

返回列表