Yamaha YAS-207 vs. Shairport Sync over TOSLINK


Introduction

As explained in the Introductory post, I was reversing the control interfaces of the Yamaha YAS-207 soundbar, in order to build a fully automated AirPlay speaker.

In the last post I hacked up a minimal Ruby-based client.

In this post I’ll extend that client somewhat, and explain how to wire the whole thing together on a Raspberry Pi with a TOSLINK output. With an explanation of the rationale behind it.

Architecture overview

Since we’re on a home stretch, let’s start with the overall architecture first:

architecture

As you can see, I’m using TOSLINK interface to connect Raspberry Pi to the soundbar, using the reasonably priced tinyToslink Optical Audio board for Raspi. It clocks at $26, which is rather reasonable.

I chose TOSLINK over USB soundcard because it’s compact, more reliable (uses the built-in I²S), and also because I don’t have another use for the “TV” (TOSLINK) input on the YAS-207. :)

raspi with toslink Here is the whole AirPlay receiver, without enclosure and PSU. And yeah, it’s on.

And I chose Raspberry Pi 4 over Raspberry Pi Zero, because I intend to run the AirPlay receiver over Ethernet, because our over-crowded Wi-Fi sometimes has issues with the mDNS (avahi, zeroconf) multicasts. By issues I mean that the multicast packets get dropped, and as a result the speaker doesn’t show up in the list of AirPlay targets on an iPhone. And that is quite maddening.

But if you don’t have issues with Wi-Fi multicast1, you could cut the price of the final solution significantly by using RPi Zero WH.

Control.rb

Since the last post I extended2 the minimal client to support sessions and a higher “intent”-based manipulation (set volume to 50%, and switch input to HDMI while you’re at it).

Thus the http interface is now more capable:

$ curl localhost:8000/start-session -d \
  'name=airplay&intent={"power":true,"input":"tv","surround":"music"}'
start session: {:power=>true, :input=>:tv, :surround=>:music, :start_session=>"airplay"}.

$ curl localhost:8000/send -d 'intent={"volume": 22}'
send intent: {:volume=>22}.

$ curl localhost:8000/stop-session -d 'name=airplay'
stop session: {:stop_session=>"airplay"}.

The idea behind sessions is that you want to save previous state (say, the soundbar was turned off, set to hdmi input, volume equal to 12, and surround “tv”) and restore it after the session ends.

Plus, the intent-based manipulation basically means that I ask for the volume to be set to 22, rather than figuring out what combination of volume_up and volume_down I need to send3.

You can get the updated script in my yamaha-yas-207 repo on GitHub (in the control subdirectory).

With that ready, it’s time to wire it up.

Shairport Sync ↔ Control.rb

Shairport Sync4 has a quite extensive configuration options in its shairport-sync.conf, but the important directives for this project are the following:

general =
{
  // keep volume at 100% (don't adjust it in software)
  ignore_volume_control = "yes";

  // instead run this to set volume (note the space at the end)
  run_this_when_volume_is_set = "/path/to/volume-control.rb ";
};

sessioncontrol =
{
  // run this before playback begins
  run_this_before_play_begins = "/path/to/turn-on.sh";

  // run this after playback ends
  run_this_after_play_ends = "/path/to/turn-off.sh";
};

The scripts mentioned in the config are almost too easy:

#!/bin/sh

# turn-on.sh
exec curl localhost:8000/start-session \
  -d 'name=airplay&intent={"power":true,"input":"tv","surround":"music"}'
#!/bin/sh

# turn-off.sh
exec curl localhost:8000/stop-session -d 'name=airplay'
#!/usr/bin/env ruby

# volume-control.rb

require 'json'

def send_intent(intent)
  exec("curl", "localhost:8000/send", "-d", "intent=#{intent.to_json}")
end

# expected input: -144.0 for mute, -30.0..0.0 for volume level
# viz: https://nto.github.io/AirPlay.html#audio-volumecontrol

volume = Float(ARGV.first || -15.0)
if volume < -100.0 # -144.0 to be exact, but what the hell
  send_intent({mute: true})
else
  volume = -30.0 if volume < -30.0
  volume = 0.0 if volume > 0.0
  # (-30.0..0) -> (0..0x32)
  #volume = (((volume + 30.0) / 30.0) * 50).round
  volume = (5/3.0*volume + 50).round
  send_intent({mute: false, volume: volume})
end

Closing words

There you have it. An AirPlay speaker that automatically controls our Yamaha YAS-207 soundbar.

It has been long journey since that introductory post on 2021-04-11, but we arrived in decent shape, with just a few failboats sunken along the path.

I still have a bit of work ahead of me (proper enclosure5, semi-automated install of the Alpine Linux distro with all the components). If I ever turn (some of) that into a post, it will definitely be under the yas 207 tag.

  1. Living in a more sparsely populated area (or a faraday cage) certainly helps.

  2. See 99e645f, 326c828, and 0688ac8. Although the diff for 0688ac8 fails to clearly show what’s really going on.

  3. I’m rather fond of the “tell me what you want, not how to do it” model.

  4. The Shairport Sync, for those who don’t know, is an AirPlay receiver (audio player).

  5. Likely just drilling a hole in the official RPi enclosure with a Dremel. But it’s Sunday today, so I will hold that off to avoid antagonizing our neighbors.