blob: 38635a83a8b9d0d85c81f8f1a4aa0f255826c94e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2012 IBM Corporation and Others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Takashi ITOH - initial API and implementation
* Kentarou FUKUDA - initial API and implementation
*******************************************************************************/
package org.eclipse.actf.ai.tts.sapi.engine;
import java.io.File;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import org.eclipse.actf.ai.tts.ISAPIEngine;
import org.eclipse.actf.ai.tts.sapi.SAPIPlugin;
import org.eclipse.actf.ai.voice.IVoiceEventListener;
import org.eclipse.actf.util.win32.COMUtil;
import org.eclipse.actf.util.win32.MemoryUtil;
import org.eclipse.actf.util.win32.NativeIntAccess;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.internal.ole.win32.GUID;
import org.eclipse.swt.internal.ole.win32.IDispatch;
import org.eclipse.swt.ole.win32.OLE;
import org.eclipse.swt.ole.win32.OleAutomation;
import org.eclipse.swt.ole.win32.Variant;
/**
* The implementation of ITTSEngine to use Microsoft Speech API.
*/
public class SapiVoice implements ISAPIEngine, IPropertyChangeListener {
public static final String ID = "org.eclipse.actf.ai.tts.sapi.engine.SapiVoice"; //$NON-NLS-1$
public static final String AUDIO_OUTPUT = "org.eclipse.actf.ai.tts.SapiVoice.audioOutput"; //$NON-NLS-1$
public static final GUID IID_SpFileStream = COMUtil
.IIDFromString("{947812B3-2AE1-4644-BA86-9E90DED7EC91}"); //$NON-NLS-1$
public ISpVoice dispSpVoice;
private Variant varSapiVoice;
private OleAutomation automation;
private int idGetVoices;
private int idGetAudioOutputs;
private ISpNotifySource spNotifySource = null;
private static IPreferenceStore preferenceStore = SAPIPlugin.getDefault()
.getPreferenceStore();
private boolean isDisposed = false;
private SpObjectToken curVoiceToken = null;
private class EngineInfo {
String name;
String langId;
String gender;
public EngineInfo(String name, String langId, String gender) {
this.name = name;
this.langId = langId;
this.gender = gender;
}
}
private Map<String, TreeSet<EngineInfo>> langId2EngineMap = new HashMap<String, TreeSet<EngineInfo>>();
public SapiVoice() {
int pv = COMUtil.createDispatch(ISpVoice.IID);
dispSpVoice = new ISpVoice(pv);
varSapiVoice = new Variant(dispSpVoice);
automation = varSapiVoice.getAutomation();
spNotifySource = ISpNotifySource.getNotifySource(dispSpVoice);
SAPIPlugin.getDefault().addPropertyChangeListener(this);
idGetVoices = getIDsOfNames("GetVoices"); //$NON-NLS-1$
idGetAudioOutputs = getIDsOfNames("GetAudioOutputs"); //$NON-NLS-1$
// init by using default engine to avoid init error of some TTS engines
String orgID = preferenceStore.getString(ID);
preferenceStore.setValue(ID, preferenceStore.getDefaultString(ID));
setAudioOutputName();
// switch to actual engine
preferenceStore.setValue(ID, orgID);
setVoiceName(); // for init curVoiceToken
Variant varVoices = getVoices(null, null);
if (null != varVoices) {
SpeechObjectTokens voiceTokens = SpeechObjectTokens
.getTokens(varVoices);
if (null != voiceTokens) {
String exclude = Platform.getResourceString(SAPIPlugin
.getDefault().getBundle(), "%voice.exclude"); //$NON-NLS-1$
int count = voiceTokens.getCount();
for (int i = 0; i < count; i++) {
Variant varVoice = voiceTokens.getItem(i);
if (null != varVoice) {
SpObjectToken token = SpObjectToken.getToken(varVoice);
if (null != token) {
String voiceName = token.getDescription(0);
String langId = token.getAttribute("language"); //$NON-NLS-1$
int index = langId.indexOf(";");
//use primary lang ID
if (index > 0){
langId = langId.substring(0, index);
}
String gender = token.getAttribute("gender"); //$NON-NLS-1$
if (null == exclude || !exclude.equals(voiceName)) {
TreeSet<EngineInfo> set = langId2EngineMap
.get(langId);
if (set == null) {
set = new TreeSet<SapiVoice.EngineInfo>(
new Comparator<EngineInfo>() {
public int compare(
EngineInfo o1,
EngineInfo o2) {
// TODO priority
return -o1.name
.compareTo(o2.name);
}
});
langId2EngineMap.put(langId, set);
}
set.add(new EngineInfo(voiceName, langId,
gender));
}
}
}
}
}
varVoices.dispose();
}
// to avoid access violation error at application shutdown
stop();
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse
* .jface.util.PropertyChangeEvent)
*/
public void propertyChange(PropertyChangeEvent event) {
if (ID.equals(event.getProperty())) {
stop();
setVoiceName();
} else if (AUDIO_OUTPUT.equals(event.getProperty())) {
stop();
setAudioOutputName();
}
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.actf.ai.tts.ITTSEngine#setEventListener(org.eclipse.actf.
* ai.voice.IVoiceEventListener)
*/
public void setEventListener(IVoiceEventListener eventListener) {
spNotifySource.setEventListener(eventListener);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#speak(java.lang.String, int, int)
*/
public void speak(String text, int flags, int index) {
int firstFlag = SVSFlagsAsync;
if (0 != (TTSFLAG_FLUSH & flags)) {
firstFlag |= SVSFPurgeBeforeSpeak;
}
if (index >= 0) {
speak("<BOOKMARK mark=\"" + index + "\"/>", firstFlag | SVSFPersistXML); //$NON-NLS-1$ //$NON-NLS-2$
speak(text, SVSFlagsAsync);
speak("<BOOKMARK mark=\"-1\"/>", SVSFlagsAsync | SVSFPersistXML); //$NON-NLS-1$
} else {
speak(text, firstFlag);
}
}
public void speak(String text, int sapiFlags) {
char[] data = (text + "\0").toCharArray(); //$NON-NLS-1$
int bstrText = MemoryUtil.SysAllocString(data);
try {
dispSpVoice.Speak(bstrText, sapiFlags);
} finally {
MemoryUtil.SysFreeString(bstrText);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#stop()
*/
public void stop() {
speak("", TTSFLAG_FLUSH, -1); //$NON-NLS-1$
}
/**
* @param rate
* The rate property to be set.
* @return The invocation is succeeded then it returns true.
*/
public boolean setRate(int rate) {
return OLE.S_OK == dispSpVoice.put_Rate(rate);
}
/**
* @return The rate property of the voice engine.
*/
public int getRate() {
NativeIntAccess nia = new NativeIntAccess();
try {
if (OLE.S_OK == dispSpVoice.get_Rate(nia.getAddress())) {
return nia.getInt();
}
} finally {
nia.dispose();
}
return -1;
}
/**
* @param varVoice
* The voice object to be set.
* @return The invocation is succeeded then it returns true.
*/
public boolean setVoice(Variant varVoice) {
boolean result = OLE.S_OK == dispSpVoice.put_Voice(varVoice
.getDispatch().getAddress());
if (result) {
curVoiceToken = SpObjectToken.getToken(varVoice);
}
return result;
}
/**
* @param varAudioOutput
* The audio output object to be set.
* @return The invocation is succeeded then it returns true.
*/
public boolean setAudioOutput(Variant varAudioOutput) {
return OLE.S_OK == dispSpVoice
.put_AudioOutput(null != varAudioOutput ? varAudioOutput
.getDispatch().getAddress() : 0);
}
private void setVoiceName() {
String voiceName = preferenceStore.getString(ID);
if (voiceName.length() > 0) {
setVoiceName("name=" + voiceName); //$NON-NLS-1$
}
}
/**
* @param voiceName
* The voice name to be set.
* @return The invocation is succeeded then it returns true.
*/
public boolean setVoiceName(String voiceName) {
boolean success = false;
Variant varVoices = getVoices(voiceName, null);
if (null != varVoices) {
SpeechObjectTokens tokens = SpeechObjectTokens.getTokens(varVoices);
if (null != tokens && 0 < tokens.getCount()) {
Variant varVoice = tokens.getItem(0);
if (null != varVoice) {
success = setVoice(varVoice);
}
}
varVoices.dispose();
}
if (!success) {
int index = voiceName.indexOf("name="); //$NON-NLS-1$
varVoices = getVoices(null, null);
if (null != varVoices && index > -1) {
String name = voiceName.substring(index + 5);
SpeechObjectTokens voiceTokens = SpeechObjectTokens
.getTokens(varVoices);
if (null != voiceTokens) {
int count = voiceTokens.getCount();
for (int i = 0; i < count; i++) {
Variant varVoice = voiceTokens.getItem(i);
if (null != varVoice) {
SpObjectToken token = SpObjectToken
.getToken(varVoice);
if (null != token
&& name.equals(token.getDescription(0))) {
success = setVoice(varVoice);
}
}
}
}
}
varVoices.dispose();
}
return success;
}
private void setAudioOutputName() {
String audioOutput = preferenceStore.getString(AUDIO_OUTPUT);
if (audioOutput.length() > 0) {
setAudioOutputName(audioOutput);
} else {
setAudioOutput(null);
}
}
/**
* @param audioOutput
* The audio output name to be set.
* @return The invocation is succeeded then it returns true.
*/
public boolean setAudioOutputName(String audioOutput) {
boolean success = false;
Variant varAudioOutputs = getAudioOutputs(null, null);
if (null != varAudioOutputs) {
SpeechObjectTokens tokens = SpeechObjectTokens
.getTokens(varAudioOutputs);
if (null != tokens) {
for (int i = 0; i < tokens.getCount(); i++) {
Variant varAudioOutput = tokens.getItem(i);
if (null != varAudioOutput) {
SpObjectToken token = SpObjectToken
.getToken(varAudioOutput);
if (null != token
&& audioOutput.equals(token.getDescription(0))) {
success = setAudioOutput(varAudioOutput);
break;
}
}
}
}
varAudioOutputs.dispose();
}
return success;
}
/**
* @param requiredAttributes
* @param optionalAttributes
* @return The tokens of voices.
*/
public Variant getVoices(String requiredAttributes,
String optionalAttributes) {
return getTokens(idGetVoices, requiredAttributes, optionalAttributes);
}
/**
* @param requiredAttributes
* @param optionalAttributes
* @return The tokens of audio outputs.
*/
public Variant getAudioOutputs(String requiredAttributes,
String optionalAttributes) {
return getTokens(idGetAudioOutputs, requiredAttributes,
optionalAttributes);
}
private Variant getTokens(int id, String requiredAttributes,
String optionalAttributes) {
if (null == requiredAttributes) {
return automation.invoke(id);
} else if (null == optionalAttributes) {
return automation.invoke(id, new Variant[] { new Variant(
requiredAttributes) });
}
return automation.invoke(id, new Variant[] {
new Variant(requiredAttributes),
new Variant(optionalAttributes) });
}
private int getIDsOfNames(String name) {
int dispid[] = automation.getIDsOfNames(new String[] { name });
if (null != dispid) {
return dispid[0];
}
return 0;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#dispose()
*/
public void dispose() {
if (!isDisposed) {
isDisposed = true;
varSapiVoice.dispose();
if (SAPIPlugin.getDefault() != null) {
SAPIPlugin.getDefault().removePropertyChangeListener(this);
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#getSpeed()
*/
public int getSpeed() {
int rate = getRate(); // -10 <= rate <= 10
return (rate + 10) * 5; // 0 <= speed <= 100
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#setSpeed(int)
*/
public void setSpeed(int speed) {
int rate = speed / 5 - 10;
setRate(rate);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#setLanguage(java.lang.String)
*/
public void setLanguage(String language) {
String gender = null;
if (curVoiceToken != null) {
gender = curVoiceToken.getAttribute("gender");
}
String langId = LANGID_MAP.get(language);
if (langId == null) {
// for backward compatibility
if (LANG_JAPANESE.equals(language)) {
langId = "411"; //$NON-NLS-1$
} else if (LANG_ENGLISH.equals(language)) {
langId = "409"; //old value "409;9" //$NON-NLS-1$
}
// TODO other lang
}
if (langId == null) {
return;
}
// Workaround:
// In some cases, getVoices("language=***",null) doesn't work well.
// So, get all voices first. Then select lang and gender.
TreeSet<EngineInfo> set = langId2EngineMap.get(langId);
if (set != null && set.size() > 0) {
if (gender != null) {
for (EngineInfo i : set) {
if (gender.equalsIgnoreCase(i.gender)) {
setVoiceName("name=" + i.name);
// System.out.println(i.name);
return;
}
}
}
setVoiceName("name=" + set.first().name);
// System.out.println(set.first().name);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#setGender(java.lang.String)
*/
public void setGender(String gender) {
if (gender == null) {
return;
}
String langId = null;
if (curVoiceToken != null) {
langId = curVoiceToken.getAttribute("language");
if ("409;9".equals(langId)) {
langId = "409";
}
}
TreeSet<EngineInfo> set = langId2EngineMap.get(langId);
if (set != null && set.size() > 0) {
for (EngineInfo i : set) {
if (gender.equalsIgnoreCase(i.gender)) {
setVoiceName("name=" + i.name);
// System.out.println(i.name);
return;
}
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.actf.ai.tts.ITTSEngine#isAvailable()
*/
public boolean isAvailable() {
return automation != null;
}
public boolean isDisposed() {
return isDisposed;
}
public boolean canSpeakToFile() {
return true;
}
public boolean speakToFile(String text, File file) {
int pv = COMUtil.createDispatch(IID_SpFileStream);
OleAutomation autoSpFileStream = null;
boolean speakToFileResult = false;
if (file == null || (file.exists() && !file.canWrite())) {
return false;
}
Variant varSpFileStream = new Variant(new IDispatch(pv));
try {
autoSpFileStream = varSpFileStream.getAutomation();
// AllowAudioOutputFormatChangesOnNextSet
// System.out.println(automation.getProperty(7).getBoolean());
// format
// System.out.println(autoSpFileStream.getProperty(1).getAutomation().setProperty(1,new
// Variant(6)));
// open 100 close 101
String tmpS = file.toURI().toString();
if (tmpS.startsWith("file:/")) {
tmpS = tmpS.substring(6).replaceAll("%20"," ");
}
autoSpFileStream.invoke(100, new Variant[] { new Variant(tmpS),
new Variant(3), new Variant(false) });
dispSpVoice.put_AudioOutputStream(pv);
char[] data = (text + "\0").toCharArray(); //$NON-NLS-1$
int bstrText = MemoryUtil.SysAllocString(data);
try {
dispSpVoice.Speak(bstrText, 0);
} finally {
MemoryUtil.SysFreeString(bstrText);
}
autoSpFileStream.invoke(101);
autoSpFileStream.dispose();
autoSpFileStream = null;
speakToFileResult = true;
} catch (Exception e) {
e.printStackTrace();
if (autoSpFileStream != null) {
autoSpFileStream.dispose();
autoSpFileStream = null;
}
}
setAudioOutputName(); // reset output
return speakToFileResult;
}
}