div.main {margin-left: 20pt; margin-right: 20pt}
Говорящая Java!
Добавьте голосовые возможности в ваши апплеты и приложения на
Java 1.3
Содержание В этой статье Тони Лотон (Tony
Loton) показывает как реализовать простой голосовой движок используя менее 150
строк кода на Java, без дополнительного оборудования и без вывода нативных
библиотек. Далее он предоставляет небольшой zip файл, содержащий все
необходимое для того чтобы заставить ваше Java приложение говорить -- просто
забавы ради или для более серьезных задач. И, если вы впервые сталкиваетесь с
Java Sound API, эта статья может послужить неплохим введением. (1,800 слов
в англ. оригинале) Автор Tony
Loton
Зачем вам может понадобиться делать свои приложения "говорящими"?
Во-первых, это забавно и может быть использовано в развлекательных программах,
таких как игры. Кроме того, существует более серьезная сторона, касающаяся
доступности. Я считаю что это не только естественность, которую нельзя достичь
использованием визуального интерфейса, но еще существуют ситуации, когда
невозможно, или даже запрещено, оторвать глаза от дела, которым занимаетесь.
Недавно я работал над технологией сбора HTML и XML информации из
Web [см. "Доступ к крупнейшей в мире базе данных при помощи связанных баз
данных в Web (Access the World's Biggest Database with Web DataBase
Connectivity)" (JavaWorld, Март 2001)]. Так получилось что я смог
объединить эти две идеи воедино и создать говорящий Web броузер. Такой броузер
может быть полезен для прослушивания различных фрагментов информации с ваших
любимых сайтов -- например, заголовков новостей. Это все равно что слушать радио
выгуливая собаку или по пути к месту работы. Конечно, сегодня для этого вам
придется иметь при себе портативный компьютер с подключенным мобильным
телефоном, но в ближайшем будущем эта ситуация может измениться с появлением
поддерживающих Java "интеллектуальных" телефонов, таких как Nokia 9210 (9290 в
США).
Что касается текущих перспектив, то удачным применением может стать
использования для чтения электронной почты, что также возможно благодаря
JavaMail API. Это приложение может периодически проверять ваш почтовый ящик и
время от времени заставляет вас вздрогнуть от неожиданности, когда голос из
ниоткуда произносит: "Пришла новая почта. Хотите чтобы я прочитал ее вам?" Или,
продолжая развивать эту тему, его можно подключить к вашему ежедневнику, чтобы
он выкрикивал: "Не забудь о встрече с боссом через 10 минут!"
Итак, возможно вы воспользуетесь одной из этих идей или у вас есть
собственные. Идем дальше. В начале я объясню как работает мой файл supplied.zip,
так что, если сочтете что это слишком сложно, вы можете опустить детали и сразу
же начать с его запуска.
Тестирование речевого движка Для использования
речевого движка вам потребуется включить файл jw-0817-javatalk.zip в ваш CLASSPATH и запустить
com.lotontech.speech.Talker класс из командной строки или из
Java программы.
Для запуска из командной строки наберите:
java com.lotontech.speech.Talker "h|e|l|oo"
Для запуска из программы Java, просто включите две строки кода:
com.lotontech.speech.Talker talker=new
com.lotontech.speech.Talker(); talker.sayPhoneWord("h|e|l|oo");
На этом месте у вас возможно возникли вопросы по поводу формата
строки "h|e|l|oo", передаваемую через командную строку или
передаваемую в качестве параметра методу sayPhoneWord(...).
Позвольте мне объяснить.
Речевой движок работает путем объединения коротких звуковых
фрагментов, составляющих человеческую (в данном случае английскую) речь. Эти
звуковые фрагменты, называемые allophones (аллофоны), имеют одно-,
двух- или трехбуквенное обозначения. Некоторые идентификаторы очевидны, а
некоторые не очень, как вы и могли убедиться на примере фонетического
представления слова "hello".
h -- звучит так как вы и ожидали
e -- звучит так как вы и ожидали
l -- звучит так как вы и ожидали, но обратите внимание на то что
я сократил двойное "l" до одиночного
oo -- подходит для слова "hello," но не для слов "bot" и "too"
Вот перечень допустимых allophones:
a -- как в слове cat
b -- как в слове cab
c -- как в слове cat
d -- как в слове dot
e -- как в слове bet
f -- как в слове frog
g -- как в слове frog
h -- как в слове hog
i -- как в слове pig
j -- как в слове jig
k -- как в слове keg
l -- как в слове leg
m -- как в слове met
n -- как в слове begin
o -- как в слове not
p -- как в слове pot
r -- как в слове rot
s -- как в слове sat
t -- как в слове sat
u -- как в слове put
v -- как в слове have
w -- как в слове wet
y -- как в слове yet
z -- как в слове zoo
aa -- как в слове fake
ay -- как в слове hay
ee -- как в слове bee
ii -- как в слове high
oo -- как в слове go
bb -- вариация b с другим акцентом
dd -- вариация d с другим акцентом
ggg -- вариация g с другим акцентом
hh -- вариация h с другим акцентом
ll -- вариация l с другим акцентом
nn -- вариация n с другим акцентом
rr -- вариация r с другим акцентом
tt -- вариация t с другим акцентом
yy -- вариация y с другим акцентом
ar -- как в слове car
aer -- как в слове care
ch -- как в слове which
ck -- как в слове check
ear -- как в слове beer
er -- как в слове later
err -- как в слове later (более протяжно)
ng -- как в слове feeding
or -- как в слове law
ou -- как в слове zoo
ouu -- как в слове zoo (более протяжно)
ow -- как в слове cow
oy -- как в слове boy
sh -- как в слове shut
th -- как в слове thing
dth -- как в слове this
uh -- вариация произношения u
wh -- как в слове where
zh -- как в слове Asian
В человеческой речи тональность слов повышается и понижается на
протяжении всего произносимого предложения. Эта интонация позволяет речи звучать
более натурально, более эмоционально, и дает возможность отличать вопрос от
утверждения. Если вы когда-нибудь слышали синтетический голос Стивена Хокингса
(Stephen Hawking's), тогда вы понимаете о чем я говорю. Рассмотрим следующие два
предложения:
It is fake -- f|aa|k
Is it fake? -- f|AA|k
Как вы могли догадаться, для повышения интонации используются
заглавные буквы. Все что от вас требуется - немного поэкспериментировать и
ознакомиться с моими рекомендациями по имитации протяжных гласных звуков.
Вот и вся информация, необходимая для использования этого
программного обеспечения. Однако, если вас интересует то, каким образом это все
работает, читайте дальше.
Реализация речевого движка Для речевого движка
требуется всего лишь один класс с четырьмя методами. Он использует Java Sound
API, включенный в состав J2SE 1.3. Я не буду приводить здесь руководство по
использованию Java Sound API, но вы сможете научиться всему необходимому
рассмотрев приведенный ниже пример. Вы увидите что в этом нет ничего сложного, а
комментарии предоставят вам всю необходимую информацию.
Так выглядит описание класса Talker:
package com.lotontech.speech;
import
javax.sound.sampled.*; import java.io.*; import java.util.*; import
java.net.*;
public class Talker { private
SourceDataLine line=null; }
Если вы запускаете Talker из командной строки, то
входной точкой программы является приведенный ниже метод main(...).
Он передает первый параметр командной строки (если он есть) методу
sayPhoneWord(...):
/* * Этот метод проговаривает фонетическое слово, указанное в
командной строке. */ public static void main(String
args[]) { Talker player=new Talker(); if
(args.length>0)
player.sayPhoneWord(args[0]); System.exit(0); }
Метод sayPhoneWord(...) вызывается в
main(...), рассмотренном выше или может быть вызван непосредственно
из вашего Java приложения или поддерживающего plug-in апплета. Это выглядит
сложнее чем есть на самом деле. По сути дела он обрабатывает allophones,
разделенные символами "|" в вводимом тексте, и проигрывает их
одно за другим через канал вывода звука. Для того чтобы звучание было более
естественным, я объединяю конец каждого звукового фрагмента с началом
следующего:
/* * Этот метод проговаривает заданное фонетическое
слово. */ public void sayPhoneWord(String word) { // --
Создание фиктивного массива байтов для предыдущего звука
-- byte[] previousSound=null;
// -- Разбиение
строки ввода на отдельные allophones -- StringTokenizer st=new
StringTokenizer(word,"|",false); while
(st.hasMoreTokens()) { // -- Формирует
имя файла для allophone -- String
thisPhoneFile=st.nextToken(); thisPhoneFile="/allophones/"+thisPhoneFile+".au";
//
-- Получение данных из файла -- byte[]
thisSound=getSound(thisPhoneFile);
if
(previousSound!=null) { //
-- Объединение предыдущего allophone с текущим (если это возможно)
-- int
mergeCount=0; if
(previousSound.length>=500 &&
thisSound.length>=500) mergeCount=500; for
(int i=0;
i<mergeCount;i++) { previousSound[previousSound.length-mergeCount+i]
=(byte)((previousSound[previousSound.length
-mergeCount+i]+thisSound[i])/2); }
//
-- Воспроизведение предыдущего allophone
-- playSound(previousSound);
//
-- Определяет текущий усеченный allophone как предыдущий
-- byte[] newSound=new
byte[thisSound.length-mergeCount]; for
(int ii=0; ii<newSound.length;
ii++) newSound[ii]=thisSound[ii+mergeCount]; previousSound=newSound;
} else previousSound=thisSound; }
//
-- Проигрывает последний звук и освобождает звуковой канал
-- playSound(previousSound); drain(); }
Как видите, в конце метода sayPhoneWord()
осуществляется вызов метода playSound(...) для воспроизведения
отдельного звукового фрагмента (allophone), а затем следует вызов метода
drain(...) для очистки звукового канала. Вот код метода
playSound(...):
/* * Этот метод воспроизводит звуковой
фрагмент. */ private void playSound(byte[] data) { if
(data.length>0) line.write(data, 0, data.length); }
И для метода drain(...):
/* * Этот метод очищает звуковой канал. */ private void
drain() { if (line!=null) line.drain(); try
{Thread.sleep(100);} catch (Exception e) {} }
Теперь, если вы вернетесь к методу sayPhoneWord(...),
вы увидите что остался один метод, который мы еще не рассмотрели:
getSound(...).
getSound(...) считывает ранее записанный звуковой
фрагмент как битовые данные из au файла. Когда я говорю "файл", я имею ввиду
ресурс, представленный в файле supplied.zip. Я обращаю на это внимание,
поскольку доступ к JAR ресурсу (используя метод getResource(...))
отличается от доступа к файлу.
Чтобы разобраться во всех этапах работы метода: чтения данных,
преобразования формата звука, формирования линии вывода звука (почему они
назвали метод SourceDataLine? я не знаю) и сборки байтов данных, я
сопроводил код соответствующими комментариями:
/* * Этот метод считывает файл с отдельным allophone и *
формирует вектор байтов. */ private byte[] getSound(String
fileName) { try { URL
url=Talker.class.getResource(fileName); AudioInputStream
stream =
AudioSystem.getAudioInputStream(url);
AudioFormat
format = stream.getFormat();
// -- Преобразует
ALAW/ULAW звук в PCM для проигрывателя -- if
((format.getEncoding() == AudioFormat.Encoding.ULAW)
|| (format.getEncoding() ==
AudioFormat.Encoding.ALAW))
{ AudioFormat
tmpFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
format.getSampleRate(),
format.getSampleSizeInBits() * 2,
format.getChannels(),
format.getFrameSize() * 2,
format.getFrameRate(),
true);
stream =
AudioSystem.getAudioInputStream(tmpFormat,
stream); format =
tmpFormat; }
DataLine.Info
info = new DataLine.Info( Clip.class,
format, ((int)
stream.getFrameLength() *
format.getFrameSize()));
if
(line==null) { //
-- Строка вывода еще не сформирована
-- // -- Можно найти подходящую строку?
-- DataLine.Info
outInfo = new DataLine.Info(SourceDataLine.class,
format); if
(!AudioSystem.isLineSupported(outInfo)) { System.out.println("Использование
строки " + outInfo + "
недопустимо."); throw new
Exception("Использование строки " + outInfo + "
недопустимо."); }
//
-- Открыть строку исходных данных (строку вывода)
-- line = (SourceDataLine)
AudioSystem.getLine(outInfo); line.open(format,
50000); line.start(); }
//
-- Вычисление размера -- int frameSizeInBytes =
format.getFrameSize(); int bufferLengthInFrames =
line.getBufferSize() / 8; int bufferLengthInBytes =
bufferLengthInFrames * frameSizeInBytes;
byte[]
data=new byte[bufferLengthInBytes];
// -- Чтение
байтов данных и их учет -- int numBytesRead =
0; if ((numBytesRead = stream.read(data)) !=
-1) { int
numBytesRemaining =
numBytesRead; }
// --
Усечение массива байтов до допустимого размера
-- byte[] newData=new
byte[numBytesRead]; for (int i=0;
i<numBytesRead;i++) newData[i]=data[i];
return
newData; } catch (Exception
e) { return new
byte[0]; } }
Вот так. Синтезатор речи содержит около 150 строк кода, включая комментарии.
Но это еще не все.
Преобразование текста в речь Фонетическое
представление слов может показаться вам несколько сложным. Поэтому, если вы
решили приступить к созданию одно из предложенных во введении приложений, вам
захочется иметь возможность проговаривания обычных слов.
После изложения сути проблемы, я предлагаю вашему вниманию
экспериментальный класс для преобразования текста в речь, представленный в zip
файле. Результаты запуска дадут вам полное представление о том, как он работает.
Вы можете запустить конвертер из командной строки:
java com.lotontech.speech.Converter "hello there"
В результате вы получите что-то вроде:
hello -> h|e|l|oo there -> dth|aer
Или, запустив его следующим образом:
java com.lotontech.speech.Converter "I like to read
JavaWorld"
вы увидите (и услышите):
i -> ii like -> l|ii|k to -> t|ouu read ->
r|ee|a|d java -> j|a|v|a world -> w|err|l|d
Если вам интересно как он работает, скажу вам что мой подход
предельно прост и заключается в применении нескольких правил замены символов,
следующих в определенном порядке. Вот несколько таких правил, которые вы можете
мысленно проверить на таких словах как "ant," "want," "wanted," "unwanted," и
"unique":
Замените "*unique*" на "|y|ou|n|ee|k|"
Замените "*want*" на "|w|o|n|t|"
Замените "*a*" на "|a|"
Замените "*e*" на "|e|"
Замените "*d*" на "|d|"
Замените "*n*" на "|n|"
Замените "*u*" на "|u|"
Замените "*t*" на "|t|"
Слову "unwanted" соответствует следующая последовательность преобразований:
unwanted un[|w|o|n|t|]ed (правило
2) [|u|][|n|][|w|o|n|t|][|e|][|d|] (правила 4, 5, 6,
7) u|n|w|o|n|t|e|d (с удалением лишних символов)
Как вы видите, слова в которых присутствуют буквы wont
проговариваются иначе чем слова, содержащие буквы ant. Также обратите
внимание на частное правило произношения слова unique, имеющее
приоритет над остальными правилами, чтобы оно произносилось как y|ou...а не
как u|n....
Призрак из машины обращается к вам Эта статья
представляет готовый к использованию, удобный речевой движок для использования в
ваших приложениях на Java 1.3. Если вы взглянете на код, то он также послужит
вам полезным введением в JavaSound API для воспроизведения аудиоклипов. Для того
чтобы он был действительно полезен вы должны подумать над идеей преобразования
текста в речь, поскольку это является фундаментом для всех приложений, связанных
с проговариванием слов. В предложенном мною подходе вам придется придумать
огромное количество правил замены букв и правильное расположение их по иерархии.
Надеюсь что у вас окажется больше выдержки чем у меня!
И, в завершении, вы помните мое упоминание телефона Nokia 9210 во
введении. У меня есть такой. Он поддерживает Java и я решил заставить его
говорить используя Java. Я также решил реализовать речь вне стандартных апплетов
(до выхода Java 2), выполняемых в броузере. Тогда как методика, рассмотренная в
этой статье, допустима для этих задач, возникают проблемы с технологией,
поскольку она базируется на звуковом движке J2SE 1.3. Требуется другой подход,
основанный на использовании простого интерфейса Java AudioClip. Это не так
просто как вам могло бы показаться, но я работаю над этим.
О авторе Тони Лотон (Tony Loton) работает в собственной компании -- LOTONtech Limited,
занимающейся разработкой программных решений, консультациями, обучением и
разработкой технической документации. В этом году на него напала литературная
чесотка и Тони принял участие в написании книги для John Wiley & Sons и Wrox
Press.
|