Morse Code Notepad
If you can’t read Morse Code off the top of your head, head over here to have my app spell it out for you!!
-—
I just recently discovered Reactive Programming, which seems to be the new way to deal with streams (things happening over time) in a nicely asynchronous manner. If anyone reading has more experience in RP than I do, I apologize if I oversimplified, or even misrepresented, the importance of it, but it is still a relatively new “technology” and there aren’t many resources that do a great job of explaining how to use it properly. As a corollary, since this is my first time with RxJS, I apologize to any veterans who may stumble upon this post, as some of my practices may seem like an abomination to reactive programming and most likely thinking in general. Anyways, it seemed like a fun way to get back into web-dev after a few months of other things. The end result is an app that interprets Morse Code into the standard Latin alphabet.
First things first, constants:
// using node.js and webpack
const $ = require("jquery")
const obs = require("rx").Observable // I didn't use any of the other Rx methods
const m = require("./dictionary") // I'll come back to this one
Then I created Observables for keyup
and keydown
events on the document, and then filtered out everything but events from the space bar.
const kd = obs.fromEvent(document,'keydown').filter(e=>e.key==" ")
const ku = obs.fromEvent(document,'keyup').filter(e=>e.key==" ")
Immediately I was faced with an issue: at least on my setup (Macbook Air 2013, Google Chrome), holding the space bar emits multiple keydown
events, much like holding down a key in a program like Microsoft Word. Since I only wanted the keydown
event to fire once upon pressing the key, I had to filter out all the repeated events. I accomplished this by combining the two streams, then using scan
to maintain the state of the key.
const k = kd.merge(ku)
.scan((a,x,i,s) => x.type=="keyup"?1:(a==1?0:-1),1)
.filter(v=>v>=0)
The magic happens within the scan
call. The function argument of scan
takes (basically) an accumulated value and the next emitted value, updating the accumulated value and passing it through. By setting the accumulated value equal to the press state of the space bar, it is only a matter of case checking to return one of three values:
- 1 -> space bar is released (
keyup
event) - 0 -> space bar is pressed for the first time (
keydown
event, previous accumulated value is 1) - -1 -> space bar is repeated (
keydown
event, previous accumulated value is 0)
All that’s left is to remove the repeated events, which is done via the filter
call. The resulting k
stream is one that emits a 0
on keydown
, and 1
on keyup
.
Next I created a stream for the Backspace key, because everyone makes mistakes.
const back = obs.fromEvent(document,'keydown')
.filter(e=>e.key=="Backspace")
Now comes the fun part. This k
stream should be all that I need in order to parse the Morse code signals. Everything else will come about as some transformation of this stream, and I should end up with a stream of letters and spaces, to which I can subscribe to output to the screen.
The first order of business was to distinguish between short and long (dit/dah, dot/dash, etc.) pulses. In order to see how to go about doing this, here’s what the morse code for “hello” would look like:
(Morse code): . . . . . . - . . . - . . - - -
keydown : --d-d-d-d---d---d-d---d-d---d-d---d-d---d---d---d------>
keyup : ---u-u-u-u---u---u---u-u-u---u---u-u-u-----u---u---u--->
k : --01010101--01--010--10101--010--10101--0--10--10--1--->
The first thing to notice is that a pulse can only be determined on a keyup
event. Also, trivially, every keyup
will follow a keydown
event corresponding to the same keypress. So, determining the pulse is merely a matter of determining the time between the latest keyup
and keydown
:
const morse = k.timeInterval()
.filter(e=>e.value==1)
.map(e=>e.interval>200?"-":".")
timeInterval
returns an object containing two values, the emitted value, and (you guessed it) the time interval between the current and previous emitted values. Because I only want the intervals between the keyup
and keydown
events of one keypress, I only keep the values that are keyup
s. Then, if the interval is greater than 200ms (which I chose according to my own experimentation with Morse Code), the value is mapped to a dash, else it is mapped to a dot.
The next thing to do is to determine when the letter ends, which happens when there is a long enough interval between the last keyup
and the next keydown
. This is surprisingly simple:
const letterDone = k
.debounce(200)
.filter(e=>e==1)
debounce
only lets through emitted values that are followed by a period during which no values are emitted (the length of which is determined by the argument to the function call). By debouncing the k
stream by 200ms, I only get the values for which there was nothing afterwards for 200ms. Filtering for only those values that signify keyup
events, I now have a stream that tells me when the letter is done.
The same sort of logic can be used in order to determine when a word has completed, so that a space can be inserted. For word completion, however, the interval to check is not between keyup
and keydown
, but between letterDone
and keydown
(ie. a word is completed when there has been a large enough pause since the last letter).
const wordDone = letterDone.merge(kd)
.debounce(500)
.filter(e=>e==1)
.map(()=>" ")
The code for letterDone
and wordDone
are basically the same. The main differences lie in the source stream. For letterDone
, the source stream consists of keyup
s and keydown
s, the combination of which already exists as k
. For wordDone
, the source stream has to be created as a merge
between letterDone
and kd
. The map
call at the end of wordDone
just emits a space character for direct input into the letters
stream.
Now that morse
and letterDone
work as intended, the letters
stream can be created from the two using the buffer
function:
const letters = morse.buffer(letterDone)
.map(b=>m.getLetter(b.join("")))
.merge(wordDone)
buffer
collects all emitted values until its argument emits a value, after which it emits everything that it has collected up to that point in an array (or “buffer”, if you’d like). The map
call then takes the buffer of pulses (which looks something like ["-",".",".","."]
at this point) and translates them to their alphabet equivalent. But wait, what is m.getLetter
?
As seen from my constants section, the variable m
refers to an import from a file at ./dictionary
. This is merely my Morse Code-to-alphabet dictionary, which I implemented as a tree (Morse code is vaguely reminiscent of a Huffman Code, which can be implemented with trees as well). The tree can be thought of as a regular binary tree, where the left and right children are dot and dash. Thus, the tree can be populated fairly easily (I chose to do this via a simple breadth-first traversal), and lookups are very simple as well (recursive, as are many tree functions):
// function in Node class
getLetter(s) {
if (s=="") return this.val
try {
return this[s[0]=="-"?"dash":"dot"].getLetter(s.slice(1))
} catch (e) {return null}
}
The last thing to do is to publish
the original kd
,ku
,and back
streams. If this isn’t done, each new subscription will get its own “instance” of the event streams, and things won’t sync up.
// back to the beginning new part here vvv
const kd = obs.fromEvent(document,'keydown').filter(e=>e.key==" ").publish()
const ku = obs.fromEvent(document,'keyup').filter(e=>e.key==" ").publish()
const back = obs.fromEvent(document,'keydown').filter(e=>e.key=="Backspace").publish()
// at the end
kd.connect()
ku.connect()
back.connect()
And that’s pretty much it! Once the connect
s are called, pressing the space bar will produce morse code that is interpreted into the alphabet.
-–
I struggled for a few days on the problems of determining when letters and words end. Initially, I was too focused on creating the intended stream all at once. Below is the original code for the letterDone
stream:
const letterDone = ku.flatMap((x)=>obs.timer(500).takeUntil(kd))
flatMap
produces a stream from the emitted value, and then pushes the emits from the stream onto the original stream. As you can see, I was too focused on producing exactly the result I wanted (ie. only emit if there are no kd
emits in the 500ms after a ku
emit). I wracked my brain for two days before I gave up and pushed the above code, only to have a revelation the night of (the present solution literally came to me in a dream).
Nothing like a good change in perspective.