, , , , , , , , , , , , ,

This post describes how to plot SPL over time, using a SPLear™ sound level measurement module connected to a Raspberry Pi. In the RPi, we use gnuplot to draw a pretty picture 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 SPL levels with the trigger about 25% from the beginning, graph it, and email it. We covered the use of gnuplot in the previous post, this post will concentrate on the monitor program. The program is demonstrated running in a Raspberry Pi connected to our SPLear board through the RPi’s UART.

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 [26b540b327].

Sending mail when scared

As in the earlier version, we set up the SPLear for raw SPL samples, then read them, keeping a history in a buffer, until we spot a large increase in SPL. Once triggered, we continue buffering for a while so that we can show data from before and after the trigger, then package up the data, draw the chart, and send it by email.

The complete script is in the fossil repository, I’ll just note some highlights here.

I’ve hidden the details of invoking gnuplot and configuring its script in a Lua module, which exports only one function named plot(). It is implemented by plotspl.lua, in the form of a simplest possible module declared in a way that is portable to Lua 5.1, 5.2, and 5.3.

-- miminal Lua 5.x module setup, M is the module's table.
local M = {
  _NAME=(...) or "plotspl",
  _DESCRIPTION="Plot SPL data with gnuplot",
package.loaded[M._NAME] = M

-- ... implementation details go here ...

-- plot the given data string to the named png file.
function M.plot(data, pngfile, trigger, title)
  local datafile = prepdata(data)
  local plotfile = prepscript(datafile, pngfile, trigger, title)
  local cmd = "gnuplot " .. plotfile .. " >" .. plotfile .. ".log 2>&1"

-- return the module table
return M

I won’t bother to show the prepdata() function, as it just writes the string to a temp file and returns the name of the file created. The prepscript() function is similarly simple, writing the gnuplot script to a second temp file, and returning the name of the script. However, I will show the resulting script file:

The variables set at the top have values passed in to the module via the plot() function, and allow the body of the script to be a constant string. The bulk of the graph follows the same pattern as in the earlier post, but some details are different. Most notably, the data has been scaled so that column 1 is actually time in seconds. It still needs to be offset by the trigger time so that the graph shows the trigger at zero, but it no longer needs to scale everything by 10. We use the png terminal to write a file, with all actual output wrapped by set output commands to open and close the file.

# Configuration
titletext="Startled at 2015-05-21 18:04:05"

set terminal push

# Graph Title, Axis labels, key and grid
set title titletext
set xlabel "seconds"
set ylabel "SPL, dB"
set key off
set grid xtics ytics

# Compute mean and sd by cheating with a constant function fit to the
# data.
f(x) = mean_y
fit f(x) datafile via mean_y
stddev_y = sqrt(FIT_WSSR / (FIT_NDF + 1 ))

set terminal png
set output outfile

# label a vertical line at trigger time, which will be at x=0 after
# transformations
set label 1 "Trigger" at 0, graph 0 offset character 0.25,0.5
set arrow 1 from 0, graph 0 to 0, graph 1 nohead linewidth 5 lc rgb "#ccccff"

# plot with stddev bounds and mean, scaling and offsetting x to
# seconds with x=0 at the trigger
plot datafile using ($1 - trigger):2 with lines linewidth 2.5, 
        mean_y with lines lc rgb "#00dd00", 
        mean_y-stddev_y with lines lc rgb "#dd0000", 
        mean_y+stddev_y with lines lc rgb "#dd0000"

set output
set terminal pop

The main program is still in scared.lua, which begins with a reference to the plotspl module. The rest of this discussion will skip over some of scared.lua, see the fossil repository for the full file.

#! lua
local plot = require "plotspl"
-- ...

Invoking here mpack to send a picture is simpler than invoking mail to send general text as I’m not going to try to put any part of the message on stdin. The picture is already in a file, and mpack really only supports a subject line and addressee. I used Lua’s %q format which provides a quoted string safe for reading as a Lua string literal. This is almost (but not quite) exactly what should be done for a shell string literal. The differences are all in the handling of characters that should never be in an email address. Replacing this with a proper shell quoted string is left as an exercise. Don’t forget about the lesson of little Bobby Tables!

-- ...
-- Send a MIME packed email containing an image of the
-- event to a recipient with a subject
-- mpack(address, eek, "scared.png")
local function mpack(address, subject, pngfile)
  local cmd = ("mpack -a -s %q %q %s"):format(subject, pngfile, address)
  -- print(cmd)
-- ...

I improved my buffer object to better parameterize the buffer depth. I also tweaked it’s text() function to produce two columns of text, with the first one scaled to be (approximate) seconds and the second (approximate) dB SPL. This is easier for gnuplot to digest.

-- ...
-- Implement a buffer of samples. For simplicity, let the
-- index increment without bound. Note that this will have
-- a problem after 2^53 samples, which is about 30 million
-- years at 10 samples per second.
local recent = {
    local n = self.n
    self[n] = spl
    self[n-self.depth] = nil
    self.n = n + 1
    local t = {"s, dB"}
    local offset = self.n-self.depth-1
    for i=1,self.depth do
      if self[i+offset] then
        t[#t+1] = ("%0.1f, %s"):format(i/10,self[i+offset])
    return table.concat(t,"n")
-- ...

The main loop has been tweaked a little to be more careful about multiple triggers during the trigger window. It now ignores additional triggers until after it finished any window currently being processed.

-- ...
while true do
  local line = port:read"*l" -- read a single line
  spl = tonumber(line)
  if spl then
    spl = 0.75 * spl
    io.write("SPL "..(triggered and "TRG " or "    "), spl, "r")

    -- Watch for the trigger condition: 12 dB louder
    if not triggered and spl0 and (spl - spl0 > 12.) then
      triggered = math.floor(recent.depth * 2. / 3.)
      eek = "EEK! "..spl.." dB"

    if triggered then
      triggered = triggered - 1
      if triggered == 0 then
        triggered = false
        local data = recent:text()
        local pngfile = "startled-plot.png"
          recent.depth/30., -- 1/3 trigger pos, 1/10 for time scale
          "Startled at "..isodate())
        local body = eek.."n"..data
        print("email " .. addr, eek, isodate())
        mpack(addr, eek, pngfile)
    spl0 = spl

This certainly can be further improved. An obvious example is that the attached file has a constant name. If it had a name with the time stamp in it, that would make it easier to keep a log of past events.

It could also attach the SPL samples for other analysis or plotting.

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.

The trigger condition itself could use some work. We have thought about using a trigger level set at several standard deviations above the recent mean instead of the current slope condition. Alternatively, triggering at a level 15 dB above mean rather than at a slope of 120 dB/s rise time might make sense. For advance use, watch for the distinctive ring pattern of the alarm you specifically want to hear, and only trigger on that case.

Capturing a long sample (24 hours?) and sending it once a day as a reassurance that the box is alive might be friendly.

But our next exploit is likely to be to record live audio when triggered, and send that in a format that can be listened to in your phone or PC.

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.

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)

From software, the UART is known to Linux as /dev/ttyAMA0.

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.

Install gnuplot with sudo apt-get install gnuplot.

The simple script uses mpack to send the plot as a MIME email attachment in the simplest way possible. mpack was installed above with the email tools.

Configuration for Sending Mail

Out of the box, Raspbian Linux doesn’t supply any email components. Configure some to suit your personal taste. I installed sSMTP along with some common utilities to make basic operation of email easy:

sudo apt-get install ssmtp mailutils mpack

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.

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

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.

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.)