3

Coding4Fun - 復刻 DOS 遊戲音樂

 2 years ago
source link: https://blog.darkthread.net/blog/coding4fun-retro-dos-music/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Coding4Fun - 復刻 DOS 遊戲音樂

calendar.svg 2022-02-21 12:28 AM comment.svg 2 eye.svg 683

自從上週在光碟存檔挖到 30 年前寫的俄羅斯方塊遊戲,當局立刻成立古蹟修復小組,期望能讓半百老人重溫舊日時光。

初步探勘後發現嚴重問題,當年遊戲很花俏地加了背景音樂,但檔案沒有留下來。

DOS 時代電腦沒內建音效卡,只有能發出單一頻率音調的蜂鳴器(故障時響三聲那種,1987 年 AdLib 音效卡才問市,它是用 FM 合成音源類似 MIDI,1989 年推出的 SoundBlaster 才開始支援 PCM 播放預先錄製的音效,對這段歷史有興趣的朋友可參考這篇:30年前,想在电脑上听到正常的声音要额外掏200美元。),透過 API 控制它發出特定頻率音調,持續指定時間長度,倒也能演奏成單音旋律。找不到當年音樂檔,但從程式碼不難反推檔案規格。資料格式很簡單,以兩個 Integer 一組(Turbo Pascal 的 Integer 是 16 位元,兩個 Byte),第一個 Integer 代表頻率、第二個 Integer 是音符長度,休止符的頻率填 0,播放時則用耳朵聽不到的 30000Hz 高頻取代。有了這些資料,我決定上網找到俄羅斯方塊的樂譜,查音階對映頻率表轉成頻率,用 2022 年的科技復刻 1992 年程式要用的資料檔。

在 YouTube 找到大提琴(Cello)演奏俄羅斯方塊主題曲的影片,有附上 E3 B2 C3 D3 C3 B2 A2 A2 C3 E3 D3 C3... 格式的簡譜,影片則有五線譜可查音符長度,手動轉成文字樂譜:

E3:2 B2:1 C3:1 D3:2 C3:1 B2:1
A2:2 A2:1 C3:1 E3:2 D3:1 C3:1
B2:3 C3:1 D3:2 E3:2
C3:2 A2:2 A2:2 X:2
X:1 D3:2 F3:1 A3:2 G3:1 F3:1
...

接到找出 E3、A2 這些音調符號對映的頻率,網路上有對映表(這些 Google 一秒就有的答案,當年去圖書館、書店翻半天還不一定找得到),把它轉成 Dictionary<string, int> 便能將 E3 B2 C3 這些符號對映成頻率。

把以上資料組裝在一起,製作 30 年前 Turbo Pascal 程式音樂資料的 C# 程式就完成了。但我好奇,能在 Windows 直接播放測試結果嗎?查了一下還真的可以,Windows API 有 Beep 函式([DllImport("kernel32.dll")]public static extern bool Beep(int frequency, int duration);),.NET 主控台程式的話更方便,直接 Console.Beep(freq, duration) 就好,介面跟當年的 BASICA 的 SOUND() 幾乎一樣,只差在它是用音效卡模擬,連續播放時會喇叭結束有輕微爆音,但還勉強可聽。學會這招,順手在程式最後加一段,利用 Console.Beep() 演奏曲子。

using System.Text.RegularExpressions;

var noteFreqs = new Dictionary<string, int>()
{
    ["C2"] = 65, ["C#2"] = 69, ["D2"] = 73, ["D#2"] = 77, ["E2"] = 82,
    ["F2"] = 87, ["F#2"] = 92, ["G2"] = 98, ["G#2"] = 104,["A2"] = 110, ["A#2"] = 117, ["B2"] = 123,
    ["C3"] = 131,["C#3"] = 139,["D3"] = 148,["D#3"] = 156,["E3"] = 165,
    ["F3"] = 175,["F#3"] = 185,["G3"] = 196,["G#3"] = 208,["A3"] = 220, ["A#3"] = 233, ["B3"] = 247,
    ["C4"] = 262,["C#4"] = 277,["D4"] = 294,["D#4"] = 311,["E4"] = 330,
    ["F4"] = 349,["F#4"] = 370,["G4"] = 392,["G#4"] = 415,["A4"] = 440,["A#4"] = 466,["B4"] = 494
};

var rawMusic = File.ReadAllText("music.raw");
var timeUnit = 180;
var data = Regex.Matches(rawMusic, @"(?<n>[A-GX][#]*\d*):(?<l>\d)")
    .Cast<Match>().Select(m => {
    var n = m.Groups["n"].Value;
    n = Regex.Replace(n, "[23]", m => (int.Parse(m.Value) + 1).ToString());
    var l = int.Parse(m.Groups["l"].Value);
    var freq = n == "X" ? 30000 : noteFreqs[n];
    return (Name: m.Value, Freq: freq, Len: timeUnit * l);
});
using (var f = new FileStream("THEME.MSC", FileMode.Create))
{
    Action<int> writeInt = (v) =>
    {
        var b = BitConverter.GetBytes((short)v);
        f.Write(b, 0, b.Length);
    };
    int size = data.Count();
    writeInt(size);
    writeInt(0);
    foreach (var n in data)
    {
        writeInt(n.Freq);
        writeInt(n.Len);
    }
}
var c = 0;
foreach (var n in data)
{
    var p = n.Name.Split(':');
    Console.Write(p[0] + " ");
    c += int.Parse(p[1]);
    if (c >= 16)
    {
        c = 0;
        Console.WriteLine();
    }
    Console.Beep(n.Freq, n.Len);
}

成果發表:

Windows 播放測試

這年頭還花時間搞這種原始音樂資料格式是浪費時間?也不盡然,這個技術在某些場合還是很有用的,例如要在 ESP/Arduino 播放音樂的原理就很類似。所以不囉嗦,我把樂譜資料轉換成 C 語言陣列,上傳到 ESP 開發板播放,經驗值+1。(影片音量有點偏大,請小心)

ESP 播放測試

and has 2 comments

Comments

Post a comment

Comment
Name Captcha 71 - 64 =

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK