/**
 * Portions Copyright 2001 Sun Microsystems, Inc.
 * Portions Copyright 1999-2001 Language Technologies Institute, 
 * Carnegie Mellon University.
 * All Rights Reserved.  Use is subject to license terms.
 * 
 * See the file "license.terms" for information on usage and
 * redistribution of this file, and for a DISCLAIMER OF ALL 
 * WARRANTIES.
 */
package com.sun.speech.freetts;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

import com.sun.speech.freetts.audio.AudioPlayer;
import com.sun.speech.freetts.lexicon.Lexicon;
import com.sun.speech.freetts.relp.LPCResult;
import com.sun.speech.freetts.util.BulkTimer;
import com.sun.speech.freetts.util.Utilities;


/**
 * Performs text-to-speech using a series of
 * <code>UtteranceProcessors</code>. It is the main conduit to the FreeTTS
 * speech synthesizer. It can perform TTS on ASCII text,
 * a JSML document, an <code>InputStream</code>, or a 
 * <code>FreeTTSSpeakable</code>, by invoking the method <code>speak</code>.
 *
 * <p>Before a Voice can perform TTS, it must have a
 * <code>Lexicon</code>, from which it gets the vocabulary, and
 * an <code>AudioPlayer</code>, to which it sends the synthesized output.
 *
 * <p><b>Example</b> (using the <code>CMUDiphoneVoice</code>,
 * <code>CMULexicon</code> and <code>JavaClipAudioPlayer</code>):
 *
 * <pre>
 * Voice voice = new CMUDiphoneVoice();
 *
 * // sets the Lexicon
 * voice.setLexicon(new CMULexicon());
 *
 * // sets the AudioPlayer
 * voice.setAudioPlayer(new JavaClipAudioPlayer());
 *
 * // loads the Voice
 * voice.allocate();
 *
 * // start talking
 * voice.speak("I can talk forever without getting tired!");
 * </pre>
 *
 *
 * <p>A user can override the AudioPlayer to use by defining the
 * "com.sun.speech.freetts.voice.defaultAudioPlayer" system property.
 * The value of this property must be the name of a class that
 * implements the AudioPlayer interface, and which also has a no-arg
 * constructor.
 *
 * @see VoiceManager
 * @see VoiceDirectory
 */
public abstract class Voice implements UtteranceProcessor, Dumpable {
    /** Logger instance. */
    private static final Logger LOGGER =
        Logger.getLogger(Voice.class.getName());
    
    /**
     * Constant that describes the name of the unit database used by
     * this voice.
     */
    public final static String DATABASE_NAME = "databaseName";
    
    private List utteranceProcessors;
    private Map featureProcessors;
    private FeatureSetImpl features;
    private boolean metrics = false;
    private boolean detailedMetrics = false;
    private boolean dumpUtterance = false;
    private boolean dumpRelations = false;
    private String runTitle = "unnamed run";
    private Lexicon lexicon = null;
    private AudioPlayer defaultAudioPlayer = null;
    private AudioPlayer audioPlayer = null;
    private UtteranceProcessor audioOutput;
    private OutputQueue outputQueue = null;
    private String waveDumpFile = null;
    private BulkTimer runTimer = new BulkTimer();
    private BulkTimer threadTimer = new BulkTimer();
    private boolean externalOutputQueue = false;
    private boolean externalAudioPlayer = false;


    private float nominalRate = 150;	// nominal speaking rate for this voice
    private float pitch = 100;		// pitch baseline (hertz)
    private float range = 10;		// pitch range (hertz)
    private float pitchShift = 1;	// F0 Shift
    private float volume = 0.8f;	// the volume (range 0 to 1)
    private float durationStretch = 1f;	// the duration stretch

    private boolean loaded = false;

    private String name = "default_name";
    private Age age = Age.DONT_CARE;
    private Gender gender = Gender.DONT_CARE;
    private String description = "default description";
    private Locale locale = Locale.getDefault();
    private String domain = "general";
    private String style = "standard";
    private String organization = "unknown";

    /**
     * Prefix for System property names.
     */
    public final static String PROP_PREFIX = "com.sun.speech.freetts.voice.";

    /**
     * Feature name for the silence phone string.
     */
    public final static String FEATURE_SILENCE = "silence";

    /**
     * Feature name for the join type string.
     */
    public final static String FEATURE_JOIN_TYPE = "join_type";

    /**
     * Feature name for the default AudioPlayer class to use.
     */
    public final static String DEFAULT_AUDIO_PLAYER =
            PROP_PREFIX + "defaultAudioPlayer";    


    /**
     * The default class to use for the DEFAULT_AUDIO_PLAYER.
     */
    public final static String DEFAULT_AUDIO_PLAYER_DEFAULT =
            "com.sun.speech.freetts.audio.JavaStreamingAudioPlayer";
    

    /**
     * Creates a new Voice. Utterances are sent to an
     * output queue to be rendered as audio.  Utterances are placed
     * on the queue by an output thread. This
     * queue is usually created via a call to 'createOutputThread,'
     * which creates a thread that waits on the queue and sends the
     * output to the audio player associated with this voice. If
     * the queue is null, the output is rendered in the calling
     * thread.
     *
     * @see #createOutputThread
     */
    public Voice() {
        /* Make the utteranceProcessors a synchronized list to avoid
         * some threading issues.
         */
        utteranceProcessors = Collections.synchronizedList(new ArrayList());
	features = new FeatureSetImpl();
	featureProcessors = new HashMap();

	try {
	    nominalRate = Float.parseFloat(
		    Utilities.getProperty(PROP_PREFIX + "speakingRate","150"));
	    pitch = Float.parseFloat(
		    Utilities.getProperty(PROP_PREFIX + "pitch","100"));
	    range = Float.parseFloat(
		    Utilities.getProperty(PROP_PREFIX + "range","10"));
	    volume = Float.parseFloat(
		    Utilities.getProperty(PROP_PREFIX + "volume","1.0"));
	} catch (SecurityException se) {
	     // can't get properties, just use defaults
	}
        outputQueue = null;
        audioPlayer = null;
        defaultAudioPlayer = null;
    }

    /**
     * Creates a new Voice like above, except that it also
     * stores the properties of the voice.
     * @param name the name of the voice
     * @param gender the gender of the voice
     * @param age the age of the voice
     * @param description a human-readable string providing a
     * description that can be displayed for the users.
     * @param locale the locale of the voice
     * @param domain the domain of this voice.  For example,
     * @param organization the organization which created the voice
     * &quot;general&quot;, &quot;time&quot;, or
     * &quot;weather&quot;.
     *
     * @see #Voice()
     */
    public Voice(String name, Gender gender, Age age,
            String description, Locale locale, String domain,
            String organization) {
        this();
        setName(name);
        setGender(gender);
        setAge(age);
        setDescription(description);
        setLocale(locale);
        setDomain(domain);
        setOrganization(organization);
    }


    /**
     * Speaks the given text.
     *
     * @param text the text to speak
     *
     * @return <code>true</code> if the given text is spoken properly;
     *   otherwise <code>false</code>
     */
    public boolean speak(String text) {
	return speak(new FreeTTSSpeakableImpl(text));
    }


    /**
     * Speaks the given document.
     *
     * @param doc the JSML document to speak
     *
     * @return <code>true</code> if the given document is spoken properly;
     *   otherwise <code>false</code>
     */
    public boolean speak(Document doc) {
	return speak(new FreeTTSSpeakableImpl(doc));
    }

    
    /**
     * Speaks the input stream.
     *
     * @param inputStream the inputStream to speak
     *
     * @return <code>true</code> if the given input stream is spoken properly;
     *   otherwise <code>false</code>
     */
    public boolean speak(InputStream inputStream) {
	return speak(new FreeTTSSpeakableImpl(inputStream));
    }


    /**
     * Speak the given queue item. This is a synchronous method that
     * does not return until the speakable is completely
     * spoken or has been cancelled.
     * 
     * @param speakable the item to speak
     *
     * @return <code>true</code> if the utterance was spoken properly,
     *         <code>false</code> otherwise
     */
    public boolean speak(FreeTTSSpeakable speakable) {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("speak(FreeTTSSpeakable) called");
        }
        boolean ok = true;
	boolean posted = false;

	getAudioPlayer().startFirstSampleTimer();

	for (Iterator i = tokenize(speakable); 
             !speakable.isCompleted() && i.hasNext() ; ) {
	    try {
		Utterance utterance = (Utterance) i.next();
		if (utterance != null) {
		    processUtterance(utterance);
		    posted = true;
		}
	    } catch (ProcessException pe) {
		ok = false;
	    }
	}
	if (ok && posted) {
	    runTimer.start("WaitAudio");
            ok = speakable.waitCompleted();
            runTimer.stop("WaitAudio");
	}
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("speak(FreeTTSSpeakable) completed");
        }
	return ok;
    }


    /**
     * @deprecated  As of FreeTTS 1.2, replaced by {@link #allocate}.
     */
    public void load() {
        allocate();
    }
    
    /**
     * Allocate this Voice. It loads the lexicon and the
     * audio output handler, and creates an audio output thread by
     * invoking <code>createOutputThread()</code>, if
     * one is not already created. It then calls the <code>loader()</code>
     * method to load Voice-specific data, which include utterance processors.
     */
    public void allocate() {
	if (isLoaded()) {
	    return;
	}
	BulkTimer.LOAD.start();


	if (!lexicon.isLoaded()) {
	    try {
		lexicon.load();
	    } catch (IOException ioe) {
	        LOGGER.severe("Can't load voice " + ioe);
	        throw new Error(ioe);
	    }
	}

	try {
	    audioOutput = getAudioOutput();
	} catch (IOException ioe) {
	    LOGGER.severe("Can't load audio output handler for voice " + ioe);
	    throw new Error(ioe);
	}
	if (outputQueue == null) {
	    outputQueue = createOutputThread();
	}
	try {
	    loader();
	} catch (IOException ioe) {
	    LOGGER.severe("Can't load voice " + ioe);
	    throw new Error(ioe);
	}
	BulkTimer.LOAD.stop();
	if (isMetrics()) {
	    BulkTimer.LOAD.show("loading " + toString() + " for " +
		    getRunTitle());
	}
	setLoaded(true);
    }
   

    /**
     * Returns true if this voice is loaded.
     *
     * @return <code>true</code> if the voice is loaded;
     *   otherwise <code>false</code>
     */
    public boolean isLoaded() {
	return loaded;
    }

    /**
     * Sets the loaded state
     *
     * @param loaded the new loaded state
     *   otherwise <code>false</code>
     */
    protected void setLoaded(boolean loaded) {
	this.loaded = loaded;
    }

    /**
     * Processes the given Utterance by passing it to each
     * UtteranceProcessor managed by this Voice.  The
     * UtteranceProcessors are called in the order they were added to
     * the Voice.
     *
     * @param u the Utterance to process
     *
     * @throws ProcessException if an exception occurred while performing
     *   operations on the Utterance
     */
    public void processUtterance(Utterance u) throws ProcessException {
        UtteranceProcessor[] processors;

        if (utteranceProcessors == null) {
            return;
        }
        if (u == null) {
            throw new ProcessException("Utterance is null.");
        }

	runTimer.start("processing");
        processors = new UtteranceProcessor[utteranceProcessors.size()];
        processors = (UtteranceProcessor[])
            utteranceProcessors.toArray(processors);
        
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Processing Utterance: " + u.getString("input_text"));
        }
	try {
	    for (int i = 0; i < processors.length && 
                     !u.getSpeakable().isCompleted(); i++) {
                runProcessor(processors[i], u, runTimer);
            }
            if (!u.getSpeakable().isCompleted()) {
                if (outputQueue == null) {
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.fine("To AudioOutput");
                    }
                    outputUtterance(u, runTimer);
                } else {
                    runTimer.start("..post");
                    outputQueue.post(u);
                    runTimer.stop("..post");
                }
            }
	}  catch (ProcessException pe) {
	    System.err.println("Processing Utterance: " + pe);
	}  catch (Exception e) {
	    System.err.println("Trouble while processing utterance " + e);
	    e.printStackTrace();
	    u.getSpeakable().cancelled();
	}

        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Done Processing Utterance: " 
                    + u.getString("input_text"));
        }
	runTimer.stop("processing");

	if (dumpUtterance) {
	    u.dump("Utterance");
	}
	if (dumpRelations) {
	    u.dumpRelations("Utterance");
	}

	dumpASCII(u);
    }


    /**
     * Dumps the wave for the given utterance.
     * 
     * @param utterance the utterance of interest
     */
    private void dumpASCII(Utterance utterance) {
	if (waveDumpFile != null) {
	    LPCResult lpcResult = 
		(LPCResult) utterance.getObject("target_lpcres");
	    try {
		if (waveDumpFile.equals("-")) {
		    lpcResult.dumpASCII();
		} else {
		    lpcResult.dumpASCII(waveDumpFile);
		}
	    } catch (IOException ioe) {
		LOGGER.severe("Can't dump file to " + waveDumpFile + " " + ioe);
		throw new Error(ioe);
	    }
	}
    }


    /**
     * Creates an output thread that will asynchronously
     * output utterances that are generated by this voice (and other
     * voices).
     * 
     * @return the queue where utterances should be placed.
     */
    public static OutputQueue createOutputThread() {
	final OutputQueue queue =  new OutputQueue();
	Thread t = new Thread() {
	    public void run() {
		Utterance utterance = null;
		do {
		    utterance = queue.pend();
		    if (utterance != null) {
			Voice voice = utterance.getVoice();
		        if (LOGGER.isLoggable(Level.FINE)) {
		            LOGGER.fine("OUT: " 
		                    + utterance.getString("input_text"));
		        }
                        voice.outputUtterance(utterance, voice.threadTimer);
		    }
		} while (utterance != null);
	    }
	};
	t.setDaemon(true);
	t.start();
	return queue;
    }


    /**
     * Sends the given utterance to the audio output processor
     * associated with this voice. If the queue item associated with
     * this utterance is completed, then this set of utterances has
     * been cancelled or otherwise aborted and the utterance should
     * not be output.
     *
     * @param utterance the utterance to be output
     * @param timer the timer for gathering performance metrics
     *
     * @return  true if the utterance was output properly; otherwise
     *    false
     */
    private boolean outputUtterance(Utterance utterance, BulkTimer timer) {
	boolean ok = true;
	FreeTTSSpeakable speakable = utterance.getSpeakable();

	if (!speakable.isCompleted())  {
	    if (utterance.isFirst()) {
		getAudioPlayer().reset();
		speakable.started();
	        if (LOGGER.isLoggable(Level.FINE)) {
	            LOGGER.fine(" --- started ---");
	        }
	    }

	    // log("   utt: " + utterance.getString("input_text"));
	    try {
                if (!speakable.isCompleted()) {
                    runProcessor(audioOutput, utterance, timer);
                } else {
                    ok = false;
                }
	    }  catch (ProcessException pe) {
		ok = false;
	    }
	    if (ok && utterance.isLast()) {
		getAudioPlayer().drain();
                speakable.completed();
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.fine(" --- completed ---");
                }
	    } else if (!ok) {
		// getAudioPlayer().drain();
		speakable.cancelled();
	        if (LOGGER.isLoggable(Level.FINE)) {
	            LOGGER.fine(" --- cancelled ---");
	        }
	    } else {
	        if (LOGGER.isLoggable(Level.FINE)) {
	            LOGGER.fine(" --- not last: " + speakable.getText() 
	                    + " --- ");
	        }
            }
	        if (LOGGER.isLoggable(Level.FINE)) {
	            LOGGER.fine("Calling speakable.completed() on " 
	                    + speakable.getText());
	        }
        } else {
	    ok = false;
	        if (LOGGER.isLoggable(Level.FINE)) {
	            LOGGER.fine("STRANGE: speakable already completed: " 
	                    + speakable.getText());
	        }
	}
	return ok;
    }


    /**
     * Runs the given utterance processor.
     *
     * @param processor the processor to run.   If the processor
     *    is null, it is ignored
     * @param utterance the utterance to process
     *
     * @throws ProcessException if an exceptin occurs while processing
     *     the utterance
     */
    private void runProcessor(UtteranceProcessor processor, 
                              Utterance utterance, BulkTimer timer) 
        throws ProcessException {
	if (processor != null) {
	    String processorName = ".." + processor.toString();
	        if (LOGGER.isLoggable(Level.FINE)) {
	            LOGGER.fine("   Running " + processorName);
	        }
	    timer.start(processorName);
	    processor.processUtterance(utterance);
	    timer.stop(processorName);
	}
    }


    /**
     * Returns the tokenizer associated with this voice.
     *
     * @return the tokenizer
     */
    public abstract Tokenizer getTokenizer();


    /**
     * Return the list of UtteranceProcessor instances.  Applications
     * should use this to obtain and modify the contents of the
     * UtteranceProcessor list.
     *
     * @return a List containing UtteranceProcessor instances
     */
    public List getUtteranceProcessors() {
        return utteranceProcessors;
    }


    /**
     * Returns the feature set associated with this voice.
     *
     * @return the feature set.
     */
    public FeatureSet getFeatures() {
	return features;
    }


    /**
     * Starts a batch of utterances. Utterances are sometimes
     * batched in groups for timing purposes.
     *
     * @see #endBatch
     */
    public void startBatch() {
	runTimer.setVerbose(detailedMetrics);
	runTimer.start();
    }

    
    /**
     * Ends a batch of utterances.
     *
     * @see #startBatch
     */
    public void endBatch() {
	runTimer.stop();

	if (metrics) {
	    runTimer.show(getRunTitle() + " run");
	    threadTimer.show(getRunTitle() + " thread");
	    getAudioPlayer().showMetrics();
	    long totalMemory = Runtime.getRuntime().totalMemory();
	    LOGGER.info
		("Memory Use    : "
		 + (totalMemory - Runtime.getRuntime().freeMemory()) / 1024
		 + "k  of " + totalMemory / 1024 + "k");
	}
    }

    /**
     * Sets the output queue for this voice. If no output queue is set
     * for the voice when the voice is loaded, a queue and thread will
     * be created when the voice is loaded.  If the outputQueue is set
     * by an external entity by calling setOutputQueue, the caller is
     * responsible for shutting down the output thread. That is, if
     * you call 'setOutputQueue' then you are responsible for shutting
     * down the output thread on your own. This is necessary since the
     * output queue may be shared by a number of voices.
     *
     * <p>Utterances are placed on the
     *    queue to be output by an output thread. This queue is
     *    usually created via a call to 'createOutputThread' which
     *    creates a thread that waits on the queue and sends the
     *    output to the audio player associated with this voice. If
     *    the queue is null, the output is rendered in the calling
     *    thread.
     *
     * @param queue the output queue
     */
    public void setOutputQueue(OutputQueue queue) {
	externalOutputQueue = true;
	outputQueue = queue;
    }

    /**
     * Returns the output queue associated with this voice.
     * 
     * @return the output queue associated with this voice
     */
    public OutputQueue getOutputQueue() {
	return outputQueue;
    }

    /**
     * Loads voice specific data. Subclasses of voice should
     * implement this to perform class specific loading.
     */
    protected abstract void loader() throws IOException;

    /**
     * tokenizes the given the queue item.
     *
     * @return an iterator that will yield a series of utterances
     */
    private Iterator tokenize(FreeTTSSpeakable speakable) {
	return new FreeTTSSpeakableTokenizer(speakable).iterator();
    }
    
    /**
     * Converts the document to a string (a placeholder for more
     * sophisticated logic to be done).
     *
     * @param dom the jsml document
     *
     * @return the document as a string.
     */
    private String documentToString(Document dom) {
	StringBuffer buf = new StringBuffer();
	linearize(dom, buf);
	return buf.toString();
    }
    
    /**
     * Appends the text for this node to the given StringBuffer.
     *
     * @param n the node to traverse in depth-first order
     * @param buf the buffer to append text to
     */
    private  void linearize(Node n, StringBuffer buf) {
        StringBuffer endText = processNode(n, buf);
        for (Node child = n.getFirstChild();
             child != null;
             child = child.getNextSibling()) {
            linearize(child, buf);
        }

        if (endText != null) {
            buf.append(endText);
        }
    }

    /**
     * Adds text for just this node and returns any text that might
     * be needed to undo the effects of this node after it is
     * processed.
     *
     * @param n the node to traverse in depth-first order
     * @param buf the buffer to append text to
     *
     * @return a <code>String</code> containing text to undo the
     *   effects of the node
     */
    protected StringBuffer processNode(Node n, StringBuffer buf) {
        StringBuffer endText = null;
        
        int type = n.getNodeType();
        switch (type) {
            case Node.ATTRIBUTE_NODE:
                 break;
                 
            case Node.DOCUMENT_NODE:
                break;
                
            case Node.ELEMENT_NODE:
                // endText = processElement((Element) n, buf);
                break;
                
            case Node.TEXT_NODE:
                buf.append(((Text) n).getData());
                break;

            // Pass processing instructions (e.g., <?blah?>
            // right on to the synthesizer.  These types of things
            // probably should not be used.  Instead the 'engine'
            // element is probably the best thing to do.
            //
            case Node.PROCESSING_INSTRUCTION_NODE:
                break;
                
            // The document type had better be JSML.
            //
            case Node.DOCUMENT_TYPE_NODE:
                break;

            // I think NOTATION nodes are only DTD's.
            //
            case Node.NOTATION_NODE:
                break;

            // Should not get COMMENTS because the JSMLParser
            // ignores them.
            //
            case Node.COMMENT_NODE:
                break;

            // Should not get CDATA because the JSMLParser is
            // coalescing.
            //    
            case Node.CDATA_SECTION_NODE:
                break;

            // Should not get ENTITY related notes because
            // entities are expanded by the JSMLParser
            //
            case Node.ENTITY_NODE:
            case Node.ENTITY_REFERENCE_NODE:
                break;

            // Should not get DOCUMENT_FRAGMENT nodes because I
            // [[[WDW]]] think they are only created via the API's
            // and cannot be defined via content.
            //
            case Node.DOCUMENT_FRAGMENT_NODE:
                break;

            default:
                break;
        }
        
        return endText;
    }

    /**
     * Dumps the voice in textual form.
     *
     * @param output where to send the formatted output
     * @param pad the initial padding
     * @param title the title to print when dumping out 
     */
    public void dump(PrintWriter output, int pad, String title) {
	Utilities.dump(output, pad, title);
	features.dump(output, pad + 4, title + " Features");
	dumpProcessors(output, pad + 4, title + " Processors");
    }


    /**
     * Dumps the voice processors.
     *
     * @param output where to send the formatted output
     * @param pad the initial padding
     * @param title the title to print when dumping out 
     */
    public void dumpProcessors(PrintWriter output, int pad, String title) {
        UtteranceProcessor[] processors;
        if (utteranceProcessors == null) {
            return;
        }

        processors = new UtteranceProcessor[utteranceProcessors.size()];
        processors = (UtteranceProcessor[])
            utteranceProcessors.toArray(processors);
        
	Utilities.dump(output, pad, title);
        for (int i = 0; i < processors.length; i++) {
	    Utilities.dump(output, pad + 4, processors[i].toString());
        }
    }


    /**
     * Returns a language/voice specific Feature Processor.
     *
     * @param name the name of the processor
     *
     * @return the processor associated with the name or null if none
     *   could be found
     */
    public FeatureProcessor getFeatureProcessor(String name) {
	return (FeatureProcessor) featureProcessors.get(name);
    }

    /**
     * Adds a language/voice specific Feature Processor to the set of
     * FeatureProcessors supported by this voice.
     *
     * @param name the name of the processor
     * @param fp  the processor
     */
    public void addFeatureProcessor(String name, FeatureProcessor fp) {
	featureProcessors.put(name, fp);
    }

    /**
     * Gets the state of the metrics mode.
     *
     * @return true if metrics mode is on
     */
    public boolean isMetrics() {
	return metrics;
    }

    /**
     * Sets the metrics mode.
     *
     * @param metrics true if metrics mode should be on
     */
    public void setMetrics(boolean metrics) {
	this.metrics = metrics;
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("Metrics mode is " + metrics);
        }
    }

    /**
     * Gets the state of the detailedMetrics mode.
     *
     * @return true if detailedMetrics mode is on
     */
    public boolean isDetailedMetrics() {
	return detailedMetrics;
    }

    /**
     * Sets the state of the detailedMetrics mode.
     *
     * @param detailedMetrics true if detailedMetrics mode should be on
     */
    public void setDetailedMetrics(boolean detailedMetrics) {
	this.detailedMetrics = detailedMetrics;
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("DetailedMetrics mode is " + detailedMetrics);
        }
    }

    /**
     * Gets the state of the dumpUtterance mode.
     *
     * @return true if dumpUtterance mode is on
     */
    public boolean isDumpUtterance() {
	return dumpUtterance;
    }

    /**
     * Sets the state of the dumpUtterance mode.
     *
     * @param dumpUtterance true if dumpUtterance mode should be on
     */
    public void setDumpUtterance(boolean dumpUtterance) {
	this.dumpUtterance = dumpUtterance;
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("DumpUtterance mode is " + dumpUtterance);
        }
    }

    /**
     * Gets the state of the dumpRelations mode.
     *
     * @return true if dumpRelations mode is on
     */
    public boolean isDumpRelations() {
	return dumpRelations;
    }

    /**
     * Sets the state of the dumpRelations mode.
     *
     * @param dumpRelations true if dumpRelations mode should be on
     */
    public void setDumpRelations(boolean dumpRelations) {
	this.dumpRelations = dumpRelations;
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.fine("DumpRelations mode is " + dumpRelations);
        }
    }
    
    /**
     * Sets the title for this run.
     *
     * @param runTitle the title for the run
     */
    public void setRunTitle(String runTitle) {
	this.runTitle = runTitle;
    }

    /**
     * Gets the title for this run.
     *
     * @return the title for the run
     */
    public String getRunTitle() {
	return runTitle;
    }

    /**
     * Given a phoneme and a feature name, returns the feature.
     *
     * @param phone the phoneme of interest
     * @param featureName the name of the feature of interest
     *
     * @return the feature with the given name
     */
    public String getPhoneFeature(String phone, String featureName) {
	return null;
    }

    /**
     * Shuts down the voice processing.
     */
    public void deallocate() {
	setLoaded(false);

        if (!externalAudioPlayer) {
            if (audioPlayer != null) {
                audioPlayer.close();
                audioPlayer = null;
            }
        }
        
	if (!externalOutputQueue) {
	    outputQueue.close();
	}
    }

    /**
     * Sets the baseline pitch.
     *
     * @param hertz the baseline pitch in hertz
     */
    public void setPitch(float hertz) {
	this.pitch = hertz;
    }

    /**
     * Retreives the baseline pitch.
     *
     * @return  the baseline pitch in hertz
     */
    public float getPitch() {
	return pitch;
    }

    /**
     * Sets the pitch range.
     *
     * @param range the range in hertz
     */
    public void setPitchRange(float range) {
	this.range = range;
    }

    /**
     * Gets the pitch range.
     *
     * @return the range in hertz
     */
    public float getPitchRange() {
	return range;
    }

    /**
     * Sets the pitch shift
     *
     * @param shift the pitch shift (1.0 is no shift)
     */
    public void setPitchShift(float shift) {
	this.pitchShift = shift;
    }

    /**
     * Gets the pitch shift.
     *
     * @return the pitch shift
     */
    public float getPitchShift() {
	return pitchShift;
    }

    /**
     * Sets the duration  stretch
     *
     * @param stretch the duration stretch (1.0 is no stretch)
     */
    public void setDurationStretch(float stretch) {
	this.durationStretch = stretch;
    }

    /**
     * Gets the duration Stretch
     *
     * @return the duration stretch
     */
    public float getDurationStretch() {
	return durationStretch;
    }

    /**
     * Sets the rate of speech.
     *
     * @param wpm words per minute
     */
    public void setRate(float wpm) {
	if (wpm > 0 && wpm < 1000) {
	    setDurationStretch(nominalRate / wpm);
	}
    }

    /**
     * Gets the rate of speech.
     *
     * @return words per minute
     */
    public float getRate() {
	return durationStretch * nominalRate;
    }


    /**
     * Sets the volume.
     *
     * @param vol the volume (0 to 1.0)
     */
    public void setVolume(float vol) {
	volume = vol;
    }

    /**
     * Gets the volume.
     *
     * @return the volume (0 to 1.0)
     */
    public float getVolume() {
	return volume;
    }

    /**
     * Gets the lexicon for this voice.
     *
     * @return the lexicon (or null if there is no lexicon)
     */
    public Lexicon getLexicon() {
	return lexicon;
    }

    /**
     * Sets the lexicon to be used by this voice.
     *
     * @param lexicon the lexicon to use
     */
    public void setLexicon(Lexicon lexicon) {
	this.lexicon = lexicon;

    }

    /**
     * Sets the dumpfile for this voice.
     *
     * @param waveDumpFile the dumpfile
     */
    public void setWaveDumpFile(String waveDumpFile) {
	this.waveDumpFile = waveDumpFile;
    }

    /**
     * Gets the dumpfile for this voice.
     *
     * @return the dumpfile
     */
    public String  getWaveDumpFile() {
	return waveDumpFile;
    }

    /**
     * Sets the audio player associated with this voice. The caller is
     * responsible for closing this player.
     *
     * @param player the audio player
     */
    public void setAudioPlayer(AudioPlayer player) {
	audioPlayer = player;
        externalAudioPlayer = true;
    }

    /**
     * Gets the default audio player for this voice.  The return
     * value will be non-null only if the DEFAULT_AUDIO_PLAYER
     * system property has been set to the name of an AudioPlayer
     * class, and that class is able to be instantiated via a
     * no arg constructor.  getAudioPlayer will automatically set
     * the audio player for this voice to the default audio player
     * if the audio player has not yet been set.
     *
     * @see #DEFAULT_AUDIO_PLAYER
     * @see #getAudioPlayer
     * @return the default AudioPlayer
     */
    public AudioPlayer getDefaultAudioPlayer() throws InstantiationException {
        if (defaultAudioPlayer != null) {
            return defaultAudioPlayer;
        }
        
        String className = Utilities.getProperty(
            DEFAULT_AUDIO_PLAYER, DEFAULT_AUDIO_PLAYER_DEFAULT);

        try {
            Class cls = Class.forName(className);
            defaultAudioPlayer = (AudioPlayer) cls.newInstance();
            return defaultAudioPlayer;
        } catch (ClassNotFoundException e) {
            throw new InstantiationException("Can't find class " + className);
        } catch (IllegalAccessException e) {
            throw new InstantiationException("Can't find class " + className);
        } catch (ClassCastException e) {
            throw new InstantiationException(className + " cannot be cast "
                                             + "to AudioPlayer");
        }
    }
    
    /**
     * Gets the audio player associated with this voice.  If the
     * audio player has not yet been set, the value will default
     * to the return value of getDefaultAudioPlayer.
     *
     * @see #getDefaultAudioPlayer
     * @return the audio player
     */
    public AudioPlayer getAudioPlayer() {
        if (audioPlayer == null) {
            try {
                audioPlayer = getDefaultAudioPlayer();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
	return audioPlayer;
    }

    /**
     * Get a resource for this voice.
     * By default, the voice is searched for in the package
     * to which the voice class belongs. Subclasses are free to
     * override this behaviour.
     */
    protected URL getResource(String resource) {
        return this.getClass().getResource(resource);
    }

    /**
     * Set the name of this voice.
     * [[[TODO: any standard format to the name?]]]
     *
     * @param name the name to assign this voice
     */
    protected void setName(String name) {
        this.name = name;
    }


    /**
     * Get the name of this voice.
     *
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the name of this Voice.
     *
     * @return the name of this Voice
     */
    public String toString() {
        return getName();
    }

    /**
     * Set the gender of this voice.
     *
     * @param gender the gender to assign
     */
    protected void setGender(Gender gender) {
        this.gender = gender;
    }

    /**
     * Get the gender of this voice.
     *
     * @return the gender of this voice
     */
    public Gender getGender() {
        return gender;
    }

    /**
     * Set the age of this voice.
     *
     * @param age the age to assign
     */
    protected void setAge(Age age) {
        this.age = age;
    }

    /**
     * Get the age of this voice.
     *
     * @return the age of this voice
     */
    public Age getAge() {
        return age;
    }

    /**
     * Set the description of this voice.
     *
     * @param description the human readable description to assign
     */
    protected void setDescription(String description) {
        this.description = description;
    }

    /**
     * Get the description of this voice.
     *
     * @return the human readable description of this voice
     */
    public String getDescription() {
        return description;
    }

    /**
     * Set the locale of this voice.
     *
     * @param locale the locale of this voice.
     */
    protected void setLocale(Locale locale) {
        this.locale = locale;
    }

    /**
     * Get the locale of this voice.
     *
     * @return the locale of this voice.
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * Set the domain of this voice.
     *
     * @param domain the domain of this voice.  For example,
     * &quot;general&quot;, &quot;time&quot;, or
     * &quot;weather&quot;.
     */
    protected void setDomain(String domain) {
        this.domain = domain;
    }

    /**
     * Get the domain of this voice.
     * 
     * @return the domain of this voice.  For example,
     * &quot;general&quot;, &quot;time&quot;, or
     * &quot;weather&quot;.
     */
    public String getDomain() {
        return domain;
    }

    /**
     * Sets the voice style. This parameter is designed for human
     * interpretation. Values might include "business", "casual",
     * "robotic", "breathy"
     *
     * @param style the stile of this voice.
     */
    public void setStyle(String style) {
        this.style = style;
    }

    /**
     * Gets the voice style. This parameter is designed for human
     * interpretation. Values might include "business", "casual",
     * "robotic", "breathy".
     */
    public String getStyle() {
        return style;
    }

    /**
     * Sets the organization which created this voice.  For example
     * "cmu", "sun", ...
     *
     * @param organization the name of the organization
     */
    protected void setOrganization(String organization) {
        this.organization = organization;
    }

    /**
     * Gets the organization which created this voice.  For example
     * "cmu", "sun", ...
     *
     * @return the name of the organization
     */
    public String getOrganization() {
        return organization;
    }

    /**
     * Returns the AudioOutput processor to be used by this voice.
     * Derived voices typically override this to customize behaviors.
     * 
     * @return the audio output processor
     * 
     * @throws IOException if an IO error occurs while getting
     *     processor
     */
    protected abstract UtteranceProcessor getAudioOutput() throws IOException ;

    /**
     * Tokenizes a FreeTTSSpeakable
     */
    private class FreeTTSSpeakableTokenizer  {
	FreeTTSSpeakable speakable;
	Tokenizer tok = getTokenizer();

	/**
	 * Constructor.
         *
	 * @param speakable the queue item to be pretokenized
	 */
    	public FreeTTSSpeakableTokenizer(FreeTTSSpeakable speakable) {
	    this.speakable = speakable;
	    if (speakable.isPlainText()) {
		tok.setInputText(speakable.getText());
	    } else if (speakable.isStream()) {
		Reader reader = new BufferedReader(
		    new InputStreamReader(speakable.getInputStream()));
		tok.setInputReader(reader);
	    } else if (speakable.isDocument()) {
		tok.setInputText(documentToString(speakable.getDocument()));
	    }
	}
	    
	/**
	 * Returns an iterator for this text item.
	 */
	public Iterator iterator() {
	    return new Iterator() {
		boolean first = true;
		Token savedToken = null;

		/**
		 * Determines if there are more utterances
		 *
		 * @return true if there are more tokens
		 */
		public boolean hasNext() {
		    return savedToken != null || tok.hasMoreTokens();
		}

		/**
		 * Returns the next utterance.
                 *
		 * @return the next utterance (as an object) or
		 *    null if there is are no utterances left
		 */
		public Object next() {
		    ArrayList tokenList = new ArrayList();
		    Utterance utterance = null;

		    if (savedToken != null) {
			tokenList.add(savedToken);
			savedToken = null;
		    }

		    while (tok.hasMoreTokens()) {
			Token token = tok.getNextToken();
			if ((token.getWord().length() == 0) ||
			    (tokenList.size() > 500) ||
			    tok.isBreak()) {
			    savedToken = token;
			    break;
			}
			tokenList.add(token);
		    }
		    utterance =  new Utterance(Voice.this, tokenList);
		    utterance.setSpeakable(speakable);
		    utterance.setFirst(first);
		    first = false;
                    boolean isLast = 
                        (!tok.hasMoreTokens() && 
                         (savedToken == null || 
                          savedToken.getWord().length() == 0));
		    utterance.setLast(isLast);
		    return utterance;
		}

		public void remove() {
                    throw new UnsupportedOperationException("remove");  
		}
	    };
	}
    }
}

  



