Alexander Dickson

Using JavaScript for identifying coins for an arcade machine

While I was working at Atlassian, I embarked on a project with some colleagues (Sean, Sharpy, Ben & Steve) to build an arcade machine, capable of four player action and with all the trimmings of a real machine you would have found at home in a shopping centre in the early 90s.

The finished AtlasCab

In our haste to order parts, a coin counter thingamajig was purchased without any idea of how to get it to work. Upon inspection of the received package, it seemed to be a plug-in extension to some other type of arcade machine board. There was talk of scrapping it, but I decided to have a tinker. After all, it can’t be that hard right?

The GangDu coin box

The first problem is the instructions were almost entirely in Chinese, in fact the only English I could see was 12v. I figured that was enough to get started, so I went on the hunt for a 12v source. I must stress, and it is about to become extremely obvious, that I am not an electrical engineer. Originally, I took a stray 12v lead from the power supply of the computer powering the arcade machine and the coin box beeped. It was alive!

Knowing that patching it to a PC’s power supply wasn’t a long-term solution, I went to JayCar and found a 12v power supply that could become its sustainer of life. I dodgied up a power connection that worked and began to see what happened when I ran coins through the opening. I figured out how to teach it how to recognise coins using a simple button interface on its side. I programmed it to accept Australian $1 and $2 coins.

When each coin passed through, the machine seemed to run a process in which it determined the coins legitimacy, and then rejected the coin (the default pathway thanks to gravity) or mechanically blocked that path via an actuator and flap and allowed the coin to fall through to an internal exit. I decided to whip out the ever-handy multimeter and start probing around, seeing if there was anything interesting occurring on the non-power lines when a coin was deposited.

The naked GangDu

I designed a high tech setup of helping hands, alligator clips and very gentle touch to not knock it all over in this dodgy setup. I rolled a coin down and watched intently at the multimeter. I was stoked to see some form of electrical activity on the line when the coin passed through (and nothing if the coin was rejected and returned to the user). This was pleasing and perhaps a sign we could do could actually accept coins and identify them in our software that powered the machine.

All well and good. Except: how the fuck is the computer going to know about these pulses? I just can’t tell it, can I? Or… can I? Don’t computers have a line-in, and isn’t sound just some form of voltage activity on the line? To test this theory, I cut up an old 3.5mm mono cable and inserted it into my Mac. I then hooked the other end up to the coin box. I downloaded some software to visualise the noise on the line and then I inserted some coins. Holy shit! I can see the coins and the different coins look different (which surprised me, as there was only about .4v between the pulses). So, how to turn this random noise into coin values?

I started looking into OS APIs around line-in and fired up Xcode to reacquaint myself with my long-lost friend, C. After a bit of head scratching, it occurred to me a strange and possibly very stupid idea: doesn’t the browser now have some way of getting microphone data? I had a look and found the getUserMedia() API. I re-wrote the app I downloaded to visualise the noise using JavaScript and the canvas element to determine if the browser could also visualise the audio in the same way (I assumed it would, but you know what they say about assumptions! Sometimes they’re incorrect.)

After confirming this, I got to work churning out some JavaScript code. This went through many iterations and I am fucking sick of putting gold coins into slots. For the brave, the code is available at the end of the post. Be sure to open your mind when reading it.

So how does this code solve the problem? Well, it’s simple really! The code basically listen to the audio-in, and…

  1. Culls the lower half of the sample, which always has noise on it
  2. Take an average of the rest of the sample
  3. Grab a time duration of when we will sample noise activity
  4. If there appears to be noise of significance (average is above 0), start counting the pulses on the line
  5. When the duration has finished,check if it looks valid, and if so, classify the coin and call a callback

I was pleased with how it was working. However, it was susceptible to its environment. I’m not sure if it’s because the cables aren’t properly shielded or because the coin detector uses the coin’s induction to classify it (so big new pieces of metal in the vicinity will affect it), but it was a pain in the ass. I had came up with some values to identify the different coins, and they were meaningless once we put it all together.

On the day before we released it, we set it all up in the cabinet and tweaked the values to suit. Eventually, we found the sweet spot. No more problems! It works! It actually works! The idea behind the coin input was to accept donations that would go to one of the fine charities that Atlassian supports. The machine didn’t require a coin to play, but it was expected you would drop off your change there if you were a repeat user. Whenever you donated, the running total was printed to the screen.

If you’re ever in Atlassian’s Sydney office (and you probably are due to the awesome meetups held there), have a look for the arcade machine (or listen for the hadoukens in Street Fighter 2). There’s a bunch of less insane JavaScript running the rest of the machine, such as the front-end effects and back-end in Node. Don’t forget to leave a one or two dollar coin!

var coinDetector = (function () {

    var AudioContext = window.AudioContext || window.webkitAudioContext;
    var getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia).bind(navigator);

    return function (coins, callback) {
        var context = new AudioContext();
        var analyser = context.createAnalyser();
        var javascriptNode = context.createJavaScriptNode(2048, 1, 1);

        // Ensure callback supplied is callable.
        if (typeof callback !=  "function") {
            throw new TypeError("Callback supplied doesn't implement [[Call]].");
        }

        // Sort coins from highest to lowest
        // so our comparator function below works.
        coins.sort(function (a, b) {
            return a.range[0] - b.range[0];
        });

        // Ensure coin ranges are valid
        // and don't tread on each other.
        var currentLow = null;
        var currentHigh = null;
        coins.forEach(function(coin, index) {
            // If only value passed, treat it as 1.
            coin.range = [].concat(coin.range);
            if (coin.range.length == 1) {
                coin.range.push(coin.range[0]);
            } else if (coin.range.length == 0 || coin.range.length > 2 || coin.range[0] > coin.range[1] || currentLow && coin.range[0] < currentHigh) {
                throw new Error("Invalid range detected for coin #" + index);
            }
            currentLow = coin.range[0];
            currentHigh = coin.range[1];
        });

        // The time a coin was successfully
        // detected for the first time.
        var calcCoinTime = 0;

        // The number of pulses detected
        // on the line.
        var pulseDetectedCount = 0;

        var proceed = function (stream) {
            var source = context.createMediaStreamSource(stream);
            analyser.smoothingTimeConstant = .3;

            source.connect(analyser);

            javascriptNode.onaudioprocess = function (event) {
                var data = new Uint8Array(analyser.frequencyBinCount);
                analyser.getByteFrequencyData(data);

                // There is noise on the lower half,
                // so ignore it.
                data = data.subarray(-500)

                var avg = [].reduce.call(data, function (total, value) {
                    return total + value;
                }, 0) / 500;
                // The time that we count pulses after the first
                // is detected.
                var coinDetectionBuffer = Date.now() - calcCoinTime < 500;

                // Is the activity on the line enough to
                // consider it something of interest?
                var detectedLineActivity = avg > 0;

                // Has a coin been detected?
                if (detectedLineActivity) {
                    if (coinDetectionBuffer) {
                        // Register a pulse.
                        pulseDetectedCount++;
                    } else {
                        // Register the start of a coin
                        // being deposited.
                        calcCoinTime = Date.now();
                        pulseDetectedCount = 1;
                    }
                } else if ( ! coinDetectionBuffer && pulseDetectedCount > 0) {
                    // Find coin (if any).
                    var coin = (coins.filter(function (coin) {
                        return coin.range[0] <= pulseDetectedCount && coin.range[1] >= pulseDetectedCount ;
                    }) || [])[0];
                    // Call user-supplied callback.
                    coin && callback(coin, pulseDetectedCount);
                    pulseDetectedCount = 0;
                }
            }

            javascriptNode.connect(context.destination);
        };

        getUserMedia({
            audio: true
        }, proceed, function () {
            console.log("AtlasCab is sad. Couldn't get line-in.");
        });

    };
})();

Want to discuss this post? Just mention me @alexdickson.