音视频-FFmpeg音频录制、播放、编码和解码(上)

前言:本文旨在介绍在跨平台开发工具QT(跨平台C++图形用户界面应用程序开发框架)上使用 FFmpeg 进行音频的录制、播放、编码和解码。
视频请参考下篇:音视频-FFmpeg视频录制、播放、编码和解码(下)

一、工具QT的安装和使用

本文使用Mac环境进行开发,Windows请参考 【秒懂音视频开发】04_Windows开发环境搭建

1、安装FFmpeg

在Mac环境中,直接使用Homebrew安装FFmpeg即可

brew install ffmpeg

查看版本

ffmpeg -version

2、安装Qt

通过brew install安装Qt,最终被安装在/usr/local/Cellar/qt目录。

brew install qt

通过brew install --cask安装Qt Creator,最终被安装在/usr/local/Caskroom/qt-creator目录。

brew install --cask qt-creator

3、集成FFmpeg到Qt项目中

在wxq_qt_ffmpeg_demo.pro 中添加

mac: {
    MMFPEG_HOME = /usr/local/Cellar/ffmpeg/5.1
    // 打印
    message($${MMFPEG_HOME})
    message($$(PATH))
# 设置头文件路径
INCLUDEPATH += $${MMFPEG_HOME}/include
# 设置库文件路径
LIBS += -L $${MMFPEG_HOME}/lib \
        -lavcodec \
        -lavdevice \
        -lavfilter \
        -lavformat \
        -lavutil \
        -lpostproc \
        -lswscale \
        -lswresample
}

二、QT的信号和槽

  • 信号(Signal):比如点击按钮就会发出一个点击信号
  • 槽(Slot):一般也叫槽函数,是用来处理信号的函数
// 比如点击按钮,关闭当前窗口
// btn发出clicked信号,就会调用this的close函数
connect(btn, &QPushButton::clicked, this, &MainWindow::close);
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    QPushButton *btn = new QPushButton;
    btn->setText("登陆");
    btn->setFixedSize(100, 30);
    btn->setParent(this);

    // 关闭窗口
    // 链接信号和槽
    // btn发出信号
    // QMainWindow接受信号,调用槽函数close
    connect(btn, &QPushButton::clicked, this, &QMainWindow::close);
}

三、音频

1、介绍

将音频数字化的常见技术方案是脉冲编码调制(PCM,Pulse Code Modulation),主要过程是:采样 → 量化 → 编码。

1.1、采样率:

每秒采集的样本数量,称为采样率(采样频率,采样速率,Sampling Rate)。比如,采样率44.1kHz表示1秒钟采集44100个样本。

1.2、位深度(采样精度,采样大小,Bit Depth):

使用多少个二进制位来存储一个采样点的样本值。位深度越高,表示的振幅越精确。常见的CD采用16bit的位深度,能表示65536(2^16)个不同的值。DVD使用24bit的位深度,大多数电话设备使用8bit的位深度。

1.3、声道(Channel)

单声道产生一组声波数据,双声道(立体声)产生两组声波数据。

采样率44.1kHZ、位深度16bit的1分钟立体声PCM数据有多大?

  • 采样率 * 位深度 * 声道数 * 时间
  • 44100 * 16 * 2 * 60 / 8 ≈ 10.34MB

1.4、比特率(Bit Rate):

指单位时间内传输或处理的比特数量,单位是:比特每秒(bit/s或bps),还有:千比特每秒(Kbit/s或Kbps)、兆比特每秒(Mbit/s或Mbps)、吉比特每秒(Gbit/s或Gbps)、太比特每秒(Tbit/s或Tbps)。

采样率44.1kHZ、位深度16bit的立体声PCM数据的比特率是多少?

  • 采样率 * 位深度 * 声道数
  • 44100 * 16 * 2 = 1411.2Kbps

通常,采样率、位深度越高,数字化音频的质量就越好。从比特率的计算公式可以看得出来:比特率越高,数字化音频的质量就越好。

2、ffmpeg音频转换

ffmpeg -i y831.wav y8.mp3

输出:

wxq@wangxueqideMBP Desktop % ffmpeg -i y831.wav y8.mp3
ffmpeg version 5.1 Copyright (c) 2000-2022 the FFmpeg developers
  built with Apple clang version 13.1.6 (clang-1316.0.21.2.5)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/5.1 --enable-shared --enable-pthreads --enable-version3 --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-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --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      57. 28.100 / 57. 28.100
  libavcodec     59. 37.100 / 59. 37.100
  libavformat    59. 27.100 / 59. 27.100
  libavdevice    59.  7.100 / 59.  7.100
  libavfilter     8. 44.100 /  8. 44.100
  libswscale      6.  7.100 /  6.  7.100
  libswresample   4.  7.100 /  4.  7.100
  libpostproc    56.  6.100 / 56.  6.100
Guessed Channel Layout for Input Stream #0.0 : mono
Input #0, wav, from 'y831.wav':
  Duration: 00:00:04.13, bitrate: 1536 kb/s
  Stream #0:0: Audio: pcm_f32le ([3][0][0][0] / 0x0003), 48000 Hz, mono, flt, 1536 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (pcm_f32le (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, mp3, to 'y8.mp3':
  Metadata:
    TSSE            : Lavf59.27.100
  Stream #0:0: Audio: mp3, 48000 Hz, mono, fltp
    Metadata:
      encoder         : Lavc59.37.100 libmp3lame
size=      33kB time=00:00:04.15 bitrate=  64.8kbits/s speed= 180x    
video:0kB audio:33kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.709411%

3、ffprobe:查看音视频的参数信息。

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

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

输出

Input #0, mp3, from 'y8.mp3':
  Metadata:
    encoder         : Lavf59.27.100
  Duration: 00:00:04.18, start: 0.023021, bitrate: 64 kb/s
  Stream #0:0: Audio: mp3, 48000 Hz, mono, fltp, 64 kb/s

4、ffplay:播放音视频。

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

ffplay y8.mp3

注意:ffplay -hide_banner y8.mp3 可以屏蔽版本信息

四、命令行录音和播放

1、查看可用设备

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

ffmpeg -devices

输出

wxq@wangxueqideMBP Desktop % ffmpeg -devices
Devices:
 D. = Demuxing supported
 .E = Muxing supported
 --
  E audiotoolbox    AudioToolbox output device
 D  avfoundation    AVFoundation input device
 D  lavfi           Libavfilter virtual input device
  E sdl,sdl2        SDL2 output device
 D  x11grab         X11 screen capture, using XCB

2、查看avfoundation支持的设备

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

ffmpeg -f avfoundation -list_devices true -i ''

输出

[AVFoundation indev @ 0x7ff2dd205200] AVFoundation video devices:
[AVFoundation indev @ 0x7ff2dd205200] [0] FaceTime高清摄像头(内建)
[AVFoundation indev @ 0x7ff2dd205200] [1] Capture screen 0
[AVFoundation indev @ 0x7ff2dd205200] AVFoundation audio devices:
[AVFoundation indev @ 0x7ff2dd205200] [0] 外置麦克风
[AVFoundation indev @ 0x7ff2dd205200] [1] MacBook Pro麦克风

3、指定设备进行录音

录音参数:pcm_f32le, 48000 Hz

ffmpeg -f avfoundation -i :1 out.mp3  // :1表示使用1号音频设备,即[1] MacBook Pro麦克风

执行后直接开始录音,可以使用快捷键Ctrl + C终止录音

输出

Input #0, avfoundation, from ':0':
  Duration: N/A, start: 317843.718292, bitrate: 1536 kb/s
  Stream #0:0: Audio: pcm_f32le, 48000 Hz, mono, flt, 1536 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (pcm_f32le (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, mp3, to 'out.mp3':
  Metadata:
    TSSE            : Lavf59.27.100
  Stream #0:0: Audio: mp3, 48000 Hz, mono, fltp
    Metadata:
      encoder         : Lavc59.37.100 libmp3lame
size=      35kB time=00:00:04.58 bitrate=  62.1kbits/s speed=0.997x    
video:0kB audio:34kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.670856%
Exiting normally, received signal 2.

4、命令行播放pcm录音

注意:采样率和格式要一样。录音参数:pcm_f32le, 48000 Hz

ffplay -ar 48000 -ac 2 -f f32le 08_15_11_58_59.pcm

五、编写代码录音和播放

1、通过编程录音

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

  • 注册设备
  • 获取输入格式对象
  • 打开设备
  • 采集数据
  • 释放资源

1.2、具体步骤:

1.2.1注册设备

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

// 初始化libavdevice并注册所有输入和输出设备 
avdevice_register_all();
1.2.2获取输入格式对象

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.2.3、录音核心代码

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

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

    // 获取输入格式对象
    AVInputFormat *fmt = (AVInputFormat *)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);
        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);
    
    qDebug() << this << "正常结束----------";
}

2、通过编程播放录音

2.1、初始化子系统

SDL分成好多个子系统(subsystem):

  • Video:显示和窗口管理
  • Audio:音频设备管理
  • Joystick:游戏摇杆控制
  • Timers:定时器
  • ...

目前只用到了音频功能,所以只需要通过SDL_init函数初始化Audio子系统即可。

// 初始化Audio子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
    // 返回值不是0,就代表失败
    qDebug() << "SDL_Init Error" << SDL_GetError();
    return;
}

2.2、打开音频设备

/* 一些宏定义 */
// 采样率
#define SAMPLE_RATE 48000
// 采样格式
#define SAMPLE_FORMAT AUDIO_F32LSB
// 采样大小
#define SAMPLE_SIZE SDL_AUDIO_BITSIZE(SAMPLE_FORMAT)
// 声道数
#define CHANNELS 2
// 音频缓冲区的样本数量
#define SAMPLES 1024
 
// 用于存储读取的音频数据和长度
typedef struct {
    int len = 0;
    int pullLen = 0;
    Uint8 *data = nullptr;
} AudioBuffer;
 
// 音频参数
SDL_AudioSpec spec;
// 采样率
spec.freq = SAMPLE_RATE;
// 采样格式(s16le)
spec.format = SAMPLE_FORMAT;
// 声道数
spec.channels = CHANNELS;
// 音频缓冲区的样本数量(这个值必须是2的幂)
spec.samples = SAMPLES;
// 回调
spec.callback = pull_audio_data;
// 传递给回调的参数
AudioBuffer buffer;
spec.userdata = &buffer;
 
// 打开音频设备
if (SDL_OpenAudio(&spec, nullptr)) {
    qDebug() << "SDL_OpenAudio Error" << SDL_GetError();
    // 清除所有初始化的子系统
    SDL_Quit();
    return;
}

2.3、打开文件

#define FILENAME "/Users/wxq/Desktop/08_15_11_58_59.pcm"
 
// 打开文件
QFile file(FILENAME);
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "文件打开失败" << FILENAME;
    // 关闭音频设备
    SDL_CloseAudio();
    // 清除所有初始化的子系统
    SDL_Quit();
    return;
}

2.4、开始播放

// 每个样本占用多少个字节
#define BYTES_PER_SAMPLE ((SAMPLE_SIZE * CHANNELS) / 8)
// 文件缓冲区的大小
#define BUFFER_SIZE (SAMPLES * BYTES_PER_SAMPLE)
 
// 开始播放
SDL_PauseAudio(0);
 
// 存放文件数据
Uint8 data[BUFFER_LEN];
 
while (!isInterruptionRequested()) {
    // 只要从文件中读取的音频数据,还没有填充完毕,就跳过
    if (buffer.len > 0) continue;
 
    buffer.len = file.read((char *) data, BUFFER_SIZE);
 
    // 文件数据已经读取完毕
    if (buffer.len <= 0) {
        // 剩余的样本数量
        int samples = buffer.pullLen / BYTES_PER_SAMPLE;
        int ms = samples * 1000 / SAMPLE_RATE;
        SDL_Delay(ms);
        break;
    }
 
    // 读取到了文件数据
    buffer.data = data;
}

2.5、回调函数

// userdata:SDL_AudioSpec.userdata
// stream:音频缓冲区(需要将音频数据填充到这个缓冲区)
// len:音频缓冲区的大小(SDL_AudioSpec.samples * 每个样本的大小)
void pull_audio_data(void *userdata, Uint8 *stream, int len) {
    // 清空stream
    SDL_memset(stream, 0, len);
 
    // 取出缓冲信息
    AudioBuffer *buffer = (AudioBuffer *) userdata;
    if (buffer->len == 0) return;
 
    // 取len、bufferLen的最小值(为了保证数据安全,防止指针越界)
    buffer->pullLen = (len > buffer->len) ? buffer->len : len;
    
    // 填充数据
    SDL_MixAudio(stream,
                 buffer->data,
                 buffer->pullLen,
                 SDL_MIX_MAXVOLUME);
    buffer->data += buffer->pullLen;
    buffer->len -= buffer->pullLen;
}

2.6、释放资源

// 关闭文件
file.close();
// 关闭音频设备
SDL_CloseAudio();
// 清理所有初始化的子系统
SDL_Quit();

六、PCM转成WAV

1、通过下面的命令可以将PCM转成WAV

ffmpeg -ar 48000 -ac 2 -f f32le -i out.pcm out.wav

需要注意的是:上面命令生成的WAV文件头有78字节。对比44字节的文件头,它多增加了一个34字节大小的LIST chunk。

加上一个输出文件参数-bitexact可以去掉LIST Chunk。

ffmpeg -ar 48000 -ac 2 -f f32le -i out.pcm -bitexact out.wav

输出

Input #0, f32le, from 'out.pcm':
  Duration: 00:00:02.98, bitrate: 3072 kb/s
  Stream #0:0: Audio: pcm_f32le, 48000 Hz, stereo, flt, 3072 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            : Lavf59.27.100
  Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 48000 Hz, stereo, s16, 1536 kb/s
    Metadata:
      encoder         : Lavc59.37.100 pcm_s16le
size=     559kB time=00:00:02.98 bitrate=1536.2kbits/s speed= 387x        
video:0kB audio:559kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.013626%

2、通过代码将PCM转成WAV

在PCM数据的前面插入一个44字节的WAV文件头,就可以将PCM转成WAV。

2.1、WAV的文件头结构

WAV的文件头结构大概如下所示:

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

2.2、PCM转WAV核心实现

封装到了FFmpegs类的pcm2wav函数中。

#include 
#include 
 
class FFmpegs {
public:
    FFmpegs();
    static void pcm2wav(WAVHeader &header,
                        const char *pcmFilename,
                        const char *wavFilename);
};
 
void FFmpegs::pcm2wav(WAVHeader &header,
                      const char *pcmFilename,
                      const char *wavFilename) {
    header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
    header.byteRate = header.sampleRate * header.blockAlign;
 
    // 打开pcm文件
    QFile pcmFile(pcmFilename);
    if (!pcmFile.open(QFile::ReadOnly)) {
        qDebug() << "文件打开失败" << pcmFilename;
        return;
    }
    header.dataChunkDataSize = pcmFile.size();
    header.riffChunkDataSize = header.dataChunkDataSize
                               + sizeof (WAVHeader) - 8;
 
    // 打开wav文件
    QFile wavFile(wavFilename);
    if (!wavFile.open(QFile::WriteOnly)) {
        qDebug() << "文件打开失败" << wavFilename;
 
        pcmFile.close();
        return;
    }
 
    // 写入头部
    wavFile.write((const char *) &header, sizeof (WAVHeader));
 
    // 写入pcm数据
    char buf[1024];
    int size;
    while ((size = pcmFile.read(buf, sizeof (buf))) > 0) {
        wavFile.write(buf, size);
    }
 
    // 关闭文件
    pcmFile.close();
    wavFile.close();
}

2.3、调用函数

// 封装WAV的头部
WAVHeader header;
header.numChannels = 2;
header.sampleRate = 44100;
header.bitsPerSample = 16;
// 调用函数
FFmpegs::pcm2wav(header, "F:/in.pcm", "F:/out.wav");

七、播放WAV

对于WAV文件来说,可以直接使用ffplay命令播放,而且不用像PCM那样增加额外的参数。因为WAV的文件头中已经包含了相关的音频参数信息。

ffplay in.wav

1、初始化子系统

// 初始化Audio子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
    qDebug() << "SDL_Init error:" << SDL_GetError();
    return;
}

2、加载WAV文件

// 存放WAV的PCM数据和数据长度
typedef struct {
    Uint32 len = 0;
    int pullLen = 0;
    Uint8 *data = nullptr;
} AudioBuffer;
 
// WAV中的PCM数据
Uint8 *data;
// WAV中的PCM数据大小(字节)
Uint32 len;
// 音频参数
SDL_AudioSpec spec;
 
// 加载wav文件
if (!SDL_LoadWAV(FILENAME, &spec, &data, &len)) {
    qDebug() << "SDL_LoadWAV error:" << SDL_GetError();
    // 清除所有的子系统
    SDL_Quit();
    return;
}
 
// 回调
spec.callback = pull_audio_data;
// 传递给回调函数的userdata
AudioBuffer buffer;
buffer.len = len;
buffer.data = data;
spec.userdata = &buffer;

3、打开音频设备

// 打开设备
if (SDL_OpenAudio(&spec, nullptr)) {
    qDebug() << "SDL_OpenAudio error:" << SDL_GetError();
    // 释放文件数据
    SDL_FreeWAV(data);
    // 清除所有的子系统
    SDL_Quit();
    return;
}

4、开始播放

// 开始播放(0是取消暂停)
SDL_PauseAudio(0);
 
while (!isInterruptionRequested()) {
    if (buffer.len > 0) continue;
    // 每一个样本的大小
    int size = spec.channels * SDL_AUDIO_BITSIZE(spec.format) / 8;
    // 最后一次播放的样本数量
    int samples = buffer.pullLen / size;
    // 最后一次播放的时长
    int ms = samples * 1000 / spec.freq;
    SDL_Delay(ms);
    break;
}

5、回调函数

// 等待音频设备回调(会回调多次)
void pull_audio_data(void *userdata,
                     // 需要往stream中填充PCM数据
                     Uint8 *stream,
                     // 希望填充的大小(samples * format * channels / 8)
                     int len
                    ) {
    // 清空stream
    SDL_memset(stream, 0, len);
 
    AudioBuffer *buffer = (AudioBuffer *) userdata;
 
    // 文件数据还没准备好
    if (buffer->len <= 0) return;
 
    // 取len、bufferLen的最小值
    buffer->pullLen = (len > (int) buffer->len) ? buffer->len : len;
 
    // 填充数据
    SDL_MixAudio(stream,
                 buffer->data,
                 buffer->pullLen,
                 SDL_MIX_MAXVOLUME);
    buffer->data += buffer->pullLen;
    buffer->len -= buffer->pullLen;
}

6、释放资源

// 释放WAV文件数据
SDL_FreeWAV(data);

// 关闭设备
SDL_CloseAudio();

// 清除所有的子系统
SDL_Quit();

八、音频重采样

1、什么叫音频重采样

音频重采样(Audio Resample):将音频A转换成音频B,并且音频A、B的参数(采样率、采样格式、声道数)并不完全相同。比如:

1.1、音频A的参数

  • 采样率:48000
  • 采样格式:f32le
  • 声道数:1

1.2、音频B的参数

  • 采样率:44100
  • 采样格式:s16le
  • 声道数:2

2、为什么需要音频重采样

这里列举一个音频重采样的经典用途。

有些音频编码器对输入的原始PCM数据是有特定参数要求的,比如要求必须是44100_s16le_2。但是你提供的PCM参数可能是48000_f32le_1。这个时候就需要先将48000_f32le_1转换成44100_s16le_2,然后再使用音频编码器对转换后的PCM进行编码。

3、命令行重采样

通过下面的命令行可以将44100_s16le_2转换成48000_f32le_1。

ffmpeg -ar 44100 -ac 2 -f s16le -i 44100_s16le_2.pcm -ar 48000 -ac 1 -f f32le 48000_f32le_1.pcm

4、编码重采样

音频重采样需要用到2个库:

  • swresample
  • avutil

4.1、函数声明

为了让音频重采样功能更加通用,设计成以下函数:

// 音频参数
typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} ResampleAudioSpec;
 
class FFmpegs {
public:
    static void resampleAudio(ResampleAudioSpec &in,
                              ResampleAudioSpec &out);
 
    static void resampleAudio(const char *inFilename,
                              int inSampleRate,
                              AVSampleFormat inSampleFmt,
                              int inChLayout,
 
                              const char *outFilename,
                              int outSampleRate,
                              AVSampleFormat outSampleFmt,
                              int outChLayout);
};
 
// 导入头文件
extern "C" {
#include 
#include 
}
 
// 处理错误码
#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));
 
void FFmpegs::resampleAudio(ResampleAudioSpec &in,
                            ResampleAudioSpec &out) {
    resampleAudio(in.filename, in.sampleRate, in.sampleFmt, in.chLayout,
                  out.filename, out.sampleRate, out.sampleFmt, out.chLayout);
}

4.2、函数调用

// 输入参数
ResampleAudioSpec in;
in.filename = "F:/44100_s16le_2.pcm";
in.sampleFmt = AV_SAMPLE_FMT_S16;
in.sampleRate = 44100;
in.chLayout = AV_CH_LAYOUT_STEREO;
 
// 输出参数
ResampleAudioSpec out;
out.filename = "F:/48000_f32le_1.pcm";
out.sampleFmt = AV_SAMPLE_FMT_FLT;
out.sampleRate = 48000;
out.chLayout = AV_CH_LAYOUT_MONO;
 
// 进行音频重采样
FFmpegs::resampleAudio(in, out);

4.3、函数实现

为了简化释放资源的代码,函数中用到了goto语句,所以把需要用到的变量都定义到了前面。

// 文件名
QFile inFile(inFilename);
QFile outFile(outFilename);
 
// 输入缓冲区
// 指向缓冲区的指针
uint8_t **inData = nullptr;
// 缓冲区的大小
int inLinesize = 0;
// 声道数
int inChs = av_get_channel_layout_nb_channels(inChLayout);
// 一个样本的大小
int inBytesPerSample = inChs * av_get_bytes_per_sample(inSampleFmt);
// 缓冲区的样本数量
int inSamples = 1024;
// 读取文件数据的大小
int len = 0;
 
// 输出缓冲区
// 指向缓冲区的指针
uint8_t **outData = nullptr;
// 缓冲区的大小
int outLinesize = 0;
// 声道数
int outChs = av_get_channel_layout_nb_channels(outChLayout);
// 一个样本的大小
int outBytesPerSample = outChs * av_get_bytes_per_sample(outSampleFmt);
// 缓冲区的样本数量(AV_ROUND_UP是向上取整)
int outSamples = av_rescale_rnd(outSampleRate, inSamples, inSampleRate, AV_ROUND_UP);
 
/*
 inSampleRate     inSamples
 ------------- = -----------
 outSampleRate    outSamples
 
 outSamples = outSampleRate * inSamples / inSampleRate
 */
 
// 返回结果
int ret = 0;

4.4、创建重采样上下文

// 创建重采样上下文
SwrContext *ctx = swr_alloc_set_opts(nullptr,
                                     // 输出参数
                                     outChLayout, outSampleFmt, outSampleRate,
                                     // 输入参数
                                     inChLayout, inSampleFmt, inSampleRate,
                                     0, nullptr);
if (!ctx) {
    qDebug() << "swr_alloc_set_opts error";
    goto end;
}

4.5、初始化重采样上下文

// 初始化重采样上下文
int ret = swr_init(ctx);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "swr_init error:" << errbuf;
    goto end;
}

4.6、创建缓冲区

// 创建输入缓冲区
ret = av_samples_alloc_array_and_samples(
          &inData,
          &inLinesize,
          inChs,
          inSamples,
          inSampleFmt,
          1);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "av_samples_alloc_array_and_samples error:" << errbuf;
    goto end;
}
 
// 创建输出缓冲区
ret = av_samples_alloc_array_and_samples(
          &outData,
          &outLinesize,
          outChs,
          outSamples,
          outSampleFmt,
          1);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "av_samples_alloc_array_and_samples error:" << errbuf;
    goto end;
}

4.7、读取文件数据

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error:" << inFilename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error:" << outFilename;
    goto end;
}
 
// 读取文件数据
// inData[0] == *inData
while ((len = inFile.read((char *) inData[0], inLinesize)) > 0) {
    // 读取的样本数量
    inSamples = len / inBytesPerSample;
 
    // 重采样(返回值转换后的样本数量)
    ret = swr_convert(ctx,
                      outData, outSamples,
                      (const uint8_t **) inData, inSamples
                     );
 
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "swr_convert error:" << errbuf;
        goto end;
    }
 
    // 将转换后的数据写入到输出文件中
    // outData[0] == *outData
    outFile.write((char *) outData[0], ret * outBytesPerSample);
}

4.8、刷新输出缓冲区

// 检查一下输出缓冲区是否还有残留的样本(已经重采样过的,转换过的)
while ((ret = swr_convert(ctx,
                          outData, outSamples,
                          nullptr, 0)) > 0) {
    outFile.write((char *) outData[0], ret * outBytesPerSample);
}

4.9、回收释放资源

end:
    // 释放资源
    // 关闭文件
    inFile.close();
    outFile.close();
 
    // 释放输入缓冲区
    if (inData) {
        av_freep(&inData[0]);
    }
    av_freep(&inData);
 
    // 释放输出缓冲区
    if (outData) {
        av_freep(&outData[0]);
    }
    av_freep(&outData);
 
    // 释放重采样上下文
    swr_free(&ctx);

九、AAC编码

1、查看FFmpeg编解码器

 ffmpeg -codecs | grep aac

输出

ffmpeg version 5.1 Copyright (c) 2000-2022 the FFmpeg developers
  built with Apple clang version 13.1.6 (clang-1316.0.21.2.5)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/5.1 --enable-shared --enable-pthreads --enable-version3 --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-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --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      57. 28.100 / 57. 28.100
  libavcodec     59. 37.100 / 59. 37.100
  libavformat    59. 27.100 / 59. 27.100
  libavdevice    59.  7.100 / 59.  7.100
  libavfilter     8. 44.100 /  8. 44.100
  libswscale      6.  7.100 /  6.  7.100
  libswresample   4.  7.100 /  4.  7.100
  libpostproc    56.  6.100 / 56.  6.100
 DEAIL. aac                  AAC (Advanced Audio Coding) (decoders: aac aac_fixed aac_at ) (encoders: aac aac_at )
 D.AIL. aac_latm             AAC LATM (Advanced Audio Coding LATM syntax)

2、通过命令行编码

-vbr

  • 开启VBR模式(Variable Bit Rate,可变比特率)
  • 如果开启了VBR模式,-b:a选项将会被忽略,但-profile:a选项仍然有效
ffmpeg -i out.wav -c:a libfdk_aac -vbr 1 out.aac

3、通过编程编码

3.1、变量定义

// 编码器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;
 
// 用来存放编码前的数据
AVFrame *frame = nullptr;
// 用来存放编码后的数据
AVPacket *pkt = nullptr;
 
// 返回结果
int ret = 0;
 
// 输入文件
QFile inFile(in.filename);
// 输出文件
QFile outFile(outFilename);

3.2、获取编码器

下面的代码可以获取FFmpeg默认的AAC编码器(并不是libfdk_aac)。

AVCodec *codec1 = avcodec_find_encoder(AV_CODEC_ID_AAC);
 
AVCodec *codec2 = avcodec_find_encoder_by_name("aac");
 
// true
qDebug() << (codec1 == codec2);
 
// aac
qDebug() << codec1->name;

不过我们最终要获取的是libfdk_aac。

// 获取fdk-aac编码器
codec = avcodec_find_encoder_by_name("libfdk_aac");
if (!codec) {
    qDebug() << "encoder libfdk_aac not found";
    return;
}

3.3、检查采样格式

接下来检查编码器是否支持当前的采样格式。

// 检查采样格式
if (!check_sample_fmt(codec, in.sampleFmt)) {
    qDebug() << "Encoder does not support sample format"
             << av_get_sample_fmt_name(in.sampleFmt);
    return;
}

检查函数check_sample_fmt的实现如下所示。

// 检查编码器codec是否支持采样格式sample_fmt
static int check_sample_fmt(const AVCodec *codec,
                            enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmts;
    while (*p != AV_SAMPLE_FMT_NONE) {
        if (*p == sample_fmt) return 1;
        p++;
    }
    return 0;
}

3.4、创建上下文

avcodec_alloc_context3后面的3说明这已经是第3版API,取代了此前的avcodec_alloc_context和avcodec_alloc_context2。

// 创建上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
    qDebug() << "avcodec_alloc_context3 error";
    return;
}
 
// 设置参数
ctx->sample_fmt = in.sampleFmt;
ctx->sample_rate = in.sampleRate;
ctx->channel_layout = in.chLayout;
// 比特率
ctx->bit_rate = 32000;
// 规格
ctx->profile = FF_PROFILE_AAC_HE_V2;

3.5、打开编码器

// 打开编码器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "avcodec_open2 error" << errbuf;
    goto end;
}

如果是想设置一些libfdk_aac特有的参数(比如vbr),可以通过options参数传递。

AVDictionary *options = nullptr;
av_dict_set(&options, "vbr", "1", 0);
ret = avcodec_open2(ctx, codec, &options);

3.6、创建AVFrame

AVFrame用来存放编码前的数据。

// 创建AVFrame
frame = av_frame_alloc();
if (!frame) {
    qDebug() << "av_frame_alloc error";
    goto end;
}
 
// 样本帧数量(由frame_size决定)
frame->nb_samples = ctx->frame_size;
// 采样格式
frame->format = ctx->sample_fmt;
// 声道布局
frame->channel_layout = ctx->channel_layout;
// 创建AVFrame内部的缓冲区
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "av_frame_get_buffer error" << errbuf;
    goto end;
}

3.7、创建AVPacket

// 创建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
    qDebug() << "av_packet_alloc error";
    goto end;
}

3.8、打开文件

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error" << in.filename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error" << outFilename;
    goto end;
}

3.9、开始编码

// frame->linesize[0]是缓冲区的大小
// 读取文件数据
while ((ret = inFile.read((char *) frame->data[0],
                          frame->linesize[0])) > 0) {
    // 最后一次读取文件数据时,有可能并没有填满frame的缓冲区
    if (ret < frame->linesize[0]) {
        // 声道数
        int chs = av_get_channel_layout_nb_channels(frame->channel_layout);
        // 每个样本的大小
        int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);
        // 改为真正有效的样本帧数量
        frame->nb_samples = ret / (chs * bytes);
    }
 
    // 编码
    if (encode(ctx, frame, pkt, outFile) < 0) {
        goto end;
    }
}
 
// flush编码器
encode(ctx, nullptr, pkt, outFile);

encode函数专门用来进行编码,它的实现如下所示。

// 音频编码
// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
static int encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }
 
    while (true) {
        // 从编码器中获取编码后的数据
        ret = avcodec_receive_packet(ctx, pkt);
        // packet中已经没有数据,需要重新发送数据到编码器(send frame)
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) { // 出现了其他错误
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_packet error" << errbuf;
            return ret;
        }
 
        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);
 
        // 释放资源
        av_packet_unref(pkt);
    }
 
    return 0;
}

3.10、资源回收

// 关闭文件    
inFile.close();   
 outFile.close();
 // 释放资源
 av_frame_free(&frame);
 av_packet_free(&pkt);
 avcodec_free_context(&ctx);

3.11、函数调用

AudioEncodeSpec in;
in.filename = "F:/in.pcm";
in.sampleRate = 44100;
in.sampleFmt = AV_SAMPLE_FMT_S16;
in.chLayout = AV_CH_LAYOUT_STEREO;
FFmpegs::aacEncode(in, "F:/out.aac");

十、AAC解码

1、命令行解码

ffmpeg -c:a libfdk_aac -i out.aac -f s16le out.pcm

2、编码进行解码

2.1、函数声明

// 解码后的PCM参数
typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioDecodeSpec;
 
class FFmpegs {
public:
    FFmpegs();
 
    static void aacDecode(const char *inFilename,
                          AudioDecodeSpec &out);
};

2.2、函数实现

// 解码后的PCM参数
typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioDecodeSpec;
 
class FFmpegs {
public:
    FFmpegs();
 
    static void aacDecode(const char *inFilename,
                          AudioDecodeSpec &out);
};

2.3、获取解码器

// 获取解码器
codec = avcodec_find_decoder_by_name("libfdk_aac");
if (!codec) {
    qDebug() << "decoder libfdk_aac not found";
    return;
}

2.4、初始化解析器上下文

// 初始化解析器上下文
parserCtx = av_parser_init(codec->id);
if (!parserCtx) {
    qDebug() << "av_parser_init error";
    return;
}

2.5、创建上下文

// 创建上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
    qDebug() << "avcodec_alloc_context3 error";
    goto end;
}

2.6、创建AVPacket

// 创建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
    qDebug() << "av_packet_alloc error";
    goto end;
}

2.7、创建AVFrame

// 创建AVFrame
frame = av_frame_alloc();
if (!frame) {
    qDebug() << "av_frame_alloc error";
    goto end;
}

2.8、打开解码器

// 打开解码器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "avcodec_open2 error" << errbuf;
    goto end;
}

2.9、打开文件

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error:" << inFilename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error:" << out.filename;
    goto end;
}

2.10、解码

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error:" << inFilename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error:" << out.filename;
    goto end;
}

具体的解码操作在decode函数中。

static int decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }
 
    while (true) {
        // 获取解码后的数据
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }
        // 将解码后的数据写入文件
        outFile.write((char *) frame->data[0], frame->linesize[0]);
    }
}

2.11、设置输出参数

// 设置输出参数
out.sampleRate = ctx->sample_rate;
out.sampleFmt = ctx->sample_fmt;
out.chLayout = ctx->channel_layout;

2.12、释放资源

end:    
inFile.close();    
outFile.close();
av_frame_free(&frame);
av_packet_free(&pkt);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);

2.13、函数调用

AudioDecodeSpec out;
out.filename = "F:/out.pcm";
FFmpegs::aacDecode("F:/in.aac", out);
// 44100
qDebug() << out.sampleRate;
// s16
qDebug() << av_get_sample_fmt_name(out.sampleFmt);
// 2
qDebug() << av_get_channel_layout_nb_channels(out.chLayout);

备注:以上是使用FFmpeg音频录制、播放、编码和解码相关介绍。


十一、lame代码

1、音频转换-caf转为mp3

#pragma mark - 转换caf为mp3
//转换caf为mp3
-(void)transformCafToMP3{
    //在录制caf文件时,需要使用双通道,否则在转换为MP3格式时,声音不对。caf录制端的设置为:
    NSMutableDictionary * recordSetting = [NSMutableDictionary dictionary];
    [recordSetting setValue :[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];//
    [recordSetting setValue:[NSNumber numberWithFloat:8000.0] forKey:AVSampleRateKey];//采样率
    [recordSetting setValue:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey];//声音通道,这里必须为双通道
    [recordSetting setValue :[NSNumber numberWithInt:16] forKey: AVLinearPCMBitDepthKey];//线性采样位数
    [recordSetting setValue:[NSNumber numberWithInt:AVAudioQualityMin] forKey:AVEncoderAudioQualityKey];//音频质量
    
    //在转换mp3端的代码为:
    NSString *urlStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    
    cafUrlStr = [urlStr stringByAppendingPathComponent:kRecordAudioCafFile];//caf文件路径
    mp3UrlStr = [urlStr stringByAppendingPathComponent:kRecordAudioMP3file];//存储mp3文件的路径
    
    @try {
        int read, write;
        
        FILE *pcm = fopen([cafUrlStr cStringUsingEncoding:1], "rb");  //source 被转换的音频文件位置
        fseek(pcm, 4*1024, SEEK_CUR);                                   //skip file header
        FILE *mp3 = fopen([mp3UrlStr cStringUsingEncoding:1], "wb");  //output 输出生成的Mp3文件位置
        
        const int PCM_SIZE = 8192;
        const int MP3_SIZE = 8192;
        short int pcm_buffer[PCM_SIZE*2];
        unsigned char mp3_buffer[MP3_SIZE];
        
        lame_t lame = lame_init();
        lame_set_in_samplerate(lame, 8000.0);
        lame_set_VBR(lame, vbr_default);
        lame_init_params(lame);
        
        do {
            read = (int)fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
            if (read == 0)
                write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
            else
                write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
            
            fwrite(mp3_buffer, write, 1, mp3);
            
        } while (read != 0);
        
        lame_close(lame);
        fclose(mp3);
        fclose(pcm);
    }
    @catch (NSException *exception) {
        NSLog(@"%@",[exception description]);
    }
    @finally {
        
    }
}

注意:本文只用于个人记录和学习,原文请参考:秒懂音视频开发
源码下载请参考:CoderMJLee/audio-video-dev-tutorial

你可能感兴趣的:(音视频-FFmpeg音频录制、播放、编码和解码(上))