Ok it doesn’t have anything to do with games unless you’re planing to do Text to speech (an actually fairly compact way of putting really shitty spoken text in a game - a narrator for ex.)
The class is supposted to be a low latency, single thread accessible text to speech object with speech events. It kinda works, but i want to diminish the latency on cancel() of speech. Discussion bellow, relevant code ahead, complete class still bellow.
Since freetts is synchronous in its speak mode, i add each speak request in a Executors singleThreadedExecutor singleThread for processing when ready. Before adding i increment an atomic integer that is decremented in case of the executor is terminated or when the speech terminates, and is used to check is speech is ended programmatically. Since the class is expected to be used in a single thread (the EDT say) there is no synchronization when accessing the shared recourse singleThread.
The effect of singleThread is to queue all requests and to send events at the right time. It also increments a volatile variable index that changes the position for the events on each update. Since only one Runnable is executing at a time i thought volatile sufficed (not simple because executor can and will change executing threads.
The problem is cancellation, since, to cancel i have to wait for the current event to terminate, on a wait loop (hidden inside awaitTermination). Then after termination i have to recreate singleThread and put an event that will reset the index (that’s why its volatile). No synchronization again because it is not thread safe™.
Can i do this to eliminate the latency?
public void cancel() {
if (queuedTasks.get() > 0) {
final ExecutorService local = Executors.newSingleThreadExecutor();
local.execute(new Runnable() {
public void run() {
killSingleThread(true);
//this is not the original thread
//but we waited for the tasks in
//the other to end, and this goes
//before any others.
index = 0;
singleThread = local;
}
});
}
}
So i run 3 threads concurrently for a short time before another terminates and passes the hand off to the other ? Or is this an extremely bad idea?
By the way if anybody ever worked with the freetts api do you know if the “real” way to cancel sound output is bugged beyond recovery? Here it just never closed on time. close() put a stop on that non-sense.
All the code and if you find any problems, and if it would be too much trouble could you please report? No pressure or anything.
package util.speech;
import com.sun.speech.freetts.VoiceManager;
import com.sun.speech.freetts.audio.JavaStreamingAudioPlayer;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.event.EventListenerList;
public final class VoiceImpl extends Voice {
private final AtomicInteger queuedTasks = new AtomicInteger();
//modified only on one thread (by the singleThread executor)
//so no synchronization beyond volatile.
private volatile int index = 0;
private final AtomicBoolean paused = new AtomicBoolean();
private final AtomicBoolean closed = new AtomicBoolean();
private com.sun.speech.freetts.Voice voice;
private final EventListenerList listenerList = new EventListenerList();
//Enqueues the audio. Another thread plays it.
private ExecutorService singleThread = Executors.newSingleThreadExecutor();
public VoiceImpl(String voice) throws IllegalArgumentException {
//bug with streaming audio player requires us to recreate it on cancelation
//make the buffer size smaller.
System.setProperty("com.sun.speech.freetts.audio.AudioPlayer.bufferSize", "1024");
setVoice(voice);
}
public VoiceImpl(com.sun.speech.freetts.Voice voice) {
this.voice = voice;
}
@Override
public String getName() {
return voice.getName();
}
private void fireSpeechEvent(EventType type, SpeechEvent evt) {
Object[] listeners = listenerList.getListenerList();
switch (type) {
case START:
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == SpeechListener.class) {
SpeechListener l = (SpeechListener) listeners[i + 1];
l.startedSpeaking(evt);
}
}
break;
case END:
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == SpeechListener.class) {
SpeechListener l = (SpeechListener) listeners[i + 1];
l.endedSpeaking(evt);
}
}
break;
}
}
public void addSpeechListener(SpeechListener l) {
listenerList.add(SpeechListener.class, l);
}
public void removeSpeechListener(SpeechListener l) {
listenerList.remove(SpeechListener.class, l);
}
private void setVoice(String voiceName) throws IllegalArgumentException {
if (voiceName == null) {
throw new IllegalArgumentException("null voice given");
}
com.sun.speech.freetts.Voice tmpVoice = VoiceManager.getInstance().getVoice(voiceName);
if (tmpVoice == null) {
throw new IllegalArgumentException("unrecognized or missing voice");
}
//tmpVoice.getUtteranceProcessors().add(new NotifierProcessor());
voice = tmpVoice;
}
private void prepareToSpeak() {
if (!voice.isLoaded()) {
voice.allocate();
}
}
private void testPrecondictions(String speech) {
if (speech == null) {
throw new IllegalArgumentException("Tried to speak null string.");
}
if (closed.get()) {
throw new IllegalStateException("Tried to speak on a closed voice.");
}
}
public void speak(final String speech) {
testPrecondictions(speech);
executeInSingleThread(new Runnable() {
public void run() {
doSpeak(speech, index);
}
});
}
public void speak(final int newEventIndex, final String speech) {
testPrecondictions(speech);
executeInSingleThread(new Runnable() {
public void run() {
doSpeak(speech, newEventIndex);
}
});
}
private void doSpeak(final String speech, int localIndex) {
prepareToSpeak();
fireSpeechEvent(EventType.START, new SpeechEvent(localIndex, speech, false));
boolean success = voice.speak(speech);
if (success) {
fireSpeechEvent(EventType.END, new SpeechEvent(localIndex, speech, false));
} else {
//terrible code inside JavaStreamingAudioPlayer forces me to this.
voice.getAudioPlayer().close();
voice.setAudioPlayer(new JavaStreamingAudioPlayer());
fireSpeechEvent(EventType.END, new SpeechEvent(localIndex, speech, true));
}
index = localIndex + speech.length();
queuedTasks.decrementAndGet();
}
private void executeInSingleThread(Runnable r) {
try {
queuedTasks.incrementAndGet();
singleThread.execute(r);
} catch (RejectedExecutionException re) {
queuedTasks.decrementAndGet();
}
}
public boolean isGeneralDomainVoice() {
return voice.getDomain().equalsIgnoreCase("general");
}
public boolean isSpeaking() {
return queuedTasks.get() != 0;
}
public boolean isPaused() {
return paused.get();
}
public void setPaused(boolean p) {
boolean pa = paused.get();
if (p && !pa) {
voice.getAudioPlayer().pause();
} else if (!p && pa) {
voice.getAudioPlayer().resume();
}
paused.set(p);
}
private void killSingleThread(boolean awaitEnd) {
List l = singleThread.shutdownNow();
queuedTasks.addAndGet(-l.size());
if (awaitEnd) {
try {
singleThread.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
throw new AssertionError("Interrupted while waiting for audio to end!");
}
}
}
/**
* Cancel can't be assynchronous.
* close can because it never will
* execute anymore.
*/
public void cancel() {
if (queuedTasks.get() > 0) {
killSingleThread(true);
singleThread = Executors.newSingleThreadExecutor();
singleThread.execute(new Runnable() {
public void run() {
//this is not the original thread
//but we waited for the tasks in
//the other to end, and this goes
//before any others.
index = 0;
}
});
}
}
public void close() {
if (!closed.getAndSet(true)) {
killSingleThread(false);
if (voice.isLoaded()) {
voice.deallocate();
}
}
}
}