代码操控音频

2021-05-06  本文已影响0人  coder_feng

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

1. 步骤流程图

Snip20210425_7.png
1.1 注册设备
#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 的过程如下

<?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>

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
上一篇下一篇

猜你喜欢

热点阅读