Projekt: Laß das Telefon vorlesen

Android Apps wie „Talk – Free“ lesen dem Benutzer einen getippten Text vor und haben meistens das Problem, daß Sie mit großen Dateien nicht zurechtkommen. Mit Dateien in meiner Größenordnung sterben die Apps weg wie Fliegen, wenn sie die Dateien nebst Inhalt überhaupt öffnen können 🙂

Deswegen hatte ich gestern die fixe Idee, ich müßte eine TTS App (Text-To-Speech) für mein Handy bauen. Ok, satte 4 Stunden hat es dann gedauert, dem bescheuerten Eclipse bei zu bringen, überhaupt wieder Apps, AVD’s SDK’s und anderes anzuzeigen, upzudaten, zubenutzen und zu bauen. Von den vielen, vielen Rückschlägen, Flüchen, Verwünschungen und :Facepalm:s  mal abgesehen, kam am Ende etwas brauchbares raus.

Ich dachte immer Eclipse wäre das Tool zum Bauen von Android, oder irgendwelchen Anwendungen, aber so beschissen wie gestern hat es sich noch nie angestellt. Das SDK Update entfernte dann gleich mal die SDK TOOLS inkl. dem SDK Manager selbst von der Platte, nachdem es min. 400 MB unnütze Files auf die Platte gesaugt hatte. Dann löschte es den einzigen API Stand, der mich als Zielplattform wirklich interessiert hatte: Android 3.2+ … AAAAAAARGG…nein, ruhig, nicht schon wieder… tief luftholen… Android 3.2 ist API Level 13 und wird, wieso auch immer, nicht mehr im SDK Manager aufgeführt. Außer dem Tools Ordner wurden auch noch die Systemimages entfernt, so daß die bislang angelegten Virtual Devices aka Debugger-Umgebungen auch weg waren und auch nicht mehr angelegt werden konnten. Nachdem ich dann einen neuen SDK benutzt hatte, konnte ich zwar einen AVD anlegen , aber nicht benutzen, weil der sich nicht in LAN geklinkt hat und ohne LAN Verbindung war mein kleines Programm komplett nutzlos.

Was nun folgte war zum Glück etwas einfacher, aber nicht weniger aufwändig. Statt der netten Tante auf meinem Tablet, mußte mein neues Handy als Zielplattform herhalten. Leider hat Google das USB Debugging, ohne das man am PC nicht sehen kann, wo es denn genau bei der Liveanwendung knallt, versteckt und zwar dämlich versteckt. Es ist ok um Dumbos davon abzuhalten, das USB Debugging und die ganzen anderen coolen Entwicklerfunktionen zu aktivieren, aber es bleibt dämlich. Am Ende wollte das Handy dann auch per ADB angesprochen werden und es konnte endlich losgehen.

Das Projekt

Oben habe ich bereits verraten, daß es um TTS und große Files geht. Ich rede von Textfiles mit einer Größe von 500kb+ . Sich die vorlesen zu lassen, bietet keine „kostenlose“ App an. Damit man sich das vorstellen kann, 500kb sind ca. 500 Seiten DIN A4 Papier, also eine Menge Buchseiten. Will man sich die vorlesen lassen, dauert das Stunden und genau das habe ich vor.

Aber wieso will man sich das überhaupt  vorlesen lassen ? Naja zum einen kann man dann die Handyausgabe als Hörbuch verkaufen 😀 zum Anderen, und da komme ich derzeit ins Spiel, ist es richtig toll zum Korrekturlesen meines Romans. Die Rechtschreibkontrolle vom Officeprogramm kann zwar Fehler entdecken, aber falsche Worte, die richtig geschrieben wurden nicht. Außerdem findet diese auch keine sprachlichen Unschönheiten z.b. zu komplizierte Texte, falsch nieder geschriebene Gedankengänge usw. . Und da setzt jetzt die App ein, denn wenn es sich vorgelesen schräg anhört, ist es schräg, auch wenn es beim Lesen ok war. Nichts ist blöder für einen Romanschreiber, als wenn man den Roman nacher nicht vorlesen kann, ohne selbst zu stocken.(Auch dieser Absatz wurde Ihnen gespondert von Hardcore – der TTS App)

Das Problem

Die freien Apps lesen vor, was andere Apps an sie teilen oder per Copy&Paste vom User eingegeben wird.

Wer Handies kennt, kann sich vorstellen, daß alleine das Markieren des Textes den man hören will, Stunden braucht, wenn man es überhaupt fehlerfrei schafft. Vom Tippen auf dem Handy ganz zu schweigen 😉

Die Idee

Verlagern wir doch das Markieren und Ausschneiden des Textblockes auf den Desktop-PC, sprich meinen Linuxrechner. Den Text sendet man einfach per Netzwerk an die Sprachausgabe.

Die Stolperfallen

Natürlich liegen die alle im Android begraben und sind mehr oder weniger :faceplam:  . Wann kommt endlich der Tag, wo jemand ein Fedora ROM für Samsungsphones veröffentlicht, es wäre das Ende von Android wie wir es kennen und hassen.

Falle 1 : Die App muß einen öffentlichen Serverport öffnen, damit man etwas hinschicken kann.
Falle 2 :  Man muß die Handy  IP Adresse kennen, sonst kann man Falle 1 nicht umschiffen.
Falle 3 : Netzwerkfunktionen in der MainActivity sind Tabu, man braucht einen Netzwerktask.
Falle 4 : GUI Elemente dürfen nur von dem Task benutzt werden, der den View geöffnet hat.

Falle 5: Wie bekommt man den Textblock eigentlich zum Handy ?

Fangen wir mal mit #5 an :

[marius@eve ~]$ nc 192.168.0.117 3000
Wie die Webseite TheHackernews.com heute berichtet, ist es Forschern der Standford University bereits in 2015 gelungen, einen Super-Super-Cookie zu nutzen, in dem sie über die Browser API der mobilen Versionen von Chrome, Opera und Firefox den Batteriestatus abgefragt haben. Durch die Kombination von angezeigter "verbleibender Zeit in Sekunden" und dem Prozentwert der Ladung, ergeben sich bis zu 14 Millionen Kombinationen, die man Geräten zu ordnen kann.

Genau, das Schweizer Armeemesser „nc“ (NetCat) muß zumindest in Phase 1 der Enwicklung reichen und genau das tut es. Man öffnet also einfach eine TCP Verbindung zur App und schreibt den zu sprechenden Text rein. Wenn alles klappt, kommt Sprache raus.

Das war einfach, kommen wir zu Falle #2 :

        
WifiManager wm = (WifiManager) getSystemService(WIFI_SERVICE);
@SuppressWarnings("deprecation")
String ip = Formatter.formatIpAddress(wm.getConnectionInfo().getIpAddress());
log("MY IP is "+ip);

Das funktioniert übrigens nur für LAN IPs. Wenn man WAN, also in dem Mobilen Internet unterwegs ist, bekommt man i.d.R. eine IPv6 Adresse und damit kommt die Routine oben nicht zurecht. Da ich WAN nicht brauche, reicht das hier.

Die TTS Engine ist leicht zu öffnen:

tts = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
   @Override
   public void onInit(int status) {
      if(status == TextToSpeech.SUCCESS ) {
         tts.setLanguage(Locale.GERMAN);
      }
   }
});

Nun zum Starten des Netzwerkthreads, den dem muß ich die IP mitgeben:

Thread serverThread = null;
...
this.serverThread = new Thread(new ServerThread(ip));
this.serverThread.start();

Der Serverthread an sich ist simpel. Ich nehme hier Port 3000, 10 Connections gleichzeitig auf meinem Socket und lege fest, daß die externe IP im LAN zum Binden des Interfaces genutzt werden soll, weil dumbo Android das sonst auf 127.0.0.1 aufmacht, da natürlich keiner was von 🙂  Der Code basiert auf einem Beispiel zu Serversockets, das aber wie dort beschrieben gar nicht funktioneiren kann 😉 Nehmt die Version hier:

public void run() {
			Socket socket = null;
			try {
				serverSocket = new ServerSocket(3000,10,InetAddress.getByName(ip));
				this.log("ServerSocket gestartet:"+ serverSocket);
			} catch (IOException e) {
				e.printStackTrace();
				this.log(e.toString());	
				return;
			} catch (Exception e) {
				e.printStackTrace();
				this.log(e.toString());
				return;
			}
			while (!Thread.currentThread().isInterrupted()) {
				this.log("ServerSocket waiting for accept:");
				try {
					if (serverSocket.isClosed()) serverSocket = new ServerSocket(3000,10,InetAddress.getByName(ip));
					
					socket = serverSocket.accept();
					System.err.println("Socket connection: "+ socket);
					this.log("Connection accepted");

					CommunicationThread commThread = new CommunicationThread(socket);
					new Thread(commThread).start();

				} catch (IOException e) {
					e.printStackTrace();
					this.log(e.toString());
				} catch (Exception e) {
					e.printStackTrace();
					this.log(e.toString());
				}
			}
		}
	}

Die doppelten Catch-Anweisungen sind natürlich nur zu Demozwecken drin. Ist die App mal gebaut, kann man unterscheiden zwischen einem echten IO Fehler und anderen Fehlern, z.b. OOM ( Out Of Memory ) und verschiedene Meldungen erzeugen.

Der CommunicationThread öffnet das Socket, daß beim Accept()  rausgekommen ist, extrahiert den InputStream(), macht einen BufferedReader daraus und ließt den Text zeilenweise ein, bis das Socket geschlossen wird:

       this.input = new BufferedReader(new InputStreamReader(this.clientSocket.getInputStream()));

try {
	String read = input.readLine();
	if ( read == null ) return; // wichtig, weil läuft sonst in einer Endlosschleife

	updateUIThread ui = new updateUIThread(read);
	updateConversationHandler.post(ui);

} catch (IOException e) {
	e.printStackTrace();
	this.log(e.toString());
	return;
} catch (Exception e) {
	e.printStackTrace();
	this.log(e.toString());
	return;
}

Der UIUpdateThread macht dann nichts weiter als die Textnachricht in Zeilen umzuwandeln und dann die TTS Engine zu füttern. Dabei wartet die Anwendung solange bis nicht mehr gesprochen wird, wofür der Thread 100ms schläft. Das schont die Batterie.

public void run() {
	String[] lines = msg.split("\\.");
	for(int i=0;i<lines.length;i++) { if ( lines[i].trim().length() > 0 ) {
			Toast.makeText(getApplicationContext(), lines[i],Toast.LENGTH_SHORT).show();
			tts.speak(lines[i], TextToSpeech.QUEUE_ADD, null);
			while( tts.isSpeaking() ) {
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					e.printStackTrace();
					Toast.makeText(getApplicationContext(), e.toString() ,Toast.LENGTH_LONG).show();
				}
			}
		}
       }
}

Fertig ist unser Remote-TTS-Reader 🙂

Nun leiten wir das Audio Signal des Telefons noch an den Mikrofonanschluß vom PC und starten FFMPEG:

ffmpeg -f pulse -i default -c:a:0 libmp3lame -b:a:0 128k test.mp3

Fertig ist das Hörbuch für unterwegs 🙂

Ihr könnt die fehlenden Teile aus dem Tutorial extrahieren, es ist der gleiche Aufbau. Nur habe ich noch einiges erweitert, z.b. logging, weil seine Routinen halt nicht funktioniert haben. Die relevanten Änderungen sind oben drin, so daß Eurer Projekt funktionieren sollte.

Anmerkung: Man kann von 10 Rechnern aus die APP auf dem Handy gleichzeitig ansprechen, aber die Sprachengine wird das nur nacheinander wiedergeben.