Posts tagged with “audio”

Bash script: Reminder to swap mics with automatic pausing of all media players

The problem

I recently purchased a double pack of wireless microphones (specifically, these ones) to replace my ageing and faltering wired one. I am very happy with the audio quality, their ease of use and their range, but for my specific use case, the battery life (around 3 hours) leaves a little to be desired. I mainly use these while hanging out with a friend over the internet while sharing my screen, and we'll watch movies and TV shows together. That can last a couple of hours or even most of the day, and at some point the battery for the microphone will run out.

The first time this happened, it took me a little while to understand what was going on, as there was no beep or any indication from the microphone to signal its demise. Apparently the LED on the device will blink when it's low on battery, but that's impossible to see when it's clipped to my shirt right underneath my chin.

But, through the magic of buying two of them, the solution is easy — swap the depleted mic with a freshly charged one, then recharge the depleted mic while you discharge the fresh one. Still, the problem of not knowing when to do that persists.

The code

That's right, we're writing more Bash. Normally, you could just set a timer for your phone or your computer, but we're watching content that has sound whilst wearing headphones. What I wanted was a solution that could pause all playing media and tell me to swap my mic out, so that's exactly what I wrote:

mic-change.sh:

#!/usr/bin/env bash
matches=$(playerctl -l)
IFS=$'\n'
read -r -d '' -a matchedPlayers <<< "$matches"
numPlayers=${#matchedPlayers[@]}
((numPlayers--)) # To use zero based indexing

for i in $(seq 0 "$numPlayers"); do
    currentPlayer=${matchedPlayers["$i"]}
    status=$(playerctl -p "$currentPlayer" status)
   
    if [[ "$status" == "Playing" ]]; then
        playerctl -p "$currentPlayer" play-pause
        
        if [[ "$currentPlayer" == "kodi" ]]; then
            sleep 1
        fi
    fi
done

mplayer "/opt/sfx/mic-change.mp3"

How to use

Run the script when you turn on your microphone, and re-run it whenever you swap your mic. Examples:

  • Run a sleep then the script from the terminal:
    sleep 9000; /opt/scripts/mic-change.sh
  • Put the sleep at the start of the script (after the shebang) and run it
  • Use a timer app that can launch scripts when the timer finishes, like KClock

(9000 seconds is 2h 30m)

Script explanation

This script uses playerctl to pause all currently playing media players via the MPRIS D-Bus specification. Most players for Linux support this natively. Kodi, my media player of choice, does not, but support can easily be added through an addon. The script goes through every currently registered media player, and checks if it is currently playing. If it is, it pauses it. If it is Kodi, it waits a second before doing anything else, since Kodi has a ~1 sec delay when pausing before audio stops playing (and also an annoying corresponding ~1 sec delay before audio starts playing again once you unpause). Finally, it plays the sound defined in the last line of the script using mplayer, which in my case is a TTS voice named Onyx telling me to "Change your mic, motherfucker."

If you'd like to get the same microphones, which are very good despite the relatively short battery life, you can get them on AliExpress here.

The product links in this article are affiliate links. If you buy something using them, I may earn a small commission at no extra cost to you.


How to make a partially defective speaker stay powered on (or: The Five Stages of Annoyance)

Acquisition

I bought a new TV a year and a half ago. Much to my dismay, I discovered that while the picture quality is great, the audio quality is not, unlike my previous TV. My daily driver Linux PC is connected to my TV, and is my main entertainment hub for TV shows, movies and video games, and also general PC usage, so I figured I had to get an external speaker or sound bar. My dad had a spare speaker which he had replaced with a newer unit, so I inherited the spare - a "Geneva Sound System Model S DAB".

It came with a small caveat: the speaker will not turn on using the remote control. Mind you, it will turn off just fine with the same exact button, and every other button on the remote works as advertised - it just will not turn on using the remote. I figured that this was no big deal, and accepted the speaker with thanks. I get the speaker situated and hooked up, and the audio quality is great - a big upgrade from the built-in TV audio. Great for music, great for movies, great for games, just all-round great...

Perturbation

...until I read some news, or do anything that doesn't produce any sound for a while. This is when the European Commission decides to take a retroactive shit in my cereal. Since 2009, audiovisual equipment such as speakers are required to switch to a low-power mode after a reasonable amount of time. In and of itself, that is a good requirement, and will save power when such devices are not in use. Sometimes it's a bad requirement, and that is when:

     a) the automatic standby feature can't be disabled, and
     b) the unit can't be powered on with the remote control

Bargaining

This particular set of circumstances means that whenever I'm using the computer, and switch between tasks that produce sound, and those that don't, I risk the speaker powering down, and having to get up, walk over to the speaker underneath the TV, and power it back up manually. That wouldn't be so bad if it was a once-a-day thing, but the auto-standby timeout for the speaker is fixed at 30 minutes, so you might see how this gets real old, real fast. The speaker itself works fine, and it's a shame to generate e-waste from something that's only partially defective, so I decide to stick with it. I wouldn't blame someone for chucking it and replacing it with a new unit though, but I am particularly stubborn.

Upon discovering this, the first thing I do is start searching the web for the manual for the device, to see if I can address point a) above: turning off the auto-standby function, but I come up short. There doesn't seem to be a way to disable this functionality at all. Since I wouldn't know the first thing about fixing the hardware side of this (fixing the remote not being able to turn the unit on any more), I default to trying to work around it through software.

My first attempt to trick the speaker into staying awake is playing a sound on an interval. A sound which is in frequency range of the speaker, but out of range of my ears. According to specs found online, the frequency range for the unit is 75 Hz to 20 kHz, so I try both ends of the spectrum. Using Audacity, I generate 5 second tones ranging from 76 Hz to 19 kHz, and make a script that plays the tone every 10 minutes, and slowly over the course of a few days, work my way through all the generated tones to see if any will keep the speaker awake.

It doesn't work. None of the tones do. They all play (confirmed by dropping in an audible .wav in place of the generated tones), but it seems as though the speaker is ignoring them. The 17 kHz tone had the added bonus of viscerally startling my friend who could hear the sound (I couldn't), asking what the hell that was.

Most likely the internal logic that checks for sound, checks for loudness/dB values, or some other obscure black magic fuckery I don't understand (spoilers: I am not an audio technician, and possess only cursory knowledge of audio specifics).

My second attempt is a much less sophisticated one: play a short, normal, audible sound every 20 minutes or so. I picked out a ~1 second audio file from my OS, and made it play every 20 minutes. This also didn't work. My conclusion is now that the aforementioned internal logic not only checks for loudness/amplitude, but also has a polling rate greater than a second (or at least, greater than the length of the audio file in question). I don't want to play a longer file either since it's on a fixed interval, it will play over any other media I am consuming, and become another point of annoyance.

Acceptance

At this point, I more or less resign to my fate, and continue using the speaker without any software magic to try to keep it alive. I do the whole song and dance of getting up and turning the speaker back on when I need it, several times a day. My record is 7 times in one long day of intensive programming interspersed by YouTube video breaks.

Redemption

A year and a half passes, to the present day. I am working on some other project, when I notice the speaker turning off again, and spontaneously get an idea. What if I continuously monitor my audio output, and only play an audible sound when it's been quiet for a while? I haven't done anything like that in the past, but after a ton of web searching, and trial and error, I come up with a solution. This relies on using PulseAudio (or PipeWire with its backward compatibility with PA).

That's right, there's finally going to be some code in this article!

anti-energy-saving.sh:

#!/usr/bin/env bash
sinkToMonitor="alsa_output.pci-0000_06_00.1.hdmi-stereo" # find with `pactl list short sinks`
soundToPlay="/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"
tmpOutputSample="/tmp/output-sample.flac" # saves the sample in ram and overwrites it every loop
timeoutSecs=1500 # timeout in seconds - 1500s = 25m

trap quit SIGINT
quit() {
    printf "\nSIGINT received, cleaning up and exiting\n"
    rm "$tmpOutputSample"
    exit
}

secsSinceLastSound=0
while true; do
    currDefaultSink=$(pacmd list-sinks | grep "\* index" -A1 | awk '/name:/ {print $2}')

    if [[ "$currDefaultSink" = "<$sinkToMonitor>" ]]; then
        timeout 5 parecord --channels=1 --file-format=flac --device "$sinkToMonitor.monitor" "$tmpOutputSample"
        meanNorm=$(sox "$tmpOutputSample" -n stat 2>&1 | awk '/Mean +norm:/ {print $3}')
        if (( $(echo "$meanNorm > 0.000700" | bc -l) )); then
            secsSinceLastSound=0
            echo "Sound threshold reached, timer reset..."
        else
            secsSinceLastSound=$((secsSinceLastSound+5))
            echo "Sound threshold not reached, timer is at $secsSinceLastSound/$timeoutSecs seconds..."
        fi

        if [[ "$secsSinceLastSound" -ge $timeoutSecs ]]; then
            echo "Timeout reached, playing sound and resetting timer..."
            paplay "$soundToPlay"
            paplay "$soundToPlay"
            secsSinceLastSound=0
        fi
    else
        echo "Active sink changed, sleeping..."
        secsSinceLastSound=0
        sleep 10
    fi
done

To use this on your system, you'll need to change the values of the variables sinkToMonitor, soundToPlay and timeoutSecs to fit your configuration.

  • sinkToMonitor needs to hold the name of your default audio sink, which you can find with pactl list short sinks
  • soundToPlay is a path to the sound you wish to play when the inactivity timer has been reached
  • timeoutSecs is the number of seconds of inactivity to wait before playing the keep-alive-sound

Make the script executable with chmod +x anti-energy-saving.sh.

You'll also notice that I call paplay twice to play the same sound twice. If you use a longer audio file, you won't have to do this, I just preferred this short and inoffensive sound to play whenever the speaker is nearing its auto-standby timeout.

If you don't speak Bash, this is a quick run-down of how the script functions:

  • Checks whether you're currently using your default audio sink, if not, sleeps for 10 seconds. I added this because I sometimes use a Bluetooth headset, and don't want the script running on that - only the HDMI output.
  • If you're using your default audio sink, the script will record 5 seconds' worth of audio that comes out of your speaker to RAM (/tmp)
  • The audio file is run through sox -n stat to get the average volume of the samples in the clip - if it exceeds a set threshold, the script will reset the inactivity timer. The reason this comparison is > 0.0007 instead of just any value > 0 is because of the aforementioned speaker polling rate; we need to make sure enough sound has been played lately that the speaker won't ignore it.
  • If the audio clip didn't exceed the threshold, 5 seconds is added to the inactivity timer.
  • If the inactivity timer is greater than or equal to the timeout value, a sound is played twice to keep the speaker alive, and the inactivity timer is reset
  • Repeats ad infinitum

You can remove all echoes if you'd like, they are just a remnant of me debugging this in an interactive terminal. The script is designed to run non-interactively in the background without any user input.

Conclusion

The script works great for me and my situation, and I haven't had a single incident of speaker snooze since implementation. I've set it to run on start-up, and it uses very little CPU and RAM. It also won't interrupt any other audio I am playing, since there's a little more thought to it beyond a simple sleep 900.