自從上週在光碟存檔挖到 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 播放測試

Using .NET 6 to create music data for Turbo Pascal program and play it on Windows.


Comments

# by Naiming

聽這音樂,起雞皮疙瘩惹,時光機啟動.....拉回Intel8088的時代。冬天打Tetris打到手凍僵、把PC版的Tetris的分數打到overflow,挑戰高手同學用組合語言寫俄羅斯方塊、恭逢brain病毒肆虐...好多回憶喔!謝謝版大...^^...

# by Joker

也有做過一樣的事情耶,剛開始學寫程式的時候有做過鍵盤鋼琴也是利用beep,很有趣 期待完成品

Post a comment