[Neues Modul] BOSE SoundTouch

Begonnen von dominik, 05 Januar 2016, 22:28:40

Vorheriges Thema - Nächstes Thema

Prof. Dr. Peter Henning

OK,next step.

Define a directory for miniDLNA where the "Music" files reside, and let your BOSE box find the miniDLNA.

Then, store a file called "speech.mp3" in this directory.

Where do you get this file? Well, for the moment we try a simple approach, and fetch this from Google. Enter into your 99_myUtils.pm the code


##############################################################################
#
#  Obtain a speechlet file from Google
#
##############################################################################

sub tts_google($$){
  my ($text,$file) = @_;
  #-- obtain speechlet
  system("wget -q --user-agent='Mozilla/5.0' \'http://translate.google.de/translate_tts?ie=UTF-8&tl=de&client=tw-ob&q=$text\' -O $file.mp3");

and then from your FHEM command line excute
{tts_google("This is my first test of speech on BOSE","<directoryname>/speech")}
This should write the speech.mp3 into that directory.

Note added in proof: In the code above you should replace tl=de by the language you want to have, say tl=fr or tl=en

You might have trouble in the beginning, maybe because the directory is not writable from FHEM, or maybe mniDLNA is not configured properly yet. Later on, I'll give you a code to control miniDLNA from FHEM - for the moment it should be enough to start a miniDLNA rescan by hand, simply by restarting the miniDLNA process. If then, on your server, you look at http://<ip-address>:8200 you should see the miniDLNA running and showing at least one audio file - your speech.mp3

If everything is ok, the following things should apply:

1. speech,mp3 has been obtained from google, and it contains the voice transcript of your sentence (check !)
2. speech.mp3 is found by miniDLNA
3. miniDLNA is found by the BOSe boxes
4. By issueing "set <BOSE Box> playTrack speech" to FHEM, you should hear the Google voice.

Regards

pah

elmattt

ok, minidlna is running, there is one audio file in it, and i can play it with the soundtouch app !!!
everything for you list from 1 to 4 is ok ;)

Prof. Dr. Peter Henning

#632
Fine. Next step is to set up code that take a longer text, splits it into chunks, passes these to Google (i prefer Amazon Polly), assembles them together, puts them into miniDLNA, caches them (such that the second identical message is not passed to Google or Amazon) or rather looks up the cache if the speechlet is already present. And then play it.

Also, this allows to put a string :3-digit-number: into any text. This string is replaced automatically by one of the predefined speech messages (I call them phrases) - they may be obtained by the subroutine tts_allnew() Note: They should be obtained from Google in your case.

Right now I have other things to do, so I post the code without commenting on it. MAybe I'll be back online tomorrow afternoon


Regards

pah



use Paws; # only for Polly
use Text::Iconv;
use MP3::Info;
use DBI;


my @ttsFiles=();

##############################################################################
#
#  TTS phrases
#
##############################################################################

my %ttsPhrases = (
"001_jeanniehilfe" => "Hallo, ich bin Jeannie. Wie kann ich Dir helfen?",
"002_ok" => "Ookeh",
"003_tml" => "Es tut mir leid, das habe ich nicht verstanden",
"011_hoftuer_sichern" => "Die Hoftür wird gesichert.",
"012_hoftuer_gesichert" => "Die Hoftür ist gesichert.",
"013_hoftuer_entsichern" => "Sicherung der Hoftür aufgehoben.",
"014_hoftuer_ungesichert" => "Die Hoftür ist nicht gesichert.",
"015_haustuer_sichern" => "Die Hauseingangstür wird gesichert.",
"016_haustuer_gesichert" => "Die Hauseingangstür ist gesichert.",
"017_haustuer_entsichern" => "Sicherung der Hauseingangstür aufgehoben.",
... several more lines of this type
"121_dachfoffen" => "Mindestens ein Dachfenster ist offen."
);

##############################################################################
#
#  Obtain new speechlets for all phrases
# aws_access_key_id=xxxxxxxxxxxxxxxxxxxxxxxxxx
# aws_secret_access_key=xxxxxxxxxxxxxxxxxxxxxxxx
#
##############################################################################

sub tts_allnew(){
  my $polly = Paws->service('Polly', region=>'eu-central-1');
 
  foreach my $file (keys %ttsPhrases) {
    my $converter = Text::Iconv->new("utf-8", "iso-8859-1");
    my $text = $converter->convert($ttsPhrases{$file});
    my $ssmltext="<speak>$text</speak>";
 
    #-- obtain speechlet 
    my $res = $polly->SynthesizeSpeech(
       VoiceId => 'Marlene',
       TextType=> 'ssml',
       Text => $ssmltext,
       OutputFormat => 'mp3');
     
    #-- write stream
    my $mp3;
    open($mp3, '>', "/home/fhem/ttsNew/".$file.".mp3");
    binmode($mp3);
    print $mp3 $res->AudioStream;
    close($mp3);
  }
}
 
#my %ttsMaxChar      = ("Google"     => 100,
#                       "VoiceRSS"   => 300,
#                       "SVOX-pico"  => 1000,
#                       "Amazon-Polly" => 3000
#                       );

#############################################################################
#
#  speak as the central output routine
#
#############################################################################
                                       
sub speak($$){
  my ($name,$text) = @_;
 
  fhem("setreading SPEAK $name.out ".substr($text,0,12));
  fhem("setreading SPEAK lastoutput $name");
  fhem("setreading SPEAK length ".length($text));
  fhem("setreading SPEAK duration ".length($text)/9);
 
  #-- Telegram, therefore distant
  if($name =~ /Telegram.*/){
    fhem193Cmd("{telegramRecognition('botreply: ".$text."')}");
    fhem("setreading SPEAK Telegram.out ".substr($text,0,12));
  #-- Digital Audio Device
  }elsif($name =~ /(^RPI)|(^Rpi)|(Sound)/){
    speakDAD($name,$text,"p");
  #-- Tablet
  }elsif($name =~ /(^Tab)|(HTC)/){   
    speakTablet($name,$text);
  #-- HTC,SZ therefore reroutable ==> check here
  }elsif($name =~ /SZ/){
    if( Value("BOSE_50338B343509") eq "playing" ){
      speakDAD("SoundTouch1.OG",$text,"p");
    }else{
      speakTablet("HTC.OG",$text);
    }
  #-- WZ,MSG therefore reroutable ==> check in 193
  }elsif($name =~ /(WZ)|(MSG)|(Msg)/){
    if( Value("BOSE_C4F312DD64C7") eq "playing" ){
      speakDAD("SoundTouch.EG",$text,"p");
    }else{
      speakTablet("Tab1.EG",$text);
    }
  }else{
    Log 1,"[speak] Error, Device $name unknown (text=$text)";
  }
}


#############################################################################
#
#  speak for digital audio devices
#
#############################################################################

sub speakDAD{
  my ($name,$text,$ttsResource) = @_;
  my ($daddev,$vol,$cmd);
  my $dadtype="";
 
  #-- default voice
  $ttsResource = "p"
    if(!defined($ttsResource));
  my $ttsMaxChar = 3000;
 
  #-- output device
  if($name =~ /R(P|p)(I|i).*DG/){
    $dadtype = "RPI";
    $daddev  = "RpiAudio.DG";
    $vol     = 25;
  }elsif($name =~ /R?(P|p)(I|i).*Meson/){
    $dadtype = "RPI";
    $daddev  = "RpiAudio.Meson";
    $vol     = 25;
  }elsif($name eq "SoundTouch.EG"){
    $dadtype = "BOSE";
    $daddev  = "BOSE_C4F312DD64C7";
    $vol     = 25; 
  }elsif($name eq "SoundTouch.EZ"){
    $dadtype = "BOSE";
    $daddev  = "BOSE_2C6B7D4B53A4";
    $vol     = 25;
  }elsif($name =~ /SoundTouch.?.OG/){
    $dadtype = "BOSE";
    $daddev  = "BOSE_50338B343509";
    $vol     = 25;
  #-- unknown DAD
  }else{
    $dadtype = "unknown";
    $daddev  = "";
    $vol     = 0;
    Log 1,"[speakDAD] called with unknown device name $name (text=$text)";
    return;
  }
 
  #-- make it sound better
  $text =~ s/(\d)\.(\d)/$1,$2/g;
  $text =~ s/Hallo, ich bin Jeannie. Wie kann ich Dir helfen\?/:001:/;
  $text =~ s/^OK$/:002:/;
  $text =~ s/^Es tut mir leid, das habe ich nicht verstanden$/:003:/;
  $text =~ s/Jeannie/dschini/;
  $text =~ s/Jacqueline/dschacklihn/;
  $text =~ s/^OK/Ookeeh/;
  $text =~ s/\:P\:/<break time="1s"\/>/g;
 
  fhem("setreading SPEAK $name.out ".substr($text,0,12));
 
  #-- settings
  #my $joiner     = "sox";
  my $joiner     = "mp3wrap";
  my $ttspath    = "/home/fhem";
  my $ttsDir     = "/home/fhem/tts/";
  my $ttsCDir    = "/home/fhem/ttsCache/";
  my $ttsTarget  = "speech";
 
  #-- delete old file to outsmart miniDLNA caching
  unlink $ttsDir.$ttsTarget.".mp3";
  Log 1, "[speakDAD] File ".$ttsDir.$ttsTarget.".mp3 could not be deleted"
    if(-e $ttsDir.$ttsTarget.".mp3");
 
  #-- obtain list of prerecorded files
  opendir (DIR, $ttsDir);
  while (my $ttsFile = readdir(DIR)) {
    next if ($ttsFile !~ m/\.mp3$/);
      push(@ttsFiles,$ttsFile);
    }
  closedir(DIR);
 
  #{Dumper(Text2Speech_SplitString(["Das ist ein Test"],10," ",0,"al"))}
  #{Dumper(Text2Speech_SplitString(["Das ist ein Test, in dem ein kleiner Teil <amazon:effect name=\"whispered\">geflüstert</amazon:effect> wird"],10," ",0,"al"))}
  #-- split text at word boundaries
  my @words      = split(' ', $text);
  my @speechlets = ();
  my $speechlet;
   
  my $i=0;
  my $j=0;
  while($i<int(@words)){
    last
      if( !$words[$i] );
    #-- word does not contain prerecorded filename, simply append words
    if( $words[$i] !~ /^\:(.*)\:$/ ){
      $speechlet="";
      while( (length($speechlet) + length($words[$i])) < $ttsMaxChar && $i<int(@words) ){
        $speechlet .= $words[$i]." ";
        $i++;
      }
      push(@speechlets,$speechlet);
      $j++;
      my $ttsKey   = md5_hex($ttsResource."|".$speechlet);
      my $ttsCFile = $ttsTarget."_".$ttsKey;
      my $found    = tts_dblog($ttsKey,$speechlet);

      #-- check if already exists     
      #if( -e $ttsCDir.$ttsCFile.".mp3" ) {
      if( $found == 1 ){
        Log 1,"[speakDAD] Speechlet '".$speechlet."' already in cache as ".$ttsCDir.$ttsCFile.".mp3";
      }else{
        if( $ttsResource =~ /p.*$/ ){
          tts_polly($speechlet,$ttsCDir.$ttsCFile);
        }else{
          tts_google($speechlet,$ttsCDir.$ttsCFile);
        }
        Log 1,"[speakDAD] Speechlet '".$speechlet."' obtained as ".$ttsCDir.$ttsCFile.".mp3";
      }
      $cmd .= $ttsCDir.$ttsCFile.".mp3 ";
     
    #-- word is prerecorded filename
    }else{
      my $name   = $1;
      my $ttsPFile = "";
      foreach my $k (@ttsFiles){
        if( $k =~ /$name/ ){
          $ttsPFile = $k;   
          last;
        }
      } 
      if($ttsPFile ne ""){
        $cmd .= $ttsDir.$ttsPFile." ";
        Log 5,"[speakDAD] Filename $name resolved as ".$ttsDir.$ttsPFile;
      }else{
        Log 5,"[speakDAD] Filename $name not resolved";
      }
      $i++; 
    }
  }
 
  #-- join speechlets and auxiliary files
  if( $joiner eq "sox" ){
    $cmd = "sox ".$ttsDir."silence_48_22050_1.mp3 ".$cmd." ".$ttsDir.$ttsTarget.".mp3" ;
    system($cmd);
    #-- write MP3 tags
    $cmd ="id3tag -sfhemspeech -aFHEM -Cdesc -Aalbum ".$ttsDir.$ttsTarget.".mp3 > /dev/null";
    system($cmd);
  }elsif( $joiner eq "mp3wrap" ){
    $cmd = "mp3wrap ".$ttsDir.$ttsTarget.".mp3 ".$ttsDir."silence_48_22050_1.mp3 ".$cmd." > /dev/null";
    system($cmd);
    #-- write MP3 tags
    $cmd ="id3tag -sfhemspeech -aFHEM -Cdesc -Aalbum ".$ttsDir.$ttsTarget."_MP3WRAP.mp3 > /dev/null";
    system($cmd);
    #-- move file to final location
    $cmd = "mv ".$ttsDir.$ttsTarget."_MP3WRAP.mp3 ".$ttsDir.$ttsTarget.".mp3";
    system($cmd);
  }else{
    Log 1,"[speakDAD] No MP3 joiner defined, playing ".substr($cmd,0,index($cmd,' '));
    #-- todo: take out first file as filename and copy to speech.mp3
    $cmd = "cp ".substr($cmd,0,index($cmd,' '))." ".$ttsDir.$ttsTarget.".mp3";
    system($cmd);
  }
  #-- get duration
  Log 1,"[speakDAD] duration of final file is ".tts_duration($ttsDir.$ttsTarget.".mp3")." s";

  #-- play it
  if( $dadtype eq "BOSE" ){
    BOSERepsas($daddev);
    fhem("set $daddev volume $vol");
    fhem("set $daddev playTrack fhemspeech");
  }elsif( $dadtype eq "RPI" ){
    fhem("set $daddev volume $vol");
    fhem("set $daddev origin speech");
  } 
}

##############################################################################
#
#  Write into speechlet database
#
##############################################################################

sub tts_dblog($$){
  my ($key,$text) = @_;
  #-- Create database by
  #  sqlite3 tts.db "CREATE TABLE tts (key INTEGER PRIMARY KEY, count INT, time INT, content TEXT);"
  #  chown fhem:dialout tts.db
  #--
  my $path = "/home/fhem";
  my $dbargs = {AutoCommit => 0, PrintError => 1};
  my $dbh = DBI->connect("dbi:SQLite:dbname=".$path."/tts.db", "", "", $dbargs);
 
  #-- check existence
  my ($res) = $dbh->selectrow_array("SELECT EXISTS ( SELECT 1 FROM tts WHERE key='".$key."')");
  #-- exists
  if( $res == 0 ){
    #-- insert a line
    $dbh->do("INSERT INTO tts (key,count,time,content) VALUES ('".$key."','1','".time()."','".$text."');");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  #-- does not exist
  }else{
    #-- modify
    $dbh->do("UPDATE tts SET count = count + 1 WHERE key='".$key."';");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  }
  $dbh->commit();
  $dbh->disconnect();
  return $res;
}

##############################################################################
#
#  Display speechlet database
#
##############################################################################

sub tts_dbshow(){
  #--
  my $path   = "/home/fhem";
  my $dbargs = {AutoCommit => 0, PrintError => 1};
  my $dbh    = DBI->connect("dbi:SQLite:dbname=".$path."/tts.db", "", "", $dbargs);
  my $ret    = "";

  #-- print rows
  my $res = $dbh->selectall_arrayref("SELECT key,count,time,content FROM tts;");
  foreach my $row (@$res) {
    my ($keyr,$countr,$timer,$textr) = @$row;
    $ret .= sprintf("%s %d %d %s\n",$keyr,$countr,$timer,$textr);
  }
  $dbh->disconnect();
  return $ret;
}

##############################################################################
#
#  Purge speechlet database
#
##############################################################################

sub tts_dbpurge($$){
  my ($deltime,$mincount) = @_;
  #--
  my $path   = "/home/fhem";
  my $dbargs = {AutoCommit => 0, PrintError => 1};
  my $dbh    = DBI->connect("dbi:SQLite:dbname=".$path."/tts.db", "", "", $dbargs);

  if( defined($deltime) && $deltime > 0){
    my $earliest = time() - 86400*$deltime;
    $dbh->do("DELETE from tts where time < ".$earliest.";");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  }elsif( defined($mincount) && $mincount > 0){
    $dbh->do("DELETE from tts where count < ".$mincount.";");
    if ($dbh->err()) {
      Log 1,"[tts_dblog] $DBI::errstr";
    }
  }
  $dbh->disconnect();
}

##############################################################################
#
#  Obtain a speechlet file from Amazon Polly
#
##############################################################################

sub tts_polly($$){
  my ($texti,$file) = @_;
    #-- we may have problems with umlaut characters
  my $converter = Text::Iconv->new("utf-8", "iso-8859-1");
  my $text = $converter->convert($texti);
 
  #<break time="1s"/>
  my $ssmltext = '<speak>'.$text.'</speak>';
 
  #-- obtain speechlet
  my $polly = Paws->service('Polly', region=>'eu-central-1');
  my $res = $polly->SynthesizeSpeech(
     VoiceId => 'Marlene',
     TextType=> 'ssml',
     Text => $ssmltext,
     OutputFormat => 'mp3');

  #-- write stream
  my $mp3;
  open($mp3, '>', $file.".mp3");
  binmode($mp3);
  print $mp3 $res->AudioStream;
  close($mp3);
}

##############################################################################
#
#  Obtain a speechlet file from Google
#
##############################################################################

sub tts_google($$){
  my ($text,$file) = @_;
  #-- obtain speechlet
  system("wget -q --user-agent='Mozilla/5.0' \'http://translate.google.de/translate_tts?ie=UTF-8&tl=de&client=tw-ob&q=$text\' -O $file.mp3");


##############################################################################
#
#  Determine duration of speech file
#
##############################################################################

sub tts_duration($) {
  my $time;
  my ($file) = @_;
  eval {
    use MP3::Info;   
    my $tag = get_mp3info($file);
    if ($tag && defined($tag->{SECS})) {
  $time = int($tag->{SECS}+0.5);
    }
  };
  if ($@) {
    return undef;
  }
  return $time;
}

elmattt

hello and thank you for your time !
unfortunately i really don't know where and how i can use your code  ;D
i have a question before ... is this piece of code will have the same fonctionality as the module ?? for example : send speech or recorded speech, with or without a gong before or after, and will pause the actual source and play the same source after the speech ??

;D

elmattt


Mad

Hallo zusammen,

ich habe mir ein Musikplayer widget für die Bose Soundtouch erstellt.
Leider erscheint immer nur ein (Reading) Cover, wenn die Soundtouch an ist.
Ist die Soundtouch aus, gibt es kein Cover....
Gibt es die Möglichkeit einen Platzhalter (ein anderes Bild) für das Cover zu setzen, wenn sie aus ist?
So sieht es momentan aus. Es geht um das reading "art"

<div data-type="image" data-device="BOSE_08DF1FXXXXX" data-get="art" data-size="100px" data-refresh="5" class="top-space"></div>

Danke.

Eisix

Hallo Mad,

ich habe mir ein userreading erstellt welches das reading art anzeigt oder wenn da keins ist mein default icon anzeigt.
Sobald ich wieder an min system komme kann ich dir das schicken.

Gruß
Eisix

Eisix

ist von einer squeezebox


ftuicoverarturl:coverarturl.*|power.* {if(ReadingsVal('SB_PLAYER_b227eb45865e','power','on') eq 'on') {return ReadingsVal('SB_PLAYER_b227eb45865e'','coverarturl','')} else {return '/fhem/images/Senderlogo.jpg'}}

Mad

Besten Dank.

Habe es nun so gelöst

Platzhalter {if(ReadingsVal("BOSE_68C90BXXXXX","art","n.a.") eq ""){return "../images/bose.png"}else{return ReadingsVal("BOSE_68C90BXXXXX","art","../images/bose.png")}}

Mad

Gibt es mittlerweile die Möglichkeit MP3 Dateien abzuspielen? Fange gerade an mich mit dem Alarm Modul zu befassen und würde gerne die Bose Anlage dazu nutzen..

Prof. Dr. Peter Henning

Aber ja. Ich habe weiter oben Code dazu gepostet:

- Entweder vorgefertigte MP3-Dateien, oder
- frisch mit Amazon Polly oder dem Sprachservice von Google geholte MP3-Dateien oder
- eine Kombination von beidem.

werden als Datei "fhemspeech.mp3" auf einen miniDLNA geschoben, und die BOSE-Kisten mit einem FHEM-Befehl
playTrack fhemspeech
abgespielt. Inklusive Caching von Sprachdateien.

Beispielsweise gibt es eine Datei "023_hoftuer.mp3" mit dem Inhalt "Die Hoftür ist offen". Auf allen meinen FHEM-Systemen wird durch den Perl-Befehl
speak("SoundTouch.EG",":023: , mach sie bitte zu")
bewirkt, dass meine BOSE SoundTouch 300 im Erdgeschoss die Nachricht "Die Hoftür ist offen, mach sie bitte zu" abspielt.

Dabei wird das Speechlet ", mach sie bitte zu" live von Amazon erzeugt und mit der vorhandenen Datei zusammengefügt, danach abgespielt. Dauert wegen der Latenz und der nötigen Anlaufzeit der BOSE 300 (ca. 1 Sekunde) insgesamt etwa 3 Sekunden vom Absetzen des Befehls bis zum Ertönen der Durchsage.

Und falls mit dem etwas informationsfreien Post gemeint war, ob man denn Musikdateien abspielen kann: Das ging schon immer.

LG

pah



Mad

#641
Vielen Dank für die Antwort.
Um meinen ursprünglichen Post mit etwas mehr Informationen zu füllen :-)
Ist es möglich MP3 Dateien, die ich auf dem Raspi ablege (auf dem auch Fhem läuft), abzuspielen? Also ohne DLNA.
Das ganze würde ich gern mit dem Alarm Modul verknüpfen....

Ich konnte es aber jetzt über die Fritzbox (fritznas) schon mal realisieren. Mit dem Befehl
playTrack <Dateiname>
funktioniert es!

In dem Zusammenhang habe ich auch versucht die Dateien von der Fritzbox als "channel" zu hinterlegen. Leider erfolglos.
Zu Testzwecken habe ich bereits vorhandene channels ausgelesen und übernommen, auch ohne Erfolg.
Sah dann z.B. so aus
attr BOSE_000C8A8XXXX channel_18 1 LIVE|/v1/playback/station/s25260|TUNEIN

In diesem Fall war das reading "contentItemSourceAccount" leer.

Was mach ich da falsch?


Besten Dank

Prof. Dr. Peter Henning

ZitatIst es möglich MP3 Dateien, die ich auf dem Raspi ablege (auf dem auch Fhem läuft), abzuspielen? Also ohne DLNA.
Nein.

FritzBox ist eben MIT DLNA. Und ich habe keine Ahnung, wie man den FB-DLNA zum Rescan bewegen kann. Dieser Rescan ist nötig, wenn man eine Datei überschreibt.

Tipp: Etwas abspielen, dann den Channel mit einer Nummer > 6 im Modul sichern und sich den Attributwert ansehen. Da steht dann z.B. so etwas:

Willy and the Poor Boys||0$=Artist$85$albums$*a394|STORED_MUSIC|1677cdb1-d452-43c6-bf7a-49ed4d92c89f/0

Ich habe noch nicht herausgefunden, wie man "manuell" einen solchen Channeleintrag erzeugen könnte, um ein Musikstück auszusuchen.

Bei mir geht das deshalb über Playlists. Wenn ich ein bestimmtes Stück spielen will, trage ich das (via gepatchtes MediaList-Modul) in eine Playlist ein, die mit einem der Channel verbunden ist und somit einfach abgespielt werden kann.

LG

pah

Mad

Danke für die Antwort.
Solange es über die FB läuft, reicht mir das dann. Mit Rescan ist aber nicht die Indizierung gemeint, die man über die Bose App anstoßen kann?
Gruß zurück

Prof. Dr. Peter Henning

Der Rescan wird vom DLNA-Server ausgeführt, um ggf. Veränderungen in den Dateien zu erkennen. Ich wüsste nicht, dass der auf der FB durch die BOSE-App angestoßen werden kann??

LG

pah