游戲引擎學習第140天

回顧并為今天的內容做準備

目前代碼的進展到了聲音混音的部分。昨天我詳細解釋了聲音的處理方式,聲音在技術上是一個非常特別的存在,但在游戲中進行聲音混音的需求其實相對簡單明了,所以今天的任務應該不會太具挑戰性。

今天我們會編寫一個非常基礎的聲音混音器,首先確保它能正常工作,接著思考一下它的接口應該是什么樣的。接下來可能會在下周進行一些優化調整,并使其更加符合實際需要。我們可能會進行一些簡化,以確保它在運行時的表現良好,但不會對其進行過多優化,因為我們更關注的是實現它能夠正常運行。

回顧一下我們上次停下的地方。昨天我們詳細討論了聲音處理,但在之前的日子里,我們加載了聲音文件,并能夠播放我們選擇的任何一個文件。現在代碼中,聲音播放的是一段鋼琴音頻采樣。

現在,我們有一個當前的設置方式,可能不是最好的選擇。我們平臺代碼的設置是,如果沒有達到目標幀率,音頻會發生撕裂現象。舉個例子,如果沒有達到幀率,音頻會跳過一些幀,在調試模式下,我們就能看到音頻中的跳過,然而如果在優化模式下,音頻播放就平滑了。問題的原因是,我們目前沒有做任何處理來避免這些問題。我們假設總是能保持目標幀率,并盡可能填充音頻樣本以補償任何可能的幀率缺失。

當前如果屏幕刷新頻率是165Hz的話
在這里插入圖片描述

讓從電腦獲取刷新頻率來計算
在這里插入圖片描述

(debug模式運行程序音頻有撕裂的現象)
(release模式正常)

接下來,我們要考慮做的工作是:我們現在有一個播放音頻的循環,功能很簡單,就是將加載的聲音樣本復制到輸出緩沖區中。而我們的目標是將這個簡單的操作擴展成可以在任意時間播放多個聲音的系統,同時將它們混音成一個合理的音頻輸出,這樣我們就能夠同時播放多個聲音而無需擔心它們之間的沖突。

為了實現這一目標,首先要清楚我們目前的代碼結構。在當前的設置中,播放音頻樣本是通過單獨的調用來實現的,之所以這么做是因為我們最開始想讓它與游戲狀態的其他部分分開,考慮到將來可能會將其單獨放到線程中運行。雖然目前還沒有決定是否將其放在不同的線程中,但我們首先假設,聲音樣本的生成和游戲的其他部分是同步進行的。

當然,未來可能會考慮將它拆分到獨立線程中,但現在我們假設聲音和游戲的其他部分同步運行,先不考慮線程和鎖的問題,之后再看是否需要對這一點做出改變。

game.h: 引入 playing_sound 并在 game_state 中添加一個指針

首先,我們需要了解當前正在播放的所有聲音,因此在 game.h 文件中,我們可能需要在 game_state 中添加一些內容,來記錄當前播放的所有聲音。這里有多種方式可以實現這一點。一種合理的方法是使用鏈表,我們可以將正在播放的聲音添加到鏈表中,然后假設鏈表中的每個節點代表一個正在播放的聲音。另一種方法是使用固定數量的槽(數組),我們只允許在這些槽中播放聲音。如果所有槽都已滿,新的聲音就無法播放。

為了解決這個問題,首先我決定使用鏈表,因為它是最簡單直觀的方式。我們可以創建一個名為 playing_sound 的結構體,并使它具有鏈表功能,使每個聲音都有一個指向下一個聲音的指針。然后,我們會在 game_state 中添加一個指針,指向鏈表的開頭,命名為 PlayingSounds

playing_sound 結構體中,我們希望每個聲音有以下信息:

  1. 聲音ID:每個聲音都會有一個唯一的標識符,這是為了區分不同的聲音,類似于我們之前在渲染中使用的ID。通過這個ID,我們可以從音頻資源中提取正確的聲音樣本進行播放。

  2. 音量:我們需要一個音量值,表示當前聲音的音量大小,這個值介于0和1之間,0表示靜音,1表示原始聲音的最大音量。這個值可以控制聲音的強度。

  3. 左右聲道音量:因為我們使用的是立體聲音頻,我們還需要分別控制左右聲道的音量。因此,我們可能需要存儲左右聲道的音量值,允許分別調整左右聲道的音量。

  4. 播放位置:我們還需要跟蹤當前聲音播放的位置,也就是已經播放了多少個音頻樣本。這是為了確保在播放聲音時,我們知道已經播放了多少,以免重復播放同樣的樣本。每個聲音會有一個“播放游標”來記錄當前播放到的樣本位置。

基于這些需求,我們的 playing_sound 結構體大致會包含以下字段:

  • sound_id ID:當前播放的聲音的ID。
  • real32 Volume[2]:聲音的整體音量(0到1之間)。
  • left_volumeright_volume:分別控制左右聲道的音量。
  • SamplesPlayed:記錄已播放的音頻樣本數量。

這些信息可以幫助我們管理音頻的播放,并確保能夠正確地混合多個聲音。如果需要支持聲音的循環播放或更多功能,后續可以繼續擴展。

接下來,我們將通過鏈表管理這些 playing_sound 實例,并在每個更新周期遍歷鏈表,播放當前的聲音。
在這里插入圖片描述

game.cpp: 設置 PlayingSound 循環

在這個循環中,首先需要做的是從 game_state 中獲取到第一個正在播放的聲音。接著,檢查這個聲音是否有效。如果有效,就對這個聲音進行處理。每次處理完當前聲音后,就移動到鏈表中的下一個聲音,繼續處理所有正在播放的聲音,直到遍歷完所有聲音。

除此之外,還可以考慮提前排隊一些樣本數據。例如,我們可以允許設置已播放的樣本數為負數,這樣就能夠實現延遲播放的功能。通過這種方式,可以將某個聲音的播放推遲一段時間,比如延遲半秒鐘再開始播放。雖然這種延遲播放功能不一定是必要的,但如果需要實現,可以通過負值來標記推遲的播放時間。

接下來,對于每個需要輸出的音頻樣本,我們的目標是將這些聲音樣本輸出到最終的音頻緩沖區。最初的循環僅僅是將16位的樣本數據復制到16位的輸出數據中,這種方式雖然簡單,但可能會導致音頻樣本值的裁剪(clipping)問題。如果我們直接使用16位的值作為中間緩沖區來累加音頻樣本數據,可能會遇到溢出或裁剪問題,因為累加的音頻值可能超出了16位的表示范圍。

為了避免這種問題,我們可以考慮使用更高精度的緩沖區(比如32位或更高位深的緩沖區),來存儲音頻樣本的中間累積值。這樣做可以減少裁剪的概率,并且在最終將這些累積值轉換成16位輸出時,能夠更好地控制音頻的動態范圍,避免失真。

總結來說,在輸出音頻樣本時,需要確保處理鏈表中的所有播放中的聲音,并在處理過程中考慮延遲播放、避免裁剪等問題。最終的目標是通過更高精度的緩沖區避免音頻失真,并將處理后的樣本正確輸出。
在這里插入圖片描述

在這里插入圖片描述

Blackboard: 在32位深度混音緩沖區中工作

在進行聲音混合時,如果我們使用16位音頻進行處理,可能會遇到裁剪(clipping)問題。假設有兩個聲音,第一種聲音的波形第二種聲音的波形當將這兩種聲音相加時,結果會超出16位的表示范圍,導致裁剪發生。即使將兩種聲音疊加,它們的值可能會超出16位的最大值,從而發生失真。

然而,如果我們再加入第三種聲音,假設第三種聲音當我們將它與前兩個聲音相加時,第三個聲音的波形可能會減弱前兩個聲音的影響,使得最終的結果回到16位范圍內。這樣就避免了裁剪問題。也就是說,多個聲音的疊加并不一定總是會導致裁剪,因為不同聲音的波形可能會互相抵消,從而將總和控制在可表示的范圍內。

因此,通常在處理16位音頻時,為了避免裁剪問題,應該在32位精度的空間中進行混合處理。這樣能夠確保即使多個聲音疊加,最終結果也不會溢出16位的范圍。

在實現過程中,使用浮點數(float)是一個不錯的選擇,因為浮點數能夠更方便地進行動態范圍調整和其他的調制操作。在混合過程中,我們可以將16位音頻數據轉換為浮點數進行處理,完成混合后再將結果轉換回16位,這樣能夠確保音頻的質量并避免裁剪。

總結來說,處理音頻時,最好使用32位浮點數作為中間緩沖區,先將16位數據轉換為浮點數進行混合,最后再將混合后的結果轉換回16位數據輸出,這樣能夠避免音頻裁剪并保持較好的音質。

game.cpp: 引入 RealChannel0 和 RealChannel1

在這個過程中,我們希望能夠創建一個真實的聲音緩沖區,并且這個緩沖區是針對每個聲道的。對于立體聲來說,我們會有兩個聲道,即左聲道和右聲道。所以,我們需要為每個聲道分別處理緩沖區的數據。

為了實現這一點,我們將為每個聲道分配一個浮點數類型的緩沖區,因為使用浮點數有助于我們在混音過程中避免裁剪問題。每個聲道的緩沖區將用來存儲混合后的音頻數據。

具體步驟如下:

  1. 初始化緩沖區:我們將為每個聲道(例如左聲道和右聲道)創建一個緩沖區。每個緩沖區中的數據將以浮點數的形式存儲,這樣我們可以避免16位音頻格式可能帶來的裁剪問題。

  2. 數據轉換:從加載的聲音文件中提取每個樣本,首先會將其轉換為浮點數格式。然后,使用浮點數表示的緩沖區進行混合操作。

  3. 混合處理:我們將逐個采樣地處理每個聲音的樣本,并將它們寫入到相應的聲道緩沖區中。通過這種方式,我們在混音過程中能夠保持較高的音質,并且避免在處理多個聲音時出現裁剪現象。

  4. 最終輸出:混合操作完成后,我們將得到一個包含所有聲音數據的緩沖區,它們已經被轉換為浮點數格式,并進行了合適的混合。接下來,我們可以將這些數據轉換回16位音頻格式,準備輸出到音頻硬件。

在這里插入圖片描述

game_asset.h: 引入 GetSound

在這個過程中,需要先確保有一個獲取音頻數據的功能。首先,代碼中并沒有直接實現獲取聲音的函數,因此需要添加一個新的函數來獲取聲音。

步驟如下:

  1. 創建獲取聲音的函數:目前代碼中并沒有 GetSound 函數,因此需要編寫這個函數。該函數的作用是根據聲音的 ID 獲取對應的音頻數據。

  2. 調用獲取函數:在處理播放的聲音時,需要通過該 GetSound 函數獲取到對應的聲音數據。通過聲音的 ID 提取出聲音文件數據,為接下來的混音操作提供數據來源。

  3. 使用聲音 ID:每個播放中的聲音都有一個唯一的聲音 ID,通過聲音 ID 可以訪問到對應的聲音資源。這個 ID 存儲在每個播放中的聲音對象里,作為獲取該聲音數據的關鍵。

在這個步驟中,最重要的是確保獲取音頻數據的功能能夠正常工作,以便后續對音頻進行混音和處理。
在這里插入圖片描述

game.cpp: 調用 GetSound 并在循環中對樣本進行求和

首先,加載聲音的過程需要從資源中獲取。資源會存儲在 ttransient_state(瞬態狀態)中,具體來說是它的 groupsTransientStorage 中,存儲了所需的音頻資產。獲取聲音時,需要檢查LoadedSound是否已加載聲音,如果未加載,便無需繼續處理。若未能獲取到聲音數據,則可以跳過相關處理。

為了確保聲音在播放時不出現突兀的響聲,建議在加載音頻時調用 LoadSound 函數,這樣可以確保音頻盡快準備好播放。另一方面,為了避免聲音出現“爆音”現象,可以在播放前設置音量漸入效果,逐漸將音量調到正常水平。實際上,最好的方法可能是延遲播放,直到音頻文件加載完畢,避免音頻未準備好就開始播放。

處理過程中,對于未LoadedSound,使用一種方法延遲音頻播放,直到所有數據準備完畢。通過調整播放進度和(播放樣本數),可以避免音頻處理錯過任何樣本。這個策略可以確保音頻的順暢播放,而無需擔心遺漏或不準確的播放。

在聲音加載完成后,需要遍歷音頻的樣本并進行加總。每個樣本的處理方式是,首先將該樣本的值與當前通道的Volume音量值相乘,再進行加總。每個通道的音量(例如 Volume0 和 Volume1)應該提前提取并清晰標記,以便確保音量的控制在正確的通道中應用。

要從加載的聲音樣本中提取特定樣本值時,可以通過計算樣本的索引位置來獲取該樣本。每個音頻樣本的位置由 SampleIndexSamplesPlayed(播放樣本數)決定,因此需要將它們相加得到準確的樣本索引。

另外,在處理音頻時,必須考慮到邊界情況。例如,可能需要處理立體聲的特殊情況(stereo)。這種特殊情況會影響到如何處理樣本值,特別是在多個通道或樣本位置不一致時。

處理聲音加總的過程時,如果多個聲音同時播放,我們需要保證清除舊的音頻數據,否則加總結果可能不準確。特別是在每次新的音頻播放開始前,需要先清空音頻通道中的原有數據,確保加總過程從零開始。這樣,音頻加總將不會受到先前數據的干擾。

為了簡化初步實現,避免對第一次播放的聲音進行特殊處理,可以在每次處理時統一方法,即對每個聲音的處理方式一致,而無需對首個聲音進行特別優化。為了確保加總結果正確,可以在加總前將所有的通道數據清零。

以上步驟可以確保音頻的加載、播放、加總和音量處理能夠平穩進行,從而避免音頻播放時出現卡頓、爆音或其他異常情況。
在這里插入圖片描述

game.cpp: 循環遍歷求和后的聲音,從中讀取并將其寫入 SampleOut 緩沖區

接下來,我們需要完成一個循環來處理所有的音頻數據。具體來說,現在已經有了所有聲音的加總結果,并且這些加總后的音頻數據已經存儲在內部緩沖區中。接下來的任務是從這些緩沖區中讀取出加總后的值,并將其寫入輸出緩沖區。

首先,我們需要確保音頻數據從內部緩沖區正確地轉換并輸出為16位的音頻數據。具體來說,就是從每個通道的源緩沖區中讀取數據,然后進行四舍五入,確保數據格式正確。四舍五入后的值將轉化為16位整數,這就是最終要寫入輸出緩沖區的音頻數據。

在左聲道和右聲道中,分別從源緩沖區(如源0、源1)讀取數據,并進行四舍五入。這一步是為了確保輸出的音頻數據精度符合16位的要求,并避免任何意外的截斷或誤差。

在數據處理過程中,原先的測試索引已經不再需要,因為現在不再依賴于這些索引來處理音頻,而是直接通過內存中的加總數據進行處理。

接下來,需要確保處理過程的區域是有效的。我們需要約束處理的音頻樣本范圍,避免越界或無效的內存訪問。為此,可以從瞬態內存(transient memory)中分配臨時內存,以確保內存管理的正確性。通過從瞬態內存中分配內存塊,確保了我們在混音過程中有足夠的內存來存儲數據,并且內存釋放也會在混音完成后自動進行。

為了管理這些臨時內存,可以創建一個臨時的音頻混音緩沖區,并在其中進行樣本的處理。這些緩沖區將用于存儲混音過程中所需的數據。每當需要使用內存時,從瞬態內存中申請相應的內存塊,處理完后再釋放回去。

在此基礎上,清理緩沖區中的音頻數據,以便開始新一輪的混音操作。然后對所有聲音進行加總,最后將加總后的數據轉換為16位格式,確保音頻數據符合輸出要求。

總結一下,這個過程的核心包括:

  1. 從源緩沖區讀取音頻數據并進行四舍五入處理;
  2. 將四舍五入后的數據轉換為16位整數;
  3. 在處理過程中管理內存,確保臨時內存的正確分配和釋放;
  4. 對所有聲音進行加總,并將結果寫入輸出緩沖區。

這些步驟確保了音頻的正確處理,避免了數據溢出或內存錯誤,并確保最終輸出的音頻數據格式正確。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

game.cpp: 引入 SamplesToMix 和 SamplesRemainingInSound,以處理音頻的有限性

目前,混音器的主要部分已經基本完成,接下來需要處理一些細節問題,特別是關于聲音時長的管理。

首先,需要考慮聲音的時長問題。當進入音頻處理循環時,每個加載的聲音LoadedSound都具有特定的樣本數量(SampleCount)。在處理過程中,如果樣本索引超出了這個樣本數量,意味著已經到達了聲音的末尾。如果繼續讀取超出范圍的數據,可能會訪問到錯誤的內存區域。雖然不會導致程序崩潰(因為內存通常是連續分配的),但會導致音頻流中出現錯誤的數據,從而產生雜音或其他不良效果。因此,需要一種機制來判斷聲音是否已經播放到結尾,以防止這種情況的發生。

為了解決這個問題,可以引入“待混合樣本數”(SamplesToMix)的概念。初始時,待混合的樣本數量默認等于輸出緩沖區的大小。但在計算時,需要檢查實際可用的樣本數,即當前聲音剩余的樣本數。具體計算方式如下:

  1. 計算當前聲音已經播放的樣本數 SamplesPlayed
  2. 計算該聲音的總樣本數 SampleCount
  3. 通過 SampleCount - SamplesPlayed 得到該聲音還剩余多少樣本可以被混合SamplesRemainingInSound。

如果待混合樣本數大于該聲音剩余的樣本數,則需要調整待混合樣本數,使其不會超出實際范圍。然后,在處理循環時,就可以使用這個調整后的 SamplesToMix,避免訪問超出范圍的內存。

調整播放樣本數后,還需要進一步處理播放列表中的聲音。當混音完成后,需要更新 SamplesPlayed,即增加已播放的樣本數量。如果發現某個聲音已經播放到了末尾,就應該從播放列表中移除它。否則,它會一直留在列表中,導致播放列表不斷膨脹,占用不必要的資源,甚至可能影響程序性能。因此,需要在適當的時機清理已播放完畢的聲音。

這部分處理的核心步驟包括:

  1. 計算剩余樣本數:確定當前聲音還有多少樣本可以播放。
  2. 調整待混合樣本數:確保不會超出聲音的實際數據范圍,避免訪問無效內存。
  3. 更新已播放樣本數:記錄已經混合的樣本數量,確保下一幀的計算正確。
  4. 移除播放完畢的聲音:如果某個聲音已經完全播放完畢,就將其從列表中刪除,以保持播放列表的整潔和高效。

這一機制保證了混音過程的穩定性,同時優化了資源管理,使得播放列表不會無限增長,提高整體性能。
在這里插入圖片描述

game.h: 在 game_state 中添加 playing_sound *FirstFreePlayingSound

為了管理播放列表,需要在播放結束時從列表中移除已播放完畢的聲音,并將其加入一個空閑列表(free list),以便后續復用。當需要播放新的聲音時,可以直接從空閑列表中取出一個已釋放的聲音對象,而不是重新分配新的內存,從而提高資源利用率和性能。

具體來說,在移除聲音時,首先要確保它不會再被使用,然后將其指向空閑列表的頭部(例如 FirstFreePlayingSound)。這樣,下一次需要播放新聲音時,可以直接從空閑列表中取出一個已有的結構,而不必重新創建新的實例。這種做法減少了內存分配和釋放的開銷,提高了播放管理的效率。
在這里插入圖片描述

game.cpp: 在 PlayingSound 循環中使用 FirstFreePlayingSound

為了優化聲音播放管理,在移除已播放完畢的聲音時,需要將其加入空閑列表,以便后續復用。具體實現方式是利用鏈表結構,將移除的聲音節點添加到空閑列表的頭部,這樣可以快速回收并復用內存,而無需頻繁申請和釋放新內存。

在執行這一操作時,需要使用 PlayingSound 結構中的 Next 指針。具體步驟如下:

  1. 取出當前游戲狀態中的 FirstFreePlayingSound,即當前空閑列表的頭部。
  2. 將即將被釋放的 PlayingSound 節點的 Next 指向 FirstFreePlayingSound,將其鏈接到空閑列表的最前端。
  3. 更新 FirstFreePlayingSound 指針,使其指向新的空閑節點,即剛剛釋放的 PlayingSound

通過這種方式,可以確保被移除的聲音不會被遺忘,而是被回收到空閑列表中,等待下一次需要播放新聲音時直接復用。這不僅減少了內存分配和釋放的開銷,還提高了聲音管理的效率,使得播放列表不會無限增長,同時減少了系統資源的浪費。

在這里插入圖片描述

game.cpp: 提前固定 Next 指針,以防止其被釋放時影響循環迭代

在遍歷播放列表時,如果在循環過程中移除當前正在處理的聲音對象,會導致迭代出現問題。因為 playing sound 被移除后,其 next 指針指向的內容可能已經改變,導致后續遍歷發生錯誤。因此,需要在移除之前先保存 next 指針,以確保迭代能夠正確進行。

具體實現方式如下:

  1. 提前獲取下一個播放聲音節點
    在處理當前 PlayingSound 之前,先將 Next 指針保存到一個獨立的變量 NextPlayingSound,這樣即使當前 PlayingSound 被移除,其 Next 仍然可以被訪問。

  2. 在循環結束時使用已保存的 Next 指針
    在完成當前節點的處理后,使用預先存儲的 NextPlayingSound 作為新的迭代對象,而不是直接訪問 PlayingSound->Next,這樣就不會受到節點被釋放的影響。

這種方法在需要從鏈表中移除元素的迭代過程中非常常見,主要目的是防止因節點刪除導致的訪問錯誤,提高代碼的穩定性和可維護性。

此外,還添加了一個斷言,確保 PlayingSound->SamplesPlayed 始終大于等于 0。這樣如果未來需要支持聲音延遲播放,代碼會提醒需要對混音邏輯進行額外調整,以正確處理延遲情況。目前暫時不支持延遲播放,但為后續擴展留出了可能性。
在這里插入圖片描述

game.h: 引入 MetaArena 概念

當前代碼中存在一些需要解決的問題,尤其是在內存管理方面。

1. 現存的問題:游戲狀態的內存管理

目前在 game_state 中存在兩種不同的內存分配區域:

  • 世界(World)相關的內存區域
    這個區域用于存儲與當前世界(游戲場景)相關的數據,當世界被銷毀時,該區域的所有數據都會被釋放。
  • 長期存活(Meta)區域
    這個區域存儲一些需要在多個世界之間持久存在的數據,例如音樂或音效。因為當玩家從一個世界退出回到主菜單時,背景音樂不應該停止,因此音效不應與世界的內存區域綁定,而應該屬于一個更持久的區域。

目前并沒有正式區分這兩種內存區域,但未來可能需要引入 “世界內存”(會隨世界銷毀而釋放)和 “元內存”(會跨世界持續存在)來更好地管理游戲狀態中的不同數據。雖然現在可以暫時不處理這個問題,但之后應該在架構層面進行改進,以適應更復雜的游戲需求。


2. 聲音加載邏輯尚未完成

當前的 load_sound 相關代碼似乎尚未完全實現,存在以下問題:

  • load_sound 函數被調用后,尚未正確地在 asset_system 中完成音效的加載。
  • 具體來說,SoundInfos 變量的 FileName 是否正確賦值仍存疑,可能導致加載邏輯無法正確執行。
  • SoundInfos(音效信息)數組似乎沒有被正確填充,導致聲音數據實際上并未正確加載到內存中。
  • sound first 變量目前也沒有正確指向任何數據,這意味著音效數據結構的初始化可能未完成。

3. 可能的解決方案

  • 內存管理優化

    • 需要在 game state 中正式引入 長期存活的內存區域(Meta Arena),將跨世界存活的音效存儲其中。
    • 目前的 World Arena 仍然保留,用于存儲隨世界創建和銷毀的臨時數據。
    • 未來可能還需要引入 動態管理機制,例如在 Meta Arena 中進行手動釋放,以避免長期運行導致的內存泄露。
  • 改進聲音加載邏輯

    • 需要確保 load sound 代碼能夠正確解析 sound info 并填充相應的數據結構。
    • 應該檢查 SoundInfos 是否在 asset system 中正確初始化并存儲音效數據。
    • 如果 Info 變量的 FileName 未被正確賦值,需要找到 LoadSound 的具體調用流程,并確保 FileName 的來源正確。

4. 結論

目前的代碼在內存管理和音效加載方面仍然有一些未完成的部分:

  • 內存管理方面 需要區分 World ArenaMeta Arena,以確保音效數據不會因世界的銷毀而被錯誤釋放。
  • 聲音加載方面 需要檢查 LoadSound 相關代碼,確保 SoundInfos 被正確初始化并存儲音效數據。
  • 未來需要進一步完善 資源管理系統,包括加載、釋放、回收等機制,以提高整體穩定性和擴展性。
    在這里插入圖片描述

在這里插入圖片描述

game_asset.h: 將音頻資源添加到 asset_type_id

目前的代碼正在處理游戲中的音效資源,并嘗試將它們納入系統中進行管理。

1. 組織音效資源

在現有的資源管理中,已經有了一個存放 圖片(bitmaps) 的區域,現在需要以類似方式管理 音效(sounds)。因此,代碼中新建了一個 sounds 目錄,并開始往其中填充一些初步的音效資源。這些音效文件包括:

  • bloop //輕微的彈跳聲
  • drop //物體掉落
  • glide //滑行
  • music //背景音樂(BGM)
  • pup //獲取道具、能力提升(power-up)

這些音效文件可能只是測試用的資源,而非最終游戲中的正式音效,但目前會先將它們納入系統進行管理和測試。


2. 加載音效

  • 代碼正在嘗試從 sounds 目錄中讀取音效文件。
  • 其中一個音效文件體積較大(大約 30MB),但目前先默認接受它,不對大小進行特殊處理。
  • 具體的加載過程暫時未詳細展開,但整體目標是確保所有列出的音效都能被正確讀取,并在游戲中使用。

3. 未來的優化方向

  • 音效管理系統完善

    • 可能需要為音效創建一個 緩存系統,避免重復加載相同的音效文件,提高加載效率。
    • 需要考慮 音效格式的轉換或壓縮,特別是大體積的音效,可能會影響加載速度和內存占用。
  • 音效的分類和使用

    • 目前的音效名稱(bloopdropglide 等)可能只是測試用,最終需要根據游戲需求重新設計音效庫。
    • 未來可能會引入 不同類型的音效(背景音樂、環境音效、UI 交互音效等),并對其進行分類管理。
  • 動態音效加載

    • 如果游戲存在大量音效,可能需要動態加載和卸載,而不是一次性全部加載到內存中,以優化資源管理。

4. 結論

當前代碼已經初步引入了 音效資源管理,并將部分測試音效納入系統。雖然目前只是簡單地將目錄中的文件加載進來,未來仍需要對 音效格式、加載方式、內存管理 等方面進行優化,以提升游戲的音效系統穩定性和性能。
在這里插入圖片描述

game_asset.cpp: 擴展資源加載,處理音頻

當前的任務是將音頻資源的處理方式擴展,使其與之前的資產系統保持一致。現階段,我們尚未定義完整的資源包文件格式和目錄結構,因此暫時通過代碼直接構造音頻資產,后續會改為從磁盤加載。

目前,我們已經有了一些臨時音效文件:

  • bloop
  • drop
  • glide
  • music
  • pup

這些音頻文件暫時通過手動方式加入到資源系統,并按照索引進行管理,例如 bloop_00.wavbloop_01.wav 等。同樣地,Puhp 也有多個變體,如 Puhp_00.wavPuhp_01.wav。此外,music 作為背景音樂資源,也被加入到資產系統中。

由于當前還沒有正式的資源文件格式,因此這些數據的加載邏輯只是臨時的。目的是為了先驗證音頻系統的可行性,確保整個流程符合需求,而不是先花大量時間去設計一個完整的資源打包格式。如果后續發現當前方式符合需求,就會基于現有邏輯創建正式的資源文件格式,并刪除這些手寫的臨時代碼。

在這個過程中,我們的目標是:

  1. 模擬資源加載 —— 先手動構造數據,模擬最終的加載方式。
  2. 驗證功能 —— 通過手動數據確保音頻系統正確運行。
  3. 優化設計 —— 在功能穩定后,再進行資源文件格式和加載流程的優化。

最終,所有的音頻資源都會存儲在一個標準化的資源包文件中,而不是硬編碼在代碼中。
在這里插入圖片描述

在這里插入圖片描述

game_asset.cpp: 引入 AddSoundAsset

為了擴展當前的音頻資產管理系統,需要創建類似于“添加位圖資產” (AddBitmapAsset) 的功能,但用于音頻資源。具體來說,需要實現一個 AddSoundAsset 的功能來將音頻資產加入到資源管理中。

這個過程大致如下:

  1. 添加音頻資產:我們將創建一個類似于 AddSoundAsset 的函數來處理音頻資源的加入。這里不涉及對齊(alignment),因此不需要額外處理對齊的細節。
  2. 調試信息:與之前位圖資產的調試方式類似,我們需要添加調試信息來追蹤加載的音頻資產。比如創建一個 DEBUGAddSoundInfo 函數,來記錄音頻資源的加載狀態,幫助我們跟蹤音頻的加載和使用情況。
  3. 音頻計數管理:需要引入一個調試函數 DEBUGUsedSoundCount,用于管理和顯示當前使用的音頻資源數量。通過這個函數,我們能夠查看音頻資源的使用情況,確保沒有遺漏或錯誤。
  4. 索引管理:在加載音頻時,類似于位圖資源的索引,我們需要維護一個 SoundCount 來確保音頻資源的正確加載和訪問。這些索引將確保音頻資源不會被錯誤地覆蓋或者未正確加載。

為了確保所有的音頻資源在加載時不會出問題,音頻數據的加載系統會在這里進行調試和優化,確認加載過程沒有問題。需要注意,可能有些類型的索引(如樣本索引)需要處理為正確的類型(比如在代碼中處理為32位整數),避免發生不兼容的類型錯誤。

總之,現在添加音頻資源的功能已經準備好,理論上應該能順利地加載音頻資源,而不會出現問題。
在這里插入圖片描述

在這里插入圖片描述

game.cpp: 使新系統模擬流中的初始內容

現在的目標是確保新系統能有效模擬最初開始時的效果,確保在繼續開發其他功能之前,已經驗證了當前實現是否正常運行。因此,我們首先會在游戲開始時,模仿之前的音效加載方式,做一些初始化工作。

具體的步驟如下:

  1. 分配音效資源:在游戲開始時,我們需要分配一個音效資源,這個資源會被設置為正在播放的音效。在初始階段,將其分配到 WorldArena 中,這樣可以保證它在正確的位置,并且是臨時使用的,不會長期保留。

  2. 將音效資源連接到播放列表:將這個音效資源賦值給 FirstPlayingSound,這表示當前正在播放的音效。這樣做可以驗證音效的加載和播放是否正常。

  3. 測試加載和播放音效:現在我們測試的是新實現的功能是否正常。我們希望通過簡單地加載一個音效并播放,來確認我們之前實現的音效系統是否能夠正常工作。

  4. 改進和簡化代碼:代碼中出現了一些重復和不太清晰的部分,比如在加載時需要獲取第一個音效的 ID。為了避免代碼冗余,我們可以優化這些函數,確保它們能夠處理不同類型的資源(如位圖和音效)。本質上,我們希望這些函數能夠通用,不僅僅局限于處理位圖,還可以處理音效等資源。

  5. 類型安全性:雖然可以讓這些函數接受任何類型的資源,但為了增強類型安全性,可以在必要時使用包裝函數。這些包裝函數將為不同類型的資源提供更多的類型檢查,使得在整個游戲開發過程中,能更有效地處理和確保類型的正確性。

總之,當前的目標是確保音效系統能夠正確加載和播放,并且讓代碼更加簡潔和具有通用性,以便后續擴展和維護。
在這里插入圖片描述

game_asset.cpp: 引入 GetFirstSlotID 和各種獲取函數,用于位圖和聲音

接下來,我們希望對音效和位圖的處理代碼進行一定的改進和重構。具體來說,就是通過對現有的函數進行封裝,使它們能夠處理不同類型的資源,而不僅限于位圖。以下是我們要進行的一些改動和改進的步驟:

  1. 封裝函數:我們將對現有的 GetFirstBitmapID 這類函數進行封裝,創建一個 GetFirstSlotID 的函數,這個函數不再與位圖直接相關,而是通用地處理所有類型的資源。我們希望這些函數能夠在不考慮資源類型的情況下返回對應的插槽 ID,保持通用性。

  2. 修改相關函數:類似地,我們還需要對 GetRandomBitmapGetRandomSlot 等函數進行相應的修改,使它們能夠支持不同類型的資源處理,而不局限于位圖。例如,我們可以創建 GetRandomSound 函數,這樣可以通過相同的邏輯來處理音效和其他資源。

  3. 代碼重構:通過這種方式,原本與位圖相關的函數會變得更加通用,能夠支持音效和其他資源的操作。雖然這些修改看起來很簡單,但它們實際上提高了代碼的可維護性和擴展性,避免了代碼冗余。

  4. 改進命名:為了提高代碼的可讀性和明確性,我們還需要改進一些函數的命名。例如,將 GetFirstBitmapID 改為 GetFirstSoundFrom 或者 GetRandomFrom,這些命名更加符合實際功能。

  5. 測試和驗證:在修改了大量的代碼后,我們需要確保這些修改不會引入新的問題。我們將在調試模式下檢查代碼,確保所有的資源加載和播放操作都按預期工作,特別是在資源管理方面。

  6. 臨時處理:在實際操作中,我們先將音效資源添加到 WorldArena 中,并臨時分配播放的音效資源。然后,通過 FirstPlayingSound 來跟蹤當前正在播放的音效。雖然這些修改是臨時的,但它們有助于驗證代碼的有效性。

  7. 調試:經過上述改動后,我們在調試時遇到了一些問題,比如訪問沖突等錯誤。為了排查問題,我們需要深入分析和逐步調試,確保所有的資源都得到了正確的分配和使用。

總結來說,當前的目標是通過對現有函數的封裝和修改,使得代碼更加通用,并且能夠靈活地處理音效和其他類型的資源,同時確保新代碼能夠順利運行,不引入新的錯誤。

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

調試器: 步進查看資源加載代碼

我們開始分配資源并設置標簽范圍。在這過程中,遇到了一些問題,比如聲音計數有時小于預期的聲音數量。這時,我們調整了聲音資源的計數器,以確保它不會低于實際需要的數量。

具體步驟如下:

  1. 分配資源:首先,我們分配了音效資源,并開始設置相關的標簽范圍。這一步是為了確保音效能夠被正確加載和使用。

  2. 設置標簽范圍:標簽范圍是用來標識不同類型資源的位置和順序。通過設置這些范圍,系統能夠快速訪問和管理資源。

  3. 調整計數器:遇到的問題是聲音計數有時低于預期的聲音數量。這可能是因為在設置或分配資源時,計數器的值沒有正確更新。因此,我們調整了聲音計數器的值,以確保它與實際資源數量一致,避免了資源使用上的錯誤。

總結來說,通過分配和調整音效資源,以及修正計數器的設置,我們確保了資源管理系統能夠正確處理音效資源,避免了計數器值過低導致的問題。
運行發現段錯誤
在這里插入圖片描述

game_asset.cpp: 將 SoundCount 設置為 256 * Asset_Count

顯然,這樣的做法是行不通的。但這其實并不需要太過擔心,因為這些問題在當前階段并不重要。我們只需要確保它在現階段的功能是足夠的,畢竟,最終這些設置不會硬編碼在程序中,而是會通過包文件來設置。我們在打包時會準確知道包文件中有多少個聲音資源,因此在那個時候一切都會正常運作。
在這里插入圖片描述

調試器: 檢查 GameState->FirstPlayingSound

現在,我們可以查看一下游戲中的第一個播放聲音,看看它被設置成了什么。實際上,它已經有了一個有效的ID,這說明我們成功地找到了一個真實的聲音資源。希望這意味著我們確實成功地定位到了一個實際的聲音資源,并且它現在已經在系統中可用。
在這里插入圖片描述

game_asset.cpp: 將音量調到最大

現在考慮到音量的問題,顯然它不應該是零,因為如果我們想聽到聲音,我們必須給它一個非零的音量。因此,暫時將音量設置為最大。接下來,在其他代碼部分,我們可以查看混音器,確保現在有聲音數據實際傳遞給它。這樣做是為了確保系統能夠正常處理并播放音效。
在這里插入圖片描述

調試器: 步進進入 LoadedSound

需要加載音頻,因為它還沒有被加載。接下來,要確保獲取音量和目標通道,這些通道應該已經被清空。我們需要確認這一點,并查看混音器實際處理了多少樣本。檢查音頻的數據流,確保樣本值正確地積累并處理。如果音頻數據有值,它應該會被正確地累積。接下來,我們會繼續播放聲音并更新狀態,確保播放過程順利進行。所有這些步驟都看起來合理,接下來將繼續處理輸出緩沖區。
在這里插入圖片描述

build.bat: 切換到 -O2 并運行游戲

現在的目標是創建一個簡單的功能,允許我們輕松地觸發聲音。在之前的代碼中,我們已經為播放聲音做了相關的內存分配。現在的計劃是將這些分配代碼移回到合適的位置。雖然目前有一些關于內存分配的問題,比如是否應該進行內存分區,或是從操作系統動態分配內存,但現在并不是討論這些問題的時機。

我們現在的重點是能夠在“World Arena”中隨意分配聲音,暫時不考慮其他復雜的內存管理問題,直接假設能夠從World Arena 中分配所需的聲音資源。

game.cpp: 引入 PlaySound

接下來,要實現一個新的功能,允許通過 PlaySound 函數播放聲音。這個函數將接受一個聲音 ID 和一個游戲狀態,用來決定在哪個狀態下播放聲音。函數的工作流程是,首先創建一個新的播放聲音對象,并初始化它,就像之前的初始化過程一樣。然后,把這個播放聲音對象放到播放列表的頂部。

在此過程中,如果存在一個空閑的播放聲音對象,則使用它;如果沒有空閑的,則會創建一個新的播放聲音對象,并把它添加到空閑列表中。同時,每個新的播放聲音對象的 next 指針會設置為零,確保它不會被其他地方使用。

這段代碼的目的是確保每次播放聲音時,都能正確地管理播放聲音對象。如果沒有空閑對象,系統會自動創建一個新的并插入到空閑列表中。最后,這個操作應該可以啟動音樂并使其在游戲中播放。
在這里插入圖片描述

運行游戲并聽到我們的聲音

game.cpp: 在觸發劍時調用 PlaySound

為了測試音頻混合的功能,計劃在特定事件發生時播放聲音,比如當角色發動攻擊或其他動作時。在代碼中,考慮通過調用 PlaySound 函數來實現這一點。具體來說,將從一個已定義的資產列表中隨機選擇一個聲音,并播放它。目標是確保能夠通過隨機選擇的聲音來測試音頻播放的過程。

在實現時,發現 PlaySound 函數并不接受兩個參數,這與預期的調用方式不符。因此需要檢查這個函數具體需要多少個參數,并弄清楚應該傳入什么類型的參數。此外,考慮到可能還沒有定義一個用于選擇隨機聲音的系列,也需要檢查是否已經創建了一個隨機選擇聲音的序列。如果沒有,需要相應地進行處理。
在這里插入圖片描述

game.h: 向 game_state 中添加 random_series GeneralEntropy

在這個階段,發現系統沒有實現隨機序列功能,因此需要添加一個隨機序列來實現音效的隨機播放。具體來說,要在系統中引入一個隨機種子(random_series)機制,以便生成不同的隨機數序列。通過對代碼的檢查,發現游戲中已經有了隨機種子的實現方式。因此,可以利用現有的機制來生成隨機數。

為此,首先需要將隨機種子設置到游戲狀態中,并且確保該種子能影響到音效播放中的隨機行為。完成這些步驟后,應該就能順利地進行聲音的隨機播放測試。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

觸發劍時候段錯誤

必須得在播放聲音吧已經播放的聲音的樣本數設置為0才行
當播放新聲音時,需要重置所有相關參數,但 SamplesPlayed(已播放的采樣數)沒有被正確重置為零。這導致在后續的播放過程中,聲音的狀態可能不正確,進而影響音頻的播放邏輯。要修復這個問題,需要在 PlaySound 函數中確保 SamplesPlayed 被正確初始化為 0,以保證聲音從頭開始播放而不會出現異常行為。
在這里插入圖片描述

在這里插入圖片描述

game.cpp: 調查 bug

在檢查代碼時,發現存在一個問題。具體來說,在播放聲音時,未正確更新“下一個播放聲音”的指針。應當在播放完成后,將“PlayingSound->Next”指向游戲狀態中的“FirstFreePlayingSound”。這個指針沒有正確更新,導致播放聲音的處理沒有按照預期進行。

Blackboard: 鏈表

在代碼中使用了一個鏈表來管理播放的聲音。鏈表中的每個節點代表一個正在播放的聲音,包含指向下一個聲音的指針。問題出在鏈表中節點的連接處理上。當播放聲音結束后,鏈表的“next”指針需要正確地跳過已完成的聲音,指向下一個正在播放的聲音。然而,在實現過程中,只有鏈表中的一部分得到了處理,忽略了更新指向新節點的指針。這導致了鏈表的指針沒有正確連接,影響了聲音播放的管理。

game.cpp: 正確構建這個鏈表

在管理播放聲音的鏈表時,需要跟蹤每個節點的指針。每個“playing sound”都指向一個聲音,并且通過鏈表的“Next”指針連接下一個播放的聲音。為了解決指針更新的問題,應該在遍歷過程中保持對當前節點的指針,并在需要時通過更新“PlayingSoundPtr”來正確指向下一個節點。

具體來說,處理播放聲音時,需要確保在移除當前節點時,前一個節點的指針能夠指向下一個節點。這個操作的關鍵在于在移除一個節點時,將當前節點的“next”指針賦值給上一個節點指針,從而保持鏈表的完整性。這樣一來,不需要額外的操作來更新鏈表,只需要確保每次移除節點時,正確更新指針,鏈表自然會自動推進。

通過這種方式,鏈表的管理變得更加高效,并且避免了不必要的重復操作。
在這里插入圖片描述

調試器: 步進查看 playing_sound 鏈表

在這個過程中,目標是簡化播放聲音的循環。最開始時,操作的是指向“playing_sound”的指針,而現在是操作指向“PlayingSoundPtr”的指針的指針,即查看指針所在的位置。在這種方法中,首先需要獲取第一個播放聲音的指針,并從這個位置獲取當前的播放聲音。然后,其他的操作就按正常流程進行。

如果播放的聲音已經完成,應該進行一些額外的操作。首先,要確保記住當前節點的“前驅”指針,即指向當前節點的指針。然后,往前推進時,只需取出當前節點的“下一個”指針來替代當前節點的指針,繼續處理下一個節點。完成后,將當前節點放回空閑列表。

通過這種方式,鏈表的結構得到更新,并且指針的操作變得更加清晰、簡化。完成后,循環會自動繼續到下一個聲音,無需額外復雜的操作。

之前的一個錯誤

在這里插入圖片描述

梳理一下
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

運行游戲并聽到我們的聲音

我們確認了一些內容,調整了一些設置,現在一切看起來都已經恢復正常。

在回顧的過程中,我們重新設置了一些參數,并進行了測試。在播放音效時,出現了一個聲音,并不完全是我們希望在游戲中射擊時聽到的聲音。這意味著當前的音效可能不太符合預期,或者需要進一步調整,以確保射擊音效聽起來更符合游戲的氛圍。

此外,還聽到了一些額外的聲音,包括一些無關的聲音元素。這表明當前的音頻處理可能還存在一些問題,需要進一步優化或篩選,以避免多余或不恰當的音效混入最終的輸出。

雖然已經完成了主要的內容,但仍然有一些額外的工作需要進行,例如進一步調整音頻參數、優化音效播放機制,或者確保所有的聲音都符合游戲設計的要求。不過,總體來說,核心部分已經完成,只是一些細節仍需優化。

由于時間關系,接下來需要轉向其他任務,后續可以再繼續優化這些細節,以確保音效的最終效果符合預期。

聲音緩沖區是對數還是線性(分貝值還是線性值),我們是否需要在構建緩沖區的和時考慮到這一點?

我們討論了聲緩沖(sound buffer)的特性,主要關注其是線性的(linear)還是對數的(logarithmic),以及在計算混音總和時是否需要考慮這一點。

在分析過程中,我們確認了聲緩沖是線性的,而不是對數的。這意味著在進行音頻混合時,樣本的數值是以線性方式進行疊加的,而不需要進行對數運算或額外的非線性處理。這一點與某些音頻系統(如人耳感知的響度)不同,因為人耳對聲音的感知通常是對數的,但聲緩沖本身依然是線性存儲的。

由于聲緩沖是線性的,在進行混音時,可以直接相加各個聲音的樣本值,而不需要考慮對數縮放或其他復雜的變換。這使得音頻混合的實現更加直觀和直接,同時也符合一般的數字音頻處理方式。

最終,我們得出結論:聲緩沖是線性的,不是對數的,因此在構建混音總和時,無需額外考慮對數縮放的問題

你們已經有人在為游戲創作原聲帶了嗎?

我們討論了游戲的音樂和藝術資源的版權問題,以及未來可能的規劃。目前,游戲的配樂是通過授權獲得的,因此在版權方面受到了限制,無法隨意發布或修改。這意味著,我們無法將當前的音樂作為游戲源代碼的一部分發布,也無法將其歸入公共領域(public domain)。

未來是否會重新制作配樂,取決于游戲的銷售情況。如果游戲的收入足夠,可能會考慮聘請作曲家專門創作音樂,以便能夠完全掌控其版權,并將其作為游戲的一部分發布。這樣的話,音樂可以像游戲的源代碼一樣,被自由分發,甚至有可能進入公共領域。但目前尚不確定是否會有足夠的資金支持這個計劃。

相比之下,游戲的美術資源是完全自主創作并擁有全部版權的,因此可以隨時以任何方式發布。例如,可以選擇將部分美術資源公開,讓任何人都可以在自己的游戲中自由使用,就像游戲的代碼計劃進入公共領域一樣。不過,目前還沒有明確的計劃,只是希望保留這樣的可能性,以便未來可以自由決定如何處理這些資源。

提醒一下 InterlockedIncrement 中的參數順序及其掩蓋的 bug

我們一直想修復這個問題,但不確定這是否就是要修復的那個問題。之前已經注意到 InterlockedIncrement 相關的問題,但不確定這是否就是討論中的問題。

關于 InterlockedIncrement 中的參數順序,可能掩蓋了某個 bug。我們一直有意修復這個問題,但現在才真正開始處理。當前遇到的疑問是,是否在錯誤的地方復用了 InterlockedIncrement,特別是在 Queue->CompletionCount 上的使用方式可能存在問題。

在檢查時,發現 InterlockedIncrement 可能被重復使用,但不確定這是否就是導致問題的原因。為了進一步確認,需要回溯到之前討論的具體內容,以確保正在處理的是正確的問題。

game_asset.cpp: 處理 LoadBitmap 中 BeginTaskWithMemory 失敗的情況

我們不確定具體指的是哪個 bug,因此需要更具體的描述。我們其實是在考慮另一個問題,主要是在進行原子比較交換(atomic compare exchange)時會遇到的情況。問題出現在任務開始時,如果任務開始失敗了,我們希望能夠確保將資源的狀態恢復為“未加載”狀態。這樣做是因為,如果沒有恢復到“未加載”狀態,那么資源將進入隊列,但永遠不會被分配任務,因為隊列中沒有任務可以處理,結果資源就永遠無法加載。

這個問題雖然今天沒有討論到,但它確實是我們在論壇上曾經提到過的一個 bug,需要修復。我們需要確認是否有在相關代碼中加上這個修復。
在這里插入圖片描述

在這里插入圖片描述

將單聲道聲音混合到立體聲時,每個通道的音量應該是中間聲道的 50%

當缺少單聲道聲音時,如果每個聲部的音量設置為50%,那么它們會被放在中間聲道(center panned)。這個做法的原因似乎是,如果我們想讓聲音移動到另一邊,可以更容易地調整音量。例如,通過在兩個聲道之間平滑過渡,可以實現聲音逐漸向另一側推送。

然而,這樣的設置是否合理則取決于我們想要的效果。一個可能的默認設置是這樣做,因為在中間聲道時,兩個聲部的音量合并成一個,而50%能避免兩個聲音疊加時過于吵雜。雖然我可以理解這種方式的合理性,但我不確定是否完全認同這一點。因為如果想要聲音在中間時達到全音量,可能需要不同的調整方式。可能我更傾向于讓聲音在向其他聲道移動時逐漸變得較安靜,而不是保持一定的音量。

因此,雖然這可以作為一個默認設置,我仍然不確定是否完全適合,可能還需要進一步探討。

這個音頻代碼是否允許你同時播放相同的聲音(即,如果在第一次播放結束之前再次啟動相同的聲音)?

這段音頻代碼的目的是讓同一個聲音可以同時播放,也就是在第一次播放還沒有結束之前,啟動同一個聲音的第二次播放。從描述來看,代碼可能是通過允許在音頻還未完全播放完時再次啟動同一音頻來實現這個功能。

不過,這是否能夠實現取決于代碼具體的實現方式。一般來說,如果音頻播放系統允許同一個聲音實例同時多次播放,就應該能夠成功。但如果系統有一個限制,比如不允許同一個音頻文件在播放完成前被再次觸發,那么代碼就不會實現預期的效果。

因此,是否能成功播放同一個音頻文件取決于音頻播放機制的設計。假如允許多次播放同一聲音而不會覆蓋或沖突,那么就能實現并行播放;但如果音頻播放器在處理時會拒絕重復播放,或者音頻資源沒有正確管理,就可能會導致問題。

比較和交換中的第二和第三個參數

關于提醒的代碼,討論的是比較和交換(compare and swap)中的第二個和第三個參數。我們最初可能混淆了函數,誤以為是在討論另一個函數。實際上,我們談論的是原子比較交換(atomic compare exchange)。

現在,問題是在比較和交換的過程中,這兩個參數的順序是否正確。需要檢查一下代碼,看這兩個參數是否被放置在了錯誤的順序里。具體來說,原子比較交換操作的順序應該是:首先是預期值(expected),然后是新的值(new)。這個順序非常重要,因為它決定了交換操作的正確性。

game_intrinsics.h: 交換 AtomicCompareExchangeUInt32 中的參數,然后再交換回來,并改變其調用的函數

我們發現,之前的比較和交換(compare and swap)操作中,參數順序確實錯了,這導致了某些行為不符合預期。雖然代碼似乎能夠正常工作,但實際操作可能存在其他隱藏的 bug。我們決定進一步調查并確保這部分的實現是正確的。

為了避免混淆,包括自己在內的開發者更容易理解,我們打算將代碼調整為與 Windows 中的實現保持一致,因為 Windows 是采用這種方式的。這種做法可以避免產生混淆,特別是參數的順序。我們查閱了相關代碼,確認原子比較交換操作中的順序應該是:預期值(expected)放在最后一個位置,新的值(new)放在前面。

在我們期望的場景中,任務的狀態應該是“未加載”(unloaded),但我們發現系統意外地將狀態設為了“排隊中”(queued)。這顯然是由于參數順序錯誤導致的,然而,系統還是能夠返回“未加載”狀態,這其實是因為交換操作從未真正執行過。由于條件設置問題,操作總是會返回“未加載”狀態,這解釋了為何代碼在錯誤的實現下仍然能夠“正常工作”,但這種行為并不符合預期。

因此,調整代碼的參數順序后,應該能夠避免混淆,并且確保原子比較交換操作的正確性。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

目前音頻似乎依賴于幀率。是否可以將其放在單獨的線程中或使用中斷(如果可以的話),使其不再依賴于幀率?

音頻的播放似乎是與幀率相關的。如果將音頻放在一個單獨的線程中,或者使用中斷,可能有助于讓音頻不再依賴于幀率。

然而,在 Windows 上使用中斷并不太現實。不過,確實可以將音頻處理放到一個獨立的線程中,這樣可以減少音頻對幀率的依賴。然而,這樣做有一個問題,就是必須確保該線程不會被“餓死”,因為操作系統無法保證一定會及時喚醒這個線程。

通常的解決方案是,雖然幀率可能不穩定,但可以通過在音頻緩沖區中加入更多的音頻數據,確保下一個幀率周期的音頻數據足夠。如果幀率保持正常,那么可以將新音頻數據覆蓋到緩沖區中,如果沒有達到預期的幀率,則音頻可能會出現“跳躍”的現象,但這并不會對整體效果造成太大影響。

目前,這個問題不太需要擔心,因為理想情況下,游戲應該總是能夠保持穩定的幀率。在發布的游戲中,錯過幀率是非常罕見的,這是不應該發生的。然而,考慮到一些外部因素,例如后臺程序占用資源,偶爾可能會錯過某一幀。在這種情況下,可能需要采取一些措施來確保音頻處理不受影響,雖然這些措施暫時并不緊急。

你能為 gcc / clang 添加一個 __sync_val_compare_and_swap(Value, Expected, New) 嗎?

我們討論了是否需要添加一個 __sync_val_compare_and_swap,并且提到需要考慮 GCC 和 Clang 編譯器的支持。雖然可以完全實現這個功能,但目前不太記得具體的實現細節。

同時,我們還考慮是否已經在代碼中定義了其他編譯器相關的宏或者標識符。如果沒有,我們可能需要再檢查一下是否需要為其他編譯器添加相應的支持。

game_intrinsics.h: 添加 tfnw 的建議

討論的重點是關于添加 __sync_val_compare_and_swap,以及如何在 GCC 中處理相關的問題。首先,回憶起上次的實現,記得曾經用過某些特定的指令來處理這個問題。對于 GCC,我們需要確定使用哪種“屏障”指令來實現同步。

具體來說,提到如果想在 GCC 中實現這個功能,我們需要知道應使用什么指令來保證內存的同步。如果使用 volatile 關鍵字,它可以起到一定的屏障作用,這符合我們記得的做法。盡管這個方法有些不尋常,但它似乎是有效的。

接下來,我們計劃將 __sync_val_compare_and_swap 的功能添加到代碼中,并測試其是否按預期工作。我們會進行一些微調,確保它能正確地執行,然后將代碼提供給其他人下載和測試,看看實際效果如何。

如果還有其他問題,可以進一步調整和完善。

game.cpp: 播放音樂而不是 bloop,以演示同時播放相同的聲音

我們決定回答關于是否能夠在同一時間播放多個聲音的問題。為了演示這個功能,我們打算不播放平常的聲音,而是選擇一個非常響亮且持續時間很長的聲音進行測試。通過這個方法,可以清楚地展示系統是否能夠處理多個聲音同時播放的情況,確保音頻不會被覆蓋或中斷。

那為什么它能工作?

之所以能實現多個聲音同時播放,是因為播放聲音和實際的聲音數據是兩個完全獨立的概念。我們采用了將播放中的聲音與實際加載的聲音分開的方法,創建了一個單獨的播放聲音列表。這樣,我們可以在列表中添加任意多個播放條目,而這些條目可以都指向同一個底層的聲音緩沖區。

換句話說,播放的聲音是基于播放列表的數量,而不是加載的聲音數量。播放的循環是針對當前正在播放的聲音進行的,而不是針對所有已加載的聲音。這種方法使得可以同時播放多個聲音,即使它們指向相同的聲音數據。

Blackboard: 將資產數據與實例數據分開

我們實現了一個完全分離的系統,具體來說,加載的聲音和正在播放的聲音是兩個獨立的部分。加載的聲音存放在一個地方,而正在播放的聲音則在另一個地方。當我們加載一個聲音并將其添加到播放列表時,可以根據需要堆疊任意數量的播放聲音,這些播放聲音都指向同一段音樂。

每個播放聲音都保存了已經播放的樣本數量,從而能夠記錄每個播放實例在不同時間點的位置。這一點非常重要,因為它說明了每個播放實例的數據是獨立的,即使它們指向相同的聲音數據。

這里的核心概念是,應該始終將資源數據(例如聲音的定義)與實例數據(例如播放聲音的狀態)分開。這就像定義一個結構體類型,然后可以創建多個該類型的對象。我們也為每個播放聲音定義了一個結構體,并可以創建多個實例。同樣,對于聲音資源系統也是如此,我們加載了聲音文件并可以隨時播放它。通過這種方式,可以在不干擾其他播放實例的情況下,反復播放同一聲音。

這樣,系統就能支持同時播放多個相同的聲音實例,并且每個實例的播放位置和狀態都能獨立管理,確保系統的靈活性和高效性。

為什么使用鏈表而不是其他數據結構,比如vector?

使用鏈表而不是其他數據結構(如vector)的原因是,鏈表在隨機添加和移除元素時表現得更高效。vector在這方面存在一些局限性。具體來說,當你往vector中添加元素時,如果vector滿了,你可能需要重新分配整個vector,這樣會帶來較大的性能開銷。而當你刪除元素時,如果沒有做額外的空閑列表跟蹤,通常需要將整個vector壓縮,重新整理元素。

而鏈表則避免了這些問題,因為它在隨機添加和移除元素時非常高效,尤其是在不需要進行隨機訪問的情況下。在這種情況下,鏈表能夠很好地工作,因為它專門為頻繁的插入和刪除操作設計,不需要像向量那樣進行整個結構的重新分配或壓縮。

總的來說,如果不需要對數據進行隨機訪問,只需要頻繁地添加和刪除元素,鏈表是一個非常合適的數據結構,能夠提供更好的性能。

但是你能拿到聲音求和機制的輸出并播放它嗎?

如果需要,完全可以將聲音合成機制的輸出播放出來。沒有任何理由不能這么做。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/71521.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/71521.shtml
英文地址,請注明出處:http://en.pswp.cn/web/71521.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

golang并發編程如何學習

《掌握 Golang 并發編程的通關秘籍》 在當今的編程世界中,Golang 并發編程正以其獨特的魅力和強大的能力吸引著眾多開發者。然而,對于許多小伙伴來說,如何學好這門技術卻成了一個頭疼的問題。別擔心,今天就讓我來為大家揭開 Gola…

SpringMVC學習(controller層加載控制與(業務、功能)bean加載控制、Web容器初始化配置類)(3)

目錄 一、SpringMVC、Spring的bean加載控制。 &#xff08;1&#xff09;實際開發的包結構層次。 &#xff08;2&#xff09;如何"精準"控制兩個容器分別加載各自bean。(分析) <1>SpringMVC相關bean加載控制。(方法) <2>Spring相關bean加載控制。(方法) …

fastapi+mysql實現增刪改查

說明&#xff1a; 我計劃用python的fastapi框架&#xff0c;實現操作MySQL數據庫的表&#xff0c;實現增刪改查的操作&#xff0c;并且在postman里面測試 step1: 安裝數據庫依賴 pip install fastapi uvicorn pymysqlstep2:C:\Users\Administrator\PycharmProjects\FastAPIPro…

Linux系統之配置HAProxy負載均衡服務器

Linux系統之配置HAProxy負載均衡服務器 前言一、HAProxy介紹1.1 HAProxy簡介1.2 主要特點1.3 使用場景二、本次實踐介紹2.1 本次實踐簡介2.2 本次實踐環境規劃三、部署兩臺web服務器3.1 運行兩個Docker容器3.2 編輯測試文件3.3 訪問測試四、安裝HAProxy4.1 更新系統軟件源4.2 安…

CS144 Lab Checkpoint 2: the TCP receiver

Overview TCPReceiver 從對等的sender接收消息&#xff0c;使用 receive() 方法&#xff0c;然后調用 Reassembler() 方法&#xff0c;后者寫入 ByteStream 中 然后應用程序從 ByteSteam 中讀取。 同時&#xff0c;TCPReceiver 還會通過 send() 方法給sender發送消息&#xff…

Spring Boot 3.x 核心注解詳解與最佳實踐

Spring Boot 3.x 核心注解詳解與最佳實踐 前言 隨著Spring Boot 3.x的正式發布&#xff0c;這個基于Spring Framework 6的里程碑版本帶來了諸多新特性。本文將深入剖析Spring Boot 3.x的核心注解體系&#xff0c;結合代碼示例講解其作用及使用場景&#xff0c;助您快速掌握新…

PHP之常量

在你有別的編程語言的基礎下&#xff0c;你想學習PHP&#xff0c;可能要了解的一些關于常量的信息。 PHP中的常量不用指定數據類型&#xff0c;可以使用兩次方法定義。 使用const //定義常量 const B 2; echo B . PHP_EOL;使用define define("A", 1); echo A . P…

計算機網絡——子網掩碼

一、子網掩碼是什么&#xff1f;它長什么樣&#xff1f; 子網掩碼的定義 子網掩碼是一個32位的二進制數字&#xff0c;與IP地址“配對使用”&#xff0c;用于標識IP地址中哪部分屬于網絡地址&#xff0c;哪部分屬于主機地址。 示例&#xff1a;IP地址 192.168.1.10&#xff0c;…

Tomcat-web服務器介紹以及安裝部署

一、Tomcat簡介 Tomcat是Apache軟件基金會&#xff08;Apache Software Foundation&#xff09;的Jakarta 項目中的一個核心項目&#xff0c;由Apache、Sun和其他一些公司及個人共同開發而成。 Tomcat服務器是一個免費的開放源代碼的Web應用服務器&#xff0c;屬于輕量級應用…

分布式存儲—— HBase數據模型 詳解

目錄 1.3 HBase數據模型 1.3.1 兩類數據模型 1.3.2 數據模型的重要概念 1.3.3 數據模型的操作 1.3.4 數據模型的特殊屬性 1.3.5 CAP原理與最終一致性 1.3.6 小結 本文章參考、總結于學校教材課本《HBase開發與應用》 1.3 HBase數據模型 在開始學習HBase之前非常…

android中activity1和activity2中接收定時消息

android中activity1和activity2中接收定時消息 業務類 import java.util.Timer; import java.util.TimerTask;public class MyAnager {private MyAnager() {}private static MyAnager instance;//回調接口onRecvTaskpublic interface OnMsgListener {void onRecvTask(String a…

BitMap實現用戶簽到、UV統計

1. Redis 的 BitMap 概述 在 Redis 中&#xff0c;BitMap 并非一種獨立的數據結構&#xff0c;而是基于 String 類型數據結構實現的一種存儲方式。由于 String 類型的最大上限是 512M&#xff0c;換算成 bit 位就是 2^32 個&#xff0c;這決定了 BitMap 可操作的最大范圍。Bit…

共享模型之管程(悲觀鎖)

共享模型之管程&#xff08;悲觀鎖&#xff09; 文章目錄 共享模型之管程&#xff08;悲觀鎖&#xff09;一、常見線程安全的類二、對象頭三、Monitor&#xff08;監視器 / 管程&#xff09;四、偏向鎖偏向鎖的實現原理撤銷偏向鎖 五、輕量級鎖輕量級鎖的釋放 六、重量級鎖七、…

網絡安全ctf試題 ctf網絡安全大賽真題

MISC 1 簽到 難度 簽到 復制給出的flag輸入即可 2 range_download 難度 中等 flag{6095B134-5437-4B21-BE52-EDC46A276297} 0x01 分析dns流量&#xff0c;發現dns && ip.addr1.1.1.1存在dns隧道數據&#xff0c;整理后得到base64: cGFzc3dvcmQ6IG5zc195eWRzIQ 解…

centos7操作系統下安裝docker,及查看docker進程是否啟動

centos7下安裝docker&#xff0c;需要用到的yun命令 &#xff08;yum命令用于添加卸載程序&#xff09; 1.設置倉庫&#xff1a; yum-config-manager \--add-repo \http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 2.安裝 Docker Engine-Community yum in…

私有云基礎架構與運維(二)

二.私有云基礎架構 【項目概述】 經過云計算基礎知識及核心技術的學習后&#xff0c;希望進一步了解 IT 基礎架構的演變過 程&#xff0c;通過學習傳統架構、集群架構以及私有云基礎架構的相關知識&#xff0c;認識企業從傳統 IT 基 礎架構到私有云基礎架構轉型的必要性。…

Linux 系統不同分類的操作命令區別

Linux 系統有多種發行版,每種發行版都有其獨特的操作命令和工具。以下是一些常見的分類及其操作命令的區別: 1. 基于 Red Hat 的發行版 (RHEL, CentOS, Fedora) 1.1 包管理 安裝軟件包: bash復制 sudo yum install <package> 更新軟件包: bash復制 sudo yum update…

?PLC數據類型和?C#數據類型的數據類型映射表

數據類型映射表 ?PLC數據類型?C#數據類型讀取方式?補充說明BitboolDBX布爾值BytebyteDBB單字節無符號整數WordushortDBW16位無符號整數DWorduintDBD32位無符號整數Intshort16位有符號整數DIntint32位有符號整數RealfloatDBR單精度浮點數LRealdoubleDBL雙精度浮點數Stringstr…

windows部署spleeter 版本2.4.0:分離音頻的人聲和背景音樂

windows部署spleeter 版本2.4.0&#xff1a;分離音頻的人聲和背景音樂 一、Spleeter 是什么&#xff1f; Spleeter 是由法國音樂流媒體公司 Deezer 開發并開源的一款基于深度學習的音頻分離工具。它能夠將音樂中的不同音軌&#xff08;如人聲、鼓、貝斯、鋼琴等&#xff09;分…

QTS單元測試框架

1.QTS單元測試框架介紹 目前QTS項目采用C/C語言,而CppUnit就是xUnit家族中的一員,它是一個專門面向C的單元測試框架。因此,QTS采用CppUnit測試框架是比較理想的選擇。 CppUnit按照層次來管理測試,最底層的就是TestCase,當有了幾個TestCase以后&#xff0c;可以將它們組織成Te…