Deeper dive into the AmpliPi API

Deeper dive into the AmpliPi API

November 15, 2024

AmpliPro hosts an open, well documented API to control what streams are playing to what zones. This is the second post in a series of blog posts introducing AmpliPro’s API; if you haven’t read our first article, you probably should! In this post, we will go deeper into some of the AmpliPro concepts, and at the end of this article play a track from the internet across all zones using a bash script as an alarm for an intruder alert 🦹 (Triggering this script is left as an exercise for the reader.) We hope that by the end of this blog post series, you will have the knowledge to begin implementing your own custom integrations for AmpliPro!

Concepts

To really understand AmpliPro’s API, it’s a bit important to understand how the AmpliPro and AmpliPro Streamer function under the hood. These primitives are what one will be interacting with when using the API; they map pretty cleanly to the hardware representation. Without explaining these primitives, this introduction may otherwise feel foreign to you. Let’s go!

Streams

Streams are configurations for services that play audio, which is probably what you think of when you hear “stream”. There are two types of streams:

  • a BaseStream plays directly to a hardware audio device. This is used as either the parent or grandparent class of every stream type.
  • a PersistentStream plays to a “fake” alsaloop device instead of a hardware audio device. This permits us to keep instances of this type of stream running in the background, giving the effect of “loading instantly”. This is especially important for some stream types like LMS, which in certain circumstances can take minutes to load completely. PersistentStream is a subclass of BaseStream, and a parent class of many other types of streams.

You do not need to know much about whether a stream is persistent or not to use it; however, that distinction might help explain particular behaviours the more you play with the API.

Sources

A source is effectively a hardware audio device that streams can play to. In a regular AmpliPro, sources are the intermediary between streams, and all muxing that happens downstream to connect to zones. In an AmpliPro Streamer, every source outputs line-level directly to one of four RCA connectors on the back of the appliance, which the frontend just calls “outputs”; there is no muxing or powered output.

Zones

In an AmpliPro, zones are the actual sound outputs. There are six in an AmpliPro, and come in both powered and unpowered varieties. Many zones can be connected to a single source at once. Zones can be logically named, but always retain their zone ID. Since there is no muxing in a Streamer, there are no zones.

Diagram representing the relationship between streams, sources and zones

Groups

Groups are logical groupings of zones, for example grouping zones 3 & 4 together and naming them “Kitchen”. Since there are no zones in a Streamer, there are no groups.

bash & curl

You’ve seen the documentation and manipulated the AmpliPro to produce sound. That’s great! But if you wanted to go to a web interface with this system every time, we already have a shiny web app that wraps a lot of the functionality for you. We want to script with the AmpliPro!

Remember how we ran a request and one of tabs that came up had a curl command in it? This makes it very easy for us to craft our requests and simply paste them into shell scripts. We’ll be using some fancy templating on top of curl, but this will definitely come in handy during your experimentation.

Here is an example script to temporarily play an ooky-spooky noise track, Subterranean Life by Wolf Eyes from my hometown, Ann Arbor, Michigan. This track was released for free download via Adult Swim’s William Street label. The /play endpoint we use will start a temporary stream, and when it completes it will be removed from the configuration. Think of this as the script you run when an intruder is detected in your home automation.

I will use bash for this example; we’ll explore our Python library pyamplipi in a later post in this series. Besides bash, we will use curl and the super handy jq to parse JSON for this example. Please read these utilities documentation or manpages if those lines make your eyes glaze over.

Have a brief look at the documentation for Play endpoint. The stream created is a fileplayer type, but it’s temporary and will be removed from the configuration when playing has ceased. We will have to find an unused source to play on, but otherwise this seems pretty simple. Let’s go 😎

#!/bin/bash
# A simple script to play a track on an empty AmpliPro source connected to all zones.
TRACK="https://media.cdn.adultswim.com/music/noise/16%20Subterranean%20Life.mp3"

# First, we'll find an empty source:
empty_sources=$(curl http://amplipi.local/api/sources 2>/dev/null | jq -r ".sources[] | select(.input == \"\" or .input == \"None\") | .id")
if [ -z "${empty_sources}" ] ; then echo "All sources are used! Exiting." ; exit 1; fi
first_available_source=$(echo $empty_sources | cut -f 1 -d' ')

# Hook up all zones to this source. JSON quoting in bash is a hassle if you need to template anything;
# we assemble our data first using a sed substution instead of direct variable expansion.
zone_patch_data=$(echo '{
  "zones": [0, 1, 2, 3, 4, 5],
  "update": {"source_id": SOURCE_ID}
}' | sed s/SOURCE_ID/${first_available_source}/ )

curl -X PATCH \
  --json "${zone_patch_data}" \
  http://amplipi.local/api/zones 2&>1 >/dev/null

# Finally, play our media!
play_data=$(echo '{
  "media": "TRACK",
  "source_id": SOURCE_ID,
  "vol": -30
}' | sed s/SOURCE_ID/${first_available_source}/ | sed s^TRACK^${TRACK}^)

curl -X POST \
  --json "${play_data}" \
  http://amplipi.local/api/play 2&>1 >/dev/null

echo "playing on source ${first_available_source}"

Paste the above into a file, save it, make it executable, and run it. You should hear the spooky track!

It’s still going, isn’t it? How long is this track anyways? Okay, 18 minutes is a lot for the sake of our example. Let’s stop this stream. We can disconnect and stop this stream by PATCHing the source’s input to nothing (set or substitute $SOURCE_ID):

curl -X PATCH --json '{"input": ""}' http://amplipi.local/api/sources/$SOURCE_ID

Cool stuff, huh? 😎

Where to from here?

I hope with this walkthrough I’ve demonstrated that the AmpliPro’s API is easy to use, well documented, and flexible enough to script your own automations. We intend to write a couple more posts in this series (using something other than bash! 😉) Additionally, we’ll be discussing our spruced up Home Assistant integration, in case you want to use that system to run deeply integrated automations.