001/**
002 * Portions Copyright 2003 Sun Microsystems, Inc.
003 * Portions Copyright 1999-2001 Language Technologies Institute, 
004 * Carnegie Mellon University.
005 * All Rights Reserved.  Use is subject to license terms.
006 * 
007 * See the file "license.terms" for information on usage and
008 * redistribution of this file, and for a DISCLAIMER OF ALL 
009 * WARRANTIES.
010 */
011package com.sun.speech.freetts.diphone;
012import java.io.BufferedInputStream;
013import java.io.BufferedReader;
014import java.io.DataInputStream;
015import java.io.DataOutputStream;
016import java.io.FileInputStream;
017import java.io.FileNotFoundException;
018import java.io.FileOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.lang.ref.Reference;
023import java.lang.ref.WeakReference;
024import java.net.URL;
025import java.nio.ByteBuffer;
026import java.nio.MappedByteBuffer;
027import java.nio.channels.FileChannel;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.LinkedHashMap;
031import java.util.Map;
032import java.util.NoSuchElementException;
033import java.util.StringTokenizer;
034import java.util.logging.Level;
035import java.util.logging.Logger;
036
037import com.sun.speech.freetts.relp.Sample;
038import com.sun.speech.freetts.relp.SampleInfo;
039import com.sun.speech.freetts.util.BulkTimer;
040import com.sun.speech.freetts.util.Utilities;
041
042/**
043 * Represents and manages the unit data for all diphones.  The diphone
044 * data set is stored in a set of data files. These data are loaded by this
045 * class into internal data structures before diphone synthesis can
046 * occur. 
047 * <p>
048 *The diphone data set is one of the largest sets of data that
049 * needs to be loaded by the synthesizer and therefore can add to the
050 * overall startup time for any system using this database.  For
051 * certain applications, the startup time is a critical spec that
052 * needs to be optimized, while for other applications, startup time
053 * is inconsequential.  This class provides settings (via system
054 * properties) that control how the database is to be loaded so that
055 * applications can tune for quick startup or optimal run time.
056 * <p>
057 * This class serves also as a testbed for comparing performance of
058 * the traditional java binary I/O and the new io ( <code>java.nio </code>)
059 * package.
060 * <p>
061 * <p> A diphone database can be loaded from a text data file, or a
062 * binary datafile.  The binary version loads significantly faster
063 * than the text version. Additionally, a binary index can be
064 * generated and used to reduce overall memory footprint.
065 * <p> 
066 * <p>
067 * A DiphoneUnitDatabase contains an array of frames, and an aray of
068 * residuals. The frames are the samples of the wave, and the
069 * residuals are for linear predictive coding use. This is called
070 * "cst_sts" (a struct) in flite.
071 * <p>
072 * Note that if 'com.sun.speech.freetts.useNewIO' is set to true and
073 * the input type is binary, than the JDK1.4+ new IO api is used to
074 * load the database.
075 * <p>
076 * The system property
077 * <pre>
078 *      com.sun.speech.freetts.diphone.UnitDatabase.cacheType 
079 * </pre>
080 *
081 * can be set to one of:
082 *
083 * <ul>
084 * <li> preload: database is loaded at startup
085 * <li> demand: database is loaded on demand
086 * <li> hard: database is loaded on demand but cached
087 * <li> soft: database is loaded on demand but cached with soft references
088 * </ul>
089 *
090 * This <code> cacheType </code> setting controls how the database is
091 * loaded. The default is to 'preload' the database. This setting
092 * gives best runtime performance but with longer initial startup
093 * cost.  
094 */
095public class DiphoneUnitDatabase {
096    /** Logger instance. */
097    private static final Logger LOGGER =
098        Logger.getLogger(DiphoneUnitDatabase.class.getName());
099
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}