upstream version 1.2.2
[debian/freetts] / com / sun / speech / freetts / diphone / DiphoneUnitDatabase.java
1 /**
2  * Portions Copyright 2003 Sun Microsystems, Inc.
3  * Portions Copyright 1999-2001 Language Technologies Institute, 
4  * Carnegie Mellon University.
5  * All Rights Reserved.  Use is subject to license terms.
6  * 
7  * See the file "license.terms" for information on usage and
8  * redistribution of this file, and for a DISCLAIMER OF ALL 
9  * WARRANTIES.
10  */
11 package com.sun.speech.freetts.diphone;
12 import java.io.BufferedInputStream;
13 import java.io.BufferedReader;
14 import java.io.DataInputStream;
15 import java.io.DataOutputStream;
16 import java.io.FileInputStream;
17 import java.io.FileNotFoundException;
18 import java.io.FileOutputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.InputStreamReader;
22 import java.lang.ref.Reference;
23 import java.lang.ref.WeakReference;
24 import java.net.URL;
25 import java.nio.ByteBuffer;
26 import java.nio.MappedByteBuffer;
27 import java.nio.channels.FileChannel;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.LinkedHashMap;
31 import java.util.Map;
32 import java.util.NoSuchElementException;
33 import java.util.StringTokenizer;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36
37 import com.sun.speech.freetts.relp.Sample;
38 import com.sun.speech.freetts.relp.SampleInfo;
39 import com.sun.speech.freetts.util.BulkTimer;
40 import com.sun.speech.freetts.util.Utilities;
41
42 /**
43  * Represents and manages the unit data for all diphones.  The diphone
44  * data set is stored in a set of data files. These data are loaded by this
45  * class into internal data structures before diphone synthesis can
46  * occur. 
47  * <p>
48  *The diphone data set is one of the largest sets of data that
49  * needs to be loaded by the synthesizer and therefore can add to the
50  * overall startup time for any system using this database.  For
51  * certain applications, the startup time is a critical spec that
52  * needs to be optimized, while for other applications, startup time
53  * is inconsequential.  This class provides settings (via system
54  * properties) that control how the database is to be loaded so that
55  * applications can tune for quick startup or optimal run time.
56  * <p>
57  * This class serves also as a testbed for comparing performance of
58  * the traditional java binary I/O and the new io ( <code>java.nio </code>)
59  * package.
60  * <p>
61  * <p> A diphone database can be loaded from a text data file, or a
62  * binary datafile.  The binary version loads significantly faster
63  * than the text version. Additionally, a binary index can be
64  * generated and used to reduce overall memory footprint.
65  * <p> 
66  * <p>
67  * A DiphoneUnitDatabase contains an array of frames, and an aray of
68  * residuals. The frames are the samples of the wave, and the
69  * residuals are for linear predictive coding use. This is called
70  * "cst_sts" (a struct) in flite.
71  * <p>
72  * Note that if 'com.sun.speech.freetts.useNewIO' is set to true and
73  * the input type is binary, than the JDK1.4+ new IO api is used to
74  * load the database.
75  * <p>
76  * The system property
77  * <pre>
78  *      com.sun.speech.freetts.diphone.UnitDatabase.cacheType 
79  * </pre>
80  *
81  * can be set to one of:
82  *
83  * <ul>
84  * <li> preload: database is loaded at startup
85  * <li> demand: database is loaded on demand
86  * <li> hard: database is loaded on demand but cached
87  * <li> soft: database is loaded on demand but cached with soft references
88  * </ul>
89  *
90  * This <code> cacheType </code> setting controls how the database is
91  * loaded. The default is to 'preload' the database. This setting
92  * gives best runtime performance but with longer initial startup
93  * cost.  
94  */
95 public class DiphoneUnitDatabase {
96     /** Logger instance. */
97     private static final Logger LOGGER =
98         Logger.getLogger(DiphoneUnitDatabase.class.getName());
99
100     private String name;
101     private int sampleRate;
102     private int numChannels;
103     private int residualFold = 1;
104     private float lpcMin;
105     private float lpcRange;
106     private int lineCount = 0;
107     private Diphone defaultDiphone;
108     private Map diphoneMap = null;
109     private Map diphoneIndex;
110     private SampleInfo sampleInfo;
111     
112     private boolean useNewIO =
113         Utilities.getProperty("com.sun.speech.freetts.useNewIO",
114                 "true").equals("true");
115     // cache can be 'preload' 'none', 'soft' or 'hard'
116     private String cacheType = 
117         Utilities.getProperty(
118             "com.sun.speech.freetts.diphone.UnitDatabase.cacheType",
119             "preload");
120     private boolean useIndexing = !cacheType.equals("preload");
121     private boolean useCache = !cacheType.equals("demand");
122     private boolean useSoftCache = cacheType.equals("soft");
123
124     private final static int MAGIC = 0xFEEDFACE;
125     private final static int INDEX_MAGIC = 0xFACADE;
126     private final static int VERSION = 1;
127     private final static int MAX_DB_SIZE = 4 * 1024 * 1024;
128
129     private String indexName = null;
130     private MappedByteBuffer mbb = null;
131     private int defaultIndex = -1;
132
133     /**
134      * Creates the DiphoneUnitDatabase from the given input stream.
135      *
136      * @param url the location of the database
137      * @param isBinary if <code>true</code> the database is in 
138      *          binary format; otherwise it is in text format
139      *
140      * @throws IOException if there is trouble opening the DB
141      */
142     public DiphoneUnitDatabase(URL url, boolean isBinary) throws IOException {
143         // MS, 22.04.2005: Commented out the "if" clause:
144         // indexing is applied only when useNewIO is turned on and
145         // data is read from a FileInputStream. This is not true when useing
146         // the default settings but setting
147         // com.sun.speech.freetts.diphone.UnitDatabase.cacheType=demand
148         //if (!useIndexing || useCache) {
149             diphoneMap = new LinkedHashMap();
150             //}
151         InputStream is = Utilities.getInputStream(url);
152
153         indexName = getIndexName(url.toString());
154
155         if (isBinary) {
156             loadBinary(is);
157         } else {
158             loadText(is);
159         }
160         is.close();
161         sampleInfo = new SampleInfo(sampleRate, numChannels,
162                 residualFold, lpcMin, lpcRange, 0.0f);
163     }
164
165     /**
166      * Return the information about the sample data
167      * for this database.
168      *
169      * @return the sample info
170      */
171
172     SampleInfo getSampleInfo() {
173         return sampleInfo;
174     }
175
176
177     /**
178      * Returns the index name from the databaseName.
179      *
180      * @param databaseName the database name
181      *
182      * @return the index name or null if the database is not
183      *         a binary database.
184      *
185      * [[[ TODO the index should probably be incorporated into the
186      * binary database ]]]
187      */
188     private String getIndexName(String databaseName) {
189         String indexName = null;
190         if (databaseName.lastIndexOf(".") != -1) {
191             indexName = databaseName.substring(0,
192                     databaseName.lastIndexOf(".")) + ".idx";
193         }
194         return indexName;
195     }
196
197     /**
198      * Loads the database from the given input stream.
199      *
200      * @param is the input stream
201      */
202     private void loadText(InputStream is) {
203         BufferedReader reader;
204         String line;
205
206         if (is == null) {
207             throw new Error("Can't load diphone db file.");
208         }
209
210         reader = new BufferedReader(new InputStreamReader(is));
211         try {
212             line = reader.readLine();
213             lineCount++;
214             while (line != null) {
215                 if (!line.startsWith("***")) {
216                     parseAndAdd(line, reader);
217                 }
218                 line = reader.readLine();
219             }
220             reader.close();
221         } catch (IOException e) {
222             throw new Error(e.getMessage() + " at line " + lineCount);
223         } finally {
224         }
225     }
226
227     /**
228      * Parses and process the given line. Used to process the text
229      * form of the database.
230      *
231      * @param line the line to process
232      * @param reader the source for the lines
233      */
234     private void parseAndAdd(String line, BufferedReader reader) {
235         try {
236             StringTokenizer tokenizer = new StringTokenizer(line," ");
237             String tag = tokenizer.nextToken();
238             if (tag.equals("NAME")) {
239                 name = tokenizer.nextToken();
240             } else if (tag.equals("SAMPLE_RATE")) {
241                 sampleRate = Integer.parseInt(tokenizer.nextToken());
242             } else if (tag.equals("NUM_CHANNELS")) {
243                 numChannels = Integer.parseInt(tokenizer.nextToken());
244             } else if (tag.equals("LPC_MIN")) {
245                 lpcMin  = Float.parseFloat(tokenizer.nextToken());
246             } else if (tag.equals("COEFF_MIN")) {
247                 lpcMin  = Float.parseFloat(tokenizer.nextToken());
248             } else if (tag.equals("COEFF_RANGE")) {
249                 lpcRange  = Float.parseFloat(tokenizer.nextToken());
250             } else if (tag.equals("LPC_RANGE")) {
251                 lpcRange  = Float.parseFloat(tokenizer.nextToken());
252             } else if (tag.equals("ALIAS")) {
253             String name = tokenizer.nextToken();
254             String origName = tokenizer.nextToken();
255             AliasDiphone diphone = new AliasDiphone(name, origName);
256             add(diphone);
257         } else if (tag.equals("DIPHONE")) {
258                 String name = tokenizer.nextToken();
259                 int start  = Integer.parseInt(tokenizer.nextToken());
260                 int mid = Integer.parseInt(tokenizer.nextToken());
261                 int end = Integer.parseInt(tokenizer.nextToken());
262                 int numSamples = (end - start);
263                 int midPoint = mid - start;
264
265                 if (numChannels <= 0) {
266                     throw new Error("For diphone '"+name+"': Bad number of channels " + numChannels);
267                 }
268
269                 if (numSamples <= 0) {
270                     throw new Error("For diphone '"+name+"': Bad number of samples " + numSamples);
271                 }
272
273                 Sample[] samples = new Sample[numSamples];
274
275                 for (int i = 0; i < samples.length; i++) {
276                     samples[i] = new Sample(reader, numChannels);
277                 }
278                 Diphone diphone = new Diphone(name, samples, midPoint);
279                 add(diphone);
280             } else {
281                 throw new Error("Unsupported tag " + tag);
282             }
283         } catch (NoSuchElementException nse) {
284             throw new Error("Error parsing db " + nse.getMessage());
285         } catch (NumberFormatException nfe) {
286             throw new Error("Error parsing numbers in db " + nfe.getMessage());
287         }
288     }
289
290
291     /**
292      * Adds the given diphone to the DB. Diphones are kept in a map so
293      * they can be accessed by name.
294      *
295      * @param diphone the diphone to add.
296      */
297     private void add(Diphone diphone) {
298         if (diphone instanceof AliasDiphone) {
299             AliasDiphone adiph = (AliasDiphone) diphone;
300             Diphone original = (Diphone) 
301                 diphoneMap.get(adiph.getOriginalName());
302             if (original != null) {
303                 adiph.setOriginalDiphone(original);
304             } else {
305                 // No original was found for this alias
306                 // -- complain, and ignore
307                 if (LOGGER.isLoggable(Level.FINER)) {
308                     LOGGER.finer("For diphone alias "
309                         +adiph.getName()+", could not find original "
310                         +adiph.getOriginalName());
311                 }
312                 return;
313             }
314         }
315         diphoneMap.put(diphone.getName(), diphone);
316         if (defaultDiphone == null) {
317             defaultDiphone = diphone;
318         }
319     }
320
321     /**
322      * Looks up the diphone with the given name.
323      *
324      * @param unitName the name of the diphone to look for
325      *
326      * @return the diphone or the defaultDiphone if not found.
327      */
328     public Diphone getUnit(String unitName) {
329         Diphone diphone = null;
330
331         if (useIndexing) {
332             diphone = getFromCache(unitName);
333             if (diphone == null) {
334                 int index = getIndex(unitName);
335                 if (index != -1) {
336                     mbb.position(index);
337                     try {
338                         diphone = Diphone.loadBinary(mbb);
339                         if (diphone != null) {
340                 // If diphone is an alias, must also get the original
341                 if (diphone instanceof AliasDiphone) {
342                     AliasDiphone adiph = (AliasDiphone) diphone;
343                     Diphone original = getUnit(adiph.getOriginalName());
344                     if (original != null) {
345                         adiph.setOriginalDiphone(original);
346                         putIntoCache(unitName, adiph);
347                     } else {
348                         // No original was found for this alias
349                         // -- complain, and ignore
350                         if (LOGGER.isLoggable(Level.FINER)) {
351                             LOGGER.finer("For diphone alias "
352                                 +adiph.getName()+", could not find original "
353                                 +adiph.getOriginalName());
354                         }
355                         diphone = null;
356                     }
357                 } else { // a normal diphone
358                     putIntoCache(unitName, diphone);
359                 }
360                         }
361                     } catch (IOException ioe) {
362                         System.err.println("Can't load diphone " +
363                                 unitName);
364                         diphone = null;
365                     }
366                 }
367             }
368         } else {
369             diphone = (Diphone) diphoneMap.get(unitName);
370         }
371
372         if (diphone == null) {
373             System.err.println("Can't find diphone " + unitName);
374             diphone = defaultDiphone;
375         }
376
377         return diphone;
378     }
379
380     /**
381      * Gets the named diphone from the cache. If we are using soft
382      * caching, the reference may be a soft/weak reference so check to
383      * see if the reference is still valid, if so return it; otherwise
384      * invalidate it. Note that we have not had good success with weak
385      * caches so far. The goal is to reduce the minimum required
386      * memory footprint as far as possible while not compromising
387      * performance. In small memory systems, the weak cache would
388      * likely be reclaimed, giving us lower performance but with the
389      * ability to still be able to run. In reality, the soft caches
390      * did not help much. They just did not work correctly. 
391      * [[[ TODO: test weak/soft cache behavior with new versions of
392      * the runtime to see if their behavior has improved ]]]
393      *
394      * @param name the name of the diphone
395      *
396      * @return the diphone or <code> null </code>  if not in the cache
397      */
398     private Diphone getFromCache(String name) {
399         if (diphoneMap == null) {
400             return null;
401         }
402         Diphone diphone = null;
403
404         if (useSoftCache) {
405             Reference ref  = (Reference) diphoneMap.get(name);
406             if (ref != null) {
407                 diphone = (Diphone) ref.get();
408                 if (diphone == null) {
409                     diphoneMap.remove(name);
410                 } else {
411                 }
412             }
413         } else {
414             diphone = (Diphone) diphoneMap.get(name);
415         }
416         return diphone;
417     }
418
419     /**
420      * Puts the diphone in the cache.
421      *
422      * @param diphoneName the name of the diphone 
423      * @param diphone the diphone to put in the cache
424      */
425     private void putIntoCache(String diphoneName, Diphone diphone) {
426         if (diphoneMap == null) {
427             return ;
428         }
429         if (useSoftCache) {
430             diphoneMap.put(diphoneName, new WeakReference(diphone));
431         } else {
432             diphoneMap.put(diphoneName, diphone);
433         }
434     }
435
436     /**
437      * Dumps the soft ref cache.
438      */
439     private void dumpCacheSize() {
440         int empty = 0;
441         int full = 0;
442         System.out.println("Entries: " + diphoneMap.size());
443         for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
444             Reference ref = (Reference) i.next();
445             if (ref.get() == null) {
446                 empty++;
447             } else {
448                 full++;
449             }
450         }
451         System.out.println("   empty: " + empty);
452         System.out.println("    full: " + full);
453     }
454
455     
456     /**
457      * Returns the name of this DiphoneUnitDatabase.
458      */
459     public String getName() {
460         return name;
461     }
462     
463     /**
464      * Dumps the diphone database.
465      */
466     public void dump() {
467         System.out.println("Name        " + name);
468         System.out.println("SampleRate  " + sampleRate);
469         System.out.println("NumChannels " + numChannels);
470         System.out.println("lpcMin      " + lpcMin);
471         System.out.println("lpcRange    " + lpcRange);
472
473         for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
474             Diphone diphone = (Diphone) i.next();
475             diphone.dump();
476         }
477     }
478
479     /**
480      * Dumps a binary form of the database.
481      *
482      * @param path the path to dump the file to
483      */
484     public void dumpBinary(String path) {
485         try {
486             FileOutputStream fos = new FileOutputStream(path);
487             DataOutputStream os = new DataOutputStream(fos);
488             int written;
489
490             os.writeInt(MAGIC);
491             os.writeInt(VERSION);
492             os.writeInt(sampleRate);
493             os.writeInt(numChannels);
494             os.writeFloat(lpcMin);
495             os.writeFloat(lpcRange);
496             os.writeInt(diphoneMap.size());
497
498             for (Iterator i = diphoneMap.values().iterator(); i.hasNext();) {
499                 Diphone diphone = (Diphone) i.next();
500                 diphone.dumpBinary(os);
501             }
502             os.flush();
503             fos.close();
504
505         } catch (FileNotFoundException fe) {
506             throw new Error("Can't dump binary database " +
507                     fe.getMessage());
508         } catch (IOException ioe) {
509             throw new Error("Can't write binary database " +
510                     ioe.getMessage());
511         }
512     }
513
514     /**
515      * Dumps a binary index. The database index is used if our
516      * cacheType is not set to 'preload' and we are loading a binary
517      * database. The index is a simple mapping of diphone names (the
518      * key) to the file position in the database. In situations where
519      * the entire database is not preloaded, this index can be loaded
520      * and used to provide quicker startup (since only the index need
521      * be loaded at startup) and quick access to the diphone data.
522      *
523      * @param path the path to dump the file to
524      */
525     void dumpBinaryIndex(String path) {
526         try {
527             FileOutputStream fos = new FileOutputStream(path);
528             DataOutputStream dos = new DataOutputStream(fos);
529
530             dos.writeInt(INDEX_MAGIC);
531             dos.writeInt(diphoneIndex.keySet().size());
532
533             for (Iterator i = diphoneIndex.keySet().iterator(); i.hasNext();) {
534                 String key = (String) i.next();
535                 int pos = ((Integer) diphoneIndex.get(key)).intValue();
536                 dos.writeUTF(key);
537                 dos.writeInt(pos);
538             }
539             dos.close();
540
541         } catch (FileNotFoundException fe) {
542             throw new Error("Can't dump binary index " +
543                     fe.getMessage());
544         } catch (IOException ioe) {
545             throw new Error("Can't write binary index " +
546                     ioe.getMessage());
547         }
548     }
549
550     /**
551      * Loads a binary index.
552      *
553      * @param url the location  of the binary index file
554      */
555     private void loadBinaryIndex(URL url) {
556
557         diphoneIndex = new HashMap();
558
559         try {
560             InputStream is = Utilities.getInputStream(url);
561             DataInputStream dis = new DataInputStream(is);
562
563             if (dis.readInt() != INDEX_MAGIC) {
564                 throw new Error("Bad index file format");
565             }
566
567             int size = dis.readInt();
568
569             for (int i = 0; i < size; i++) {
570                 String diphoneName = dis.readUTF();
571                 int pos = dis.readInt();
572                 diphoneIndex.put(diphoneName, new Integer(pos));
573             }
574             dis.close();
575
576         } catch (FileNotFoundException fe) {
577             throw new Error("Can't load binary index " +
578                     fe.getMessage());
579         } catch (IOException ioe) {
580             throw new Error("Can't read binary index " +
581                     ioe.getMessage());
582         }
583     }
584
585     /**
586      * Gets the index for the given diphone.
587      *
588      * @param diphone the name of the diphone
589      *
590      * @return the index into the database for the diphone
591      */
592     private int getIndex(String diphone) {
593         Integer index = (Integer) diphoneIndex.get(diphone);
594         if (index != null) {
595             int idx = index.intValue();
596             if (defaultIndex == -1) {
597                 defaultIndex = idx;
598             }
599             return idx;
600         } else {
601             System.out.println("Can't find index entry for " + diphone);
602             return defaultIndex;
603         }
604     }
605
606
607
608     /**
609      * Loads a binary file from the input stream. 
610      * <p>
611      * Note that we currently have four! methods of loading up the
612      * database. We were interested in the performance characteristics
613      * of the various methods of loading the database so we coded it
614      * all up.
615      *
616      * @param is the input stream to read the database
617      *          from
618      *
619      * @throws IOException if there is trouble opening the DB
620      * 
621      */
622     private void loadBinary(InputStream is) throws IOException {
623         // we get better performance if we can map the file in
624         // 1.0 seconds vs. 1.75 seconds, but we can't
625         // always guarantee that we can do that.
626         if (useNewIO && is instanceof FileInputStream) {
627             FileInputStream fis = (FileInputStream) is;
628             if (useIndexing) {
629                 loadBinaryIndex(new URL(indexName));
630                 mapDatabase(fis);
631             } else {
632                 loadMappedBinary(fis);
633             }
634         } else {
635         useIndexing = false; // just to make this clear
636             DataInputStream dis = new DataInputStream(
637                     new BufferedInputStream(is));
638             loadBinary(dis);
639         }
640     }
641
642
643     /**
644      * Loads the binary data from the given input stream.
645      *
646      * @param dis the data input stream.
647      */
648     private void loadBinary(DataInputStream dis) throws IOException {
649         int size;
650         if (dis.readInt() != MAGIC)  {
651             throw new Error("Bad magic in db");
652         }
653         if (dis.readInt() != VERSION)  {
654             throw new Error("Bad VERSION in db");
655         }
656
657         sampleRate = dis.readInt();
658         numChannels = dis.readInt();
659         lpcMin = dis.readFloat();
660         lpcRange = dis.readFloat();
661         size = dis.readInt();
662
663         for (int i = 0; i < size; i++) {
664             Diphone diphone = Diphone.loadBinary(dis);
665             add(diphone);
666         }
667     }
668
669
670     /**
671      * Loads the database from the given FileInputStream.
672      *
673      * @param is the InputStream to load the database from
674      *
675      * @throws IOException if there is trouble opening the DB
676      */
677     private void loadMappedBinary(FileInputStream is) throws IOException {
678         FileChannel fc = is.getChannel();
679
680         MappedByteBuffer bb = 
681             fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
682         bb.load();
683         loadDatabase(bb);
684         is.close();
685     }
686
687     /**
688      * Maps the database from the given FileInputStream.
689      *
690      * @param is the InputStream to load the database from
691      *
692      * @throws IOException if there is trouble opening the DB
693      */
694     private void mapDatabase(FileInputStream is) throws IOException {
695         FileChannel fc = is.getChannel();
696         mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
697         mbb.load();
698         loadDatabaseHeader(mbb);
699     }
700
701     /**
702      * Loads the database header from the given byte buffer.
703      *
704      * @param bb the byte buffer to load the db from
705      *
706      * @throws IOException if there is trouble opening the DB
707      */
708     private void loadDatabaseHeader(ByteBuffer bb) throws IOException {
709         if (bb.getInt() != MAGIC)  {
710             throw new Error("Bad magic in db");
711         }
712         if (bb.getInt() != VERSION)  {
713             throw new Error("Bad VERSION in db");
714         }
715
716         sampleRate = bb.getInt();
717         numChannels = bb.getInt();
718         lpcMin = bb.getFloat();
719         lpcRange = bb.getFloat();
720     }
721
722     /**
723      * Loads the database from the given byte buffer.
724      *
725      * @param bb the byte buffer to load the db from
726      *
727      * @throws IOException if there is trouble opening the DB
728      */
729     private void loadDatabase(ByteBuffer bb) throws IOException {
730         int size;
731         loadDatabaseHeader(bb);
732         size = bb.getInt();
733
734         diphoneIndex = new HashMap();
735         for (int i = 0; i < size; i++) {
736             int pos = bb.position();
737             Diphone diphone = Diphone.loadBinary(bb);
738             add(diphone);
739             diphoneIndex.put(diphone.getName(), new Integer(pos));
740         }
741     }
742
743     /**
744      * Compares this database to another. This is used for testing.
745      * With this method we can load up two databases (one perhaps from
746      * a text source and one from a binary source) and compare to
747      * verify that the dbs are identical
748      *
749      * @param other the other database
750      *
751      * @return <code>true</code>  if the DBs are identical; 
752      *          otherwise <code>false</code> 
753      */
754     public boolean compare(DiphoneUnitDatabase other) {
755         if (sampleRate != other.sampleRate) {
756             return false;
757         }
758
759         if (numChannels != other.numChannels) {
760             return false;
761         }
762
763         if (lpcMin != other.lpcMin) {
764             return false;
765         }
766
767         if (lpcRange != other.lpcRange) {
768             return false;
769         }
770
771         for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
772             Diphone diphone = (Diphone) i.next();
773             Diphone otherDiphone = (Diphone) other.getUnit(diphone.getName());
774             if (!diphone.compare(otherDiphone)) {
775                 System.out.println("Diphones differ:");
776                 System.out.println("THis:");
777                 diphone.dump();
778                 System.out.println("Other:");
779                 otherDiphone.dump();
780                 return false;
781             }
782         }
783         return true;
784     }
785
786
787     /**
788      * Manipulates a DiphoneUnitDatabase. This program is typically
789      * used to generate the binary form (with index) of the 
790      * DiphoneUnitDatabase from the text form. Additionally, this program 
791      * can be used to compare two databases to see if they are
792      * identical (used for testing).
793      *
794      * <p>
795      * <b> Usage </b>
796      * <p>
797      *  <code> java com.sun.speech.freetts.diphone.DiphoneUnitDatabase
798      *  [options]</code> 
799      * <p>
800      * <b> Options </b>
801      * <p>
802      *    <ul>
803      *          <li> <code> -src path </code> provides a directory
804      *          path to the source text for the database
805      *          <li> <code> -dest path </code> provides a directory
806      *          for where to place the resulting binaries
807      *          <li> <code> -generate_binary [filename] </code> 
808      *          reads in the text
809      *          version of the database and generates the binary
810      *          version of the database.
811      *          <li> <code> -compare </code>  Loads the text and
812      *          binary versions of the database and compares them to
813      *          see if they are equivalent.
814      *          <li> <code> -showTimes </code> shows timings for any
815      *          loading, comparing or dumping operation
816      *    </ul>
817      * 
818      */
819     public static void main(String[] args) {
820         boolean showTimes = false;
821         String srcPath = ".";
822         String destPath = ".";
823         
824         try {
825             if (args.length > 0) {
826                 BulkTimer timer = BulkTimer.LOAD;
827                 timer.start();
828                 for (int i = 0 ; i < args.length; i++) {
829                     if (args[i].equals("-src")) {
830                         srcPath = args[++i];
831                     } else if (args[i].equals("-dest")) {
832                         destPath = args[++i];
833                     } else if (args[i].equals("-generate_binary")) {
834                          String name = "diphone_units.txt";
835                          if (i + 1 < args.length) {
836                              String nameArg = args[++i];
837                              if (!nameArg.startsWith("-")) {
838                                  name = nameArg;
839                              }
840                          } 
841
842                          int suffixPos = name.lastIndexOf(".txt");
843
844                          String binaryName = "diphone_units.bin";
845                          if (suffixPos != -1) {
846                              binaryName = name.substring(0, suffixPos) + ".bin";
847                          }
848
849                          String indexName = "diphone_units.idx";
850
851                          if (suffixPos != -1) {
852                              indexName = name.substring(0, suffixPos) + ".idx";
853                          }
854
855                          System.out.println("Loading " + name);
856                          timer.start("load_text");
857                          DiphoneUnitDatabase udb = new DiphoneUnitDatabase(
858                                 new URL("file:"
859                                         + srcPath + "/" + name), false);
860                          timer.stop("load_text");
861
862                          System.out.println("Dumping " + binaryName);
863                          timer.start("dump_binary");
864                          udb.dumpBinary(destPath + "/" + binaryName);
865                          timer.stop("dump_binary");
866
867                          timer.start("load_binary");
868                          DiphoneUnitDatabase budb = 
869                             new DiphoneUnitDatabase(
870                                     new URL("file:"
871                                             + destPath + "/" + binaryName),
872                                     true);
873                          timer.stop("load_binary");
874
875                          System.out.println("Dumping " + indexName);
876                          timer.start("dump index");
877                          budb.dumpBinaryIndex(destPath + "/" + indexName);
878                          timer.stop("dump index");
879                     } else if (args[i].equals("-compare")) {
880
881                         timer.start("load_text");
882                          DiphoneUnitDatabase udb = new DiphoneUnitDatabase(
883                                 new URL("file:./diphone_units.txt"), false);
884                         timer.stop("load_text");
885
886                         timer.start("load_binary");
887                         DiphoneUnitDatabase budb = 
888                             new DiphoneUnitDatabase(
889                                     new URL("file:./diphone_units.bin"), true);
890                         timer.stop("load_binary");
891
892                         timer.start("compare");
893                         if (udb.compare(budb)) {
894                             System.out.println("other compare ok");
895                         } else {
896                             System.out.println("other compare different");
897                         }
898                         timer.stop("compare");
899                     } else if (args[i].equals("-showtimes")) {
900                         showTimes = true;
901                     } else {
902                         System.out.println("Unknown option " + args[i]);
903                     }
904                 }
905                 timer.stop();
906                 if (showTimes) {
907                     timer.show("DiphoneUnitDatabase");
908                 }
909             } else {
910                 System.out.println("Options: ");
911                 System.out.println("    -src path");
912                 System.out.println("    -dest path");
913                 System.out.println("    -compare");
914                 System.out.println("    -generate_binary");
915                 System.out.println("    -showTimes");
916             }
917         } catch (IOException ioe) {
918             System.err.println(ioe);
919         }
920     }
921 }