Smart Text to Speaker

All my devices are connected through MQTT either natively or through reverse engineered adapters. Some actors, sensors or logic blocks produce interesting events. Some of them I would like to produce nice scifi-like voice notifications virtually making the automation some sort of „notification assistant“.

For example if the watering system stopped due to malfunction or if some batteries of remote sensors are about to die. Also if the kids are tinkering with the window sensors is good to know. Finally a speaker was designed during rainy days to listen on a MQTT subject so that any of the ECUs or automation scripts can directly push a plain text message to the speaker.

Amazon Polly is then used by the Node.js script to synthesize the text using neural engine which makes it sound very nice. Some reverb and chorus effects are then applied with sox after a signature sound signalizes the notification. Also the door bell and other alerts is used by the same speaker to play hifi door bell sounds. I can place multiple speakers across the house. With zero effort I can get all notifications also in the basement which virtually scales this horizontally to a multi-room notification system.

I use a raspberry plus a class d amp module for driving a way to expensive wide band speaker. The main construction is done using oak wood. This was … lets say … „external requirements“ ;-)

All electronics is invisibly mounted on the bottom of the speaker so it still gets some cool air and maintenance is easy. I used this 100W mono amp from Amazon connected to some retired laptop power supply.

An illuminated 10cm crystal ball distributes the sound coming vertically up from the speaker 360° in the room. OK maybe that’s a bit too fancy. But the sound is awsome.

The code

const AWS = require('aws-sdk');
const Stream = require('stream');
const Speaker = require('speaker');

var mqtt = require('mqtt');
var client  = mqtt.connect('mqtt://mqtt');

var player = require('play-sound')(opts = {});
const Fs = require('fs');
const { spawn } = require( 'child_process' );
var exec = require('child_process').exec;

var path = "/home/pi/";
var queue = [];
var speaking = false;
var doorbelling = false;

const Polly = new AWS.Polly({
    signatureVersion: 'v4',
    region: 'eu-west-1'
});

process.env.AWS_ACCESS_KEY_ID = "__REMOVED__";
process.env.AWS_SECRET_ACCESS_KEY = "__REMOVED__";

spawn( 'amixer', [ 'set' ,'PCM', '--', '85%' ] );

player.play(path + 'startup.wav', function(err){});

client.on('connect', function () {
  client.subscribe('jarvis/#');
  client.publish('jarvis', 'online');
  console.log("Connected to MQTT");
});

client.on('message', function (topic, message) {

  if(topic == "jarvis/ping") {
    client.publish('jarvis/pong', '');
  }

  if(topic == "jarvis/speak") {
    console.log("Speak: " + message.toString());
    queue.push("<speak>" + message.toString() + "</speak>");
    processQueue(true);
  }

  if(topic == "jarvis/doorbell") {
    console.log("Play doorbell sound");
    if(!doorbelling)
      playDoorbellSound();
  }

  if(topic == "jarvis/alert") {
    console.log("Play alert sound");
  }

  if(topic == "jarvis/volume") {
    setTimeout(function (){
      queue.push("<speak>Setting volume to " + message.toString() + "%"  + "</speak>");
      spawn( 'amixer', [ 'set' ,'PCM', '--', message.toString() + '%' ] );
      processQueue(true);
    }, 1000);
  }

});

function playDoorbellSound() {
  doorbelling = true;
  var ls = spawn( 'play', [path + 'doorbell.wav'] );

  ls.on('close', code => {
    doorbelling = false;
    console.log("doorbell finished");
  });
}


function processQueue(beep) {
  if(queue.length == 0)
    return;
  if(speaking)
    return;
  var value = queue.shift();
  speaking = true;

  speak(beep, value, "ssml");
}

function speak(beep, text, type) {

  let params = {
    'TextType':type,
    'Text': text,
    'OutputFormat': 'mp3',
    'VoiceId': 'Emma',
    'Engine': 'neural'
  };

  Polly.synthesizeSpeech(params, (err, data) => {
    if (err) {
        console.log("synthesizeSpeech" + err.code)
    } else if (data) {
        if (data.AudioStream instanceof Buffer) {

          if(beep) {
            player.play(path + 'notify.wav', function(err){
              if (err) console.log(err);
            });
            setTimeout(function (){
              playStream(data.AudioStream);
            }, 1000);
          }
          else
            playStream(data.AudioStream);

          function playStream(stream) {
            Fs.writeFile(path + "speech.mp3", stream, function(err) {
              if (err)
                return console.log(err);
            });

            var ls = spawn( 'play', [ path + 'speech.mp3' ,'pad', '0', '1', 
            'treble', '+10', "chorus", "0.7", "0.9", "55.0", "0.4", "0.25", "2.0", "-t" ] );

            ls.on('close', code => {
                speaking = false;
                processQueue(false);
            });
          }
        }
     }
  })
}