Tuesday, August 13, 2013

Raspberry Pi Pandora Streamer


I recently finished a project to turn the Raspberry Pi into a music streamer.  I had seen a couple of other projects here and here that were the same basic concept, so I knew it was possible (this is nothing new...just new for me), and I wanted to try to figure it out by myself.  My basic justification for the project was that I listen to a lot of music on Pandora or Spotify while reading and doing homework, but having it up on the computer means I have an instant distraction any time I feel like checking email, news, or whatever.  Building a small, standalone player with just a screen to display song titles and a simple interface using a few buttons removes that temptation.  I also had a couple of weeks off school between summer and fall sessions, and I wanted a quick project I could finish up before I lose all free time again.

Spotify provides a C API which looks pretty cool, but you need to have a premium membership ($10/month) to use it.  It would also be more involved to get working correctly, but I may come back to that in the future if I get more time, because it seems like it'd be a fun project.  For Pandora, there is a Linux command line program called pianobar that provides a really clean interface to Pandora and doesn't have any advertisements, and it's free.  I decided to stick with Pandora via pianobar for now and create my program to control it in Python.

I'm doing the development work using my Ubuntu desktop to ssh into the Raspberry Pi.  I'm using the Raspbian Wheezy distribution on the Pi, and you have to enable ssh when you first configure it.  To enter the configuration menu again later you can use the following command.
sudo raspi-config
I connected my Raspberry Pi to a monitor to figure out it's IP address (using the "ifconfig" command), which is 192.168.1.11 (replace that with your IP address the following command).  Running the following command from the terminal on your local computer will give you a terminal on the Raspberry Pi.  Using the "-X" option lets you open up GUI windows like the text editor "nedit" (or the simple Pandora GUI below).  You will be prompted for your password for the "pi" user, which is "raspberry", unless you've changed it.
ssh -X pi@192.168.1.11
First off, you need to install pianobar.  The instructions here are for Lubuntu, but they work on the Raspberry Pi as well.  The "make" program was already installed for me, but the rest were new.  You need to install "git" in order to download the source code from github, and then the rest of the packages are needed to build.
sudo apt-get install git
git clone https://github.com/PromyLOPh/pianobar.git
cd pianobar
sudo apt-get install libao-dev libgcrypt11-dev libgnutls-dev libfaad-dev libmad0-dev libjson0-dev make pkg-config
make
sudo make install
After all of this you can type "pianobar" on the command line and the program will start, prompting you for your Pandora username (email) and password.
 
When I first started pianobar, it printed the following error: "ALSA lib pcm.c:2217:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.front".  I found a post here that describes how to fix it.  You need to change the line that says "pcm.front cards.pcm.front" to "pcm.front cards.pcm.default" in /usr/share/alsa/alsa.conf.  The file is read only, so you'll need to open up the permissions or edit it using sudo.  For me, the line number to change was 130 but I'm not sure if that will be true for others.

I first set up pianobar back in May, and at that time there was a loud popping sound at the beginning and end of every song.  I did an update since then, and the popping went away, so they must have fixed whatever was causing that issue. 

Another issue I've had (and still do) is that if you try to skip songs too many times Pandora locks you out.  It starts out by making all tracks 42 seconds long of just silence, and if you keep skipping it eventually locks you out entirely.  It's not really consistent how many skips triggers it, but just realize that if it stops working you should probably just give it a break for a bit.

You can store the username and password in a file that pianobar will automatically load when it starts.  The filename is "/home/pi/.config/pianobar/config" and it should contain the following two lines.  This worked for me when I used pianobar interactively, but I later realized that you have to run programs that use the GPIO pins as root, so you'll want to put the same file at "/root/.config/pianobar/config".
user = zjonesengineering@gmail.com
password = your_password_here
The final step to configuring pianobar so that you can control it from python is to set up a fifo using the following command.  The same thing goes here for running as root, so I've included a second command for that.
mkfifo ~/.config/pianobar/ctl
sudo mkfifo /root/.config/pianobar/ctl
I started out by making a simple python GUI as a substitute for the buttons and screen that would later be connected to the Raspberry Pi.  This allowed me to get the interface between python and pianobar working correctly before having to worry about any of the hardware.  I used Tkinter for the GUI, and it was pretty basic.  There were a couple of lines that displayed information that would later be shown on a 16x2 LCD, and then buttons that have the same functions as the physical buttons that would be added later.  I have buttons to start/stop pianobar, play/pause the song, skip to next song, ban song (don't play again for 1 month), or "love" song.  There are plenty of other things I could add, and pianobar has a very full-featured command set, but I wanted to keep it simple.  These are the same buttons I kept on my final physical version.
# input_GUI.py

from tkinter import *
import subprocess
import re

fifo = "/home/zach/.config/pianobar/ctl"


class App:
    
    def __init__(self,master):
        frame = Frame(master)
        frame.pack()
        
        self.row_counter = 0
        self.pianobarOnFlag = 0

        #Start/Stop
        button = Button(frame, text='Start/Stop', command=self.startStopButton)
        button.grid(row=self.row_counter, column = 0)

        #Play/Pause
        button = Button(frame, text='Play/Pause', command=self.playPauseButton)
        button.grid(row=self.row_counter, column = 1)
        
        #Next Song
        button = Button(frame, text='Next', command=self.nextButton)
        button.grid(row=self.row_counter, column = 2)

        #Love Song
        button = Button(frame, text='Love', command=self.loveButton)
        button.grid(row=self.row_counter, column = 3)
        
        #Ban Song
        button = Button(frame, text='Ban', command=self.banButton)
        button.grid(row=self.row_counter, column = 4)
        
        self.row_counter+=1
        
        #Display: Line 1
        self.line1  =StringVar()
        Label(frame, text = 'Song').grid(row=self.row_counter, column=0)
        Entry(frame, textvariable=self.line1).grid(row=self.row_counter, column=1)
        self.row_counter+=1

        #Display: Line 1
        self.line2  =StringVar()
        Label(frame, text = 'Artist').grid(row=self.row_counter, column=0)
        Entry(frame, textvariable=self.line2).grid(row=self.row_counter, column=1)
        self.row_counter+=1
        
        
    def startStopButton(self):
        print('Start/Stop button pressed')
        if self.pianobarOnFlag == 0:
            print('Starting pianobar!')
            self.pianobar_output = open("pianobar.out","w")
            self.pianobar_output.close()
            self.pianobar_output = open("pianobar.out","r+")
            self.pianobarOnFlag = 1
            self.outfilePosition = 0
            subprocess.Popen("pianobar", stdout = self.pianobar_output)
        else:
            print('Stopping pianobar!')
            self.writeFifo(command = 'q')
            self.pianobarOnFlag = 0
            self.pianobar_output.close()

    def playPauseButton(self):
        print('Play/Pause button pressed')
        self.writeFifo(command = 'p')
        
    def nextButton(self):
        print('Next button pressed')
        self.writeFifo(command = 'n')

    def loveButton(self):
        print('Love button pressed!')
        self.writeFifo(command = '+')

    def banButton(self):
        print('Ban button pressed!')
        self.writeFifo(command = '-')
        
        
    def getSongInfo(self):
        print('getSongInfo Called!')
        if self.pianobarOnFlag == 1:
            print('Grepping for song info!')
            self.pianobar_output.seek(self.outfilePosition)
            text = self.pianobar_output.read()
            self.outfilePosition = self.pianobar_output.tell()
            print(text)

            infoStr = re.search('\>.*by.*on.*', text)
            if infoStr is not None:
                print('Info String:')
                print(infoStr.group(0))
                infoStrSplit = infoStr.group(0).split()
                print(infoStrSplit)
                for index in range(len(infoStrSplit)):
                    if infoStrSplit[index]=='by':
                        byIdx = index
                    if infoStrSplit[index]=='on':
                        onIdx = index
                song = ' '.join(infoStrSplit[1:byIdx])
                song = song.replace('"','')
                artist = ' '.join(infoStrSplit[byIdx+1:onIdx])
                artist = artist.replace('"','')
                self.line1.set(song)
                self.line2.set(artist)

    def getSongNameLoop(self):
        self.getSongInfo()
        root.after(2000,self.getSongNameLoop)

    def writeFifo(self, command):
        fifo_w = open(fifo, 'w')
        fifo_w.write(command)
        fifo_w.close()


root = Tk()
root.wm_title('Jukebox')
app = App(root)

root.after(2000,app.getSongNameLoop()) #Check for new song info every 2 seconds
root.mainloop()
I used Python 3 for the GUI, and I ran it from my Ubuntu desktop until I was ready to transition to the Raspberry Pi.  If you copy the code below into a text file called "input_GUI.py", you can call it from the terminal by typing "python3 input_GUI.py".  It will open up a GUI window that looks like the one below, and you can click the buttons to interface with pianobar.


Once I had the interface between Python and pianobar working correctly, I transitioned to the Raspberry Pi.  The main things to add here were the physical screen and buttons.  The most common library I was finding for GPIOs was for Python 2, not Python 3, so I switched to that.

There are two tutorials from Adafruit Industries that I used as a basis for setting up the GPIOs and the LCD library.  I've copied the pertinent commands below that you need for set up.
sudo apt-get install python-dev
sudo apt-get install python-rpi.gpio
sudo apt-get install python-setuptools
sudo easy_install -U distribute
sudo apt-get install python-pip
sudo pip install rpi.gpio
git clone http://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code.git
I think that I've listed all of the packages that are needed here.  I may have installed some a while ago that I forgot about, so if you try this and it doesn't work let me know and I'll figure out what I've left out.

Inside the package that downloads with the last command is a file named "Adafruit_CharLCD/Adafruit_CharLCD.py", which is the Python class I used to interface with the 16x2 LCD display.  You need to copy this file to wherever you are running the final Python script from.  This class is set to use the GPIO pins for a Raspberry Pi v1, and I have a Raspberry Pi v2 (you likely do as well), so that requires a minor tweak to line 57 of the file to the following because of a renaming of the pins.  The key is that the "27" has replaced a "21".
def __init__(self, pin_rs=25, pin_e=24, pins_db=[23, 17, 27, 22], GPIO = None):
This site has a good graphic that maps the pin headers on the Raspberry Pi to their GPIO numbers, and I found it to be helpful.  I used a Pi Cobbler for prototyping, and it only lists GPIO numbers for pins that don't have an alternate function.  I'm using the SPI pins as GPIOs, so I it was useful to see what numbers those were.

The circuit diagram for the display is very simple.  It basically just connects the LCD screen and buttons to the Raspberry Pi.  I drew up a quick schematic in Eagle of the connections.  Ignore the "P$" numbers on the Raspberry Pi header pins...I made a rough Eagle part to represent the header and those numbers don't mean anything.


The code should be fairly self-explanatory.  The first section is a class to interface with pianobar, which is very similar to the class I set up in the GUI version.  The second section contains a function to update the display.  Artist/song names can be longer than 16 characters, so I set it up so that if the text is too long it scrolls across the display.  The bulk of a code is a loop that lasts forever that checks for button presses every half second.  I set it up so that an LED would turn on whenever I pushed a button other than the "start/stop" button and stay on until I released it as a simple form of debouncing the inputs.  The full code is below.
#!/usr/bin/python

# rpi_jukebox.py

from Adafruit_CharLCD import Adafruit_CharLCD
from time import sleep
import subprocess
import re
import math
import RPi.GPIO as GPIO

##############################################
# Pianobar Class
##############################################

class Pianobar:
    def __init__(self):
        #FIFO location
        self.fifo = "/root/.config/pianobar/ctl"

    def start(self):
        print('Starting pianobar!')
        #Create file for pianobar output...used to get artist names and song titles
        self.pianobar_output = open("pianobar.out","w")
        self.pianobar_output.close()
        self.pianobar_output = open("pianobar.out","r+")
        self.outfilePosition = 0
        #Start pianobar
        subprocess.Popen("pianobar", stdout = self.pianobar_output)

    def stop(self):
        print('Stopping pianobar!')
        #Stop pianobar
        self.writeFifo(command = 'q')
        #Close output file
        self.pianobar_output.close()

    def playPause(self):
        print('Play/Pause')
        self.writeFifo(command = 'p')
        
    def next(self):
        print('Next')
        self.writeFifo(command = 'n')

    def love(self):
        print('Love')
        self.writeFifo(command = '+')

    def ban(self):
        print('Ban')
        #Ban song for 1 month
        self.writeFifo(command = '-')
        
    def getSongInfo(self):
        #Get artist name and song name from output file
        self.pianobar_output.seek(self.outfilePosition)
        text = self.pianobar_output.read()
        self.outfilePosition = self.pianobar_output.tell()
        infoStr = re.search('\>.*by.*on.*', text)
        if infoStr is not None:
            infoStrSplit = infoStr.group(0).split()
            for index in range(len(infoStrSplit)):
                if infoStrSplit[index]=='by':
                    byIdx = index
                if infoStrSplit[index]=='on':
                    onIdx = index
            song = ' '.join(infoStrSplit[1:byIdx])
            song = song.replace('"','')
            artist = ' '.join(infoStrSplit[byIdx+1:onIdx])
            artist = artist.replace('"','')
            songInfo = [song,artist]
        else:
            songInfo = ['','']
        return songInfo


    def writeFifo(self, command):
        #Write a command to the pianobar FIFO
        fifo_w = open(self.fifo, 'w')
        fifo_w.write(command)
        fifo_w.close()


##############################################
# Update Display Function
##############################################

def updateDisplay(songInfo,lcd,displayOld):
    display = ['',displayOld[1],displayOld[2]]
    #Song - line1
    if len(songInfo[0]) <= 16: #Center on screen
        padding = (16 - len(songInfo[0])) / 2
        line1 = (' ' * int(math.floor(padding))) + songInfo[0] + (' ' * int(math.ceil(padding)))
    else: #Scroll
        line1 = songInfo[0][display[1]:int(min(len(songInfo[0]),display[1]+16))]
        line1 = line1 + ' ' * int(16-len(line1))
        display[1]+= 1
        if display[1] == len(songInfo[0]):
            display[1] = 0
    #Artist - line2
    if len(songInfo[1]) <= 16: #Center on screen
        padding = (16 - len(songInfo[1])) / 2
        line2 = (' ' * int(math.floor(padding))) + songInfo[1] + (' ' * int(math.ceil(padding)))
    else: #Scroll
        line2 = songInfo[1][display[2]:int(min(len(songInfo[1]),display[2]+16))]
        line2 = line2 + ' ' * int(16-len(line2))
        display[2]+= 1
        if display[2] == len(songInfo[1]):
            display[2] = 0
    display[0] = str(line1 + '\n' + line2)
    if display[0] != displayOld[0]: #Only update if new
        lcd.clear()
        lcd.message(display[0])
    return display

##############################################
# Main Program
##############################################

#Set up GPIO for buttons
GPIO.setmode(GPIO.BCM) #Use GPIO numbers

#Assign buttons
startStopButton = 2 #Header Pin 3
playPauseButton = 3 #Header Pin 5
nextButton = 4      #Header Pin 7
loveButton = 10     #Header Pin 19
banButton = 9       #Header Pin 21

#Set up button pins as inputs
GPIO.setup(startStopButton, GPIO.IN)
GPIO.setup(playPauseButton, GPIO.IN)
GPIO.setup(nextButton, GPIO.IN)
GPIO.setup(loveButton, GPIO.IN)
GPIO.setup(banButton, GPIO.IN)

#Status LED
statusLED = 11      #Header Pin 23
GPIO.setup(statusLED, GPIO.OUT)
GPIO.output(statusLED, False)

#Initialize LCD 
lcd = Adafruit_CharLCD()
lcd.begin(16,2)

pianobarOnFlag = 0

while (1):
    sleep(0.5)
    if GPIO.input(startStopButton) == False:
        if pianobarOnFlag == 0:
            #Turn on pianobar
            pb = Pianobar()
            pb.start()
            songInfo = ['RPI Jukebox','Initializing...'] 
            displayOld = ['',0,0] #Display String, Song Scroll Idx, Artist Scroll Idx
            pianobarOnFlag = 1
        else:
            #Turn off pianobar
            pb.stop()
            pianobarOnFlag = 0
            songInfo = ['RPI Jukebox','Goodbye...']
            updateDisplay(songInfo,lcd,displayOld)
            sleep(5)
            songInfo = [' ',' ']
            updateDisplay(songInfo,lcd,displayOld)
    if (GPIO.input(playPauseButton) == False) and (pianobarOnFlag == 1):
        pb.playPause()
        #Flash LED to register input
        GPIO.output(statusLED, True)
        lcd.clear()
        lcd.message('   Play/Pause   ')
        displayOld = [' ',0,0] #Force display update after
        while(GPIO.input(playPauseButton) == False):
            sleep(0.1)
        GPIO.output(statusLED, False)
    if (GPIO.input(nextButton) == False) and (pianobarOnFlag == 1):
        pb.next()
        #Flash LED to register input
        GPIO.output(statusLED, True)
        lcd.clear()
        lcd.message('   Next Song    ')
        displayOld = [' ',0,0] #Force display update after
        while(GPIO.input(nextButton) == False):
            sleep(0.1)
        GPIO.output(statusLED, False)
    if (GPIO.input(loveButton) == False) and (pianobarOnFlag == 1):
        pb.love()
        #Flash LED to register input
        GPIO.output(statusLED, True)
        lcd.clear()
        lcd.message('   Love Song    ')
        displayOld = [' ',0,0] #Force display update after
        while(GPIO.input(loveButton) == False):
            sleep(0.1)
        GPIO.output(statusLED, False)
    if (GPIO.input(banButton) == False) and (pianobarOnFlag == 1):
        pb.ban()
        #Flash LED to register input
        GPIO.output(statusLED, True)
        lcd.clear()
        lcd.message('    Ban Song    ')
        displayOld = [' ',0,0] #Force display update after
        while(GPIO.input(banButton) == False):
            sleep(0.1)
        GPIO.output(statusLED, False)
    if pianobarOnFlag == 1: 
        #Update Display
        songInfoNew = pb.getSongInfo()
        if songInfoNew[0] != '' and songInfoNew[0] != songInfo[0]:
            songInfo = list(songInfoNew)
        displayOld = updateDisplay(songInfo,lcd,displayOld)
I started out by breadboarding the circuit, and I used an Adafruit Pi Cobbler breakout board for the Raspberry Pi header.  Once it all worked, I then soldered it up on a small protoboard to make it more permanent.  I soldered the screen on at as much of an angle as the header would allow, and then used a couple of long machine screws in two of the mounting holes on the protoboard to prop up the entire thing so the screen is readable while sitting on my desk.  It's not pretty or polished, but it does what I need and was fun to put together.

Here's a video showing the finished project.  I hit the limit where it stops playing songs (and does 42 seconds of silence instead) while I was filming the video, but you can see the idea of how it works.