【秒懂音視頻開發】08_音頻錄制02_編程


通過編程錄音

開發錄音功能的主要步驟是:

  • 注冊設備
  • 獲取輸入格式對象
  • 打開設備
  • 采集數據
  • 釋放資源

主要步驟

需要用到的FFmpeg庫有4個。

extern "C" {
// 設備相關API
#include <libavdevice/avdevice.h>
// 格式相關API
#include <libavformat/avformat.h>
// 工具相關API(比如錯誤處理)
#include <libavutil/avutil.h>
// 編碼相關API
#include <libavcodec/avcodec.h>
}

權限申請

在Mac平台,有2個注意點:

  • 需要在Info.plist中添加麥克風的使用說明,申請麥克風的使用權限
  • 使用Debug模式運行程序
  • 不然會出現閃退的情況
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>NSMicrophoneUsageDescription</key>
        <string>使用麥克風采集您的天籟之音</string>
</dict>
</plist>

注冊設備

在整個程序的運行過程中,只需要執行1次注冊設備的代碼。

// 初始化libavdevice並注冊所有輸入和輸出設備
avdevice_register_all();

獲取輸入格式對象

宏定義

Windows和Mac環境的格式名稱、設備名稱都是不同的,所以使用條件編譯實現跨平台。

// 格式名稱、設備名稱目前暫時使用宏定義固定死
#ifdef Q_OS_WIN
    // 格式名稱
    #define FMT_NAME "dshow"
    // 設備名稱
    #define DEVICE_NAME "audio=麥克風陣列 (Realtek(R) Audio)"
#else
    #define FMT_NAME "avfoundation"
    #define DEVICE_NAME ":0"
#endif

核心代碼

根據格式名稱獲取輸入格式對象,后面需要利用輸入格式對象打開設備。

AVInputFormat *fmt = av_find_input_format(FMT_NAME);
if (!fmt) {
    // 如果找不到輸入格式
    qDebug() << "找不到輸入格式" << FMT_NAME;
    return;
}

打開設備

// 格式上下文(后面通過格式上下文操作設備)
AVFormatContext *ctx = nullptr;
// 打開設備
int ret = avformat_open_input(&ctx, DEVICE_NAME, fmt, nullptr);
// 如果打開設備失敗
if (ret < 0) {
    char errbuf[1024] = {0};
    // 根據函數返回的錯誤碼獲取錯誤信息
    av_strerror(ret, errbuf, sizeof (errbuf));
    qDebug() << "打開設備失敗" << errbuf;
    return;
}

采集數據

宏定義

#ifdef Q_OS_WIN
    // PCM文件的文件名
    #define FILENAME "F:/out.pcm"
#else
    #define FILENAME "/Users/mj/Desktop/out.pcm"
#endif

核心代碼

#include <QFile>

// 文件
QFile file(FILENAME);

// WriteOnly:只寫模式。如果文件不存在,就創建文件;如果文件存在,就刪除文件內容
if (!file.open(QFile::WriteOnly)) {
    qDebug() << "文件打開失敗" << FILENAME;
    // 關閉設備
    avformat_close_input(&ctx);
    return;
}

// 暫時假定只采集50個數據包
int count = 50;

// 數據包
AVPacket *pkt = av_packet_alloc();
while (count-- > 0) {
    // 從設備中采集數據,返回值為0,代表采集數據成功
    ret = av_read_frame(ctx, pkt);

    if (ret == 0) { // 讀取成功
        // 將數據寫入文件
        file.write((const char *) pkt->data, pkt->size);
    
        // 釋放資源
        av_packet_unref(pkt);
    } else if (ret == AVERROR(EAGAIN)) { // 資源臨時不可用
        continue;
    } else { // 其他錯誤
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "av_read_frame error" << errbuf << ret;
        break;
    }
}

釋放資源

// 關閉文件
file.close();

// 釋放資源
av_packet_free(&pkt);

// 關閉設備
avformat_close_input(&ctx);

想要了解每一個函數的具體作用,可以查詢:官方API文檔

獲取錄音設備的相關參數

// 從AVFormatContext中獲取錄音設備的相關參數
void showSpec(AVFormatContext *ctx) {
    // 獲取輸入流
    AVStream *stream = ctx->streams[0];
    // 獲取音頻參數
    AVCodecParameters *params = stream->codecpar;
    // 聲道數
    qDebug() << params->channels;
    // 采樣率
    qDebug() << params->sample_rate;
    // 采樣格式
    qDebug() << params->format;
    // 每一個樣本的一個聲道占用多少個字節
    qDebug() << av_get_bytes_per_sample((AVSampleFormat) params->format);
    // 編碼ID(可以看出采樣格式)
    qDebug() << params->codec_id;
    // 每一個樣本的一個聲道占用多少位(這個函數需要用到avcodec庫)
    qDebug() << av_get_bits_per_sample(params->codec_id);
}

多線程

錄音屬於耗時操作,為了避免阻塞主線程,最好在子線程中進行錄音操作。這里創建了一個繼承自QThread的線程類,線程一旦啟動(start),就會自動調用run函數。

.h

#include <QThread>
 
class AudioThread : public QThread {
    Q_OBJECT
private:
    void run();
 
public:
    explicit AudioThread(QObject *parent = nullptr);
    ~AudioThread();
};

.cpp

AudioThread::AudioThread(QObject *parent,
                         AVInputFormat *fmt,
                         const char *deviceName)
    : QThread(parent), _fmt(fmt), _deviceName(deviceName) {
    // 在線程結束時自動回收線程的內存
    connect(this, &AudioThread::finished,
            this, &AudioThread::deleteLater);
}

AudioThread::~AudioThread() {
    // 線程對象的內存回收時,正常結束線程
    requestInterruption();
    quit();
    wait();
}

void AudioThread::run() {
    // 錄音操作
    // ...
}

開啟線程

AudioThread *audioThread = new AudioThread(this);
audioThread->start();

結束線程

// 外部調用線程的requestInterruption,請求結束線程
audioThread->requestInterruption();

// 線程內部的邏輯
void AudioThread::run() {
    // 可以通過isInterruptionRequested判斷是否要結束線程
    // 當調用過線程的requestInterruption時,isInterruptionRequested返回值就為true,否則為false
    while (!isInterruptionRequested()) {
    	// ...
    }
}

改造錄音代碼

// 數據包
AVPacket *pkt = av_packet_alloc();
while (!isInterruptionRequested()) {
    // 從設備中采集數據,返回值為0,代表采集數據成功
    ret = av_read_frame(ctx, pkt);

    if (ret == 0) { // 讀取成功
        // 將數據寫入文件
        file.write((const char *) pkt->data, pkt->size);
    
        // 釋放資源
        av_packet_unref(pkt);
    } else if (ret == AVERROR(EAGAIN)) { // 資源臨時不可用
        continue;
    } else { // 其他錯誤
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "av_read_frame error" << errbuf << ret;
        break;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM