倉庫:https://gitee.com/mrxiao_com/2d_game_3
資產:game_hero_test_assets_003.zip 發布
我們的目標是展示游戲運行時的完整過程,從像素渲染到不使用GPU的方式,我們自己編寫了渲染器并完成了所有的工作。今天我們開始了一些新的內容,覺得現在是一個合適的時機,整理一下之前的工作,開始著手聲音部分的開發。聲音是一個像圖形一樣的領域,許多人只會用現成的庫,而不深入理解其內部原理。然而,我們的目標是教育,幫助大家了解其中的每個環節,理解這些東西的工作原理,聲音處理就是其中之一。通過理解聲音,你能做很多事情,而如果只使用庫而不清楚其原理,可能就無法做到這些。
為了實現這一目標,game_test_asset\data\test3這個文件夾中包含了我自己制作的WAV文件,我確保這些文件沒有版權問題,可以自由使用并復制。我們今天的目標是加載這些WAV文件,以便我們能夠進行播放,從而完善我們的資源加載系統。目前,我們的資源加載系統只能處理位圖,還不能處理WAV文件,因此需要擴展它來支持WAV文件的加載。
為了實現這一點,我們需要理解WAV文件的結構,因此我們需要實現WAV文件的加載代碼。WAV文件雖然比位圖稍微復雜一些,但它們還是相對容易處理的文件格式。作為第一步,我們需要能夠加載WAV數據并進行處理,這樣后續的音頻混音等功能才能繼續進行。
解析 WAV 文件中的數據塊(Chunks)
WAV 文件采用 塊(Chunk)結構,即文件內部是由多個獨立的子塊組成的,每個子塊都有自己的 Chunk ID 和 Chunk Size:
- 我們遍歷整個 WAV 文件,按 Chunk ID 解析各個部分,例如:
"fmt "
(格式塊):定義音頻格式(PCM、采樣率、通道數等)。"WAVE"
(數據塊):存儲真正的音頻數據。- 其他擴展塊(可忽略)。
遍歷數據塊
- 定義數據塊結構
typedef struct {uint32_t ID; // 數據塊類型("fmt "、"data" 等)uint32_t Size; // 數據塊大小 } WaveChunk;
- 遍歷所有數據塊
uint8_t* cursor = fileData + sizeof(WaveHeader); uint8_t* fileEnd = fileData + header->Size + 8;while (cursor < fileEnd) {WaveChunk* chunk = (WaveChunk*)cursor;// 處理 "fmt " 數據塊if (chunk->ID == RIFF_CODE('f', 'm', 't', ' ')) {// 解析格式信息}// 處理 "data" 數據塊else if (chunk->ID == RIFF_CODE('d', 'a', 't', 'a')) {// 解析音頻數據}// 移動到下一個數據塊cursor += sizeof(WaveChunk) + chunk->Size; }
總結
- 檢查 WAV 頭部
ID
必須是"RIFF"
,Format
必須是"WAVE"
,否則assert
失敗。
- 遍歷數據塊
- 讀取
ID
和Size
,根據不同ID
解析不同數據塊。 - 關鍵數據塊包括
"fmt "
(格式信息)和"data"
(音頻數據)。
- 讀取
- 使用
assert
進行調試- 該代碼僅用于調試模式,最終的資產加載器將進行更完整的錯誤處理。
這樣,我們就完成了 WAV 文件的基本解析框架,后續可以繼續實現對 "fmt "
和 "data"
數據塊的具體解析。
game_asset.cpp:修正 WAVE_fmt 中的拼寫錯誤
我們現在正在查看資源加載器,尤其是加載WAV文件的部分。昨天有觀眾指出,在我編寫WAV文件頭部時,可能有一個拼寫錯誤,我記得可能是在格式塊(format chunk)里出錯了,不過我不記得具體是哪一項。經過檢查,發現確實是“有效位數”這一項出錯了,我把它寫成了4,但應該是2,所以感謝觀眾指出這個問題,避免了額外的調試工作。
昨天的工作主要是開始構建WAV文件的基礎結構。我們并沒有做太多,只是初步地復制了WAV文件的結構。接下來,我打算把這些內容放到和位圖定義相同的位置,以確保它們在同一個pragma pack
指令內。我們希望這些結構體能夠使用pragma pack 1
,這樣編譯器就不會插入額外的空間。例如,int16
類型的數據可能會被填充到32位,但是如果我們使用pragma pack
指令,就能夠確保數據按需對齊,不會出現額外的填充。
總之,我要確保這些數據在內存中是緊湊排列的,不會被編譯器處理成其他格式,接下來會繼續優化這些部分,保證WAV文件的加載能夠順利進行。
game.cpp:在 GameUpdateAndRender 中調用 DEBUGLoadWAV
為了測試這些功能,首先決定在代碼中進行一些調整,以便更高效地進行測試,而無需每次都運行整個游戲。具體的做法是在每次更新和渲染的調用開始時,插入一段代碼來加載一個 WAV 文件。這樣一來,可以在代碼的某個特定位置設置斷點,這樣測試就可以從該位置直接開始,避免了繁瑣的啟動游戲過程。這種方式能夠提高測試效率,特別是當需要調試和測試一些功能時。
為了實現這一點,決定在代碼中添加一個“加載聲音”的步驟,示例文件為 bloop000.wav
,這樣就可以確保每次進入測試時,程序會加載指定的音頻文件,方便檢查和驗證音頻加載的相關功能。
調試器:進入 DEBUGLoadWAV
多寫了一個data/test
編譯并運行代碼,確保文件能夠完整加載。查看 WAV 文件的頭部信息,檢查文件的尺寸參數是否符合預期,并確認 RIFF ID 是否正確。檢查時,發現 RIFF ID 確實按照小端字節序(little-endian)排列,并且文件頭中的字符代碼為 52 49 46 46,與預期一致。此外,文件中的 WAV ID 也符合預期的字符碼 57 41 56 45。
通過這些檢查,確認 WAV 文件頭部的加載沒有問題,斷言檢查也通過了。接下來需要處理 WAV 文件的解析部分,繼續進行文件解析的實現。
game_asset.cpp:引入 riff_iterator 用于 DEBUGLoadWAV
在實現 WAV 文件解析時,首先需要處理文件中的數據塊(chunk)。為了方便處理這些塊,設計了一個類似迭代器的工具來幫助管理讀取文件的過程。這個工具將包含一個指向當前文件位置的指針以及當前處理的 RIFF 塊的信息。
在實現中,首先解析文件頭,然后使用迭代器開始讀取文件中的第一個數據塊。每個塊有一個大小參數,迭代器將幫助逐步遍歷每個塊,并在每次讀取后移動到下一個塊。為了使這個過程更加清晰和易于管理,使用了一個循環結構來處理每個塊。
在循環中,首先判斷當前塊是否有效,然后使用 NextChunk
函數來跳轉到下一個塊。每當讀取一個塊時,檢查該塊的類型,根據類型執行不同的操作。比如,如果讀取的是格式塊(format chunk),則根據塊的內容進行相應的處理。
總的來說,這個實現的目的是為了在讀取 WAV 文件時,能夠高效地管理每個數據塊,并根據不同塊的類型執行不同的操作,簡化了文件解析的過程。
game_asset.cpp:向 RIFF_CODE 添加 WAVE_ChunkID_data
在分析 WAV 文件格式時,發現其中有幾個數據塊(chunk),但并不是所有的數據塊都需要關心。對于當前的需求,壓縮格式的文件并不需要處理,因此可以忽略 fact
塊。主要關注兩個數據塊:格式塊(format chunk)和數據塊(data chunk)。
格式塊包含了 WAV 文件的基本信息,例如音頻的格式和參數;數據塊則包含了實際的音頻樣本數據。因此,在實現解析時,主要需要處理這兩個塊:格式塊用于解析音頻的格式,數據塊則用于讀取實際的聲音數據。
在代碼中,首先要讀取格式塊,這將幫助理解后續數據塊的內容。然后,讀取數據塊,獲取實際的音頻數據。除了這兩個塊,其他的塊可以暫時忽略,因為它們對當前目標沒有實際意義。
簡而言之,整個解析過程主要集中在格式塊和數據塊的讀取,確保能夠正確解析音頻的格式并獲取樣本數據。
https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
game_asset.cpp:繼續編寫 DEBUGLoadWAV 的使用代碼
接下來要做的事情是從 WAV 文件中獲取實際的音頻數據。為了實現這一點,可以使用 GetChunkData
函數來提取格式塊(format chunk)和數據塊(data chunk)。對于格式塊(WAVE_fmt
),可以通過迭代器獲取該塊的數據并存儲為 WAVE_fmt
結構體。對于數據塊(data chunk
),雖然它本身只包含音頻的字節數據,不包含其他信息,但依然可以使用 GetChunkData
來提取該數據,并將其保存為 SampleData
。
然后,提取到的樣本數據會被保存,這樣就能夠獲得實際的音頻數據,準備進行后續處理。整體流程是:首先提取格式塊數據來了解音頻的基本信息,然后提取數據塊以獲取實際的音頻樣本數據。這樣做可以幫助完成 WAV 文件的解析,進而能夠進行后續的音頻處理和播放。
game_asset.cpp:引入 ParseChunkAt
首先需要創建一個 riff_iterator
,它將幫助在 WAV 文件的各個塊之間進行迭代。這個迭代器將假定當前指向一個有效的塊,例如 WAVE_chunk
,這是迭代器的初始狀態。迭代器的主要職責就是在文件中遍歷這些塊,提取所需的數據。接下來,還需要確保迭代器知道文件的大小,這樣它才能知道何時超出文件的有效數據范圍,避免訪問無效區域。為了做到這一點,迭代器還需要存儲當前塊的大小,并且在讀取數據時要對文件的邊界進行檢查。
此外,還需要實現一個 next
函數,該函數將幫助迭代器跳轉到下一個有效的塊,以便進行后續的讀取和處理。在此過程中,要特別注意文件的邊界和數據的有效性,以確保迭代過程的正確性。
game_asset.cpp:引入 NextChunk
首先,創建一個 riff_iterator NextChunk(riff_iterator Iter)
函數,用來推進到下一個塊。在這個函數中,需要通過當前塊的大小來決定要前進多少位置。根據 WAV 文件格式,塊的大小通常是指塊頭以外的數據大小。因此,在計算前進的距離時,可能需要排除塊頭的部分。假設一個塊的大小為 16 字節,如果塊頭的大小為 8 字節,那么有效數據的大小為 8 字節,前進時就需要跳過這部分數據。
為了處理這一情況,假設塊大小不包括塊頭,代碼會在當前指針位置上增加相應的有效數據長度。接著,為了確保正確解析,設置一個停止條件參數來控制迭代器在合適的位置停止。通過這種方法,可以有效地解析文件中的每個塊,并確保在迭代時不會超出文件數據的有效范圍。
game_asset.cpp:引入 IsValid
在實現 IsValid
方法時,核心目標是判斷迭代器是否有效。具體來說,通過檢查當前的迭代器位置(Iter.At
)是否小于預設的停止位置(Iter.Stop
)來確定迭代器是否仍然有效。如果當前迭代器位置小于停止點,則返回有效,表示可以繼續迭代。如果不滿足條件,則表示迭代器無效,無法繼續迭代。
此外,遇到代碼縮進問題時,需要確保所有的代碼塊按照正確的縮進規則進行排列,以避免編譯器產生不必要的警告或錯誤。
game_asset.cpp:繼續編寫 DEBUGLoadWAV
在實現 ParseChunkAt
時,需要考慮如何處理數據的讀取位置。具體而言,在解析時,會根據頭部信息確定從哪里開始讀取數據,并按照頭部指定的大小讀取后續數據。然而,是否應包括頭部的大小,仍不確定。這意味著要判斷頭部的大小是否包含在內,可能需要通過調試或其他方法確認,具體的處理方式會在后續的測試中明確。
此外,盡管最初考慮為 GetChunkData
增加類型安全檢查,但發現實際使用中并不需要,因為當前的實現并不會在多個不同場景下調用該方法。所以,最終決定不做額外的類型安全處理。
game_asset.cpp:引入 GetChunkData
在實現 GetChunkData
時,目標是返回塊的數據內容。該方法通過從當前迭代器的位置返回數據,具體來說就是在頭部信息之后的實際數據部分。為了簡化,這些方法實際上都是簡單的工具函數,因此最好將它們內聯。
GetChunkData
方法會返回當前迭代器位置的指針,并且跳過頭部部分,返回實際數據。GetType
方法則返回一個 32 位的數值,表示當前塊的類型。對于 wave chunk
,它包含一個 ID 和大小,方法應返回這個 ID,以便我們能夠識別出塊的類型。
調試器:進入 DEBUGLoadWAV
在進行第一次實現時,首先我們讀取了文件并開始解析。當我們創建了一個迭代器后,查看迭代器的內容時,發現其包含了一個塊ID。為了確定塊ID的具體值,可以通過打印輸出這個ID來查看。第一次解析時,遇到的是格式塊(format chunk),并且我們確認其符合預期。
在查看這個格式塊時,發現它包含了音頻文件的一些重要信息,比如采樣率為48kHz,立體聲(兩個通道)等,這符合預期。格式標簽(format tag)也表明文件是PCM格式,這是我們計劃讀取的無壓縮音頻格式。
其他字段,比如塊對齊(block align)和每個樣本的位數(bits per sample),也看起來正常,16位的樣本深度符合預期。不過,其中有一些字段(如CV size)可能對我們當前的讀取操作沒有太大影響。
接著,解析到下一個塊時,發現又出現了一個格式塊,這顯然不符合預期。此時意識到,可能在代碼中出現了某些錯誤,導致格式塊被錯誤地重復讀取。
game_asset.cpp:移除 riff_iterator 和 ParseChunkAt 中的 Chunk,改為在 NextChunk 和 GetType 中使用
在發現出現重復的塊后,決定修復這個問題。問題出在一個冗余的指針上,且沒有在移動時更新這個指針。因此,考慮到這個指針已經沒有太大作用,決定將其移除,改為直接通過at
(塊頭)來進行操作。這樣可以避免之前的錯誤,并使代碼更加簡潔,避免出現不必要的重復讀取。
通過這種方式,代碼能夠更加清晰和高效,同時也解決了之前的問題。
調試器:繼續逐步調試 DEBUGLoadWAV
在查看下一個塊類型時,發現它是數據塊(data chunk),這是存儲音頻樣本的地方。除了這個數據塊,還有其他塊,但它們不是我們能夠理解的類型,因此跳過了這些塊。為了進一步了解情況,決定查看一下這些未讀取的塊的類型。結果發現,數據塊的類型是零,這讓人感到有些奇怪。此時懷疑可能是在數據塊的末尾四個字節附近,可能存在一些問題。
繼續分析后,猜測可能存在一些數據對齊的問題或其他原因,導致讀取到的數據塊位置出現了偏差。
網絡:嘗試確定 RIFF 文件是否應以零結尾
在檢查文件結尾時,發現沒有明確的規范說明文件應該如何結束。因此,想確保沒有出現錯誤。如果文件結尾確實應該是零字節,那就沒有問題,但還是想確保沒有 bug。查閱相關文檔時,發現文檔中提到文件的字節排列方式。特別是,當文件大小是奇數時,會在數據塊后面添加一個零值的填充字節(pad byte)。這意味著如果塊大小是奇數,文件會在數據部分后附加一個零字節,以確保文件對齊。
目前實現沒有遵循這個規定,這可能導致文件末尾出現不符合規范的情況。因此,需要更新代碼以遵循這一規則,確保文件按照規范處理。
然而,文檔中沒有完全解釋為什么會出現讀取到零值的現象,因此可能需要進一步調查。
https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf
game_asset.cpp:如果 Chunk->Size 為奇數,則填充
在處理塊大小時,如果塊大小是奇數,那么需要根據規范進行填充。可以通過調整塊大小,確保其為偶數。例如,可以通過將塊大小加一來進行向上舍入,這樣就符合規范,確保正確對齊。然后,按照新的大小進行處理。
盡管如此,這并沒有完全解決問題,因為仍然不清楚為何會遇到零字節的情況。雖然文檔中提到當塊大小為奇數時需要進行填充,但這并沒有完全解釋為什么在特定情況下會出現零字節。因此,還需要進一步分析和確認這一現象的原因。
這行代碼的作用是確保 Size
是一個偶數,即使 Chunk->Size
是奇數。
詳細解釋:
uint32 Size = (Chunk->Size + 1) & ~1;
-
Chunk->Size + 1
:- 這部分代碼將
Chunk->Size
增加 1。如果Chunk->Size
是奇數,增加 1 后它會變成偶數;如果已經是偶數,則增加后變成下一個奇數。
- 這部分代碼將
-
& ~1
:~1
是按位取反操作,1
在二進制中是00000001
,因此~1
就是11111110
。- 按位與(
&
)操作會將Chunk->Size + 1
的最低有效位(最右邊的那個二進制位)清零。這樣,任何數的最低有效位都被強制設置為 0,從而使得最終的Size
成為偶數。
例子:
- 如果
Chunk->Size
是 5(奇數):Chunk->Size + 1
變成 6(偶數),然后6 & ~1
還是 6(因為6
已經是偶數)。
- 如果
Chunk->Size
是 4(偶數):Chunk->Size + 1
變成 5(奇數),然后5 & ~1
變成 4(通過按位與操作清除最低有效位)。
總結:
這行代碼確保無論 Chunk->Size
是奇數還是偶數,最終的 Size
總是一個偶數。
game_asset.cpp:從 Header->Size 中減去 4
在這種情況下,可能是因為我們沒有正確解釋頭部大小所包含的內容。所以,可能發生的情況是,RIFF 文件中的塊大小(chunk size)是 4 + n。也許他們的意思是塊頭部的大小不包括在內,而我們錯誤地將其包括了進去。如果這是正確的理解,那么就可以推測出問題所在。
為了正確處理這個問題,應該將實際大小的計算從塊頭部的大小中減去 4,因為我們需要排除掉已經跳過的那個wave部分(wave part)。這種方式可能是更合適的解決辦法。所以可以嘗試這樣調整,看看是否能解決問題。
調試器:逐步調試 DEBUGLoadWAV,發現正確地遍歷了所有塊
現在可以看看這樣是否會讓事情變得更清晰了。進入之后,發現有格式塊(format chunk)和數據塊(data chunk),除此之外沒有其他塊。因此,實際上我們不需要處理或跳過其他塊。事實證明,這些文件是從一個叫做 Gold Wave 的程序中保存下來的,結果是文件中沒有其他塊,只有我們具體想要的塊。這樣,當我們加載時,處理就變得簡單了。
game_asset.cpp:斷言某些數據是有效的
現在想要做的是記住實際需要的顯著數據,也就是我們真正想要獲取的數據。所以我會嘗試具體查找我們需要的數據。接下來,我會進行一些斷言,確保我們期望的數據格式是正確的。首先,我們希望 wFormatTag 的標簽是 PCM 格式。然后,我們希望通道數為 1 或 2,采樣率始終是我們首選的采樣率。接著,我們不太關心是否是塊對齊(block aligned),但我們希望每個樣本的位數總是 16 位。
網絡:查找 nBlockAlign 的含義
不太清楚“channel mask”到底意味著什么,不過看起來跟位置信息有關,但不太關心這一部分。至于“block aligned data block size in bytes”是什么意思,也不清楚。查找了一些文檔,發現格式塊的第一個部分提到,PCM 數據經過增強之后,格式塊和頭部聲明了每個樣本的位數。原始文檔規定,樣本的位數應向上取整為 8 位的倍數,這個取整后的值被用作容器大小,這個信息在容器中是冗余的。每個樣本的大小可以通過塊大小除以通道數來確定。這個冗余信息被用來定義新格式,例如 Cool Edit 使用 4 來聲明樣本大小為 24 位,其他格式也根據塊大小和通道數來確定。看來“block aligned”指的是通過塊大小除以通道數來確定每個樣本的字節數,而不是位數。
game_asset.cpp:斷言 nBlockAlign 為 2 * nChannels
基本上,“block aligned”必須是 2 或 4。我們希望它等于通道數乘以 2,因為我們規定每個通道的位數必須是 16 位。所以,最終的目標是確保每個通道都為 16 位,因為這就是我們唯一支持的格式。因此,所有這些條件都必須滿足,才能保證格式是正確的。
game_asset.cpp:設置 ChannelCount 和 SampleData
需要的信息是通道數,因此在代碼中設定了一個 ChannelCount
,初始值為 0。接著,通過讀取相關數據來更新這個通道數。其他的參數目前并不需要關注,只需要這個通道數,因為在此實現中只支持這種格式。
在處理WAVE_ChunkID_data時,會提取出實際的WAVE數據。對于 PCM 格式,數據本身僅包含大小和原始數據。PCM 格式不包含其他復雜的數據結構,因此提取的就是需要的樣本數據。
game_asset.cpp:斷言 ChannelCount 和 SampleData 為有效
在完成數據的提取后,接下來需要確認通道數和樣本數據的有效性,因此會使用 assert
來驗證這兩個條件,確保文件有效。之后,目標是讓 load sound
功能正常工作。要實現這一點,需要知道樣本的數量和內存位置,其中內存位置就是之前提取的樣本數據。
在處理內存數據時,可能還需要做一些額外的操作,建議對這些數據進行進一步處理或調整,以確保在加載聲音時數據的正確性和效率。
game_asset.h:向 loaded_sound 中添加 ChannelCount
考慮到需要支持不同類型的聲音格式,計劃對 loaded_sound
進行改進,使其支持單聲道(mono)和立體聲(stereo)兩種聲音。為此,打算引入 ChannelCount
(通道計數)和 SampleCount
(樣本計數)的概念,以便能夠處理單聲道和立體聲的音頻文件。這樣一來,未來也可以輕松擴展到支持更多通道的格式,例如杜比音效(Dolby)。
這種做法的好處在于,它不僅能支持當前的需求,還能為將來可能的音頻格式擴展做好準備,避免在面對更多通道的音頻時沒有合適的解決方案。
網絡:嘗試確定如何計算樣本計數,減去任何舍入部分
為了正確設置音頻數據,首先需要確定樣本計數。樣本計數是指音頻數據中實際包含的樣本數量。需要考慮是否存在任何因為文件格式的要求而進行的四舍五入或填充操作。根據音頻文件的格式,可能會有一些計算規則來確定樣本數,例如通過塊大小(block size)來推算每個樣本的大小。
在查看文件格式的描述時,看到了一些信息,比如 bits per sample
和 block size
,這些信息對于計算樣本計數很有幫助。具體來說,可以通過塊大小(block size)除以每個樣本的字節數來推算樣本數。如果每個樣本為16位(即2字節),那么可以根據塊大小除以2來獲得樣本數。
所以,關鍵是在文件格式的不同字段中找到相關的信息,如塊大小(block size),并根據每個樣本的大小來進行計算,確保樣本計數是準確的。
game_asset.cpp:計算 SampleCount
為了正確計算樣本數量,首先需要知道通道數和每個樣本的大小。可以通過以下方式計算:樣本數量 = 通道數 × 每個樣本的大小。由于每個樣本的大小是16位(即2字節),可以通過這個公式來確定樣本總數。
在計算時,還需要考慮樣本數據的大小。因此,樣本數量的計算不僅依賴于通道數,還需要用到樣本數據的大小。可以通過將樣本數據的總大小除以每個樣本的大小來計算樣本數量。因此,樣本數據的大小需要作為一個重要的變量來考慮,在處理數據時也需要保持對這個值的跟蹤。
在實現時,應確保正確地獲取并使用數據塊的大小,這樣可以確保樣本數量的計算是準確的。
game_asset.cpp:引入 GetChunkDataSize
為了計算樣本數量,首先需要獲取數據塊的大小。這里的 GetChunkDataSize
方法返回的大小不包括頭部數據,因此是準確的。然后,通過獲取樣本數據大小 (sample data size
),可以進行計算,確定樣本的數量。
具體來說,通過 GetChunkDataSize
方法,我們可以得到樣本數據的總大小,這個大小不包含文件頭部。通過此數據,結合每個樣本的大小(假設每個樣本大小為16位,即2字節),可以計算出總的樣本數量。這個計算過程包括將樣本數據的總大小除以每個樣本的大小。
通過這樣的方式,能夠準確計算出樣本的數量,并繼續進行后續的數據處理和操作。
game_asset.cpp:為 SampleData 創建數組
首先,如果通道數量為 1,則處理起來非常簡單。我們只需要確保結果中的通道數等于當前的通道數(即 1),然后樣本數據直接指向正確的內存位置。在這種情況下,所有樣本都已按順序存儲,可以直接使用。
但如果通道數量為 2(立體聲),則樣本數據是交替存儲的,左聲道和右聲道交替排列,例如 左,右,左,右
。這種存儲方式對于處理每個通道的數據來說并不方便,因為需要分別訪問每個通道的數據。因此,為了方便處理,我決定不直接使用交錯的數據格式,而是將其拆開,使得每個通道的數據分別存儲。
這樣處理后,我們可以更加方便地訪問和操作每個通道的數據,避免交錯帶來的復雜性。
黑板:就位進行音頻通道去交錯處理
在處理音頻數據時,目標是將音頻樣本從交錯存儲格式(即左右聲道交替存儲)轉換為每個聲道單獨存儲的格式。這意味著原始的樣本數據格式是像這樣存儲的:L0, R0, L1, R1, L2, R2,依此類推。我們希望將其轉換為類似于:L0, L1, L2, L3, L4 和 R0, R1, R2, R3, R4 的格式,使得每個通道的樣本數據能夠獨立存取。
為了實現這一點,關鍵的問題是如何在內存中對交錯的音頻數據進行“去交錯”操作。首先,可以通過交換樣本來實現這一目標,然而,這個過程可能并不像想象中那么簡單。理想情況下,如果我們交換每一對 L 和 R 樣本,那么會發現最終的結果可能并不會符合預期,因為只是簡單交換并沒有完全解決問題。
比如,如果將 L0 和 R0 交換,再將 L1 和 R1 交換,接著繼續按此規則進行交換,最終可能得到一個看似無序的排列。而要解決這一問題,需要通過一種方式“旋轉”樣本順序,讓它們變得整齊。具體來說,需要在交換后按一定規則將順序調整正確,例如通過交換某些特定位置的元素,來保證每個通道的數據正確對齊。
這種去交錯的操作實際上可能并不簡單,尤其是在要求數據能夠就地(in-place)操作的情況下。雖然初步的交換操作看起來能起到一定作用,但對最終的樣本進行排序時,如何有效地調整順序并確保每個通道的數據正確對齊,仍然是一個需要考慮的問題。
可以通過嘗試實現這些操作,或者用一組更長的樣本數據來驗證這一策略,看看是否能找到更有效的解決方案。
game_asset.cpp:編寫左聲道的 swizzling 算法
在處理音頻數據時,關鍵步驟之一是將交錯的左右聲道數據(L0, R0, L1, R1等)轉換為每個聲道單獨存儲的格式(例如,L0, L1, L2, L3和R0, R1, R2, R3等)。為了實現這一目標,需要在內存中操作這些數據。
首先,確定樣本數據類型為16位整數(16-bit samples),因為這是所需的格式。然后,基于每個樣本數據,計算出正確的位置。由于數據是交錯存儲的,因此每次訪問某個樣本時,都需要確保讀取的是正確的左或右聲道樣本。例如,在處理時,如果數據按照“L0, R0, L1, R1”的順序存儲,我們需要通過指針偏移來訪問正確的樣本,并將左聲道(L)數據正確地放入新的緩沖區。
在實現過程中,需要通過循環遍歷樣本數據,并為每個樣本在適當的位置存儲其值。具體來說,首先獲取當前樣本的地址,通過計算偏移量來定位到正確的位置,然后將其放入目標位置,確保左聲道和右聲道的數據分開存儲。
此外,需要注意的是,循環過程的執行順序非常重要。在每次迭代時,左聲道和右聲道樣本都需要從交錯的數據中提取出來,存儲到新的位置。通過這種方式,可以確保數據的去交錯操作按預期進行。
總體來說,核心操作是通過在緩沖區中進行位置交換(swap)來實現數據的重新排列。通過這種方式,最終可以將交錯存儲的音頻數據轉換為每個聲道單獨存儲的格式。這些操作可以逐步進行,確保每個樣本的數據正確存儲。
這段代碼的目的是對交錯存儲的立體聲(stereo)音頻數據進行去交錯(uninterleave)操作,也就是將存儲在內存中的左右聲道數據分別提取出來,存放到兩個獨立的緩沖區中。下面是對這段代碼的解釋,并通過一個簡單的例子來說明它是如何工作的。
舉例說明:
假設有以下交錯存儲的音頻數據(只考慮簡化的2個樣本,假設每個樣本為16位即2字節):
L0, R0, L1, R1
其中:
L0
是左聲道的第一個樣本,R0
是右聲道的第一個樣本,L1
是左聲道的第二個樣本,R1
是右聲道的第二個樣本。
我們要做的事情是將交錯存儲的樣本數據拆分為兩個獨立的緩沖區,分別存儲左聲道和右聲道的數據。
步驟:
-
初始化:
Result.Samples[0] = SampleData;
左聲道的數據L0, L1
存儲在Result.Samples[0]
中。Result.Samples[1] = SampleData + (SampleDataSize / 2);
右聲道的數據R0, R1
存儲在Result.Samples[1]
中。
-
交換操作:
假設SampleData
中的內容為:SampleData = [L0, R0, L1, R1]
-
對于
SampleIndex = 0
(即第一個樣本):Source = SampleData[0]
,即L0
。- 將
SampleData[0]
設置為SampleData[0]
(即沒有變化,因為本來就是L0
)。 - 將
SampleData[0]
設置為Source
,即L0
。
經過交換后,
SampleData
還是:[L0, R0, L1, R1]
-
對于
SampleIndex = 1
(即第二個樣本):Source = SampleData[2]
,即L1
。- 將
SampleData[2]
設置為SampleData[1]
(即右聲道R0
)的值。 - 將
SampleData[1]
設置為Source
,即將左聲道L1
放到右聲道的位置。
經過交換后,
SampleData
變為:[L0, L1, R0, R1]
-
最終效果:
這樣經過去交錯操作后,音頻數據就被拆分成了兩個獨立的聲道:
- 左聲道
L0, L1
- 右聲道
R0, R1
這種操作是為了將交錯存儲的樣本數據轉換為獨立存儲的左聲道和右聲道數據,使得每個聲道的數據更容易單獨處理。
段錯誤
game_asset.cpp:插入一些測試數據
當加載任何類型的WAV文件時,遇到的問題是,如果沒有正確處理,左聲道會正常加載,而右聲道會變得完全錯誤。因此,想要檢查右聲道的輸出結果,以確定問題所在。在調試時,可能暫時將數據類型從void
改為uint16
,這樣更容易查看結果。
為了便于調試,決定在加載數據之前手動填充一些特定的值。具體來說,將樣本數據的索引與其對應的值一一對應。舉例來說,數據序列將從0開始,按順序填充:0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,這樣可以方便地在分析過程中進行對比。
一個通道16字節雙通道32 差一個括號
加載之后,先查看第一個32個樣本,確認左聲道的數據是否符合預期,結果顯示左聲道的數據是正確的,這與預期一致。接下來,問題出現在第二個聲道(右聲道)。在查看右聲道數據時,結果顯示為零,這顯然是出錯了。懷疑是代碼中某些地方的計算出了問題,應該仔細檢查如何處理數據。
調試器:檢查右聲道的值
在進行Swizzle操作后,觀察到左右聲道的數據分布情況相當有趣。左聲道的數據按順序遞增,例如97、98、99,而右聲道的數據同樣也按順序遞增,但在某些部分出現了不同的模式。這些數據的分布方式看起來很奇怪,特別是在某些區域,每隔一個值就會出現不同的變化模式,有時是當前數值的一半。
觀察這種數據分布,感覺要在原地進行正確的Swizzle操作會相當復雜。仔細分析后,發現數據排列方式相當奇特,不太容易找到一個簡單的方法在原地完成數據重排。因此,推測可能存在一種更智能的方式來完成這個Swizzle操作,但目前還不確定具體的實現方法。
考慮到當前的情況,可以采取以下幾種應對策略:
- 使用額外的內存:可以申請額外的緩沖區,將數據正確地拷貝并整理,而不是嘗試在原地重排。
- 僅加載左聲道:暫時只加載左聲道的數據,忽略右聲道。這樣可以保證至少有一部分數據是正確的,而不必立即解決右聲道的處理問題。
- 添加待辦任務:在代碼中標記一個待辦事項,未來再考慮如何正確加載右聲道的數據。
為了暫時繞開這個問題,決定僅加載左聲道,并在代碼中標注TODO
,提醒以后再處理右聲道數據。因此,暫時將聲道數設為1,即:
Result.ChannelCount = 1;
這樣,即使當前無法正確完成Swizzle操作,也至少能夠保證左聲道數據的正確性,并避免因為右聲道的數據錯亂而導致更大的問題。
game.cpp 和 game.h:重新啟用 tSine
有部分好像之前給刪掉了現在重新修改一下
目前不太確定聲音播放的具體狀態,但GameOutputSound
函數仍然被調用,并且確實在輸出某些內容。目前的測試是一個正弦波(sine wave),但代碼中似乎并沒有實際定義它。不過,可以手動創建一個tSine
變量用于測試。
檢查代碼后發現,GameState.tSine
是否被遞增,查看WavePeriod
的值,并確認GameState.tSine = 1032
。因此,當前的做法是重新插入測試代碼,以驗證聲音輸出是否仍然正常。在不確定如何繼續的情況下,暫時恢復正弦波輸出進行調試。
接下來,執行代碼并運行,確認聲音是否仍然可以播放。運行后,聽到聲音,證明基本的音頻輸出仍然有效。這說明:
- 聲音播放機制仍然正常工作。
- 現階段可以繼續進行進一步的音頻處理,例如加載和播放WAV文件,而不用擔心底層音頻系統是否正常。
接下來,可以考慮如何加載WAV文件,并確保其數據被正確處理后輸出,而不是簡單地使用正弦波作為測試信號。
補充正弦波聲音
https://www.geogebra.org/m/vputraas
正弦波(Sine Wave)是一種最基本的波形,在聲音和信號處理領域中有著重要的地位。以下是對正弦波聲音的相關介紹:
1. 什么是正弦波?
正弦波是一種單一頻率的波形,其數學表達式為 y ( t ) = A sin ? ( 2 π f t + ? ) y(t) = A \sin(2\pi ft + \phi) y(t)=Asin(2πft+?),其中:
- A A A 是振幅(Amplitude),決定聲音的響度;
- f f f 是頻率(Frequency),決定音調的高低(單位為赫茲 Hz);
- t t t 是時間;
- ? \phi ? 是相位(Phase),影響波形的起始位置。
正弦波的特點是它的波形平滑、周期性強,沒有諧波(Harmonics),因此聽起來非常“純凈”。
2. 正弦波的聲音特性
- 音調:正弦波的聲音是單一頻率的,聽起來像一個純音。例如,440 Hz 的正弦波對應于音樂中的“A4”音(國際標準音高)。
- 音色:由于沒有諧波,正弦波的音色非常簡單,缺乏復雜樂器或人聲那樣的豐富性。人類耳朵可能會覺得它有些“單調”或“電子感”。
- 響度:由振幅決定,振幅越大,聲音越響。
3. 正弦波在現實中的應用
- 聲音合成:正弦波是合成聲音的基礎波形。許多電子音樂或合成器(如Moog、Roland)通過疊加正弦波來創造復雜的音色。
- 聽力測試:正弦波常用于聽力檢測設備(如純音測聽),因為它能精確測試人對特定頻率的感知能力。
- 信號處理:在通信和音頻工程中,正弦波用于分析和調試系統。
4. 正弦波的聽覺體驗
如果你通過揚聲器或耳機播放一個正弦波(比如用在線工具或音頻軟件生成),會聽到一種持續的、沒有起伏的“嗡嗡”聲。例如:
- 低頻正弦波(20-100 Hz):聽起來像深沉的低鳴,可能伴隨振動感。
- 中頻正弦波(300-1000 Hz):類似電子設備的提示音。
- 高頻正弦波(2000 Hz 以上):像尖銳的哨聲,可能會讓人感到不適。
5. 生成正弦波聲音
如果你想自己體驗,可以使用免費軟件(如Audacity)生成正弦波:
- 打開Audacity;
- 選擇“生成” > “音調”;
- 設置頻率(如440 Hz)、振幅和持續時間;
- 播放即可聽到。
總結
正弦波是聲音世界中的“原子”,簡單而純粹。雖然它本身聽起來不復雜,但它是理解聲波、音調和音頻技術的基礎。如果你有具體問題或想深入某個方面(比如生成正弦波的代碼),可以告訴我!
game.h:引入 loaded_sound TestSound
我們現在想要播放一個真實的聲音波形。如果要在游戲中輸出其他聲音,我們可以使用一個已經加載的聲音,比如 TestSound
。
首先,我們需要找到加載聲音的地方,比如 DebugLoadWAV
,并將加載的聲音賦值給 GameState
中的 TestSound
,確保它可以在后續被使用。之前的代碼中,每一幀都會重新加載聲音,這雖然運行正常,但看起來有些奇怪。為了改進,我們可以調整邏輯,使得 TestSound
只在需要時加載一次,而不是每一幀都重新加載。
接下來,我們可以利用 GAME_GET_SOUND_SAMPLES
這個接口,使它執行更有意義的操作,比如實際播放 TestSound
的數據,而不僅僅是填充一個正弦波。盡管當前代碼中 GameOutputSound
是一個單獨的函數,但它的作用可能可以整合到 GAME_GET_SOUND_SAMPLES
里,從而更方便地管理游戲的音頻輸出邏輯。
game.cpp:注釋掉 GameOutputSound,改用 TestSound
我們決定不再調用之前的代碼,而是直接在當前邏輯中處理音頻輸出。具體來說,我們的目標是將 TestSound
(已經加載的調試音頻)的數據填充到聲音緩沖區中。
首先,我們需要訪問 game_state
中的 TestSound
采樣數據,并將其寫入聲音緩沖區。我們假設 TestSound
具有 samples
數組,并從 samples[0]
開始讀取數據。然后,我們根據全局采樣索引 TestSampleIndex
計算當前播放的位置,并將相應的音頻數據存入緩沖區。
為了讓聲音能夠連續播放,我們需要維護 TestSampleIndex
,表示當前播放的位置。這個索引在每一幀音頻數據輸出后需要遞增,以確保下次寫入緩沖區時不會重復相同的采樣值,而是按照正確的順序播放整個音頻數據。
在遞增 TestSampleIndex
時,需要確保不會超出 TestSound
采樣數據的范圍。因此,我們使用取模運算 TestSampleIndex % TestSound.SampleCount
,使得索引始終處于有效范圍內,防止讀取超出 TestSound
的數據區域。
最后,我們確保正確寫入 SampleOut
,并在緩沖區中填充對應的音頻數據。如果 SampleOut
位置不對,或者 SampleValue
沒有正確獲取 TestSound
的采樣數據,則需要檢查代碼,確保所有的索引計算和緩沖區寫入邏輯正確無誤。
運行游戲,發現聲音不太好
當前的聲音效果并不好,因此需要進行更詳細的調試和分析。
首先,我們檢查 SampleIndex
和 TestSampleIndex
,確保它們正確地指向 TestSound
采樣數據中的位置。這兩個索引決定了我們當前讀取的音頻數據是否是正確的,因此需要驗證它們的計算是否符合預期。
接下來,我們檢查 SampleIndex
,確保它正確地從 TestSound
采樣數據中獲取了相應的值。如果 SampleIndex
計算有誤,可能會導致聲音出現失真、跳躍或其他異常現象。因此,我們需要仔細驗證 SampleIndex
是否與 TestSound.samples[SampleIndex]
一致,并確保數據沒有越界訪問或被錯誤修改。
然后,我們暫停代碼執行,并逐步進入關鍵代碼部分,以確認 SampleIndex
和 SampleIndex
計算的正確性。這有助于找出可能的錯誤,比如索引計算錯誤、數據讀取錯誤或緩沖區寫入錯誤。
最后,我們保存當前進度,并繼續分析代碼邏輯,以確保所有的索引計算、采樣數據讀取和緩沖區填充都符合預期。如果仍然存在音質問題,我們需要進一步檢查音頻數據格式、緩沖區大小、數據采樣率以及是否正確處理了循環播放邏輯。
發現忘記遞增TestSampleIndex
調試器:逐步調試 SampleIndex 循環
我們現在檢查 TestSoundSampleIndex
,發現它的初始值是 0
,SampleValue
也是 0
。然后,我們讓代碼繼續執行,并觀察 TestSampleIndex
是否按預期遞增,同時檢查 SampleValue
是否正確地從 TestSound
采樣數據中獲取。
在調試過程中,我們發現 SampleValue
沒有正確輸出,導致音頻數據可能不符合預期。經過檢查,發現 SampleValue
被誤解碼為無符號 uint16
,而實際上音頻數據是 有符號 int16
。這是一個關鍵問題,因為如果使用無符號類型,負數采樣值會被錯誤地解釋為很大的正數,從而導致聲音失真或完全錯誤。更改為 int16
之后,這應該能解決一部分問題。
除此之外,我們還檢查 SampleCount
,發現它的值是 1066
,表示當前 TestSound
采樣數據的總數。在代碼運行過程中,TestSampleIndex
按預期遞增,說明索引計算大體上是正確的。
下一步,我們繼續驗證所有索引計算、數據讀取和緩沖區填充邏輯,確保 TestSound.samples[TestSampleIndex]
讀取的值是正確的 有符號 16 位整數,并且 TestSampleIndex
在 SampleCount
之內正確循環。修復 int16
誤用問題后,應該能改善音頻播放效果,但仍需進一步測試,以確保聲音播放正常。
game.cpp:將 Sample 數據設置為 int16
經過檢查,代碼整體上看起來是相對正確的。但是,之前錯誤地使用了 uint16
(無符號 16 位整數)來存儲音頻數據,而實際上音頻數據是以 int16
(有符號 16 位整數)編碼的。這是一個重要的錯誤,因為音頻數據需要包含負值,以正確表示波形。如果使用 uint16
,負數部分會被錯誤地解釋為很大的正數,導致聲音播放異常。
這個錯誤主要是因為長時間沒有編寫音頻處理代碼,導致在數據類型選擇上出現疏忽。在代碼的多個地方,都應該將 uint16
改為 int16
,確保音頻數據能夠正確存儲和解析。
修正數據類型后,當前實現應該更符合預期,能夠正確解碼和輸出聲音。接下來,需要進行進一步的測試,以確保所有 int16
相關的計算、索引操作和緩沖區填充都是正確的,避免因數據類型問題導致的音頻失真或其他異常現象。
運行游戲,依然聽到點擊聲
目前聲音仍然不正確,聽起來像是“點擊”聲,而不是預期的音頻效果。因此,我們需要進一步分析問題的原因。
首先,檢查當前加載的音頻文件是否正確。回顧之前的邏輯,我們加載了 bloop_00
這個音頻文件,因此需要確認它的內容是否符合預期。可以通過播放 bloop_00
的原始文件,驗證它的實際聲音是否正確。如果原始文件聽起來正常,那么問題可能出在加載或播放的代碼上。
接下來,我們需要檢查音頻數據在加載時是否被正確解析。例如:
- 采樣格式是否匹配:確認
bloop_00
的音頻格式是否與代碼中的解析方式一致,例如16-bit PCM
、采樣率
、單聲道/立體聲
等。 - 數據是否完整:檢查
TestSound.samples
是否正確填充了音頻數據,確保數據沒有丟失或截斷。 - 緩沖區寫入是否正確:檢查
SampleIndex
計算是否準確,是否正確遍歷TestSound.samples
,并且寫入到緩沖區時數據沒有錯位或重復。
最后,可以嘗試逐步調試音頻輸出流程,比如:
- 直接在代碼中打印
TestSound.samples
的前幾個值,確認數據是否合理。 - 將
TestSound.samples
寫入一個文件,再用外部工具(如 Audacity)查看波形是否正常。 - 調整播放邏輯,嘗試不同的緩沖區大小、不同的采樣率,觀察聲音是否有所變化。
Audacity 是這個東西
通過這些方法,可以逐步縮小問題范圍,找出導致“點擊”聲的真正原因,并最終修正音頻播放的邏輯。
聽取聲音
我們遇到了一些音頻問題,特別是在播放時出現了點擊聲,這似乎是 VLC 播放器的一個 bug,盡管如此,至少可以聽到其中的 “bloop” 音效部分。為了避免這個問題,我們打算換一個播放器進行嘗試,希望能找到一個不會出現這么大問題的播放器。
接下來,我們需要找出出錯的原因,分析并進行修復。我們可以稍微進入一些質量保證的步驟,這樣可以確保在繼續開發之前,當前的狀態已經達到一個合理的水平,這樣明天我們就可以從音頻部分開始討論,并確保能夠順利加載所需的資源。通過這樣做,我們可以更有條理地推進工作,避免出現無法解決的問題。
總的來說,我們的目標是盡快理清現有的問題,并且將其解決,以便后續的進展能夠順利進行。在這個過程中,我們還需要處理和測試音頻樣本,確保在不同的播放器和設置下都能順暢播放。
game.cpp:檢查 SampleIndex 循環
當前我們處理的是立體聲輸出,寫入了左右聲道,并且這是按預期進行的。我們只是將相同的內容寫入左右聲道,這應該是沒有問題的。我們從測試音頻中獲取了第一個樣本通道的數據,這是我們預期的操作,而且我們是按順序獲取數據的,同時還對音頻緩沖區進行了循環處理。所以理論上,音頻應該會反復播放,形成一個連續的循環。
目前的情況看起來還算正常,沒有明顯的錯誤。我現在最想做的是再次檢查加載代碼,因為我對加載部分的實現有一些擔心。相比之下,我對播放代碼的關注度要稍微低一些。雖然這只是一個猜測,但這是我目前的直覺。
因此,我們需要深入查看加載音頻的部分,確保一切按預期進行,避免在這一部分出現問題,從而影響后續的音頻播放處理。
調試器:進入并檢查 SampleData
我們想要查看音頻采樣數據,以確認它看起來是否符合預期。首先,查看這些采樣數據的值,發現它們看起來像是合理的聲音數據,至少在某種程度上可以認為是可信的。這些數據波動看起來也符合常見的聲音數據模式,盡管波動比較快速,但從整體上看是可以接受的。
進一步觀察時,發現這些數據是緊密排列的,并且是立體聲樣本,即每兩個連續的樣本分別對應左聲道和右聲道。通過查看這些值,像是 -21
、-11
、-24
、42
、64
、-60
等,感覺這些數值有些異常,可能并不像預期的那樣平滑或正常。這引發了懷疑,覺得它們可能有些不對勁。
因此,決定返回去仔細檢查音頻文件的格式,看看是否在解析過程中遺漏了什么關鍵步驟或錯誤。可能在解碼時出現了問題,導致數據異常,需要進一步確認格式是否正確處理,或者是否存在其他解析錯誤。
網絡:查看 PCM 數據
我們需要查看PCM數據的實際內容,首先確定我們正在處理的是PCM數據,而不是其他格式。具體來說,我們關心的是是否正確處理了通道交織數據,每個樣本的字節數以及每個樣本的位數,應該是八倍樣本數。所以目前看起來我們在解釋數據時應該是正確的。
不過,我們還需要考慮是否存在數據的排列順序(Swizzle)問題。我們需要確認是否所有的數據都是雙通道的,這樣我們才能判斷問題是否出在數據排列的處理上。為了排查這個問題,可以測試一下是否只是我們的排列處理代碼有問題。
但在進行這些操作之前,首先確認一下在我們進行排列處理之前,數據本身是否已經符合預期。之前我們曾通過零、一個、二、三、四這樣的方式檢查過數據,所以我們可以再檢查一遍,確保排列操作(Swizzle)按我們預期的方式工作。
game_asset.cpp:移除測試代碼
問題終于找到了,原來是測試代碼沒有移除。哈哈,這真是太搞笑了!確實,如果在測試代碼中寫了很多無意義的數據,結果就不應該感到驚訝,因為它不會發出正常的聲音。這真是個笑話,但也是個很好的教訓。
現在我們明白了問題的根源,只是忘了清理測試時使用的那些垃圾數據。這樣一來,輸出的音頻自然就不會像預期的聲音一樣了。
運行游戲,聽到更正確的聲音
現在情況有所改善,音頻加載和播放已經正確進行,但仍然存在一些問題。雖然音頻能夠正確加載和播放,但似乎沒有達到預期的幀率。這意味著雖然音頻在播放時沒有明顯的錯誤,但可能因為幀率的問題導致播放效果不夠流暢,需要進一步檢查并解決幀率方面的瓶頸。
目前的計劃是切換到 -O2
優化級別進行構建。我懷疑,如果我們繼續構建,現在可能會對幀率有所影響,不過如果這樣做,應該會變得更好。然而,我們還需要確保將幀率同步到正確的值,而不是固定在 60Hz。所以,首先需要回到最初的設置,并進行重置。
接下來,我們要檢查當前的設置,并確保在構建之后可以正確調整幀率,以確保程序能夠按預期運行,不會因為幀率問題導致性能不穩定。
win32_game.cpp:將 GameUpdateHz 設置為 MonitorRefreshHz / 2.0f
目前的顯示器刷新率是 60Hz,所以我們打算暫時將設置調整為 60Hz。這樣做是為了確保與顯示器的刷新率保持一致,避免出現因幀率不同步而導致的問題。因此,我們決定先按這個設置進行,確保程序能夠正常運行。
這個已經寫死
運行游戲,聲音播放正確
現在應該會好很多。接下來可以進入質量保證階段,不過為了避免讓大家對當前的聲音感到煩躁,我們可能會將音頻切換成鋼琴音樂。我記得我在這里添加了來自 ad-lib 庫的音頻文件,具體是哪個文件呢?應該是 ../../../../game_test_asset/data/test3/music_test.wav
,這個文件可以用來替代現在的音效,避免長時間播放同一聲音造成不適。
game.cpp:播放 music_test.wav
好的,現在我們換成了一個更加平和的音頻,播放一些不會引起不適的聲音,這樣就沒問題了。這個新的音頻更為溫和,不會像之前的那樣讓人感到煩躁,應該會更合適。
你可以嘗試像這樣去交錯通道:http://imgur.com/ZcDu4Tb
有一個建議是嘗試將通道交織操作直接放在當前的位置進行處理。有人提供了一個鏈接,我查看了一下,鏈接內容似乎是關于交換通道的操作,具體來說是從開始和結束的位置進行交換,類似于內外交換的操作。我在想,這種方法是否有效?是否能夠解決問題?
不過,有一個不確定的地方是,這種操作是否對較長的音頻序列有效。對于較長的序列,可能會出現一些不同的表現,這一點還需要進一步確認。所以,現在的問題是,是否能夠適用于長序列,還是只有在短序列中才有效?這個仍然是一個需要驗證的疑問。
去交錯的第二容易方法是多次傳遞。交換 R0/L1, R2/L3, L4/L5 等。然后在兩個樣本塊、四個樣本塊中做同樣的事。最簡單的方法是直接在混合/輸出緩沖區中去交錯。
有一個建議是,通過多次交換(multi-pass swap)來進行通道交織,這種方法相對簡單。具體操作是先交換 R0
和 R1
,然后交換 L1, R2, L3, L4, L5
,接著繼續對兩個樣本塊進行相同的交換。這樣的方法可以直接將交織后的數據放入混音輸出緩沖區。然而,考慮到效率,我們并不希望采取這種方法。我們更傾向于在構造時就完成去交織(D-interleave)的操作,這樣在生成的藝術包(art pack)中,數據已經是去交織好的。
這樣做的原因是,提前處理交織操作可以避免在后續處理時的額外復雜性,從而提高效率。我們不需要在每次運行時都進行去交織的操作,直接在構建時完成能更簡便且高效。
為什么要去交錯聲音?播放時你還得讀取左右聲道的值
在播放聲音時,即使已經去交織(uninterleave),仍然需要讀取左右聲道的值。這是因為左右聲道的處理方式不同,我們需要分別處理這些值。為了達到更廣泛的兼容性和靈活性,我們希望能夠更精細地控制左右聲道的數據處理,確保每個聲道的數據都能按照預期的方式處理。因此,去交織操作后,左右聲道的數據仍然需要被分別讀取和處理。
有人在問 extern C 的好處是什么?它解決了什么問題?
extern "C"
主要解決了幾個問題,首先,它防止了函數名被修改。通常在鏈接時,C++ 會對函數名進行修飾,以確保操作符重載等特性在鏈接器不支持的情況下能正常工作。而 extern "C"
則能防止這種修飾,使得函數名保持原樣,這對于動態鏈接庫(DLL)尤其重要。舉個例子,當將游戲的入口點作為 DLL 導出時,如果沒有使用 extern "C"
,DLL 導入表中的函數名會被修改,影響正確導入。
其次,extern "C"
還指定了調用約定。在 Windows 的 64 位系統上,通常只有一個調用約定,即標準調用(standard call),因此通常沒有問題。但在 32 位系統上,存在多種調用約定,比如 Pascal 調用約定、C 風格調用約定等,使用 extern "C"
可以明確指定使用 C 風格的調用約定,尤其是在涉及可變參數時(例如 varargs
)會更加重要。
總的來說,當需要進行跨語言互操作性時,extern "C"
就非常有用,它確保了鏈接和調用的方式正確無誤。如果只是編譯自己的代碼,不涉及外部鏈接,通常不需要使用 extern "C"
,因為在這種情況下,函數名修改和調用約定問題并不重要。
你能解釋一下: (Chunk->Size + 1) & -1 嗎?
在這個解釋中,主要是講解了如何通過對“塊大小”進行加一和與負一按位與操作來確保數據對齊。
首先,解釋了如何判斷一個數字是奇數還是偶數:在二進制中,最低位(最低有效位)決定了一個數字是否為奇數,如果該位為1,數字就是奇數。如果最低位為0,數字就是偶數。而在塊大小的處理過程中,規范要求如果塊大小是奇數,則需要對齊到偶數大小。因此,如果塊大小是奇數(例如15),需要在它后面插入一個空字節,使其成為偶數大小(例如16)。
為了實現這個對齊,首先可以將塊大小加1,這樣可以確保無論當前的塊大小是奇數還是偶數,都能正確地進行向上取整。對于奇數大小(比如15),加1后得到16,清除最低位后就得到了一個偶數大小。如果塊大小本來就是偶數(例如16),加1后得到17,再通過按位與負一操作清除最低位,從而確保最終的塊大小依然是偶數,并且滿足對齊要求。
這其實是一種常見的對齊模式,類似于對齊到 16 字節邊界的方式(例如使用 +15
和 ~15
的方式)。通過這種方式,能確保塊大小始終向上取整到最接近的偶數或其他的 2 的冪次方的倍數。
總結起來,就是通過“塊大小加1”再與“-1”進行按位與操作,確保數據對齊,避免因為奇數塊大小而導致的不對齊問題,這種方法適用于任何 2 的冪次方對齊問題。
為什么樣本是交錯的,而不是平的?
關于為什么音頻數據是交織(interleaved)而不是平鋪(flat)的,原因可能是因為早期的聲卡通常是以這種方式處理數據的。過去,音頻數據經常是交織發送的,這樣的做法是當時硬件和處理方式的需求。
然而,現代的音頻處理已經不再需要使用這種交織方式,因為現在的音頻處理通常是在可變數量的聲道上進行的。例如,可能是2聲道、5聲道或者更多聲道。交織數據會增加額外的處理負擔,這對于靈活的音頻輸出(如支持不同的聲道輸出)來說并不是高效的做法。因此,雖然早期聲卡使用交織方式,但如今這種方式已經不再適用,因為它增加了不必要的復雜性和工作量。
總的來說,雖然歷史上使用交織數據格式有其原因,但隨著音頻處理的需求變得更加靈活和高效,現代的音頻系統通常會避免使用交織方式,轉而采用更簡單、直接的數據格式。
如果把交錯的數據看作是 Nx2 矩陣,轉置就可以看作是交錯。這里有一種就地做的方法:https://goo.gl/fgPmrg
https://en.wikipedia.org/wiki/In-place_matrix_transposition#Non-square_matrices:_Following_the_cycles
在這個解釋中,討論了如何實現或者將數據保持為一個 N 行 2 列的矩陣,并將轉置視為交織操作。提到了一種就地(in-place)實現的方法。具體的過程是:
- 初始化:首先,對于大于 1 的長度,遍歷每個排列,并選擇一個起始地址(S&C)。
- 數據存儲:設定
d
等于起始地址 S 處的數據。 - 保存數據:將起始地址 S 處的數據保存在一個變量 X 中。
- 循環操作:當 X 不等于 S 時,進行以下操作:
- 從 X 中移動數據到 S 的下一個位置。
- 更新 X 為 S 的前驅位置。
- 將數據從 D 移動到測試位置。
這個方法的核心思想是通過循環和數據移動的方式,實現就地的數據轉置或者交織操作,而不需要額外的內存分配。 需要注意的是,實際實現時需要仔細處理數據的位置和順序,確保不會丟失任何數據并且操作的效率較高。
這段說明看起來像是一個需要仔細閱讀和理解的算法,尤其是在處理數據搬移和循環中的細節時。
在使用 arena(內存分配器)時,創建了一個子 arena 用于游戲資產,但在操作過程中未正確地抓取一些數組數據。提出了是否應該將這些數據也放入 arena 的討論,盡管最終認為這可能并不一定是一個 bug,而是最初設計的目的。
在考慮了這個問題后,表達了對該設計的重新理解,認為這種分配方式是合理的,因為它分別處理了結構性信息和數據性信息,因此沒有必要將所有內容都放入 arena 中。最終,得出了這個問題并非 bug,而是按預期設計進行的結論。
為什么要就地更改 PCM 數據,而不是將其壓縮成你需要的信息并分配額外的空間?
在這段話中,首先提到了關于是否需要直接在運行時修改 PCM 數據的問題。這個問題更多是出于好奇,而非實際需求。實際上,在運行時并不需要進行這樣的修改,因為資產處理器(Asset Processor)會在加載過程中將數據整理成正確的頻道順序。因此,不需要在程序中手動改變 PCM 數據,也不需要額外分配空間來精簡數據,因為資產處理器會在預處理階段完成這一工作。
接下來,提到原本嘗試在運行時對數據進行交錯處理(interleave)時,發現雖然可以做到,但需要更加巧妙的方法,而不是簡單地進行處理。最終,得出了一個結論:雖然可以做,但并不值得在運行時進行,因為處理器會在前期做好這部分工作。