A touchscreen driven jukebox player for Sonic Pi

TouchScreen Sonic Pi Jukebox

TouchScreen Sonic Pi Jukebox

This is a project which grew out of a previous jukebox program I had written for Sonic Pi, which enabled it to be controlled either with a python program running on the Pi, or via my mobile phone using the Telegram App. With the arrival of the Raspberry Pi official touch screen display, I saw that this might be used as the front end to such a program. I produced a first working version a couple of months ago, but it had some shortcomings, and recently I revisited and the result is this article.

There is a video of the project here

Prerequisites
You need a Raspberry Pi offical touch screen, with a Raspberry Pi attached. Preferrably this should be a Pi2. I haven’t tested with a Pi Model B+ but it should work, although performance may not be as good.
Also required, is a suitable means of listening to the sound via the 3.5mm output jack on the Pi.
You should have an up to date copy of Raspian (Jessie) on the SD micro card in the Pi.

A touch screen is not a magic panacea. You have to do a little bit of work to be able to use it to control a program. One of the environments which lends itself to producing touch screen operated front ends is Kivy. I had not heard of this before, but came across it in the superb article on using the Raspberry Pi touch screen to control GPIO connected devices by Matt Richardson.

The project falls into Four parts.
1 Installing Kivy
2 Adding the sonic_pi_cli gem which enables text files to be sent to Sonic Pi for execution.
3. Creating the kivy main.py program which which is the basis of the Jukebox. This will also involve choosing the (up to 12) pieces that you wish to play. For convenience I will supply some.
4. Configuring the Pi to autostart Sonic Pi and the Kivy program on startup.

Installing Kivy
I cannot better the excellent description in Matt Richardson’s article. Follow this up to and including step 17.

Adding sonic-pi-cli gem
In order to do this, without installing and using a system like rvm, we have to do a bit of preliminary work so that the correct permissions will be in place during the install process. First, if you have switched to booting into a command line environment, start the graphical environment using startx. You may also like to switch back to this environment on boot by using Raspberry Pi Configuration utility in Menu -> Preferences. The graphics environment is needed for Sonic Pi to work.
Start a terminal window and type the following:

sudo mkdir /var/lib/gems
sudo chown pi /var/lib/gems
sudo chown pi /usr/local/bin
gem install sonic-pi-cli

When the install has completed, reset the ownership of /usr/local/bin

sudo chown root /usr/local/bin

Now test the cli by starting Sonic Pi, loading in any piece of reasonable duration, eg one of the example files, and running it. While it is running from the terminal window type:

sonic_pi stop

The piece should stop playing, showing that the cli command is working.

Creating the kivy main.py program
Kivy can be quite confusing to start with. Partly this is because there are two ways to use it. You can have a .kv file which contains details of the screen layout, and a separate python script which contains the logic of how these items are used, or everything can be contained just in a python script. After some playing around, and googling for kivy scripts I elected to use the former, and the program I came up with has elements of Matt Richardson’s script to control GPIO pins, together with bits from a snippet I found at http://stackoverflow.com/questions/18958520
The program listing is below. Also required are two small graphics files which can be downloaded in the resources file at the end of the post.

#!/usr/bin/python
#Sonic Pi touch screen kivy program by Robin Newman
#with acknowlegements: https://github.com/mrichardson23/rpi-kivy-screen
# and http://stackoverflow.com/questions/18958520
# written January 2016

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.slider import Slider
from kivy.uix.image import Image
import os

b={} #holds button ids for binding
s=[] #holds text for buttons
c=[] #holds linked commands for buttons

#build text lists
#path to Sonic Pi files to play (include trailing /)
sf='/home/pi/SP-files/'
#used in building command line for sonic_pi gem
pre='cat '+sf
post=' | sonic_pi' 

#description (d**) (2 lines max separated by \n), and filename  (n**) entries
#for the 12 files in the jukebox selection
d1='Ambient\nExperience'
n1='ambient_experiment.rb'
d2='Blimp\nZones'
n2='blimp_zones.rb'
d3='Blip\nRhythm'
n3='blip_rhythm.rb'
d4='Chord\nInversions'
n4='chord_inversions.rb'
d5='Filtered\ndnb'
n5='filtered_dnb.rb'
d6='FM\nNoise'
n6='fm_noise.rb'
d7='Jungle'
n7='jungle.rb'
d8='Ocean'
n8='ocean.rb'
d9='Reich\nPhase'
n9='reich_phase.rb'
d10='Shufflit'
n10='shufflit.rb'
d11='Tilburg\nv2'
n11='tilburg_2.rb'
d12='Time\nMachine'
n12='time_machine.rb'

####### You shouldn't need to alter anything after this line
#setup strings for description and filenames
s.append('Tune 1\n'+d1)
c.append(pre+n1+post)
s.append('Tune 2\n'+d2)
c.append(pre+n2+post)
s.append('Tune 3\n'+d3)
c.append(pre+n3+post)
s.append('Tune 4\n'+d4)
c.append(pre+n4+post)
s.append('Tune 5\n'+d5)
c.append(pre+n5+post)
s.append('Tune 6\n'+d6)
c.append(pre+n6+post)
s.append('Tune 7\n'+d7)
c.append(pre+n7+post)
s.append('Tune 8\n'+d8)
c.append(pre+n8+post)
s.append('Tune 9\n'+d9)
c.append(pre+n9+post)
s.append('Tune 10\n'+d10)
c.append(pre+n10+post)
s.append('Tune 11\n'+d11)
c.append(pre+n11+post)
s.append('Tune 12\n'+d12)
c.append(pre+n12+post)
s.append('Stop\nPlaying')
c.append('sonic_pi stop')
s.append('Quit\nProgram')
c.append('')

def press_callback(obj):
    global s,c #make lists global
    if obj.text == str(s[13]):
        os.system('sonic_pi stop')
        os.system('killall sonic-pi')
        os.system('killall ruby')
        App.get_running_app().stop()
    for i in range(0,13):
        if obj.text == s[i]:
            os.system('sonic_pi stop') #double stop sent for stop command but doesn't matter
            os.system(str(c[i]))

def update_vol(obj, value):  #upadte volume via alsamixer
    global vol
    #print ("Updating volume to:" + str(obj.value))
    vol=obj.value
    os.system("amixer sset 'Master' "+str(vol)+"% >/dev/null")

    

Builder.load_string("""
:	
    boxes: _boxes 
    AnchorLayout:
        anchor_x: 'center'
        anchor_y: 'top'
        ScreenManager:  #maybe can simplify as only 1 screen used
            size_hint: 1, 1
            id: _screen_manager
            Screen:
                name: 'screen1'
                BoxLayout: 
                    orientation: 'vertical'
                    padding: 50
                    id: _boxes
    AnchorLayout:
        anchor_x: 'center'
        anchor_y: 'bottom'
        BoxLayout:
            orientation: 'horizontal'
            size_hint: 1,.12
            Label:
                #valign: 'top'
                font_size: '20sp'
                text: 'Sonic Pi 12-Tunes Touch Screen Jukebox'""")


class Boxes(FloatLayout):
    global s,c
    def __init__(self, **kwargs):
        super(Boxes, self).__init__(**kwargs)
        bx1 = BoxLayout(orientation='horizontal')
        bx2 = BoxLayout(orientation='horizontal')
        bx3 = BoxLayout(orientation='horizontal')
        bx4 = BoxLayout(orientation='horizontal')
        bx5 = BoxLayout(orientation='horizontal')
        bx6 = BoxLayout(orientation='horizontal')

        bx1.add_widget(Image(source='sonic-pi-web-logo.png'))
        bstop=Button(text=s[12])
        bstop.bind(on_press=press_callback)
        bx1.add_widget(bstop)
        bquit=Button(text=s[13])
        bquit.bind(on_press=press_callback)
        bx1.add_widget(bquit)
        bx1.add_widget(Image(source='logo.png'))

        for i in range(0,4):
            b[i]=Button(text=str(s[i]))
            b[i].bind(on_press=press_callback) 
            bx2.add_widget(b[i])
        for i in range(4,8):
            b[i]=Button(text=str(s[i]))
            b[i].bind(on_press=press_callback)
            bx3.add_widget(b[i])
        for i in range(8,12):
            b[i]=Button(text=str(s[i]))
            b[i].bind(on_press=press_callback)
            bx4.add_widget(b[i])       
        bx5.add_widget(Label(text='Adjust volume -->',font_size='20sp'))
        vol=80 #initial value
        b[12]= Slider(orientation='horizontal', min=0,max=100,value=vol)
        b[12].bind(on_touch_down=update_vol,on_touch_move=update_vol)
        bx5.add_widget(b[12])

        self.boxes.add_widget(bx1)
        self.boxes.add_widget(bx2)
        self.boxes.add_widget(bx3)
        self.boxes.add_widget(bx4)
        self.boxes.add_widget(bx5)

class TestApp(App):
    def build(self):
        return Boxes()

if __name__ == '__main__':
    TestApp().run()

I do not intend to give a detailed explanation of the program, but a few pointers may help. There are one or two user configurable bits near the start of the program. The variable sf holds the absolute path to the location of the sonic pi files that will be played by the jukebox program.

n1 to n12 are variables which hold the filenames of the 12 sonic pi files to be played. I have them ending in .rb but they could equally well be .txt files. In this example version I have used 12 of the example files included in Sonic Pi. These were accessed from /opt/sonic-pi/etc/examples and I used entries in the wizard and iillusionist folders stored there, copying them to the folder specified by sf /home/pi/SP-files.
In my own version I have 12 of my own sonic pi files used instead.

d1 to d12 are corresponding variables which hold a 1 or two line description of these files, which appear as the text on the selector buttons on the screen. If you want a second line it is separated from the first by a \n eg d4=’Chord\nInversions’.

From the variables n1-12 and d1-12 two string arrays s and c are generated, the first containing the complete text for each button incluidng the Tune 1…Tune 12 prefix, and teh second the complete command needed to be sent to the cli gem to tell Sonic Pi what to play.

The program defines two functions which are activated by the touch elements in the screen. First there is a callback routing which is called whenever one of the 14 touch buttons is touched. (The 12 program select buttons, plus buttons for Stop Playing and Quit Program.) This compares the text of the calling button with the values in the s array and when it finds a match  stops Sonic Pi playing the current tune (if any) and then sends the command for the selected tune. It deals separately with the commands to stop playing or to quit the program, in the latter case quitting Sonic Pi as well.
The second function deals with the graphical slider which is used as a volume control. This extracts the value (0-100) selected by the user, and sends it via an os.system command to utility amixer where it is used to adjust the ‘Master’ control. Here it is perhaps worth saying that I also installed pulseaudio on the system, which you should also do before using this script. Normally amixer only exposes a ‘PCM’ control which can be used to adjust the volume. However experiment showed that this was very non-linear, and did not give a very satisfactory performance. By installing pulseaudio the ‘Master’ volume control is exposed which gives a much more satisfactory adjustment to the volume. If you DONT want to install pulseaudio, you can amend the program by altering the line

os.system("amixer sset 'Master' "+str(vol)+"% >/dev/null")

to read

os.system("amixer sset 'PCM' "+str(vol)+"% >/dev/null")

To install pulseaudio use

sudo apt-get install pulseaudio

The program should be saved with name main.py and stored in a folder named SP-touch-screen created in the user pi home director together with the two logo files sonic-pi-web-logo.png and logo.png (download from the resources file at the end of the post).

Interim testing
At this stage you do some interim testing to see whether things are working ok so far. One word of warning. Once you start a kivy program ctrl-C will NOT work to stop the program. You can only use the built in Quit Program button to do this. It there are any problems you can end up with no means ot stopping the program other than removing the power lead.  As this is undesirable, you are strongly advised to have a remote ssh login available to the Raspberry Pi, so that you can log onto it from an external PC or Mac, and from there use killall python to stop the program running. If you are unfamiliar how to set this up, then the guide https://www.raspberrypi.org/documentation/remote-access/ssh/ should help.
To try out the program, make sure you have set up the folder SP-files to contain the 12 examples files used in the program, then start up Sonic Pi from the Menu->Programming link. When it has started, use a terminal window and type:

cd ~/SP-touch-screen
python main.py

which should startup the touch screen kivy front end. Try out some of the buttons to see if you can play a tune. If a tune is running pressing another tune selector button will stop the first tune and start the second. Stop Playing should sto the currently playing tune. You should also be able to change the volume by tapping on the horizontal selector at different points, or sliding the circle left or right. Finally pressing Quit Program should quit both the kivy screen AND Sonic Pi.

Configuring the Raspberry Pi to autostart into the touchscreen jukebox
The final stage is to add a small script and a .desktop file in order to get the Raspberry Pi to startup the program automatically on boot. To do this I utilised the fact that the Pi will autostart any application whose desktop file entry is copied to the folder /home/pi/.config/autostart
In this case we have to generate a small python script because we want TWO programs to start one after the other. First we must ensure that Sonic Pi is running, then we must start the kivy screen program. The startup script which I called startupkivy is shown below.

#!/usr/bin/python

import os
import time

os.system('/usr/bin/sonic-pi &')
time.sleep(8)
os.system("amixer sset 'Master' 90%") #starting volume setting
os.system('python /home/pi/SP-touch-screen/main.py &')

This needs to be placed in the folder /usr/local/bin
which you can do using

sudo nano /usr/local/bin/startupkivy

type in the text and then use ctrl+x followed by Y and return key to exit.
Set the file executable using

sudo chmod 755 /usr/local/bin/startupkivy

Now set up the desktop file:

cd ~/.config
mkdir autostart
cd autostart
nano kivy.desktop

type in the text:

[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Exec="startupkivy"
Icon=/usr/share/pixmaps/sonic-pi.png
Terminal=false
Name=startupkivy
Categories=Application;Development;

and quit with ctrl+x, followed by Y, and return key
If you wish, you can copy this file to the desktop as well using:

cp kivy.desktop ~/Desktop

That completes the setup. Now you can test it out.
First make sure that the Pi is set too boot to the GUI interface. You can check with the Raspberry Pi Configuration utility in Preferences. Make sure that the Boot: To Desktop button is marked.
Now reboot your Pi and it should startup in the GUI and immediately start Sonic Pi, followed 8 seconds later by the touchscreen jukebox program.
It should be possible to startup, play a selection of tunes, quit the program, select Shutdown from the Menu and shutdown the Pi, all with the use of your finger, and with no mouse or keyboard at all.
If you have copied the .desktop file to the Desktop you can double click it to restart Sonic Pi and the kivy main.py program if you wish.

At the time of writing Sonic Pi 2.7 is still the version of Sonic Pi bundled in Jessie. IF you want a later version you can download it from sonic-pi.net and install it on your desktop. For example version 2.9 installs in sonic-pi-v2.9.0 YOu can alter the startupkivy file to use this instead of the default version by changing the line:

os.system('/usr/bin/sonic-pi &')

to

os.system('/home/pi/sonic-pi-v2.9.0/bin/sonic-pi &')

Resources
You can download a file containing :
the SP-touch-screen folder containing the two logo files and the main.py script
the startupkivy program which should be copied (using sudo) to /usr/local/bin
the kivy.desktop file  which should be placed in ~/.config/autostart

To download open a terminal window and type

wget http://r.newman.ch/rpi/touchscreen/resources.tar.gz
tar zxvf resources.tar.gz
cd resources

All you have to do is to install the other requirements, kivy, sonic-pi-cli, pulseaudio as detailed above. Have fun!

Advertisements

2 thoughts on “A touchscreen driven jukebox player for Sonic Pi

    • It’s several months since I wrote the article, and I haven’t used kivy since so I’m very rusty. I think it should be possible to add another page, but I’m afraid you’ll have to work it out by looking at the numerous references to and examples of kivy use on the Internet. That’s how I managed to get as far as I did with this. Good luck!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s