====== 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 |}}