====== Audio Alerts (squawk) ======
Traditionally this was handled by doorpi. However, that pi has too much stuff hanging off it, and was very out of date. There was an issue of a loud pop occurring before any sound played, this was fixed in later firmware updates. Rob has purchased a pi (original B), amplifier and speakers for the purpose and documents them thus:
===== Usage =====
Sounds and TTS messages are triggered via the [[mqtt#sound|message bus]].
Examples for testing might be something like:
Play a sound file:
mosquitto_pub -h mqtt -t 'sound/g1/play' -m canttouchthis.mp3
Play a list of sound files with no gap:
mosquitto_pub -h mqtt -t 'sound/g1/playlist' -m "long_long_man/long.mp3,long_long_man/loooong.mp3,long_long_man/maaaan.mp3"
Text-to-speech message (with a subtle notification bong):
mosquitto_pub -h mqtt -t 'sound/g1/speak' -m "All your base are belong to us"
Text-to-speech message (with a clear airport/transit chiming sound):
mosquitto_pub -h mqtt -t 'sound/g1/announce' -m "Good evening, my sensors indicate that the laser cutter may be on fire."
**Theory:** You could direct sound two only two rooms at once by playing the left or right channel only. By default mpg123 is called with the -m option to ensure that the output is mono and thus the same sound comes out every speaker.
===== Adding new sounds =====
Use the web file browser at http://squawk.hacklab:8080/.
===== Raspberry Pi OS =====
Latest Raspberry Pi OS, installed using Raspberry Pi Imager.
==== raspi-config ====
* Expand filesystem
* Medium overclock
* Audio output forced to 3.5mm
* Hostname set
==== RAM /tmp /var/log ====
Appended to /etc/fstab:
tmpfs /var/log tmpfs defaults,noatime,nosuid,mode=0755,size=100m 0 0
tmpfs /tmp tmpfs defaults,noatime,nosuid,size=100m 0 0
tmpfs /var/tmp tmpfs defaults,noatime,nosuid,size=30m 0 0
tmpfs /var/log tmpfs defaults,noatime,nosuid,mode=0755,size=100m 0 0
tmpfs /var/run tmpfs defaults,noatime,nosuid,mode=0755,size=2m 0 0
tmpfs /var/spool/mqueue tmpfs defaults,noatime,nosuid,mode=0700,gid=12,size=30m 0 0
==== Packages ====
sudo apt-get install mpg123
sudo apt-get install sox
sudo apt-get install python3-pip
sudo pip install --break-system-packages paho-mqtt
==== systemd service ====
/etc/systemd/system/squawk.service
[Unit]
Description=Runs the squawk listener script
[Service]
ExecStart=/home/pi/startup.sh & /home/pi/respawn --syslog --max-backoff=10 /home/pi/squawk.pi
[Install]
WantedBy=multi-user.target
...and enabled with
$ sudo systemctl start squawk
$ sudo systemctl enable squawk
===== Scripts =====
Various scripts to make things happen.
==== /home/pi/squawk.py ====
#!/usr/bin/env python
import paho.mqtt.client as mqtt
import subprocess
import os
import logging
import signal
import time
import random
logging.basicConfig(level=logging.INFO)
max_playtime = 15
sounds_path = "/home/pi/sounds"
status = 'closed'
# runs a command and terminates it after a specified timeout
def call_with_timeout(command, timeout):
logging.info('call_with_timeout(%r, %r)' % (command, timeout))
class TimeoutException(Exception):
pass
def alrm_handler(signum, frame):
raise TimeoutException()
try:
old_handler = signal.signal(signal.SIGALRM, alrm_handler)
signal.alarm(timeout)
p = subprocess.Popen(command)
retcode = p.wait()
logging.info('call_with_timeout: command exited with code %s' % (retcode))
except TimeoutException:
logging.info('call_with_timeout: command exceeded timeout, terminating...')
p.terminate()
retcode = p.wait()
finally:
signal.signal(signal.SIGALRM, old_handler)
signal.alarm(0)
return retcode
# waffle waffle
def speak(data, timeout=max_playtime):
command = ['/home/pi/pico.sh', data]
call_with_timeout(command, timeout=timeout)
def getfiles(path, exts=[".mp3", ".wav"]):
allfiles = []
for dirpath, dirnames, filenames in os.walk(path):
for filename in filenames:
base, ext = os.path.splitext(filename)
if ext in exts:
#allfiles.append(os.path.relpath(os.path.join(dirpath, filename), path))
allfiles.append(os.path.join(dirpath, filename))
return allfiles
# make some noise for the vengaboys
def play(filename, timeout=max_playtime):
allfiles = getfiles(sounds_path)
filename = os.path.join(sounds_path, filename)
if filename.endswith('/'):
# pick a random file from a directory
candidates = []
for f in allfiles:
if f.startswith(filename):
candidates.append(f)
if len(candidates) == 0:
logging.error('No files matching %s' % (filename))
return
filename = random.choice(candidates)
else:
# single file requested
if filename not in allfiles:
logging.error('File %s not found' % (filename))
return
base, ext = os.path.splitext(filename)
if ext == '.mp3':
command = ['mpg123', '-q', '-m', filename]
call_with_timeout(command, timeout=timeout)
else:
command = ['play', '-q', filename]
call_with_timeout(command, timeout=timeout)
def on_connect(client, userdata, flags, rc):
client.subscribe("sound/g1/play")
client.subscribe("sound/g1/speak")
client.subscribe("sound/g1/announce")
client.subscribe("display/doorbot/intercom")
client.subscribe("labstatus")
def on_message(client, userdata, msg):
global status
if msg.topic == 'labstatus':
if msg.payload == 'open':
status = 'open'
else:
status = 'closed'
# ignore retained (non-realtime) messages
if msg.retain:
return
if msg.topic == 'sound/g1/play':
play(msg.payload)
if msg.topic == 'sound/g1/speak':
play('dongq.mp3')
speak(msg.payload)
if msg.topic == 'sound/g1/announce':
play('chime.mp3')
speak(msg.payload)
if msg.topic == 'access/entrance/request':
if status == 'closed':
play('doorbell.mp3')
m = mqtt.Client()
m.on_connect = on_connect
m.on_message = on_message
m.connect("mqtt")
m.loop_forever()
==== /home/pi/respawn ====
#!/usr/bin/env python
#
# Tim Hawes
# April 2015
#
import argparse
import logging
import logging.handlers
import os
import signal
import subprocess
import sys
import time
process = None
hup_received = False
term_received = False
parser = argparse.ArgumentParser(description='Respawn an application.')
parser.add_argument('--name', type=str, dest='name', action='store')
parser.add_argument('--delay', type=int, dest='delay', action='store', default=1)
parser.add_argument('--min-backoff', type=int, dest='min_backoff', action='store', default=1)
parser.add_argument('--max-backoff', type=int, dest='max_backoff', action='store', default=60)
parser.add_argument('--reset-backoff-after', type=int, dest='backoff_reset_after', action='store', default=30)
parser.add_argument('--syslog', dest='syslog', action='store_true', default=False)
parser.add_argument('--debug', dest='debug', action='store_true', default=False)
parser.add_argument('command', nargs='*')
args = parser.parse_args()
def setup_logging(log_level=logging.INFO, syslog=True, stdout=False, ident=os.path.basename(sys.argv[0])):
logger = logging.getLogger()
logger.setLevel(log_level)
if syslog:
syslog_format_string = ident + "[%(process)d]: %(message)s"
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_USER)
syslog_handler.log_format_string = "<%d>%s"
syslog_handler.setFormatter(logging.Formatter(fmt=syslog_format_string))
syslog_handler.setLevel(log_level)
logger.addHandler(syslog_handler)
if stdout:
stream_format_string = "%(asctime)s %(message)s"
stream_handler = logging.StreamHandler(stream=sys.__stdout__)
stream_handler.setFormatter(logging.Formatter(fmt=stream_format_string))
stream_handler.setLevel(log_level)
logger.addHandler(stream_handler)
def run():
global process
start_time = time.time()
process = subprocess.Popen(args.command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
line = process.stdout.readline()
if line == '':
break
logging.info("< " + line.rstrip())
returncode = process.wait()
runtime = time.time()-start_time
process = None
if returncode == 0:
logging.info('exit=%d runtime=%.3f' % (returncode, runtime))
else:
logging.warning('exit=%d runtime=%.3f' % (returncode, runtime))
return returncode, runtime
def hup_handler(signum, frame):
global process
global hup_received
if process is not None:
logging.warning("received SIGHUP, sending SIGTERM to process")
hup_received = True
process.send_signal(signal.SIGTERM)
else:
logging.warning("received SIGHUP, but no process running")
def term_handler(signum, frame):
global process
global term_received
if process is not None:
logging.warning("received SIGTERM, sending SIGTERM to process")
term_received = True
process.send_signal(signal.SIGTERM)
else:
logging.warning("received SIGTERM, but no process running")
term_received = True
signal.signal(signal.SIGHUP, hup_handler)
signal.signal(signal.SIGTERM, term_handler)
ident = os.path.basename(sys.argv[0])
if args.name is not None:
ident = ident + "/" + args.name
if args.debug:
setup_logging(log_level=logging.DEBUG, stdout=True, syslog=False, ident=ident)
else:
setup_logging(log_level=logging.INFO, stdout=False, syslog=True, ident=ident)
if args.delay > args.min_backoff:
logging.debug('increasing min-backoff to match delay (%d)' % (args.delay))
args.min_backoff = args.delay
if args.min_backoff > args.max_backoff:
logging.debug('increasing max-backoff to match min-backoff (%d)' % (args.min_backoff))
args.max_backoff = args.min_backoff
exit_requested = False
backoff = args.min_backoff
logging.info("command: %r" % (args.command))
while True:
start_time = time.time()
returncode, runtime = run()
if term_received:
logging.debug("exited after SIGTERM")
break
if hup_received:
logging.debug("exited after SIGHUP, restarting immediately")
hup_received = False
continue
if returncode == 0:
if runtime > args.backoff_reset_after:
backoff = args.min_backoff
logging.debug('resetting backoff to %d' % (backoff))
else:
logging.debug('delaying for %d after a successful run' % (args.delay))
time.sleep(args.delay)
else:
logging.info('backing-off for %d seconds' % (backoff))
time.sleep(backoff)
backoff = min(backoff*2, args.max_backoff)
logging.debug('next backoff will be %d seconds' % (backoff))
logging.info('exiting respawn')
==== /home/pi/pico.sh ====
#!/bin/bash
pico2wave -l en-GB -w /tmp/pico.wav "$1"
play -q /tmp/pico.wav
rm -f /tmp/pico.wav
==== /home/pi/startup.sh ====
#!/bin/bash
sleep 3
_IP4=$(hostname -I | cut -d ' ' -f 1) || true
mpg123 -m -q /home/pi/sounds/indyboot.mp3
/home/pi/pico.sh "System boot complete. IP address is $_IP4"
===== Audio files =====
These live in /home/pi/sounds and can be wav or mp3. Generally prefer mp3. SCP new ones into here.
pi@squawk:~ $ ls -l /home/pi/sounds/
total 504
-rw-r--r-- 1 pi pi 9249 Oct 4 22:32 alert12.mp3
-rw-r--r-- 1 pi pi 8594 Oct 4 22:32 bingo.mp3
-rw-r--r-- 1 pi pi 59350 Oct 4 22:32 canttouchthis.mp3
-rw-r--r-- 1 pi pi 11703 Oct 4 22:32 cheese.mp3
-rw-r--r-- 1 pi pi 71457 Oct 4 22:32 commandcodesverified_ep.mp3
-rw-r--r-- 1 pi pi 7713 Oct 4 22:32 computerbeep_4.mp3
-rw-r--r-- 1 pi pi 21759 Oct 5 02:24 dong.mp3
-rw-r--r-- 1 pi pi 118725 Oct 5 01:48 doorbell.mp3
-rw-r--r-- 1 pi pi 44329 Oct 5 02:07 indyboot.mp3
-rw-r--r-- 1 pi pi 90825 Oct 4 22:32 scatman.mp3
-rw-r--r-- 1 pi pi 5486 Oct 4 22:32 touchdown.mp3
-rw-r--r-- 1 pi pi 6414 Oct 4 22:32 uhoh.mp3
-rw-r--r-- 1 pi pi 27584 Oct 4 22:32 whistle.mp3
===== Pico TTS =====
**This is probably not required as we now use AWS TTS service.**
mkdir ~/pico
cd ~/pico
wget http://incrediblepbx.com/picotts-raspi.tar.gz
tar -zxf picotts-raspi.tar.gz
sudo cp -R usr /
cd /usr/src/pico_build
sudo dpkg -i libttspico-data_1.0+git20110131-2_all.deb
sudo dpkg -i libttspico0_1.0+git20110131-2_armhf.deb
sudo dpkg -i libttspico-utils_1.0+git20110131-2_armhf.deb
rm -rf ~/pico
===== Hardware =====
{{ :photo_2016-10-10_16-35-17.jpg?direct&200|}}
The "squawk" unit lives above the IRC terminal, to the right of the main door in G1. It consists of a Raspberry Pi B and a TPA3116 based amplifier board.
There are four speakers connected, two per channel, located in each of the rooms on the ceiling. The speakers are 8 Ohm moisture resistant cheap ceiling speakers that have exceeded expectations and produce a surprisingly full sound. This opens up the potential for using them for background music in the future (e.g. via MPD).
==== Raspberry Pi ====
Standard pi other than having some wires soldered on the underside of the board to connect to the 3.5mm audio jack. Powered via the header rather than USB.
==== Amplifier ====
* TPA3116
* "100 W"
* 12-24v (more volts = more power)
* Volume pot
* 3-pin header input or 3.5mm socket
Audio is input via the 3-pin header.
Power is input on the centre screw terminal, with left and right speaker outputs either side.
==== Power Supply ====
The system runs off a 19.5v laptop PSU located in the ceiling space. This is directly connected into the amplifier board, which is rated up to 24v. A spur from this connects to a DC-DC buck converter, supplying the Raspberry Pi with 5v. The original mini converter used was overheating and melting the heatshrink. It's now been replaced with a larger type which so far is running relatively cool.
Care should be taken when re-wiring as connecting the laptop PSU accidentally reverse polarity, to the speaker terminals, or to the speakers themselves will likely result in one or more of those being instantly destroyed.
{{ :speakers.png?nolink|}}
==== Speakers ====
* 8 Ohm
* 80 W
* Spade terminals
* Dual cone
Two wired in parallel to each channel, as per the following diagram. This roughly equates to 4 Ohm per channel, which is within the 4-8 Ohm spec of the amplifier.
{{:photo_2016-10-10_16-39-03.jpg?direct&200 |}} {{:photo_2016-10-10_16-39-09.jpg?direct&200 |}} {{:photo_2016-10-10_16-39-10.jpg?direct&200 |}} {{:photo_2016-10-10_16-39-12.jpg?direct&200 |}}