Berbicara tentang Java!

Mengapa Anda ingin membuat aplikasi Anda berbicara? Pertama, ini menyenangkan, dan cocok untuk aplikasi menyenangkan seperti game. Dan ada sisi aksesibilitas yang lebih serius. Saya berpikir di sini bukan hanya mereka yang secara alami dirugikan saat menggunakan antarmuka visual, tetapi juga situasi di mana tidak mungkin - atau bahkan ilegal - untuk mengalihkan pandangan dari apa yang Anda lakukan.

Baru-baru ini saya telah bekerja dengan beberapa teknologi untuk mengambil informasi HTML dan XML dari Web [lihat "Mengakses Database Terbesar di Dunia dengan Konektivitas Basis Data Web" ( JavaWorld, Maret 2001)]. Terpikir oleh saya bahwa saya dapat menggabungkan pekerjaan itu dan ide ini bersama-sama untuk membangun browser Web yang berbicara. Peramban seperti itu akan terbukti berguna untuk mendengarkan cuplikan informasi dari situs favorit Anda - tajuk berita, misalnya - seperti mendengarkan radio sambil berjalan-jalan dengan anjing atau mengemudi ke tempat kerja. Tentu saja, dengan teknologi saat ini Anda harus membawa-bawa komputer laptop Anda dengan ponsel terpasang, tetapi skenario yang tidak praktis itu dapat berubah dengan baik dalam waktu dekat dengan kedatangan ponsel pintar yang mendukung Java seperti Nokia 9210 (9290 di KAMI).

Mungkin lebih berguna dalam jangka pendek adalah pembaca email, juga dimungkinkan berkat JavaMail API. Aplikasi ini akan memeriksa kotak masuk Anda secara berkala, dan perhatian Anda akan tertarik oleh suara entah dari mana yang menyatakan "Anda memiliki email baru, apakah Anda ingin saya membacakannya untuk Anda?" Dalam nada yang sama, pertimbangkan pengingat berbicara - terkait dengan aplikasi buku harian Anda - yang meneriakkan "Jangan lupakan pertemuan Anda dengan bos dalam 10 menit!"

Dengan asumsi Anda menjual ide-ide itu, atau memiliki beberapa ide bagus dari Anda sendiri, kami akan melanjutkan. Saya akan mulai dengan menunjukkan cara menggunakan file zip yang saya sediakan agar Anda dapat langsung menjalankan dan melewatkan detail implementasi jika menurut Anda itu terlalu banyak kerja keras.

Uji coba mesin bicara

Untuk menggunakan mesin ucapan, Anda harus menyertakan file jw-0817-javatalk.zip di CLASSPATH Anda dan menjalankan com.lotontech.speech.Talkerkelas dari baris perintah atau dari dalam program Java.

Untuk menjalankannya dari baris perintah, ketik:

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

Untuk menjalankannya dari program Java, cukup sertakan dua baris kode:

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

Pada titik ini Anda mungkin bertanya-tanya tentang format "h|e|l|oo"string yang Anda berikan pada baris perintah atau berikan ke sayPhoneWord(...)metode. Biar saya jelaskan.

Mesin bicara bekerja dengan menggabungkan sampel suara pendek yang mewakili unit terkecil manusia - dalam hal ini bahasa Inggris - ucapan. Sampel suara tersebut, yang disebut alofon, diberi label dengan pengenal satu, dua, atau tiga huruf. Beberapa pengenal terlihat jelas dan beberapa tidak begitu jelas, seperti yang Anda lihat dari representasi fonetik kata "halo".

  • h - terdengar seperti yang Anda harapkan
  • e - terdengar seperti yang Anda harapkan
  • l - terdengar seperti yang Anda harapkan, tapi perhatikan bahwa saya telah mengurangi "l" ganda menjadi satu
  • oo - adalah suara untuk "halo", bukan untuk "bot", dan bukan untuk "juga"

Berikut adalah daftar alofon yang tersedia:

  • a - seperti pada kucing
  • b - seperti di taksi
  • c - seperti pada kucing
  • d - seperti pada titik
  • e - seperti dalam taruhan
  • f - seperti pada katak
  • g - seperti pada katak
  • h - seperti pada babi
  • i - seperti pada babi
  • j - seperti di jig
  • k - seperti dalam tong
  • l - seperti di kaki
  • m - seperti di bertemu
  • n - seperti pada awalnya
  • o - seperti tidak
  • p - seperti dalam pot
  • r - seperti pada busuk
  • s - seperti di sat
  • t - seperti di sat
  • u - seperti pada put
  • v - seperti di miliki
  • w - seperti basah
  • y - belum
  • z - seperti di kebun binatang
  • aa - seperti palsu
  • ay - seperti jerami
  • ee - seperti pada lebah
  • ii - seperti di tinggi
  • oo - as in go
  • bb - variasi b dengan penekanan berbeda
  • dd - variasi d dengan penekanan berbeda
  • ggg - variasi g dengan penekanan berbeda
  • hh - variasi h dengan penekanan yang berbeda
  • ll - variasi l dengan penekanan berbeda
  • nn - variasi n dengan penekanan berbeda
  • rr - variasi r dengan penekanan berbeda
  • tt - variasi t dengan penekanan yang berbeda
  • yy - variasi y dengan penekanan yang berbeda
  • ar - seperti di mobil
  • aer - seperti dalam perawatan
  • ch - seperti di mana
  • ck - seperti di cek
  • telinga - seperti pada bir
  • er - seperti nanti
  • err - seperti nanti (suara lebih panjang)
  • ng - seperti dalam memberi makan
  • atau - seperti dalam hukum
  • ou - seperti di kebun binatang
  • ouu - seperti di kebun binatang (suara lebih panjang)
  • ow - seperti pada sapi
  • oy - seperti pada anak laki-laki
  • sh - seperti di tutup
  • th - seperti dalam hal
  • dth - seperti ini
  • uh - variasi u
  • wh - seperti di mana
  • zh - seperti di Asia

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