Webradio, Sound- und Sprachausgabe an (entferntem) Raspberry mittels mpv, php, hifiberry amp+ und openHAB

Mein erster Raspberry, der auch die Hausautomation steuert, ist bereits als squeezebox via squeezelite tätig, nun möchte ich an anderer Stelle ebenso einen WLAN-Lautsprecher haben. Auch hier habe ich mich für einen Raspberry Pi 3 und für die Soundausgabe für eine HifiBerry AMP+ Soundkarte inkl. 2x25Watt-Verstärker entschieden. Daran habe ich zwei alte aber gute Boxen gehängt und das alles ergibt durchaus einen schönen Klang.

Zuerst habe ich versucht, via squeezebox-Actions zu arbeiten, da bin ich aber schnell an die Grenzen gestoßen. Teils spielte die Squeezebox direkt mit den Squeezebox-Actions übermittelte mp3s mehrfach ab und zudem, wenn hintereinander mehrere mp3s abgespielt werden sollen, wirds schwierig, weil er die Dateien sofort abspielt und nicht erst in eine Queue einreiht. Daher habe ich squeezelite wieder entfernt und ein eigenes Script – unter Rückgriff auf Shell-Programme zum Abspielen von Streams und Sounds – geschrieben.

Use-Cases:

  1. Das System soll einen Planungsfehler beim Hausbau ausgleichen und im Erdgeschoss die Klingelfunktion übernehmen. Die normale Klingel ist ungünstig platziert und daher bei Musik im Wohnzimmer nicht immer hörbar.
    Dazu ist ein Klingelsensor in openHAB eingebunden und die Boxen sollen dann (sofort) den Klingelton abspielen.
  2. Verschiedene weitere Events sollen per TTS ausgegeben werden. Dazu gehören informationen über eingehende Anrufe (wer ruft an) oder das automatische Abspielen der Anrufbeantworter-Nachrichten, wenn man ins Wohnzimmer geht.
    Auch hier gibt es verschiedene Events, welche jeweils eine entsprechende Priorität haben sollen.
  3. Webradio soll abspielbar sein und wenn das Radio durch oben aufgeführte Events unterbrochen wird, dann soll der Stream danach gleich wieder aufgenommen werden.
  4. Die Lautstärke des Webradios soll steuerbar sein und – falls Streams mp3s unterbrechen – soll jedes Objekt mit der dafür vorgesehen Lautstärke weitergespielt werden.
  5. Text soll ausgsprochen wiedergegeben werden können.

Meine Anforderungen sind daher:

  1. Der Pi muss Audiodateien und Stream übernehmen können
  2. Die Audiowiedergabe muss wieder gestoppt werden könnne (wichtig für Streams)
  3. Eine Warteschlange muss implementiert werden und nacheinander müssen die entsprechenden Files abgespielt werden
  4. Wird ein Stream durch ein MP3 unterbrochen, so soll nach Abspielen der MP3s in der Queue der Stream wieder geladen werden
  5. Die Lautstärke soll regelbar sein und auch persistiert werden
  6. TTS: Text muss übergebbar sein und als Audio ausgegeben und entspr. in die Queue eingereiht werden.

Was wird benötigt?

  1. Ein Raspberry Pi 3 ohne Netzteil ca. 40 €
  2. Eine microSD-Karte (ich habe 16GB gewählt) ca 10 €
  3. Ein Netzteil für den Hifiberry Amp+ 60W 12V 5A, ca 16 €; es versorgt auch den Raspberry, der braucht dafür kein Netzteil mehr
  4. Den Hifiberry amp+ Aufsatz (Soundkarte mit Verstärker), ca 60 €
  5. Beliebige passive Boxen (ich hatte noch alte rumstehen).

Summe ohne Boxen: 126 €.

Konfiguration des Ziel-Raspberry Pi

Das Installieren von HifiBerry amp+ sowie von Raspbian selbst werde ich hier nicht aufzeigen, das ist in folgenden Tutorials beschrieben:

Dann gehts los:

Grundsätzlich muss der Pi Kommandos entgegen nehmen können. Dies wäre per ssh automatisiert möglich oder – da ich Scripte gerne mit PHP umsetze – über einen kleinen Apachen, der am Ziel-Pi läuft.

Hierzu loggen wir uns als root ein oder führen alternativ alle Befehle mittels „sudo“ aus.

apt-get install php5 mpv

…installiert alle hierzu benötigten Pakete. Der Apache fährt nun bei jedem Systemstart mit hoch und ist über die IP des Pis auf Port 80 ansprechbar.

mpv ist als abspieltool dann über die Kommandozeile gleichermaßen ansprechbar.

Unter /var/www/html (Standard http-Dokumenten-Rootverzeichnis unter Raspbian) erstellen wir ein Verzeichnis „streampi“.

Dieses Verzeichnis übergeben wir komplett dem www-data User, damit der Webserver-Benutzer, unter dem der Apache ausgeführt wird, auch darauf schreibend zugreifen kann.

mkdir /var/www/html/streampi
touch /var/www/html/streampi/streampi.php
touch /var/www/html/streampi/streampi.db
touch /var/www/html/streampi/streampi.volume
chown -R www-data.www-data /var/www/html/streampi

streampi.db dient zum Speichern der Queue sowie des aktuellen / letzten Titels, daher muss diese anfangs keinen Inhalt beinhalten und es genügt, diese leer zu erstellen.

Die streampi.php befüllen wir mit folgendem Inhalt:

<?php

// Some configuration
$ttsScript = '/usr/bin/php ' . realpath(dirname(__FILE__)) . '/includes/tts.php';
$volumeFile = 'streampi.volume';
$amixer = '/usr/bin/amixer';
$mpv = '/usr/bin/mpv --really-quiet ';
$dbFile = 'streampi.db';
$telUrl = 'http://localhost/fritzpi/lookup.php?nr=';

// Handle input
$prio = (int) $_REQUEST['prio'];
$file = $_REQUEST['file'];
$type = strtolower($_REQUEST['type']);
$text = $_REQUEST['text'];
$tel = $_REQUEST['tel']; // addon for fritz box. replaces %NR% with looked up number that is passed via "tel" parameter
if (isset($tel) && (strlen($tel) > 0)) {
    $name = file_get_contents($telUrl . $tel);
    $text = str_replace('NUMMER', $name, $text);
}
if (!in_array($type, array('stream', 'stop', 'volume', 'tts'))) {
    $type = 'mp3';
}
if ($type == 'volume') {
    $volume = (int) $_REQUEST['value'];
    if ($volume < 0) {
        $volume = 0;
    } else if ($volums > 200) {
        $volume = 200;
    }
    file_put_contents($volumeFile, $volume);
    setVolume($volume);
} else {
    // TTS service called? Then we will handle this request first
    if ($type == 'tts') {
        if ($text != '') {
            $command = $ttsScript . ' ' . str_replace('ß', 'ss', $text);
            $ttsFile = shell_exec($command);
            // now we will proceed as if the tts file was given via parameter
            $file = $ttsFile;
            $type = 'mp3';
        } else {
            exit;
        }
    } else if ($type == 'stream') {
        // streams have always prio 999 (lowest priority)
        $prio = 999;
    }
    // Check if mpv is running already
    $mpvRunning = (substr_count(shell_exec('ps aux | grep mpv'), "\n") > 2);

    // Get Database
    $db = getDB();

    // Stop playing if necessary
    if ($type == 'stop') {
        exec('killall mpv');
        $db = array();
        storeDB($db);
        $mpvRunning = false;
    } else if ($file != '') {
        // playing already? cancel, if stream is played, and play then, otherwise queue
        $key = (str_pad($prio, 3, 0, STR_PAD_LEFT) . ( 32483067507 - time() ));
        $playItem = array(
            'file' => $file,
            'type' => $type,
            'prio' => $prio,
            'volume' => (int) $_REQUEST['volume'],
            'key' => $key
        );
        if ((($mpvRunning) && ($db['last']['type'] == 'stream')) || !$mpvRunning) {
            if ($mpvRunning) {
                exec('killall mpv');
                $lastItem = $db['last'];
                $db['queue'][$lastItem['key']] = $lastItem;
            }
            $db['last'] = $playItem;
            $mpvRunning = false;
        }
        // add to Queue. Play Method does sorting later...
        $db['queue'][$playItem['key']] = $playItem;
        storeDB($db);
        if (!$mpvRunning) {
            playNext();
        }
    }
}

function storeDB($db) {
    global $dbFile;
    file_put_contents($dbFile, serialize($db));
}

function getDB() {
    global $dbFile;
    $dbSource = unserialize(file_get_contents($dbFile));
    if (!is_array($dbSource)) {
        $db = array('last' => array(), 'queue' => array());
    } else {
        return $dbSource;
    }
    return $db;
}

function playNext() {
    global $mpv;
    $db = getDB();
    $queue = $db['queue'];
    // sort queue
    ksort($queue);
    $playItem = array_shift($queue);
    if ($playItem['key'] != '') {
        // Set volume level
        if ($playItem['volume'] > 0) {
            setVolume($playItem['volume']);
        } else {
            setVolume(getVolume(false));
        }
        // Modify database
        $db = array(
            'last' => $playItem,
            'queue' => $queue
        );
        storeDB($db);
        // stream? play in background!
        if ($playItem['type'] == 'stream') {
            exec($mpv . ' ' . $playItem['file'] . ' > /dev/null 2>/dev/null &');
        } else {
            exec($mpv . ' ' . $playItem['file']);
            playNext();
        }
    }
}

function getVolume($fromFileOnly = false) {
    global $volumeFile;
    if (isset($_REQUEST['volume']) && $fromFileOnly) {
        $volume = $_REQUEST['volume'];
    } else {
        $volume = file_get_contents($volumeFile);
    }
    if (!($volume >= 0) && !($volume <= 200)) {
        $volume = 100;
    }
    return $volume;
}

function setVolume($value) {
    global $amixer;
    $cmd = $amixer . ' sset Master ' . $value . ' unmute';
    exec($cmd);
}

Damit aber der Apache Audio ausgeben darf muss der www-data-Benutzer noch der Gruppe „audio“ hinzugefügt werden und der Apache muss dann neu gestartet werden, damit die Änderung auch berücksichtigt werden kann:

usermod -aG audio www-data
service apache2 restart

Wichtig ist noch, dass das Script nur sehr rudimentär ist (im Moment) und die übergebenen Parameter nicht geprüft werden, bevor sie an exec übergeben werden. Theoretisch kann man damit auch etwas kaputt machen, ich für mich sehe aktuell zumindest in meinem Bereich kein großes Problem, da das Netz von außen geschützt ist und niemand drauf zugreifen kann. Hier muss man ggf. noch einen Zugriffsschutz draufpacken.

Konfiguration des Smarthome-Raspberry Pi

Auf dem openHAB-Pi nutzen wir die http-Actions um auf dem Ziel-Pi das Script anzustoßen.

MP3-Klingel

Im Beispiel hier ist das in meine Klingel-Anlage integriert, eine Rule löst aus, wenn der Klingelsensor abschlägt und dieser stößt dann per HTTP-GET-Request die Klingel an.

rule "Türklingel wurde gedrückt"
when 
    Item klingel changed from OFF to ON
then
    logInfo('rules','klingel wurde ausgelöst')
    sendHttpGetRequest('http://<raspberry_ip_address>/streampi/streampi.php?file=/home/pi/klingel.mp3');

Webradio

Eine Auswahl an ausgewählten Radiostreams ist als Webradio in meine Sitemap integriert.

items-Datei

Number    webradioKuecheEG        "Webradio Küche EG" <network>
Number    webradioKuecheEGvolume  "Webradio Küche Volume [%d]" <network>

sitemap-Datei

    Frame label="Squeezebox-Player" {
        Selection item=squeezeboxEG mappings=[1="Aus",2="Radio 2Day München",3="SWR3",4="Bayern 1",5="Charivari"]
        Setpoint item=webradioKuecheEGvolume minValue=0 maxValue=200 step=10
    }

rules-Datei

rule "Webradio Raspberry 2 geschalten"
when
    Item webradioKuecheEG received command
then
    switch(receivedCommand) {
        case 1: {
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stop');
        }
        case 2: {
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stop');
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stream&file=http://radio2day.ip-streaming.net/radio2day.m3u');
        }
        case 3: {
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stop');
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stream&file=http://mp3-live.swr3.de/swr3_m.m3u');
        }
        case 4: {
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stop');
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stream&file=http://streams.br.de/bayern1nbopf_2.m3u');
        }
        case 5: {
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stop');
            sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=stream&file=http://www.vtuner.com/vtunerweb/mms/m3u33003.m3u');
        }
    }
end

rule "Lautstärkenänderung Raspberry 2"
when
    Item webradioKuecheEGvolume received update
then
    sendHttpGetRequest('http://<IP-ADDRESS>/streampi/streampi.php?type=volume&value='+ webradioKuecheEGvolume.state
end

TTS-Service anbinden

Zur Sprachausgabe habe ich ja bereits gebloggt, und genau das Script verwenden wir einfach wieder und legen es in einem Unterordner „includes“ ab.

Wenn alle Dateien platziert wurden und das mp3-Ablageverzeichnis konfiguriert wurde am besten dem Unterverzeichnis includes ebenso www-data als Besitzer geben, denn es ist nun ja der Apache-Benutzer, der schreibenden Zugriff benötigt:

chown -R www-data.www-data /var/www/http/streampi/includes

Wenn dann in einer Rule Text ausgegeben werden soll, ist das ebenso einfach möglich wie das Abspielen von Streams oder MP3s, allerdings verwenden wir – um bei Leerzeichen usw. in den Sätzen nicht in Probleme zu laufen – hier nicht die GET sondern die POST Methode.

sendHttpPostRequest('<IP-ADDRESS>/streampi/streampi.php','application/x-www-form-urlencoded','type=tts&prio1&volume=140&text=Achtung eine wichtige Durchsage')

Zusätzlich zeigt das Beispiel auf, wie mit Prioritäten und Lautstärken gearbeitet werden kann. Durch Angabe von Prio=1 wird das File als nächstes abgespielt, auch wenn weitere in der Queue sind. Zudem wird die Lautstärke unabhängig von der über openhab konfigurierten Lautstärke auf 140 gesetzt. Dies gilt natürlich nur für diesen Satz, danach wird die Lautstärke automatisch wieder zurück gesetzt.

Damit kann man nun vieles Umsetzen: Ansagen, wenn das Telefon klingelt (ggf. wer dran ist), Erinnerungen an Termine, und und und. Die Möglichkeiten sind eher unbegrenzt.

Viel Spaß damit!

2 Antworten auf „Webradio, Sound- und Sprachausgabe an (entferntem) Raspberry mittels mpv, php, hifiberry amp+ und openHAB“

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.