一、前言
本系列文章是对音视频技术入门知识的整理和复习,为进一步深入系统研究音视频技术巩固基础。文章列表:
- 01-📝音视频技术核心知识|了解音频技术【移动通信技术的发展、声音的本质、深入了解音频】
- 02-📝音视频技术核心知识|搭建开发环境【FFmpeg与Qt、Windows开发环境搭建、Mac开发环境搭建、Qt开发基础】
- 03-📝音视频技术核心知识|Qt开发基础【
.pro
文件的配置、Qt控件基础、信号与槽】 - 04-📝音视频技术核心知识|音频录制【命令行、C++编程】
- 05-📝音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】
- 06-📝音视频技术核心知识|音频重采样【音频重采样简介、用命令行进行重采样、通过编程重采样】
- 07-📝音视频技术核心知识|AAC编码【AAC编码器解码器、编译FFmpeg、AAC编码实战、AAC解码实战】
- 08-📝音视频技术核心知识|成像技术【重识图片、详解YUV、视频录制、显示BMP图片、显示YUV图片】
- 09-📝音视频技术核心知识|视频编码解码【了解H.264编码、H.264编码、H.264编码解码】
- 10-📝音视频技术核心知识|RTMP服务器搭建【流媒体、服务器环境】
二、播放PCM
2.1 ffplay
可以使用ffplay播放《音频录制02_编程》中录制好的PCM文件,测试一下是否录制成功。
播放PCM需要指定相关参数:
- ar:采样率
- ac:声道数
- f:采样格式
- s16le:PCM signed 16-bit little-endian
- 更多PCM的采样格式可以使用命令查看
- Windows:ffmpeg -formats | findstr PCM
- Mac:ffmpeg -formats | grep PCM
ffplay -ar 44100 -ac 2 -f s16le out.pcm
接下来演示一下,如何通过编程的方式播放PCM数据。
2.2 SDL
ffplay是基于FFmpeg、SDL两个库实现的。通过编程的方式播放音视频,也是需要用到这2个库。FFmpeg大家都已经清楚了,比较陌生的是SDL。
2.2.1 简介
SDL(Simple DirectMedia Layer),是一个跨平台的C语言多媒体开发库。
- 支持Windows、Mac OS X、Linux、iOS、Android
- 提供对音频、键盘、鼠标、游戏操纵杆、图形硬件的底层访问
- 很多的视频播放软件、模拟器、受欢迎的游戏都在使用它
- 目前最新的稳定版是:2.0.14
- API文档:wiki
2.2.2 下载
SDL官网下载地址:download-sdl2。
2.2.2.1 Windows
由于我们使用的是MinGW编译器,所以选择下载SDL2-devel-2.0.14-mingw.tar.gz。
解压后的目录结构如下图所示,跟FFmpeg的目录结构类似,因此就不再赘述每个文件夹的作用。
2.2.2.2 Mac
从brew官网可以看得出来:之前执行brew install ffmpeg时,已经顺带安装了SDL,安装目录是:/usr/local/Cellar/sdl2。
如果没有这个目录,就执行brew install sdl2进行安装即可。
2.3 HelloWorld
来个简单的SDL HelloWorld吧,打印一下SDL的版本号。
2.3.1 .pro
文件
win32 {
FFMPEG_HOME = F:/Dev/ffmpeg-4.3.2
SDL_HOME = F:/Dev/SDL2-2.0.14/x86_64-w64-mingw32
}
macx {
FFMPEG_HOME = /usr/local/Cellar/ffmpeg/4.3.2
SDL_HOME = /usr/local/Cellar/sdl2/2.0.14_1
}
INCLUDEPATH += $${FFMPEG_HOME}/include
LIBS += -L$${FFMPEG_HOME}/lib \
-lavdevice \
-lavcodec \
-lavformat \
-lavutil
INCLUDEPATH += $${SDL_HOME}/include
LIBS += -L$${SDL_HOME}/lib \
-lSDL2
在Windows环境中,还需要处理一下dll文件,参考:《dll文件处理》。
2.3.2 cpp
代码
#include <SDL2/SDL.h>
SDL_version v;
SDL_VERSION(&v);
// 2 0 14
qDebug() << v.major << v.minor << v.patch;
2.4 播放PCM
2.4.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.4.2 打开音频设备
/* 一些宏定义 */
// 采样率
#define SAMPLE_RATE 44100
// 采样格式
#define SAMPLE_FORMAT AUDIO_S16LSB
// 采样大小
#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.4.3 打开文件
#define FILENAME "F:/in.pcm"
// 打开文件
QFile file(FILENAME);
if (!file.open(QFile::ReadOnly)) {
qDebug() << "文件打开失败" << FILENAME;
// 关闭音频设备
SDL_CloseAudio();
// 清除所有初始化的子系统
SDL_Quit();
return;
}
2.4.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.4.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.4.6 释放资源
// 关闭文件
file.close();
// 关闭音频设备
SDL_CloseAudio();
// 清理所有初始化的子系统
SDL_Quit();
三、PCM转WAV
播放器是无法直接播放PCM的,因为播放器并不知道PCM的采样率、声道数、位深度等参数。当PCM转成某种特定的音频文件格式后(比如转成WAV),就能够被播放器识别播放了。
本文通过2种方式(命令行、编程)演示一下:如何将PCM转成WAV。
1. WAV文件格式
在进行PCM转WAV之前,先再来认识一下WAV的文件格式。
- WAV、AVI文件都是基于RIFF标准的文件格式
- RIFF(Resource Interchange File Format,资源交换文件格式)由Microsoft和IBM提出
- 所以WAV、AVI文件的最前面4个字节都是RIFF四个字符
找遍了全网,并没有找到令我十分满意的WAV文件格式图,于是按照自己的理解画了一张表格,个人觉得还是极其通俗易懂的。
每一个chunk(数据块)都由3部分组成:
- id:chunk的标识
- data size:chunk的数据部分大小,字节为单位
- data,chunk的数据部分
整个WAV文件是一个RIFF chunk,它的data由3部分组成:
- format:文件类型
- fmt chunk
- 音频参数相关的chunk
- 它的data里面有采样率、声道数、位深度等参数信息
- data chunk
- 音频数据相关的chunk
- 它的data就是真正的音频数据(比如PCM数据)
RIFF chunk除去data chunk的data(音频数据)后,剩下的内容可以称为:WAV文件头,一般是44字节。
四、PCM转WAV
1. 命令行
通过下面的命令可以将PCM转成WAV。
ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm out.wav
需要注意的是:上面命令生成的WAV文件头有78字节。对比44字节的文件头,它多增加了一个34字节大小的LIST chunk。
关于LIST chunk的参考资料:
加上一个输出文件参数*-bitexact*可以去掉LIST Chunk。
ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm -bitexact out2.wav
2. 编程
在PCM数据的前面插入一个44字节的WAV文件头,就可以将PCM转成WAV。
2.1 WAV的文件头结构
WAV的文件头结构大概如下所示:
#define AUDIO_FORMAT_PCM 1
#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 <QFile>
#include <QDebug>
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
接下来演示一下如何使用SDL播放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;
如果想要轻松加载MP3、Ogg、FLAC等格式的音频文件,可以使用第三方库:SDL_mixer。
3. 打开音频设备
// 打开设备
if (SDL_OpenAudio(&spec, nullptr)) {
qDebug() << "SDL_OpenAudio error:" << SDL_GetError();
// 释放文件数据
SDL_FreeWAV(data);
// 清除所有的子系统
SDL_Quit();
return;
}
开始播放
// 开始播放(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;
}
4. 回调函数
// 等待音频设备回调(会回调多次)
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;
}
5. 释放资源
// 释放WAV文件数据
SDL_FreeWAV(data);
// 关闭设备
SDL_CloseAudio();
// 清除所有的子系统
SDL_Quit();
专题系列文章
1. 前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、属性
、方法
、swift多态原理
、String
、Array
、Dictionary
、引用计数
、MetaData
等Swift基本语法和相关的底层原理文章有如下几篇:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
4. C++核心语法
- 01-C++核心语法|C++概述【C++简介、C++起源、可移植性和标准、为什么C++会成功、从一个简单的程序开始认识C++】
- 02-📝C++核心语法|C++对C的扩展【::作用域运算符、名字控制、struct类型加强、C/C++中的const、引用(reference)、函数】
- 03-📝C++核心语法|面向对象1【 C++编程规范、类和对象、面向对象程序设计案例、对象的构造和析构、C++面向对象模型初探】
- 04-📝C++核心语法|面向对象2【友元、内部类与局部类、强化训练(数组类封装)、运算符重载、仿函数、模板、类型转换、 C++标准、错误&&异常、智能指针】
- 05-📝C++核心语法|面向对象3【 继承和派生、多态、静态成员、const成员、引用类型成员、VS的内存窗口】
5. Vue全家桶
- 01-📝Vue全家桶核心知识|Vue基础【Vue概述、Vue基本使用、Vue模板语法、基础案例、Vue常用特性、综合案例】
- 02-📝Vue全家桶核心知识|Vue常用特性【表单操作、自定义指令、计算属性、侦听器、过滤器、生命周期、综合案例】
- 03-📝Vue全家桶核心知识|组件化开发【组件化开发思想、组件注册、Vue调试工具用法、组件间数据交互、组件插槽、基于组件的
- 04-📝Vue全家桶核心知识|多线程与网络【前后端交互模式、promise用法、fetch、axios、综合案例】
- 05-📝Vue全家桶核心知识|Vue Router【基本使用、嵌套路由、动态路由匹配、命名路由、编程式导航、基于vue-router的案例】
- 06-📝Vue全家桶核心知识|前端工程化【模块化相关规范、webpack、Vue 单文件组件、Vue 脚手架、Element-UI 的基本使用】
- 07-📝Vue全家桶核心知识|Vuex【Vuex的基本使用、Vuex中的核心特性、vuex案例】
6. 音视频技术核心知识
- 01-📝音视频技术核心知识|了解音频技术【移动通信技术的发展、声音的本质、深入了解音频】
- 02-📝音视频技术核心知识|搭建开发环境【FFmpeg与Qt、Windows开发环境搭建、Mac开发环境搭建、Qt开发基础】
- 03-📝音视频技术核心知识|Qt开发基础【
.pro
文件的配置、Qt控件基础、信号与槽】 - 04-📝音视频技术核心知识|音频录制【命令行、C++编程】
- 05-📝音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】
- 06-📝音视频技术核心知识|音频重采样【音频重采样简介、用命令行进行重采样、通过编程重采样】
- 07-📝音视频技术核心知识|AAC编码【AAC编码器解码器、编译FFmpeg、AAC编码实战、AAC解码实战】
- 08-📝音视频技术核心知识|成像技术【重识图片、详解YUV、视频录制、显示BMP图片、显示YUV图片】
- 09-📝音视频技术核心知识|视频编码解码【了解H.264编码、H.264编码、H.264编码解码】
- 10-📝音视频技术核心知识|RTMP服务器搭建【流媒体、服务器环境】
其它底层原理专题
1. 底层原理相关专题
2. iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案