File Coverage

blib/lib/Speech/Synthesis.pm
Criterion Covered Total %
statement 66 307 21.5
branch 16 180 8.8
condition 5 61 8.2
subroutine 20 39 51.2
pod 8 8 100.0
total 115 595 19.3


line stmt bran cond sub pod time code
1             package Speech::Synthesis;
2            
3 9     9   17007 use warnings;
  6         10  
  6         196  
4 9     9   1346 use strict;
  6         11  
  6         393  
5            
6             our $VERSION = '0.03';
7            
8 7     7   3309 eval "use Win32::MSAgent 0.05";
  0         0  
  0         0  
9 7     7   2958 eval "use Win32::SAPI4 0.07";
  0         0  
  0         0  
10 7     7   2901 eval "use Win32::SAPI5 0.04";
  0         0  
  0         0  
11 7     7   2794 eval "use Win32::OLE";
  0         0  
  0         0  
12 7     7   2410 eval "use Win32::Locale";
  1         2  
  1         39  
13 6     6   2464 eval "use Mac::Speech";
  0         0  
  0         0  
14 6     6   13742 eval "use Festival::Client::Async qw(parse_lisp)";
  6         236596  
  6         358  
15             # These two are core modules
16 9     9   6286 use Locale::Language;
  6         1894070  
  6         490  
17 9     9   6637 use Locale::Country;
  6         181677  
  6         557  
18            
19 9     9   79 use constant SUPPORTED_ENGINES => qw(MSAgent SAPI4 SAPI5 MacSpeech Festival);
  9         38  
  9         37038  
20            
21             my $LANGUAGES = {
22             0 => 'English',
23             1 => 'French',
24             2 => 'German',
25             3 => 'Italian',
26             4 => 'Dutch',
27             5 => 'Swedish',
28             6 => 'Spanish',
29             7 => 'Danish',
30             8 => 'Portuguese',
31             9 => 'Norwegian',
32             10 => 'Hebrew',
33             11 => 'Japanese',
34             12 => 'Arabic',
35             13 => 'Finnish',
36             14 => 'Greek',
37             15 => 'Icelandic',
38             16 => 'Maltese',
39             17 => 'Turkish',
40             18 => 'Croatian',
41             19 => 'Traditional Chinese',
42             20 => 'Urdu',
43             21 => 'Hindi',
44             22 => 'Thai',
45             23 => 'Korean',
46             24 => 'Lithuanian',
47             25 => 'Polish',
48             26 => 'Hungarian',
49             27 => 'Estonian',
50             28 => 'Latvian',
51             29 => 'Sami',
52             30 => 'Faroese',
53             31 => 'Farsi',
54             31 => 'Persian',
55             32 => 'Russian',
56             33 => 'Simplified Chinese',
57             34 => 'Dutch',
58             35 => 'Irish Gaelic',
59             36 => 'Albanian',
60             37 => 'Romanian',
61             38 => 'Czech',
62             39 => 'Slovak',
63             40 => 'Slovenian',
64             41 => 'Yiddish',
65             42 => 'Serbian',
66             43 => 'Macedonian',
67             44 => 'Bulgarian',
68             45 => 'Ukrainian',
69             46 => 'Byelorussian',
70             46 => 'Belorussian',
71             47 => 'Uzbek',
72             48 => 'Kazakh',
73             49 => 'Azerbaijani',
74             50 => 'Azerbaijani',
75             51 => 'Armenian',
76             52 => 'Georgian',
77             53 => 'Moldavian',
78             54 => 'Kirghiz',
79             55 => 'Tajiki',
80             56 => 'Turkmen',
81             57 => 'Mongolian',
82             58 => 'Mongolian',
83             59 => 'Pashto',
84             60 => 'Kurdish',
85             61 => 'Kashmiri',
86             62 => 'Sindhi',
87             63 => 'Tibetan',
88             64 => 'Nepali',
89             65 => 'Sanskrit',
90             66 => 'Marathi',
91             67 => 'Bengali',
92             68 => 'Assamese',
93             69 => 'Gujarati',
94             70 => 'Punjabi',
95             71 => 'Oriya',
96             72 => 'Malayalam',
97             73 => 'Kannada',
98             74 => 'Tamil',
99             75 => 'Telugu',
100             76 => 'Sinhalese',
101             77 => 'Burmese',
102             78 => 'Khmer',
103             79 => 'Lao',
104             80 => 'Vietnamese',
105             81 => 'Indonesian',
106             82 => 'Tagalog',
107             83 => 'Malaysian',
108             84 => 'Malaysian',
109             85 => 'Amharic',
110             86 => 'Tigrinya',
111             87 => 'Oromo',
112             88 => 'Somali',
113             89 => 'Swahili',
114             90 => 'Kinyarwanda',
115             90 => 'Ruanda',
116             91 => 'Rundi',
117             92 => 'Nyanja',
118             92 => 'Chewa',
119             93 => 'Malagasy',
120             94 => 'Esperanto',
121             128 => 'Welsh',
122             129 => 'Basque',
123             130 => 'Catalan',
124             131 => 'Latin',
125             132 => 'Quechua',
126             133 => 'Guarani',
127             134 => 'Aymara',
128             135 => 'Tatar',
129             136 => 'Uighur',
130             137 => 'Dzongkha',
131             138 => 'Javanese',
132             139 => 'Sundanese',
133             140 => 'Galician',
134             141 => 'Afrikaans'
135            
136             };
137            
138             my $REGIONS = {
139             0 => 'United States',
140             1 => 'France',
141             2 => 'Great Britain',
142             3 => 'Germany',
143             4 => 'Italy',
144             5 => 'Netherlands',
145             6 => 'Belgium',
146             7 => 'Sweden',
147             8 => 'Spain',
148             9 => 'Denmark',
149             10 => 'Portugal',
150             11 => 'Canada',
151             12 => 'Norway',
152             13 => 'Israel',
153             14 => 'Japan',
154             15 => 'Australia',
155             16 => 'Arabia',
156             17 => 'Finland',
157             18 => 'Switzerland',
158             19 => 'Switzerland',
159             20 => 'Greece',
160             21 => 'Iceland',
161             22 => 'Malta',
162             23 => 'Cyprus',
163             24 => 'Turkey',
164             25 => 'Croatia',
165             26 => 'Netherlands',
166             27 => 'Belgium',
167             28 => 'Canada',
168             29 => 'Canada',
169             30 => 'Portugal',
170             31 => 'Norway',
171             32 => 'Denmark',
172             33 => 'India',
173             34 => 'Pakistan',
174             35 => 'Turkey',
175             36 => 'Switzerland',
176             37 => 'Unknown',
177             39 => 'Romania',
178             40 => 'Greece',
179             41 => 'Lithuania',
180             42 => 'Poland',
181             43 => 'Hungary',
182             44 => 'Estonia',
183             45 => 'Latvia',
184             46 => 'Sami',
185             47 => 'Faroe Islands',
186             48 => 'Iran',
187             49 => 'Russia',
188             50 => 'Ireland',
189             51 => 'Korea',
190             52 => 'China',
191             53 => 'Taiwan',
192             54 => 'Thailand',
193             55 => 'Unknown',
194             56 => 'Czech',
195             57 => 'Slovenia',
196             58 => 'Unknown',
197             59 => 'Magyar',
198             60 => 'Bengali',
199             61 => 'ByeloRussian',
200             62 => 'Ukraine',
201             64 => 'GreeceAlt',
202             65 => 'Serbian',
203             66 => 'Slovenian',
204             67 => 'Macedonian',
205             68 => 'Croatia',
206             70 => 'German',
207             71 => 'Brazil',
208             72 => 'Bulgaria',
209             73 => 'Catalonia',
210             74 => 'Unknown',
211             75 => 'Scotland',
212             76 => 'ManxGaelic',
213             77 => 'Breton',
214             78 => 'Nunavut',
215             79 => 'Welsh',
216             81 => 'Ireland',
217             82 => 'Canada',
218             83 => 'Bhutan',
219             84 => 'Armenia',
220             85 => 'Georgia',
221             86 => 'Unknown',
222             88 => 'Tonga',
223             91 => 'Unknown',
224             92 => 'Austria',
225             94 => 'Gujarat',
226             95 => 'Punjab',
227             96 => 'India',
228             97 => 'Vietnam',
229             98 => 'Belgium',
230             99 => 'Uzbekistan',
231             100 => 'Singapore',
232             101 => 'Norway',
233             102 => 'South Africa',
234             103 => 'Unknown',
235             104 => 'Marathi',
236             105 => 'Tibet',
237             106 => 'Nepal',
238             107 => 'Greenland',
239             108 => 'Ireland'
240             };
241            
242             our $DIALECTS = {
243             'flemish' => 'Belgium',
244             'american' => 'United States',
245             'castilian' => 'Spain'
246             };
247            
248             sub InstalledEngines
249             {
250 9     9 1 73 my $class = shift;
251 9         20 my @engines = ();
252 9         16 my $engine;
253 9     5   820 $engine = eval "use Win32::MSAgent";
  5         2348  
  0         0  
  0         0  
254 9 50       669 push @engines, 'MSAgent' unless $@;
255 9     5   594 $engine = eval "use Win32::SAPI4";
  5         2073  
  0         0  
  0         0  
256 9 50       590 push @engines, 'SAPI4' unless $@;
257 9     5   534 $engine = eval "use Win32::SAPI5";
  5         2075  
  0         0  
  0         0  
258 9 50       799 push @engines, 'SAPI5' unless $@;
259 9     5   553 $engine = eval "use Mac::Speech";
  5         2006  
  0         0  
  0         0  
260 9 50       596 push @engines, 'MacSpeech' unless $@;
261 9     5   549 $engine = eval 'use Festival::Client::Async';
  5         31  
  5         11  
  5         230  
262 9 50       44 push @engines, 'Festival' unless $@;
263 9         41 return @engines;
264             }
265            
266             sub InstalledLanguages
267             {
268 1     1 1 676 my $class = shift;
269 1         10 my %params = @_;
270 1 50 33     21 return unless (exists $params{engine}) && (grep {$_ eq $params{engine}} Speech::Synthesis->InstalledEngines());
  1         10  
271 1         2 my @alllangs = ();
272 1 50 33     18 if (($params{engine} eq 'MSAgent') || ($params{engine} eq 'SAPI4'))
    50          
    50          
    50          
273             {
274 0   0     0 my $sapi4 = Win32::SAPI4::VoiceText->new() || die "Can't start SAPI4: ".Win32::OLE->LastError();
275 0         0 @alllangs = $sapi4->GetInstalledLanguages();
276 0 0       0 @alllangs = grep {$params{engine} ne 'MSAgent' || $_ ne 'unknown'} @alllangs;
  0         0  
277 0         0 @alllangs = map {/(.+?)\((.+)\)/;my $l = $1;chop $l;language2code($l).'_'.uc(country2code($2));} @alllangs;
  0         0  
  0         0  
  0         0  
  0         0  
278             }
279             elsif ($params{engine} eq 'SAPI5')
280             {
281 0   0     0 my $sapi5 ||= Win32::SAPI5::SpVoice->new() || die "Can't start SAPI5: ".Win32::OLE->LastError();
      0        
282 0         0 @alllangs = $sapi5->GetInstalledLanguages();
283 0         0 @alllangs = map {/(.+?)\((.+)\)/;my $l = $1;chop $l;language2code($l).'_'.uc(country2code($2));} @alllangs;
  0         0  
  0         0  
  0         0  
  0         0  
284             }
285             elsif ($params{engine} eq 'MacSpeech')
286             {
287 0         0 my %langs;
288 0         0 my $count = CountVoices();
289 0         0 my %maclangs = ();
290 0         0 for (my $i = 0; $i++ < $count; )
291             {
292 0         0 my $voice = GetIndVoice($i);
293 0         0 my $desc = ${GetVoiceDescription($voice)};
  0         0  
294 0         0 my ($synt, $id, $version,$nlen,$name,$clen,$comment,$gender,$age,$script,$language,$region)
295             = unpack("x4 a4 l l C a63 C a255 s s s s s", $desc);
296 0         0 $langs{language2code($LANGUAGES->{$language}).'_'.uc(country2code($REGIONS->{$region}))} = 1;
297             }
298 0         0 @alllangs = keys %langs;
299             }
300             elsif ($params{engine} eq 'Festival')
301             {
302 1         3 my %langs;
303 1         7 my @voices = Speech::Synthesis->InstalledVoices(engine => 'Festival',
304             host => $params{host},
305             port => $params{port});
306 1         3 foreach my $voice(@voices)
307             {
308 0         0 $langs{$voice->{language}} = 1;
309             }
310 1         3 @alllangs = keys %langs;
311             }
312 1         5 return @alllangs;
313             }
314            
315             sub InstalledVoices
316             {
317 3     3 1 600 my $class = shift;
318 3         19 my %params = @_;
319 3 50 33     28 return unless (exists $params{engine}) && (grep {$_ eq $params{engine}} Speech::Synthesis->InstalledEngines());
  3         28  
320 3         8 my @allvoices = ();
321 3 50 33     50 if (($params{engine} eq 'MSAgent') || ($params{engine} eq 'SAPI4'))
    50          
    50          
    50          
322             {
323 0   0     0 my $sapi4 = Win32::SAPI4::VoiceText->new() || die "Can't start SAPI4: ".Win32::OLE->LastError();
324 0         0 my $object = $sapi4->GetObject;
325 0         0 for (my $i=1; $i <= $object->CountEngines; $i++)
326             {
327 0         0 my $lang = Win32::Locale::get_language($object->LanguageID($i));
328 0 0 0     0 next if $params{engine} eq 'MSAgent' && not $lang;
329 0         0 my ($l, $r) = split(/-/,$lang);
330 0 0       0 if (exists $params{language})
331             {
332 0 0       0 next unless $params{language} eq lc($l)."_".uc($r);
333             }
334 0 0       0 my $gen = $object->Gender($i) == 1 ? 'female' : $object->Gender($i) == 2 ? 'male' : 'neutral';
    0          
335            
336 0 0       0 if (exists $params{gender})
337             {
338 0 0       0 next unless $gen eq $params{gender};
339             }
340 0         0 push @allvoices, { name => $object->ModeName($i),
341             id => $object->ModeID($i),
342             age => eval "$object->Age($i)",
343             gender => $gen,
344             language=> lc($l)."_".uc($r),
345             description => $object->ProductName($i)
346             }
347             }
348             }
349             elsif ($params{engine} eq 'SAPI5')
350             {
351 0   0     0 my $sapi5 ||= Win32::SAPI5::SpVoice->new() || die "Can't start SAPI5: ".Win32::OLE->LastError();
      0        
352 0         0 my $object = $sapi5->GetObject();
353 0         0 my $tokens = $object->GetVoices;
354 0         0 for (my $i = 0; $i < $tokens->Count; $i++)
355             {
356 0         0 my ($lang, undef) = split(/;/,$tokens->Item($i)->GetAttribute('Language'));
357 0         0 $lang = Win32::Locale::get_language(hex("0x$lang"));
358 0 0       0 if ($lang)
359             {
360 0         0 my ($l, $r) = split(/-/,$lang);
361 0 0       0 if (exists $params{language})
362             {
363 0 0       0 next unless $params{language} eq lc($l)."_".uc($r);
364             }
365 0         0 my $gender = lc($tokens->Item($i)->GetAttribute('Gender'));
366            
367 0 0       0 if (exists $params{gender})
368             {
369 0 0       0 next unless $gender eq $params{gender};
370             }
371 0         0 push @allvoices, { name => $tokens->Item($i)->GetAttribute('Name'),
372             id => $tokens->Item($i)->Id,
373             age => eval("$tokens->Item($i)->GetAttribute('Age')"),
374             gender => $gender,
375             language=> lc($l)."_".uc($r),
376             description => $tokens->Item($i)->GetDescription()
377             }
378             }
379             }
380             }
381             elsif ($params{engine} eq 'MacSpeech')
382             {
383 0         0 my $count = CountVoices();
384 0         0 for (my $i = 0; $i++ < $count; )
385             {
386 0         0 my $thislang;
387 0         0 my $voice = GetIndVoice($i);
388 0         0 my $desc = ${GetVoiceDescription($voice)};
  0         0  
389 0         0 my ($synt, $id, $version,$nlen,$name,$clen,$comment,$gender,$age,$script,$language,$region)
390             = unpack("x4 a4 l l C a63 C a255 s s s s s", $desc);
391 0         0 $thislang = language2code($LANGUAGES->{$language}).'_'.uc(country2code($REGIONS->{$region}));
392 0 0       0 if (exists $params{language})
393             {
394 0 0       0 next unless $thislang eq $params{language};
395             }
396            
397 0 0       0 my $gen = $gender == 0 ? 'neutral' : $gender == 1 ? 'male' : 'female';
    0          
398 0 0       0 if (exists $params{gender})
399             {
400 0 0       0 next unless $gen eq $params{gender};
401             }
402            
403 0         0 $name = substr $name, 0, $nlen;
404 0         0 $comment = substr $comment, 0, $clen;
405 0         0 push @allvoices, { name => $name,
406             id => $id,
407             age => $age,
408             gender => $gen,
409             language => $thislang,
410             description => $comment
411             };
412             }
413             }
414             elsif ($params{engine} eq 'Festival')
415             {
416 3   33     25 my $fest = Festival::Client::Async->new($params{host}, $params{port}) || warn "No festival server seems to be running: $!";
417 3 50       4558 return () unless ref $fest;
418             $fest->server_eval_sync("(voice.list)",
419             {
420             LP => sub {
421 0     0   0 my $l = shift;
422 0         0 my $p = parse_lisp($l);
423 0         0 @allvoices = map {{name => $_, id => $_ }} @$p
  0         0  
424             },
425 0     0   0 WV => sub {}
426 0         0 });
427 0         0 foreach my $voice (@allvoices)
428             {
429             $fest->server_eval_sync("(voice.description '".$voice->{id}.')',
430             {
431             LP => sub {
432 0     0   0 my $l = shift;
433 0         0 my $p = parse_lisp($l);
434 0 0       0 if ($p eq 'nil')
435             {
436 0         0 $voice->{description} = undef;
437 0         0 $voice->{age} = undef;
438 0         0 $voice->{gender} = undef;
439 0         0 $voice->{language} = 'unknown';
440             }
441             else
442             {
443 0         0 my @return = @{$p->[1]};
  0         0  
444 0         0 my %h = map {$_->[0] => $_->[1]} @return;
  0         0  
445 0         0 $voice->{description} = $h{description};
446 0         0 $voice->{age} = undef;
447 0         0 $voice->{gender} = $h{gender};
448 0         0 $h{dialect} =~ s/['"]//g;
449 0         0 $voice->{language} = lc(language2code($h{language}));
450 0 0       0 $voice->{language} = sprintf("%s_%s", lc(language2code($h{language})), uc(country2code($DIALECTS->{$h{dialect}}))) if exists $DIALECTS->{$h{dialect}};
451 0 0       0 $voice->{language} = sprintf("%s_%s", lc(language2code($h{language})), uc(country2code($h{dialect}))) if country2code($h{dialect});
452             }
453             },
454 0     0   0 WV => sub {}
455 0         0 });
456            
457             }
458             }
459 0         0 return @allvoices;
460             }
461            
462             sub InstalledAvatars
463             {
464 0     0 1 0 my $class = shift;
465 0         0 my %params = @_;
466 0 0 0     0 return () unless (exists $params{engine}) && ($params{engine} eq 'MSAgent');
467 0   0     0 my $agent = Win32::MSAgent->new() || die "Can't start Microsoft Agent";
468 0 0       0 return $agent->GetInstalledCharacters if defined $agent;
469             }
470            
471             sub new
472             {
473 0     0 1 0 my $proto = shift;
474 0         0 my %params = @_;
475 0   0     0 my $class = ref($proto) || $proto;
476 0         0 my $self = {};
477 0         0 bless $self, $class;
478 0 0       0 unless (exists $params{engine})
479             {
480 0         0 warn "The 'engine' parameter is mandatory";
481 0         0 return;
482             }
483 0 0       0 unless (grep {$params{engine} eq $_} SUPPORTED_ENGINES)
  0         0  
484             {
485 0         0 warn "Unknown 'engine': $params{engine}";
486 0         0 return;
487             }
488 0         0 $self->{_engine} = $params{engine};
489 0         0 $self->{_voice} = $params{voice};
490 0 0       0 $self->{_async} = exists($params{async}) ? $params{async} : 1;
491 0 0       0 if ($self->{_engine} eq 'MSAgent')
    0          
492             {
493 0 0 0     0 unless ((exists $params{language}) && (exists $params{avatar}))
494             {
495 0 0       0 warn "The 'language' parameter is mandatory if you specify 'MSAgent' as the engine type" unless exists $params{language};
496 0 0       0 warn "The 'avatar' parameter is mandatory if you specify 'MSAgent' as the engine type" unless exists $params{avatar};
497 0         0 return;
498             }
499 0         0 $self->{_language} = $params{language};
500 0         0 $self->{_avatar} = $params{avatar};
501             }
502             elsif ($self->{_engine} eq 'Festival')
503             {
504 0   0     0 $self->{_host} = $params{host} || 'localhost';
505 0   0     0 $self->{_port} = $params{port} || 1314;
506             }
507 0         0 $self->_init();
508 0         0 return $self;
509             }
510            
511             sub getobject
512             {
513 0     0 1 0 my $self = shift;
514 0 0       0 return unless exists $self->{_engine};
515 0 0       0 if ($self->{_engine} eq 'MSAgent')
    0          
    0          
    0          
    0          
516             {
517 0         0 return $self->{_char};
518             }
519             elsif ($self->{_engine} eq 'SAPI4')
520             {
521 0         0 return $self->{_sapi4};
522             }
523             elsif ($self->{_engine} eq 'SAPI5')
524             {
525 0         0 return $self->{_sapi5};
526             }
527             elsif ($self->{_engine} eq 'MacSpeech')
528             {
529             # Can't really return anything here, now can we?
530             }
531             elsif ($self->{_engine} eq 'Festival')
532             {
533 0         0 return $self->{_fest};
534             }
535             }
536            
537             our $AUTOLOAD;
538             sub AUTOLOAD
539             {
540 0     0   0 my $self = shift;
541 0 0       0 return unless exists $self->{_engine};
542 0         0 my @params = @_;
543 0         0 (my $auto = $AUTOLOAD) =~ s/.*:://;
544 0 0       0 if ($self->{_engine} eq 'MSAgent')
    0          
    0          
    0          
    0          
545             {
546 0         0 $self->{_char}->$auto(@params);
547             }
548             elsif ($self->{_engine} eq 'SAPI4')
549             {
550 0         0 $self->{_sapi4}->$auto(@params);
551             }
552             elsif ($self->{_engine} eq 'SAPI5')
553             {
554 0         0 $self->{_sapi5}->$auto(@params);
555             }
556             elsif ($self->{_engine} eq 'MacSpeech')
557             {
558             # Can't really autoload anything here, now can we?
559             }
560             elsif ($self->{_engine} eq 'Festival')
561             {
562 0         0 $self->{_fest}->$auto(@params);
563             }
564             }
565            
566             sub voice
567             {
568 0     0 1 0 my $self = shift;
569 0         0 my $id = shift;
570 0 0       0 $self->{_voice} = $id if defined $id;
571 0 0       0 $self->_init() if defined $id;
572 0         0 return $self->{_voice};
573             }
574            
575             sub speak
576             {
577 0     0 1 0 my $self = shift;
578 0         0 my $text = shift;
579 0 0       0 if ($self->{_engine} eq 'MSAgent')
    0          
    0          
    0          
    0          
580             {
581 0         0 $self->{_char}->Speak($text);
582             }
583             elsif ($self->{_engine} eq 'SAPI4')
584             {
585             # Normally we would use the 'Speak' method, but it seems like
586             # Fluency speechengines make the application crash when using Speak
587             # if pVoice starts up using the Fluency engine. Very strange, but this
588             # seems to be a workaround...
589 0         0 $self->{_sapi4}->TextData(0,0,$text);
590             }
591             elsif ($self->{_engine} eq 'SAPI5')
592             {
593 0         0 my $status = $self->{_sapi5}->Status;
594 0         0 $self->{_sapi5}->Speak($text);
595 0 0       0 unless ($self->{_async})
596             {
597 0         0 do {} while ($status->{RunningState} == 2);
598             }
599             }
600             elsif ($self->{_engine} eq 'MacSpeech')
601             {
602 0         0 SpeakText($self->{_macspeech}, $text);
603 0 0       0 unless ($self->{_async}){ do {} while (SpeechBusy())}
  0         0  
604             }
605             elsif ($self->{_engine} eq 'Festival')
606             {
607 0         0 $text =~ s/\"/\'/g;
608 0 0       0 if ($self->{_async})
609             {
610 0         0 $self->{_fest}->server_eval('(SayText "'.$text.'")');
611 0 0       0 if ($self->{_fest}->write_pending) { while (defined(my $buf = $self->{_fest}->write_more)) {last unless $buf} }
  0 0       0  
  0         0  
612             }
613             else
614             {
615 0 0   0   0 $self->{_fest}->server_eval_sync('(SayText "'.$text.'")', { LP => sub {}, WV => sub {} }) || warn "Festival error";
  0         0  
  0         0  
616             }
617             }
618             }
619            
620             sub _init
621             {
622 0     0   0 my $self = shift;
623 0 0       0 $self->_initagent if $self->{_engine} eq 'MSAgent';
624 0 0       0 $self->_initsapi4 if $self->{_engine} eq 'SAPI4';
625 0 0       0 $self->_initsapi5 if $self->{_engine} eq 'SAPI5';
626 0 0       0 $self->_initmacspeech if $self->{_engine} eq 'MacSpeech';
627 0 0       0 $self->_initfestival if $self->{_engine} eq 'Festival';
628             }
629            
630             sub _initagent
631             {
632 0     0   0 my $self = shift;
633 0 0       0 return unless $self->{_engine} eq 'MSAgent';
634 0 0 0     0 if ((exists $self->{_agent}) && ($self->{_loadedchar} ne $self->{_avatar}))
635             {
636 0         0 $self->{_char}->Hide();
637 0         0 $self->{_agent}->Characters->Unload($self->{_loadedchar});
638             }
639             else
640             {
641 0   0     0 $self->{_agent} = Win32::MSAgent->new() || die "Can't start MSAgent: ".Win32::OLE->LastError();;
642             }
643 0         0 $self->{_agent}
644             ->Characters->Load($self->{_avatar},
645             $self->{_avatar}.".acs");
646 0         0 $self->{_loadedchar} = $self->{_avatar};
647             # To be able to access the character from $self's
648             # action methods, we have to define it as a property of that
649 0         0 $self->{_char} = $self->{_agent}->Characters($self->{_avatar});
650 0         0 my %langtag2msloc = reverse %Win32::Locale::MSLocale2LangTag;
651 0         0 my $lang = lc($self->{_language});
652 0         0 $lang =~ s/_/-/;
653 0         0 $lang = $langtag2msloc{$lang};
654 0         0 $self->{_char}->SetProperty('LanguageID',$lang);
655 0 0       0 $self->{_char}->SetProperty('TTSModeID', "{".$self->{_voice}."}")
656             if exists $self->{_voice};
657 0         0 $self->{_char}->MoveTo(0, 350);
658            
659             # Show the MS Agent
660 0         0 $self->{_char}->Show();
661             }
662            
663             sub _initsapi4
664             {
665 0     0   0 my $self = shift;
666 0 0       0 return unless $self->{_engine} eq 'SAPI4';
667 0   0     0 $self->{_sapi4} = Win32::SAPI4::DirectSpeechSynthesis->new() || die "Can't start SAPI4: ".Win32::OLE->LastError();
668 0         0 for (my $i=1; $i <= $self->{_sapi4}->CountEngines; $i++)
669             {
670 0 0       0 $self->{_sapi4}->Select($i) if $self->{_sapi4}->ModeID($i) eq $self->{_voice};
671             }
672 0         0 do {} until ($self->{_sapi4}->Initialized);
673             }
674            
675             sub _initsapi5
676             {
677 0     0   0 my $self = shift;
678 0 0       0 return unless $self->{_engine} eq 'SAPI5';
679 0   0     0 $self->{_sapi5} ||= Win32::SAPI5::SpVoice->new() || die "Can't start SAPI5: ".Win32::OLE->LastError();
      0        
680 0         0 my $tokens = $self->{_sapi5}->GetVoices;
681 0         0 for (my $i = 0; $i < $tokens->Count; $i++)
682             {
683 0 0       0 $self->{_sapi5}->SetProperty('Voice', $tokens->Item($i)) if $tokens->Item($i)->Id eq $self->{_voice};
684             }
685             }
686            
687             sub _initmacspeech
688             {
689 0     0   0 my $self = shift;
690 0 0       0 return unless $self->{_engine} eq 'MacSpeech';
691 0         0 for (my $i=1; $i <= CountVoices(); $i++)
692             {
693 0         0 my $voice = GetIndVoice($i);
694 0         0 my $desc = ${GetVoiceDescription($voice)};
  0         0  
695 0         0 my ($synt, $id, $version,$nlen,$name,$clen,$comment,$gender,$age,$script,$language,$region)
696             = unpack("x4 a4 l l C a63 C a255 s s s s s", $desc);
697 0 0       0 $self->{_macspeech} = NewSpeechChannel($voice) if $id eq $self->{_voice};
698             }
699             }
700            
701             sub _initfestival
702             {
703 0     0   0 my $self = shift;
704 0 0       0 return unless $self->{_engine} eq 'Festival';
705 0   0     0 $self->{_fest} = Festival::Client::Async->new($self->{_host}, $self->{_port}) || die "No festival server seems to be running: $!";
706 0 0   0   0 $self->{_fest}->server_eval_sync('(voice.select "'.$self->{_voice}.'")', { LP => sub {}, WV => sub {} }) || die "Festival error";
  0         0  
  0         0  
707             }
708            
709             sub DESTROY
710             {
711 0     0   0 my $self = shift;
712 0 0       0 if (exists $self->{_agent})
713             {
714 0         0 $self->{_char}->Hide();
715 0         0 $self->{_agent}->Characters->Unload($self->{_loadedchar});
716             }
717             }
718            
719             1; # End of Speech::Synthesis
720            
721             __END__