I struggled to find any comprehensive solutions on the Home Assistant forums for fading media player volumes with common attenuation curves. I really don’t like abrupt changes to audio volume, so I put this script together.
Media Player Volume Fade
This script fades the volume of a target_player
media player, starting at it’s current volume_level
, to a user-defined target_volume
over the user-defined duration
in seconds. It also applies one of three curve
algorithms to shape the transition of the fade. These algorithms result in a far more natural fade than simply increasing/decreasing volume in linear steps.
For those interested in the code; the script is fully commented, and I’ve put together a quick breakdown of how it works at the bottom of the post.
Use-case Ideas
-
Gentle Alarm Clock
An alarm sound gently fading in over a long period of time. -
“Soft”-mute
A button on your dashboard or Android lock-screen to softlymute
all the media players in your house over 3 seconds; instead of the instant, deafening silence when you hit pause. -
Lullaby Fadeout
Alullaby
automation to very slowly fade out sounds that help you sleep (white-noise/rainforest) once a certain amount of time has passed since it was started before playback stops.
How to use it
- Copy the script to your clipboard from either:
-
fade-volume.yaml
(direct link) on my GitHub repo. - or, from the code snippet at the bottom of this post.
-
- Paste it at the very bottom of your script config file at
/config/scripts.yaml
. - Reload
scripts
underConfiguration > Server Controls > YAML configuration reloading
. - Create a new automaton with the GUI editor.
- Under the
Actions
section of the automation, select:- Call Service for the action type
- and, script.fade_volume for the service.
- Fill out the script with a target media player, volume and duration, then hit save.
- Scroll to the top of the automation GUI and hit run actions on the right-hand side
If you have something playing on your media player, you should hear the volume changed based on the settings you entered in the automation.
Below is a more real-world
example use-case of the script in action.
Automation Example - Morning Alarm
This is an example of the script added to an automation in the GUI editor. Here it’s used to gently fade in an audio file as an alarm in the morning.
Here’s a breakdown of what each automation step looks like as the exported yaml
code in automations.yaml
:
Trigger
- Time = 7am
- platform: time
at: 07:00:00
Actions
- Set bedroom Sonos volume to 0.
- service: media_player.volume_set
data:
volume_level: 0
target:
entity_id: media_player.bedroom_sonos
- Play
soft_alarm_music.wav
on the media player.
- service: media_player.play_media
data:
media_content_id: http://192.168.20.5:8123/local/soft_alarm_music.wav
media_content_type: music
target:
entity_id: media_player.bedroom_sonos
[Fade Volume Script]
- This is where the fade script is called, changing the volume gently from 0% to 60% over a minute.
- service: script.fade_volume
data:
curve: logarithmic
target_player: media_player.bedroom_sonos
duration: 60
target_volume: 0.6
- And if that doesn’t wake me up, once the volume fade script is done, I finish off the automation with a
light.turn_on
action to increase to full over 2 minutes.
- service: light.turn_on
target:
entity_id: light.bedroom_lamp
data:
transition: 120
brightness: 255
mode: single
All together
The final automation code that’s exported into automations.yaml
would look like this:
- id: '1635153241041'
alias: Morning Alarm
description: ''
trigger:
- platform: time
at: 07:00:00
condition: []
action:
- service: media_player.volume_set
data:
volume_level: 0
target:
entity_id: media_player.bedroom_sonos
- service: media_player.play_media
data:
media_content_id: http://192.168.20.5:8123/local/soft_alarm_music.wav
media_content_type: music
target:
entity_id: media_player.bedroom_sonos
- service: script.fade_volume
data:
curve: logarithmic
target_player: media_player.bedroom_sonos
duration: 60
target_volume: 0.6
- service: light.turn_on
target:
entity_id: light.bedroom_lamp
data:
transition: 120
brightness: 255
mode: single
Details
Input Parameters
Name | Type | Range | Required | Default | Description |
---|---|---|---|---|---|
target_player | media_player | entity | yes | - | Media player entity to perform the fade on. |
target_volume | float | 0.0 - 1.0 | yes | 0.5 | Normalised volume_level to fadeto by the end of the duration. |
duration | float | 0.1 - 60 | yes | 5.0 | Length of time in seconds the fade should take. |
curve | selector | logarithmic, bezier, linear | yes | logarithmic | Shaping algorithm applied to the volume change over the fade duration. |
Curve Algorithms:
f(x)
is the instantaneous amplitude added to the original for each step, where x
is the normalised time value based on the duration
, starting at 0
and ending at 1
.
- (red) Linear:
f(x) = x
- (blue) Bezier:
f(x) = x / (1 + (1 - x ))
- (green) Logarithmic:
f(x) = x * x * (3 - 2x)
Script code
Add this to your Home Assistant config scripts.yaml
, and use it anywhere that allows a service call (such as automations):
fade_volume:
alias: Fade the volume of a media player
mode: restart
fields:
target_player:
name: Target media player
description: Target media player of volume fade.
required: true
example: media_player.lounge_sonos
selector:
entity:
domain: media_player
target_volume:
name: Target volume
description: Volume the media play will be at the end of the fade duration.
required: true
default: 0.5
example: '0.5'
selector:
number:
max: 1
min: 0
step: 0.01
mode: slider
duration:
name: Fade duration
description: Length of time in seconds the fade should take.
required: true
default: 5
example: '5'
selector:
number:
mode: box
min: 0
max: 100000
unit_of_measurement: s
curve:
name: Fade curve algorithm
description: Shape of the fade curve to apply.
required: true
default: logarithmic
example: logarithmic
selector:
select:
options:
- logarithmic
- bezier
- linear
variables:
steps_per_second: 10
total_steps: '{{ (steps_per_second * duration) | int(0) }}'
start_volume: '{{ state_attr(target_player, ''volume_level'') | float(0) }}'
start_diff: '{{ (target_volume - start_volume) | float(0) }}'
sequence:
- repeat:
while:
- condition: template
value_template: '{{ repeat.index < total_steps }}'
- condition: template
value_template: '{{ ((state_attr(target_player, "volume_level") - target_volume)
| abs) > 0.001 }}'
sequence:
- service: media_player.volume_set
data_template:
entity_id: '{{ target_player }}'
volume_level: "{% set t = repeat.index / total_steps %} {% if curve == 'logarithmic'\
\ %}\n {{ (start_volume + (t / (1 + (1 - t))) * start_diff) | float(0)\
\ }}\n{% elif curve == 'bezier' %}\n {{ (start_volume + (t * t * (3 -\
\ 2 * t)) * start_diff) | float(0) }}\n{% else %}\n {{ (start_volume\
\ + t * start_diff) | float(0) }}\n{% endif %}\n"
- delay: '00:00:00.1'
- service: media_player.volume_set
data_template:
entity_id: '{{ target_player }}'
volume_level: '{{ target_volume }}'
icon: mdi:tune-vertical
For the Script Junkies
Code Explanation
The script works first calculating the number of total_steps
required to fade based on the user-defined duration
multiplied by a hard-coded step_duration
of 100ms
(or 10 per second). For example, a duration of 5 seconds equates to 500 steps.
It determines the difference between the media player’s current volume and the user-defined target_volume
. It applies this value as a factor to the shaped fade amount, adds it to the original volume, and applies it to the media player entity volume_level
for each step in a while
loop.
Limitation
Timing Problem
From what I could gather, Home Assistant calls its services on a 1 second clock. I don’t know the details, however it’s clear that sub-second delay calls aren’t timed perfectly. So don’t expect the fade duration to be perfect. The larger the duration, the more noticeable the duration discrepancy will be.
Potential Workaround
To make this script duration-accurate, instead of defining total_steps
at the start of the script, a steps_left
value could be used, defined by the script’s start_time
, end_time
(which would be fixed), and the current_time
for each iteration of the loop. The repeat condition would end at the pre-defined end-time, with the fade amount increasing or decreasing each step depending on if the calls are lagging or ahead… but I’ve already spent way too much time on this, so be my guest