Detecting sound via a webcam mic in Linux
Tags: linux, python, signalprocessing
Published: 2022-05-12 11:35:55

---
title: Detecting sound via a webcam mic in Linux
published: true
description: A HowTo on detecting sound with python on Linux. It uses arecord and root mean squares.
tags: linux, python, signalprocessing
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnxvtxksg0bkp061oxyv.png
---

I want to make it so that if a sound happens in my front room, that triggers something else to happen.

So my first step is detecting a sound.

How can I do that? Well, I have a webcam with a mic, and a raspberry pi. Maybe I can combine them in some way to do that. I'm a programmer, right? so it can't be *that* hard...

## Step 1. Get sound from a mic.

There are plenty of ways to do this, for example [PyAudio](https://pypi.org/project/PyAudio/), but I know all I really want is a stream of bytes. [arecord](https://alsa.opensrc.org/Arecord) exists on my raspberry pi by default, so let's see if I can get a stream to pipe out from there.

The first conundrum is: what is my device name? Luckily arecord has an option to tell me.

``` bash
arecord --list-pcms
```
Well, this is a long list, but I see something called "sysdefault" which is a promising name:

```
sysdefault:CARD=C920
    HD Pro Webcam C920, USB Audio
    Default Audio Device
```

OK, so, what else? Well, let's test this by only recording for a second, and recording to a file.

``` bash
arecord --device="sysdefault:CARD=C920" --duration=1 file.wav
```

Looks good!

> Recording WAVE 'file.wav' : Unsigned 8 bit, rate 8000 Hz, Mono

And I have a one second long wave file I can listen to, if I want.

So, if I want to pipe this into something, I just need to change `file.wav` to `-`, which is Linux shorthand for stdout. (If you ran the command substituting `file.wav` for `-` then the stream would be written to your terminal, which would be quite ugly.)

## Step 2. What is a sound?

[Signal processing](https://en.wikipedia.org/wiki/Signal_processing) is a rich field of study that has produced many useful tools for working on analogue and digital data to determine things about it. It's where we get data compression, for example, as well as those nifty visualisations on music players.

There are plenty of methods for determining if something is a signal or just noise, for example I could take a [Fast Fourier Transform](https://en.wikipedia.org/wiki/Fast_Fourier_transform) of the data and see if the peaks in frequency are what I want to hear.

Except that seems like a lot of work. All I want to know is if any sound has happened, and the easiest tool for the job is the trusty [root mean square of the error](https://byjus.com/maths/root-mean-square/) (aka RMS of the error).

So, the first step here is to determine what "no sound" means. To do that I can calculate the RMS of the signal in a quiet place and save that value.

```python
sumsq = 0
count = 0

for b in buffer:
  count += 1
  sumsq += b*b

mean = sumsq/count
rms = math.sqrt(mean)
```
Because the sound stream's packets are 8 bits, and the stream is in mono configuration, rather than stereo, it's easy to calculate the RMS of the signal from individual bytes. A different setup would require some changes to the calculation.

So, I can take that RMS value and save it to a file, and use it later to calculate the RMS of the error to see if there is a signal.

``` python
def rmsError(buffer, quiet_value):
  sumsqdiff = 0
  count = 0

  for b in buffer:
    count += 1
    sumsqdiff += (b-quiet_value)**2

  mean = sumsqdiff / count
  rms = math.sqrt(mean)

  return rms
```

Just like that.

## Step 3. Plumbing the sound pipes

So, I know how to generate the stream of sound, and I know how I am going to process it, but how do I get it into python to do so?

For this I shall use the [subprocess](https://docs.python.org/3/library/subprocess.html) module. Specifically the `Popen` function.

This is the line:
``` python
proc = subprocess.Popen(['arecord', '--device=sysdefault:CARD=C920', '-M', '--rate=32000', '-'], stdout=subprocess.PIPE)
```
The `'-'` at the end of the list is the file name, if you recall, that is the shorthand for stdout. I've also upped the sampling rate from 8000 to 32000, just because I can, and set the M flag which means arecord will memory map the stream, which seems pretty fancy. 

The first thing to note is that I have omitted a duration, which means the mic will be listened to until the process is stopped by some method (see below if you can't wait to find out).

Next, note that the `--device` argument is system dependent. You should use the method I detailed above to determine your own mic's pcm name.

The other, important, thing to note is that the stdout parameter is assigned `subprocess.PIPE`, which allows me to read the output arecord is generating.

To get the buffer that will be analysed, I just need to read a certain number of bytes from the stdout stream:
``` python
buffer = proc.stdout.read(32000)
```
Thanks to the stream being 8 bit, the number of bytes per second is the same as the sampling frequency! Isn't that just dandy.

So, pulling it all together:

``` python
import subprocess
import math
import os
import signal as killsig

def detect():
  quiet_val = 127.0
  with open('calibration.txt', 'r') as califile:
    quiet_val_str = califile.readline()
    quiet_val = float(quiet_val_str)

  proc = subprocess.Popen(['arecord', '--device=sysdefault:CARD=C920', '-M', '--rate=32000', '-'], stdout=subprocess.PIPE)

  proc.stdout.read(44) #skip the file header

  signal = 0

  while signal < 2:
    buffer = proc.stdout.read(32000)
    signal = rmsError(buffer, quiet_val)

  print('signal: %f'%signal)

  os.kill(proc.pid, signal.SIGINT)
```
So, as noted in the code, I want to skip the file header, so it's not considered part of the signal. I figured out the length by looking at `file.wav`. I'm sure it's in an RFC or other specification somewhere, but who has the time to check?

I've decided that "sound" is anything with an RMS error of 2 or above. I got that from some experimenting. I could set it higher if I want a less sensitive sound detector.

I decided to use `os.kill(proc.pid, signal.SIGINT)` (which is the equivalent of pressing CTRL+C in the terminal) over `proc.kill()` or `proc.terminate()` because it seems gentler. Those other methods are fine, I'm just fussy.

## Discussion
This is a quick and kinda dirty, but not very dirty, way to detect sound from a mic. 

I have defined sound to be any signal that has an RMS error of 2 or above from the quietness value I calculated.

There is no error detection in this code, so if anything goes wrong, e.g. the calibration file isn't present, or the device name is wrong, the script will crash inelegantly. Something for the readers to look into 😉.

Now I have to figure out what to do once I have detected a sound...

Thanks for reading and let me know in the comments if you found this helpful, any tips to improve it, or anything else!