Projects Research & Design Hub

DIY Streaming Audio Player

Written by Ed Nisley

How would you like a streaming audio player? Sometimes the right way to build a gadget doesn’t require building anything at all.

  • How build a DIY streaming audio player

  • How to implement the power control

  • How to set up the connectivity and the sound bar

  • How to program the keypad controls

  • How to configure the Raspberry Pi

  • How to control the MPlayer’

  • AC511 USB Sound Bar from Dell

  • Raspberry Pi 2 Model B SBC

  • Wireless numeric keypads

  • Python programming language

Mary recently asked for a gadget that would provide background music in her sewing areas and the kitchen. As we discussed what she wanted, the “design specs” boiled down to a simple box that Just Played Music. Although we could certainly buy a streaming radio player, neither of us liked the complex user interfaces, EULA entanglements, and documented cases of network spyware that accompany many of those players.

After some experiments, I settled on the hardware shown in Photo 1: a Raspberry Pi 2 Model B in a standard case, a USB sound bar intended for an LCD monitor, a relabeled USB numeric keypad, and either a USB Wi-Fi adapter or a wired Ethernet connection. On the software side, I wrote a Python program that controls mplayer, a command-line streaming player, running on Raspbian Jessie Lite Linux.

Photo 1
The Raspberry Pi and USB speaker bar normally sit on the far side of Mary’s quilting room, with the USB keypad near her sewing machine. Poor Wi-Fi reception on that end of the house required a wired Ethernet connection, rather than the long-antenna Wi-Fi adapter shown here. The quilt is under construction and still held together by special safety pins like the one near the Pi.

In this column I’ll describe our straightforward streaming audio player and, along the way, you’ll see just how much you can accomplish with what Steve would call “A simple matter of software.” The fact that you can assemble a system like this from readily available hardware and software components shows that today is truly the Golden Age of Making Things.

POWER CONTROL

The Raspberry Pi 2 Model B boards I used receive power from a 5 V, 2.5 A wall wart plugged into their Micro-B USB connector (on the far left of Photo 1), which means I must unplug the wall wart to completely turn off the power. I’ve seen elaborate power-control boards with MOSFET switches that disconnect a Raspberry Pi from its supply, but, because they don’t turn off the wall wart, its idle power dissipation remains a “vampire load” that never dies.

The entire streaming player draws 450 to 500 mA from the 5 V supply, varying slightly with audio volume, for a total dissipation around 2.5 W. After shutting the system down with sudo shutdown -P now, it draws less than 30 mA and dissipates 150 mW; much of that current may light up the RPI’s red power LED hidden inside the case. The vampire load on the 120 VAC side seems to be under 1 W, below the minimum load I can measure accurately, and I decided I wouldn’t worry about it.

As I’ll describe later, the Python program decodes system events produced by keys on the numeric keypad. When we press-and-hold the Backspace key in the upper-right corner of the keypad (covered with a red Halt label), the program executes the code in Listing 1 to shut the RPi down. Restarting the RPi then requires a power cycle (unplugging the wall wart!) or a hardware reset.

— ADVERTISMENT—

Advertise Here

Listing 1
Pressing and holding the Backspace key on the numeric pad triggers a clean shutdown using this code, after which the system’s power dissipation drops to 150 mW. Although this Python program will run on a desktop PC, you probably shouldn’t do that!

if (kc == 'KEY_BACKSPACE') and (KeyEvent(e).keystate == KeyEvent.key_hold):
    print 'Backspace = shutdown!'
    p = subprocess.call(['sudo','shutdown','-P','now'])

Recent RPi boards include two pads for a RUN header that, despite its name, resets the CPU when a switch connects the pads. Photo 2 shows the header location on the RPi 2 Model B boards I used; it’s in a different location on other versions. Soldering that header in place and modifying the case to allow plugging a connector atop the pins were the only two “hardware” changes I made during this entire project.

Photo 2 The only hardware modifications in this project involved soldering a two pin header onto the Raspberry Pi’s PCB and carving space for it into the plastic case. Momentarily shorting the RUN pins together resets the CPU and starts the boot process. I used an existing hole in a standard Pi case for a small pushbutton switch.
Photo 2
The only hardware modifications in this project involved soldering a two pin header onto the Raspberry Pi’s PCB and carving space for it into the plastic case. Momentarily shorting the RUN pins together resets the CPU and starts the boot process. I used an existing hole in a standard Pi case for a small pushbutton switch.

I glued a tiny pushbutton switch to the black plastic case lid, with the actuator protruding through the decorative Raspberry cutout, as shown in Photo 1. The arrow on the red Reset label points directly to the inconspicuous switch.

While this isn’t quite as convenient as a remote on-off power switch, it suffices for our purposes: we tap the Reset button to start the music on the way into the room, then push the Halt key to turn it off. The white LED to the left of the volume control knob on the USB sound bar provides the only visible indication that the system is running; the music tells us everything we need to know.

With the power under control, the audio output posed the next challenge.

AUDIO: THE GOOD, THE BAD, & THE UGLY

All Raspberry Pi boards include a four-conductor 3.5 mm (a.k.a. 1/8 inch) jack that produces audio charitably described as “basic” by many reviewers. Perhaps that’s because the jack also carries a composite video output signal on its Sleeve terminal, with the circuit Ground (a.k.a. Common) on the Ring 2 terminal. The RPi’s jack therefore connects a three conductor audio cable’s Shield to the Video output, instead of Ground, and may short the RPi’s Video output to Ground in the process.

I planned to plug PC-style amplified speakers into that jack, but, after some testing and reconfiguring with various plugs, we decided that the Pi’s audio circuitry produced enough background noise and overall distortion to render classical music unlistenable. I deferred to Mary’s opinion, as her hearing remains much better than mine, although the audio sounded pretty bad to me, too.

Raspberry Pi boards have an HDMI video output that carries digital-quality audio, but you must add an HDMI monitor with audio outputs or an HDMI audio splitter to extract analog audio signals for the speakers. While it’s possible to convince the RPi to enable the HDMI output without a monitor, I wasn’t sure a splitter would work in that situation and was unwilling to invest in a rather specialized bit of hardware to find out.

Instead, I plugged in an old Creative Labs Sound Blaster USB audio converter that I knew produced good-quality audio. The ALSA sound system normally uses the RPi’s on-board audio hardware (which it calls card 0) for all sound output, so I added the configuration file in Listing 2 to set the USB converter (card 1) as the default.

Listing 2 
The ALSA sound system labels a USB audio converter as “Card 1”. These lines in /etc/asound.conf set it as the default sound device.

pcm.!default {
 type hw card 1
}
ctl.!default {
 type hw card 1
}

A test setup sounded good enough, although neither of us liked the additional clutter and cables. I wrote the first version of the Python program using that configuration, with the promise that it wasn’t permanent.

— ADVERTISMENT—

Advertise Here

Some online searching produced a new-in-box surplus Dell AC511 USB sound bar, originally intended to mount on the bezel of recent Dell LCD monitors. As you can see in Photo 1, it’s essentially a narrow black box sprouting a USB cable that plugs directly into the RPi, thus eliminating the external USB audio converter. Unlike most powered speakers, it’s also inconspicuous. I must make brackets to mount the bars on the bottom of the shelves in Mary’s sewing rooms, which seems much easier than building complete enclosures.

The Linux device and audio interface modules treat the external USB converter and the AC511 identically, so mplayer works equally well with either setup. As you’d expect, the sound produced for a given mixer volume setting depends on the hardware, so I adjusted the defaults to match the AC511. The mixer channel names also depend on the hardware, which provided another motivation to build three identical players.

The AC511’s pair of 1.5 inch speakers, obviously designed for across-the-desktop listening, produce adequate volume in the sewing room. Although a more powerful amplifier and larger speakers would produce a higher sound pressure level, that’s not what Mary wanted to accompany her sewing.

KEYPAD CONTROLS

The RPi and speakers reside across the room, away from fabric on the quilting table, but the controls must remain close to the sewing machine. Remote controls seem to be a solved problem, particularly because Linux supports a wide variety of infra-red remote controls through the LIRC (Linux Infrared Remote Control) project, until you consider the “user interface” issues.

We needed only stream selection, volume, and playback controls, not a full alphanumeric keyboard, and I wanted a compact package that didn’t require any construction. All IR remote controls sport a profusion of miniature buttons, usually in cryptic layouts appropriate for the union of a TV with a set-top box, a DVR, and various other device, leaving no space for legible labels near the tiny button-posts.

Worse than all that, reliable IR control requires aiming the transmitting LED in the general direction of the receiver. Pressing the buttons on a remote behind a large roll of fabric won’t work well, no matter how “bouncy” IR signals may be in an unobstructed room.

Unlike IR remote controls, the pair of USB numeric keypads in Photo 3 provide nearly all of the functions we wanted. I used the wired keypad on the left for my test setup and the wireless keypad on the right in the sewing room, with the latter’s 2.4 GHz RF link working reliably regardless of orientation or location. Because the numeric keypads return the same key codes as the keypad section of a standard PC keyboard, you can also use a wired or wireless keyboard, although that would occupy far too much space near a sewing machine.

Photo 3 USB numeric keypads resemble control panels with large “buttons” and standardized scancodes. The triple zero key on the wired keypad returns three successive KEY_KP0 scancodes, which wasn’t useful for my purposes.
Photo 3
USB numeric keypads resemble control panels with large “buttons” and standardized scancodes. The triple zero key on the wired keypad returns three successive KEY_KP0 scancodes, which wasn’t useful for my purposes.

The first three lines in Listing 3 direct the Linux udev device interface to assign the name /dev/input/keypad to the two keypads and a full PC keyboard from my collection, under the assumption that only one of them will be plugged into the RPi at any time. The Python program can then refer to one device name, regardless of the underlying hardware.

Listing 3
Putting these UDEV rules in /etc/udev/rules.d/streamer.rules will automatically assign standard names to the numeric keypads and sound bar volume control. If you plug multiple keypads into the RPi, only one will be used.

ATTRS{name}=="HID 13ba:0001", SYMLINK+="input/keypad"
ATTRS{idVendor}=="062a", ATTRS{idProduct}=="4101", ENV{ID_INPUT_KEYBOARD}=="1", 
	SYMLINK+="input/keypad"
ATTRS{name}=="DELL Dell USB Entry Keyboard", SYMLINK+="input/keypad"

ATTRS{name}=="Dell Dell AC511 USB SoundBar", SYMLINK+="input/volume"

Although the wireless keypad uses the same 2.4 GHz spectrum as the Wi-Fi network connection, transmitting a few key presses doesn’t cause any network disruption. In the other direction, the keypad receiver works reliably despite continuous network traffic through the Wi-Fi antenna in the adjacent USB connector.

The AC511 soundbar includes a volume control dial that, to my surprise, reports itself as a USB keyboard, rather than directly controlling the soundbar’s output, so the udev rule in the last line of Listing 3 creates a device named /dev/input/volume for it. The dial rotates continuously, producing KEY_VOLUMEUP “key presses” in one direction and KEY_VOLUMEDOWN keys in the other, and the Python program uses those key codes to adjust the volume in small, smooth increments.

You can write similar udev rules for other keypad-like USB devices, although producing a syntactically correct and semantically useful udev rule requires considerable system-level exploration. The links in the Resources section document the process.

We’re not completely satisfied with the keypad labels you see in Photo 1 and I expect to adjust the font and size after we settle on the final set of streaming sources. The sound bar’s volume knob, however, works exactly as we expect!

RASPBERRY PI CONFIGURATION

The typical RPi configuration resembles an ordinary desktop PC with a graphical display, a full keyboard, and a mouse. Although the CPU can’t deliver desktop performance, the familiar GUI works well and serves the Raspberry Pi Foundation’s goals of making computers accessible to school students.

In contrast to that, these audio players resemble simple embedded systems running a single program with a very restricted user interface. Installing Jessie Lite, a CLI (command-line interface) version of the Raspbian Linux OS that omits all the GUI components, requires a display and keyboard, but after I configured the system to allow remote access from my desktop PC, each unit had only the hardware you see in Photo 1 and I did all further work over the network.

— ADVERTISMENT—

Advertise Here

You’ll find several Raspberry Pi projects providing complete multimedia environments for televisions, monitors, amplifiers, remote controls, and so forth, but we had much more limited goals. I installed MPlayer, a media player that can run in either a GUI environment, where it plays video files from DVDs and network streams, or a CLI configuration with audio files and streams.

The brief MPlayer configuration file in Listing 4 defines the setup needed for audio-only streaming. In most cases, the player automatically chooses the right options, but putting the defaults in the configuration file eliminates some start-up delay. I found that using the ffmpeg codec seemed to produce fewer MP3 stream decoding errors than the default mpg123 codec.

Listing 4 
These mplayer configuration settings (in ~/.mplayer/config) tailor it for audio-only streaming. The volume settings depend on the output hardware and your preferred sound level.

prefer-ipv4=true 
novideo=true 
cache=500 
cache-min=50 
ao=alsa 
format=s16le 
afm=ffmpeg,faad 
mixer-channel=PCM 
volume=20 
volstep=1 
quiet=false 

Under normal circumstances, you start MPlayer from the command line, then control it using keyboard characters while reading its status outputs on the display. As with most Unix-style programs, however, MPlayer reads its input and displays its output using standard I/O operations that allow another process to start, control, and monitor its operation, so I wrote a Python program to do exactly that.

The snippet of code from /etc/rc.local shown in Listing 5 starts the Python program immediately after the RPi finishes starting up. That program then uses the Python subprocess library to start mplayer with the default stream URL from the dictionary in Listing 6. The normal RPi boot sequence requires 7 s (depending on the MicroSD card’s speed), but the music doesn’t begin until mplayer buffers 250 KB from the stream, which, depending on the stream’s data rate, can add anywhere from 5 to 20 s.

Listing 5
All three streaming players execute the same Python program from a network share. This snippet belongs in /etc/rc.local and runs immediately after the RPi starts up.

mount -o ro server:/mnt/Projects/Streaming_Player/Firmware/ /mnt/part
sudo -u pi python /mnt/part/Streamer.py &

Because all three of our players have identical hardware, they fetch the same Python program from a file server on our home network. That means I can edit the program using my favorite text editor on a big portrait-mode display atop my desk, then have each player run the new version whenever it starts up.

CONTROLLING MPLAYER

As with most simple embedded programs, the Python program consists of an infinite loop that parses MPlayer’s output for status messages and checks for inputs from the numeric keypad and volume control knob. I don’t have enough space here to show the complete source code that you can download from the Resources link.

MPlayer’s output consists of separate text lines that the code in Listing 7 reads and parses, line by line, on each pass through the main loop. I discovered that MPlayer will shut down when it encounters network problems or detects an end-of-file mark from the streaming station. In most cases, however, simply restarting MPlayer with the same URL resolves the problem, so the code simply kills the MPlayer subprocess, does some cleanup, and restarts it. Only on the rare occasions when a station abruptly stops streaming must we manually switch to a different station.

Listing 7 
This code reads successive lines from MPlayer’s output and parses them to determine its status. I printed the StreamTitle string for diagnostic purposes, but it could go to an LCD display. MPlayer can shut down due to network or streaming server problems, although detecting that condition and automatically restarting MPlayer usually solves the problem.

text = lr.readline() 
if 'ICY Info: ' in text: 
  trkinfo = text.split(';') 
  for ln in trkinfo: 
    if 'StreamTitle' in ln: 
      trkhit = re.search(r"StreamTitle='(.*)'",ln) 
      TrackName = trkhit.group(1) 
      print 'Track name: ', TrackName 
      break 
elif 'Exiting...' in text: 
  print 'Got EOF / stream cutoff' 
  print ' ... killing dead mplayer' 
  p.kill() 
  print ' ... flushing pipes' 
  lw.truncate(0) 
  print ' ... discarding keys' 
  while [] != kp.poll(0): 
    kev = k.read 
  print ' ... restarting mplayer: ',Media[CurrentKC][0] 
  p = subprocess.Popen(Media[CurrentKC][-1], 
       stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT) 
  print ' ... running' 
  continue

The code in Listing 8 handles normal station changes in response to buttons on the numeric keypad. As before, the program executes this code each time around the main loop when it detects a keypress. The first line checks to see if the keycode in variable kc matches any of the keycodes stored in the Media dictionary shown in Listing 6. If so, it sends the “q” character that tells MPlayer to shut down gracefully, then restarts it with the new URL. However, MPlayer can’t respond if has already shut down due to a network problem, so the code forcibly kills the process before restarting.

Listing 6
A Python dictionary associates key codes with mplayer command lines. Many streaming radio stations implement load sharing with variable playlists that specify different stream URLs on each invocation, so a specific URL can become obsolete almost immediately. The CurrentKC variable defines both the initial station and the most recently selected one.

Media = {'KEY_KP7' : ['Classical',['mplayer','-playlist','http://stream2137.init7.net/listen.pls']],
         'KEY_KP8' : ['Jazz',['mplayer','-playlist','http://stream2138.init7.net/listen.pls']],
         'KEY_KP9' : ['WMHT',['mplayer','http://live.str3am.com:2070/wmht1']],
       ... more streaming radio stations ...
}

CurrentKC = 'KEY_KP7'
Listing 8 
Changing to a different streaming station requires shutting MPlayer
down and restarting it with the new URL. If MPlayer had terminated due to a station or network failure, it can’t respond and must be forcibly terminated.


if kc in Media: 
  print 'Switching stream to ',Media[kc][0]," -> ",Media[kc][-1][-1] 
  CurrentKC = kc 
  print ' ... halting player' 
  try: 
    p.communicate(input='q') 
  except Exception as e: 
    print 'Perhaps mplayer died?',e 
    print ' ... killing it for sure' 
    p.kill() 
  print ' ... flushing pipes' 
  lw.truncate(0) 
  print ' ... restarting player: ',Media[CurrentKC][0] 
  p = subprocess.Popen(Media[CurrentKC][-1], 
        stdin=subprocess.PIPE,stdout=lw,stderr=subprocess.STDOUT) 
  print ' ... running' 

Similar logic uses another dictionary to convert keypad and AC511 knob key codes into the characters that control MPlayer’s volume. The keypad buttons change the volume in larger steps than the knob, by the simple expedient of sending more characters for each press to MPlayer.

The entire Python program required only 150 lines, with much of the complexity devoted to figuring out when MPlayer stopped due to external forces beyond my control. I certainly cannot model all possible failure modes, much less write code to recover gracefully from each fault, but the current version works well enough for our purposes.

CONTACT RELEASE

Commercial streaming players, with more features and tidier packages, didn’t meet our requirements. Building these players and writing the code turned out to be surprisingly satisfying and, even better, we now have quiet music exactly where and when we want it. 

RESOURCES
Linux udev Device Manager, https://en.wikipedia.org/wiki/Udev.
E. Nisley, “Raspberry Pi: Jessie Lite Setup for Streaming Audio,” http://softsolder.com/2016/02/08/raspberry-pi-jessie-lite-setup-for-streaming-audio/.
———, “UDEV Rules for Cheap Numeric Keypads,”
http://softsolder.com/2016/02/18/udev-rules-
for-cheap-numeric-keypads/
.
Python evdev Package, https://python-evdev.readthedocs.org/.
Python Subprocess Management, https://docs.python.org/2/library/subprocess.html.
Xiph Stream Directory, http://dir.xiph.org/.

SOURCES
AC511 USB Sound Bar
Dell | https://accessories.dell.com/sna/product
detail.aspx?c=us&l=en&s=dhs&cs=19&sku=318
-2885

Raspberry Pi 2 Model B
Raspberry Pi Foundation | www.raspberrypi.org
Wireless numeric keypads
eBay | www.ebay.com/sch/i.html?_nkw=numeric
+keypad+usb+wireless&_sop=15

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JULY 2016 #312  – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
EE and Author at Poughkeepsie, NY | + posts

Ed Nisley is an EE and author in Poughkeepsie, NY. Contact him at ed.nisley@pobox.com with “Circuit Cellar” in the subject line to avoid spam filters.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2022 KCK Media Corp.

DIY Streaming Audio Player

by Ed Nisley time to read: 14 min