美文网首页
代码操控音频

代码操控音频

作者: coder_feng | 来源:发表于2021-05-06 16:34 被阅读0次

    上一篇文章已经展示了如何通过命令行来录制音频,这篇文章,我们尝试通过代码来实践一下

    1. 步骤流程图

    Snip20210425_7.png
    1.1 注册设备
    • main.cpp
    #include "mainwindow.h"
    
    #include <QApplication>
    
    extern "C" {
    // 设备
    #include <libavdevice/avdevice.h>
    }
    
    int main(int argc, char *argv[])
    {
        //注册设备
        avdevice_register_all();
        QApplication a(argc, argv);
        MainWindow w;
        w.show();
        return a.exec();
    }
    
    
    1.2 获取输入格式对象
    1.2.1 定义相关变量
    #define FMT_NAME "avfoundation"
    #define DEVICE_NAME ":0"
    #define FILEPATH "/Users/songlin/audio/qt_record/"
    
    核心代码
     // 获取输入格式对象
        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 += ".pcm";
        QFile file(filename);
    
        // 打开文件
        // WriteOnly:只写模式。如果文件不存在,就创建文件;如果文件存在,就会清空文件内容
        if (!file.open(QFile::WriteOnly)) {
            qDebug() << "文件打开失败" << filename;
    
            // 关闭设备
            avformat_close_input(&ctx);
            return;
        }
    
        // 数据包
    //    AVPacket pkt;
        AVPacket *pkt = av_packet_alloc();
        while (!isInterruptionRequested()) {
            // 不断采集数据
    //        ret = av_read_frame(ctx, &pkt);
            ret = av_read_frame(ctx, pkt);
    
            if (ret == 0) { // 读取成功
                // 将数据写入文件
    //            file.write((const char *) pkt.data, pkt.size);
    
                file.write((const char *) pkt->data, pkt->size);
            } else if (ret == AVERROR(EAGAIN)) { // 资源临时不可用
                continue;
            } else { // 其他错误
                char errbuf[1024];
                av_strerror(ret, errbuf, sizeof (errbuf));
                qDebug() << "av_read_frame error" << errbuf << ret;
                break;
            }
    
            // 必须要加,释放pkt内部的资源
    //        av_packet_unref(&pkt);
            av_packet_unref(pkt);
        }
    //    while (!_stop && av_read_frame(ctx, &pkt) == 0) {
    //        // 将数据写入文件
    //        file.write((const char *) pkt.data, pkt.size);
    //    }
    
        // 释放资源
        // 关闭文件
        file.close();
    
        // 释放资源
        av_packet_free(&pkt);
    
        // 关闭设备
        avformat_close_input(&ctx);
    
    1.2.2 获取录音设备的相关参数
    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);
    }
    
    

    2 多线程操作

    因为录音属于一个耗时操作,为了避免主线程拥塞,录音在子线程中操作效果会更佳

    2.1 audiothread.h
    #ifndef AUDIOTHREAD_H
    #define AUDIOTHREAD_H
    
    #include <QThread>
    
    class AudioThread : public QThread {
        Q_OBJECT
    private:
        void run();
        bool _stop = false;
    
    public:
        explicit AudioThread(QObject *parent = nullptr);
        ~AudioThread();
        void setStop(bool stop);
    signals:
    
    };
    
    #endif // AUDIOTHREAD_H
    
    

    上述的代码集成了一个QThread的线程类,线程一旦启动(start),那么run方法就会自动调用

    2.2 audiothread.cpp
    #include "audiothread.h"
    #include <QDebug>
    #include <QFile>
    #include <QDateTime>
    
    extern "C" {
    // 设备
    #include <libavdevice/avdevice.h>
    // 格式
    #include <libavformat/avformat.h>
    // 工具(比如错误处理)
    #include <libavutil/avutil.h>
    }
    
    #define FMT_NAME "avfoundation"
    #define DEVICE_NAME ":0"
    #define FILEPATH "/Users/songlin/audio/qt_record"
    
    AudioThread::AudioThread(QObject *parent) : QThread(parent) {
        // 当监听到线程结束时(finished),就调用deleteLater回收内存
        connect(this, &AudioThread::finished,
                this, &AudioThread::deleteLater);
    }
    
    AudioThread::~AudioThread() {
        // 断开所有的连接
        disconnect();
        // 内存回收之前,正常结束线程
        requestInterruption();
        // 安全退出
        quit();
        wait();
        qDebug() << this << "析构(内存被回收)";
    }
    
    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);
    }
    
    // 当线程启动的时候(start),就会自动调用run函数
    // run函数中的代码是在子线程中执行的
    // 耗时操作应该放在run函数中
    void AudioThread::run() {
        qDebug() << this << "开始执行----------";
    
        // 获取输入格式对象
        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 += ".pcm";
        QFile file(filename);
    
        // 打开文件
        // WriteOnly:只写模式。如果文件不存在,就创建文件;如果文件存在,就会清空文件内容
        if (!file.open(QFile::WriteOnly)) {
            qDebug() << "文件打开失败" << filename;
    
            // 关闭设备
            avformat_close_input(&ctx);
            return;
        }
    
        // 数据包
        AVPacket *pkt = av_packet_alloc();
        while (!isInterruptionRequested()) {
            // 不断采集数据
            ret = av_read_frame(ctx, pkt);
    
            if (ret == 0) { // 读取成功
                file.write((const char *) pkt->data, pkt->size);
            } else if (ret == AVERROR(EAGAIN)) { // 资源临时不可用
                continue;
            } else { // 其他错误
                char errbuf[1024];
                av_strerror(ret, errbuf, sizeof (errbuf));
                qDebug() << "av_read_frame error" << errbuf << ret;
                break;
            }
    
            // 必须要加,释放pkt内部的资源
            av_packet_unref(pkt);
        }
        // 释放资源
        // 关闭文件
        file.close();
    
        // 释放资源
        av_packet_free(&pkt);
    
        // 关闭设备
        avformat_close_input(&ctx);
    
        qDebug() << this << "正常结束----------";
    }
    
    void AudioThread::setStop(bool stop) {
        _stop = stop;
    }
    
    
    2.3 mainwindow.cpp
    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow) {
        ui->setupUi(this);
    }
    
    MainWindow::~MainWindow() {
        delete ui;
    }
    
    void MainWindow::on_audioButton_clicked() {
        if (!_audioThread) { // 点击了“开始录音”
            // 开启线程
            _audioThread = new AudioThread(this);
            _audioThread->start();
    
            connect(_audioThread, &AudioThread::finished,
            [this]() { // 线程结束
                _audioThread = nullptr;
                ui->audioButton->setText("开始录音");
            });
    
            // 设置按钮文字
            ui->audioButton->setText("结束录音");
        } else { // 点击了“结束录音”
            // 结束线程
    //        _audioThread->setStop(true);
            _audioThread->requestInterruption();
            _audioThread = nullptr;
    
            // 设置按钮文字
            ui->audioButton->setText("开始录音");
        }
    
    }
    
    

    3.录制过程中遇到的问题

    3.1 闪退

    闪退的信息大概是说The program has unexpectedly finished. 我的是mac平台,从网上找到的答案是说要配置Info.plist 文件,其实我已经配置了的

    Info.plist

    CONFIG+=sdk_no_version_check
    macx {
        FFMPEG_HOME = /usr/local/Cellar/ffmpeg/4.3.2_4
        QMAKE_INFO_PLIST =  mac/Info.plist
    }
    
    INCLUDEPATH += $${FFMPEG_HOME}/include
    
    LIBS += -L $${FFMPEG_HOME}/lib \
            -lavdevice \
            -lavformat \
            -lavutil
    
    

    创建Info.plist 的过程如下

    • 1.在QT项目创建Info.plist文件
      qt创建plist文件.png
      qt创建plist文件.png
    • 2.Info.plist 文件中增加内容
    <?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>Use Microphone</string>
    </dict>
    </plist>
    
    
    • 在项目xxx.pro 文件中配置
    QMAKE_INFO_PLIST = mac/Info.plist
    
    3.2 av_read_frame 的return值为-35

    我尝试在隐私中给项目APP添加磁盘完全访问权限,但是发现并没有生效,返回码还是-35,但是这个不影响录音,我也不知道哪里有问题,如果有找到好的方法的可以告诉我一下

    3.3 录音播放是噪音

    录音设备的输入声道是2,播放的时候也要传递参数是2,mac平台默认采样格式-f 为f32le,采样率为44100

    System Information

    system Information.png
     ✘ songlin@feng-sl  ~   master ±✚  ffmpeg -f avfoundation -i :0 out.wav
    ffmpeg version 4.3.2 Copyright (c) 2000-2021 the FFmpeg developers
      built with Apple clang version 12.0.0 (clang-1200.0.32.29)
      configuration: --prefix=/usr/local/Cellar/ffmpeg/4.3.2_4 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox
      libavutil      56. 51.100 / 56. 51.100
      libavcodec     58. 91.100 / 58. 91.100
      libavformat    58. 45.100 / 58. 45.100
      libavdevice    58. 10.100 / 58. 10.100
      libavfilter     7. 85.100 /  7. 85.100
      libavresample   4.  0.  0 /  4.  0.  0
      libswscale      5.  7.100 /  5.  7.100
      libswresample   3.  7.100 /  3.  7.100
      libpostproc    55.  7.100 / 55.  7.100
    Input #0, avfoundation, from ':0':
      Duration: N/A, start: 52087.499524, bitrate: 2822 kb/s
        Stream #0:0: Audio: pcm_f32le, 44100 Hz, stereo, flt, 2822 kb/s
    Stream mapping:
      Stream #0:0 -> #0:0 (pcm_f32le (native) -> pcm_s16le (native))
    Press [q] to stop, [?] for help
    Output #0, wav, to 'out.wav':
      Metadata:
        ISFT            : Lavf58.45.100
        Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, stereo, s16, 1411 kb/s
        Metadata:
          encoder         : Lavc58.91.100 pcm_s16le
    size=    1986kB time=00:00:11.84 bitrate=1373.9kbits/s speed=   1x
    video:0kB audio:1986kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.003835%
    Exiting normally, received signal 2.
    

    最终播放命令

     songlin@feng-sl  ~/Downloads/audio-video-dev-tutorial-main/02_code/09_record_audio_sub_thread   master ±✚  ffplay -ar 44100 -ac 2 -f  f32le /Users/songlin/audio/qt_record/05_05_23_05_35.pcm
    ffplay version 4.3.2 Copyright (c) 2003-2021 the FFmpeg developers
      built with Apple clang version 12.0.0 (clang-1200.0.32.29)
      configuration: --prefix=/usr/local/Cellar/ffmpeg/4.3.2_4 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox
      libavutil      56. 51.100 / 56. 51.100
      libavcodec     58. 91.100 / 58. 91.100
      libavformat    58. 45.100 / 58. 45.100
      libavdevice    58. 10.100 / 58. 10.100
      libavfilter     7. 85.100 /  7. 85.100
      libavresample   4.  0.  0 /  4.  0.  0
      libswscale      5.  7.100 /  5.  7.100
      libswresample   3.  7.100 /  3.  7.100
      libpostproc    55.  7.100 / 55.  7.100
    [f32le @ 0x7fd405841000] Estimating duration from bitrate, this may be inaccurate
    Input #0, f32le, from '/Users/songlin/audio/qt_record/05_05_23_05_35.pcm':
      Duration: 00:00:25.97, bitrate: 2822 kb/s
        Stream #0:0: Audio: pcm_f32le, 44100 Hz, 2 channels, flt, 2822 kb/s
      19.88 M-A:  0.000 fd=   0 aq=  356KB vq=    0KB sq=    0B f=0/0
     songlin@feng-sl  ~/Downloads/audio-video-dev-tutorial-main/02_code/09_record_audio_sub_thread   master ±✚ 
    

    最终还是录制和播放成功的,上面说到的配置了Info.plist 文件还是不能录制的原因:存放项目的目录之前已经编译了一个产物在那里了,所以我再重新配置Info.plist 之后,需要删掉之前的编译文件产物,然后通过debug的形式处理,不能直接点击Run


    Debug Run.png

    相关文章

      网友评论

          本文标题:代码操控音频

          本文链接:https://www.haomeiwen.com/subject/hdhjrltx.html