Real time detect silence from audio input in Linux

Real time detect silence from audio input in Linux

As a

The current issue at hand is “how to understand that the web radio is streaming, when the stream is silent?”

understand that the web radio is streaming, but the stream is silent

How can this happen? Web radio streaming is a chain of multiple parts, both analog and digital, and it can happen that some of them are silent, while others are working as expected. For example, suppose we have a computer working as our streamer; it’s sending the audio input grabbed from a sound card directly to a streamer server. Even if the internet works, there’s connection, password is known and host is ok, there is no way to understand if the audio grabbed is silent or not. This results in audio streaming that is up-and-running, but not producing audio (or perhaps some white noise can be heard). 

If the input cable is detached, if the audio source is turned off, or if the mixer fader is completely faded out instead of being at top volume, all these situations may lead to silent audio that goes through the streaming chain. The optimum solution for this is something that real-time monitors the input audio, checks if there are at least X seconds of silence and, if this happens, starts some procedures that can be:

  • start some playlist so that the audio is no more silent
  • send an e-mail and/or an alert to someone

Also, when the audio input is no more silent (a super smart radio speaker has moved the mixer fader to the correct position, for example…), this software needs to stop playing the playlist. It must run forever, it must restart if it’s stopped, and it must run at boot time. So let’s start with something that I’ve found: Python hassounddevice that do what we need to do. 

Sounddevice with Python

I’m not so used to Python, but it seems very easy to understand, easy to use, easy to modify and super powerful. I’ve started from this thread with this code snippet

# Print out realtime audio volume as ascii bars

import sounddevice as sd
import numpy as np

def print_sound(indata, outdata, frames, time, status):
    volume_norm = np.linalg.norm(indata)*10
    print ("|" * int(volume_norm))

with sd.Stream(callback=print_sound):
    sd.sleep(10000)

 that shows some bars from the audio input level, with some modifications, a chain of “if”; I’ve modified the script so that it writes to a file when X silent samples are found. Silent is defined as “a level under the threshold, th, value.
 

#!/usr/bin/env python3

import numpy as np
import sounddevice as sd
import datetime


duration = 10 #in seconds

th = 10
sec = 0
maxNumberOfSilent = 4000
isSilent = True
logfile = open("soundlevel.log", "a")

def audio_callback(indata, frames, time, status):
	global sec
	global isSilent
	global logfile
	dateLog = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S] ")
	volume_norm = np.linalg.norm(indata) * 10 
	#print("|" * int(volume_norm))
	#print(volume_norm)
	if volume_norm < th: sec += 1 else: sec = 0 if (sec > maxNumberOfSilent and not isSilent):
		 isSilent = True
		 logfile.write(dateLog+"Silent for "+str(maxNumberOfSilent)+" samples\n")
		 logfile.flush()
		 #print("Silent for "+str(maxNumberOfSilent)+" samples")
	elif (sec == 0 and isSilent):
		 isSilent = False
		 logfile.write(dateLog+"Music\n")
		 logfile.flush()
		 #print("Music")


stream = sd.InputStream(callback=audio_callback)
with stream:
	while (True):
		sd.sleep(duration * 1000)

After some more research, I’ve found a class to send e-mails using a Gmail account:

import smtplib, ssl

class Mail:

    def __init__(self):
        self.port = 465
        self.smtp_server_domain_name = "smtp.gmail.com"
        self.sender_mail = "........"
        self.password = "........"

    def send(self, emails, subject, content):
        ssl_context = ssl.create_default_context()
        service = smtplib.SMTP_SSL(self.smtp_server_domain_name, self.port, context=ssl_context)
        service.login(self.sender_mail, self.password)
        
        for email in emails:
            result = service.sendmail(self.sender_mail, email, f"Subject: {subject}\n{content}")

        service.quit()


if __name__ == '__main__':
    mails = input("Enter emails: ").split()
    subject = input("Enter subject: ")
    content = input("Enter content: ")

    mail = Mail()
    mail.send(mails, subject, content)

 
To put everything together, I’ve created a system that sends an e-mail when the sound is silent: 

#!/usr/bin/env python3

import numpy as np
import sounddevice as sd
import datetime
import smtplib, ssl

th = 10
sec = 0
maxNumberOfSilent = 10000
isSilent = True
logfile = open("soundlevel.log", "a")
to_addresses = ("myemail@mail.com",)


class Mail:

    def __init__(self):
        self.port = 465
        self.smtp_server_domain_name = "smtp.gmail.com"
        self.sender_mail = "....."
        self.password = "...."

    def send(self, emails, subject, content):
        ssl_context = ssl.create_default_context()
        service = smtplib.SMTP_SSL(self.smtp_server_domain_name, self.port, context=ssl_context)
        service.login(self.sender_mail, self.password)

        for email in emails:
            result = service.sendmail(self.sender_mail, email, f"Subject: {subject}\n{content}")

        service.quit()

mail_client = Mail()
def audio_callback(indata, frames, time, status):
   global sec
   global isSilent
   global logfile
   dateLog = datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S] ")
   volume_norm = np.linalg.norm(indata) * 10 
   #print("|" * int(volume_norm))
   #print(volume_norm)
   if volume_norm < th: sec += 1 else: sec = 0 if (sec > maxNumberOfSilent and not isSilent):
       isSilent = True
       logfile.write(dateLog+"Silent for "+str(maxNumberOfSilent)+" samples\n")
       logfile.flush()
       mail_client.send(to_addresses,"Audio is silent",dateLog+" audio is silent") 
       #print("Silent for "+str(maxNumberOfSilent)+" samples")
   elif (sec == 0 and isSilent):
       isSilent = False
       logfile.write(dateLog+"Music\n")
       logfile.flush()
       mail_client.send(to_addresses,"Audio back to normal",dateLog+" audio back to normal") 
       #print("Music")

stream = sd.InputStream(callback=audio_callback)
with stream:
   while (True):
      sd.sleep(10 * 1000)
MiroAdmin