東方Project原作BGM相關(guān)文件探究
本文使用Markdown語(yǔ)言編寫,經(jīng)轉(zhuǎn)換為富文本后上傳為專欄。
Markdown原文見?https://blog.strmnl.top/posts/202307021905/?。

(本文主要討論的是彈幕作。格斗作不討論。)
(沒錯(cuò),這是我的上一篇專欄的重寫版本)
眾所周知,原作的游戲大小多在400~500MB(除了體驗(yàn)版)。而游戲文件里最大的都是一個(gè)名為thbgm.dat(體驗(yàn)版游戲中為thbgm_tr.dat)的文件(th07+)或一個(gè)名為bgm的文件夾(th06)。顯然這就是游戲BGM所在了。果然游戲只是音樂的載體而已。游戲BGM為什么這么大?它是如何儲(chǔ)存的?本文將討論原作BGM的相關(guān)文件格式。
目錄
為什么BGM這么大——BGM音頻部分的文件格式
不難發(fā)現(xiàn)東方紅魔鄉(xiāng)(搶曽峠杺嫿)的bgm文件夾里都是以th06_xx的格式命名的wav文件。嗯......這么pristine(原始)的不壓縮的音頻格式能不大嗎......
而從東方妖妖夢(mèng)起,bgm文件夾為thbgm.dat文件所取代。文件格式變了嗎?東方妖妖夢(mèng)~東方花映冢的游戲里仍然把音頻版的BGM音源叫做“WAV”,但thbgm.dat改個(gè)后綴名直接扔進(jìn)音頻播放器會(huì)發(fā)現(xiàn)打不開。
由這篇文章 (https://www.cnblogs.com/blumia/p/4282114.html) 可知,把thbgm.dat當(dāng)成原始音頻數(shù)據(jù)扔進(jìn)GoldWave、Audition等軟件里,便可以播放了。所以這個(gè)文件應(yīng)該仍然包含wav格式的音頻數(shù)據(jù)部分。
以星蓮船的thbgm.dat為例,扔進(jìn)HxD Hex Editor里,可以看到這樣的內(nèi)容:
通過(guò)觀察可以推測(cè),文件的結(jié)構(gòu)如下圖所示。

本質(zhì)上確實(shí)還是wav格式。
那么這就是“為什么BGM這么大”和“BGM如何儲(chǔ)存”的全部答案。
全文終......ん?
你說(shuō)游戲怎么從連續(xù)的音頻數(shù)據(jù)里找到每一首曲子的位置?
曲子在游戲里不是循環(huán)播放的嗎,游戲怎么知道這首曲子從哪到哪是循環(huán)部分?
原來(lái)的wav格式的文件頭里還有那么多正常播放需要的信息(比如采樣率、聲道數(shù)、位深度),怎么在thbgm.dat里沒有?
難道這些是硬編碼在游戲代碼里了嗎?
嗯......這些可能會(huì)改變的東西理論上不應(yīng)該硬編碼,尤其是循環(huán)點(diǎn),肯定是每次都要改的。如果不在thbgm.dat里,那么這一些應(yīng)該放在了另外的二進(jìn)制文件或者文本文件里。這樣便于修改,還可以寫個(gè)腳本一鍵生成,減少修改時(shí)的重復(fù)性工作。酒鬼正是設(shè)計(jì)了另外的文件來(lái)存放這些內(nèi)容。
還有一些信息去哪了——循環(huán)點(diǎn)與wav文件頭的儲(chǔ)存
紅魔鄉(xiāng)
BGM仍然使用最原始的wav格式的紅魔鄉(xiāng)不存在文件頭的問題,只需要解決循環(huán)點(diǎn)的問題。
用Touhou Toolkit中的thdat解包游戲目錄下的紅魔郷MD.DAT,得到的除了midi格式的BGM以外,發(fā)現(xiàn)還有一些和BGM同名的th06_xx.pos文件和一個(gè)musiccmt.txt。后者留到后面再做討論,前者應(yīng)該就是我們要找的循環(huán)點(diǎn)了。
以th06_01.pos為例,同樣用HxD打開:
可以猜測(cè),前一半是循壞開始點(diǎn),后一半是循環(huán)結(jié)束點(diǎn)(順便一提,數(shù)值的采用的字節(jié)順序是小端序 [注1] )。Touhou Wiki上的有關(guān)信息 (https://en.touhouwiki.net/wiki/Technical_Information/BGM) 表明確實(shí)是這樣:
loops in 紅魔郷MD.dat/*.pos
Format: <start sample>.4b <end sample>.4b
(你thb上的腳本對(duì)照表里怎么沒有這個(gè)x)
這個(gè)"sample"是指什么?由這個(gè)帖子 (https://tieba.baidu.com/p/4817367665/) 可以歸納出其數(shù)值的計(jì)算公式:
設(shè)曲目播放到第t秒時(shí)開始/結(jié)束循環(huán).
則循壞開始點(diǎn)/結(jié)束點(diǎn)的sample = 44100 * t(結(jié)果取整數(shù))
顯然44100即紅魔鄉(xiāng)所有曲目的采樣率44.1kHz。
那么結(jié)合大學(xué)《計(jì)算機(jī)基礎(chǔ)》(或者高中的《信息技術(shù)》)中音頻數(shù)字化的相關(guān)內(nèi)容 [注2] ,我們可以知道sample的取值n即表示“文件中的第 (n+1) 個(gè)采樣點(diǎn)”。
所以我們現(xiàn)在知道了,紅魔鄉(xiāng)的BGM循環(huán)點(diǎn)用采樣點(diǎn)表示,儲(chǔ)存在紅魔郷MD.DAT里的th06_xx.pos中。
妖妖夢(mèng)及妖妖夢(mèng)以后
這一部分就是這篇文章前兩版的內(nèi)容,自己看去。
妖妖夢(mèng)起,游戲目錄下的thxx.dat(xx為游戲編號(hào),體驗(yàn)版游戲?yàn)閠hxx_tr.dat)用thtk解包后會(huì)得到一個(gè)thbgm.fmt文件(體驗(yàn)版游戲可能是thbgm_tr.fmt)。
"fmt",不難想到wav文件頭里的FormatChunk [注3] 。而我們剛才所說(shuō)的wav文件“正常播放需要的信息”,其實(shí)就是wav文件頭里的FormatChunk的數(shù)據(jù)部分。
以th19的thbgm_tr.fmt為例,這個(gè)也用HxD打開看看:
發(fā)現(xiàn)第三行確實(shí)就是完完整整的FormatChunk的數(shù)據(jù)部分,一點(diǎn)不差:
那其他內(nèi)容呢?在HxD里可以看見,第一行對(duì)應(yīng)的字符便是我們?cè)诩t魔鄉(xiāng)里見過(guò)的文件名格式:th19_01.wav
第二行則是循環(huán)點(diǎn)相關(guān)。具體見下面整個(gè)文件的結(jié)構(gòu)。
在THBWiki上的腳本對(duì)照表 (https://thwiki.cc/-/19gc/#FMT%E6%96%87%E4%BB%B6) 用了一個(gè)C語(yǔ)言的結(jié)構(gòu)體來(lái)表示整個(gè)文件的結(jié)構(gòu),我覺得挺清晰的。唯一不清晰的點(diǎn)是沒有對(duì)語(yǔ)言做任何標(biāo)注,對(duì)C或類似編程語(yǔ)言不了解的人真看不出你在寫什么。
改了一下copy過(guò)來(lái):
Touhou Wiki上的說(shuō)明也貼上來(lái),這個(gè)比較簡(jiǎn)潔:
設(shè)前奏或全曲時(shí)長(zhǎng)為t秒,則對(duì)應(yīng) <intro length> 或 <total length> 的計(jì)算公式:
至于 <start offset> ,所謂的偏移量其實(shí)就是前面全部?jī)?nèi)容的length嘛。
因此,手動(dòng)獲取的話,可以采取前面提到的把thbgm.dat當(dāng)成原始音頻數(shù)據(jù)扔進(jìn)音頻編輯軟件里的做法。
在軟件中查看曲目開始的時(shí)間點(diǎn)的時(shí)間t,即為前面所有內(nèi)容的時(shí)長(zhǎng)t。
但是注意代入公式的參數(shù)應(yīng)與你打開文件時(shí)的設(shè)置一致,而不是thbgm.fmt里的參數(shù)。
軟件會(huì)把thbgm.dat的文件頭也當(dāng)做音頻數(shù)據(jù),所以算出來(lái)的結(jié)果已經(jīng)包含了文件頭的長(zhǎng)度。
但是,感覺不如寫個(gè)腳本,生成thbgm.dat的同時(shí)生成配套的thbgm.fmt。(
名與實(shí)的聯(lián)系——Music Room 中的曲名和文字
現(xiàn)在來(lái)看看剛才提到的musiccmt.txt。在thxx.dat解包后的文件里也發(fā)現(xiàn)了這個(gè)。
試著用記事本打開紅魔鄉(xiāng)的musiccmt.txt。打開后可以看到......
?
嗯......Windows自帶的記事本不管怎么翻新都是這么垃圾,在中文環(huán)境下打開Shift-JIS編碼的文件會(huì)亂碼。
換個(gè)編輯器打開就好了。
打開之后,可以發(fā)現(xiàn)主要是出現(xiàn)在Music Room里的文字,還有曲目對(duì)應(yīng)的文件名。
推測(cè)應(yīng)該是Music Room的曲目列表。
紅妖永
對(duì)照著Music Room來(lái)看,在紅魔鄉(xiāng)中(也適用于妖和永):
那一行全角數(shù)字不知道有什么用。
格式大體上是:
文件名雖然寫上了后綴.mid,但使用WAV音源時(shí)游戲應(yīng)該會(huì)匹配同名的.wav文件。
嘗試將mid改成任意的3個(gè)字母,仍然能播放;但是改成非3個(gè)字母,在紅魔鄉(xiāng)里就不能正常播放,而在妖妖夢(mèng)和永夜抄里可以正常播放。如果嘗試刪掉后綴名,紅妖永都會(huì)發(fā)生崩潰。
另外,上面的無(wú)法正常播放的情況,僅限于在Music Room里無(wú)法播放??梢酝茰y(cè)游戲進(jìn)程中,以及標(biāo)題畫面中,BGM的讀取并不受這個(gè)文件影響。
樂評(píng)正文的縮進(jìn)是每行前敲一個(gè)全角空格實(shí)現(xiàn)的。
另外,在永夜抄里添加了"Now Playing"的提示。內(nèi)容和 <樂評(píng)里的標(biāo)題> 是一樣的。

花映冢中的小改動(dòng)
可以發(fā)現(xiàn)相比紅妖永少了一行 **<Music Room列表里顯示的標(biāo)題>**。因?yàn)镸usic Room里的樣式改了。原來(lái)的 <樂評(píng)里的標(biāo)題> 不再顯示在樂評(píng)中,只顯示上方在“再生中”的文字旁,列表里的標(biāo)題直接使用了“再生中”旁的文字。
縮進(jìn)變?yōu)榱嗣恳痪涞拈_頭用兩個(gè)全角空格,句中換行用一個(gè)全角空格。
沒有midi的曲目的后綴名仍然是.mid。后綴名相關(guān)特性同妖永。

(題外話:為什么花映冢Music Room里新加的暫停和淡出到后面又沒了)
風(fēng)神錄中的小改動(dòng)
前面添加了三行。
"#"是注釋嗎?似乎不是。全角數(shù)字那一行的內(nèi)容變了,前面也加了個(gè)"#"。
(數(shù)字里第1個(gè)0就是半角的,并不是我復(fù)制時(shí)候出錯(cuò)了。而且一直到獸王園這個(gè)半角的0都沒改過(guò)來(lái)......)
隨著MIDI音源的徹底移除,文件名的后綴改成了.wav。但后綴名相關(guān)特性仍同妖永花。
其余部分又回到了之前的三行的格式。但是原本的 <樂評(píng)里的標(biāo)題> 的格式繼續(xù)用在列表中,現(xiàn)在的樂評(píng)中的標(biāo)題前面加了個(gè)"?"(于是在游戲里和正文對(duì)齊了)。
另外樂評(píng)中的空行上其實(shí)都有一個(gè)全角空格。沒有全角空格的行讀取時(shí)會(huì)被忽略。

神靈廟中的小改動(dòng)
文件名的后綴去掉了。嘗試加上后綴名無(wú)法正常播放,但不會(huì)崩潰,而是會(huì)播放標(biāo)題曲。
列表中的標(biāo)題的"No."與數(shù)字之間加了一個(gè)半角空格。
此外,嘗試將瘡痍曲加進(jìn)來(lái)時(shí),可以播放,但總是會(huì)顯示為沒有在游戲進(jìn)程中播放過(guò)的曲目,如下圖所示。
暫時(shí)不知道游戲是怎么記錄有沒有播放過(guò)的。最后一行是我剛才說(shuō)“#似乎不是注釋”的原因。


[注1]: 將一個(gè)多位數(shù)的低位放在較小的地址處,高位放在較大的地址處,則稱小端序。
例如在這一處,值0x000409a7被記為了a7 09 04 00。
后文提到的大端序則會(huì)反過(guò)來(lái)記為00 04 09 a7,和我們從左往右的閱讀順序一致。
[注2]: 在音頻數(shù)字化的三個(gè)步驟“采樣——量化——編碼”中,“采樣”時(shí)每秒采樣的次數(shù)稱為采樣率。故采樣率 * 秒數(shù)即表示使用這一采樣率時(shí),時(shí)長(zhǎng)為這一秒數(shù)的音頻包含的采樣點(diǎn)個(gè)數(shù)。
[注3]: 該chunk開頭三個(gè)字節(jié)便是字符"fmt"。