你知道如何用Python对图片和音频进行格式检测、以及格式转换吗

楔子

现在图像识别、语音识别之类的项目越来越火,通过机器学习训练出一套模型,再将图像、音频之类的上传上去,就可以识别图像上的内容、将音频转成文字等等。

但有的我们训练的模型可能不够完善,对格式限制的比较死,比如图片只接收 png 格式,音频只接收 mp3 格式等等。这个时候我们就需要判断用户的上传的文件格式了,如果不是我们希望的格式,那么就直接返回文件格式错误,提示用户重新上传指定格式的文件。

当然如果用户上传的格式不符合要求,更智能的做法应该是将文件自动转成我们希望的格式,或者训练出一套支持多种格式的模型等等,当然这都是后话了,我们目前要解决的就是判断用户上传的文件到底是什么格式的。格式判断的话,很明显不能使用后缀名进行判断,一个 png 格式的图片即便我们将后缀改成 jpg,它还是可以正常显示的,并且实际的格式仍然是 png,因为这张图片底层对应的字节流没有任何变化;音频文件也是同样的道理,将 wav 改成 mp3 也是可以播放的,但它的格式仍然是 wav。

总之:通过文件后缀来判断一个文件的格式是不准确的;通过修改文件后缀来试图改变文件格式也是没有用的。

好的,下面就来看看如何判断 图片、以及音频的格式。

检测图片

图片的检测还是比较简单的,我们只需要读取一张图片的前 32 个字节(其实也用不到 32 个)即可判断它的种类。

图片的类型主要分为以下几种:jpeg,png、gif、tiff、rgb、pbm、pgm、ppm、rast、xmb、bmp、webp、exr。

def delete_picture(file):
    """
    检测图片的类型
    :param file: 路径
    :return: 
    """
    # 读取前 32 个字节
    data = open(file, "rb").read(32)

    if data[6:10] in (b'JFIF', b'Exif'):
        return 'jpeg'

    elif data.startswith(b'\211PNG\r\n\032\n'):
        return 'png'

    elif data[:6] in (b'GIF87a', b'GIF89a'):
        return 'gif'

    elif data[:2] in (b'MM', b'II'):
        return 'tiff'

    elif data.startswith(b'\001\332'):
        return 'rgb'

    elif len(data) >= 3 and data[0] == ord(b'P') and data[1] in b'14' and data[2] in b' \t\n\r':
        return 'pbm'

    elif len(data) >= 3 and data[0] == ord(b'P') and data[1] in b'25' and data[2] in b' \t\n\r':
        return 'pgm'

    elif len(data) >= 3 and data[0] == ord(b'P') and data[1] in b'36' and data[2] in b' \t\n\r':
        return 'ppm'

    elif data.startswith(b'\x59\xA6\x6A\x95'):
        return 'rast'

    elif data.startswith(b'#define '):
        return 'xbm'

    elif data.startswith(b'BM'):
        return 'bmp'

    elif data.startswith(b'RIFF') and data[8:12] == b'WEBP':
        return 'webp'

    elif data.startswith(b'\x76\x2f\x31\x01'):
        return 'exr'

    else:
        return None

虽然没有注释,但是仍然很好理解。另外 Python 内部还有一个标准库叫 imghdr,就是专门来解析图片类型的,我们上面的代码就是根据这个模块改的。

import imghdr

# 可以使用 imghdr.what 进行判断
# 如果是文件名的话, 直接传入文件名即可
print(imghdr.what("1.png"))  # png


# 如果是现成的字节流的话, 那么也是支持的
print(imghdr.what(None, bytes(b"\x59\xA6\x6A\x95")))  # rast

"""
第一个参数是文件名, 第二个参数是字节流
我们可以只传递一个文件名, 会读取文件的前 32 个字节进行判断; 也可以传入现成的字节流, 来进行判断
只不过第一个参数是必传的, 所以在使用字节流的时候,我们需要显式地给第一个参数传递一个None、或者随便一个内容进去, 不然会报函数参数错误
"""

# 如果没有匹配到指定的格式, 那么会返回None 
print(imghdr.what(None, bytes(b"xxxxxxxxxxxx")))  # None

当然,检测图片种类我们已经知道怎么做了,那转换呢?转换的话,我们可以借助一个模块,叫做 PIL,直接 pip install pillow 安装即可。

import imghdr
from PIL import Image

# 文件是 jpeg 格式的
print(imghdr.what("古明地觉.jpg"))  # jpeg

# 读取文件得到字节流
im = Image.open("古明地觉.jpg")
# 然后保存, 接收 保存的文件名 和 格式, 如果格式不传递的话, 那么会根据文件的扩展名进行判断
# 这里我们指定为 png 格式, 所以保存的就是 png 格式, 尽管我们文件的扩展名叫 gif
im.save("古明地觉.gif", "png")

print(imghdr.what("古明地觉.gif"))  # png

以上就是图片转化的方式,关于 PIL 这个模块,我也介绍过,非常的详细,可以去看看。

检测音频

音频的检测比较重要,并且音频还有采样率、采样深度、通道数等等,关于这方面的细节我们会慢慢说。后面还会介绍如何使用 Python 的 pydub 第三方库来设置音频属性,以及格式之间的转化。

检测 wav

wav 格式的音频非常简单,由文件头 + 音频数据组成,音频数据就是我们听到的具体内容了,而文件头则是描述这个音频的元信息。对应的结构体如下:

typedef struct {
    // wav文件标志, 值为 "RIFF", 一个固定字符;
    // 如果我们想判断一个音频文件是不是 wav 格式, 那么就看它的前四个字符是不是 "RIFF" 即可;
    char ChunkID[4];

    // 除了 ChunkID 和 ChunkSize 之外, 整个文件其它部分所占的字节数;
    // ChunkID 和 ChunkSize 显然都是 4 字节, 那么 ChunkSize 就是文件总字节数减去 8
    unsigned long ChunkSize;

    // 表示是 WAVE 文件, 值为 "WAVE" 一个固定字符 */
    char  Format[4];

    // 波形格式标志, 值为 "fmt ", 一个固定字符
    char  Subchunk1ID[4];

    // 数据格式, 一般为 16、32等等
    unsigned long  Subchunk1Size;

    // 压缩格式, 大于 1 表示有压缩, 等于 1 表示无压缩(PCM格式)
    unsigned short AudioFormat;

    // 声道数量
    unsigned short NumChannels;

    // 采样频率
    unsigned long  SampleRate;

    // 字节率, 等于 采样频率 * 声道数量 * 采样位数 / 8
    unsigned long  ByteRate;

    // 块对齐之后的大小, 也表示一帧的字节数, 等于 通道数 * 采样位数 / 8
    unsigned short BlockAlign;

    // 采样位数, 存储每个采样值所用的二进制数的位数, 一般是 4 8 12 16 24 32
    unsigned short BitsPerSample;
    
    // 数据标志位, 值为 "data", 一个固定字符
    char  Subchunk2ID[4];    
    
    // 实际的音频数据总字节数, 也就是整个音频文件的字节数 减去 文件头部的字节数
    unsigned long  Subchunk2Size; 
} WAVHeader;

以上便是 wav 文件的头部,而除了头部之外,剩下的部分就是实际的音频内容了。我们将上面的成员所占的字节数加起来,结果是 44,所以一个 wav 文件的头部大小是 44 字节。用一张图表示的话:

下面我们来看看如何使用 Python 操作 wav 数据:

# 以 rb 的模式打开, 读取里面的字节, 当然其实我们只需要读取一部分即可
data = open("上海アリス幻樂団 - 神々が恋した幻想郷.wav", "rb").read()
# 开头如果是 b'RIFF', 那么说明是 wav 文件, 非常简单
print(data[: 4])  # b'RIFF'

当然我们还可以获取其它的信息,由于是二进制流,我们需要使用 struct 模块。

import struct

data = open("上海アリス幻樂団 - 神々が恋した幻想郷.wav", "rb").read()
# 总字节数
print(len(data))  # 62934860

# 1. wav 文件标志
print(
    struct.unpack(">4s", data[: 4])[0]
)  # b'RIFF'

# 2. ChunkSize, 显然结果应该是 62934860 - 8 = 62934852
print(
    struct.unpack("<L", data[4: 8])[0]
)  # 62934852

# 3. Format, 一个固定字符 "WAVE"
print(
    struct.unpack(">4s", data[8: 12])[0]
)  # b'WAVE'

# 4. Subchunk1ID 一个固定字符
print(
    struct.unpack(">4s", data[12: 16])[0]
)  # b'fmt '

# 5. Subchunk1Size, 数据格式
print(
    struct.unpack("<L", data[16: 20])[0]
)  # 16

# 6. AudioFormat, 压缩格式
print(
    struct.unpack("<H", data[20: 22])[0]
)  # 1

# 7. NumChannels, 声道数量
print(
    struct.unpack("<H", data[22: 24])[0]
)  # 2

# 8. SampleRate, 采样频率
print(
    struct.unpack("<L", data[24: 28])[0]
)  # 44100

# 9. ByteRate, 字节率, 等于 采样频率 * 声道数量 * 采样位数 / 8
print(
    struct.unpack("<L", data[28: 32])[0]
)  # 176400

# 10. BlockAlign, 块对齐之后的大小, 也表示一帧的字节数, 等于 通道数 * 采样位数 / 8
print(
    struct.unpack("<H", data[32: 34])[0]
)  # 4

# 11. BitsPerSample, 采样位数, 存储每个采样值所用的二进制数的位数, 一般是 4 8 12 16 24 32
print(
    struct.unpack("<H", data[34: 36])[0]
)  # 16

# 12. Subchunk2ID, 数据标志位, 值为 "data", 一个固定字符
print(
    struct.unpack(">4s", data[36: 40])[0]
)  # b'data'

# 13. Subchunk2Size, 音频文件的字节数 减去 文件头部的字节数, 62934860 - 44 = 62934816
print(
    struct.unpack("<L", data[40: 44])[0]
)  # 62934816

怎么样,是不是很简单呢?当然一个一个获取有点太麻烦了,我们可以写在一起。虽然每个成员规定了大小端存储方式,但我们在解析的时候可以不指定。

from collections import namedtuple
import struct

data = open("上海アリス幻樂団 - 神々が恋した幻想郷.wav", "rb").read()

WavHeader = namedtuple("WavHeader",
                        ["ChunkID", "ChunkSize", "Format", "Subchunk1ID", "Subchunk1Size",
                         "AudioFormat", "NumChannels", "SampleRate", "ByteRate", "BlockAlign",
                         "BitsPerSample", "Subchunk2ID", "Subchunk2Size"])


values = struct.unpack("4s L 4s 4s L H H L L H H 4s L", data[: 44])
wav_header = WavHeader(*values)
print(wav_header)
"""
WavHeader(ChunkID=b'RIFF', ChunkSize=62934852, Format=b'WAVE', Subchunk1ID=b'fmt ', 
          Subchunk1Size=16, AudioFormat=1, NumChannels=2, SampleRate=44100, 
          ByteRate=176400, BlockAlign=4, BitsPerSample=16, Subchunk2ID=b'data', Subchunk2Size=62934816)
"""

print(f"采样率: {wav_header.SampleRate}")  # 采样率: 44100
print(f"声道数量: {wav_header.NumChannels}")  # 声道数量: 2

# 文件的总字节数, 除以每一帧的大小, 再除以采样率, 便可以得到时长
print(f"总时长: {len(data) / wav_header.BlockAlign / wav_header.SampleRate}")  # 总时长: 356.7735827664399

此时,关于 wav 音频的一些属性我们已经知道如何获取了,下面来看看关于音频的一些知识点。

PCM:

我们说 AudioFormat 表示压缩格式,其实也可以叫做编码格式,大于 1 表示有压缩,等于 1 表示无压缩(PCM编码格式)。PCM 编码是直接存储声波采样被量化后所产生的非压缩数据,所以它是单纯的无损耗编码格式,优点是可以获得高质量的音频信号。

基于 PCM 编码的 wav 格式是最基本的 wav 格式,被声卡直接支持,能直接存储采样的声音数据。其存储的数据能够直接通过声卡进行播放,还原的波形曲线与原始声音波形十分接近,播放质量是一流的,在 Windows 上被支持的最好,常常被用于在其它编码的文件之间转换的中间文件。但 PCM 有一个缺点就是文件体积过大,不适合长时间记录。正因为如此,又出现了许多在 PCM 编码的基础上进行改进的编码格式,比如:DPCM、ADPCM 编码等等。

采样频率:

又被称作取样频率,是单位时间内的采样次数,决定了数字化音频的质量。采样频率越高,数字化音频的质量越好,还原的波形越完整,播放的声音越真实,当然所占的大小也就越大。根据奎特采样定理,要从采样中完全恢复原始信号的波形,采样频率要高于声音中最高频率的两倍。人耳可听到的声音的频率范围是在 16赫兹 到 20千赫兹 之间,因此要将听到的原声音真实地还原出来,采样频率必须大于 40千赫兹。而 44千赫兹 的音频可以达到 CD 的音质,当然可以更高,只不过高于 48 千赫兹 的采样频率人耳很难分别,没有实际意义。

采样位数:

也叫量化位数(单位:比特),是存储每个采样值所用的二进制位数,采样值反映了声音的波动状态,采样位数决定了量化精度。采样位数越长,量化的精度就越高,还原的波形曲线越真实,产生的量化噪音越小,回放的效果越真实。常用的量化位数有 4、8、12、16、24等等,量化位数与声卡的位数和编码有关。如果采用 PCM 编码同时使用 8 位声卡,可将音频信号幅度从上限到下限划分为 256 个音量等级,取值范围是 0 到 255;使用 16 为声卡,可将音频引号幅度划分为 64千 个音量等级,取值范围是 -32768 到 32767。

声道数:

使用的声音通道的个数,也是采样时所产生的声音波形个数。播放声音时,单声道的 wav 一般使用一个喇叭发声,立体声的 wav 可以使用两个喇叭发声。记录声音时,单声道每次产生一个波形的数据;双声道每次产生两个波形的数据,当然最终音频所占的存储空间也会增加一倍。

比特率:

比特率是指每秒传送的比特(bit)数,单位为 bps(Bit Per Second),比特率越高,传送的数据越大。在音频、视频领域,比特率又被称为码率、位率、位速(这四个老铁是同一个东西,只是不同领域、不同翻译造就了这么多的名词)。比特率表示经过编码(压缩)后的音、视频数据每秒钟需要用多少个比特来表示。比特率与音、视频压缩的关系,简单来说就是比特率越高,音频、视频的质量就越好,但编码后的文件就越大;如果比特率越少则情况刚好相反,比特率 = 采样频率 * 采样位数 * 声道数。

检测 mp3

MP3 的全称是 MPEG Audio Layer3,它是一种高效的计算机音频编码方案,以较大的压缩比将音频文件转换成较小的扩展名为 .mp3 的文件,基本保持原文件的音质。MP3 是 ISO / MPEG 标准的一部分,ISO / MPEG 标准描述了使用高性能感知编码方案的音频压缩,此标准一致在不断更新以满足 "质高量小" 的追求,现已形成 MPEG Layer1、Layer2、Layer3 三个音频编码解码方案。MPEG Layer3 的压缩率可以达到 10比1 到 12比1。如果 1M 的 MP3 文件可以播放一分钟,那么同样可以播放一分钟的CD音质的 wav文件(采样率44100赫兹、采样位数16、双声道)要占至少 10M。可能对于现在而言,10M没啥大不了的,但是当文件非常大的时候就体现出来了,因此 MP3 的优势是 CD 难以比拟的。

MP3 格式始于 80 年代中期,德国的一家研究所致力于研究高质量、低数据率的声音编码。MP3 音频压缩包含编码和解码两个部分,编码是将 wav 文件中的数据转换成高压缩率的位流形式,解码是接收位流并将其重建到 wav 文件中。

但是 MP3 对音频信号采用的是有损压缩的方式,为了降低声音失真度,MP3 采用了感知音频编码这一失真算法。即编码之前先对音频文件进行频谱分析,然后截掉大量的冗余信号和无关的信号,编码器通过混合滤波器组将原始声音变换到频率域,利用心理声学模型,估算刚好能被察觉到的噪声水平,再经过量化,转换成Huffman编码,形成MP3位流。而解码器要简单得多,它的任务是从编码后的谱线成分中,经过反量化和逆变换,提取出声音信号。

在压缩音频数据时,先将原始声音数据分成固定的分块,然后作顺向 MDCT 变换,MDCT 本身并不进行数据压缩,只是将一组时域数据转换成频域数据,以得知时域变化情况,顺向 MDCT 将每块的值转换为 512 个 MDCT 系数。量化使数据得到压缩,在对量化后的变换样值进行比特分配时要考虑使整个量化块最小,这就成为有损压缩了。解压时,经反向 MDCT 将 512 个系数还原成原始声音数据,前后的原始声音数据是不一致的,因为在压缩过程中,去掉了冗余和不相关数据。

 

MP3 文件大体分为三部分:TAG_V2(ID3V2)、Frame、TAG_V1(ID3V1)

ID3V2 和 ID3V1 都属于标签,用来记录 MP3 文件的信息的,只不过 ID3V1 现在很少用了,然后中间就是音频数据实体。

ID3V1

我们先来看看 ID3V1,因为它比较简单,主要是它的长度是固定的,并且位于文件的尾部。我们说 ID3V1 标签的长度固定为 128 字节,那么读取一个 MP3 文件之后,获取它的后 128 字节就是 ID3V1 标签了。当然在获取之前,我们肯定要看看它的底层结构:

typedef struct {
    char Header[3];         /* 标签头, 必须是 "TAG", 一个固定写死的字符串 */
    char Title[30];         /* 标题 */
    char Artist[30];        /* 作者 */
    char Album[30];         /* 专辑 */
    char Year[4];           /* 发行年 */
    char Comment[30];       /* 备注 */
    char Genre;             /* 类型 */
} ID3V1Tag;

每个成员的长度是写死的,如果长度不够用 \0 补齐。但其实 ID3V1 已经很少用了,所以我们重点关注 ID3V2,但是 ID3V1 标签的前三个字符一定是 "TAG"。

data = open("上海アリス幻樂団 - 神々が恋した幻想郷.mp3", "rb").read()
print(data[-128: -125])  # b'TAG'

ID3V2

然后我们来看看 ID3V2 标签,ID3V2 到目前为止总共有 4 个版本,但是主流的播放软件一般只支持第 3 版,即 ID3V2.3。由于 ID3V1 在文件的末尾,那么 ID3V2 只能记录在文件的首部。也正是由于这个原因,对 ID3V2 的操作比 ID3V1 要慢,而且 ID3V2 结构比 ID3V1 的结构要复杂得多,但是比 ID3V1 全面且可以伸缩和扩展。

每个 ID3V2.3 的标签都一个标签头 和 若干个标签帧或一个扩展标签头 组成,标签头记录版本以及整个 ID3v2.3 的大小,标签帧则是记录歌曲信息,比如标题、作者等都存放在不同的标签帧中。扩展标签头并不是必要的,但每个标签至少要有一个标签帧,标签头和标签帧一起顺序存放在 MP3 文件的首部。

1. 标签头

MP3 文件的前 10 个字节便是 ID3V2.3 的标签头,我们看一下结构体:

typedef struct {
    char Header[3];         /* 标签头, 必须是 "ID3", 一个固定写死的字符串 */
    char Ver;               /* 如果是 ID3V2.3, 值为 3; 如果是 ID3V2.4, 值为4 */
    char Revision;          /* 副版本号 */
    char Flag;              /* 存放标志的字节 */
    char Size[4];           /* 标签大小, 除去当前标签头的 10 个字节的剩余部分的大小 */
} ID3V2TagHeader;

所以我们如果想要判断一个音频文件是不是 mp3 格式,只需要检测前三个字符是不是 "ID3"、倒数第 128 个字符 到 倒数第126字符是不是 "TAG" 即可。

from collections import namedtuple
import struct

ID3V1TagHeader = namedtuple("ID3V1TagFrame", ["Header", "Ver", "Revision", "Flag",
                                             "Size"])
data = open("上海アリス幻樂団 - 神々が恋した幻想郷.mp3", "rb").read()
values = struct.unpack("3s b b b 4s", data[: 10])

header = ID3V1TagHeader(*values)
print(header)  # ID3V1TagFrame(Header=b'ID3', Ver=3, Revision=0, Flag=0, Size=b'\x00<f\x16')

但是我们看到 Size 貌似有点不对劲啊,这是因为它是一个字符串,而且大小虽然占 4 个字节,但是每个字节只用 7 个位,所以我们需要单独计算。

data = open("上海アリス幻樂団 - 神々が恋した幻想郷.mp3", "rb").read()
size = data[6: 10]
print(
    (size[0] & 0b1111111) * 2 ** 21 +
    (size[1] & 0b1111111) * 2 ** 14 +
    (size[2] & 0b1111111) * 2 ** 7 +
    (size[3] & 0b1111111)
)  # 996118

说实话个人也不清楚为什么要这么做,因为不是专门搞音频相关的。但是 Python 中的整数底层也是使用多个无符号 32 位整数进行存储,每个 32 位整数只用 30 个位,也许两者之间会有一些相似性。

2. 标签帧

标签由一个标签头和多个标签帧组成,而每个标签帧又由帧头和帧内容组成,帧头的定义如下:

typedef struct {
    /* 用四个字符标识一个帧, 表示这个帧是来描述啥的
    比如: TIT2 表示标题, TPE1 表示作者, TALB表示专辑, 
    TRCK表示音轨(格式: N/M, 其中 N 为专集中的第 N 首, M 为专集中共 M 首, N 和 M 为ascii码表示的数字)
    TYER表示年代(ascii表示的数字), TCON 表示类型, COMM 表示备注(格式: eng\O 备注内容, 其中eng表示备注使用的自然语言) 
    
    常用的就以上几个, 其它的可以自己网上搜索
    */
    char ID[4];

    /* 帧内容的大小, 这个很重要, 因为每一个标签帧之间是没有分隔符的, 所以必须知道具体大小 */
    /* 否则在读取标签帧的时候, 可以会读取到下一个标签帧, 这里的每一个字节 8 位全部用完 */
    char Size[4];

    /* 存放标志, 每个字节只用 6 位*/
    char Flags[2];         
} ID3V2TagFrameHeader;

可以看到每个帧头固定也是 10 个字节,然后是帧内容,两者组合形成一个标签帧,然后多个标签帧依次排在一起。

而不同的标签帧的帧头是一样的,都是 10 字节,但是帧内容则有大有小。而具体大小则由帧头的 Size 来确定,如果不知道 Size 的话,那么你就不知道应该读多少字节,因为当前帧的帧内容和下一帧的帧头是无缝连接的。小了读不完,大了就会读到下一帧。

Frame

Frame 是音频数据帧,也是有效数据帧,它位于 MP3 文件的中间部分,也是最重要的部分,因为我们听到的就是它。同理,每一个音频数据帧也由多个部分组成:

每一个音频数据帧都有一个帧头,长度是 4 个字节,帧后面可能有 2 字节的 CRC 校验,取决于帧头的第 16 位的值,为 0 无校验,为 1 则有校验(后面会说)。接下来是可变长度的附加信息,对于标准的 MP3 文件来说,其长度是 32 字节。最后就是压缩的声音数据了,不包含任何其它信息,就是单纯的声音数据,解码器读到此处就可以进行解码了。

然后我们看看帧头的定义,帧头只有 4 字节,但是它的成员却不止 4 个,所以在定义成员的时候指定了变量的宽度。

typedef struct {
    unsigned int sync: 11;              // 同步信息, 11 个位均为 1, 也就是恒为 FF
    unsigned int version: 2;            // 版本
    unsigned int layer: 2;              // 层
    unsigned int error_protection: 1;   // 是否进行 CRC 检验, 决定了帧头后面是否跟着之前说的那 2 字节
    unsigned int bit_rate_index: 4;     // 比特率(也叫位率、位速、码率) 索引
    unsigned int sample_rate_index: 2;  // 采样频率索引
    unsigned int padding: 1;            // 帧长调节
    unsigned int private: 1;            // 保留字
    unsigned int mode: 2;               // 声道模式
    unsigned int mode_extension: 2;     // 扩充模式
    unsigned int copyright: 1;          // 版权
    unsigned int original: 1;           // 原版标志
    unsigned int emphasis: 2;           // 强调方式
} DataFrameHeader;

解释一下上面的字段信息:

1. sync

同步信息,我们说 11 个位均为 1;说明第一个字节的 8 位均为 1,第二个字节至少前三位均为 1

2. version

版本,占两个位,即第二个字节的第 4 个位和第 5 个位

  • 00 代表 MPEG2.5
  • 01 代表 未定义
  • 10 代表 MPEG2
  • 11 代表 MPEG1

3. layer

层,占两个位,即第二个字节的第 6 个位和第 7 个位

  • 00 代表 未定义
  • 01 代表 Layer3
  • 10 代表 Layer2
  • 11 代表 Layer1

4. error_protection

是否进行 CRC 检验,占 1 个位,即第二个字节的第 8 个位

5. bit_rate_index

比特率索引,占 4 个位。对于 MPEG1 而言:

  • 0001 表示 32kbps 0010 表示 40kbps
  • 0011 表示 48kbps 0100 表示 56kbps
  • 0101 表示 64kbps 0110 表示 80kbps
  • 0111 表示 96kbps 1000 表示 112kbps
  • 1001 表示 128kbps 1010 表示 160kbps
  • 1011 表示 192kbps 1100 表示 224kbps
  • 1101 表示 256kbps 1110 表示 320kbps

6. sample_rate_index

采样频率,占 2 个位

对于 MPEG1 而言
    00 表示   44.1千赫兹   
    01 表示     48千赫兹   
    10 表示     32千赫兹
    11 表示       未定义

对于 MPEG2 而言
    00 表示  22.05千赫兹 
    01 表示     24千赫兹   
    10 表示     16千赫兹 
    11 表示       未定义

对于 MPEG2.5 而言        
    00 表示 11.025千赫兹  
    01 表示     12千赫兹
    10 表示      8千赫兹
    11 表示       未定义

7. padding

帧长调节,占 1 个位。用来调整文件长度,0表示无需调整,1表示调整。

8. private

保留字,占 1 个位

9. mode

声道模式,两位,第四个字节的前两个位

  • 00 表示立体声Stereo
  • 01 表示Joint Stereo
  • 10 表示双声道
  • 11 表示单声道

10. mode_extension

扩充模式,当声道模式为 01 时才使用。

Value      强度立体声      MS立体声
 00          off           off
 01          on            off
 10          off           on
 11          on            on

11. copyright

版权是否合法,0 表示不合法,1 表示合法

12. original

是否原版,0 表示非原版,1 表示原版

13. emphasis

强调方式,用于声音经降噪压缩后再补偿的分类,基本用不到

使用 pydub 库来检测音频属性

Python 自带一个 wave 标准库,它只能处理 wav 文件,所以如果想要支持更丰富的格式,实现更强大的编辑功能,还是离不开 ffmpeg。而 pydub 就是这样的库,提供了一个针对 ffmpeg 的高层接口,我们不需要关注 ffmpeg 的底层细节,pydub 都已经帮我们做好了。

这个库的安装直接 pip install pydub 即可,非常的简单,但是它依赖于 ffmpeg,这个我们是需要单独安装的,并且还要配置到环境变量,否则找不到。

打开一个文件

我们可以迅速打开一个音频文件,至于格式只要 ffmpeg 支持的就行,而 ffmpeg 基本支持所有主流的音频格式。

  • pydub.AudioSegment.from_mp3("1.mp3") 打开一个 mp3 文件
  • pydub.AudioSegment.from_wav("1.wav") 打开一个 wav 文件
  • pydub.AudioSegment.from_ogg("1.ogg") 打开一个 ogg 文件

以上所有接口都调用了 pydub.AudioSegment.from_file:

  • pydub.AudioSegment.from_file("1.mp3", "mp3")
  • pydub.AudioSegment.from_file("1.wav", "wav")
  • pydub.AudioSegment.from_file("1.ogg", "ogg")

注意:在调用的时候格式一定要匹配,否则报错。

import pydub

try:
    pydub.AudioSegment.from_wav("高梨康治 - 百鬼夜行.mp3")
except Exception as e:
    print(e)
"""
Decoding failed. ffmpeg returned error code: 1

Output from ffmpeg/avlib:

b'ffmpeg version 4.2 Copyright.........Invalid data found when processing input\r\n
"""

我们的音频是 mp3 格式的,但是却调用了 from_wav,所以会报错。当然千万不要自作聪明将文件扩展名改成 wav 就以为万事大吉了,我们说过格式取决于文件的字节流,因此在调用之前先判断一下。

import pydub

song = pydub.AudioSegment.from_mp3("高梨康治 - 百鬼夜行.mp3")
print(song)  # <pydub.audio_segment.AudioSegment object at 0x0000021782910C40>

返回的是一个 AudioSegment 对象,它就是音频读取之后的结果,通过该对象我们可以对音频进行各种操作,比如增加音量、淡入淡出等等。并且这些操作都是链式的,每一个操作都会返回一个新的对象,不会修改原来的对象。所以我们在操作的时候,可以一直写下去,song.xxx.xxx.xxx.xxx,不用每一次操作都重新赋值一个变量。

注意:pydub 做的任何操作,只要和时间相关,那么单位都是毫秒。

下面我们来看看它都支持哪些操作。

截取某一个片段

对音频进行切片,这是一个非常常用的操作,一个长音频,我们可能只要前 5 秒,或者后 5 秒等等。

# 截取前 5 秒
first_5_seconds = song[: 5 * 1000]

# 截取后 5 秒
last_5_seconds = song[-5000:]

返回的都是一个新的 AudioSegment 对象,保存之后正好是原始音频文件的前 5 秒和后 5 秒,关于保存文件后面会说。

音量增加和减小

我们可以让音量放大和缩小,并且实现起来也非常简单。

# 声音增大 9 分贝
first_5_seconds = first_5_seconds + 9

# 声音减小 7 分贝
last_5_seconds = last_5_seconds - 7

怎么样,是不是非常简单呢?

音频拼接

估计有人猜到做法了,没错,直接相加即可。

song_first_last = first_5_seconds + last_5_seconds

此时 song_first_last 就是由原始音频的前 5 秒放大 9 分贝,和原始音频的后 5 秒减小 7 分贝组合而成的新的音频(AudioSegment 对象)。

淡入淡出

song_first_last = first_5_seconds.append(last_5_seconds, crossfade=1500)

调用 append 也相当于将音频组合在一起,但是这种方式可以增加一些淡入淡出的效果。当然我们也可以手动实现:

song_first_last = first_5_seconds.fade_in(2000) + last_5_seconds.fade_out(3000)

前 5 秒和后 5 秒拼接起来得到 10 秒钟的音频,并且前 2 秒淡入,后 3 秒淡出。

重复

将一个片段重复 n 遍

repeat_5 = song[: 3000] * 5

这里就将前 3 秒重复了 5 遍,相当于 song[: 3000] 重复相加 5 次。

翻转音频

说白了就是倒放

song_reverse = song.reverse()

上面就是一些常见的操作,所以 pydub 在简单易用方面绝对是无法挑剔的。当然它的功能还不止这些,比如两个音频重叠播放,声道的分离等等,只不过我本人不是这领域相关的,所以用不到那么多的功能,有兴趣可以自己去查看官网。

得到音频的某一帧

获取音频的某一帧,个人觉得不常用。

song.get_frame(1)  # 获取第一帧

获取音频属性

下面我们就来获取音频的一些属性,像我们之前说的什么通道、采样频率之类的,然后保存成文件的时候也可以改变它的属性。

import pydub

song = pydub.AudioSegment.from_mp3("高梨康治 - 百鬼夜行.mp3")

# 声道数, 1 表示单声道, 2 表示双声道
print(song.channels)  # 2

# 采样宽度, 我们之前介绍了采样位数, 采样位数除以 8 就是采样为宽了, 因为一个字节有 8 位
# 同理采样宽度乘以 8 就是采样位数
print(song.sample_width)  # 2
print(song.sample_width * 8)  # 16

# 采样频率, 采样频率等于帧速率
print(song.frame_rate)  # 44100

# 块对齐之后的大小, 或者一帧的字节数, 等于 通道数 * 采样位数 / 8, 或者 通道数 * 采样宽度
print(song.frame_width)  # 4
print(song.channels * song.sample_width)  # 4

# 字节率, 等于 采样频率 * 声道数量 * 采样宽度(采样位数 / 8), 可以直接计算得到
print(song.frame_rate * song.channels * song.sample_width)  # 176400

# 时长(单位秒)
print(song.duration_seconds)  # 87.8225850340136

# 帧数目
print(song.frame_count())  # 3872976.0

# 原始的音频数据, 不打印了
song.raw_data

还是很方便的,我们不需要使用 struct 进行获取。

文件保存

我们对音频进行了一些操作之后,怎么保存到本地呢?这也是关键的一部分,不然你处理完了也没有用啊。很简单,直接调用 AudioSegment 对象的 export 方法即可。

import pydub
song = pydub.AudioSegment.from_mp3("高梨康治 - 百鬼夜行.mp3")
song.export("百鬼夜行.wav", "wav")

指定文件名和保存的类型即可,注意:第二个参数表示保存的音频的类型,如果不指定那么默认是 mp3,即便我们第一个参数的文件名结尾是 .wav,但是保存的时候仍是 mp3。

import struct
import pydub

song = pydub.AudioSegment.from_mp3("高梨康治 - 百鬼夜行.mp3")
# 这里文件名是 .mp3 结尾, 但是第二个参数是 wav, 所以保存的实际上是一个 wav 格式的数据
song.export("百鬼夜行.mp3", "wav")

# 读取之后查看前 4 个字节, 发现是 b"RIFF", 证明确实是 mp3 格式
data = open("百鬼夜行.mp3", "rb").read(4)
val = struct.unpack("4s", data)[0]
print(val)  # b'RIFF'

假设我们有一百个 mp3 文件,要转成 wav 格式该怎么做呢。

from pathlib import Path
import pydub

p = Path(r"mp3_file_dir")
for file in p.glob("*.mp3"):
    pydub.AudioSegment.from_mp3(file).export(p.with_suffix(".wav"), "wav")

设置属性

有时我们需要改变文件的格式,但有时需要改变文件的属性。比如某个 MP3 文件采样频率有点高,我们需要降低一些,或者双声道变成单声道等等,这个时候该怎么做呢?

import pydub

song = pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行.mp3")
print(song.channels)  # 2

# 将通道设置为 1, 然后导出
song.set_channels(1).export("高梨康治 - 百鬼夜行_1.mp3", "mp3")

# 重新读取, 查看通道
print(pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行_1.mp3").channels)  # 1

1 表示单声道,2 表示双声道。从单声道转成双声道不会有任何的改变,但从双声道转成单声道可能会导致质量损失(当左右声道不同时)。

单声道:只用一条音频通道记录声音,是最古老、最基础的声音记录方式。单声道因为只有一条音频通道,所以我们的大脑接受左右耳的信息没有差异,听觉系统就不会产生心理声学的定位,所以不会有宽度及深度的差异。只能感受到声音、音乐的前后位置及音色、音量的大小,而不能感受到声音从左到右等横向的移动。效果相对于真实的自然声来说,是简单化的,是失真了的。所以听出来的声音干涩,没有层次感,没有现场感,一般用来听新闻广播,因为单声道信号简单不易丢失。原理是把来自不同方位的音频信号混合后统一由录音器材把它记录下来,再由一只音箱进行重放。

双声道:人们听到声音时可以根据左耳和右耳对声音的相位差来判断声源的具体位置,在电路上它们往往各自传递的电信号是不一样的。相当于实现立体声的原理,在空间放置两个互成一定角度的扬声器,每个扬声器单独由一个声道提供信号。而每个声道的信号在录制的时候就经过了处理,有些音乐就跟气流一样,从左到右再从右到左,因为是两个不同的声道,当一个声道的响度比另一个声道大的时候,我们就感觉声音好像有了方向一样。双声道立体感强,有音场,多用于音乐、CD等专辑。基本上音乐都是双声道,如果是单声道的音乐,只能说明音质非常非常差。

注意:设置的话不要通过这种方式来设置。

import pydub

song = pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行.mp3")
song.channels = 1
song.export("高梨康治 - 百鬼夜行_1.mp3", "mp3")

因为一个属性变了,可能会影响其它的属性,比如:帧大小,它等于 通道数 乘上 采样宽度(采样位数 / 8),如果通道变了,那么帧大小也会受到影响。所以我们应该通过 pydub 提供的 API 来设置,内部会自动帮我们处理。

import pydub

# 我们还可以更改采样频率
song = pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行.mp3")
print(song.frame_rate)  # 44100

# 更改采样频率, 一般都是 44100, 我们可以修改为其它的值
# 注意: 并不是任意值都可以, 只能是 8000 12000 16000 24000 32000 44100 48000 之一
# 如果不是这些值当中的一个, 那么会当中选择与设置的值最接近的一个
# 比如我们设置 18000, 那么会自动变成 16000
song.set_frame_rate(18000).export("高梨康治 - 百鬼夜行_1.mp3", "mp3")
print(pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行_1.mp3").frame_rate)  # 16000

采样频率等于帧速率,以赫兹为单位。增大这个值通常不会导致质量的下降,但降低这个值一定会导致质量的下降,因为更高的帧速率意味着更大的频响特征(即可以表示更高的频率)。

除了通道数、采样频率之外,我们还可以设置采样宽度(采样位数除以 8),对于一个音频而言能设置这些属性已经足够了。像很多大厂提供的音频识别服务,也会对音频属性有严格的限制,而限制的属性也基本上就这些。无非是通道、采样频率、采样位数等等。

import pydub

song = pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行.mp3")
print(song.sample_width)  # 2
song.set_sample_width(3).export("高梨康治 - 百鬼夜行_1.mp3", "mp3")
print(pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行_1.mp3").sample_width)  # 2

从打印的结果上来看,我们似乎没有设置成功,因为这个音频本身也是有相应关系的。可能音频本身的采样宽度就只能是 2,不过绝大部分音频的采样宽度都是 2,即采样位数为 16。

export 其它参数

我们导出音频的时候使用的是 export,这里面还可以接收其它参数, 我们先来看看我们导出的音频的原始的音频之间的差异。

我们看到原始的音频有很多其它信息,比如作曲人、专辑等等,但是我们导出的没有,那么可不可以设置呢。答案是可以的,在导出的时候加上一个 tags 参数即可。

import pydub

song = pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行.mp3")
song.export("高梨康治 - 百鬼夜行_1.mp3",
            "mp3",
            tags={"artist": "古明地觉",
                  "album": "地灵殿专辑",
                  "title": "好听的百鬼夜行",
                  "comments": "妈耶, 真好听"})

其它的属性可以单击右键,然后点击属性查看。对了还有图片,如果在导出的时候想要自定义封面的话,可以通过 cover 参数,传递一个图片文件地址即可。

另外,我们这里导出的文件要比原始文件小很多,原因在于比特率不一样。原始的音频的比特率是 320kbps,而我们导出的音频的比特率要小很多。因为比特率表示音频一秒所需的比特数,比特率越小,显然文件就越小。而我们在导出的时候也是可以修改比特率的:

import pydub

song = pydub.AudioSegment.from_mp3(r"高梨康治 - 百鬼夜行.mp3")
song.export("高梨康治 - 百鬼夜行_1.mp3",
            "mp3",
            bitrate="320k")

导入其它文件

除了我们说的那几种文件之外,还存在其它种类的文件,比如苹果手机自带的录音软件录出来的就是 m4a 格式的,这是我们就需要使用 from_file 方法了。

import pydub

# 因为没有 from_m4a 方法
song = pydub.AudioSegment.from_file(r"录音.m4a", "m4a")
print(song.duration_seconds)  # 12.009333333333334
print(song.frame_rate)  # 48000
# 采样宽度基本都是 2
print(song.sample_width)  # 2

# 将采样率(帧率) 变成 16k, 并变成 wav 格式导出
song.set_frame_rate(16000).export("录音_16k.wav", "wav")

以上 pydub 对音频的一些常见操作了,总的来说支持的功能还是比较多的,而且有一部分我们还没有介绍到,因为对于我个人而言不是很常用,如果你不是专门搞音视频的话。

总结

关于 Python 检测图片、音频我们就说到这里,之所以要说这个是因为本人最近在做语音识别项目,调用的是疼讯的云小微服务。而该服务对音频格式的要求比较严格,只接受采样频率为 16k 的音频,否则无法正确识别音频内容,因此迫不得已研究一下。总的来说个人觉得算是有些跨领域了,至少对我而言,如果你不是专门搞音频相关的话,差不多这些内容应该够了。当然音频、还有视频相关的技术确实非常火,说句实话薪水也很高,因为这一领域涉足的人真的不多,如果你对这方面很感兴趣的话可以继续深入下去,前途绝对很光明,当然前提是坚持下去。

posted @ 2020-03-01 20:39  古明地盆  阅读(3620)  评论(0编辑  收藏  举报