很久沒有 Coding4Fun,想玩個有趣的題目 - 用 C# 無中生有產生聲音檔。

做了研究,.wav 檔用未壓縮的 byte[] 記錄聲音波形,只要依規範在檔案開頭填入聲道數、取樣頻率、解析度位元數... 等資訊,輕鬆就能在電腦上生出一段正弦波。(謎:要多無聊才會幹這種事?到底)

不過,大費周章讓電腦播放正弦波聲音太無趣,我想到一個讓它更好玩的點子 - 寫一個 C# 小工具將文字轉成摩斯電碼! (謎:好像並沒有比較好)

我從小就覺得摩斯電碼很酷,能靠一條電線跟簡單裝置傳送複雜訊息,以現在的眼光看當然沒什麼,隨便一支智慧型手機都能用 1/100 時間傳 100 倍資訊到千里之外,但在那個網路還不發達的年代,可以自己動手實現電子傳輸是很神奇的事。當年我有一套類似動動腦的電子套件玩具,其中有個按鍵點亮燈泡的模組,模組上還印了摩斯電碼表,記得我還抄了一份放在書包立志要背起來,但如大家所猜,沒多久它便與其他一百條志願一起消逝在風中。

此生第二次與摩斯密碼近距離接觸是在當兵,當時隊上有搞通訊的海軍弟兄,聽電報是本職學能,偶爾會看到他們聽著錄音機「嘟嘟嘟...嘟嘟...」,緊張兮兮地練習抄報,擔心沒考好被禁假,是我第一次聽到真實的電報聲。(當年沒有 Google 大神,長見識全靠機緣)

第三次與它相遇,這回我要寫程式把文字轉成摩斯電碼播放出來。

文字轉摩斯電碼不是什麼深奧技術,在 Github 就有 Python 範例,甚至有網站可以線上轉換 - Play! Morse Code

那... 還需要自己寫嗎?Why Not?哈!

先看成果:

展示影片

影片右下角的手機上跑的是一個可以聽摩斯電碼轉成文字的 App - Morse Code Reader,用來驗證程式產生的摩斯電碼是否標準。

核心程式如下,包含字元與摩斯電碼映表 Dictionary,一個將字串轉成 000111010101000... 格式轉換函式,每個 0, 1 為固定時間長度,0 代表無聲、1 代表發聲,111 是長音,1 是短音,以此類推。對大家較有參考價值的是 CreateWav(),示範用 byte[] 從無到有建立 .wav 檔。

public class MorseCodeModule
{
    //REF: https://github.com/cduck/morse
    static Dictionary<char, string> MorseCodeTable = new Dictionary<char, string>
    {
        ['A'] = ".-",
        ['B'] = "-...",
        ['C'] = "-.-.",
        ['D'] = "-..",
        ['E'] = ".",
        ['F'] = "..-.",
        ['G'] = "--.",
        ['H'] = "....",
        ['I'] = "..",
        ['J'] = ".---",
        ['K'] = "-.-",
        ['L'] = ".-..",
        ['M'] = "--",
        ['N'] = "-.",
        ['O'] = "---",
        ['P'] = ".--.",
        ['Q'] = "--.-",
        ['R'] = ".-.",
        ['S'] = "...",
        ['T'] = "-",
        ['U'] = "..-",
        ['V'] = "...-",
        ['W'] = ".--",
        ['X'] = "-..-",
        ['Y'] = "-.--",
        ['Z'] = "--..",
        ['0'] = "-----",
        ['1'] = ".----",
        ['2'] = "..---",
        ['3'] = "...--",
        ['4'] = "....-",
        ['5'] = ".....",
        ['6'] = "-....",
        ['7'] = "--...",
        ['8'] = "---..",
        ['9'] = "----.",
        ['.'] = ".-.-.-",
        [','] = "--..--",
        ['?'] = "..--..",
        ['\\'] = ".----.",
        ['!'] = "-.-.--",
        ['/'] = "-..-.",
        ['('] = "-.--.",
        [')'] = "-.--.-",
        ['&'] = ".-...",
        [':'] = "---...",
        [';'] = "-.-.-.",
        ['='] = "-...-",
        ['+'] = ".-.-.",
        ['-'] = "-....-",
        ['_'] = "..--.-",
        ['\"'] = ".-..-.",
        ['$'] = "...-..-",
        ['@'] = ".--.-.",
        ['\x02'] = "-.-.-", //Start
        ['\x03'] = "...-." //End
    };


    static string TextToMorseCode(string message)
    {
        return string.Join("000", $"\x2 {message} \x3".ToUpper().ToArray()
            .Select(ch =>
            {
                if (ch == ' ') return "0000"; //Word Spacing 3+4
                return string.Join("0",
                    MorseCodeTable[ch].ToArray()
                    .Select(o => o == '.' ? "1" : "111")
                    .ToArray());
            }).ToArray());
    }

    public static void CreateWavFile(string wavPath, string message)
    {
        var f = new FileStream(wavPath, FileMode.Create);
        CreateWav(f, " " + message);
        f.Dispose();
    }

    public static void CreateWav(Stream f, string message)
    {
        ushort channelCount = 1;
        ushort sampleBytes = 1; // in bytes
        uint sampleRate = 8000;
        int freq = 641; // US Army 641
        int dotPerSec = 20;

        var bitData = TextToMorseCode(message);

        // 641Hz tone sample
        byte[] toneSample = new byte[sampleRate / freq];
        for (var i = 0; i < toneSample.Length; i++)
        {
            byte v = i > toneSample.Length / 2 ? (byte)255 : (byte)0;
            toneSample[i] = v;
        }

        uint dataLen = (uint)(sampleRate / dotPerSec * bitData.Length);

        var wr = new BinaryWriter(f);
        wr.Write("RIFF".ToArray());
        uint fileLength = 36 + dataLen * channelCount * sampleBytes;
        wr.Write(fileLength); //FileLength
        wr.Write("WAVEfmt ".ToArray());
        wr.Write(16); //ChunkSize
        wr.Write((ushort)1); //FormatTag
        wr.Write(channelCount); //Channels
        wr.Write(sampleRate); //Frequency
        wr.Write(sampleRate * sampleBytes * channelCount); //AverageBytesPerSec
        wr.Write((ushort)(sampleBytes * channelCount)); //BlockAlign
        wr.Write((ushort)(8 * sampleBytes)); //BitsPerSample
        wr.Write("data".ToArray());
        wr.Write(dataLen * sampleBytes); //ChunkSize

        var sampleIdx = 0;
        var bitIndex = 0;
        var mute = false;
        int dotDura = 0;
        for (int i = 0; i < dataLen; i++)
        {
            dotDura--;
            if (dotDura <= 0)
            {
                dotDura = (int)sampleRate / dotPerSec;
                mute = bitData[bitIndex] == '0';
                bitIndex++;
            }
            wr.Write(mute ? (byte)127 : toneSample[sampleIdx]);
            sampleIdx++;
            if (sampleIdx >= toneSample.Length) sampleIdx = 0;
        }
        wr.Dispose();
    }
}

主程式如下,接受文字參數,將 WAVE 內容存成 test.wav,並呼叫 Windows Shell 用預設開啟程式播放:

var wavPath = "test.wav";
MorseCodeModule.CreateWavFile(wavPath, args.Length == 0 ? "TEST" : args[0]);
System.Diagnostics.Process.Start("explorer", wavPath);

為防大家跟我一樣無聊想動手玩玩,程式已推上 Github,有興趣的朋友請自取。

【參考資料】

A example of using C# to create .wav file from scratch and the final prodcut is a text to morse code converter.


Comments

Be the first to post a comment

Post a comment