Freitag, 24. August 2012

HomeAutomation SoundServer (Teil 2)

Im ersten Post habe ich bereits mein Bedürfnis für die Sound-Integration in die HomeAutomation und meinen Enthusiasmus über die Netzwerkfähigkeit von EsounD zum Ausdruck gebracht. Im folgenden möchte ich meine rudimentären Perl-Kenntnisse zur Schau stellen ;-)

Die dargestellten Scripte bilden sozusagen das Herzstück der Interaktion mit den diversen EsounD Servern auf den Netzwerk Clients, da sie über das Webinterface angesteuert werden können. Aber auch meine Cron-Scripte (Weckzeitplaner, etc.) greifen darauf zurück.


EsounD ist eigentlich selbst ein Soundserver. Sowas benötigte man "früher" um unter Linux mehrere Audiosignale gleichzeitig auf die Soundkarte zu schicken - das alte OSS unterstützte kein Mixing. Inzwischen implementiert Alsa mit dem DMIX Plugin diese Funktion.

Lange Zeit unterstützte das Gnome-Projekt EsounD, doch inzwischen hat PulseAudio den ESD abgelöst. PulseAudio bringt nativ bereits Protokollunterstützung für das alte ESD mit und wäre ohne Zweifel ein würdiger Nachfolger in meiner ESD-Installation, wären da nicht die vielen Abhängigkeiten von anderen Softwarepaketen. (siehe auch meine "final thoughts")

Benötigt werden die Pakete

esound-common
esound-clients

Wenn zusätzlich am Server Audio ausgegeben werden soll (mein Server steht im Wohnzimmer) wird noch ein lokaler ESD-kompatibler Soundserver benötigt. Entweder

esound

oder

pulseaudio
pulseaudio-esound-compat

Bei meinen Experimenten mit PulseAudio musste ich allerdings die "libesd" patchen, da PulseAudio seine Socket im Verzeichnis "/tmp/.esd-<UID>" ablegt, ESD aber wahrscheinlich unter einer anderen UID laufen wird. Mit der Umgebungsvariable AUDIODEV lässt sich mit meinem Patch die UID des PulseAudio Prozesses an die ESD-Clients übergeben.

Für die Soundausgabe habe ich eine Reihe von Wrapper Scripts geschrieben, das Wichtigste ist wahrscheinlich "snd-play".

#!/usr/bin/perl -w
#
# snd-play kueche /usr/local/bin/mpg123 -b 4096 -y -q \
#          "http://rbb.ic.llnwd.net/stream/rbb_radioeins_mp3_m_b"
#

use strict;
use POSIX ":sys_wait_h";

# UID of PulseAudio
$ENV{AUDIODEV} = '108';

my $esdopt = '';
my $esdctl = '/usr/bin/esdctl';
my $esdlib = '/usr/lib/esound/libesddsp.so.0 libesd.so.0';

my $usage = "Usage: $0 wohnzimmer|schlafzimmer|kueche|alex|all \n";

my ($pid, $chan);

if(!defined($chan = shift))
{
 print $usage;
 exit 1;
}
else
{
 if($chan !~ /^wohnzimmer$|^schlafzimmer$|^kueche$|^alex$|^all$/)
 {
  print $usage;
  exit 1;
 }
}
if($#ARGV < 0)
{
 print "ERROR:  is missing\n";
 print $usage;
 exit 1;
}

if( $chan =~ /all/ )
{
 foreach ( 'wohnzimmer', 'schlafzimmer', 'kueche' ){ &start_fork($_); }
}
else{ &start_fork($chan); }

exit 0;

sub start_fork
{
 $_ = shift;
 if   ( /alex/         ){ $esdopt = '192.168.1.100:16001'; }
 elsif( /schlafzimmer/ ){ $esdopt = '192.168.1.248:16000'; }
 elsif( /kueche/       ){ $esdopt = '192.168.1.249:16000'; }

 ####################################################################################
 #
 # this is the parent's process
 #
 ####################################################################################
 if( $pid = fork )
 {
  $SIG{KILL} = sub{ kill 9, $pid; };
  my @id=();
  if( $esdopt ){ $esdopt = ' "-s" ' . $esdopt; }

  while( !$id[0] )
  {
   @id = grep(s/^player\s*([0-9]+)\s*name\s*=.*$pid\s*$/$1/, `${esdctl}${esdopt} allinfo`);
   if(waitpid($pid, &WNOHANG)!=0){ print "ERROR: \"@ARGV\" cannot be used with EsounD...\n"; exit 1; }
  }
 }

 ####################################################################################
 #
 # here comes the child
 #
 ####################################################################################
 elsif(defined($pid))
 {
  close(STDOUT);
  close(STDERR);

  # define environment variables for EsounD and set the
  # /dev/dsp redirection via LD_PRELOAD
  $ENV{ESPEAKER} = $esdopt;     # esd server
  $ENV{ESDDSP_NAME} = $$;       # child's pid as stream name
  $ENV{LD_PRELOAD} = $esdlib;   # libesd library path

  # execute the command line in the child process
  exec { $ARGV[0] } @ARGV;
 }
}

Natürlich will man auch wissen, was gerade spielt. Da ich jedem Stream die PID der abspielenden Anwendung mitgebe, kann man später aus der Prozessliste herauslesen, ob es sich um einen MP3 Stream handelt, oder ob jemand per MBROLA ein Buch vorliest.

Den Status ermittelt "snd-status":

#!/usr/bin/perl -w
#
# snd-status [raum]
#
# Ausgabe in der Form:
#  raum stream_id stream_pid volume_links volume_rechts
#

use strict;
$ENV{AUDIODEV} = '108';

my $esdopt = '';
my $esdctl = '/usr/bin/esdctl';
my $usage  = "Usage: $0 [wohnzimmer|schlafzimmer|kueche|alex]\n";
my (%mixer, $chan, $id);

if(defined($chan = shift))
{
 if($chan !~ /^wohnzimmer$|^schlafzimmer$|^kueche$|^alex$/)
 {
  print $usage;
  exit 1;
 }
 $_ = $chan;
 if   ( /alex/         ){ $esdopt = '"-s" 192.168.1.100:16001'; }
 elsif( /schlafzimmer/ ){ $esdopt = '"-s" 192.168.1.248:16000'; }
 elsif( /kueche/       ){ $esdopt = '"-s" 192.168.1.249:16000'; }
}
else
{
 foreach ( 'wohnzimmer', 'schlafzimmer', 'kueche' ){ print `$0 $_`; }
 exit 0;
}

foreach (split(/\n/, `$esdctl $esdopt allinfo`))
{
 if( /^player\s*([0-9]+)\s*(\S*)\s*=\s*(.*)$/)
 {
  $mixer{$1}{$2}=$3;
 }
}

foreach $id (keys %mixer)
{
 if( $mixer{$id}{name} =~ /^[0-9]+$/ ){ print "$chan $id $mixer{$id}{name} $mixer{$id}{left} $mixer{$id}{right}\n"; }
}

exit 0;

Was man noch so alles mit EsounD treiben kann zeigt "snd-fade", welches aktive Streams ausblendet und wieder einblenden kann - z.B. während einer laufenden Radiosendung um einen eingehenden Telefonanruf zu "durchzusagen".

#!/usr/bin/perl -w
#
# snd-fade in|out [raum][stream_id]
#

use strict;
$ENV{AUDIODEV} = '108';

my $esdopt = '';
my $esdctl = '/usr/bin/esdctl';
my $usage  = "Usage: $0 in|out [wohnzimmer|schlafzimmer|kueche|alex] [id]\n";

my (%mixer, @idlist, $id, $dir, @chan, $chan, $done1, $done2);

if(defined($dir = shift))
{
 if(defined($chan = shift))
 {
  if($chan !~ /^wohnzimmer|^schlafzimmer$|^kueche$|^alex$/)
  {
   print $usage;
   exit 1;
  }
  $_ = $chan;
  if   ( /alex/         ){ $esdopt = '"-s" 192.168.1.100:16001'; }
  elsif( /schlafzimmer/ ){ $esdopt = '"-s" 192.168.1.248:16000'; }
  elsif( /kueche/       ){ $esdopt = '"-s" 192.168.1.249:16000'; }
 }
 $chan[0] = 'left';
 $chan[1] = 'right';
 if($dir eq "in"){ $dir = "+16"; }else{ $dir = "-16"; }
}
else
{
 print $usage;
 exit 1;
}

foreach (split(/\n/, `$esdctl $esdopt allinfo`))
{
 if( /^player\s*([0-9]+)\s*(\S*)\s*=\s*(.*)$/)
 {
  $mixer{$1}{$2}=$3;
 }
}

if(defined($id = shift))
{
 if(!defined($mixer{$id}{format})){ print "ERROR: unknown id($id)!!!\n"; exit 1; }
 $idlist[0] = $id;
}
else
{
 @idlist = keys %mixer;
}

$done1 = 1;
$done2 = 0;
while(!$done2)
{
 $done1 = 1;
 foreach $chan ( @chan )
 {
  foreach $id ( @idlist )
  {
   $mixer{$id}{$chan} = $mixer{$id}{$chan} + $dir;
   if( $mixer{$id}{$chan} < 0   ){ $mixer{$id}{$chan} = 0; next; }
   if( $mixer{$id}{$chan} > 256 ){ $mixer{$id}{$chan} = 256; next; }
   $done1 = 0;
   `$esdctl $esdopt panstream $id $mixer{$id}{left} $mixer{$id}{right}`;
   select(undef, undef, undef, 0.125);
  }
 }
 if( $done1 == 0 ){ $done2 = 0; }else{ $done2 = 1; }
}

exit 0;

Mit diesem Grundgerüst lassen sich schon einfache Zeit-Steuerungen per Cron realisieren:

# am Wochenende Radio1 zum Frühstück einschalten
59 08 * * 6-7 if [ -z "`snd-status kueche`" ]; then snd-play kueche mpg123 -b 4096 -y -q "http://rbb.ic.llnwd.net/stream/rbb_radioeins_mp3_m_b" ; fi

# Wochentags mit NDR2 wecken
35 06 * * 1-5  if [ -z "`snd-status schlafzimmer`" ]; then snd-play schlafzimmer mpg123 -b 4096 -y -q "http://ndrstream.ic.llnwd.net/stream/ndrstream_ndr2_hi_mp3" ; fi

Natürlich kann man das Wecken mit einem nachfolgenden "snd-mute schlafzimmer && snd-fade in schlafzimmer" noch sanfter gestalten ;-)

Die Web-Scripte mag ich an dieser Stelle nicht veröffentlichen, da sie meine dilettantische Script-Programmierung noch mehr zum Ausdruck bringen würden. Wer nett fragt, bekommt sie evtl. dennoch zu sehen...