Skip to content

Instantly share code, notes, and snippets.

@marcan

marcan/turnitdown.py

Last active Mar 26, 2021
Embed
What would you like to do?
Add Spotify volume normalization to Google Cast devices
#!/usr/bin/python3
#
# turnitdown.py - normalize Spotify playback volume on Google Cast devices
#
# Dependencies: pychromecast, spotipy
#
# Usage: register a Spotify app and put the credentials in the
# SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET environment variables.
# Then just launch the script. It will autodetect all cast devices
# on the network.
import logging
import time
import pychromecast
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
VOLUME_RANGE = 34.0
MAX_VOL = 1.0
MIN_VOL = 0.001
WIGGLE = 0.01
FUDGE = 0.001
class VolumeFixer(pychromecast.controllers.media.MediaStatusListener):
def __init__(self, sp, cc):
self.sp = sp
self.cc = cc
self.last_track = None
self.last_loudness = None
self.ref_vol = None
def new_media_status(self, status: pychromecast.controllers.media.MediaStatus):
if status.content_type != "application/x-spotify.track":
return
track = status.content_id
if track == self.last_track:
return
try:
af = self.sp.audio_features([track])
loudness = af[0]["loudness"]
except:
return
if self.last_loudness is None:
self.last_loudness = loudness
cur_vol = self.cc.status.volume_level
cur_ref = self.last_loudness + VOLUME_RANGE * (cur_vol - 1)
if (self.ref_vol is None or
(cur_vol > (MIN_VOL + FUDGE) and cur_ref > (self.ref_vol + WIGGLE)) or
(cur_vol < (MAX_VOL - FUDGE) and cur_ref < (self.ref_vol - WIGGLE))):
self.ref_vol = cur_ref
new_vol = min(MAX_VOL, max(MIN_VOL, (self.ref_vol - loudness) / VOLUME_RANGE + 1))
print(f"[{self.cc.device.friendly_name}] Playing {status.title} ({track}) loudness {loudness} volume {cur_vol} -> {new_vol} (ref: {self.ref_vol})")
self.cc.set_volume(new_vol)
self.last_loudness = loudness
self.last_track = track
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
print("Locating chromecasts...")
chromecasts, browser = pychromecast.get_chromecasts()
print("Found chromecasts:")
for cc in chromecasts:
print(f" - {cc.device.friendly_name} ({cc.device.cast_type})")
cc.wait()
cc.media_controller.register_status_listener(VolumeFixer(sp, cc))
print("Waiting for events...")
while True:
time.sleep(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment