chinese直男口爆体育生外卖, 99久久er热在这里只有精品99, 又色又爽又黄18禁美女裸身无遮挡, gogogo高清免费观看日本电视,私密按摩师高清版在线,人妻视频毛茸茸,91论坛 兴趣闲谈,欧美 亚洲 精品 8区,国产精品久久久久精品免费

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文讀懂WAV音頻文件格式

jf_88434166 ? 來源:jf_88434166 ? 作者:jf_88434166 ? 2025-08-25 17:49 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

簡介

最近在學習I2S音頻相關(guān)內(nèi)容,無可避免會涉及到關(guān)于音頻格式的內(nèi)容,所以剛開始接觸的時候有點一頭霧水,后面了解了下WAV相關(guān)內(nèi)容,大致能夠看懂wav音頻格式是怎么樣的了。本文主要為后面ESP32 I2S音頻系列文章做鋪墊,所以本篇將介紹WAV音頻文件格式,并通過C代碼生成一段1S的正弦波WAV音頻寫入到SD卡里面。


WAV(Waveform Audio File Format) 是一種音頻文件格式,用于存儲音頻數(shù)據(jù)。它是由 微軟 和 IBM 開發(fā)的,通常用于存儲高質(zhì)量的原始音頻數(shù)據(jù)。

如果一段單聲道音頻的采樣率為 44100 Hz,一分鐘的音頻數(shù)據(jù)大約有 5.04MB。這個值是可以大致計算的,后面我們會提到。WAV 文件一般未經(jīng)過壓縮,因此能夠提供音頻的 高保真度 ,但相比其他音頻格式,相同時間內(nèi)的文件會顯得較大。所以一開始我打算用SPIFFS存儲WAV音頻的時候發(fā)現(xiàn)好像不太現(xiàn)實,畢竟ESP32 SPIFFS空間太小了,而 WAV文件幾秒的音頻動不動就好幾M了,這樣子的話只能播放短時間的音頻就不符合我的要求了。

WAV文件基于RIFF格式,這是一種用于存儲多媒體數(shù)據(jù)的通用格式。

也就是說WAV是基于RIFF格式的一種具體應(yīng)用,RIFF格式還被用于許多其他文件類型。

什么是RIFF格式

RIFF (Resource Interchange File Format,資源交換文件格式)是一種通用的文件格式標準,由微軟和IBM于1991年聯(lián)合開發(fā),用于存儲和交換多媒體數(shù)據(jù),如音頻、視頻、圖像等。RIFF格式以其靈活性可擴展性著稱,能夠容納各種類型的數(shù)據(jù),并被廣泛應(yīng)用于多種文件類型,例如:

  • WAV (音頻)
  • AVI (視頻)
  • ANI (動畫光標)

可以簡單理解為它是一種通用的文件容器格式,它通過一個個塊的形式(稱之為chunk)存儲多媒體數(shù)據(jù)。

以下是基于RIFF格式的不同文件類型及其用途的表格:

文件類型擴展名用途
WAV.wav存儲音頻數(shù)據(jù)
AVI.avi存儲音頻和視頻數(shù)據(jù)
RMI.rmi存儲MIDI音樂數(shù)據(jù)
ANI.ani存儲動畫光標
WEBP.webp存儲圖像數(shù)據(jù)(主要用于Web)

可以看到除了WAV是基于RIFF格式的,還有其他文件類型也是基于RIFF的,這里我們也可以看到很多文件格式會用特定的標識符,比如WAV, AVI,這里就涉及到FOURCC標識符。RIFF 文件的結(jié)構(gòu)通常以標識符 "RIFF" 開頭,緊接著是文件大小(4 字節(jié)),再后面跟著的就是一個四字符代碼(FOURCC),用于指明文件的數(shù)據(jù)類型。

FOURCC標識符

FOURCC(Four-Character Code,四字符代碼)是由 4 個字節(jié)組成的標識符,通常使用可打印的 ASCII 字符,它在 RIFF 文件中用來標識數(shù)據(jù)的具體格式。比如:

WAV 文件:以 "RIFF" 開頭,F(xiàn)OURCC 為 "WAVE",表示這是一個音頻文件。
AVI 文件:以 "RIFF" 開頭,F(xiàn)OURCC 為 "AVI ""(注意末尾有空格),表示這是一個視頻文件。

FOURCC 的設(shè)計要求正好 4 個字符,如果不足則用空格填充,且對大小寫敏感。這種標識方式不僅用于文件類型的最頂層定義,還用于文件內(nèi)部的各個數(shù)據(jù)塊,每個數(shù)據(jù)塊稱作一個chunk,比如 WAV 文件中包含 "fmt "(格式信息)和 "data"(音頻數(shù)據(jù))這兩個chunk。

字節(jié)序

WAV文件的字節(jié)數(shù)據(jù)還涉及到字節(jié)序的問題。字節(jié)序(Byte Order)是指多字節(jié)數(shù)據(jù)(如整數(shù)、浮點數(shù)等)在計算機內(nèi)存中存儲的順序。不同的計算機體系結(jié)構(gòu)可能采用不同的字節(jié)序方式,這可能會導(dǎo)致在不同平臺之間傳輸數(shù)據(jù)時出現(xiàn)問題。字節(jié)序問題主要體現(xiàn)在多字節(jié)數(shù)據(jù)的存儲順序上,尤其是在跨平臺的數(shù)據(jù)交換和存儲中需要特別注意。根據(jù)字節(jié)存儲時從低位開始還是從高位開始分為兩種:大端序和小端序。

大端序(Big-Endian)

大端字節(jié)序是一種字節(jié)順序,其中數(shù)據(jù)的高字節(jié)存儲在內(nèi)存的低地址處,低字節(jié)存儲在高地址處。

例如,對于一個4字節(jié)的整數(shù) 0x12345678,它的字節(jié)序會按以下順序存儲:

地址0123
數(shù)據(jù)0x120x340x560x78

這種存儲方式類似于我們閱讀數(shù)字的順序,從左到右。

小端序(Little-Endian)

小端字節(jié)序是一種字節(jié)順序,其中數(shù)據(jù)的低字節(jié)存儲在內(nèi)存的低地址處,高字節(jié)存儲在高地址處。

對于同樣的4字節(jié)整數(shù) 0x12345678,它的字節(jié)序會按以下順序存儲:

地址0123
數(shù)據(jù)0x780x560x340x12

這種存儲方式將數(shù)字的低位放在前面,更符合計算機內(nèi)部的處理邏輯。

WAV文件結(jié)構(gòu)

WAV文件基于RIFF格式。RIFF格式的結(jié)構(gòu)是一個個塊構(gòu)成的,一個塊稱為一個chunk,每個chunk都有一個4字節(jié)的ID(FOURCC),緊隨其后的是4字節(jié)的塊大?。╟hunk size),然后是塊數(shù)據(jù) (data) 。 最外層的是RIFF chunk,里面在套著"fmt" chunk和"data" chunk。

在這里插入圖片描述

我們來看一下WAV文件的結(jié)構(gòu):

在這里插入圖片描述

這張圖的最左邊是字節(jié)序,然后是偏移量,每個數(shù)據(jù)字段區(qū)域的名稱及對應(yīng)區(qū)域的字節(jié)大小。

字節(jié)序:
前面我們提到WAV的字節(jié)序問題,那在WAV中每個chunk里面的字節(jié)數(shù)據(jù)是以什么方式存儲的呢?在RIFF格式中,所有多字節(jié)的 數(shù)值數(shù)據(jù)(如塊大小、音頻采樣率等)都以小端序存儲。而ID,即FOURCC標識符,是4個ASCII字符的組合,按照ASCII字符的順序直接存儲, 所以它的字節(jié)序是大端序。

偏移量:
偏移量是指當前數(shù)據(jù)字段相對于文件開始位置的字節(jié)數(shù)。比如ChunkID的偏移量是0,表示它是文件的開始部分;ChunkSize的偏移量是4,表示它從文件的第4個字節(jié)開始,

WAVE音頻文件結(jié)構(gòu)主要分為三個部分:


1. RIFF Chunk Descriptor(偏移量0-12)

這是文件的頭部,提供文件的身份和基本信息:

  • ChunkID (偏移量0,4字節(jié)) 標識文件為RIFF類型,通常為字符串 "RIFF"。 每個字符在ASCII表中都對應(yīng)一個十六進制數(shù)。比如,R的ASCII碼是0x52,I是0x49,F(xiàn)是0x46,第二個F也是0x46,那連起來的話, "RIFF"這四個字母對應(yīng)的ASCII碼就是0x52 0x49 0x46 0x46。
  • ChunkSize (偏移量4,4字節(jié)) 表示整個文件的大小(不包括前8字節(jié),即 ChunkID 和 ChunkSize)。 整個文件大?。ú话?字節(jié))= 36 + SubChunk2Size或 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)或文件總大小-8
  • Format (偏移量8,4字節(jié)) 指定文件格式為 "WAVE"。對應(yīng)57 41 56 45。 ---

2. fmt Sub-chunk(偏移量12-36)

這部分描述音頻的格式信息,是播放或處理音頻時必須了解的關(guān)鍵數(shù)據(jù):

  • Subchunk1ID (偏移量12,4字節(jié)) 標識這是 "fmt " 子塊。和上面的"RIFF"一樣,使用ASCII字符標識,不足四個字符,末尾用空格補齊。對應(yīng)66 6D 74 20
  • Subchunk1Size (偏移量16,4字節(jié)) 表示此子塊的大小(對于PCM通常為16字節(jié))。
  • AudioFormat (偏移量20,2字節(jié)) 指定音頻格式,例如PCM(未壓縮音頻,值為1)。
  • NumChannels (偏移量22,2字節(jié)) 聲道數(shù),例如1(單聲道)或2(立體聲)。
  • SampleRate (偏移量24,4字節(jié)) 采樣率,例如44100 Hz(CD音質(zhì))。
  • ByteRate (偏移量28,4字節(jié)) 每秒字節(jié)數(shù),計算公式為: SampleRate * NumChannels * BitsPerSample / 8
  • BlockAlign (偏移量32,2字節(jié)) 每個采樣塊的字節(jié)數(shù),計算公式為: NumChannels * BitsPerSample / 8
  • BitsPerSample (偏移量34,2字節(jié)) 每個樣本采樣的位數(shù),例如8位或16位。

3. data Sub-chunk(偏移量36起)

這部分存儲實際的音頻數(shù)據(jù):

  • Subchunk2ID (偏移量36,4字節(jié)) 標識這是 "data" 子塊。對應(yīng)64 61 74 61。

  • Subchunk2Size (偏移量40,4字節(jié)) 表示音頻數(shù)據(jù)的大小。 datasize = NumSamples × NumChannels × BitsPerSample / 8,其中NumSamples 是總樣本數(shù)

  • data (偏移量44起,可變大?。?包含原始的音頻采樣數(shù)據(jù)。 ---

    WAV文件頭

    WAV文件的前44字節(jié)稱為 WAV的文件頭 ,剩下的data為WAV文件實際的音頻數(shù)據(jù)。所以整個WAV文件的大小應(yīng)等于文件頭44字節(jié) + data字節(jié)大小

    在這里插入圖片描述

    這個文件頭主要注意ChunkSize ,Subchunk2Size,ByteRate ,BlockAlign 這幾個參數(shù),我們重點介紹一下。

    ChunkSize

    ChunkSize字段里面存儲著 “它之后的數(shù)據(jù)總大小” 的這個數(shù)據(jù) (對于當前chunk的剩余部分)。所以 ChunkSize 指 對于ChunkSize字段后面的數(shù)據(jù)大小,不包括前8字節(jié),即 4字節(jié)的ChunkID 和4字節(jié)的ChunkSize,所以ChunkSize大小是文件總大小-8。 (從ChunkID到data是一個WAV文件,ChunkSize實際就是從下個地址08開始到WAV文件結(jié)尾的總字節(jié)數(shù))

    在這里插入圖片描述

    ChunkSize大小還等于36 + SubChunk2Size。(下圖紅色框+藍色框)。

    因為同理Subchunk2Size 指 對于Subchunk2Size字段后面的數(shù)據(jù)大小,而這個數(shù)據(jù)剛好就是WAV真正的音頻數(shù)據(jù),即 datasize,

    在這里插入圖片描述

    ChunkSize還等于 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size),這個式子比較長,主要是分的比較細,如下圖:
    在這里插入圖片描述

    ByteRate

    ByteRate表示每秒傳輸?shù)淖止?jié)數(shù),比如一段采樣率8000hz,采樣深度16bit的音頻,單聲道,則一秒采樣8000個樣本,每個樣本16位,每秒采樣樣本字節(jié)大小為8000 * 16 / 8 * 1聲道 = 16000字節(jié),除以8是為了轉(zhuǎn)換為字節(jié),所以ByteRate = SampleRate * NumChannels * BitsPerSample / 8

    BlockAlign

    BlockAlign指每個采樣塊的字節(jié)數(shù),或者說一幀的樣本,如果是單聲道音頻,一幀樣本就包含一個聲道數(shù)據(jù);如果是雙聲道音頻,一幀樣本包含左聲道數(shù)據(jù)和右聲道數(shù)據(jù)。比如一段采樣深度16bit的音頻,單聲道,一幀就是16 / 8 * 1聲道 = 2字節(jié)。所以BlockAlign = NumChannels * BitsPerSample / 8。

    講到采樣幀這里順便提一下之前學習遇到的困惑,之前學習I2S了解到在對音頻樣本采樣時,如果是雙聲道音頻,左聲道和右聲道是一幀樣本,在同一時刻采樣,那為什么WS又區(qū)分WS=0和WS=1呢? 在之前學過I2S的通信格式的那個圖里一般左邊是左聲道,右邊是右聲道,這樣子看起來并不是在同一個時刻。這里其實是我混淆了采樣和傳輸?shù)倪^程,采樣確實是同時采樣的,但是傳輸是先傳輸左聲道,再傳輸右聲道。這里參考了別人畫的圖,很形象借用一下。

    假設(shè)一個 buffer 包含 4 個周期、而一個周包含 1024 幀、一幀包含兩個樣本(左、右兩個聲道),每個樣本長度為2bytes。

    在這里插入圖片描述

    Subchunk2Size

    Subchunk2Size表示音頻數(shù)據(jù)的大?。ㄗ止?jié)),一般可以預(yù)估計算,有了ByteRate ,一般乘以時間,就可以得到音頻總大小。 或者知道樣本數(shù)也可以估算出來,比如一段采樣率44100,采樣深度16bit的音頻,單聲道,時間一分鐘60s,字節(jié)速率ByteRate=44100 * 16 / 8 = 88200,即每秒傳輸字節(jié)數(shù)88200字節(jié),再乘以時間,88200 * 60 = 5292000字節(jié) ≈ 5.04 MB。Subchunk2Size大小因為表示的是WAV音頻實際數(shù)據(jù)大小,所以也叫datasize,后面編寫程序時我們將使用datasize這個字段名稱。 使用時間去估計音頻數(shù)據(jù)大小可能會有誤差,但是這個誤差一般不會很大。我們還可以通過樣本數(shù)去估計音頻數(shù)據(jù)大小,即NumSamples × NumChannels × BitsPerSample / 8,其中NumSamples是總樣本數(shù),NumChannels × BitsPerSample / 8 就是每個采樣樣本的字節(jié)數(shù)(即BlockAlign), 乘以總樣本數(shù),就可以得到總樣本字節(jié)大小。
    以上我們講了ChunkSize ,Subchunk2Size,ByteRate ,BlockAlign這幾個比較主要的參數(shù),還有一些其他參數(shù)在WAV文件中是默認的。為了方便查看,將以上內(nèi)容整理為表格:

    偏移大小字段名內(nèi)容/說明
    04ChunkID"RIFF"(52 49 46 46)
    44ChunkSize文件大小 - 8 或 36 + SubChunk2Size或 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)
    84Format"WAVE"(57 41 56 45)
    124Subchunk1ID"fmt "(66 6D 74 20)
    164Subchunk1Size16(表示 PCM 格式時)
    202AudioFormat1 表示 PCM;其他為壓縮格式
    222NumChannels聲道數(shù)(1=單聲道,2=立體聲)
    244SampleRate采樣率(如 44100)
    284ByteRate每秒傳輸?shù)淖止?jié)數(shù) = SampleRate * NumChannels * BitsPerSample / 8
    322BlockAlign每個采樣塊的字節(jié)數(shù) = NumChannels × BitsPerSample / 8
    342BitsPerSample每個樣本的位數(shù)(如 16)
    364Subchunk2ID"data"(64 61 74 61)
    404Subchunk2Size音頻數(shù)據(jù)的大小(字節(jié)) = NumSamples × NumChannels × BitsPerSample / 8

    WAV音頻文件格式示例

    了解了RIFF格式,字節(jié)序和WAV文件結(jié)構(gòu)等相關(guān)參數(shù)后,我們先舉一個WAV音頻文件格式示例,再來看看實際的音頻文件格式是什么樣子的。
    假設(shè)有一段WAV音頻文件如下(十六進制顯示)

    52 49 46 46 24 08 00 00 57 41 56 45 66 6d 74 20 10 00 00 00 01 00 02 00
    22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 08 00 00 00 00 00 00
    24 17 1e f3 3c 13 3c 14 16 f9 18 f9 34 e7 23 a6 3c f2 24 f2 11 ce 1a 0d

    對音頻數(shù)據(jù)按照上面WAV文件結(jié)構(gòu)進行劃分:
    在這里插入圖片描述
    我們可以得到RIFF chunk, ChunkSize, Subchunk1Size,AudioFormat等相關(guān)參數(shù),這里要注意除了ASCII字符,其他數(shù)據(jù)都是以小端序存儲的。 比如ByteRate為 88 58 01 00,小端序應(yīng)為:0x00015888,對應(yīng)的十進制為88200。
    在這里插入圖片描述
    再比如BlockAlign=4, 根據(jù)我們前面舉的例子計算(雙倍),它是一段雙聲道音頻。
    那對于一段實際音頻,我們?nèi)绾尾榭此氖M制格式呢?我們可以通過 Hex Editor這個軟件,

    HxD Hex Editor 是一款功能強大的十六進制編輯器和磁盤編輯器,它可以讓你直接查看和編輯二進制文件的內(nèi)容。你可以使用HxD Hex
    Editor來分析、修改和處理各種數(shù)據(jù)格式,包括程序文件、磁盤映像、內(nèi)存轉(zhuǎn)儲以及其他二進制文件。

    這里我自己生成了一段30S的WAV音頻。我們用HxD軟件打開它看看。
    在這里插入圖片描述
    在這里插入圖片描述
    當我們框選頭四個字節(jié)時,可以看到右邊也有顯示它的對應(yīng)文本為:RIFF,表示這是一個基于RIFF格式的文件。我們將每個數(shù)據(jù)按照上面的結(jié)構(gòu)進行劃分,可以看到這個數(shù)據(jù)格式和我們介紹的WAV格式相符。除了框選的部位,后面都是真正的WAV音頻數(shù)據(jù)即data。 框選的所有部分我們稱之為 文件頭,以四個字節(jié)為一組,數(shù)一下可以發(fā)現(xiàn)剛好有11組,11 * 4= 44字節(jié),剛好是WAV文件頭的字節(jié)數(shù)。 而WAV數(shù)據(jù)大小就是上面圖片最后紅色框的2646016字節(jié),則整個WAV文件字節(jié)數(shù)應(yīng)為2646016 + 44 = 2646060字節(jié)。右鍵查看這個音頻的文件屬性:
    在這里插入圖片描述
    這和我們的計算結(jié)果一致。
    關(guān)于這個WAV文件頭的詳細信息如下:
    52 49 46 46 RIFF標識
    24 60 28 00 ChunkSize = 2646052(除去前8個字節(jié)文件大?。?br /> 57 41 56 45 WAV標識
    66 6D 74 20 fmt標識
    10 00 00 00 , Subchunk1Size =16(表示 PCM 格式時固定為16)
    01 00 AudioFormat=1 ,音頻格式:PCM(未壓縮)(表示 PCM 格式時固定為1)
    01 00 聲道數(shù):1(單聲道)
    44 ac 00 00 采樣率:44100 Hz
    88 58 01 00 字節(jié)率:88200 字節(jié)/秒
    02 00 塊對齊:2字節(jié)(每個采樣點的字節(jié)數(shù))
    10 00 位深度:16位(每個采樣點2字節(jié))
    64 61 74 61 data標識
    00 60 28 00 Subchunk2Size = 2646016 (音頻數(shù)據(jù)大?。?br /> WAV文件大小:2646060 字節(jié)
    現(xiàn)在我們是通過WAV文件信息得到這些參數(shù),比如音頻數(shù)據(jù)大小 2646016 。前面我們說過WAV文件大小可以預(yù)估,那我們來計算一下看看有什么差異。以上面我生成的audio.wav文件為例, 假設(shè)我們已經(jīng)知道一些基本參數(shù),一段采樣率44100, 采樣深度16bit, 單聲道WAV音頻,如果我們通過字節(jié)速率ByteRate去計算再乘以時間,則估計總音頻文件大小應(yīng)為44100 * 16 / 8 * 30 = 2646000字節(jié),但實際大小為2646016字節(jié),我們估計出來的音頻大小比實際小。這是因為采樣音頻時長并不是精確的30 秒, 如果是精確30秒,采樣點數(shù)量應(yīng)該是44100 × 30 = 1323000個,我們通過 Subchunk2Size (實際音頻大?。?,計算實際樣本數(shù)卻為2646016 / 2 = 1323008,比 1323000 多 8 個采樣點,而每個采樣點占 2 字節(jié),所以實際整體多了16字節(jié)。 反過來我們可以計算實際采樣時間為1323008 / 44100 ≈ 30.0001814058956秒, 多出8個采樣點的時間剛好為1 / 44100 * 8 = 0.0001814058956秒。所以我們通過時間去預(yù)估WAV音頻數(shù)據(jù)大小的話和實際相比是有差異的,但是我們一般會先預(yù)估大小,然后再更新WAV文件頭。

    使用ESP32將WAV文件寫入SD卡

    以上我們介紹了WAV相關(guān)內(nèi)容后,我們將介紹一個例子,將WAV音頻文件寫入SD卡,生成的WAV音頻為一段1S的正弦波音頻。
    上面我們知道通過一段WAV文件頭信息,可以得到它的一些參數(shù);反過來我們也可以寫入一些參數(shù)到WAV文件頭里,生成WAV文件,所以WAV頭部的定義是不可避免的。

    【定義WAV文件頭】

    假設(shè)我們要生成的WAV音頻參數(shù),采樣率8000,采樣深度16bit, 單聲道,那么我們可以預(yù)估ChunkSize,Subchunk2Size(即datasize)大小,因為采樣率是8000Hz,我們要生成1秒的音頻,則1秒有8000個樣本,每個樣本大小為2字節(jié)(采樣深度16bit),則 datasize = 16000, 根據(jù)公式直接計算的話就是NumSamples × NumChannels × BitsPerSample / 8 = 8000 x 1 x 16 /8 = 16000字節(jié)。 ChunkSize = 36 + datasize = 16036字節(jié)。其他參數(shù)可以參考上面的表格,這里就不贅述了。將其轉(zhuǎn)化為16進制,小端序,
    定義WAV文件頭:

    const uint8_t wavHeader[44] = {
      0x52, 0x49, 0x46, 0x46, // "RIFF"
      0xA4, 0x3E, 0x00, 0x00, // chunksize: 16036
      0x57, 0x41, 0x56, 0x45, // "WAVE"
      0x66, 0x6D, 0x74, 0x20, // "fmt "
      0x10, 0x00, 0x00, 0x00, // fmt塊大小 (16)
      0x01, 0x00,             // 音頻格式 (1 = PCM)
      0x01, 0x00,             // 聲道數(shù) (1)
      0x40, 0x1F, 0x00, 0x00, // 采樣率 (8000 Hz)
      0x80, 0x3E, 0x00, 0x00, // 字節(jié)率 (16000)
      0x02, 0x00,             // 塊對齊 (2)
      0x10, 0x00,             // 每樣本位數(shù) (16)
      0x64, 0x61, 0x74, 0x61, // "data"
      0x80, 0x3E, 0x00, 0x00  // datasize: 16000
    };
    

    【創(chuàng)建并打開文件】

    為了寫入SD卡,我們還要初始化SD卡。創(chuàng)建一個文件取名為test.wav并打開它:

    #define SD_CS_PIN 5
    
    // 初始化SD卡
    if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD卡初始化失??!");
    return;
    }
    Serial.println("SD卡初始化成功。");
    
    
    //創(chuàng)建并打開文件
    File wavFile = SD.open("/test.wav", FILE_WRITE);
    if (!wavFile) {
    Serial.println("無法創(chuàng)建文件!");
    return;
    }
    

    【寫入WAV頭部】

    File 類是Arduino SD庫的一部分,這里我們創(chuàng)建了一個 File 類對象取名為wavFile,wavFile.write用于向 SD 卡上的文件寫入數(shù)據(jù)。使用size_t write(const uint8_t *buf, size_t size)將文件頭寫入前面創(chuàng)建的文件中,這里要注意第一個參數(shù)類型是 uint8_t *類型的,如果寫入的buffer不是uint8_t *類型,需要進行強制類型轉(zhuǎn)換。

    wavFile.write(wavHeader, 44);
    

    【 生成440Hz正弦波音頻】

    正弦波公式為:y = A * sin(ωt+φ)
    其中,
    A:振幅,那么y的取值范圍就是[-A, A]
    ω:角頻率,ω = 2 * π * f,其中f為頻率,周期T = 1 / f;
    φ:初相位;
    以下是生成一段1秒440Hz正弦波音頻的示例:

    // 生成并寫入440Hz正弦波音頻數(shù)據(jù)
    const int sampleRate = 8000;  // 采樣率
    const int frequency = 440;    // 正弦波頻率
    const int numSamples = sampleRate * 1; // 1秒的樣本數(shù)
    for (int i = 0; i < numSamples; i++) {
    float time = (float)i / sampleRate;
    int16_t sample = (int16_t)(32767.0 * sin(2.0 * PI * frequency * time));
    }
    

    【寫入WAV音頻文件并關(guān)閉文件】

    使用size_t write(const uint8_t *buf, size_t size)將前面生成的正弦波音頻數(shù)據(jù)寫入前面創(chuàng)建的文件中并進行強制類型轉(zhuǎn)換。

    wavFile.write((uint8_t*)&sample, 2); // 寫入16位樣本
    wavFile.close();
    Serial.println("WAV文件寫入完成。");
    

    整合后的代碼如下:

    #include < SD.h >
    #include < SPI.h >
    
    // SD卡片選引腳
    #define SD_CS_PIN 5
    
    // WAV文件頭部(44字節(jié))
    const uint8_t wavHeader[44] = {
      0x52, 0x49, 0x46, 0x46, // "RIFF"
      0xA4, 0x3E, 0x00, 0x00, // chunksize: 16036
      0x57, 0x41, 0x56, 0x45, // "WAVE"
      0x66, 0x6D, 0x74, 0x20, // "fmt "
      0x10, 0x00, 0x00, 0x00, // fmt塊大小 (16)
      0x01, 0x00,             // 音頻格式 (1 = PCM)
      0x01, 0x00,             // 聲道數(shù) (1)
      0x40, 0x1F, 0x00, 0x00, // 采樣率 (8000 Hz)
      0x80, 0x3E, 0x00, 0x00, // 字節(jié)率 (16000)
      0x02, 0x00,             // 塊對齊 (2)
      0x10, 0x00,             // 每樣本位數(shù) (16)
      0x64, 0x61, 0x74, 0x61, // "data"
      0x80, 0x3E, 0x00, 0x00  // datasize: 16000
    };
    
    void setup() {
    Serial.begin(115200);
    
    // 初始化SD卡
    if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD卡初始化失?。?);
    return;
    }
    Serial.println("SD卡初始化成功。");
    
    // 創(chuàng)建并打開文件
    File wavFile = SD.open("/test.wav", FILE_WRITE);
    if (!wavFile) {
    Serial.println("無法創(chuàng)建文件!");
    return;
    }
    
    // 寫入WAV頭部
    wavFile.write(wavHeader, 44);
    
    // 生成并寫入440Hz正弦波音頻數(shù)據(jù)
    const int sampleRate = 8000;  // 采樣率
    const int frequency = 440;    // 正弦波頻率
    const int numSamples = sampleRate * 1; // 1秒的樣本數(shù)
    for (int i = 0; i < numSamples; i++) {
    float time = (float)i / sampleRate;
    int16_t sample = (int16_t)(32767.0 * sin(2.0 * PI * frequency * time));
    wavFile.write((uint8_t*)&sample, 2); // 寫入16位樣本
    }
    
    // 關(guān)閉文件
    wavFile.close();
    Serial.println("WAV文件寫入完成。");
    }
    
    void loop() {
    }
    

    這里我們觀察到如果使用數(shù)組定義WAV文件頭的話需要計算它的十六進制比較麻煩,我們可以定義一個WAV頭部結(jié)構(gòu)體,寫入ASCII字符和公式,這樣可以更方便地計算 WAV 文件頭的信息,而不用手動去處理十六進制數(shù)據(jù)。
    使用結(jié)構(gòu)體定義WAV文件頭:

    // 定義 WAV 頭部結(jié)構(gòu)體
    struct WavHeader {
        char     riff[4] = {'R', 'I', 'F', 'F'};    // "RIFF"
        uint32_t chunkSize;                         // 文件大小 - 8
        char     wave[4] = {'W', 'A', 'V', 'E'};    // "WAVE"
        char     fmt[4] = {'f', 'm', 't', ' '};     // "fmt "
        uint32_t fmtChunkSize = 16;                 // fmt 塊大小 (16 for PCM)
        uint16_t audioFormat = 1;                   // 音頻格式 (1 = PCM)
        uint16_t numChannels = 1;                   // 聲道數(shù) (1 = 單聲道)
        uint32_t sampleRate = SAMPLE_RATE;          // 采樣率 (8000 Hz)
        uint32_t byteRate = SAMPLE_RATE * 2;        // 字節(jié)率 (sampleRate * numChannels * bitsPerSample / 8)
        uint16_t blockAlign = 2;                    // 塊對齊 (numChannels * bitsPerSample / 8)
        uint16_t bitsPerSample = 16;                // 每樣本位數(shù) (16 bits)
        char     data[4] = {'d', 'a', 't', 'a'};    // "data"
        uint32_t dataSize;                          // 數(shù)據(jù)塊大小
    };
    

    使用結(jié)構(gòu)體定義WAV文件頭的話我們只是定義了一個類型,所以我們需要定義一個結(jié)構(gòu)體變量,因為我們沒有直接給出 chunkSize 和 datasize ,所以我們需要計算音頻數(shù)據(jù)大小,創(chuàng)建并初始化WAV文件頭。這里由于樣本比較簡單,所以我們直接可以確定樣本數(shù)去計算音頻數(shù)據(jù)大小,后面就不需要再更新WAV文件頭了。

    // 計算音頻數(shù)據(jù)大小
        const int numSamples = SAMPLE_RATE * 1;     // 1 秒的樣本數(shù)
        const int bytesPerSample = 2;               // 16 位,每個樣本 2 字節(jié)
        uint32_t dataSize = numSamples * bytesPerSample; // 數(shù)據(jù)大?。?6000 字節(jié)
        uint32_t chunkSize = 36 + dataSize;         // 文件總大小 - 8:16036 字節(jié)
    
        // 創(chuàng)建并初始化 WAV 頭部
        WavHeader header;
        header.chunkSize = chunkSize;               // 設(shè)置 chunkSize
        header.dataSize = dataSize;                 // 設(shè)置 dataSize
    

    完整代碼

    修改后的完整代碼如下:

    #include < SD.h >
    #include < SPI.h >
    
    // 定義常量
    #define SD_CS_PIN 5         // SD卡片選引腳
    #define SAMPLE_RATE 8000    // 采樣率(8000 Hz)
    #define PI 3.1415926535     // π 值
    
    // 定義 WAV 頭部結(jié)構(gòu)體
    struct WavHeader {
        char     riff[4] = {'R', 'I', 'F', 'F'};    // "RIFF"
        uint32_t chunkSize;                         // 文件大小 - 8
        char     wave[4] = {'W', 'A', 'V', 'E'};    // "WAVE"
        char     fmt[4] = {'f', 'm', 't', ' '};     // "fmt "
        uint32_t fmtChunkSize = 16;                 // fmt 塊大小 (16 for PCM)
        uint16_t audioFormat = 1;                   // 音頻格式 (1 = PCM)
        uint16_t numChannels = 1;                   // 聲道數(shù) (1 = 單聲道)
        uint32_t sampleRate = SAMPLE_RATE;          // 采樣率 (8000 Hz)
        uint32_t byteRate = SAMPLE_RATE * 2;        // 字節(jié)率 (sampleRate * numChannels * bitsPerSample / 8)
        uint16_t blockAlign = 2;                    // 塊對齊 (numChannels * bitsPerSample / 8)
        uint16_t bitsPerSample = 16;                // 每樣本位數(shù) (16 bits)
        char     data[4] = {'d', 'a', 't', 'a'};    // "data"
        uint32_t dataSize;                          // 數(shù)據(jù)塊大小
    };
    
    void setup() {
        Serial.begin(115200);
    
        // 初始化 SD 卡
        if (!SD.begin(SD_CS_PIN)) {
            Serial.println("SD卡初始化失敗!");
            return;
        }
        Serial.println("SD卡初始化成功。");
    
        // 創(chuàng)建并打開文件
        File wavFile = SD.open("/test.wav", FILE_WRITE);
        if (!wavFile) {
            Serial.println("無法創(chuàng)建文件!");
            return;
        }
    
        // 計算音頻數(shù)據(jù)大小
        const int numSamples = SAMPLE_RATE * 1;     // 1 秒的樣本數(shù)
        const int bytesPerSample = 2;               // 16 位,每個樣本 2 字節(jié)
        uint32_t dataSize = numSamples * bytesPerSample; // 數(shù)據(jù)大小:16000 字節(jié)
        uint32_t chunkSize = 36 + dataSize;         // 文件總大小 - 8:16036 字節(jié)
    
        // 創(chuàng)建并初始化 WAV 頭部
        WavHeader header;
        header.chunkSize = chunkSize;               // 設(shè)置 chunkSize
        header.dataSize = dataSize;                 // 設(shè)置 dataSize
    
        // 寫入 WAV 頭部
        wavFile.write((uint8_t*)&header, sizeof(header));
    
        // 生成并寫入音頻數(shù)據(jù)(440 Hz 正弦波)
        const int frequency = 440;
        for (int i = 0; i < numSamples; i++) {
            float time = (float)i / SAMPLE_RATE;
            int16_t sample = (int16_t)(32767.0 * sin(2.0 * PI * frequency * time));
            wavFile.write((uint8_t*)&sample, 2);
        }
    
        // 關(guān)閉文件
        wavFile.close();
        Serial.println("WAV文件寫入完成。");
    }
    
    void loop() {
    }
    

    以上通過ESP32生成的一段1S的正弦波音頻寫入SD卡模塊,硬件上只需ESP32和SD模塊。下面我們介紹如何將ESP32和SD模塊進行接線。
    ESP32
    在這里插入圖片描述
    SD卡模塊
    在這里插入圖片描述

    ESP32和SD模塊接線

    ESP32SD模塊
    D5CS
    D18SCK
    D23MOSI
    D19MISO
    5VVCC
    GNDGND

    按照以上步驟,編譯上傳代碼后,應(yīng)能在SD卡找到生成的名為test.wav的音頻文件,播放會聽到1秒的正弦波聲音。
    同樣我們用HxD軟件打開我們生成的test.wav文件
    在這里插入圖片描述
    對比我們代碼里的WAV文件頭數(shù)據(jù),可以發(fā)現(xiàn)數(shù)據(jù)是一樣的,這說明WAV文件頭確實是按照我們的要求寫入了WAV文件了,而且使用數(shù)組或者結(jié)構(gòu)體表示 WAV 頭部這兩種方法都可以實現(xiàn),建議采用第二個代碼的方式,使用結(jié)構(gòu)體和動態(tài)計算 chunkSize等數(shù)據(jù),確保 WAV 文件頭部的正確性、靈活性和兼容性。

    總結(jié)

    以上我們介紹了什么是WAV音頻文件,還有一些音頻格式的相關(guān)概念、參數(shù),并實際觀察了WAV文件的數(shù)據(jù)內(nèi)容,對WAV文件結(jié)構(gòu)有了更深入的了解,然后我們通過ESP32生成了一段1S的正弦波音頻,并將其寫入SD模塊,方法是通過將音頻參數(shù)寫入WAV文件頭,并通過SD和文件系統(tǒng)相關(guān)函數(shù)將文件頭寫入我們創(chuàng)建的文件里,這樣我們就可以在SD卡里通過讀卡器讀取里面的正弦波音頻數(shù)據(jù)了。
    關(guān)于WAV文件頭的每個參數(shù)是如何計算和填寫的,在我們介紹WAV文件頭的時候,已經(jīng)舉例并且說明了,我們也可以直接參考一開始總結(jié)的表格,里面有詳細說明和相關(guān)公式,這些公式并不需要死記硬背,理解了每個參數(shù)的含義還是比較容易理解的。在介紹WAV文件頭的時候,還有一些參數(shù)沒有詳細說明,比如AudioFormat, 1表示PCM,至于其他值表示的壓縮格式應(yīng)該是什么樣子這里沒有提到,還有LIST 塊相關(guān)本文也沒有提到,因為我們主要針對WAV文件進行介紹,所以這里不作提及,感興趣的小伙伴可以自行去了解下~
    本文是為后面ESP I2S音頻學習內(nèi)容作為鋪墊,因為WAV文件格式的內(nèi)容還是比較多的,所以單獨寫一篇介紹。后面大家關(guān)于WAV文件有疑惑的地方,可以參考這篇文章。因為本人也是初學,以上是個人理解加上搜索資料學習到的,如果有什么問題,可以提出交流討論,歡迎指正!需要HxD軟件和想聽一下源代碼工程生成的WAV音頻文件是什么聲音的可以評論區(qū)留言!已經(jīng)整理好所有文件 ~ 創(chuàng)作不易,多多點贊收藏哦!

審核編輯 黃宇

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • WAV
    WAV
    +關(guān)注

    關(guān)注

    0

    文章

    23

    瀏覽量

    19691
  • I2S
    I2S
    +關(guān)注

    關(guān)注

    1

    文章

    77

    瀏覽量

    43690
  • ESP32
    +關(guān)注

    關(guān)注

    21

    文章

    1042

    瀏覽量

    20277
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點推薦

    服務(wù)器數(shù)據(jù)恢復(fù)—ocfs2文件系統(tǒng)被格式化為Ext4文件系統(tǒng)的數(shù)據(jù)恢復(fù)案例

    服務(wù)器存儲數(shù)據(jù)恢復(fù)環(huán)境&故障: 人為誤操作將Ext4文件系統(tǒng)誤裝入臺服務(wù)器存儲上的Ocfs2文件系統(tǒng)數(shù)據(jù)卷上,導(dǎo)致原Ocfs2文件系統(tǒng)被格式
    的頭像 發(fā)表于 06-10 12:03 ?397次閱讀
    服務(wù)器數(shù)據(jù)恢復(fù)—ocfs2<b class='flag-5'>文件</b>系統(tǒng)被<b class='flag-5'>格式</b>化為Ext4<b class='flag-5'>文件</b>系統(tǒng)的數(shù)據(jù)恢復(fù)案例

    CCG4十六進制文件格式是什么?

    你好,先生, 我可以知道 CCG4 十六進制文件格式嗎? 2055 行是什么意思?
    發(fā)表于 05-15 07:22

    KT142C語音芯片支持的語音文件格式什么?Mp3還是wav呢?

    真沒有必要 2、如果客戶強烈需要,我們也是可以添加的。但是KT142C內(nèi)置的寶貴可用空間就變少了 3、因為同等音質(zhì)、同等時長的mp3文件,比wav文件體積小很多很多。 所以優(yōu)先壓縮為mp3
    的頭像 發(fā)表于 02-17 11:07 ?661次閱讀
    KT142C語音芯片支持的語音<b class='flag-5'>文件格式</b>什么?Mp3還是<b class='flag-5'>wav</b>呢?

    EE-110:ELF和DWARF文件格式快速入門

    電子發(fā)燒友網(wǎng)站提供《EE-110:ELF和DWARF文件格式快速入門.pdf》資料免費下載
    發(fā)表于 01-05 09:41 ?0次下載
    EE-110:ELF和DWARF<b class='flag-5'>文件格式</b>快速入門

    EPS文件格式如何轉(zhuǎn)換 EPS和SVG文件的區(qū)別

    EPS文件格式轉(zhuǎn)換 EPS(Encapsulated PostScript)是種用于存儲矢量圖形的文件格式,最初由Adobe公司開發(fā)。由于EPS文件在打印時能夠保持極高的質(zhì)量,并且廣
    的頭像 發(fā)表于 11-19 10:31 ?2585次閱讀

    我想問下頻率信號如何在TDMS文件格式下保存???

    我想問下頻率信號如何在TDMS文件格式下保存???
    發(fā)表于 11-12 18:40

    讀懂單燈控制器工作原理

    讀懂單燈控制器工作原理
    的頭像 發(fā)表于 11-11 13:13 ?1756次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>單燈控制器工作原理

    讀懂MSA(測量系統(tǒng)分析)

    讀懂MSA(測量系統(tǒng)分析)
    的頭像 發(fā)表于 11-01 11:08 ?1891次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>MSA(測量系統(tǒng)分析)

    tlv320ac23b-Q支持的格式是什么?

    我現(xiàn)在播放網(wǎng)絡(luò)上下載的wav,mp3音樂般沒問題,但通過語音合成軟件合成的文件,格式也是wav或者mp3,在PC上播放正常,用此芯片卻播
    發(fā)表于 11-01 07:26

    能在DSP的存儲器上存MP3或其他格式音頻文件,由DSP讀后控制TLV320AIC23轉(zhuǎn)換成語音?

    我想用TLV320AIC23和DSP做音頻系統(tǒng)。我想問下能在DSP的存儲器上存MP3或其他格式音頻文件,由DSP讀后控制TLV32
    發(fā)表于 11-01 06:45

    請問TLV320DAC3100可以播放什么格式音頻文件?

    請問TLV320DAC3100可以播放什么格式音頻文件
    發(fā)表于 10-31 07:22

    TLV320AIC3106335板子wince系統(tǒng)下調(diào)用playsound接口播放wav格式聲音總是有雜音,怎么解決?

    TLV320AIC3106335板子wince系統(tǒng)下調(diào)用playsound接口播放wav格式聲音總是有雜音,同塊板子如果用播放器播放音頻 是沒有雜音的。 我用用飛思卡爾的某個ce系
    發(fā)表于 10-25 08:00

    請問如何把WAV,MP3格式音頻文件轉(zhuǎn)化為16位的數(shù)據(jù)IIS格式?

    你好,請問如何把WAV,MP3格式音頻文件轉(zhuǎn)化為16位的數(shù)據(jù)IIS格式
    發(fā)表于 10-23 07:24

    需要同時播放4路WAV文件,在個DAC輸出,這樣需要什么算法混合4路音頻???

    求教4路WAV文件播放混合問題,我需要同時播放4路WAV文件,在個DAC輸出,這樣需要什么算法混合4路
    發(fā)表于 10-21 07:25

    常用對象文件格式

    電子發(fā)燒友網(wǎng)站提供《常用對象文件格式.pdf》資料免費下載
    發(fā)表于 10-15 09:25 ?0次下載
    常用對象<b class='flag-5'>文件格式</b>