Coding4Fun - 使用 C# 製作聲音檔,以摩斯電碼為例
0 |
很久沒有 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,有興趣的朋友請自取。
【參考資料】
- Github 專案:Morse code audio generation tool (Python)
- Stackoverflow:Creating a .wav File in C#
- Making Sounds with Waves Using C# by Peter Shaw
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