Your Sonos can speak to you (TTS), and it’s easy…

I’ve bought a Sonos wireless speaker, and started investigating it…

Discovered a great Python package that can control Sonos. There are libraries for other languages, so the ideas here would be relevant even if you’re not a Pythonist.

This post will develop the required code gradually.

At the end, I will paste the complete code.

First, some ‘games’:

import soco
import time

ip_addr = '192.168.1.108'

## Not working for me. Strange - it used to work before...
# for zone in soco.discover(timeout=5):
#     print zone.player_name
#     print zone

my_zone = soco.SoCo(ip_addr)

## Print interesting information
print my_zone.player_name
print my_zone.status_light
print my_zone.volume
print my_zone.get_speaker_info()
print my_zone.get_current_track_info()

Wow, it actually works 🙂

Strangely, discovery doesn’t work for me, but since I know the ip_addr of my Sonos, I hard-coded it.

 

Text To Speach

‘VoiceRSS’ is a nice web service that freely translates text strings to speech, in several languages. Play with it’s demo a little bit. Register (for free), and get an API Key.

You can open in your browser something like http://api.voicerss.org/?key=9..a&hl=en-us&src=%22Hello%20world%22 (just replace the ‘9..a’ in the middle with your API key) and enjoy it…

Now, in order to send an external URL to the Sonos, there is a play_uri function. The tricky part is that the URL shouldn’t be with http! I’ve Found a post at openHAB forum that tells to use x-rincon-mp3radio instead.

(If someone can point me to some documentation that explains this, it’d be great…)

So, here is our Sonos TTS function:

def tts(message):
    key = "9..a"                # replace with your real key
    lang = "en-us"              # or whatever language
    freq = "44khz_8bit_mono"    # Sonos cannot play the default, this one is working...
    my_zone.play_uri("x-rincon-mp3radio://api.voicerss.org/?key=%s&hl=%s&f=%s&src='%s'" %
                            (key, lang, freq, message),
                            title="Computer speaking")   # You'll see this title in the Sonos controller



Amazing, ha?

Well, our job is not finished here. We still have 2 problems:

  • The text is being played over and over
  • Sonos doesn’t return to the track that was played before we told it to speak

 

Every good thing must come to end…

In order to solve the first problem, we need to calculate the duration of the message, and send a stop command to Sonos.

First, I tried using something like this code, based on mutagen package:

file_path = r"C:\Users\zvika\Downloads\a.mp3"
from mutagen.mp3 import MP3
audio = MP3(file_path)
print audio.info.length

But it needs the file to be downloaded first. I was to lazy to do that, so I resorted to this:

def calc_duration(message):
    # based on https://community.smartthings.com/t/better-an-reloaded-fix-for-tts-texttospeech/22599/22
    # but either it has some mistake - or I misunderstood something.
    # anyway, I've modified it...
    return min(2, len(message) / 3) + 6

6 is the latency before Sonos start playing.
/3   is a rough estimation of the proportion between text length in letters and text length in seconds.
2 is the minimal length of the message itself, in case that the dividing gives a too short length.
(Strangely, they used max instead of min)

And here’s the code that uses this:

duration = calc_duration(message)
print "Sending TTS."
tts(message)
print "Going to sleep for %d seconds" % duration
time.sleep(duration)
my_zone.stop()
print "Stopped TTS"

 

Complete code

Here’s the complete code, which also addresses our second problem – saving Sonos state, and returning to it:

# documentation: http://docs.python-soco.com/en/latest/api/soco.core.html
import soco
import time

ip_addr = '192.168.1.108'

## Not working for me. Strange - it used to work before...
# for zone in soco.discover(timeout=5):
#     print zone.player_name
#     print zone

my_zone = soco.SoCo(ip_addr)

## Print interesting information
# print my_zone.player_name
# print my_zone.status_light
# print my_zone.volume
# print my_zone.get_speaker_info()
print my_zone.get_current_track_info()


## Nice library that calculated MP3 length
## Considered using it for 'duration', but regretted...
# file_path = r"C:\Users\zharamax\Downloads\a.mp3"
# from mutagen.mp3 import MP3
# audio = MP3(file_path)
# print audio.info.length

def calc_duration(message):
    # based on https://community.smartthings.com/t/better-an-reloaded-fix-for-tts-texttospeech/22599/22
    # but either it has some mistake - or I misunderstood something.
    # anyway, I've modified it...
    return min(2, len(message) / 3) + 6

def tts(message):
    key = "9..a"      # REPLACE THIS!
    lang = "en-us"
    freq = "44khz_8bit_mono"
    my_zone.play_uri("x-rincon-mp3radio://api.voicerss.org/?key=%s&hl=%s&f=%s&src='%s'" %
                            (key, lang, freq, message),
                            title="Computer speaking")

def speak(message):
    # save player status, in order to return it at the end
    # afte I coded this, I've discovered 'snapshots' - http://docs.python-soco.com/en/latest/api/soco.snapshot.html
    # however, they recommend not to use it (??)
    current_transport_info =  my_zone.get_current_transport_info()[u'current_transport_state']
    current_position = my_zone.get_current_track_info()[u'position']
    current_uri = my_zone.get_current_track_info()[u'uri']

    # default
    player_source = "QUEUE"
    if my_zone.is_playing_radio:
        player_source = "RADIO"
    elif my_zone.is_playing_line_in:
        player_source = "LINE_IN"
    elif my_zone.is_playing_tv:
        player_source = "TV"

    print "Currently %s, %s" % (current_transport_info, player_source)
    duration = calc_duration(message)

    print "Sending TTS."
    tts(message)
    print "Going to sleep for %d seconds" % duration
    time.sleep(duration)
    my_zone.stop()
    print "Stopped TTS"

    if player_source == "QUEUE":
        if current_transport_info in ["PLAYING", "PAUSED_PLAYBACK"]:
            #TODO: verify that's '0' is always true... maybe we should use  u'playlist_position'+1 ?
            my_zone.play_from_queue(0)

            #TODO: go back few seconds
            my_zone.seek(current_position)

            pause = (current_transport_info == "PAUSED_PLAYBACK")
    else:
        # either RADIO, LINE_IN or TV
        my_zone.play_uri(current_uri)

        #TODO: return also nice title
        # I don't have LINE_IN and TV, hope this covers them as well...

        pause = (current_transport_info == "STOPPED")


    if pause:
        my_zone.pause()
        print "Returned to %s, paused" % player_source
    else:
        print "Playing from %s again" % player_source



speak("Good night everyone")


As you see in the TODO comments, the work is not complete yet, but we’re almost there…

 

Advertisements

Leave a comment

Filed under Specman

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