Parler de Java!

Pourquoi voudriez-vous faire parler vos applications? Pour commencer, c'est amusant et adapté aux applications amusantes comme les jeux. Et il y a un côté d'accessibilité plus sérieux. Je pense ici non seulement à ceux qui sont naturellement défavorisés lors de l'utilisation d'une interface visuelle, mais aussi à ces situations où il est impossible - voire illégal - de détourner les yeux de ce que vous faites.

Récemment, j'ai travaillé avec certaines technologies pour extraire des informations HTML et XML du Web [voir «Accéder à la plus grande base de données du monde avec la connectivité Web DataBase» ( JavaWorld, mars 2001)]. Il m'est venu à l'esprit que je pouvais connecter ce travail et cette idée pour créer un navigateur Web parlant. Un tel navigateur s'avérerait utile pour écouter des extraits d'informations de vos sites préférés - les titres de l'actualité, par exemple - tout comme écouter la radio en promenant votre chien ou en conduisant au travail. Bien sûr, avec la technologie actuelle, vous devrez transporter votre ordinateur portable avec votre téléphone portable connecté, mais ce scénario peu pratique pourrait bien changer dans un proche avenir avec l'arrivée de téléphones intelligents compatibles Java comme le Nokia 9210 (9290 dans le NOUS).

Peut-être plus utile à court terme serait un lecteur de courrier électronique, également possible grâce à l'API JavaMail. Cette application vérifierait votre boîte de réception périodiquement, et votre attention serait attirée par une voix venue de nulle part proclamant «Vous avez un nouveau courrier, voulez-vous que je vous le lise? Dans le même ordre d'idées, pensez à un rappel vocal - lié à votre application de journal - qui crie "N'oubliez pas votre rencontre avec le patron dans 10 minutes!"

En supposant que vous êtes convaincu de ces idées ou que vous avez de bonnes idées, nous allons passer à autre chose. Je vais commencer par montrer comment faire fonctionner mon fichier zip fourni afin que vous puissiez être opérationnel immédiatement et ignorer les détails de l'implémentation si vous pensez que c'est trop de travail.

Testez le moteur vocal

Pour utiliser le moteur vocal, vous devrez inclure le fichier jw-0817-javatalk.zip dans votre CLASSPATH et exécuter la com.lotontech.speech.Talkerclasse depuis la ligne de commande ou depuis un programme Java.

Pour l'exécuter à partir de la ligne de commande, tapez:

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

Pour l'exécuter à partir d'un programme Java, ajoutez simplement deux lignes de code:

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

À ce stade, vous vous interrogez probablement sur le format de la "h|e|l|oo"chaîne que vous fournissez sur la ligne de commande ou que vous fournissez à la sayPhoneWord(...)méthode. Laisse-moi expliquer.

Le moteur de parole fonctionne en concaténant de courts échantillons sonores qui représentent les plus petites unités de parole humaine - dans ce cas l'anglais -. Ces échantillons sonores, appelés allophones, sont étiquetés avec un identifiant à une, deux ou trois lettres. Certains identifiants sont évidents et d'autres pas si évidents, comme vous pouvez le voir dans la représentation phonétique du mot «bonjour».

  • h - sonne comme vous vous en doutez
  • e - sonne comme vous vous en doutez
  • l - sonne comme vous vous en doutez, mais notez que j'ai réduit un double "l" à un simple
  • oo - est le son pour "bonjour", pas pour "bot" et pas pour "trop"

Voici une liste des allophones disponibles:

  • a - comme chat
  • b - comme dans la cabine
  • c - comme dans cat
  • d - comme dot
  • e - comme pari
  • f - comme grenouille
  • g - comme grenouille
  • h - comme porc
  • je - comme dans le porc
  • j - comme dans jig
  • k - comme dans keg
  • l - comme dans la jambe
  • m - comme met
  • n - comme dans begin
  • o - comme non
  • p - comme en pot
  • r - comme pourriture
  • s - comme sat
  • t - comme sat
  • u - comme dans put
  • v - comme avoir
  • w - comme humide
  • y - pour l'instant
  • z - comme au zoo
  • aa - comme faux
  • ay - comme dans le foin
  • ee - comme abeille
  • ii - comme en haut
  • oo - comme aller
  • bb - variation de b avec un accent différent
  • dd - variation de d avec un accent différent
  • ggg - variation de g avec un accent différent
  • hh - variation de h avec un accent différent
  • ll - variation de l avec un accent différent
  • nn - variation de n avec une emphase différente
  • rr - variation de r avec une emphase différente
  • tt - variation de t avec une emphase différente
  • yy - variation de y avec une accentuation différente
  • ar - comme en voiture
  • aer - comme dans les soins
  • ch - comme dans lequel
  • ck - comme dans le chèque
  • oreille - comme dans la bière
  • euh - comme plus tard
  • err - comme plus tard (son plus long)
  • ng - comme dans l'alimentation
  • ou - comme dans la loi
  • ou - comme au zoo
  • ouu - comme au zoo (son plus long)
  • ow - comme vache
  • oy - comme boy
  • sh - comme fermé
  • th - comme dans la chose
  • dth - comme dans ce
  • euh - variation de u
  • wh - comme où
  • zh - comme asiatique

In human speech the pitch of words rises and falls throughout any spoken sentence. This intonation makes the speech sound more natural, more emotive, and allows questions to be distinguished from statements. If you've ever heard Stephen Hawking's synthetic voice, you understand what I'm talking about. Consider these two sentences:

  • It is fake -- f|aa|k
  • Is it fake? -- f|AA|k

As you might have guessed, the way to raise the intonation is to use capital letters. You need to experiment with this a little, and my hint is that you should concentrate on the long vowel sounds.

That's all you need to know to use the software, but if you're interested in what's going on under the hood, read on.

Implement the speech engine

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....