Smooth media player volume fade with multiple curve functions

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 softly mute all the media players in your house over 3 seconds; instead of the instant, deafening silence when you hit pause.
  • Lullaby Fadeout

    A lullaby 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

  1. 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.
  2. Paste it at the very bottom of your script config file at /config/scripts.yaml.
  3. Reload scripts under Configuration > Server Controls > YAML configuration reloading.
  4. Create a new automaton with the GUI editor.
  5. Under the Actions section of the automation, select:
    • Call Service for the action type
    • and, script.fade_volume for the service.
  6. Fill out the script with a target media player, volume and duration, then hit save.
  7. 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

  1. Set bedroom Sonos volume to 0.
  - service: media_player.volume_set
    data:
      volume_level: 0
    target:
      entity_id: media_player.bedroom_sonos
  1. 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]

  1. 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

  1. 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 :slight_smile:

32 Likes

Looks neat.

the only thing I would suggest is to show an example of using it in an automation.

likely there are lots of people who won’t have any idea how to pass data to the script.

Cheers @finity - good call.

1 Like

Great work!

I’m a newbie to HA and your script and guide covered everything I needed to know to get this working for me, thank you so much!

One question though, is there a way to stop a fade once it has started?

1 Like

I can’t thank you enough for this. It’s absolutely brilliantl and should be integrated officially into HA. Nice one.

3 Likes

It’s great to hear that the guide got you all the way through, so thanks for sharing the feedback. Yes, it would absolutely be possible to stop the fade - and actually that could be a great addition feature I might add soon.

In the meantime you can add an additional hard-coded condition to the while section in the script sequence to the script on your own HA system.

NOTE: This will add the condition to any and all automations that use this script. To integrate this feature into the script as a user-defined variable, you’d need to add another field into the script include it in the while statement. But let’s just hard-code it now as an example.

Say you had a switch to turn on light.bedroom_light next to your bed and want the morning alarm music to stop fading in when you on the light…

  sequence:
    - repeat:
        # Only continue if the following conditions are true:
        while:

            # [EXAMPLE LIGHT CONDITION]
            # Bedroom light is off
          - condition: template
            value_template: "{{is_state("light.bedroom_light", "off")}}"
            # [END EXAMPLE LIGHT CONDITION]

            # Pre-calculated total step index has not been reached.
          - condition: template
            value_template: "{{ repeat.index < total_steps }}"
            # Media player's current volume is not close to the target, otherwise we're just wasting processing time.
          - condition: template
            value_template: "{{ ((state_attr(target_player, 'volume_level') - target_volume) | abs) > 0.001 }}"

But you can replace light.bedroom_light with any other entity. For instance, you could create an input boolean called input_boolean.i_am_awake that gets triggered by any number of conditions could be used to determine that you’re awake, e.g. phone activity, your phone’s tracked “away” status, etc, etc.

In that case, you’d change the condition value template to {{is_state("input_boolean.i_am_awake", "false")}}.

If you haven’t already discovered the Developer Tools (the hammer icon just above the Configuration button in your sidebar), they can really help to double-check the validity of your script code. Specifically:

The States tab provides a super convenient way to view the current state and attributes of any entity in your network.

And the Template tab is a great scratch-pad to copy bits of scripts and mess with the code. It evaluates each line of code immediately and displays its output on the right hand side panel.

Enjoy Home Assistant!

1 Like

Thanks so much for the feedback, that’s an awesome complement!

Hello,

can it be that this does not work with Amazon Echos?
For me nothing happens when I apply the script to my Alexas.

service: script.fade_volume
data:
  curve: linear
  target_volume: 0
  duration: 30
  target_player: media_player.echo_buro

Hi @Loki81 . Apologies, I started replying the other week but closed the tab and forgot about it.

I suppose it depends on how it’s been integrated within Home Assistant. Did you end up solving the problem? If you had another media player device of a different model or manufacturer we could work out if there’s a potential quirk specific to the Echo.

Hi @MaxVRAM

I have now tested the script with my Philips TV. There it works in general. Only the runtime is not correct. I have set 30 seconds and the script has run about 80 seconds.

service: script.fade_volume
data:
  curve: linear
  target_volume: 0
  duration: 30
  target_player: media_player.55pus6262_12

On my Echo’s it does not work at all.
The volume does not change. With the MediaPlayer Card I can adjust the volume without any problems.

grafik

@MaxVRAM Thank you ever so much for sharing this! I have also been wanting a smoother fade of music for different scenes. Awesome work - and as a complete bonus I learned new things around how to do scripts!

1 Like

That’s really interesting @Loki81. I need to revisit this code and have a look into why the Echo isn’t responding to the automation. If anyone else has Echo devices and has the time to test this script out on their media players it would be most helpful, since I don’t have any Echos.

As for the run-time variance, the variation between your configured time and actual use-case time is huge, far bigger than the difference in my own experience. A quick question, what hardware are you running your HA instance on? I’m curious if CPU power has any impact on this drift.

I just want to thank you for providing feedback and responding to my questions, it’s super helpful when developing tools that run on a large variety of hardware platforms.

That’s awesome. Glad to hear it @herbert … Feel free to explain how you’re using it, because it’s always really exciting to hear the range of ways people use my projects.

Is there a reason why duration is limited to 60 seconds? I’d like to set it to 5 minutes.

1 Like

Hi @MaxVRAM
my HA runs on a Protectli Vault Hardware. CPU Usage is at 5%, so i don’t think that this is the problem.
For runtime I also tried a few seconds, so this shouldn’t be the problem, too.

I have no idea actually! I’ve just updated the script to allow any value in the transition input box. Thanks for the suggestion :slight_smile:

hi @MaxVRAM,
thanks for this awesome idea.
I tried it with two different media players (Philips TV and Sonos Speaker), but the volume immediately went up/down as I started my automation. Like if there was no duration, but I tried it with 60s and even more.

service: script.fade_volume
data:
  curve: logarithmic
  target_player: media_player.49pus7101_12
  duration: 60
  target_volume: 0.8

Any idea or someone, who has the same problems?
thanks in advance

2 Likes

Great spot!

I recently changed one of the lines to use abs to abs(0), in an attempt to resolve an issue with null target volume values producing an error. But it turns out abs(0) doesn’t work the same way float(0) works, so I’ve reverted. I’ll find another solution and update it accordingly.

Thanks for the report. If you update your script using the recent code it should work as expected. If not, please let me know.

Cheers,
Chris.

1 Like

Now it works, but I have the same problem with my Sonos as Loki81 before.
The duration is much longer than set.
I have set it to 5 seconds, but it took almost 60 seconds from 0.00 to 0.06 volume level. And then this error occurred:

Error calling SonosMediaPlayerEntity.set_volume_level on media_player.sonos: HTTPConnectionPool(host='192.168.1.105', port=1400): Max retries exceeded with url: /MediaRenderer/RenderingControl/Control (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7f571340a0>, 'Connection to 192.168.1.105 timed out. (connect timeout=9.5)'))

So I think it’s not a problem with HA or the script. It seems the media player itself is the bottleneck.
I have testet it with my Philips TV. It took 12 seconds instead of 5 seconds.

Of course!
We like our music a bit louder while we cook. When we then sit down to the table and activate Dinner mode all the lights in the house dim down to a nice level and now the music also has a nice fade down in volume. This is way nicer than the abrupt change of volume we used to have.