荏苒追寻个人博客

做一个有追求的青年


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 日程表

Android 音视频基础——音视频压缩原理

发表于 2024-05-09 | 分类于 Android , 音视频基础

https://rainmonth.github.io/posts/A240509.html

摘要

音视频相关开发时,如果碰到音频文件或视频文件较大时,通常需要对文件进行压缩,那么音视频文件为什么可以压缩、以什么方法方式压缩等,这些都是音视频压缩需要解决的问题,这篇文章通过从各方面搜集资料来,来大致了解音视频文件压缩的原理,从而加深自己对音视频开发的理解。

所谓压缩,应该就是将文件中冗余的信息(如无关紧要的、或者人无法识别的)去掉,或者以一种更节省空间的方式来重新编码文件。也就是需要从两个方面来入手:

  • 根据使用场景,确定哪些是冗余信息,并通过技术手段见冗余信息去掉;

  • 找到一种更合适的算法,来缩小音视频内容占用的空间。

音频压缩的原理

在了解音频压缩原理之前,先介绍一下音频相关的几个概念:采样和采样频率、采样位数、声道、码率、音频采集和播放。

采样和采样频率

音频采样 指的是 将现有录音的一部分作为音色或片段,直接或经过处理、重建再运用在新作品中的过程。

一秒钟内采样的次数称为采样频率。采样频率越高,越接近原始信号,但是也加大了运算处理的复杂度。根据Nyquist采样定理,要想重建原始信号,采样频率必须大于信号中最高频率的两倍。人能感受到的频率范围为20HZ——20kHZ, 一般音乐的采样频率为44.1kHZ, 更高的可以是48kHZ和96kHZ,不过一般人用耳听感觉不出差别了。语音主要是以沟通为主,不需要像音乐那样清晰,用16k采样的语音就称为高清语音了。现在主流的语音采样频率为16kHz。

采样位数

数字信号是用0和1来表示的。采样位数就是采样值用多少位0和1来表示,也叫采样精度,用的位数越多就越接近真实声音。如用8位表示,采样值取值范围就是-128——127,如用16位表示,采样值取值范围就是-32768——32767。现在一般都用16位采样位数。

声道

声道(Sound Channel) 是指声音在录制或播放时在不同空间位置采集或回放的相互独立的音频信号,所以声道数也就是声音录制时的音源数量或回放时相应的扬声器数量。

码率

码率就是比特率,即每秒传送的比特(bit)数。码率=采样率 x 采样位数 x 声道数。

音频采集和播放

一般用专门的芯片(通常叫codec芯片)采集音频,做AD转换(Analog-to-Digital Converter),然后把数字信号通过I2S总线(主流用I2S总线,也可以用其他总线,比如PCM总线)送给CPU处理(也有的会把codec芯片与CPU芯片集成在一块芯片中)。当要播放时CPU会把音频数字信号通过I2S总线送给codec芯片,然后做DA转换得到模拟信号再播放出来。

音频信号的冗余信息

假如现在有一个 双声道、采样率为44.1KHz、采样位数为16的音频流,那么它的码率就是:

1
2 * 44.1 * 1000 * 16  = 1411200 bit/s (约等于 1.411M/s) 

也就是说,这样的音频 每秒要传递 1.411M的数据,这将占用超级大的带宽,所以需要进行音频压缩。

数字音频压缩 指的就是 在保证音频信号在听觉方面不失真的情形下,对音频信号做最大程度的压缩。那么怎么实现呢?数字音频压缩 采用的是 去除声音信号中冗余成分的方法来实现。这里的冗余成分指的是 音频中不能被人耳感知到的信号,它们对确定声音的音色,音调等信息没有任何的帮助。

冗余信号包含人耳听觉范围外的音频信号以及被掩蔽掉的音频信号等。例如,人耳所能察觉的声音信号的频率范围为20Hz~20KHz,除此之外的其它频率人耳无法察觉,都可视为冗余信号。此外,根据人耳听觉的生理和心理声学现象,当一个强音信号与一个弱音信号同时存在时,弱音信号将被强音信号所掩蔽而听不见,这样弱音信号就可以视为冗余信号而不用传送。这就是人耳听觉的掩蔽效应,主要表现在频谱掩蔽效应和时域掩蔽效应,现分别介绍如下:

频谱掩蔽效应

一个频率的声音能量小于某个阈值之后,人耳就会听不到,这个阈值称为最小可闻阈。当有另外能量较大的声音出现的时候,该声音频率附近的阈值会提高很多,即所谓的掩蔽效应。

时域掩蔽效应

当强音信号和弱音信号同时出现时,还存在时域掩蔽效应。即两者发生时间很接近的时候,也会发生掩蔽效应。时域掩蔽过程曲线如图所示,分为前掩蔽、同时掩蔽和后掩蔽三部分。

  • 前掩蔽是指人耳在听到强信号之前的短暂时间内,已经存在的弱信号会被掩蔽而听不到;

  • 同时掩蔽是指当强信号与弱信号同时存在时,弱信号会被强信号所掩蔽而听不到;

  • 后掩蔽是指当强信号消失后,需经过较长的一段时间才能重新听见弱信号,称为后掩蔽;

音频压缩编码的方法

当前数字音频编码领域存在着不同的编码方案和实现方式, 但基本的编码思路大同小异, 如图所示。

对每一个音频声道中的音频采样信号,首先都要将它们映射到频域中,这种时域到频域的映射可通过子带滤波器实现。每个声道中的音频采样块首先要根据心理声学模型来计算掩蔽门限值, 然后由计算出的掩蔽门限值决定从公共比特池中分配给该声道的不同频率域中多少比特数,接着进行量化以及编码工作,最后将控制参数及辅助数据加入数据之中,产生编码后的数据流。

视频压缩的原理

视频信号的冗余信息

以记录数字视频的YUV分量格式为例,YUV分别代表亮度与两个色差信号。例如对于现有的PAL制电视系统,其亮度信号采样频率为13.5MHz;色度信号的频带通常为亮度信号的一半或更少,为6.75MHz或3.375MHz。以4:2:2的采样频率为例,Y信号采用13.5MHz,色度信号U和V采用6.75MHz采样,采样信号以8bit量化,则可以计算出数字视频的码率为:

1
13.5*8 + 6.75*8 + 6.75*8= 216Mbit/s

如此大的数据量如果直接进行存储或传输将会遇到很大困难,因此必须采用压缩技术以减少码率。数字化后的视频信号能进行压缩主要依据两个基本条件:

  • 数据冗余。例如如空间冗余、时间冗余、结构冗余、信息熵冗余等,即图像的各像素之间存在着很强的相关性。消除这些冗余并不会导致信息损失,属于无损压缩。

  • 视觉冗余。人眼的一些特性比如亮度辨别阈值,视觉阈值,对亮度和色度的敏感度不同,使得在编码的时候引入适量的误差,也不会被察觉出来。可以利用人眼的视觉特性,以一定的客观失真换取数据压缩。这种压缩属于有损压缩。

数字视频信号的压缩正是基于上述两种条件,使得视频数据量得以极大的压缩,有利于传输和存储。一般的数字视频压缩编码方法都是混合编码,即将变换编码,运动估计和运动补偿,以及熵编码三种方式相结合来进行压缩编码。通常使用变换编码来消去除图像的帧内冗余,用运动估计和运动补偿来去除图像的帧间冗余,用熵编码来进一步提高压缩的效率。下文简单介绍这三种压缩编码方法。

压缩编码的方法

变化编码

变换编码的作用是将空间域描述的图像信号变换到频率域,然后对变换后的系数进行编码处理。一般来说,图像在空间上具有较强的相关性,变换到频率域可以实现去相关和能量集中。常用的正交变换有离散傅里叶变换,离散余弦变换等等。数字视频压缩过程中应用广泛的是离散余弦变换。

离散余弦变换简称为DCT变换,离散余弦变换的一般步骤:分块、块DCT变换、对DCT变化后的块信息进行量化、对量化结果的非0部分进行压缩编码。

  • 分块,即将视频的帧图像划分成若干个大小相等的块(块中数字表示图像像素的亮度)。

  • 块DCT变化

    通过上图可以看出,经过DCT变换后,左上角的低频系数集中了大量能量,而右下角的高频系数上的能量很小。

  • 量化,信号经过DCT变换后需要进行量化。由于人的眼睛对图像的低频特性比如物体的总体亮度之类的信息很敏感,而对图像中的高频细节信息不敏感,因此在传送过程中可以少传或不传送高频信息,只传送低频部分。量化过程通过对低频区的系数进行细量化,高频区的系数进行粗量化,去除了人眼不敏感的高频信息,从而降低信息传送量。因此,量化是一个有损压缩的过程,而且是视频压缩编码中质量损伤的主要原因。

量化过长时有一个公式的,经过量化后,途中的块区域大多数的值都变成了0,既可以舍弃的部分,接下来只需要对非0部分进行压缩就可以了。

  • 量化后的非零部分压缩
熵编码

熵编码是因编码后的平均码长接近信源熵值而得名。熵编码多用可变字长编码(VLC,Variable Length Coding)实现。其基本原理是对信源中出现概率大的符号赋予短码,对于出现概率小的符号赋予长码,从而在统计上获得较短的平均码长。可变字长编码通常有霍夫曼编码、算术编码、游程编码等。其中游程编码是一种十分简单的压缩方法,它的压缩效率不高,但编码、解码速度快,仍被得到广泛的应用,特别在变换编码之后使用游程编码,有很好的效果。

首先要在量化器输出直流系数后对紧跟其后的交流系数进行Z型扫描(如图箭头线所示)。Z型扫描将二维的量化系数转换为一维的序列,并在此基础上进行游程编码。最后再对游程编码后的数据进行另一种变长编码,例如霍夫曼编码。通过这种变长编码,进一步提高编码的效率。

运动估计和运动补偿

运动估计(Motion Estimation)和运动补偿(Motion Compensation)是消除图像序列时间方向相关性的有效手段。

上文介绍的DCT变换、量化、熵编码的方法是在一帧图像的基础上进行,通过这些方法可以消除图像内部各像素间在空间上的相关性。实际上图像信号除了空间上的相关性之外,还有时间上的相关性。例如对于像新闻联播这种背景静止,画面主体运动较小的数字视频,每一幅画面之间的区别很小,画面之间的相关性很大。对于这种情况我们没有必要对每一帧图像单独进行编码,而是可以只对相邻视频帧中变化的部分进行编码,从而进一步减小数据量,这方面的工作是由运动估计和运动补偿来实现的。

运动估计技术一般将当前的输入图像分割成若干彼此不相重叠的小图像子块,例如一帧图像的大小为1280720,首先将其以网格状的形式分成4045个尺寸为16*16的彼此没有重叠的图像块,然后在前一图像或者后一个图像某个搜索窗口的范围内为每一个图像块寻找一个与之最为相似的图像块。这个搜寻的过程叫做运动估计。通过计算最相似的图像块与该图像块之间的位置信息,可以得到一个运动矢量。这样在编码过程中就可以将当前图像中的块与参考图像运动矢量所指向的最相似的图像块相减,得到一个残差图像块,由于残差图像块中的每个像素值很小,所以在压缩编码中可以获得更高的压缩比。这个相减过程叫运动补偿。

混合编码

混合编码的一般流程如下:

当前输入的图像首先要经过分块,分块得到的图像块要与经过运动补偿的预测图像相减得到差值图像X,然后对该差值图像块进行DCT变换和量化,量化输出的数据有两个不同的去处:一个是送给熵编码器进行编码,编码后的码流输出到一个缓存器中保存,等待传送出去。另一个应用是进行反量化和反变化后的到信号X’,该信号将与运动补偿输出的图像块相加得到新的预测图像信号,并将新的预测图像块送至帧存储器。

总结

本文主要介绍了音频视频压缩的基本原理,涉及到了音视频压缩的一些基本概念,这为之后学习音视频开发做一个预热,不至于后面谈到音视频压缩以及音视频压缩相关概念而摸不着北。

Android 对象池的使用

发表于 2024-05-07 | 分类于 Android

摘要

探讨了在短时间内频繁创建对象可能导致内存抖动和GC频繁的问题,提出对象池技术作为解决方案。对象池是一种预先创建并存储对象的技术,它允许重复利用对象而非每次都进行new操作,从而减少性能损耗。文中介绍了实现对象获取与释放的Pool<T>接口及其两个具体实现类:非线程安全的SimplePool和线程安全的SynchronizedPool。SimplePool采用数组存储对象,按需初始化,并提供懒加载机制;而SynchronizedPool则在此基础上增加了同步锁以确保多线程环境下的安全性。最后总结了对象池的优势和适用场景,强调对于包含大量资源初始化工作的对象以及多线程环境中,使用对象池可以节省资源初始化时间,但也指出了其可能带来的状态恢复开销和线程同步成本。

为什么会有对象池

在日常开发的时候,常常会遇到需要段时间内频繁创建对象的这种情形,会频繁出现 new 操作,这就会导致 内存抖动,从而导致 GC 频繁发生,进而产生性能上的问题,比如 UI卡顿等。这个时候使用 对象池技术就可以一定程度上解决这个问题。

所谓对象池,就是一个存放对象的池子,既然是池子,肯定会有自己的最大容量,即最多可以放多少个对象,还有就是有方法从池子中获取对象,同样也有方法往池子中放入对象。

放入对象 或取出对象的实现

对象的获取和对象的释放通过 Pool<T>接口来实现,具体接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Pool<T> {

/**
* 从对象池中获取一个对象
*/
@Nullable
T acquire();

/**
* 释放一个对象(将对象重新放回对象池)
*
* @param instance 对象实例
* @return 对象是否成功放入到了对象池
*
* @throws IllegalStateException 如果对象已经在池子里就抛出异常
*/
boolean release(@NonNull T instance);
}

简单看一下其实现类,共有两个:SimplePool 和 SynchronizedPool,前者未采用锁(即多线程不安全的),后者对取对象和放入对象都进行了锁操作,是线程安全的。

SimplePool

对象池的简单实现,实现了Pool<T>接口,用一个数组承载对象,并采用了懒加载的方式(不是一开始就创建 若干个对象,而是在获取之前需要先向 池子 中添加对象)代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public static class SimplePool<T> implements Pool<T> {
private final Object[] mPool;

private int mPoolSize;

/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public SimplePool(int maxPoolSize) {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("The max pool size must be > 0");
}
mPool = new Object[maxPoolSize];
}

@Override
@SuppressWarnings("unchecked")
public T acquire() {
if (mPoolSize > 0) {
final int lastPooledIndex = mPoolSize - 1;
T instance = (T) mPool[lastPooledIndex];
mPool[lastPooledIndex] = null;
mPoolSize--;
return instance;
}
return null;
}

@Override
public boolean release(@NonNull T instance) {
if (isInPool(instance)) {
throw new IllegalStateException("Already in the pool!");
}
if (mPoolSize < mPool.length) {
mPool[mPoolSize] = instance;
mPoolSize++;
return true;
}
return false;
}

private boolean isInPool(@NonNull T instance) {
for (int i = 0; i < mPoolSize; i++) {
if (mPool[i] == instance) {
return true;
}
}
return false;
}
}

说明:

  1. 根据SimplePool的实现,如果一开始就调用 acquire()方法,返回的肯定是空对象,这说明要使用 SimplePool 的话,需要先调用 release(@NonNull T instance)方法将对象先放入到对象池中;

  2. SimplePool中的对象并不是一次性都创建出来的,而是每次外部调用 release(@NonNull T instance)才加入到内存中的;

  3. 不在对象池中的对象才能调用release放入对象池中,否则会抛出IllegalStateException;

SynchronizedPool

这个类继承自 SimplePool,和它的主要区别就是对 acquire 和 release 操作加锁了,所以如果多线程中使用对象池,最好使用这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Synchronized) pool of objects.
*
* @param <T> The pooled type.
*/
public static class SynchronizedPool<T> extends SimplePool<T> {
private final Object mLock = new Object();

public SynchronizedPool(int maxPoolSize) {
super(maxPoolSize);
}

@Override
public T acquire() {
synchronized (mLock) {
return super.acquire();
}
}

@Override
public boolean release(@NonNull T element) {
synchronized (mLock) {
return super.release(element);
}
}
}

总结

使用对象池有什么好处呢,其实我觉得这个玩意跟线程池是差不多的,只不过线程池 功能更明确,只产生线程对象(因为线程的创建是十分昂贵的)。同理,对于对象池,如果对象中存在较多的资源初始化工作,这个时候对象复用很有必要,毕竟节省了大量的资源初始化时间,但是如果对象有很多 运行时需要改变的状态,就不建议使用对象池了,因为如果对象有很多运行时改变的状态,那么在重新放入对象池或者重新从对象池取出使用时,就会存在状态的恢复开销(已经使用过的对象需要reset到初始态)。

另外,多线程场景中使用对象池也是有代价的,需要进行线程同步操作,这部分的操作可能比 使用对象池带来的收益还大,得不偿失。

Android 知识点速记

发表于 2024-04-17 | 分类于 Android

Application

Handler

RecyclerView

HarmonyOS开发之真机调试

发表于 2024-03-18 | 分类于 Harmony

摘要

由于鸿蒙模拟器开发时糟糕的体验,决定搞定鸿蒙真机调试,虽然这个真机调试相对于Android来说有点复杂(这套配置有点类似于苹果的真机测试),但为了开发效率,还是抽时间查阅资料,研究了一下,这里记录一下。

整体过程如下:

  1. 使用DevEco Studio生成证书请求文件;

  2. 申请应用调试证书和Profile

  3. 在DevEco Studio中配置签名信息

阅读全文 »

通用功能——Markdown常用格式说明

发表于 2024-02-29 | 分类于 通用

写技术总结的时候,发现自己要用Markdown语法表示一下常用的数学公式,于是便总结了一下一些常用的数学方面的内容(如矩阵、方程、极限等)在Markdown语法上的实现,当然,文章不断在不断完善与整理中…

Markdown表示数学公式,主要分行内数学公式,和块数学公式,表现形式如下:

1
$行内公式内容$

行间数学公式

1
2
3
$$
块公式内容
$$
阅读全文 »

通用功能——矩阵的操作

发表于 2024-02-28 | 分类于 通用

写这篇文章,是为了重温一下还给老师的数学知识,加上前段时间 Android 开发过程中自定义View时设计到矩阵的变换,就找了一下资料,重新复习复习,内容大多数来自百度百科,记录下来一是熟悉一下概念,另外是熟悉下Markdown下数学公式的书写。

概念

由 $m \times n$ 个数$a_{ij}$排成的m行n列的数表称为$m$行$n$列的矩阵,简称$m\times n$矩阵。记作:

这$m \times n$ 个数称为矩阵$A$的元素,简称为元,数$a_{ij}$位于矩阵$A$的第$i$行第$j$列,称为矩阵$A$的$(i,j)$元,以数 $a_{ij}$为$(i,j)$元的矩阵可记为$(a_{ij})$或$(a_{ij})m \times n$,$m\times n$矩阵$A$也记作$Amn$。

元素是实数的矩阵称为实矩阵,元素是复数的矩阵称为复矩阵。而行数与列数都等于n的矩阵称为n阶矩阵或n阶方阵

基本运算

矩阵的基本运算包括加法,减法,数乘,转置,共轭和共轭转置。

阅读全文 »

通用功能——Mac自定义脚本并全局运行

发表于 2024-02-05 | 分类于 通用

摘要

移动项目开发工作中总是有一些固定的工作流程,比如:

  1. 本地分支正在工作中,需要拉去远程分支的代码;

  2. 本地分支正在工作中,需要将之前提交而未push的代码推送到远程分支;

  3. 想快速启动App;

  4. …

这些都是很常用的操作,作为一个coder,这些固定的流程肯定有程序化的解决思路,如是就想着用shell脚本来完成这个自动化操作的过程。

这个功能之前自己在另一台Mac上实现过,但是换电脑后,自己想重新实现一下结果鼓捣了好久,好在最后还是搞出来了,这里姑且记录下,免得日后忘记了自己有得在网上找了。

阅读全文 »

Python 小工具——二维码生成

发表于 2023-12-13 | 分类于 Python

背景

工作中经常需要用到生成二维码,然后通过扫码解析码中的内容做Schema协议跳转,刚好正在学习python,就用python高了一个二维码生成的工具,十分简单,也十分有趣,目前只有功过代码运行生成并展示二维码,后期学习Python GUI时尝试做一个带界面的,想想就舒服。

开始

要生成二维码,我就搜了下需要使用哪些包,发现还挺多的,其中 pyqrcode感觉挺不错的,官网介绍如下地址如下:PyQrCode 介绍。使用文档有介绍,展示二维码只需要两行代码。

先安装需要的包:pyqrcode

1
pip install pyqrcode

由于后面需要转换成png图片显示出来,这里还需要安装一个包,即 pypng

1
pip install git+https://gitlab.com/drj11/pypng@pypng-0.20220715.0
1
2
3
4
5
import pyqrcode


code = pyqrcode.create('要生成二维码的文本')
code.show()# 展示二维码

上面的create方法定义如下:

1
2
def create(content, error='H', version=None, mode=None, encoding=None):
    return QRCode(content, error, version, mode, encoding)

调用的时候只需要传递 content内容即可了,方法会放回一个QRCode对象。QRCode对象对外暴露的方法有:

  • show(),展示生成的二维码对象;

  • get_png_size(),获取png文件的大小;

  • png(),将二维码保存为png文件;

  • png_as_base64_str(),将二维码生成的png图片转换成base64 字符串;

  • xbm(),将二维码转换成 X Bitmap,作为bitmap对象方便在Tinker中使用;

  • svg(),将二维码转换成 svg图片;

  • eps(),将二维码转换成eps文档格式;

  • terminal(),返回一个ASCII码转义过的字符串,如果终端支持,将在终端中直接打印出二维码;

  • text(),返回代表二维码的文本(用0和1表示);

注意:这里用了代理模式,上面的方法都是通过QRBuilder类来实现的。

总结

用Python做一些小工具还是比较方便快捷的,毕竟“人生苦短,我用Python”不是吹的。

新人体验

发表于 2023-10-26 | 分类于 Android

新人体验

打开H5新人体验

绘本单本

  1. 绘本单本75759
  2. 绘本单本69663

听书单本

  1. 听书单本36563
  2. 听书单本36504

绘本合辑

  1. 绘本合辑60649
  2. 绘本合辑66301

听书合辑

  1. 听书合辑20314

  2. 听书合辑26921

AI课程详情页

  1. AI课程90224

视频课程指定课节

  1. 视频课程92095

电子书

电子书10025

电子书10447

漫画

  1. 漫画10077

漫画指定章节

  1. 漫画10070带章节id1258

阅读计划楼梯

  1. 阅读计划1000000

阅读计划任务

  1. 阅读计划1000000任务id10039

名师精讲详情页

  1. 名师精讲课程92096

打开名师精讲-单元

  1. 名师精讲课程92096单元112

小白学理财——开篇

发表于 2023-10-13 | 分类于 理财

https://rainmonth.github.io/posts/F2102190.html

  • [ ] 小白学理财——开篇
  • [ ] 小白学理财——股票仓位管理
  • [ ] 小白学理财——股票基础知识
  • [ ] 小白学理财——基金基础知识
  • [ ] 小白学理财——可转债
<i class="fa fa-angle-left" aria-label="上一页"></i>123…22<i class="fa fa-angle-right" aria-label="下一页"></i>

216 日志
43 分类
43 标签
GitHub
© 2025 Randy Zhang
由 Hexo 强力驱动
|
主题 — NexT.Gemini v6.1.0