This Sonos Goes to Eleven

I'll admit this right up front: this hack is purely gratuitous. My other Sonos hacks had at least a whiff of actual utility about them, but this one is solely a “just because I can” endeavor. Also, a “I love my shiny new Sonos Sub so let's play with it” effort. None of this means that we shouldn't dive in with enthusiasm, though.
A little backstory: when I brought my new Sub home and hooked it up, my kids were just as stoked as I was. Kids love bass. The problem arose during setup: I opted for a more moderate gain setting for the sub, while the kids were lobbying for everything she's got. Seeing as how I paid for it, I won, but a seed was planted. Could I modify the gain of the sub (from normal to ludicrous) depending on the genre of the currently playing song? Yes. Yes I could.
Architecture
I'll need some way to get the currently playing song, a way to get that song's genre, and some way to modify the gain of the sub. The first part, getting the currently playing song, was easy. In my PowerMate hack, I used the fabulous sonos-discovery node package, and I'll use it again here. I can set up “listeners” to grab song change events, and find out the new song from the provided data.
The second part is pretty easy, too. I'll use the Last.fm API, and send the currently playing song title and artist name to the track.getInfo endpoint to get a list of tags (usually genres) that folks have applied to the song. We'll compare these tags to a predefined list of genres for which we want to pump up the bass.
Finally, to modify the sub's gain setting, I did some packet sniffing and some UPnP discovering, and figured out the format to control sub settings. I submitted a pull request against the sonos-discovery project (which the maintainer kindly accepted), and now sonos-discovery provides a sub object with methods to control gain, crossover, and other parameters. So we're all set.
Video
I made a video to show it in action, but as you can imagine, the true depth of the ridiculous bass this sub puts out at max gain can't be heard (or felt) through your computer speakers. Headphones help a bit, but you still miss that subwoofer kick in the gut. You can hear the bass cutting out at the beginning of the third song I skip to, and cutting back in at the beginning of the fourth. (As an aside, that's a bug. Version 0.0.2 will cache the next song's genre so we don't have to wait for the callout to Last.fm to resolve before we change the gain.)
Setup
The first step is to get a developer account at Last.fm and create a new app. You'll end up with an API key and secret. Note these, as we'll need them in the next step.
Clone the git repo, cd into the created directory, and type “npm install” to install the dependencies. Then, edit the file “sonossubto11.js” and look for the string “family room”. Replace this string with the name of your Sonos zone that has a sub that you'd like to send to 11. Look for “LASTFM_API_KEY” and replace it with your api key, and replace “LASTFM_API_SECRET” with your secret.
After that's all done, it's as simple as typing “node sonossubto11.js”, and playing some music that might require bass.
Details
The code is pretty straight forward. This section:
// Here we're going to look for messages from the various Sonos zones.....
discovery.on('sub-info', function(msg) {
if (!player || !player.sub || msg.uuid != player.sub.uuid) return;
console.log(msg);
});
simply listens for the “sub-info” emit from the sub object. This emit will contain information about any configuration changes to any subs on the network. We reject any messages not originating from our sub, and display those that are. This is purely informational, and can be taken out if desired.
This is the meat of the app:
discovery.on('transport-state', function(msg) {
if (!player || !player.sub || msg.uuid != player.uuid) return;
if (msg.state.zoneState != 'PLAYING') return;
var request = lastfm.request("track.getInfo", {
artist: msg.state.currentTrack.artist,
track: msg.state.currentTrack.title,
handlers: {
success: function(data) {
if (data.track.toptags.tag) {
for (var i=0; i < data.track.toptags.tag.length; i++) {
if (bassTags.indexOf(data.track.toptags.tag[i].name) > -1) {
player.sub.setGain(15);
return;
}
}
}
player.sub.setGain(0);
},
error: function(error) {
player.sub.setGain(0);
}
}
});
});
We're listening for a transport-state emit, and confirming that it is a) from our player, and b) is of type “PLAYING”. If not, we return without taking action. (Transport-state is Sonos/UPnP lingo for something happening to the play status of a Zone, like PLAYING, or PLAYBACK-PAUSED. If you're really interested in the guts of Sonos UPnP, this and this are great posts.)
If we are playing a song on our player, we send the title and artist to Last.fm via the lastfm node package. Upon successful return, we look at the list of tags provided, and compare that to our predefined list of tags that should result in ludicrous bass. If there's a match, we call the “setGain” method on our sub, and set the gain to the max (15). If there's no match, or an error in the callout, we set the gain to a more reasonable 0. The allowed range is -15 to 15.
Wrap Up
Again, the code is all available on github, so clone it and try it out. The concepts in here could be extended to do things like control the general EQ settings of a regular Zone player, or turn up the player's volume if a rock song came on. Those two scenarios didn't make use of my shiny new sub, so they weren't seriously considered.
In a separate post, I plan to write up specific details on getting info from and modifying parameters for a Sonos Sub.
Let me know if you get it working. I'd love to hear how it went.
Comments ()