04-📝音视频技术核心知识|音频录制【命令行、C++编程】

1,488 阅读16分钟

一、前言

本系列文章是对音视频技术入门知识的整理和复习,为进一步深入系统研究音视频技术巩固基础。文章列表:

二、通过命令行进行音频录制

终于要开始进行FFmpeg实战了,一起来感受一下FFmpeg的强大吧。

1. 命令简介

FFmpeg的bin目录中提供了3个命令(可执行程序),可以直接在命令行上使用。

3个命令

1.1 ffmpeg

ffmpeg的主要作用:对音视频进行编解码。

# 将MP3文件转成WAV文件
ffmpeg -i xx.mp3 yy.wav

当输入命令ffmpeg时,可以看到ffmpeg命令的使用格式是:

ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...

简化一下,常用格式是:

ffmpeg arg1 arg2 -i arg3 arg4 arg5
  • arg1:全局参数
  • arg2:输入文件参数
  • arg3:输入文件
  • arg4:输出文件参数
  • arg5:输出文件

更多详细用法,可以参考官方文档:ffmpeg-all.html,或者使用以下命令查看:

# 简易版
ffmpeg -h
# 详细版
ffmpeg -h long
# 完整版
ffmpeg -h full

# 或者使用
# ffmpeg -help
# ffmpeg -help long
# ffmpeg -help full

1.2 ffprobe

ffprobe的主要作用:查看音视频的参数信息。

# 可以查看MP3文件的采样率、比特率、时长等信息
ffprobe xx.mp3

当输入命令ffprobe时,可以看到ffprobe命令的使用格式是:

ffprobe [OPTIONS] [INPUT_FILE]
# OPTIONS:参数
# INPUT_FILE:输入文件

更多详细用法,可以参考官方文档:ffprobe-all.html,或者使用以下命令查看:

# 简易版
ffprobe -h
# 详细版
ffprobe -h long
# 完整版
ffprobe -h full

# 或者使用
# ffprobe -help
# ffprobe -help long
# ffprobe -help full

1.3 ffplay

ffplay的主要作用:播放音视频。

# 播放MP3文件
ffplay xx.mp3

当输入命令ffplay时,可以看到ffplay命令的使用格式是:

ffplay [options] input_file
# options:参数
# input_file:输入文件

更多详细用法,可以参考官方文档:ffplay-all.html,或者使用以下命令查看:

# 简易版
ffplay -h
# 详细版
ffplay -h long
# 完整版
ffplay -h full

# 或者使用
# ffplay -help
# ffplay -help long
# ffplay -help full

1.4 hide_banner

增加*-hide_bannder*参数可以隐藏一些冗余的描述信息,可以去实践比较以下2条命令的区别:

ffprobe xx.mp3

ffprobe -hide_banner xx.mp3

# ffmpeg、ffprobe、ffplay都适用

2. 通过命令行录音

2.1 查看可用设备

使用命令行查看当前平台的可用设备:

ffmpeg -devices

Windows的输出结果如下所示:

  • 列表中有个dshow,全名叫DirectShow,是Windows平台的多媒体系统库
  • 我们可以使用dshow去操作多媒体输入设备(比如录音设备)
Devices:
 D. = Demuxing supported
 .E = Muxing supported
 --
  E caca            caca (color ASCII art) output device
 D  dshow           DirectShow capture
 D  gdigrab         GDI API Windows frame grabber
 D  lavfi           Libavfilter virtual input device
 D  libcdio
  E sdl,sdl2        SDL2 output device
 D  vfwcap          VfW video capture

Mac的输出结果如下所示:

  • 列表中有个avfoundation,是Mac平台的多媒体系统库
  • 我们可以使用avfoundation去操作多媒体输入设备(比如录音设备)
Devices:
 D. = Demuxing supported
 .E = Muxing supported
 --
 D  avfoundation    AVFoundation input device
 D  lavfi           Libavfilter virtual input device
  E sdl,sdl2        SDL2 output device

2.2 查看dshow支持的设备

# 查看dshow支持的设备
ffmpeg -f dshow -list_devices true -i dummy

# 或者
# ffmpeg -list_devices true -f dshow -i ''
# ffmpeg -list_devices true -f dshow -i ""
  • -f dshow

    • dshow支持的
  • -list_devices true

    • 打印出所有的设备
  • -i dummy-i ''-i ""

    • 立即退出

我的笔记本外接了一只麦克风。

外接麦克风

因此,命令的执行结果大致如下所示:

DirectShow video devices (some may be both video and audio devices)
  "Integrated Camera"

DirectShow audio devices
  "线路输入 (3- 魅声T800)"
  "麦克风阵列 (Realtek(R) Audio)"
  • dshow支持的视频设备
    • Integrated Camera:笔记本自带的摄像头
  • dshow支持的音频设备
    • 线路输入 (3- 魅声T800):外接的麦克风
    • 麦克风阵列 (Realtek(R) Audio):笔记本自带的麦克风

2.3 查看avfoundation支持的设备

在Mac平台,使用的是avfoundation,而不是dshow。

ffmpeg -f avfoundation -list_devices true -i ''	

输出结果如下所示:

AVFoundation video devices:
 [0] FaceTime高清摄像头(内建)
 [1] Capture screen 0
AVFoundation audio devices:
 [0] MS-T800
 [1] Edu Audio Device
 [2] MacBook Pro麦克风

列表中的MS-T800是外接的麦克风。在Mac上,FFmpeg还给每一个视频、音频设备进行了编号,比如MS-T800的编号是0、Mac自带麦克风的编号是2。

2.4 指定设备进行录音

# 使用外接的麦克风进行录音,最后生成一个wav文件
ffmpeg -f dshow -i audio="麦克风阵列 (Realtek(R) Audio)" out.wav

# 在Mac上通过编号指定设备
ffmpeg -f avfoundation -i :2 out.wav
# :0表示使用0号音频设备
# 0:2表示使用0号视频设备和2号音频设备
  • 可以使用快捷键Ctrl + C终止录音
  • 我这边的测试结果显示,音频参数是:
    • Windows:44100Hz采样率、16位深度、2声道、1411Kbps比特率
    • Mac:48000Hz采样率、16位深度、2声道、1536Kbps比特率

2.5 设置dshow的参数

先通过命令查看一下dshow可以使用的参数,详情可以查看官方文档:dshow参数

# 从ffmpeg -devices命令的结果可以看得出来:dshow属于demuxer,而不是muxer
ffmpeg -h demuxer=dshow

部分输出结果如下所示:

# 采样率
-sample_rate         <int> set audio sample rate (from 0 to INT_MAX)
# 采样大小(位深度)
-sample_size         <int> set audio sample size (from 0 to 16)
# 声道数
-channels            <int> set number of audio channels, such as 1 or 2 (from 0 to INT_MAX)
# 列出特定设备支持的参数
-list_options        <boolean> list available options for specified device (default false)

然后再看看你的设备支持哪些参数。

ffmpeg -f dshow -list_options true -i audio="麦克风阵列 (Realtek(R) Audio)"

输出结果如下所示:

DirectShow audio only device options (from audio devices)
  Pin "Capture" (alternative pin name "Capture")
  min ch=1 bits=8 rate= 11025 max ch=2 bits=16 rate= 44100
  
# 可以看出来:采样率范围是11025~44100Hz

接下来设置录音时的音频参数。

ffmpeg -f dshow -sample_rate 15000 -sample_size 16 -channels 1 -i audio="麦克风阵列 (Realtek(R) Audio)" out.wav

二、通过编程进行音频录制

1. 通过编程录音

开发录音功能的主要步骤是:

    1. 打开设备
    • 注册设备
    • 打开设备
      • 设置 设备 采集的上下文
      • 设置 选中的设备
      • 设置 选中的输入格式(不同平台的格式不同,要提前 配置好,若是要跨平台,需要提前做好条件编译)
      • 设置采集的 配置信息
    1. 采集数据
    • 获取 设备 的采集上下文对象
    • 采集数据
      • 获取采集数据的每一个当前采集好好的数据包(采集的数据不是马上通过IO存储在系统硬盘的是先存储在缓冲区的,这属于不同平台对文件IO的优化,因为一直进行系统的IO操作,会影响执行效率:存入缓存比存入硬盘快)
      • 将数据包写入文件系统
      • 释放写好的数据包
      • 继续采集下一个数据包/退出采集
      • ....(Tips:1. 要写好采集的开始和结束的控制逻辑 2. 要避免主线程采集,卡顿主线程下)
    • 通过文件存储API 存储 采集好的 数据
      • 打开文件操作
      • 写入数据
      • 写完要关闭文件操作
    1. 释放资源

主要步骤

需要用到的FFmpeg库有4个。

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

1.1 权限申请

在Mac平台,有2个注意点:(可以在编译之后的软件包上改info.plist、也可以自定义info.plist)

  • 需要在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.2 注册设备

在整个程序的运行过程中,只需要执行1次注册设备的代码。

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

1.3 获取输入格式对象

1.3.1 宏定义

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

1.3.2 核心代码

根据格式名称获取输入格式对象,后面需要利用输入格式对象打开设备。

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

1.4 打开设备

// 格式上下文(后面通过格式上下文操作设备)
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;
}

1.5 采集数据

1.5.1 宏定义

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

1.5.2 核心代码

#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;
    }
}

1.6 释放资源

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

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

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

想要了解每一个函数的具体作用,可以查询:官方API文档

1.7 获取录音设备的相关参数

// 从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);
}

2. 多线程

录音属于耗时操作,为了避免阻塞主线程,最好在子线程中进行录音操作。这里创建了一个继承自QThread的线程类,线程一旦启动(start),就会自动调用 run 函数。

2.1 .h

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

2.2 .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() {
    // 录音操作
    // ...
}

2.3 开启线程

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

2.4 结束线程

// 外部调用线程的requestInterruption,请求结束线程
audioThread->requestInterruption();

// 线程内部的逻辑
void AudioThread::run() {
    // 可以通过isInterruptionRequested判断是否要结束线程
    // 当调用过线程的requestInterruption时,isInterruptionRequested返回值就为true,否则为false
    while (!isInterruptionRequested()) {
    	// ...
    }
}

2.5 改造录音代码

// 数据包
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;
    }
}

专题系列文章

1. 前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

4. C++核心语法

5. Vue全家桶

6. 音视频技术核心知识

其它底层原理专题

1. 底层原理相关专题

2. iOS相关专题

3. webApp相关专题

4. 跨平台开发方案相关专题

5. 阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6. Android、HarmonyOS页面渲染专题

7. 小程序页面渲染专题