音视频开发之旅——音频基础概念、交叉编译原理和实践(LAME的交叉编译)(iOS)

本文主要讲解的是音频基础概念交叉编译原理和实践(LAME的交叉编译) ,是基于iOS平台,示例代码如下所示:

iOSAudioDemo

另外,Android平台也有相关的文章,如下所示:

音视频开发之旅------音频基础概念、交叉编译原理和实践(LAME的交叉编译)(Android)

音频基础概念

在进行音频开发的之前,了解声学的基础还是很有必要的。

声音的物理性质

在初中物理的时候学过,声音是由三要素 组成:音调响度音色

音调

声音的高低 叫做音调 。物体振动得越快,发出声音的音调就越高;物体振动得越慢,发出的音调越低。频率过零率 ,指信号的符号变化的比率)决定了音调,频率越高,波长越短,声音更容易绕过障碍物,也就是能量衰减越小,反之得到相反的结论。

响度

声音的强弱 叫做响度 。我们可以一般用分贝(dB)来描述响度,分贝越大,声音响度越大,反之得到相反的结论。

音色

声音的品质 叫做音色 ,它反映了每个物体发出的声音特有的品质。例如在同样的音调和响度下,吉他和钢琴的声音听起来是不同的,也就是音色是不同的。波的形状决定声音的音色,吉他和钢琴音色不同就是因为它们介质产生的波形不同。

业界来说,人耳能够听到频率范围大约为20Hz~20kHz ,对3kHz~4kHz 频率范围内的声音比较敏感,对于较低或者较高频率的声音,人耳的敏感度会减弱;在分贝较低时,听觉的频率特性会很不均匀,反之就会较为均匀。一个频率范围较宽的音乐,最佳的分贝范围为80dB~90dB超过90dB 就会损害人耳,105dB是人耳的极限。

声音在不同的介质传播的速度也会不一样,在空气中的传播速度为340m/s,不过在真空是无法传播的。

有时候我们在空旷的地方或者高山大喊的时候,会听到回声(echo) ,产生回声的原因是声音在传播的过程中遇到障碍物后反弹回来后再次让我们听到,但是如果这两种声音传回到我们耳朵的时差小于80毫秒的话,我们就无法分辨这两种声音。

音频数字化

将声音模拟信号转换为数字信号的过程称之为音频数字化 ,这里需要经过三个步骤:采样量化编码

采样

首先对模拟信号进行采样,采样是指在时间轴(横轴)对信号进行数字化,根据奎斯特定理(采样定理 ,我们要按比声音最高音频高两倍以上 的频率对声音进行采样,这个过程也称为AD转换 。上面提过的人耳能够听到的频率为20Hz~20kHz ,所以一般采样频率为44.1kHz ,也就是说1秒会采样44100次

量化

上面提到的,具体每个采样需要怎样处理呢?这就需要量化 ,量化是指在幅度轴(纵轴)上对信号进行数字化,要注意的是,和上面提到的采样形成平面直角坐标系 ,举个例子:用16bit的二进制信号表示这个声音的一个采样,16bit等于一个short,表示范围为[-32768, 32767],也就是说有65536个可能取值,所以在幅度上分为65536层。

编码

最后一步就是要将采样的数据进行存储,也就是是需要进行编码 ,编码就是按照一定的格式记录采样和量化后的数据数据,例如:顺序存储压缩存储 等等。常用的格式为音频的裸数据格式 ,也就是脉冲编码调制(Pulse Code Modulation,简称PCM) 。描述一段PCM的数据需要这几个概念:采样率(sampleRate)量化格式(sampleFormat,也称为位深度)声道数(channel)比特率 用于衡量音频数据单位时间内的容量大小,也就是一秒时间内的比特数目,我们以常见的CD格式和DVD-Audio格式为例子:

CD格式的采样率为44100Hz,量化格式为16bit(2byte),声道数为2,那么它的比特率为:

44100 * 16 * 2 = 1411200bps

转换可得1411200bps / 1024 = 1378.125Kibps

DVD-Audio格式的采样率为96000Hz,量化格式为24bit(3byte),声道数为6.那么它的比特率为:

96000 * 24 * 6 = 13824000bps

转换可得13824000bps / 1024 = 13500Kibps,再转换可得13500Kibps / 1024 ≈ 13.18Mibps

一般来说一首歌曲的时间大概在4分钟左右,那我们算下CD格式和DVD-Audio格式会占用多大的存储空间,如下所示:

CD格式:1411200bps * 4 * 60 = 338688000b,转换可得338688000b / 8 / 1024 / 1024 ≈ 40.37MiB

DVD-Audio格式:13824000bps * 4 * 60 = 3317760000b,转换可得3317760000b / 8 / 1024 / 1024 = 395.51MiB

由数据可得,DVD-Audio格式一秒时间内的比特数目大于CD格式,因此它的音质会更好,当然所占的储存空间也会相应得大。

压缩编码

由上面可以看到一首歌如果仅仅是已CD格式去存储的已经占用了40.37MiB,如果只是存储在存储设备上(例如:硬盘或者光盘)那还可以接受,但是如果在网络上实时在线传输的话,这样的大小实在是太大了,所以我们需要对其进行压缩编码 ,压缩编码里有个指标叫做压缩比 ,压缩比是小于1,压缩比越小(越接近0),丢失的信息就越多,反之得出相反的结论。压缩算法有两种:无损压缩有损压缩。无损压缩是指解压后的数据能够复原;有损压缩是指解压后的数据不能够复原,压缩导致的丢失得越多,还原的失真就越大。

有如下常用的压缩编码格式:

WAV编码

WAV(Waveform Audio File Format)是微软专门为Windows开发的一种编码格式,它会在PCM数据格式的前面加上44字节,分别用来描述该PCM数据的采样率、声道数、量化格式。

优点:音质非常好,有大量软件支持。

缺点:占用的存储空间较大。

适用场合:多媒体开发的中间文件、音乐和音效素材。

MP3编码

MP3(MPEG-1或者MPEG-2 Audio Layer III)是一种有损压缩 的编码格式,它通过舍弃PCM数据人类听觉不重要的部分,已达到压缩成较小文件的目的,对于大多数用户来说,它的音质和不压缩的音频没有明显的下降。我们常用LAME编码MP3文件,下面会讲解到。

优点:音质在**高码率(≥128Kbit/s)**表现不错,同时压缩比也比较高;有大量硬件和软件支持,兼容性不错。

适用场合:高码率(≥128Kbit/s)的音频。并且需要比较好的兼容性。

AAC编码

AAC(Advanced Audio Coding,高级音频编码)是一种高压缩比 的编码格式,由于采用多声道和使用低复杂性的描述方式,使其比几乎所有的传统编码方式在同规格的情况下更胜一筹。目前衍生出LC-AACHE-AAC v1HE-AAC v2 三种主要的编码格式。LC-AAC是比较传统的AAC,主要编码中高码率(≥80Kbit/s)的音频;HE-AAC v1是高效AAC,是对AAC的扩展,它使用 频段复制(SBR)提高频域的压缩效率,适用于中低码率(≤80Kbit/s) ;HE-AAC v2结合使用了**频段复制(SBR) 参数立体声(PS)提高立体声信号的压缩效率,进一步降低了对码率的需要(接近于50%),主要编码低码率(≤48Kbit/s)**的音质。大部分编码器都设置为≤48Kbit/s自动启用PS,>48Kbit/s就关闭PS,箱单与HE-AAC v1。

优点:音质在**中低码率(<128Kbit/s)**表现优异,多用于视频中音频轨的编码。

适用场合:中低码率(<128Kbit/s)的音频,多用于视频中音频轨的编码。

Ogg编码

Ogg 在各种码率下都有优秀的表现,尤其在中低码率的场景表现不错,同时它不收到软件专利的限制,完全免费。Ogg有着非常出的的算法,可以用更小的码率编码出更好的音质,举个例子:128Kbit/s的Ogg音质甚至比192Kbit甚至更高的MP3还要好。

优点:可以用更小的码率编码出更好的音质,在各种码率下都变现优异。

缺点:目前兼容性不够好,流媒体特性不支持。

适用场合:语音聊天的音频消息。

iOS平台增加C和C++支持

相比于Android,iOS对C和C++支持简单很多。如果我们使用Objective-C 开发iOS,由于Objective-C语法支持混编所以我们只需要把引用C++的Objective-C类的后缀名改为.mm(Objective-C类的正常后缀名是.m),就可以和C++一起编译。下面介绍一些基础概念:

GNU、GCC、gcc、g++

  • GNU:它是一个完全自由的操作系统,起源于GNU计划。

  • GCC:GNU Compiler Collection(GNU编译器套件)的缩写,它是一组GNU操作系统中的编译器集合,可以用于编译C、C++、Java、Go等语言。

  • gcc:GCC中的GNU C Compiler(C编译器)。

  • g++:GCC中的GNU C++ Compiler(C++编译器)。

对于.c文件和.cpp文件,gcc会分别当作c文件和cpp文件编译,而g++会统一当作cpp文件编译。

编译C/C++的四个步骤

接下来我们要了解一下使用gcc(GNU Compiler Collection,GNU编译器套件)生成可执行二进制文件的大概过程:

预处理(Preprocess)

预处理(Preprocess) :预处理会处理一些编译前的准备工作,把一些#define的宏定义完成文本替换,然后将#include里的文件复制到.cpp文件,如果.h文件里还有.h文件,那么就会递归展开,要注意的是,在这一步中,代码注释会被忽略。通过g++ -E命令将.c文件预处理为.i文件,它是文本文件。

编译(Compile)

编译(Compile) :编译是把代码转换成汇编代码,同时检查词法规则和语法规则,如果没有出现语法错误,那么不管逻辑是否错误都不会报错。通过g++ -S命令将.i文件转换为.s文件,它是文本文件。

汇编(Assemble)

汇编(Assemble) :汇编是把汇编代码(.s文件)转换为机器码。通过g++ -c命令将.s文件转换为.o文件(目标文件),它是二进制格式。

链接(Link)

C/C++代码经过汇编后生成的.o文件(目标文件),它是二进制文件,但是它不是最终可执行的,需要和系统组件(例如:标准库、动态链接库)链接起来才能得到可执行的二进制文件(Executable File),完成这个过程的组件叫做链接器(Linker) 。链接分为静态链接动态链接 ,生成的文件叫做静态库动态库

静态库

静态库在Linux下为.a文件,在Windows下为.lib文件。之所以称之为静态库,是因为在链接阶段会将.o文件和引用到的库一起链接打包到可执行文件中,它有如下特点:

  • 会在编译时期完成静态库对函数库的链接。

  • 程序在运行的时候与函数无关,方便移植。

  • 会浪费一定的空间和资源,因为所有目标文件和涉及到的函数库被链接合成一个可执行文件。

动态库

动态库在Linux下为.so文件,在Windows下为.dll文件。动态库在程序编译时不会被链接到目标文件,而是在程序运行时才被载入,它有如下特点:

  • 把对一些库函数的链接载入推迟到程序运行的时期。

  • 不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,可以实现进程之间的资源共享,节省了空间。

  • 由于动态库是在程序运行的时候才载入,因此解决了静态库对程序的更新、部署和发布带来的麻烦,只需要更新动态库就可以了,即增量更新

  • 开发者可以在程序代码中控制链接载入,即显示调用

Make

Make其实是一个批量处理 的工具,它是通过调用makefile文件中开发者指定的命令来进行编译链接,例如调用gcc或者其他编译器的命令。

本机编译

我们要在PC上运行一个二进制的程序(要注意的是,是以源码的方式进行编译,而不是以包管理器的方式去安装)会经过如下步骤:

  1. 得到这段程序的源代码,它可以是自己编写的源代码,也可以是从第三方开源网站上下载的源代码。

  2. 在PC上编译链接这些源代码生成可执行文件。

  3. 在终端(Terminal)下执行该可执行文件。

总结就是使用本机器的编译器和链接器,将源代码编译链接成一个可以在本机器运行的程序,这个编译过程叫做本机编译,它是正常的编译过程。

交叉编译

了解完本机编译后,交叉编译 就好理解了,它就是一个平台(例如:PC)上生成另外一个平台(例如:Android、iOS、其他嵌入式设备)可执行的程序。这里的编译机器 是PC,所以编译器 是安装在PC上,并且运行在PC上的,而这个编译器叫做交叉工具编译链。那其实为啥需要交叉编译呢?因为运行程序的目标平台运算能力和存储能力都是有限的,尽管现在iOS和Android设备的性能越来越强劲,但是和PC还是有一定的距离,而且ARM平台下的编译工具和整个编译过程异常繁琐,所以PC是最佳选择。目前大部分的嵌入式开发平台都提供本身平台交叉编译所需要运行在PC上的交叉工具编译链。

在所有的编译器中,包括自行安装在PC上的编译器和嵌入式平台的交叉工具编译链,都包含以下这几个工具:

  • CC:编译器,作用是对C或者C++源文件编译成汇编文件。

  • AS:将汇编文件翻译成机器码,生成目标文件,汇编文件使用的是指令助记符。

  • AR:打包器,它可以从一个库增加或者删除目标代码模块。

  • LD:链接器,作用是为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者可执行文件。

  • GDB:调试工具,它可以对正在运行的程序进行代码调试。

  • STRIP:消除最终生成的库文件或者可执行文件其中的源码。

  • NM:查看静态库文件中符号表。

  • Objdump:查看静态库或者动态库的方法签名。

由于在安装iOS的开发环境Xcode的时候,配套的编译器已经安装好了,所以我们就不需要再单独下载交叉工具编译链。

LAME的交叉编译

我们了解完交叉编译后,以LAME库为例进行实践。

先介绍一下LAME库,它是目前最优秀也是最常用的MP3编码引擎。当码率达到320Kbit/s以上的时候,LAME编码出来的音频质量几乎可以和CD音质媲美,并且还能保证其文件体积非常小,因此如果要在移动端编码MP3文件,使用LAME是唯一选择。

下面来讲解下,在iOS平台下如何交叉编译LAME库,并且打印LAME版本。

下载LAME库并解压

然后在SourceForge 下载最新版本的LAME库,目前为3.100,点击下面文本即可下载:

lame-3.100.tar.gz

下载完成后,解压文件得到lame-3.100文件夹。

编写编译脚本

由于苹果在Xcode 14 Release Notes 中声明构建iOS项目不再支持armv7armv7si386指令集。

Building iOS projects with deployment targets for the armv7, armv7s, and i386 architectures is no longer supported. (92831716)

所以我们只需要编译arm64x86_64 指令集下的版本,其中x86_64的版本用于在模拟器上运行。下面来编写相关的脚本,相关的代码已经push到示例代码中,代码如下所示:

arm64指令集下的编译脚本

ios_lame_build_arm64.sh

shell 复制代码
./configure \
--disable-shared \
--disable-frontend \
--host=arm-apple-darwin \
--prefix="/Users/tanjiajun/lame-3.100/output/arm64" \
CC="xcrun -sdk iphoneos clang -arch arm64" \
CFLAGS="-arch arm64 -miphoneos-version-min=11.0" \
LDFLAGS="-arch arm64 -miphoneos-version-min=11.0"
make clean
make -j8
make install

x86_64指令集下的编译脚本

ios_lame_build_x86_64.sh

shell 复制代码
./configure \
--disable-shared \
--disable-frontend \
--host=x86_64-apple-darwin \
--prefix="/Users/tanjiajun/lame-3.100/output/x86_64" \
CC="xcrun -sdk iphonesimulator clang -arch x86_64" \
CFLAGS="-arch x86_64 -mios-simulator-version-min=11.0" \
LDFLAGS="-arch x86_64 -mios-simulator-version-min=11.0"
make clean
make -j8
make install

下面解释下这些命令和选项的含义:

configure 是符合GNU 标准的软件包发布所必备的命令,所以这里通过configure 的方式生成Makefile 文件,然后使用makemake install编译和安装整个库。

  • make-clean:清除上次make命令产生的.o文件和可执行文件。

  • make -j8:根据Makefile 文件执行编译四个步骤,也就是预处理编译汇编链接 ,最后生成可执行文件。make -j 可以带一个参数,用于进行并行编译j8 是指让make同时执行最多八个命令。

  • make install:将编译成功的可执行文件安装到系统目录中,一般为/usr/local/bin目录。

我们可以使用configure -h 命令查看configure的帮助文档,同时了解LAME的可选配置项。

  • --disable-shared:GNU标准中用于关闭动态链接库,通常是在编译出命令行工具的时候,期望命令行工具可以单独使用而不需要动态链接库的配置。

  • --disable-frontend:不编译出LAME的可执行文件。

  • --host:指定最终生成的库要运行的平台,arm64 指令集指定的是arm-apple-darwinx86_64 指令集指定的是x86_64-apple-darwin

  • --prefix:指定最终生成的库要放在哪个目录下,通常来说,这个是GNU大部分库的标准配置。

  • CC:指定交叉工具编译链的路径,在这里就是指定gcc的路径。

  • CFLAGS:指定编译时所带的参数。-arch 用于指定最终生成的库运行的目标平台,要注意的是,这个选项只在Darwin 平台有效,也就是只在Mac电脑 有效,如果不是该平台可以使用-march。另外,由于苹果在Xcode 14 Release Notes 中声明弃用bitcode,在这之前是需要添加-fembed-bitcode选项,它用于打开bitcode,现在不需要了。同时指定了编译出来这个库所支持的最低iOS版本为11.0。

  • LDFLAGS:指定链接过程中的参数。参数含义和上面所述的CFLAGS一样,这里就不再赘述。

bitcode(苹果已弃用)

以前是把所有的指令集的源码编译好,然后全部打包到一个APP,开启bitcode后,开发者提交APP到App Store的时候,Xcode会将程序编译为一个中间表现形式 ,只需要上传这个中间件(Intermediate Representation) ,而不是最终的可执行文件,从而减少二进制包的大小;在用户下载APP之前,App Store会根据用户设备的指令集自动编译中间件,然后产生设备所需要的可执行文件提供给用户下载安装;以后如果有新指令集的CPU,就可以继续从这份bitcode编译出这个CPU上的可执行文件提供给用户下载安装。

Starting with Xcode 14, bitcode is no longer required for watchOS and tvOS applications, and the App Store no longer accepts bitcode submissions from Xcode 14.

Xcode no longer builds bitcode by default and generates a warning message if a project explicitly enables bitcode: "Building with bitcode is deprecated. Please update your project and/or target settings to disable bitcode." The capability to build with bitcode will be removed in a future Xcode release. IPAs that contain bitcode will have the bitcode stripped before being submitted to the App Store. Debug symbols can only be downloaded from App Store Connect / TestFlight for existing bitcode submissions and are no longer available for submissions made with Xcode 14. (86118779)

脚本编写完成后,存储到lame-3.100 文件夹下,然后通过下面这两条命令修改这两个脚本的文件权限为可执行权限,命令如下所示:

shell 复制代码
chmod 777 /Users/tanjiajun/lame-3.100/ios_lame_build_arm64.sh
chmod 777 /Users/tanjiajun/lame-3.100/ios_lame_build_x86_64.sh

执行完毕后运行脚本,命令如下所示:

shell 复制代码
/Users/tanjiajun/lame-3.100/ios_lame_build_arm64.sh
/Users/tanjiajun/lame-3.100/ios_lame_build_x86_64.sh

执行完毕后,可以在lame-3.100 文件夹中的output 文件下看到两个文件夹分别为arm64x86_64 ,这两个文件夹下会看到includelibshare 文件夹,由于在配置的时候裁剪了可执行文件,所以不会产生bin 文件夹,include 文件夹为编译过程中所需要引用的lame.h头文件lib 文件夹为链接过程中所需要链接的libmp3lame.a静态库文件

至此我们编译出arm64x86_64 这两个指令集下的头文件静态库文件

合并静态库

我们需要将上面两个指令集下的静态库文件合并到一个libmp3lame.a静态库文件中,可以通过如下命令实现:

shell 复制代码
lipo -create /Users/tanjiajun/lame-3.100/output/arm64/lib/libmp3lame.a /Users/tanjiajun/lame-3.100/output/x86_64/lib/libmp3lame.a -output libmp3lame.a

执行完毕后就可以在output 文件夹下看到libmp3lame.a静态库文件,然后我们在该文件夹下执行如下命令:

shell 复制代码
file libmp3lame.a

执行完毕后,如果看到以下信息,说明编译成功:

shell 复制代码
libmp3lame.a: Mach-O universal binary with 2 architectures: [x86_64:current ar archive random library] [arm64:current ar archive random library]
libmp3lame.a (for architecture x86_64):	current ar archive random library
libmp3lame.a (for architecture arm64):	current ar archive random library

Xcode准备工作

我们在Xcode新建一个SwiftUI的项目,执行以下步骤:

  1. 新建一个lame 文件夹,导入上面生成的lame.h头文件 和合并后的libmp3lame.a静态库文件

  2. 因为我们需要用Swift 调用C++文件,所以要创建桥接文件iOSAudioDemo-Bridging-Header.h ,并且在工程中Build Settings 中的Objective-C Bridging Header 设置文件名为iOSAudioDemo-Bridging-Header.h

引入lame.h头文件,代码如下所示:

c++ 复制代码
//
//  iOSAudioDemo-Bridging-Header.h
//  iOSAudioDemo
//
//  Created by 谭嘉俊 on 2024/3/7.
//

#ifndef iOSAudioDemo_Bridging_Header_h
#define iOSAudioDemo_Bridging_Header_h

#import "iOSAudioDemo/lame/lame.h"

#endif /* iOSAudioDemo_Bridging_Header_h */

打印LAME库的版本

我们用SwiftUI写界面,代码如下所示:

swift 复制代码
//
//  iOSAudioDemoApp.swift
//  iOSAudioDemo
//
//  Created by 谭嘉俊 on 2024/3/5.
//

import SwiftUI

@main
struct iOSAudioDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

调用get_lame_version 函数就能获取当前LAME版本,ContentView代码如下所示:

swift 复制代码
//
//  ContentView.swift
//  iOSAudioDemo
//
//  Created by 谭嘉俊 on 2024/3/5.
//

import SwiftUI

struct ContentView: View {
    
    // 调用get_lame_version函数就能获取当前LAME版本
    let lameVersion: String = String(cString: get_lame_version())
    
    var body: some View {
        NavigationView {
            Text(lameVersion)
                .navigationTitle("iOSAudioDemo")
        }
    }
}

#Preview {
    ContentView()
}

运行后,我们就可以看到界面有个居中的3.100文本,这就是目前编译的LAME版本,代表我们编译LAME库成功。

我的GitHub: TanJiaJunBeyond

Android通用框架: Android通用框架

我的掘金: 谭嘉俊

我的简书: 谭嘉俊

我的CSDN: 谭嘉俊

相关推荐
web1350858863516 分钟前
前端node.js
前端·node.js·vim
m0_5127446417 分钟前
极客大挑战2024-web-wp(详细)
android·前端
若川26 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点41 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748238921 小时前
webgis入门实战案例——智慧校园
开发语言·ios·swift
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js