基於FFmpeg的視訊播放器開發系列教程(三)
本篇開始講解音訊解碼播放,該專案用Qt的音訊類QAudioFormat, QAudioOutput等進行解碼,先講解一些關於音訊的知識。
1.取樣頻率
指每秒鐘取得聲音樣本的次數。取樣的過程就是抽取某點的頻率值,很顯然,在一秒中內抽取的點越多,獲取得頻率資訊更豐富,為了復原波形,取樣頻率越高,聲音的質量也就越好,聲音的還原也就越真實,但同時它佔的資源比較多。由於人耳的解析度很有限,太高的頻率並不能分辨出來。22050 的取樣頻率是常用的,44100已是CD音質,超過48000或96000的取樣對人耳已經沒有意義。這和電影的每秒24幀圖片的道理差不多。如果是雙聲道(stereo)
在數字音訊領域,常用的取樣率有:
32000 Hz - miniDV 數碼視訊 camcorder、DAT (LP mode)所用取樣率
44100 Hz - 音訊 CD, 也常用於 MPEG-1 音訊(VCD,SVCD,MP3)所用取樣率
47250 Hz - 商用 PCM 錄音機所用取樣率
48000 Hz - miniDV、數字電視、DVD、DAT、電影和專業音訊所用的數字聲音所用取樣率
50000 Hz - 商用數字錄音機所用取樣率
96000 Hz或者 192000 Hz - DVD-Audio、一些 LPCM DVD 音軌、BD-ROM(藍光碟)音軌、和 HD-DVD
2.通道數
即聲音的通道的數目。常見的單聲道和立體聲(雙聲道),現在發展到了四聲環繞(四聲道)和5.1聲道。
3.取樣位數
取樣位數也叫取樣大小或量化位數。它是用來衡量聲音波動變化的一個引數,也就是音效卡的解析度或可以理解為音效卡處理聲音的解析度。它的數值越大,解析度也就越高,錄製和回放的聲音就越真實。而音效卡的位是指音效卡在採集和播放聲音檔案時所使用數字聲音訊號的二進位制位數,音效卡的位客觀地反映了數字聲音訊號對輸入聲音訊號描述的準確程度。常見的音效卡主要有8位和16位兩種,如今市面上所有的主流產品都是16位及以上的音效卡。
每個取樣資料記錄的是振幅, 取樣精度取決於取樣位數的大小:
1 位元組(也就是8bit) 只能記錄 256 個數, 也就是隻能將振幅劃分成 256 個等級;
2 位元組(也就是16bit) 可以細到 65536 個數, 這已是 CD 標準了;
4 位元組(也就是32bit) 能把振幅細分到 4294967296 個等級, 實在是沒必要了.
需要本篇部落格的原始碼,請【點選下載】 。
從Qt助手可以查詢類QAudioFormat 的一些資訊。
The QAudioFormat class stores audio stream parameter information.
The following table describes these in more detail.
封裝一個關於音訊的類MyAudio,標頭檔案如下
MyAudio.h
#pragma once
#include <QtMultimedia/QAudioOutput>
class MyAudio
{
public:
static MyAudio *GetObj()
{
static MyAudio mau;
return &mau;
}
~MyAudio();
void Stop();
bool Start();
void Play(bool isPlay);
bool Write(const char *data, int datasize);
int GetFree();
int sampleRate = 48000;
int sampleSize = 16;
int channel = 2;
public:
QAudioOutput *output = NULL;
QIODevice *io = NULL;
QMutex mutex;
protected:
MyAudio();
};
MyAudio.cpp
#include "MyAudio.h"
MyAudio::MyAudio()
{
}
MyAudio::~MyAudio()
{
}
void MyAudio::Stop()
{
mutex.lock();
if (output)
{
output->stop();
delete output;
output = NULL;
io = NULL;
}
mutex.unlock();
}
bool MyAudio::Start()
{
Stop();
mutex.lock();
QAudioFormat fmt; //Qt音訊的格式
fmt.setSampleRate(this->sampleRate); //1秒採集48000個聲音
fmt.setSampleSize(this->sampleSize); //16位
fmt.setChannelCount(this->channel); //聲道2雙聲道
fmt.setCodec("audio/pcm"); //音訊的格式
fmt.setByteOrder(QAudioFormat::LittleEndian); //次序
fmt.setSampleType(QAudioFormat::UnSignedInt); //樣本的類別
output = new QAudioOutput(fmt);
io = output->start();
mutex.unlock();
return true;
}
void MyAudio::Play(bool isPlay)
{
mutex.lock();
if (!output)
{
mutex.unlock();
return;
}
if (isPlay)
{
output->resume();
}
else
{
output->suspend();
}
mutex.unlock();
}
bool MyAudio::Write(const char *data, int datasize)
{
if (!data || datasize <= 0)
return false;
mutex.lock();
if (io)
{
mutex.unlock();
io->write(data, datasize);
return true;
}
mutex.unlock();
}
int MyAudio::GetFree()
{
mutex.lock();
if (!output)
{
mutex.unlock();
return 0;
}
int free = output->bytesFree();
mutex.unlock();
return free;
}
程式碼分析
先設定音訊的一些引數,根據以上音訊的知識,設定取樣頻率,通道數等
int sampleRate = 48000;
int sampleSize = 16;
int channel = 2;
通過QAudioOutput類播放音訊。
當然,在解碼時也要,音訊是視訊都要進行解碼,這樣在播放時才能影象和聲音同步,那麼上一篇的解碼函式DecodeFrame需要作修改。
void MyFFmpeg::DecodeFrame(const AVPacket *pkt)
{
mtx.lock();
if (!m_afc)
{
mtx.unlock();
return;
}
if (m_yuv == NULL)
{
m_yuv = av_frame_alloc();
}
if (m_pcm == NULL)
{
m_pcm = av_frame_alloc();
}
AVFrame *frame = m_yuv; //解碼後m_yuv會改變
if (pkt->stream_index == m_audioStream)
{
frame = m_pcm;
}
//根據索引 stream_index 來判斷是音訊還是視訊
int re = avcodec_send_packet(m_afc->streams[pkt->stream_index]->codec, pkt);
if (re != 0)
{
mtx.unlock();
return;
}
re = avcodec_receive_frame(m_afc->streams[pkt->stream_index]->codec, frame);
if (re != 0)
{
//失敗
mtx.unlock();
return;
}
mtx.unlock();
}
通過ffmpeg解碼得到音訊幀後,需要對音訊幀進行解碼,這個解碼動作線上程run()方法中進行,音訊解碼如下:
int MyFFmpeg::ToPCM(char *out)
{
mtx.lock();
if (!m_afc || !m_pcm || !out)
{
mtx.unlock();
return 0;
}
AVCodecContext *ctx = m_afc->streams[m_audioStream]->codec;
if (m_aCtx == NULL)
{
m_aCtx = swr_alloc();
swr_alloc_set_opts(m_aCtx, ctx->channel_layout,
AV_SAMPLE_FMT_S16,
ctx->sample_rate,
ctx->channels,
ctx->sample_fmt,
ctx->sample_rate,
0, 0);
swr_init(m_aCtx);
}
uint8_t *data[1];
data[0] = (uint8_t *)out;
int len = swr_convert(m_aCtx, data, 10000, (const uint8_t **)m_pcm->data, m_pcm->nb_samples);
if (len <= 0)
{
mtx.unlock();
return 0;
}
int outsize = av_samples_get_buffer_size(NULL, ctx->channels, m_pcm->nb_samples, AV_SAMPLE_FMT_S16, 0);
mtx.unlock();
return outsize;
}
解碼之後需要通過電腦的音效卡等裝置進行播放,本專案是用Qt開發的,那麼就需要使用Qt輸入輸出裝置的類QIODevice,需要呼叫以下函式,將聲音寫入裝置,進行播放。
qint64 write(const char *data);
檢視Qt助手的解釋:Writes data from a zero-terminated string of 8-bit characters to the device. //將資料從零終止的8位字元字串寫入裝置。上面的類MyAudio中的方法
bool MyAudio::Write(const char *data, int datasize)
既是播放聲音。
那麼如何實現視訊暫停呢,這個更簡單,因為我們的解碼是在子執行緒中進行的,只要停止執行子執行緒即可。解碼執行緒截圖如下;
只要控制m_isPlay的真假,即可實現執行緒阻塞,視訊暫停。程式碼如下:
void FFVideoPlyer::slotPlay()
{
if (ui.btn_Play->text() == QString::fromLocal8Bit("暫停"))
{
MyFFmpeg::GetObj()->m_isPlay = false; //執行緒阻塞,視訊暫停
ui.btn_Play->setText(QString::fromLocal8Bit("播放"));
}
else
{
MyFFmpeg::GetObj()->m_isPlay = true; //執行緒執行,視訊播放
ui.btn_Play->setText(QString::fromLocal8Bit("暫停"));
}
}
經過教程(二),(三)我們已經實現了音視訊同步,視訊暫停。那麼如何實現實現拖拽呢,這裡又是一大挑戰,通過Qt的Slider控制元件,然後重寫相關槽函式,但是這涉及到音視訊的一些知識,從下章開始,我們來介紹如何實現拖拽的功能。
過多的解析不說了,本篇部落格的原始碼,請開啟連結:專案原始碼,自行學習。