Building a Nerves Lullaby Player

Lullabeam devices with colorful, painted buttons
Nathan Long

Engineer

Nathan Long

Since I began programming in 2007, nearly all of my work has been on the web. But, about a year ago, I started a project to make a physical device using Elixir Nerves to help make bedtime/naptime easier for my kids. It’s been very satisfying.

When I started this project, my wife and I had two wonderful kids who needed help falling asleep. We just welcomed our third and imagine we will face similar challenges when the time comes!

Playing lullabies helped them but I wasn’t happy with using an iPod or CD player to do that. The main sticking point was naptime. At bedtime, we could play an entire playlist or disc, but at naptime, we wanted to cut it short. Our deal was “lie in bed for 20 minutes; if you’re still awake when the music stops, you can get up.” But manually tweaking playlists or skipping tracks to approximate 20 minutes was annoying. Couldn’t I make something better?

What I wanted

What I really wanted was a player with a timer, in addition to a few other features, such as:

  • Be kid-friendly so the kids could get up and turn it back on if they woke in the night
  • Be cheap so if they knocked it down and broke it, it wouldn’t be a big deal
  • Be screenless because looking at a screen is bad for sleep
  • Play off a USB stick so that changing the songs would be easy
  • Have a headphone jack so that the speakers could be replaced independently
  • Work offline so that I’d never need to patch the device once the software was done

I’d heard about Elixir’s Nerves framework for embedded software and saw that it supported using Raspberry Pis. A commercial team might prototype with a Pi and build custom hardware later but, for me, a Pi could be the final hardware. I also saw that the Raspberry Pi 3 Model B+ had a built-in headphone jack and several USB ports.

This made my lullaby player seem like a perfect first project with Nerves: it didn’t require any wiring or soldering and the logic would be pretty simple.

To control it without a screen, I wanted physical buttons. After chatting with the Nerves team, I decided to use a USB numeric keypad and interpret key presses as “play,” “pause,” etc.

Why Nerves?

The Nerves framework is a way of creating embedded software. But what does that mean?

I think of embedded devices as “single purpose” ones: thermostats, kiosks, sprinkler systems, traffic lights, sensors, cameras, robots, or any of what people call the “internet of things.” The hardware and software are custom-built together to serve the same purpose.

The draw of the Nerves framework is that it lets you write such software in a productive, clear, high-level language. And the specific advantage of Elixir is reliability.

Unlike the web, where you might spin up a new server on-demand, remote hardware is expensive to troubleshoot and fix. So, the fact that the Erlang virtual machine – which runs Elixir – is designed for reliability is a real advantage. If one part of your code crashes, the system can recover without manual intervention.

After writing software on a development machine, Nerves lets you compile your code, along with a minimal Linux OS – courtesy of Buildroot – onto a bootable microSD card and run your device from that. If needed, you can also use NervesHub to update devices in the field.

The Yak Shave

For my lullaby player, I would have liked to directly control the process of reading music files and playing the audio, but that proved too difficult. So I opted to use mpv, an open-source media player that can handle pretty much any audio file format.

This meant that I needed to customize my Linux build to include mpv, which involved building, publishing, and depending on a custom “Nerves system.” Ultimately, my system differed from the standard one by only a single line of configuration, but it took me a lot of head-scratching to figure out how to make that change and get the dependency set up correctly.

Fortunately, Frank Hunleth and Justin Schneck of the Nerves team came to my rescue more than once along the way, giving advice and even making patches to help me make progress. I wouldn’t have finished this project without their help, and their kindness is probably as big a factor in Nerves’ success as their technical brilliance.

Thanks to them, Nerves’ tooling is getting better all the time. And when it came to actually writing Elixir code, I had a blast.

Lullabeam Overview

The basic flow of Lullabeam (as I called my player software) is as follows.

At startup, it mounts the thumb drive, builds playlists of music files, configures the sound card to use the headphone jack, and plays a chime when it’s ready to be used.

After that, it listens for input on the keypad and interprets the keystrokes as player controls – for instance, the keystroke “5” is interpreted as “play”.

mpv, which does the heavy lifting for playback, runs as a separate Linux process, which is managed using a port. If it needs to pause or unpause a song, it uses mpv’s JSON IPC protocol, sending messages over a Unix socket. (This is needed because mpv isn’t written to work with standard input and output, as a port-friendly process would be.)

When told to change songs, it actually kills the mpv process and starts a new one. Similarly, mpv exits when it finishes playback of a song.

Each time a song ends, it checks the remaining time on the timer, and if time is up, it doesn’t play another song.

That’s the basic idea. There are a few Elixir-specific techniques that help things work well, though.

Managing Startup

First, the startup process I described is encoded as a supervision tree.

In Elixir, supervision trees are used for fault tolerance: if a process fails, it can be restarted in a known good state and processes which depend on it can be restarted with it. If necessary, repeated failures can cause larger portions of the system to restart.

But supervision trees also control startup order. In Lullabeam, the supervision tree looks like this.

!

Processes are started from left to right and top to bottom. So in this case, StartupSounder starts up only after everything else is ready, ensuring we don’t chime prematurely. RpiSetup deliberately delays startup by not returning from its init/1 call until it’s able to configure the sound card.

Pattern-matching Input

Interpreting the USB keypad input as player controls required two things.

First, I used the input_event library to be notified of incoming keystrokes.

Second, I wrote a module to interpret those keystrokes for the specific keypad I was using. Elixir’s pattern-matching function heads made the code to do this very concise: I just defined matches for the keys I expected and ignored anything else.

def interpret({:ev_key, :key_kpslash, 0 = _keyup}), do: {:cmd, {:set_timer, :nap}}
def interpret({:ev_key, :key_kp5, 0 = _keyup}), do: {:cmd, :play_or_pause}
# ... etc
def interpret(_e), do: :unknown

Keeping State

You’re probably familiar with client-server model when it comes to the web. Clients (like browsers) make requests. Servers respond, often after checking or updating their internal state.

!

In Elixir, it’s common to have processes which work that way: they receive messages, keep state, and send responses. The GenServer module provides a standard interface for writing server processes.

In Lullabeam, the DJ process manages playback, and keeps track of related state: what playlists are available, which track is playing now, how much time remains on the timer, etc. Commands like “pause” or “next track” are sent to it and it responds accordingly.

But forwarding every button press from the keyboard straight to the DJ wasn’t going to work well. Knowing my user base (ages five and three), I needed another GenServer.

Debouncing Input

As a device designed for little kids, I knew that Lullabeam had to withstand some sillies. If pressing a button is fun, what about pressing all the buttons as fast as possible? 😜

I wanted Lullabeam to respond calmly. So I wrote a Debouncer GenServer to deal with erratic input and inserted into the input handling chain before the DJ.

!

The debouncer logic is like this:

  • Start out in a “listening” state
  • When a keypress comes in, hold onto it for a few milliseconds
  • If no further input is detected, forward the request to the DJ
  • If more input comes in immediately, go into “ignoring” mode. Ignore all input until you see that input has stopped for a few milliseconds, then go back to listening

The Elixir technique which makes this easy is GenServer timeouts, which (as I added to the documentation) “can be used to detect a lull in incoming messages.”

Finishing Touches

Admittedly, running a real Linux computer just to play music is overkill. But using this tech stack let me build it myself, the way I liked it. And to reduce power usage, I used Chris Freeze’s power_control library, which he’s written about on the DockYard blog.

All this technical work was fun for me, but a lullaby player should look like one. My wife helped out there: she painted the keys in soft colors, using glow-in-the-dark paint so they’d be visible at night.

Reflection

I’ve written a lot of software over the years and a lot of it has been fun. But each time I tuck my kids in, there’s a different kind of satisfaction in pressing “play” on a lullaby player I built myself, knowing that it works just the way I made it to.

I have ideas for a few more improvements, based on feedback from my family. But maybe you’d like to build your own lullaby player, or modify mine? If so, check out the code and have fun.

Need help with your next Nerves project? Reach out to DockYard to learn how we can help bring your idea to life.

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js, React.js, Ruby, and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including San Francisco, Los Angeles, Denver, Chicago, Austin, New York, and Boston.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box