六、ffmpeg录制音频为wav文件

前言


测试环境:

  • ffmpeg的shared版本
  • windows环境
  • qt5.12

ffmpeg录制音频为wav文件,思路和录制成pcm相同,关键思路是在打开文件之初,先加上wav文件头,后续再写入音频数据的二进制信息即可

链接:一、ffmpeg录制音频为pcm文件


完整代码:(我这里是将功能封装在线程里,其中还考虑了线程的关闭问题)

AudioRecordWavThread.h

#ifndef AUDIORECORDWAVTHREAD_H
#define AUDIORECORDWAVTHREAD_H

#include <QThread>
#include "pcmtowavthread.h"

class AudioRecordWavThread : public QThread {
    
    
    Q_OBJECT
private:
    void run();
    bool _stop = false;

public:
    explicit AudioRecordWavThread(QObject *parent = nullptr);
    ~AudioRecordWavThread();
    void setStop(bool stop);
signals:
    void timeChanged(unsigned long long ms);
};

#endif // AUDIORECORDWAVTHREAD_H

.h文件中包含pcmtowavthread.h是为了使用其内部的WAVHeader结构体

WAVHeader结构体如下

#define AUDIO_FORMAT_PCM 1
#define AUDIO_FORMAT_FLOAT 3

// WAV文件头(44字节)
typedef struct {
    
    
    // RIFF chunk的id
    uint8_t riffChunkId[4] = {
    
    'R', 'I', 'F', 'F'};
    // RIFF chunk的data大小,即文件总长度减去8字节
    uint32_t riffChunkDataSize;

    // "WAVE"
    uint8_t format[4] = {
    
    'W', 'A', 'V', 'E'};

    /* fmt chunk */
    // fmt chunk的id
    uint8_t fmtChunkId[4] = {
    
    'f', 'm', 't', ' '};
    // fmt chunk的data大小:存储PCM数据时,是16
    uint32_t fmtChunkDataSize = 16;
    // 音频编码,1表示PCM,3表示Floating Point
    uint16_t audioFormat = AUDIO_FORMAT_PCM;
    // 声道数
    uint16_t numChannels;
    // 采样率
    uint32_t sampleRate;
    // 字节率 = sampleRate * blockAlign
    uint32_t byteRate;
    // 一个样本的字节数 = bitsPerSample * numChannels >> 3
    uint16_t blockAlign;
    // 位深度
    uint16_t bitsPerSample;

    /* data chunk */
    // data chunk的id
    uint8_t dataChunkId[4] = {
    
    'd', 'a', 't', 'a'};
    // data chunk的data大小:音频数据的总长度,即文件总长度减去文件头的长度(一般是44)
    uint32_t dataChunkDataSize;
} WAVHeader;

AudioRecordWavThread.cpp

#include "audiorecordwavthread.h"

#include <QDebug>
#include <QFile>
#include <QDateTime>

extern "C" {
    
    
// 设备
#include <libavdevice/avdevice.h>
// 格式
#include <libavformat/avformat.h>
// 工具(比如错误处理)
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
}

#ifdef Q_OS_WIN
    // 格式名称
    #define FMT_NAME "dshow"
    // 设备名称
    #define DEVICE_NAME "audio=耳机 (Baseus Bowie E8 Hands-Free AG Audio)"
    // PCM文件名
    #define FILEPATH "E:/media/"
#else
    #define FMT_NAME "avfoundation"
    #define DEVICE_NAME ":0"
    #define FILEPATH "/Users/mj/Desktop/"
#endif

AudioRecordWavThread::AudioRecordWavThread(QObject *parent) : QThread(parent) {
    
    
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AudioRecordWavThread::finished,
            this, &AudioRecordWavThread::deleteLater);
}

AudioRecordWavThread::~AudioRecordWavThread() {
    
    
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";
}

// 当线程启动的时候(start),就会自动调用run函数
// run函数中的代码是在子线程中执行的
// 耗时操作应该放在run函数中
void AudioRecordWavThread::run() {
    
    
    qDebug() << this << "开始执行----------";

    // 获取输入格式对象
    const 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];
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "打开设备失败" << errbuf;
        return;
    }

    // 打印一下录音设备的参数信息
    // showSpec(ctx);

    // 文件名
    QString filename = FILEPATH;
    filename += QDateTime::currentDateTime().toString("MM_dd_HH_mm_ss");
    filename += ".wav";
    QFile file(filename);

    // 打开文件
    if (!file.open(QFile::WriteOnly)) {
    
    
        qDebug() << "文件打开失败" << filename;

        // 关闭设备
        avformat_close_input(&ctx);
        return;
    }

    // 获取输入流
    AVStream *stream = ctx->streams[0];
    // 获取音频参数
    AVCodecParameters *params = stream->codecpar;

    // 写入WAV文件头
    WAVHeader header;
    header.sampleRate = params->sample_rate;
    // 2
    header.bitsPerSample = av_get_bits_per_sample(params->codec_id);
    header.numChannels = params->channels;
    if (params->codec_id >= AV_CODEC_ID_PCM_F32BE) {
    
    
        header.audioFormat = AUDIO_FORMAT_FLOAT;
    }
    header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
    header.byteRate = header.sampleRate * header.blockAlign;
//    header.dataChunkDataSize = 0;
    file.write((char *) &header, sizeof (WAVHeader));

    // 数据包
    AVPacket *pkt = av_packet_alloc();
    while (!isInterruptionRequested()) {
    
    
        // 不断采集数据
        ret = av_read_frame(ctx, pkt);

        if (ret == 0) {
    
     // 读取成功
            // 将数据写入文件
            file.write((const char *) pkt->data, pkt->size);

            // 计算录音时长
            header.dataChunkDataSize += pkt->size;
            unsigned long long ms = 1000.0 * header.dataChunkDataSize / header.byteRate;
            emit timeChanged(ms);

            // 释放资源
            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;
        }
    }

//    qDebug() << file.size() << header.dataChunkDataSize;

//    int size = file.size();

    // 写入dataChunkDataSize
//    header.dataChunkDataSize = size - sizeof (WAVHeader);
    file.seek(sizeof (WAVHeader) - sizeof (header.dataChunkDataSize));
    file.write((char *) &header.dataChunkDataSize, sizeof (header.dataChunkDataSize));

    // 写入riffChunkDataSize
    header.riffChunkDataSize = file.size()
                               - sizeof (header.riffChunkId)
                               - sizeof (header.riffChunkDataSize);
    file.seek(sizeof (header.riffChunkId));
    file.write((char *) &header.riffChunkDataSize, sizeof (header.riffChunkDataSize));

    // 释放资源
    av_packet_free(&pkt);

    // 关闭文件
    file.close();

    // 关闭设备
    avformat_close_input(&ctx);

    qDebug() << this << "正常结束----------";
}

void AudioRecordWavThread::setStop(bool stop) {
    
    
    _stop = stop;
}

注意:该代码对可能出现的问题都进行了一定的优化,读者使用时只需关注关键代码


线程调用

void MainWindow::on_pushButton_record_to_wav_clicked()
{
    
    
    if (!audioRecordWavThread) {
    
     // 点击了“开始录音”
        // 开启线程
        audioRecordWavThread = new AudioRecordWavThread(this);
        audioRecordWavThread->start();

        connect(audioRecordWavThread, &AudioRecordWavThread::timeChanged,
                this, &MainWindow::onTimeChanged);

        connect(audioRecordWavThread, &AudioThread::finished,
        [this]() {
    
     // 线程结束
            audioRecordWavThread = nullptr;
            ui->pushButton_record_to_wav->setText("音频录制为wav");
        });

        // 设置按钮文字
        ui->pushButton_record_to_wav->setText("结束录音");
    } else {
    
     // 点击了“结束录音”
        // 结束线程
//        _audioThread->setStop(true);
        audioRecordWavThread->requestInterruption();
        audioRecordWavThread = nullptr;

        // 设置按钮文字
        ui->pushButton_record_to_wav->setText("音频录制为wav");
    }
}

其中槽函数

void onTimeChanged(unsigned long long ms);

的具体实现为

void MainWindow::onTimeChanged(unsigned long long ms) {
    
    
    QTime time(0, 0, 0, 0);
    QString text = time.addMSecs(ms).toString("mm:ss.zz");
    ui->label_record_wav_time->setText(text.left(5));
}

在主函数的构造方法中还要初始化一下时间

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    
    
    ui->setupUi(this);

    // 初始化libavdevice并注册所有输入和输出设备
    avdevice_register_all();

    // 初始化时间
    onTimeChanged(0);
}

注意:.h文件中提前声明了以下全局变量

	AudioRecordWavThread *audioRecordWavThread = nullptr;

注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用


码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方

猜你喜欢

转载自blog.csdn.net/weixin_44092851/article/details/128317786