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 keyups. 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 keyups and keydowns, 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 connects 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.