Turn Your Raspberry Pi Into a Voice-Activated DJ

Posted by Renato Recio on November 16, 2015

Home Automation Part Three:
Use your Speakers, Siri, and your Raspberry Pi to play any YouTube song instantly, and ad-free!


In Part Two of our Home Automation Series, we learned how to remotely control RF devices using our Raspberry Pi and voice commands through Siri. Next, we are going to combine everything we have learned to enhance our program and integrate a DJ module that will play any YouTube song instantly through your speakers. We can tell our DJ to play, stop, pause, and control the volume of any song. And the best part is, we can control it from anywhere!
The only additional requirement for this tutorial is to have a set of speakers that contain an audio cable that can plug into your Raspberry Pi's audio port. Aside from that, we will rely heavily on software for this tutorial.

So the first thing we need to do is put our speakers in front of an RF controlled power outlet. If you haven't seen the previous tutorial, check it out here to see how we operate on RF devices. Once we have our speakers connected, we will set up two additional commands to our YAML file pertaining to the outlet we just added. Add these two lines to the end of commands.yaml:
'play': '123123123'
'stop': '123123123'

The 'play' command will send the code to turn on your outlet, and the 'stop' command will turn off the outlet. This part isn't required, but lets us save power by turning on our speakers only when we need them.

Next, we will install the necessary libraries for playing our music. I spent some time contemplating what to use for streaming music, and I concluded that youtube-dl is the ideal software to do this. It turns out there is a great wrapper around youtube-dl called mps-youtube, which adds a lot of additional functionality that we will need to create our DJ. To download this, enter the following into your command line:
sudo apt-get install python3-pip
sudo pip3 install mps-youtube

Considering the installation was successful, we now have the program we will need to query and play our music. The great thing about mps-youtube is that it is very efficient at querying the youtube API and streaming the audio live, and it has several configurable options such as letting you decide which audio player to use and whether or not to stream video, etc. By default, we want to use omxplayer and we want to set video streaming to false. To enforce these settings, start the mpsyt program and set the commands by typing in the following:
sudo mpsyt
set show_video false
set player omxplayer

Now we should have the dependencies required to run our program. For this tutorial, we will create a new file called djraspberry.py that will be used by driver.py. Starting at the top of our new djraspberry.py file, we will introduce a few variables and imports:
import os, signal, subprocess, time

VOLUME_SETTINGS = (LOW, MEDIUM, NORMAL, HI, MAX) = ('low', 'medium', 'normal', 'hi', 'max')
COMMANDS = (PLAY, STOP, PAUSE, RESUME, VOLUME) = ('play', 'stop', 'pause', 'resume', 'volume')
VOLUME_CONTROL = {LOW: 0, MEDIUM: 10, NORMAL: 20, HI: 25, MAX: 30}

This is what I am using to control volume, and the commands I chose to do various actions. If you would like your player to behave differently, feel free to change the voice commands for actions (play, stop, pause...), the voice commands for the volume levels (low, medium, normal), and the actual levels for the volume.

Next, we will create the class that we will be using:
class DJRaspberry(object):

    def __init__(self):
        self.music_process = None
        self.current_volume = VOLUME_CONTROL[NORMAL]

The music_process will be the subprocess in memory that will be running the music. Having a reference to this object allows us to execute additional commands later on, such as raising the volume or pausing the song.

Then, we will define our run() function. This will be called directly by driver.py, and it will determine which function to call based off of the Siri command that we pass as a parameter.
    def run(self, data):
        action = data.split(' ')[0]
        if PLAY in action:
            self.play_music(data.split(PLAY)[1].strip())
        elif STOP in action:
            self.stop_music()
        elif VOLUME in action:
            self.adjust_volume(data.split(VOLUME)[1].strip())
        elif PAUSE in action or RESUME in action:
            self.pause_and_resume()

The play_music() and adjust_volume() functions are the only functions that need additional information, such as the song to play, and the level of volume to change to.

Now we will define each function, starting with play_music:
    def play_music(self, song):
        self.stop_music()
        self.music_process = subprocess.Popen('sudo mpsyt;'.format(**locals()),
                                              shell=True,
                                              stdout=subprocess.PIPE,
                                              stdin=subprocess.PIPE,
                                              preexec_fn=os.setsid)
        self.music_process.stdin.write("/{song}\n1\n".format(**locals()))

This function will force an existing song to stop playing by killing the music_process (which will we see next when we implement stop_music()). After that, it will create a subprocess and execute the same mpsyt command we used earlier, and it will then use the stdin PIPE to relay commands since mpsyt opens up an interactive shell. Once we are in the interactive mode, we pass in '/name of song' and '1', which will play the first song in the result list.

Here is how it would look if we were running mpsyt directly, instead of through Python:

Mpsyt actually has the ability to play a url without entering the interactive mode that you see here, but it turns out that mpsyt already interacts with Google's APIs, so we get very powerful and fast search results. This means that the whole process of launching mpsyt, entering a search query, and getting back results takes less than 2 seconds overall (depending on your internet connection of course). Also, since we are relying purely on YouTube APIs to get our search result, we will get the most optimal search results ranked first, and therefore we will blindly assume that the first result is what we want to play. Another benefit of leveraging YouTube as our music player is that we don't even have to say the entire song name correctly. We can say just part of the song's lyrics if the song is well known enough, and the search algorithm is advanced enough to know which song we want to listen to.

So now that we have a play function, let's implement stop_music():
    def stop_music(self):
        if self.music_process:
            os.killpg(self.music_process.pid, signal.SIGTERM)
            self.music_process = None

Simple enough. If our dj class is holding a music process, it will execute a SIGTERM signal to kill the process that has a matching pid. This will immediately terminate the song from playing.

Next we have our pause_and_resume():
    def pause_and_resume(self):
        if self.music_process:
            self.music_process.stdin.write(" ")

Our easiest function yet. Simply sends a space character through the stdin PIPE that we created when we initialized our music subprocess. Mpsyt then interprets this as a pause / resume (the same key is used for pause and resume).

Now on to our last function, adjust_volume():
    def adjust_volume(self, target_volume):
        if not self.music_process:
            return
        target_volume = VOLUME_CONTROL.get(target_volume.split(' ')[0])
        while self.current_volume < target_volume:
            self.current_volume += 1
            self.music_process.stdin.write("=")
            time.sleep(1)

        while self.current_volume > target_volume:
            self.current_volume -= 1
            self.music_process.stdin.write("-")
            time.sleep(1)

Similar to our pause_and_resume function, the adjust volume function will send a character through the stdin PIPE, and mpsyt will interpret it as either volume down or volume up. Each command (up/down) will increase/decrease the volume by 3 dB, so we will have to loop through several iterations before we get the current_volume to our desired target_volume.

This completes the djraspberry.py class that we need for our driver. Now, we just have to integrate it with driver.py. Open driver.py, and add the following at the top:
from djraspberry import DJRaspberry
dj = DJRaspberry()

And in our execute_command function, this will be the only change we need to make:
    if 'dj' in data:
        dj.run(data.split('dj')[1].strip())

So to give an overview of everything we have covered, here are our total three files:

Click here to see driver.py

Click here to see djraspberry.py

Click here to see commands.yaml

At this point, we have completed our djraspberry.py class, which concludes Part Three of our Raspberry Pi Home Automation system with Siri. We will run our file the same way we ran it previously, and now we will say 'Note DJ Play {SongName}' every time we want to play a song, and 'Note DJ Stop' or 'Note DJ Volume Low' for some of our various other commands. If you have any questions, or want to learn more about this, simply comment below!

For this tutorial, I've also created a demo to give you a clearer idea of how this all comes together. Check out the video below!


Thanks for visiting!