MPD - Announce current song

Posted by Eric Scheibler at December 29, 2018

My server runs an MPD instance for music playback. The following script for Mac announces the currently playing song after every song change. It uses Mac OS build in TTS and tries to guess the song title language.

Download: mpd_speak_current_song.py

#!/Users/eric/.virtualenvs/speak_current_song/bin/python
# -*- coding: utf-8 -*-

# mpd: speak currently playing song after it has changed
#
# This script uses the mpd client mpc to watch for song changes on the running mpd server and speaks them aloud
# Song title language detection with https://github.com/Mimino666/langdetect
# It's written for Mac OS but could be easily adapted to Linux as well (change tts_command variable)
#
# author: Eric Scheibler <email@eric-scheibler.de>
# Date: 2019-03-29
# Version: 0.2
#
# Installation requirements:
#   brew install mpc
#   pip install langdetect
#
# Usage: Must run in background for example within a screen session
#   ./mpd_speak_current_song

# configuration
#
# adapt the following params to your needs
# leave them blank if not required
#
# mpd host and port
mpd_host = "192.168.2.11"
mpd_port = "6600"
# audio output device
# Mac: get all devices with: say -a '?'
audio_output_device = ""
# voice table
# Mac: get all installed voices with: say -v '?'
voices = {
        "de" : {"name":"Yannick", "rate":280},
        "en" : {"name":"Samantha", "rate":200},
        "fr" : {"name":"Thomas", "rate":200},
        # default: different German voice
        "default" : {"name":"Anna", "rate":240}
        }


import subprocess, sys, threading
from langdetect import detect, DetectorFactory
from langdetect.lang_detect_exception import LangDetectException


# construct commands
#
# control mpd with: mpc ...
mpc_command = ["mpc", "--quiet"]
if mpd_host:
    mpc_command += ["--host", mpd_host]
if mpd_port:
    mpc_command += ["--port", mpd_port]
# speak with: say ...
tts_command = ["say"]
if audio_output_device:
    tts_command += ["-a", audio_output_device]


class SpeakCurrentSong(threading.Thread):

    def __init__(self, song, voice):
        threading.Thread.__init__(self)
        self.song = song
        self.voice = voice
        # class variables
        self.process = None
        self.finished = False
        self.killed = False

    def run(self):
        # pause playback
        if not self.killed:
            self.execute(mpc_command + ["pause"])
        # announce new song
        if not self.killed:
            self.execute(tts_command + ["-v", self.voice.get("name"), "-r", str(self.voice.get("rate")), self.song])
        # resume playback
        if not self.killed:
            self.execute(mpc_command + ["play"])
        self.finished = True

    def execute(self, command):
        self.process = subprocess.Popen(command)
        self.process.wait()

    def kill(self):
        self.process.kill()
        self.killed = True


def main():
    speakCurrentSongThread = None
    # force deterministic language detection results
    DetectorFactory.seed = 0
    while True:
        try:
            # wait for change of current song
            current_song = subprocess.check_output(mpc_command + ["current", "--wait"], encoding='UTF-8').strip()
        except subprocess.CalledProcessError as e:
            # could not get current song
            error_message = "Error: Could not get the current song"
            subprocess.Popen(tts_command + ["-v", "Alex", error_message])
            print('{}: {}'.format(error_message, e))
            sys.exit(1)
        else:
            # detect language and get tts voice
            voice = voices.get("default")
            try:
                language = detect(current_song)
            except LangDetectException as e:
                pass
            else:
                if language in voices.keys():
                    voice = voices.get(language)
            # start SpeakCurrentSong thread
            if speakCurrentSongThread:
                # kill old thread if necessary
                if not speakCurrentSongThread.finished:
                    speakCurrentSongThread.kill()
                speakCurrentSongThread.join()
            speakCurrentSongThread = SpeakCurrentSong(current_song, voice)
            speakCurrentSongThread.start()
            # log to stdout
            print('{}.{}: {}'.format(language, voice.get("name"), current_song))


if __name__ == "__main__":
    main()