Decoding Morse Code With JavaScript
A couple of weekends ago, I was down in North Carolina for the NCDevCon conference. At the conference, I got into a good conversation with Dan Skaggs about Ham Radios and Morse Code. I know nothing about Morse Code (or Ham radios for that matter); but, I thought it would be a fun little exercise to try and build a Morse Code demo in JavaScript. There's really no point to this post other than sharing with you what I did this morning before work.
Morse Code is a way to transmit information using a series of tones consisting of dots and dashes. Each alpha-numeric character in the English alphabet is represented by a specific sequence of dots and dashes. Technically, a dash is supposed to last 3-times as long as a dot. And, the space between characters is suppose to be equivalent to the duration of a dot. In my demo, though, I'm pretty loose with the timing. I just assume that dots last less than 250 milliseconds and dashes last more than 250 milliseconds.
Click here to try the demo out for yourself. In the demo, the page binds to all keyDown and keyUp events in the browser in order to track tones. So, any key you hit (and hold down) will trigger a tone (ie. a dot or a dash).
Here's the code that makes this possible. I wouldn't say that I have any particular best-practices being baked in here - I was just trying to get the code to work. For my Morse Code domain object (singleton), you will see, however, that I am using the call() approach to a self-execution function block.
<!DOCTYPE html>
<html>
<head>
<title>Decoding Morse Code With JavaScript</title>
<style type="text/css">
div.output {}
div.output p.message {
font-size: 20px ;
line-height: 30px ;
}
div.output span.possibleCharacters {
background-color: gold ;
display: inline-block ;
padding: 0px 7px 0px 7px ;
}
div.alphabet {
bottom: 0px ;
position: fixed ;
}
div.alphabet h3 {
margin: 0px 0px 20px 0px ;
}
div.alphabet ul.characters {
margin: 0px 0px 0px 0px ;
padding: 0px 0px 0px 0px ;
}
div.alphabet ul.characters li {
border: 1px solid #CCCCCC ;
display: inline-block ;
padding: 10px 0px 10px 0px ;
text-align: center ;
width: 75px ;
}
div.alphabet ul.characters li span.character {
display: block ;
font-size: 14px ;
line-height: 18px ;
margin: 0px 0px 7px 0px ;
}
div.alphabet ul.characters li span.sequence {
display: block ;
font-size: 18px ;
line-height: 18px ;
}
</style>
<script type="text/javascript" src="./jquery-1.6.3.js"></script>
<script type="text/javascript">
// Create a model for the Morse Code interpreter.
var morseCode = (function(){
// Define the duration of the dot in milliseconds.
this._dotDuration = 250;
// Define the duration of the dash in milliseconds. The
// dash is supposed to be 3x that of the dot.
this._dashDuration = (this._dotDuration * 3);
// Define the pause duration. This is the time between
// letters and is supposed to be 1x that of the dot.
this._pauseDuration = (this._dotDuration * 1);
// Define the pattern map for the morse code patterns
// as the relate the alpha-numeric characters that they
// represent.
this._patternMap = {
".-": "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"
};
// I am the current, transient sequence being evaluated.
this._sequence = "";
// ---------------------------------------------- //
// ---------------------------------------------- //
// I add the given value to the current sequence.
//
// Throws InvalidTone if not a dot or dash.
this.addSequence = function( value ){
// Check to make sure the value is valid.
if (
(value !== ".") &&
(value !== "-")
){
// Invalid value.
throw( new Error( "InvalidTone" ) );
}
// Add the given value to the end of the current
// sequence value.
this._sequence += value;
// Return this object reference.
return( this );
};
// I add a dash to the current sequence.
this.dash = function(){
// Reroute to the addSequence();
return( this.addSequence( "-" ) );
};
// I add a dot to the current sequence.
this.dot = function(){
// Reroute to the addSequence();
return( this.addSequence( "." ) );
};
// I get the alpha-numeric character set as an array of
// sequence-character pairs.
this.getAlphabet = function(){
// Create the empty set.
var characterSet = [];
// Loop over the patterns to map them to a character
// set item.
for (var pattern in this._patternMap){
// Push it onto the set.
characterSet.push({
sequence: pattern,
character: this._patternMap[ pattern ]
});
}
// Sort the character set alphabetically.
characterSet.sort(
function( a, b ){
return( a.character <= b.character ? -1 : 1 );
}
);
// Return the character set.
return( characterSet );
};
// I get the dash duration.
this.getDashDuration = function(){
return( this._dashDuration );
};
// I get the dot duration.
this.getDotDuration = function(){
return( this._dotDuration );
};
// I get the pause duration.
this.getPauseDuration = function(){
return( this._pauseDuration );
};
// I reset the current sequence.
this.resetSequence = function(){
// Clear the sequence.
this._sequence = "";
};
// I get the possible character matches based on the
// current sequence.
this.resolvePartial = function(){
// Create an array to hold our possible characters.
var potentialCharacters = [];
// Loop over the pattern match to find partial matches.
for (var pattern in this._patternMap){
// Check to see if the current sequence can be
// the start of the given pattern.
if (pattern.indexOf( this._sequence ) === 0){
// Add this character to the list.
potentialCharacters.push(
this._patternMap[ pattern ]
);
}
}
// Return the potential character matches.
return( potentialCharacters.sort() );
};
// I get the alpha-numeric charater repsented by the
// current sequence. I also also reset the internal
// sequence value.
//
// Throws InvalidSequence if it cannot be mapped to a
// valid alpha-numeric character.
this.resolveSequence = function(){
// Check to see if the current sequence is valid.
if (!this._patternMap.hasOwnProperty( this._sequence )){
// The sequence cannot be matched.
throw( new Error( "InvalidSequence" ) );
}
// Get the alpha-numeric mapping.
var character = this._patternMap[ this._sequence ];
// Reset the sequence.
this._sequence = "";
// Return the mapped character.
return( character );
};
// ---------------------------------------------- //
// ---------------------------------------------- //
// Return this object reference.
return( this );
}).call( {} );
</script>
</head>
<body>
<h1>
Decoding Morse Code With JavaScript
</h1>
<!-- BEGIN: Output. -->
<div class="output">
<h3>
Your Message (Press any key to interact):
</h3>
<p class="message">
<span class="characters">
<!-- This will be populated by the user. -->
</span>
<span class="possibleCharacters">
<!-- This will be populated dynamically. -->
</span>
</p>
</div>
<!-- END: Output. -->
<!-- BEGIN: Alphabet. -->
<div class="alphabet">
<h3>
Morse Code Alphabet
</h3>
<ul class="characters">
<!-- This will be populated dynamically. -->
</ul>
<!-- Define the template. --->
<script type="application/x-template" class="template">
<li>
<span class="character"></span>
<span class="sequence"></span>
</li>
</script>
</div>
<!-- END: Alphabet. -->
<script type="text/javascript">
// Initialize the alphabet display.
(function( $, container, morseCode ){
// Get the dom elements in this module.
var dom = {
characters: container.find( "ul.characters" ),
template: container.find( "script.template" )
};
// Get the the alphabet from the morse code model.
var alphabet = morseCode.getAlphabet();
// Loop over the alphabet to build the output.
for (var i = 0 ; i < alphabet.length ; i++){
// Get the current letter short-hand.
var letter = alphabet[ i ];
// Create a new instance of the template.
var template = $( dom.template.html() );
// Set the character.
template.find( "span.character" ).text(
letter.character
);
// Set the sequence.
template.find( "span.sequence" ).text(
letter.sequence
);
// Add the template to the output.
dom.characters.append( template );
}
})( jQuery, $( "div.alphabet" ), morseCode );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Initialize the interpreter.
(function( $, container, morseCode ){
// Get the dom elements in this module.
var dom = {
characters: container.find( "span.characters" ),
possibleCharacters: container.find( "span.possibleCharacters" )
};
// Get the dot and dash durations (in milliseconds).
var dotDuration = morseCode.getDotDuration();
var dashDuration = morseCode.getDashDuration();
var pauseDuration = morseCode.getPauseDuration();
// Store the date/time for the keydown.
var keyDownDate = null;
// Keep a timer for post-key resolution for characters.
var resolveTimer = null;
// Keep a timer for adding a new space to the message.
var spaceTimer = null;
// For this module, we are going to bind to any key click
// to indicate an interaction with the interpreter. There
// will be no other key interaction.
$( document ).keydown(
function( event ){
// Prevent any default action.
event.preventDefault();
// Check to see if there is a key-down date. If
// so, then exit - we only want the first press
// event to be registered.
if (keyDownDate){
// Don't process this event.
return;
}
// Clear the resolution timer.
clearTimeout( resolveTimer );
// Clear the space timer.
clearTimeout( spaceTimer );
// Store the date for this key-down.
keyDownDate = new Date();
}
);
$( document ).keyup(
function( event ){
// Prevent any default action.
event.preventDefault();
// Determine the keypress duration.
var keyPressDuration = ((new Date())- keyDownDate);
// Clear the key down date so subsequent key
// press events can be processed.
keyDownDate = null;
// Check to see if the duration indicates a dot
// or a dash.
if (keyPressDuration <= dotDuration){
// Push a dot.
morseCode.dot();
} else {
// Push a dash.
morseCode.dash();
}
// Display the possible characters for the current
// sequence.
dom.possibleCharacters.text(
morseCode.resolvePartial().join( " , " )
);
// Now that the key has been pressed, we need to
// wait a bit to see if we need to resolve the
// current sequence (if the user doesn't interact
// with the interpreter, we'll resolve).
resolveTimer = setTimeout(
function(){
// Try to resolve the sequence.
try {
// Get the character respresented by
// the current sequence.
var character = morseCode.resolveSequence();
// Add it to the output.
dom.characters.text(
dom.characters.text() + character
);
} catch (e) {
// Reset the sequence - something
// went wrong with the user's input.
morseCode.resetSequence();
}
// Clear the possible matches.
dom.possibleCharacters.empty();
// Set a timer to add a new space to the
// message.
spaceTimer = setTimeout(
function(){
// Add a "space".
dom.characters.text(
dom.characters.text() + "__"
);
},
3500
);
},
(pauseDuration * 3)
);
}
);
})( jQuery, $( "div.output" ), morseCode );
</script>
</body>
</html>
On an aside, I do think it would be cool to know Morse Code. I always thought it would be awesome to have a "secret language" like ASL (American Sign Language) that could be used to communicate surreptitiously with people at social events. Anyway, I guess, JavaScript for the win?!
Want to use code from this post? Check out the license.
Reader Comments
Fun use of some JS! You wrote this before work this morning?? Damn dude!
@Andrew,
Don't get me wrong - it took like 2 hours to write. It's not like I whipped it together in 10 minutes :)
http://goo.gl/rCq5T
How did you figure out how to decode the morse code? How would you know the difference between one set of 3 dots and 1 dot and 2 dots separately?
@Andy,
Oh snap, I forgot about that post! To separate the words, I am basically just saying that if you haven't typed anything in X milliseconds, the current sequence gets resolved. Then, I say if nothing is typed in ANOTHER Y milliseconds, arbitrarily add a "space" to the output.
I asked for someone to write an audio morse code generator here:
http://codegolf.stackexchange.com/questions/2618/morse-code-generator-in-sound
Code Golf is a site where people try to write programs in the fewest number of characters.
Pretty mind blowing some of the solutions that people come up with.
For instance: Because Morse code is a variable-length code that uses between 1 and 4 symbols for each letter, the tables 31313855 (bit 1) and 60257815 (bit 0) can together represent one less than the length of each letter's code.
Ben, that's a really neat use of the the .call method to pass in an empty object to the IIFE. Do you know of any blog posts or references where that pattern is explained in more detail? I'd really like to see some other examples of how this is used.
@Phillip,
People's ability to write small code is kind of mind-blowing sometimes. I see people that write code on Twitter in less that 140 chars... bananas :)
@Ryan,
Thanks! I first heard of it when attending the jQuery conference up in Boston. I tried to write it up a bit here:
www.bennadel.com/blog/2264-Changing-The-Execution-Context-Of-Your-Self-Executing-Function-Blocks-In-JavaScript.htm
But, before it was mentioned in a presentation, I had never seen this pattern before.