代码操控音频
2021-05-06 本文已影响0人
coder_feng
上一篇文章已经展示了如何通过命令行来录制音频,这篇文章,我们尝试通过代码来实践一下
1. 步骤流程图
Snip20210425_7.png1.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.pngSystem Information
✘ 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