你知道如何用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 的音頻,否則無法正確識別音頻內容,因此迫不得已研究一下。總的來說個人覺得算是有些跨領域了,至少對我而言,如果你不是專門搞音頻相關的話,差不多這些內容應該夠了。當然音頻、還有視頻相關的技術確實非常火,說句實話薪水也很高,因為這一領域涉足的人真的不多,如果你對這方面很感興趣的話可以繼續深入下去,前途絕對很光明,當然前提是堅持下去。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM