Weekend Challenge: Detecting and Decoding Morse Code in an Audio File
Recently I received a message in morse code from a family member using this site. It said that the sender had hidden the message, so I was presented with 2 options: I could sit and decode the message by listening to it over and over again, or write a program to do it for me.
Naturally, as a computer science student and enthusiast, I chose the second option. My first problem: Capture a recording of the target morse code. This was easy - the audio-recorder
package in the ubuntu repositories solved that one easily, as it has an option to record the audio output of my laptop.
Second problem: Figure out how to read the recording in and extract the samples in C♯. This one wasn't so easy. Amidst issues with flatpak and Monodevelop (flatpak is terrible!), I eventually found the NAudio (Codeplex, GitHub, NuGet) package to do the job. After some digging, I discovered that NAudio is actually really powerful! It's got some pretty advanced functions for handling audio that I'll have to explore at a later date.
Anyway, with a plan of action I set to work. - decided to work in reverse, so the first thing I needed was a chart that converted morse code into the latin alphabet. Wikipedia to the rescue:
(Source, Direct link, My Mirror)
With a handy-dandy conversion chart, it was relatively simple to create a class to handle the conversion from dots and dashes to the latin alphabet automatically:
using System;
using System.Collections.Generic;
namespace SBRL.Algorithms.MorseCodeTranslator
{
/// <summary>
/// A simple class to translate a morse code string into a normal string.
/// </summary>
/// <license>Mozilla Public License version 2.0</license>
/// <origin></origin>
/// <author>Starbeamrainbowlabs (https://starbeamrainbowlabs.com/)</author>
/// <changelog>
/// v0.1 - 26th May 2017:
/// - Creation! 😁
/// </changelog>
public static class MorseDecoder
{
/// <summary>
/// The morse code lookup table. Use the methods in this class is possible,
/// rather than accessing this lookup table directly!
/// </summary>
public static Dictionary<string, char> morseCodeLookup = new Dictionary<string, char>()
{
[".-"] = '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',
[".----"] = '1',
["..---"] = '2',
["...--"] = '3',
["....-"] = '4',
["....."] = '5',
["-...."] = '6',
["--..."] = '7',
["---.."] = '8',
["----."] = '9',
["-----"] = '0',
};
/// <summary>
/// Translates a single letter from morse code.
/// </summary>
/// <param name="morseSource">The morse code to translate.</param>
/// <returns>The translated letter.</returns>
public static char TranslateLetter(string morseSource)
{
return morseCodeLookup[morseSource.Trim()];
}
/// <summary>
/// Translates a string of space-separated morse code strings from morse code.
/// </summary>
/// <param name="morseSource">The morse code to translate.</param>
/// <returns>The translated word.</returns>
public static string TranslateWord(string morseSource)
{
string result = string.Empty;
string[] morseLetters = morseSource.Split(" ".ToCharArray());
foreach(string morseLetter in morseLetters)
result += TranslateLetter(morseLetter);
return result;
}
/// <summary>
/// Translates a list of morse-encoded words.
/// </summary>
/// <param name="morseSources">The morse-encoded words to decipher.</param>
/// <returns>The decoded text.</returns>
public static string TranslateText(IEnumerable<string> morseSources)
{
string result = string.Empty;
foreach(string morseSource in morseSources)
result += $"{TranslateWord(morseSource)} ";
return result.Trim();
}
}
}
That was easy! The next challenge to tackle was considerably more challenging though: Read in the audio file and analyse the samples. I came up with that I think is a rather ingenious design. It's best explained with a diagram:
- Read the raw samples into a buffer. If there isn't enough space to hold it all at once, then we handle it in chunks.
- Move a sliding-window along the raw buffer, with a width of 100 samples and sliding along 25 samples at a time. Extracts the maximum value from the window each time and places it in the windowed buffer.
- Analyse the windowed buffer and extract context-free tokens that mark the start or end of a tone.
- Convert the context-free tokens into ones that hold the starting point and length of the tones.
- Analyse the contextual tokens to extract the morse code as a string
- Decipher the morse code string
It's a pretty complicated problem when you first think about it, but breaking it down into steps as I did in the above diagram really helps in figuring out how you're going to tackle it. I, however, ended up drawing the diagram after Id finished writing the program.... I appear to find it easy to break things down in my head - it's only when it gets too big to remember all at once or if I'm working with someone else that I draw diagrams :P
Having drawn up an algorithm and 6 steps I needed to follow to create the program, I spent a happy afternoon writing some C♯. While the remainder of the algorithm is not too long (only ~202 lines), it's a bit too long to explain bit by bit here. I have uploaded the full program to a repository on my personal git server, which you can find here: sbrl/AudioMorseDecoder.
If you're confused about any part of it, ask away in the comments below! Binaries available on request.
I'll leave you with a pair of challenging messages of my own to decode. Try not to use my decoder - write your own!
Message A (easy), Message B (hard) (hard message generated with cwwav)