Java sprechen!

Warum sollten Sie Ihre Anwendungen zum Sprechen bringen? Zunächst einmal macht es Spaß und eignet sich für unterhaltsame Anwendungen wie Spiele. Und es gibt eine ernstere Seite der Barrierefreiheit. Ich denke hier nicht nur an diejenigen, die bei der Verwendung einer visuellen Oberfläche von Natur aus benachteiligt sind, sondern auch an Situationen, in denen es unmöglich oder sogar illegal ist, den Blick von dem abzuwenden, was Sie tun.

Kürzlich habe ich mit einigen Technologien gearbeitet, um HTML- und XML-Informationen aus dem Web zu entnehmen [siehe "Zugriff auf die weltweit größte Datenbank mit Web DataBase Connectivity" ( JavaWorld, März 2001)]. Mir kam der Gedanke, dass ich diese Arbeit und diese Idee zusammenfügen könnte, um einen sprechenden Webbrowser zu erstellen. Ein solcher Browser würde sich als nützlich erweisen, um Ausschnitte von Informationen von Ihren Lieblingsseiten anzuhören - zum Beispiel Schlagzeilen -, genauso wie Sie Radio hören, während Sie mit Ihrem Hund spazieren gehen oder zur Arbeit fahren. Natürlich müssten Sie mit der aktuellen Technologie Ihren Laptop mit angeschlossenem Mobiltelefon mit sich herumtragen, aber dieses unpraktische Szenario könnte sich in naher Zukunft mit der Einführung von Java-fähigen Smartphones wie dem Nokia 9210 (9290 in the) ändern UNS).

Kurzfristig wäre vielleicht ein E-Mail-Reader nützlicher, der auch dank der JavaMail-API möglich ist. Diese Anwendung überprüft Ihren Posteingang regelmäßig und Ihre Aufmerksamkeit wird von einer Stimme aus dem Nichts angezogen, die verkündet: "Sie haben neue E-Mails, möchten Sie, dass ich sie Ihnen vorlese?" Betrachten Sie in ähnlicher Weise eine sprechende Erinnerung - verbunden mit Ihrer Tagebuchanwendung -, die lautet: "Vergessen Sie nicht, sich in 10 Minuten mit dem Chef zu treffen!"

Angenommen, Sie sind von diesen Ideen überzeugt oder haben eigene gute Ideen, werden wir weitermachen. Ich werde zunächst zeigen, wie meine bereitgestellte Zip-Datei funktioniert, damit Sie sofort einsatzbereit sind und die Implementierungsdetails überspringen können, wenn Sie der Meinung sind, dass dies zu viel harte Arbeit ist.

Testen Sie die Sprachmaschine

Um die Sprach-Engine verwenden zu können, müssen Sie die Datei jw-0817-javatalk.zip in Ihren CLASSPATH aufnehmen und die com.lotontech.speech.TalkerKlasse über die Befehlszeile oder in einem Java-Programm ausführen .

Geben Sie Folgendes ein, um es über die Befehlszeile auszuführen:

java com.lotontech.speech.Talker "h | e | l | oo" 

Um es von einem Java-Programm aus auszuführen, fügen Sie einfach zwei Codezeilen hinzu:

com.lotontech.speech.Talker talker = new com.lotontech.speech.Talker (); talker.sayPhoneWord ("h | e | l | oo");

An diesem Punkt wundern Sie sich wahrscheinlich über das Format der "h|e|l|oo"Zeichenfolge, die Sie in der Befehlszeile oder für die sayPhoneWord(...)Methode angeben. Lassen Sie mich erklären.

Die Sprachmaschine verkettet kurze Klangbeispiele, die die kleinsten Einheiten menschlicher - in diesem Fall englischer - Sprache darstellen. Diese als Allophone bezeichneten Klangbeispiele sind mit einer Kennung aus einem, zwei oder drei Buchstaben gekennzeichnet. Einige Bezeichner sind offensichtlich und andere nicht so offensichtlich, wie Sie aus der phonetischen Darstellung des Wortes "Hallo" ersehen können.

  • h - klingt wie erwartet
  • e - klingt wie erwartet
  • l - klingt wie erwartet, aber beachten Sie, dass ich ein doppeltes "l" auf ein einzelnes reduziert habe
  • oo - ist der Sound für "Hallo", nicht für "Bot" und nicht für "zu"

Hier ist eine Liste der verfügbaren Allophone:

  • a - wie bei Katze
  • b - wie in der Kabine
  • c - wie bei Katze
  • d - wie in Punkt
  • e - wie in der Wette
  • f - wie beim Frosch
  • g - wie beim Frosch
  • h - wie bei Schwein
  • Ich - wie beim Schwein
  • j - wie in jig
  • k - wie im Fass
  • l - wie im Bein
  • m - wie in erfüllt
  • n - wie am Anfang
  • o - wie in nicht
  • p - wie im Topf
  • r - wie in rot
  • s - wie in sat
  • t - wie in sat
  • u - wie in put
  • v - wie in haben
  • w - wie bei nass
  • y - wie noch
  • z - wie im Zoo
  • aa - wie in Fälschung
  • ay - wie im Heu
  • ee - wie bei der Biene
  • ii - wie in hoch
  • oo - wie in go
  • bb - Variation von b mit unterschiedlicher Betonung
  • dd - Variation von d mit unterschiedlicher Betonung
  • ggg - Variation von g mit unterschiedlicher Betonung
  • hh - Variation von h mit unterschiedlicher Betonung
  • ll - Variation von l mit unterschiedlicher Betonung
  • nn - Variation von n mit unterschiedlicher Betonung
  • rr - Variation von r mit unterschiedlicher Betonung
  • tt - Variation von t mit unterschiedlicher Betonung
  • yy - Variation von y mit unterschiedlicher Betonung
  • ar - wie im Auto
  • aer - wie in Pflege
  • ch - wie in welcher
  • ck - wie in Scheck
  • Ohr - wie beim Bier
  • ähm - wie später
  • err - wie später (längerer Ton)
  • ng - wie beim Füttern
  • oder - wie gesetzlich
  • ou - wie im Zoo
  • ouu - wie im Zoo (längerer Ton)
  • ow - wie bei der Kuh
  • oy - wie bei einem Jungen
  • sh - wie in geschlossen
  • th - wie in der Sache
  • dth - wie hier
  • uh - Variation von u
  • wh - wie wo
  • zh - wie auf asiatisch

In der menschlichen Sprache steigt und fällt die Tonhöhe der Wörter in jedem gesprochenen Satz. Diese Intonation lässt die Sprache natürlicher und emotionaler klingen und ermöglicht es, Fragen von Aussagen zu unterscheiden. Wenn Sie jemals Stephen Hawkings synthetische Stimme gehört haben, verstehen Sie, wovon ich spreche. Betrachten Sie diese beiden Sätze:

  • Es ist falsch - f | aa | k
  • Ist es falsch? - f | AA | k

Wie Sie vielleicht erraten haben, können Sie die Intonation mit Großbuchstaben erhöhen. Sie müssen ein wenig damit experimentieren, und mein Hinweis ist, dass Sie sich auf die langen Vokale konzentrieren sollten.

Das ist alles, was Sie wissen müssen, um die Software zu verwenden. Wenn Sie jedoch daran interessiert sind, was unter der Haube vor sich geht, lesen Sie weiter.

Implementieren Sie die Sprachmaschine

The speech engine requires just one class to implement, with four methods. It employs the Java Sound API included with J2SE 1.3. I won't provide a comprehensive tutorial of the Java Sound API, but you'll learn by example. You'll find there's not much to it, and the comments tell you what you need to know.

Here's the basic definition of the Talker class:

package com.lotontech.speech; import javax.sound.sampled.*; import java.io.*; import java.util.*; import java.net.*; public class Talker { private SourceDataLine line=null; } 

If you run Talker from the command line, the main(...) method below will serve as the entry point. It takes the first command line argument, if one exists, and passes it to the sayPhoneWord(...) method:

/* * This method speaks a phonetic word specified on the command line. */ public static void main(String args[]) { Talker player=new Talker(); if (args.length>0) player.sayPhoneWord(args[0]); System.exit(0); } 

The sayPhoneWord(...) method is called by main(...) above, or it may be called directly from your Java application or plug-in supported applet. It looks more complicated than it is. Essentially, it simply steps though the word allophones -- separated by "|" symbols in the input text -- and plays them one by one through a sound-output channel. To make it sound more natural, I merge the end of each sound sample with the beginning of the next one:

/* * This method speaks the given phonetic word. */ public void sayPhoneWord(String word) { // -- Set up a dummy byte array for the previous sound -- byte[] previousSound=null; // -- Split the input string into separate allophones -- StringTokenizer st=new StringTokenizer(word,"|",false); while (st.hasMoreTokens()) { // -- Construct a file name for the allophone -- String thisPhoneFile=st.nextToken(); thisPhoneFile="/allophones/"+thisPhoneFile+".au"; // -- Get the data from the file -- byte[] thisSound=getSound(thisPhoneFile); if (previousSound!=null) { // -- Merge the previous allophone with this one, if we can -- int mergeCount=0; if (previousSound.length>=500 && thisSound.length>=500) mergeCount=500; for (int i=0; i
   
    

At the end of sayPhoneWord(), you'll see it calls playSound(...) to output an individual sound sample (an allophone), and it calls drain(...) to flush the sound channel. Here's the code for playSound(...):

/* * This method plays a sound sample. */ private void playSound(byte[] data) { if (data.length>0) line.write(data, 0, data.length); } 

And for drain(...):

/* * This method flushes the sound channel. */ private void drain() { if (line!=null) line.drain(); try {Thread.sleep(100);} catch (Exception e) {} } 

Now, if you look back at the sayPhoneWord(...) method, you'll see there's one method I've not yet covered: getSound(...).

getSound(...) reads in a prerecorded sound sample, as byte data, from an au file. When I say a file, I mean a resource held within the supplied zip file. I draw the distinction because the way you get hold of a JAR resource -- using the getResource(...) method -- proceeds differently from the way you get hold of a file, a not obvious fact.

For a blow-by-blow account of reading the data, converting the sound format, instantiating a sound output line (why they call it a SourceDataLine, I don't know), and assembling the byte data, I refer you to the comments in the code that follows:

/* * This method reads the file for a single allophone and * constructs a byte vector. */ private byte[] getSound(String fileName) { try { URL url=Talker.class.getResource(fileName); AudioInputStream stream = AudioSystem.getAudioInputStream(url); AudioFormat format = stream.getFormat(); // -- Convert an ALAW/ULAW sound to PCM for playback -- if ((format.getEncoding() == AudioFormat.Encoding.ULAW) || (format.getEncoding() == AudioFormat.Encoding.ALAW)) { AudioFormat tmpFormat = new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, format.getSampleRate(), format.getSampleSizeInBits() * 2, format.getChannels(), format.getFrameSize() * 2, format.getFrameRate(), true); stream = AudioSystem.getAudioInputStream(tmpFormat, stream); format = tmpFormat; } DataLine.Info info = new DataLine.Info( Clip.class, format, ((int) stream.getFrameLength() * format.getFrameSize())); if (line==null) { // -- Output line not instantiated yet -- // -- Can we find a suitable kind of line? -- DataLine.Info outInfo = new DataLine.Info(SourceDataLine.class, format); if (!AudioSystem.isLineSupported(outInfo)) { System.out.println("Line matching " + outInfo + " not supported."); throw new Exception("Line matching " + outInfo + " not supported."); } // -- Open the source data line (the output line) -- line = (SourceDataLine) AudioSystem.getLine(outInfo); line.open(format, 50000); line.start(); } // -- Some size calculations -- int frameSizeInBytes = format.getFrameSize(); int bufferLengthInFrames = line.getBufferSize() / 8; int bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes; byte[] data=new byte[bufferLengthInBytes]; // -- Read the data bytes and count them -- int numBytesRead = 0; if ((numBytesRead = stream.read(data)) != -1) { int numBytesRemaining = numBytesRead; } // -- Truncate the byte array to the correct size -- byte[] newData=new byte[numBytesRead]; for (int i=0; i
     
      

So, that's it. A speech synthesizer in about 150 lines of code, including comments. But it's not quite over.

Text-to-speech conversion

Specifying words phonetically might seem a bit tedious, so if you intend to build one of the example applications I suggested in the introduction, you want to provide ordinary text as input to be spoken.

After looking into the issue, I've provided an experimental text-to-speech conversion class in the zip file. When you run it, the output will give you insight into what it does.

You can run a text-to-speech converter with a command like this:

java com.lotontech.speech.Converter "hello there" 

What you'll see as output looks something like:

hello -> h|e|l|oo there -> dth|aer 

Or, how about running it like:

java com.lotontech.speech.Converter "I like to read JavaWorld" 

to see (and hear) this:

i -> ii like -> l|ii|k to -> t|ouu read -> r|ee|a|d java -> j|a|v|a world -> w|err|l|d 

If you're wondering how it works, I can tell you that my approach is quite simple, consisting of a set of text replacement rules applied in a certain order. Here are some example rules that you might like to apply mentally, in order, for the words "ant," "want," "wanted," "unwanted," and "unique":

  1. Replace "*unique*" with "|y|ou|n|ee|k|"
  2. Replace "*want*" with "|w|o|n|t|"
  3. Replace "*a*" with "|a|"
  4. Replace "*e*" with "|e|"
  5. Replace "*d*" with "|d|"
  6. Replace "*n*" with "|n|"
  7. Replace "*u*" with "|u|"
  8. Replace "*t*" with "|t|"

For "unwanted" the sequence would be thus:

unwantedun[|w|o|n|t|]ed (rule 2) [|u|][|n|][|w|o|n|t|][|e|][|d|] (rules 4, 5, 6, 7) u|n|w|o|n|t|e|d (with surplus characters removed) 

You should see how words containing the letters wont will be spoken in a different way to words containing the letters ant. You should also see how the special case rule for the complete word unique takes precedence over the other rules so that this word is spoken as y|ou... rather than u|n....