1

I am trying to keep a sox pipe input from a sound card open and execute a player commend only when there is sound in the pipe (without killing the pipe or using a file).

This could be easily achieved with sox silence 1 0.1 5% -1 0.1 5% for files but when I use it for a pipe output it doesn't work.

This is the sox record command I'm using

/bin/sox -V2 -q \
-r 48000 -b 16 -c 2 -t alsa hw:CARD=sndrpihifiberry,DEV=0 \
-t wav -r 44100 -b 16 -c 2 - \ 
silence 1 0.1 0.1% -1 2 0.5% \ 
> $streamFile &

I would like to attach and detach a player to the pipe only when there's a sound in the pipe. Something like:

while [ true ]; do 
  
        until [ WAIT FOR  SOUND ]; do
        
        TEST FOR SOUND IN THE PIPE
        
        done
        
        echo "Sound Detected starting @ $(date)" >> $log
        /usr/bin/player < $streamFile &
        PLAYERpid=$!

        until [ WAIT FOR SILENCE ]; do
  
        TEST FOR SILENCE IN THE PIPE

        done

        kill $PLAYERpid
        echo "Silence Detected killing PLAYER @ $(date)" >> $log

done

Any ideas?

2 Answers 2

3

If a dirty hack is good enough...

I defer to Marcus' response, but if a dirty hack is good enough, you could try this one.

Caveats:

  • Requires PulseAudio or PipeWire (with pulseaudio utils installed)
  • Requires ffmpeg for silence detection
  • You will lose a tiny portion of sound before your player kicks in
  • Silence detection might need some tuning, to avoid killing the player during quiet periods in the middle of audio

1. Detecting Audio

You don't actually need to do any fancy signal processing here, you can simply record a low-quality capture of your sound device and pass it straight to grep, which just checks if there is anything there...

#!/bin/bash

# Get your PulseAudio source monitor (edit regex to suit you)
pulse_monitor=$(pactl list short sources | awk '$1 = /alsa.*monitor/ {print $2}')

function wait_for_audio() {
  parec --rate 1000 -d $pulse_monitor 2>/dev/null \
  | LC_ALL=C fgrep -qm 1 .
}

while [ true ]; do
  wait_for_audio
  echo "Sound Detected starting @ $(date)" >> $log
  /usr/bin/player < $streamFile &
  PLAYERpid=$!

  ...
done

That works for me, the latency is extremely low because the fgrep exits the pipe as soon as the first ~byte of data gets through. Starting the player at that point will obviously lose some audio, but in my tests it has been acceptable. That's the easy part.

2. Detecting silence

This is a little more difficult because we don't want to exit the pipe when we detect "something", we instead want to wait until it detects "nothing". We can't use grep here. One way to detect silence is to use ffmpeg, which has a configurable silence detection filter. However, adding ffmpeg to the pipe complicates things because using parec | ffmpeg | fgrep -m1 doesn't exit when fgrep -m1 detects something. You see, whereas parec exits with sigpipe after fgrep terminates, ffmpeg does not, and bash doesn't return from the pipe until all commands finish. So we will use process substitution instead of a pipe. Also, ffmpeg is extremely noisy, and its silence detection outputs to stderr instead of stdout, so we're also going to swap ffmpeg's stderr and stdout

detect_silence() {
  # By default, exit after detecting 2 seconds of continuous silence
  SECONDS=${1:-2}
  LC_ALL=C fgrep -m 1 silence_start \
  <(parec \
    --rate 1000 \
    --raw \
    -d ${pulse_monitor} 2>/dev/null \
  | ffmpeg \
    -hide_banner \
    -f s8 \
    -ar 1k \
    -ac 2 \
    -i pipe: \
    -af silencedetect=noise=-50dB:d=${SECONDS} \
    -f null - \
    3>&1 1>&2 2>&3)
}

CPU Overhead

Audio Detection <0.3%

Audio detection overhead is very low. I'm sampling audio at 1khz rate because I don't care about quality, I just want a small trickle of raw data. I'm reducing overhead by using fgrep with LC_ALL=C (which is ~1400% faster than bare grep). On my Raspberry Pi 4, pulseaudio CPU is ~0.3% for the first few seconds that this was running. After that, it dropped back down to almost nothing.

Silence Detection ~1% CPU

Silence detection overhead is slightly higher, because ffmpeg uses ~1% CPU on my Raspberry Pi 4 when I'm running it.

Complete command with my hacks

#!/bin/bash

# Get your PulseAudio source monitor (edit regex to suit you)
pulse_monitor=$(pactl list short sources | awk '$1 = /alsa.*monitor/ {print $2}')

function wait_for_audio() {
  # sample audio at 1khz and exit as soon as data is detected
  parec --rate 1000 -d $pulse_monitor 2>/dev/null \
  | LC_ALL=C fgrep -qm 1 .
}

detect_silence() {
  # By default, exit after detecting 2 seconds of continuous silence
  SECONDS=${1:-2}
  LC_ALL=C fgrep -m 1 silence_start \
  <(parec \
    --rate 1000 \
    --raw \
    -d ${pulse_monitor} 2>/dev/null \
  | ffmpeg \
    -hide_banner \
    -f s8 \
    -ar 1k \
    -ac 2 \
    -i pipe: \
    -af silencedetect=noise=-50dB:d=${SECONDS} \
    -f null - \
    3>&1 1>&2 2>&3)
}

while [ true ]; do
  wait_for_audio
  echo "Sound Detected starting @ $(date)" >> $log
  /usr/bin/player < $streamFile &
  PLAYERpid=$!

  detect_silence 2
  echo "Silence Detected starting @ $(date -d "2 seconds ago")" >> $log
  kill $PLAYERpid
  "Silence Detected killing PLAYER @ $(date)" >> $log

done
0

Honestly, signal processing is not something you should try to implement in a shell script. It depends on regularly executing arithmetics on a bunch of binary data. Your computer will have a hard time even running this loop the several hundred times a second that would allow you to do continuous signal processing.

So, sorry, this won't work.

You can use one of many very nice frameworks to do this easily, but none of them, for the reasons described above, will give you a shell script. Doesn't matter, you don't need a shell script, you need something that works!

I'd encourage you to play around with GNU Radio. Despite its Software-Defined Radio focus, it's quite OK for designing simple audio processing toolkits. If quickly built this for you; it works for me as described below, but you might need to adjust the "Threshold" high (start) and low (stop) values.

GNU Radio-based sound detection and output

Just an example above: as soon as the input volume rises above 10% of the maximum possible value it starts emitting a tone, and stops when it falls below 5%.

When you hit the "generate" button in GNU Radio Companion (which is the tool used to design these signal processing flow graphs), you get an executable script – not a bash script, but Python, but that makes no difference to your system (that's what these #!/usr/bin/env python or #!/bin/bash lines at the beginning of files are for: telling your system how to run a script).

There's not really a limit to what you can build with this – there's sources to read audio files, there's way more signal processing, and if you need to do something that GNU Radio can't do by itself, writing a block in Python isn't that hard, actually!

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .