Sound monitoring via Web Cam

Hi All,

We recently got a little puppy and she is at the other end of the house overnight. She cry’s when she needs to go to the toilet so I made a little sound sensor using a webcam i had already setup. When the noise is over a certain threshold it turns a bedside light on in our room.

Setup:
1 x Pi Zero (w)
1 x LifeCam HD 3000
& Home Assistant running on Pi3

Software:
Motion running on Pi Zero
MQTT on Pi3 and Pi Zero

First I used these steps to setup a the Pi Zero running Motion to stream the webcam via Home Assistant:

Then I modified some ruby code written by Marco Mornati on his blog

The code on his blog monitors sound samples every 5 seconds then if above a threshold, emails you the 5 second sound file. I didn’t want the emails so I removed that section and instead posted the threshold via MQTT back to Home Assistant.

#!/usr/bin/ruby -w
# 
# Modified for Home Assistant by J. Phillips 2017
# Copyright (C) 2013 Marco Mornati [http://www.mornati.net]
# Based on Thomer M. Gil First [http://thomer.com/] version template
#
# Oct  05, 2012: Initial version
#
# This program is free software. You may distribute it under the terms of
# the GNU General Public License as published by the Free Software
# Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# This program detects the presence of sound and invokes a program.
#

require 'getoptlong'
require 'optparse'
require 'net/smtp'
require 'logger'
require 'date'
require 'rubygems'
require 'mqtt'


HW_DETECTION_CMD = "cat /proc/asound/cards"
# You need to replace MICROPHONE with the name of your microphone, as reported
# by /proc/asound/cards
SAMPLE_DURATION = 5 # seconds
FORMAT = 'S16_LE'   # this is the format that my USB microphone generates
THRESHOLD = 0.05
RECORD_FILENAME='/tmp/noise.wav'
LOG_FILE='/var/log/micLevel.log'
PID_FILE='/etc/noised/noised.pid'

logger = Logger.new(LOG_FILE)
logger.level = Logger::DEBUG

logger.info("Noise detector started @ #{DateTime.now.strftime('%d/%m/%Y %H:%M:%S')}")


def self.check_required()
  if !File.exist?('/usr/bin/arecord')
    warn "/usr/bin/arecord not found; install package alsa-utils"
    exit 1
  end

  if !File.exist?('/usr/bin/sox')
    warn "/usr/bin/sox not found; install package sox"
    exit 1
  end

  if !File.exist?('/proc/asound/cards')
    warn "/proc/asound/cards not found"
    exit 1
  end
  
end

# Parsing script parameters
options = {}
optparse = OptionParser.new do |opts|
  opts.banner = "Usage: noise_detection.rb -m ID [options]"

  opts.on("-m", "--microphone SOUND_CARD_ID", "REQUIRED: Set microphone id") do |m|
    options[:microphone] = m
  end
  opts.on("-s", "--sample SECONDS", "Sample duration") do |s|
    options[:sample] = s
  end
  opts.on("-n", "--threshold NOISE_THRESHOLD", "Set Activation noise Threshold. EX. 0.1") do |n|
    options[:threshold] = n
  end
  opts.on("-e", "--email DEST_EMAIL", "Alert destination email") do |e|
    options[:email] = e
  end
  opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options[:verbose] = v
  end
  opts.on("-d", "--detect", "Detect your sound cards") do |d|
    options[:detection] = d
  end
  opts.on("-t", "--test SOUND_CARD_ID", "Test soundcard with the given id") do |t|
    options[:test] = t
  end
  opts.on("-k", "--kill", "Terminating background script") do |k|
    options[:kill] = k
  end
end.parse!

if options[:kill]
  logger.info("Terminating script");
  logger.debug("Looking for pid file in #{PID_FILE}")
  begin
    pidfile = File.open(PID_FILE, "r")
    storedpid = pidfile.read
    Process.kill("TERM", Integer(storedpid))
  rescue Exception => e
    logger.error("Cannot read pid file: " + e.message)
    exit 1
  end
  exit 0
end

if options[:detection]
    puts "Detecting your soundcard..."
    puts `#{HW_DETECTION_CMD}`
    exit 0
end

#Check required binaries
check_required()

if options[:sample]
    SAMPLE_DURATION = options[:sample]
end

if options[:threshold]
    THRESHOLD = options[:threshold].to_f
end

if options[:test]
    puts "Testing soundcard..."
    puts `/usr/bin/arecord -D plughw:#{options[:test]},0 -d #{SAMPLE_DURATION} -f #{FORMAT} 2>/dev/null | /usr/bin/sox -t .wav - -n stat 2>&1`
    exit 0
end

optparse.parse!

#Now raise an exception if we have not found a host option
raise OptionParser::MissingArgument if options[:microphone].nil?
#raise OptionParser::MissingArgument if options[:email].nil?

if options[:verbose]
   logger.debug("Script parameters configurations:")
   logger.debug("SoundCard ID: #{options[:microphone]}")
   logger.debug("Sample Duration: #{SAMPLE_DURATION}")
   logger.debug("Output Format: #{FORMAT}")
   logger.debug("Noise Threshold: #{THRESHOLD}")
   logger.debug("Record filename (overwritten): #{RECORD_FILENAME}")
   logger.debug("Destination email: #{options[:email]}")
end

#Starting script part
pid = fork do
  stop_process = false
  Signal.trap("USR1") do
    logger.debug("Running...")
  end
  Signal.trap("TERM") do
    logger.info("Terminating...")
    File.delete(PID_FILE)
    stop_process = true 
  end

  loop do
    if (stop_process)
	logger.info("Noise detector stopped @ #{DateTime.now.strftime('%d/%m/%Y %H:%M:%S')}")	
	break
    end
    rec_out = `/usr/bin/arecord -D plughw:#{options[:microphone]},0 -d #{SAMPLE_DURATION} -f #{FORMAT} -t wav #{RECORD_FILENAME} 2>/dev/null`
    out = `/usr/bin/sox -t .wav #{RECORD_FILENAME} -n stat 2>&1`
    out.match(/Maximum amplitude:\s+(.*)/m)
    amplitude = $1.to_f
    logger.debug("Detected amplitude: #{amplitude}") if options[:verbose]
    if amplitude > THRESHOLD
      logger.info("Sound detected!!!")
      MQTT::Client.connect(
        :host => '192.168.0.123',
        :username => 'USER',
        :password => 'PASSWORD',
        :port => 1883,
      ) do |client|
      client.publish('HA/RUBY',1)
      client.publish('HA/RUBY',amplitude * 100)
      end

    else
      logger.debug("No sound detected...")
      MQTT::Client.connect(
        :host => '192.168.0.123',
        :username => 'USER',
        :password => 'PASSWORD',
        :port => 1883,
      ) do |client|
      client.publish('HA/RUBY','0')
      end
    end
end
end

Process.detach(pid)
logger.debug("Started... (#{pid})")
File.open(PID_FILE, "w") { |file| file.write(pid) }

The issue I had was the graph would have diagonal lines from the most recent state change to 0 to the new threshold value even though it reports a 0 state every 5 seconds. That is why I changed the state to 1 prior to reporting the threshold value in line 178. I want the graph to only have horizontal and vertical lines so will post in another thread.

This probably could have been done with Python rather than ruby but I thought it would be worth me experimenting with Ruby as I had never touched it before.

3 Likes