Tags
audio, email, lame, Lua, Lua Lanes, microphone, mp3, muLaw, multithreading, PDM, Raspberry Pi, RPi, SMTP, SoX, SPL, SPLear, u-Law
This post describes how to record audio from a SPLear™ sound level measurement module connected to a Raspberry Pi, when the SPLear signals an alert. In the RPi, we record the audio, encode to MP3, and email it when scared by a loud noise.
This is part of our series of articles on the general subject of audio signal processing from air to information.
When startled, the program described in this post will capture about 20 seconds of audio with the trigger about 25% from the beginning, encode it to mp3, and email it. We use SoX for formatting and LAME for encoding the audio, and mpack
for attaching it to an email.
Install recent builds of sox
and lame
on your RPi with sudo apt-get install lame sox
. See the earlier posts for setting up email and using mpack
, and the previous post about using GPIO pins.
Much of the firmware and code running in the RPi has been discussed and published in past posts on this blog, and the source code is available from a public repository. The firmware and Lua program used here is checked in as [8d8ccc7f52].
Recording audio from the SPLear
The recent RPi work has mostly used the SPLear firmware’s S
mode which uses the UART to deliver about 10 SPL samples per second. This post will use R
mode which instead delivers 8000 µLaw audio samples per second. It will also take advantage of a GPIO pin which will signal an alert when the SPL of the audio changes rapidly
The basic outline is to keep a buffer of recent audio samples, collected in conveniently sized chunks. As we will describe later, a separate thread will monitor the GPIO pin, and notify the recorder when it alerts. Once triggered, we continue to fill the buffer so that the trigger is about 1/3 of the way in.
We then write a file in the easy to specify .au
format, pass it to sox
for sample rate adjustment and conversion to something that lame
can read for encoding to MP3, and to mpack
to attach to an email.
Writing the .au
file requires filling out the file header. That will be easy to do using Roberto’s struct module, which is easily added to our Lua installation with sudo luarocks install struct
.
The file header looks like this, expressed as a C struct
:
/* Sun .au file header, must be big-endian. */ struct au { uint32_t magic; // ".snd" uint32_t offset; // byte offset of first sample uint32_t size; // data size or ~0 uint32_t encoding; // format flag uint32_t rate; // sample rate, Hz uint32_t channels; // interleaved channels char comment[4]; // NUL terminated ASCII };
All fields are big-endian in files or streaming on a wire. The comment field is room for an optional comment, which must be NUL terminated, and must occupy a minimum of four bytes. It potentially fills all the space from the first byte of the field to the first byte of the audio, or .offset - 24
bytes. Since all fields are big-endian and four bytes wide, the struct
format specification is easy to write, and a valid header can be formed like this:
local struct = require"struct" local function auheader(buf, rate) local fmt = ">c4LLLLLL" local hdr = struct.pack(fmt, ".snd", -- 0 magic number struct.size(fmt), -- 1 data offset #buf, -- 2 length or -1 1, -- 3 encoding uLaw rate, -- 4 8152 Hz sample rate 1, -- 5 mono 0) -- 6 empty metadata string return hdr end
The complete .au
file can be written like this:
local function writeau(au, buf) local f = assert(io.open(au,"wb")) f:write(auheader(buf, 8152), buf) f:close() end
The uLaw samples in a .au
file are still larger than needed, and are generally pretty quiet when an alarm isn’t ringing. To make the recordings more interesting, we’d like to normalize the volume level, possibly apply a noise gate to keep silences silent, and transform it to a more widely available format like MP3. To do that, we’ll use the two audio utilities that we mentioned earlier in this post, SoX and lame.
SoX is a very flexible command line audio manipulation tool. We’ll use it here to gate and normalize the audio and then pass it to lame for encoding. If building SoX from source, you can include lame’s encoder library, but unfortunately the distributed version was not built that way.
function writemp3(name, udata) local soxgate = 'compand .1,.2 -inf,-65.1,-inf,-65,-65 0 -90 .1 ' local soxcompand = 'compand 0.3,1 6:−70,−60,−20 −5 −90 0.2 ' local soxcmd = 'sox -t au - -t wav -b 16 - ' .. soxgate .. soxcompand ..'| lame - '..name print(soxcmd) local fp = assert(io.popen('sh -c "'..soxcmd..'"', 'w')) fp:write(auheader(udata, 8152), udata) fp:close() end
The complex command it assembles invokes SoX to process .au
format data from stdin
, filter it with compand
configured as a noise gate, and with compand
configured to normalize the level, writing the processed samples as 16 bit PCM in a .wav
file to stdout
. The processed PCM samples are collected by lame
from its stdin
, compressed to MP3 with its default settings, and written to a file. The Lua function io.popen
is a wrapper for the C runtime function popen(3)
which handles the gory details of creating a subprocess from a passed shell command line and returning a file descriptor which can be written on to be the command’s stdin
.
The above code fragments are part of the program record.lua
found in the repository, which demonstrates recording for a short time and saving the recording to a file named like rec-20150610-181019.mp3
.
Acting when scared
Since we are using the UART to carry the audio samples to be recorded, we can’t use it (at least with the current firmware) to carry the SPL information as well. But the SPLear has a feature we demonstrated in the previous post: it wiggles an output pin when a condition is met. The RPi can then use one of its available GPIO pins to hear that, and take an action.
To do this, we need to do two things. First, we need to tweak the condition in the SPLear, which is set to be too sensitive in all the firmware released so far. (While it would be a good idea in the long run to allow that sensitivity to be adjusted, we won’t do that now. Consider it an exercise for the reader.)
Second, we need to get access to the RPi’s GPIO pins from our Lua program. The easy way to do that is to simply use the sysfs
mechanism provided in the Linux kernel, and demonstrated in the previous post. With that, after a small amount of setup to declare one specific pin to be readable, the program could read it’s current state as if it were any other file in the filesystem.
We considered using a port to Lua of the Python RPi.GPIO
library, but it is overkill for our needs and would require us to be running as root. For our application, a combination of /sysfs/class/gpio/
and the poll(2)
system call will do everything we need.
The program pinsysfs.lua
from the previous post will be the starting point for a brute force Lua module that provides just the services we need for GPIO pins, found in the repository as sysfsgpio.lua
. I’ll provides some highlights here to show how I turned the demo into a module, but see the full file for the details.
I add a preamble to provide the form of a module table:
-- Module to make GPIO via the sysfs gpio class interface easy -- for simple Lua applications. -- simplest minimal module framework, just return M local M = { _NAME=(...), _VERSION="v0.001", }
The balance of the module is pretty much identical to the old demo, aside from local gpio = M
in the middle to make the function declarations look nice, and return M
at the end to satisfy the Lua module system.
All functions are either declared local
, or are members of the M
table (which is also called gpio
). That table will be returned as the result of require "sysfsgpio"
. As before, I require "posix"
, implement some utilities to provide easy access to the content of the gpio control files as strings, and create a table of valid GPIO pin numbers for the RPi B+ in gpio.pins
.
Our module API is through the functions export(pin)
, unexport(pin)
, direction(pin, dir)
, edge(pin, edge)
, and wait(pin)
, all members of the gpio
table.
There is room for improvement in this module, specifically it likely should check what model and version of RPi it is running on and adjust its table of valid pins to suit, and it certainly could be friendlier about checking if this is a Linux platform with a /sys/class/gpio
at all.
Doing two things at once in Lua
Our program will need to collect sound samples for the recording at the same time it is watching the GPIO pin for an alert. While that can be done by careful alternation of calls on both the serial port and the GPIO, in this case it is easy to do it by running two threads.
The usual way to run Lua in multiple cooperating threads is to use the Lanes module. Install it with sudo luarocks install lanes
and wait a few minutes for it to compile and install.
A simple example of lanes could be something like this, which volleys a token back and forth between lanes until it has moved enough times to measure the run time. Run it from one shell, and use something like top
to watch it spawn threads and run. The function volley
is executing in both worker threads, when launched by the thread generator function gen
. It simply decides on the names of its message slots, announces it is ready with the done
message, then catches, increments, and throws the ball around. The main thread constructs a generator for threads running that function, then calls it twice to spawn two worker threads. It then waits for the first message on done
to be delivered twice indicating that both workers are waiting for the ball, then tosses the ball in with an initial value of 0 and waits again for done
to be delivered twice more. The rest is just elapsed time measurement and arithmetic.
local lanes = require 'lanes'.configure() local msg = lanes.linda() local function volley(n,isa) print("Starting "..(isa and "A" or "B"), n) local rkey,skey = "a","b" if isa then skey,rkey = rkey,skey end msg:send("done",true) for i=1,n do local ball,value = msg:receive(rkey) msg:send(skey, value+1) end msg:send("done",true) end print("starting threads") local gen = lanes.gen("",volley) local N = 1e5 local a = gen(N, true) local b = gen(N) local ok,_ = msg:receive(0.5, msg.batched, "done", 2) if not ok then print "ouch" return end local t0 = os.time() msg:send("a", 0) msg:receive(nil, msg.batched, "done", 2) local t1 = os.time() print((t1-t0).." sec") print(((t1-t0)/N).." sec") print((N/(t1-t0)).." per sec")
When run on my RPi B+, I see the following:
pi@treepi ~/pdmstunts/Scared $ lua volley.lua
starting threads
Starting A 100000
Starting B 100000
57 sec
0.00057 sec
1754.3859649123 per sec
pi@treepi ~/pdmstunts/Scared $
and top
indicates that three threads running Lua are splitting the CPU pretty much equally. This isn’t a surprise since the worker threads are just doing arithmetic and sending a message, and internally all the message traffic involves the original thread to mediate the data marshalling. While 1700 volleys per second isn’t an amazing figure, it is more than enough performance to support our needs with the SPLear.
Lanes can be far more complicated to use if you have complicated requirements, but for this project we only need two threads, and only one message. Even better, since you get one thread free just for running the program, we will just need a single lane to sequester our GPIO watcher.
Follow along in the latest version of scared.lua
from the repository. Here’s a few highlights.
We added a configuration file, and extended it to include the parameter mode
to choose between the simple SPL graph email and the more complicated sound bite email. The SPL graph mode is largely unchanged from earlier posts.
-- command the requested mode if rc.mode == "record" then port:write"R" -- so this write is immediate
The inter-thread communications channel is provided by the function lanes.linda()
. We only need one. You call its send()
method to send a message and receive()
method to receive a message, each operating on a specific key. We’ll use the key named “eek” to pass an alert from the watch to the recorder.
local msgs = lanes.linda()
The anonymous function passed to lanes.gen()
will run in its own thread. It will use the sysfsgpio
module to configure and watch pin P1-26
(aka GPIO07
) which we have wired to the SPLear. This setup and loop is pretty much identical to the demo from the last post. The key difference is that when the rising edge is detected, we now call msgs:send("eek", isodate())
which passes a nicely formatted time stamp for the alert to any thread that might be listening.
-- Function running in a separate thread that watches for loud -- noises signled via GPIO pin pinwatcher = lanes.gen("*", function() gpio = require "sysfsgpio" gpio.export(7) gpio.direction(7, "in") gpio.edge(7, "rising") print("Watching GPIO07 which is pin P1-26") while true do local r = gpio.wait(7) if r == 0 then -- poll timed out elseif r == 1 then msgs:send("eek", isodate()) else -- something unexpected happened end end end )()
Putting it all together
Continuing to look at scared.lua
which is in the repository.
We set up the SPLear for audio samples, then read them, keeping a history in a buffer, until we are signaled on our GPIO pin. Once triggered, we continue buffering for a while so that we can show data from before and after the trigger, then package up the audio, convert it to mp3, and send it by email.
-- command the requested mode if rc.mode == "record" then port:write"R" -- so this write is immediate local msgs = lanes.linda() -- ... (described above) -- main loop reads data as usual, keeping an eye out for -- being scared, after which it saves and emails a recording. local triggered = false local eek while true do local sample = port:read(800) -- read 1/10 sec audio if sample then io.write("Rec "..(triggered and "TRG " or " "), recent.n, "r") io.flush() recent:add(sample) -- Notice GPIO pin local s = pinwatcher.status if not (s == "waiting" or s == "running" or s == "pending") then print("Watcher status: "..s) break end local eekflag, eekval = msgs:receive(0.0, "eek") if eekflag then if not triggered then triggered = math.floor(recent.depth * 2. / 3.) eek = "EEK! "..eekval end end if triggered then triggered = triggered - 1 if triggered == 0 then triggered = false local data = recent:raw(false) local mp3file = "eek-"..namedate()..".mp3" writemp3(mp3file, data) print("email " .. addr, mp3file, isodate()) mpack(addr, eek, mp3file) end end end end else -- mode == "SPL" -- ... end
The complete program, when run with scared.rc
that specifies mode="record"
, monitors the audio and sends an email. It does have a few weak spots that can be refined.
The firmware is now in charge of when to be scared, and it is currently a lot more sensitive than the mode="SPL"
code ended up. That should be tweaked, and probably even improved to allow it to be controlled from the RPi, from a parameter in scared.rc
.
The scared.rc
file is in the current directory, not anywhere sensible. In fact, the whole scared.lua
program needs to grow a makefile and get “properly installed” to be run on the RPi at boot so that it becomes an appliance.
The trigger window may not be the right length. 20 seconds seemed long enough in playing around on my desk, but perhaps a couple of minutes would be more useful in the real world. In any case, making this a parameter read from scared.rc
is probably a sensible idea.
Raspberry Pi Background
The Raspberry Pi (or RPi for short) is a very inexpensive single board computer that can run Linux, and supports USB, ethernet, and HDMI for both video and audio output. My RPi is booting Raspbian, is headless, and has been configured (with sudo raspi-config
) to speak as little as possible on its UART. An earlier post documented the configuration steps and the connection to the SPLear. To that configuration, add a wire from the SPLear’s P13 to GPIO07 on the RPi.
In short, wire it as follows:
RPi GPIO header SPLear J1
--------------- -----------
2 (5V) J1.2 (VCC)
6 (GND) J1.1 (GND)
8 (UART0_TxD) J1.6 (RXD)
10 (UART0_RxD) J1.7 (TXD)
26 (GPIO07) J1.4 (P13)
From software, the UART is known to Linux as /dev/ttyAMA0
.
Don’t forget to do sudo apt-get update
and possibly sudo apt-get upgrade
if it has been a since you first set up Raspbian on your RPi.
Setup email tools, and a suitable SMTP daemon such as sSMTP
with sudo apt-get install ssmtp mailutils mpack
and don’t forget to configure your SMTP appropriately. I’m using GMail for sending mail, and don’t want or need to be able to receive mail on the RPi. I have GMail configured for 2-factor authorization, so I used its application password feature to issue a password for my RPi to use. This is a 16-character password that is managed by Google, and is specific to that mail account from that single device. Generate one for your RPi, and don’t use the same one in any other device connected to GMail.
Install gnuplot with sudo apt-get install gnuplot
.
In one mode, the program uses mpack
to send the plot as a MIME email attachment in the simplest way possible. In another mode, it uses mpack
to send MP3 audio. mpack
was installed above with the email tools.
I’ve also added support for LuaSocket to my RPi to go with the Lua 5.1.5 that it had already. LuaSocket provides a broad base of internet protocol support for programs written in Lua. It can be built from source, but it is also available as a package:
sudo apt-get install lua-socket-dev
To make managing Lua modules easier, I installed luarocks with sudo apt-get install luarocks
. It is possible that it could have been used to install LuaSocket. I explicitly used it for struct
(with sudo luarocks install struct
) and lualanes
(sudo luarocks install lanes
).
What Now?
The next step is to continue to find interesting things to do with the measured SPL level, which will certainly be the subject of future posts. Watch this space!
Please let us know what you think about the SPLear!
If you have a project involving embedded systems, micro-controllers, electronics design, audio, video, or more we can help. Check out our main site and call or email us with your needs. No project is too small!
+1 626 303-1602
Cheshire Engineering Corp.
120 W Olive Ave
Monrovia, CA 91016
(Written with StackEdit.)