Coverage Report

Created: 2026-10-28 06:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/espeak-ng/src/libespeak-ng/readclause.c
Line
Count
Source
1
/*
2
 * Copyright (C) 2005 to 2015 by Jonathan Duddington
3
 * email: jonsd@users.sourceforge.net
4
 * Copyright (C) 2015-2017 Reece H. Dunn
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, see: <http://www.gnu.org/licenses/>.
18
 */
19
20
#include "config.h"
21
22
#include <ctype.h>
23
#include <errno.h>
24
#include <locale.h>
25
#include <math.h>
26
#include <stdint.h>
27
#include <stdio.h>
28
#include <stdlib.h>
29
#include <string.h>
30
#include <unistd.h>
31
#include <wchar.h>
32
#include <wctype.h>
33
34
#include <espeak-ng/espeak_ng.h>
35
#include <espeak-ng/speak_lib.h>
36
#include <espeak-ng/encoding.h>
37
#include <ucd/ucd.h>
38
39
#include "readclause.h"
40
#include "common.h"               // for GetFileLength, strncpy0
41
#include "dictionary.h"           // for LookupDictList, DecodePhonemes, Set...
42
#include "error.h"                // for create_file_error_context
43
#include "phoneme.h"              // for phonSWITCH
44
#include "soundicon.h"               // for LookupSoundIcon
45
#include "speech.h"               // for LookupMnem, PATHSEP
46
#include "ssml.h"                 // for SSML_STACK, ProcessSsmlTag, N_PARAM...
47
#include "synthdata.h"            // for SelectPhonemeTable
48
#include "translate.h"            // for Translator, utf8_out, CLAUSE_OPTION...
49
#include "voice.h"                // for voice, voice_t, espeak_GetCurrentVoice
50
51
857k
#define N_XML_BUF   500
52
53
static void DecodeWithPhonemeMode(char *buf, char *phonemes, Translator *tr, Translator *tr2, unsigned int flags[]);
54
static void TerminateBufWithSpaceAndZero(char *buf, int index, int *ungetc);
55
56
static const char *xmlbase = ""; // base URL from <speak>
57
58
static int namedata_ix = 0;
59
static int n_namedata = 0;
60
char *namedata = NULL;
61
62
static int ungot_char2 = 0;
63
espeak_ng_TEXT_DECODER *p_decoder = NULL;
64
static int ungot_char;
65
66
static bool ignore_text = false; // set during <sub> ... </sub>  to ignore text which has been replaced by an alias
67
static bool audio_text = false; // set during <audio> ... </audio>
68
static bool clear_skipping_text = false; // next clause should clear the skipping_text flag
69
int count_characters = 0;
70
static int sayas_mode;
71
static int sayas_start;
72
73
#define N_SSML_STACK  20
74
static int n_ssml_stack;
75
static SSML_STACK ssml_stack[N_SSML_STACK];
76
77
static espeak_VOICE base_voice;
78
static char base_voice_variant_name[40] = { 0 };
79
static char current_voice_id[40] = { 0 };
80
81
static int n_param_stack;
82
PARAM_STACK param_stack[N_PARAM_STACK];
83
84
static int speech_parameters[N_SPEECH_PARAM]; // current values, from param_stack
85
int saved_parameters[N_SPEECH_PARAM]; // Parameters saved on synthesis start
86
87
5.84M
#define ESPEAKNG_CLAUSE_TYPE_PROPERTY_MASK 0xFFF0000000000000ull
88
89
int clause_type_from_codepoint(uint32_t c)
90
5.84M
{
91
5.84M
  ucd_category cat = ucd_lookup_category(c);
92
5.84M
  ucd_property props = ucd_properties(c, cat);
93
94
5.84M
  switch (props & ESPEAKNG_CLAUSE_TYPE_PROPERTY_MASK)
95
5.84M
  {
96
1.01M
  case ESPEAKNG_PROPERTY_FULL_STOP:
97
1.01M
    return CLAUSE_PERIOD;
98
170
  case ESPEAKNG_PROPERTY_FULL_STOP | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER:
99
170
    return CLAUSE_PERIOD | CLAUSE_OPTIONAL_SPACE_AFTER;
100
364k
  case ESPEAKNG_PROPERTY_QUESTION_MARK:
101
364k
    return CLAUSE_QUESTION;
102
0
  case ESPEAKNG_PROPERTY_QUESTION_MARK | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER:
103
0
    return CLAUSE_QUESTION | CLAUSE_OPTIONAL_SPACE_AFTER;
104
1.73k
  case ESPEAKNG_PROPERTY_QUESTION_MARK | ESPEAKNG_PROPERTY_PUNCTUATION_IN_WORD:
105
1.73k
    return CLAUSE_QUESTION | CLAUSE_PUNCTUATION_IN_WORD;
106
3.21k
  case ESPEAKNG_PROPERTY_EXCLAMATION_MARK:
107
3.21k
    return CLAUSE_EXCLAMATION;
108
509
  case ESPEAKNG_PROPERTY_EXCLAMATION_MARK | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER:
109
509
    return CLAUSE_EXCLAMATION | CLAUSE_OPTIONAL_SPACE_AFTER;
110
11
  case ESPEAKNG_PROPERTY_EXCLAMATION_MARK | ESPEAKNG_PROPERTY_PUNCTUATION_IN_WORD:
111
11
    return CLAUSE_EXCLAMATION | CLAUSE_PUNCTUATION_IN_WORD;
112
20.1k
  case ESPEAKNG_PROPERTY_COMMA:
113
20.1k
    return CLAUSE_COMMA;
114
257
  case ESPEAKNG_PROPERTY_COMMA | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER:
115
257
    return CLAUSE_COMMA | CLAUSE_OPTIONAL_SPACE_AFTER;
116
869k
  case ESPEAKNG_PROPERTY_COLON:
117
869k
    return CLAUSE_COLON;
118
5
  case ESPEAKNG_PROPERTY_COLON | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER:
119
5
    return CLAUSE_COLON | CLAUSE_OPTIONAL_SPACE_AFTER;
120
859k
  case ESPEAKNG_PROPERTY_SEMI_COLON:
121
860k
  case ESPEAKNG_PROPERTY_EXTENDED_DASH:
122
860k
    return CLAUSE_SEMICOLON;
123
284
  case ESPEAKNG_PROPERTY_SEMI_COLON | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER:
124
284
  case ESPEAKNG_PROPERTY_QUESTION_MARK | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER | ESPEAKNG_PROPERTY_INVERTED_TERMINAL_PUNCTUATION:
125
305
  case ESPEAKNG_PROPERTY_EXCLAMATION_MARK | ESPEAKNG_PROPERTY_OPTIONAL_SPACE_AFTER | ESPEAKNG_PROPERTY_INVERTED_TERMINAL_PUNCTUATION:
126
305
    return CLAUSE_SEMICOLON | CLAUSE_OPTIONAL_SPACE_AFTER;
127
451
  case ESPEAKNG_PROPERTY_ELLIPSIS:
128
451
    return CLAUSE_SEMICOLON | CLAUSE_SPEAK_PUNCTUATION_NAME | CLAUSE_OPTIONAL_SPACE_AFTER;
129
2.24k
  case ESPEAKNG_PROPERTY_PARAGRAPH_SEPARATOR:
130
2.24k
    return CLAUSE_PARAGRAPH;
131
5.84M
  }
132
133
2.71M
  return CLAUSE_NONE;
134
5.84M
}
135
136
static int IsRomanU(unsigned int c)
137
7.31k
{
138
7.31k
  if ((c == 'I') || (c == 'V') || (c == 'X') || (c == 'L'))
139
4.34k
    return 1;
140
2.97k
  return 0;
141
7.31k
}
142
143
int Eof(void)
144
16.6M
{
145
16.6M
  if (ungot_char != 0)
146
88.5k
    return 0;
147
148
16.6M
  return text_decoder_eof(p_decoder);
149
16.6M
}
150
151
static int GetC(void)
152
13.1M
{
153
13.1M
  int c1;
154
155
13.1M
  if ((c1 = ungot_char) != 0) {
156
43.0k
    ungot_char = 0;
157
43.0k
    return c1;
158
43.0k
  }
159
160
13.1M
  count_characters++;
161
13.1M
  return text_decoder_getc(p_decoder);
162
13.1M
}
163
164
static void UngetC(int c)
165
43.1k
{
166
43.1k
  ungot_char = c;
167
43.1k
}
168
169
const char *WordToString2(char buf[5], unsigned int word)
170
57.2k
{
171
  // Convert a language mnemonic word into a string
172
57.2k
  int ix;
173
57.2k
  char *p;
174
175
57.2k
  p = buf;
176
286k
  for (ix = 3; ix >= 0; ix--) {
177
228k
    if ((*p = word >> (ix*8)) != 0)
178
114k
      p++;
179
228k
  }
180
57.2k
  *p = 0;
181
57.2k
  return buf;
182
57.2k
}
183
184
static const char *LookupSpecial(Translator *tr, const char *string, char *text_out)
185
28
{
186
28
  unsigned int flags[2];
187
28
  char phonemes[55];
188
28
  char *string1 = (char *)string;
189
190
28
  flags[0] = flags[1] = 0;
191
28
  if (LookupDictList(tr, &string1, phonemes, flags, 0, NULL, 0)) {
192
0
    DecodeWithPhonemeMode(text_out, phonemes, tr, NULL, flags);
193
0
    return text_out;
194
0
  }
195
28
  return NULL;
196
28
}
197
198
static const char *LookupCharName(char buf[74], Translator *tr, int c, bool only)
199
23.5k
{
200
  // Find the phoneme string (in ascii) to speak the name of character c
201
  // Used for punctuation characters and symbols
202
203
23.5k
  int ix;
204
23.5k
  unsigned int flags[2];
205
23.5k
  char single_letter[24];
206
23.5k
  char phonemes[60];
207
23.5k
  const char *lang_name = NULL;
208
23.5k
  char *string;
209
210
23.5k
  buf[0] = 0;
211
23.5k
  flags[0] = 0;
212
23.5k
  flags[1] = 0;
213
23.5k
  single_letter[0] = 0;
214
23.5k
  single_letter[1] = '_';
215
23.5k
  ix = utf8_out(c, &single_letter[2]);
216
23.5k
  single_letter[2+ix] = 0;
217
218
23.5k
  if (only == true) {
219
443
    string = &single_letter[2];
220
443
    LookupDictList(tr, &string, phonemes, flags, 0, NULL, 0);
221
443
  }
222
223
23.5k
  if (only == false) {
224
23.1k
    string = &single_letter[1];
225
23.1k
    if (LookupDictList(tr, &string, phonemes, flags, 0, NULL, 0) == 0) {
226
      // try _* then *
227
17.8k
      string = &single_letter[2];
228
17.8k
      if (LookupDictList(tr, &string, phonemes, flags, 0, NULL, 0) == 0) {
229
        // now try the rules
230
16.1k
        single_letter[1] = ' ';
231
16.1k
        TranslateRules(tr, &single_letter[2], phonemes, sizeof(phonemes), NULL, 0, NULL);
232
16.1k
      }
233
17.8k
    }
234
    
235
23.1k
    if (((phonemes[0] == 0) || (phonemes[0] == phonSWITCH)) && (tr->translator_name != L('e', 'n'))) {
236
        // not found, try English
237
14.9k
        SetTranslator2(ESPEAKNG_DEFAULT_VOICE);
238
14.9k
        string = &single_letter[1];
239
14.9k
        single_letter[1] = '_';
240
14.9k
      if (LookupDictList(translator2, &string, phonemes, flags, 0, NULL, 0) == 0) {
241
11.7k
          string = &single_letter[2];
242
11.7k
        LookupDictList(translator2, &string, phonemes, flags, 0, NULL, 0);
243
11.7k
        }
244
14.9k
        if (phonemes[0])
245
3.17k
          lang_name = ESPEAKNG_DEFAULT_VOICE;
246
11.7k
        else
247
11.7k
          SelectPhonemeTable(voice->phoneme_tab_ix); // revert to original phoneme table
248
14.9k
      }
249
23.1k
  }
250
251
23.5k
  if (phonemes[0]) {
252
10.2k
    if (lang_name) {
253
3.17k
      DecodeWithPhonemeMode(buf, phonemes, tr, translator2, flags);
254
3.17k
      SelectPhonemeTable(voice->phoneme_tab_ix); // revert to original phoneme table
255
7.06k
    } else {
256
7.06k
      DecodeWithPhonemeMode(buf, phonemes, tr, NULL, flags);
257
7.06k
    }
258
13.3k
  } else if (only == false)
259
12.9k
    strcpy(buf, "[\002(X1)(X1)(X1)]]");
260
261
23.5k
  return buf;
262
23.5k
}
263
264
static int AnnouncePunctuation(Translator *tr, int c1, int *c2_ptr, char *output, int *bufix, int end_clause)
265
23.1k
{
266
  // announce punctuation names
267
  // c1:  the punctuation character
268
  // c2:  the following character
269
270
271
23.1k
  const char *punctname = NULL;
272
23.1k
  int soundicon;
273
23.1k
  int attributes;
274
23.1k
  int short_pause;
275
23.1k
  int c2;
276
23.1k
  int len;
277
23.1k
  int bufix1;
278
23.1k
  char buf[200];
279
23.1k
  char ph_buf[30];
280
23.1k
  char cn_buf[74];
281
282
23.1k
  c2 = *c2_ptr;
283
23.1k
  buf[0] = 0;
284
285
23.1k
  if ((soundicon = LookupSoundicon(c1)) >= 0) {
286
    // add an embedded command to play the soundicon
287
0
    sprintf(buf, "\001%dI ", soundicon);
288
0
    UngetC(c2);
289
23.1k
  } else {
290
23.1k
    if ((c1 == '.') && (end_clause) && (c2 != '.')) {
291
28
      if (LookupSpecial(tr, "_.p", ph_buf))
292
0
        punctname = ph_buf; // use word for 'period' instead of 'dot'
293
28
    }
294
23.1k
    if (punctname == NULL)
295
23.1k
      punctname = LookupCharName(cn_buf, tr, c1, false);
296
297
23.1k
    if (punctname == NULL)
298
0
      return -1;
299
300
301
23.1k
    if ((*bufix == 0) || (end_clause == 0) || (tr->langopts.param[LOPT_ANNOUNCE_PUNCT] & 2)) {
302
22.6k
      int punct_count = 1;
303
28.1k
      while (!Eof() && (c2 == c1) && (c1 != '<')) { // don't eat extra '<', it can miss XML tags
304
5.54k
        punct_count++;
305
5.54k
        c2 = GetC();
306
5.54k
      }
307
22.6k
      *c2_ptr = c2;
308
22.6k
      if (end_clause)
309
493
        UngetC(c2);
310
311
22.6k
      if (punct_count == 1)
312
20.0k
        sprintf(buf, " %s", punctname); // we need the space before punctname, to ensure it doesn't merge with the previous word  (eg.  "2.-a")
313
2.56k
      else if (punct_count < 4) {
314
1.88k
        buf[0] = 0;
315
1.88k
        if (embedded_value[EMBED_S] < 300)
316
582
          sprintf(buf, "\001+10S"); // Speak punctuation name faster, unless we are already speaking fast.  It would upset Sonic SpeedUp
317
318
1.88k
        char buf2[80];
319
6.05k
        while (punct_count-- > 0) {
320
4.16k
          sprintf(buf2, " %s", punctname);
321
4.16k
          strcat(buf, buf2);
322
4.16k
        }
323
324
1.88k
        if (embedded_value[EMBED_S] < 300) {
325
582
          sprintf(buf2, " \001-10S");
326
582
          strcat(buf, buf2);
327
582
        }
328
1.88k
      } else
329
679
        sprintf(buf, " %s %d %s",
330
679
                punctname, punct_count, punctname);
331
22.6k
    } else {
332
      // end the clause now and pick up the punctuation next time
333
520
      ungot_char2 = c1;
334
520
      TerminateBufWithSpaceAndZero(buf, 0, &c2);
335
520
    }
336
23.1k
  }
337
338
23.1k
  bufix1 = *bufix;
339
23.1k
  len = strlen(buf);
340
23.1k
  strcpy(&output[*bufix], buf);
341
23.1k
  *bufix += len;
342
343
23.1k
  if (end_clause == 0)
344
22.1k
    return -1;
345
346
1.01k
  if (c1 == '-')
347
0
    return CLAUSE_NONE; // no pause
348
349
1.01k
  attributes = clause_type_from_codepoint(c1);
350
351
1.01k
  short_pause = CLAUSE_SHORTFALL;
352
1.01k
  if ((attributes & CLAUSE_INTONATION_TYPE) == 0x1000)
353
171
    short_pause = CLAUSE_SHORTCOMMA;
354
355
1.01k
  if ((bufix1 > 0) && !(tr->langopts.param[LOPT_ANNOUNCE_PUNCT] & 2)) {
356
520
    if ((attributes & ~CLAUSE_OPTIONAL_SPACE_AFTER) == CLAUSE_SEMICOLON)
357
96
      return CLAUSE_SHORTFALL;
358
424
    return short_pause;
359
520
  }
360
361
493
  if (attributes & CLAUSE_TYPE_SENTENCE)
362
211
    return attributes;
363
364
282
  return short_pause;
365
493
}
366
367
int AddNameData(const char *name, int wide)
368
0
{
369
  // Add the name to the namedata and return its position
370
  // (Used by the Windows SAPI wrapper)
371
372
0
  int ix;
373
0
  int len;
374
375
0
  if (wide) {
376
0
    len = (wcslen((const wchar_t *)name)+1)*sizeof(wchar_t);
377
0
    n_namedata = (n_namedata + sizeof(wchar_t) - 1) % sizeof(wchar_t);  // round to wchar_t boundary
378
0
  } else
379
0
    len = strlen(name)+1;
380
381
0
  if (namedata_ix+len >= n_namedata) {
382
    // allocate more space for marker names
383
0
    void *vp;
384
0
    if ((vp = realloc(namedata, namedata_ix+len + 1000)) == NULL)
385
0
      return -1;  // failed to allocate, original data is unchanged but ignore this new name
386
    // !!! Bug?? If the allocated data shifts position, then pointers given to user application will be invalid
387
388
0
    namedata = (char *)vp;
389
0
    n_namedata = namedata_ix+len + 1000;
390
0
  }
391
0
  memcpy(&namedata[ix = namedata_ix], name, len);
392
0
  namedata_ix += len;
393
0
  return ix;
394
0
}
395
396
void SetVoiceStack(espeak_VOICE *v, const char *variant_name)
397
2
{
398
2
  SSML_STACK *sp;
399
2
  sp = &ssml_stack[0];
400
401
2
  if (v == NULL) {
402
1
    memset(sp, 0, sizeof(ssml_stack[0]));
403
1
    return;
404
1
  }
405
1
  if (v->languages != NULL)
406
1
    strcpy(sp->language, v->languages);
407
1
  if (v->name != NULL)
408
1
    strncpy0(sp->voice_name, v->name, sizeof(sp->voice_name));
409
1
  sp->voice_variant_number = v->variant;
410
1
  sp->voice_age = v->age;
411
1
  sp->voice_gender = v->gender;
412
413
1
  if (variant_name[0] == '!' && variant_name[1] == 'v' && variant_name[2] == PATHSEP)
414
0
    variant_name += 3; // strip variant directory name, !v plus PATHSEP
415
1
  strncpy0(base_voice_variant_name, variant_name, sizeof(base_voice_variant_name));
416
1
  memcpy(&base_voice, espeak_GetCurrentVoice(), sizeof(base_voice));
417
1
}
418
419
static void RemoveChar(char *p)
420
61
{
421
  // Replace a UTF-8 character by spaces
422
61
  int c;
423
424
61
  memset(p, ' ', utf8_in(&c, p));
425
61
}
426
427
static int lookupwchar2(const unsigned short *list, int c)
428
2.78M
{
429
  // Replace character c by another character.
430
  // Returns 0 = not found, 1 = delete character
431
432
2.78M
  int ix;
433
434
8.34M
  for (ix = 0; list[ix] != 0; ix += 2) {
435
5.56M
    if (list[ix] == c)
436
738
      return list[ix+1];
437
5.56M
  }
438
2.77M
  return 0;
439
2.78M
}
440
441
2.78M
static bool IgnoreOrReplaceChar(Translator *tr, int *c1) {
442
2.78M
    int i;
443
2.78M
    if ((i = lookupwchar2(tr->chars_ignore, *c1)) != 0) {
444
738
        if (i == 1) {
445
            // ignore this character
446
738
            return true;
447
738
        }
448
0
        *c1 = i; // replace current character with the result
449
0
    }
450
2.77M
    return false;
451
2.78M
}
452
453
2.77M
static int CheckPhonemeMode(int option_phoneme_input, int phoneme_mode, int c1, int c2) {
454
2.77M
    if (option_phoneme_input) {
455
2.77M
      if (phoneme_mode > 0)
456
1.39k
        phoneme_mode--;
457
2.77M
      else if ((c1 == '[') && (c2 == '['))
458
771
        phoneme_mode = -1; // input is phoneme mnemonics, so don't look for punctuation
459
2.77M
      else if ((c1 == ']') && (c2 == ']'))
460
700
        phoneme_mode = 2; // set phoneme_mode to zero after the next two characters
461
462
2.77M
    }
463
2.77M
    return phoneme_mode;
464
2.77M
}
465
466
int ReadClause(Translator *tr, char *buf, short *charix, int *charix_top, int n_buf, int *tone_type, char *voice_change)
467
56.5k
{
468
  /* Find the end of the current clause.
469
      Write the clause into  buf
470
471
      returns: clause type (bits 0-7: pause x10mS, bits 8-11 intonation type)
472
473
      Also checks for blank line (paragraph) as end-of-clause indicator.
474
475
      Does not end clause for:
476
          punctuation immediately followed by alphanumeric  eg.  1.23  !Speak  :path
477
          repeated punctuation, eg.   ...   !!!
478
   */
479
480
56.5k
  int c1 = ' '; // current character
481
56.5k
  int c2; // next character
482
56.5k
  int cprev = ' '; // previous character
483
56.5k
  int c_next = 0;
484
56.5k
  int parag;
485
56.5k
  int ix = 0;
486
56.5k
  int j;
487
56.5k
  int nl_count;
488
56.5k
  int linelength = 0;
489
56.5k
  int phoneme_mode = 0;
490
56.5k
  int n_xml_buf;
491
56.5k
  int terminator;
492
56.5k
  bool any_alnum = false;
493
56.5k
  int punct_data = 0;
494
56.5k
  bool is_end_clause;
495
56.5k
  int announced_punctuation = 0;
496
56.5k
  bool stressed_word = false;
497
56.5k
  int end_clause_after_tag = 0;
498
56.5k
  int end_clause_index = 0;
499
56.5k
  wchar_t xml_buf[N_XML_BUF+1];
500
501
56.5k
  #define N_XML_BUF2 20
502
56.5k
  char xml_buf2[N_XML_BUF2+2]; // for &<name> and &<number> sequences
503
56.5k
  static char ungot_string[N_XML_BUF2+4];
504
56.5k
  static int ungot_string_ix = -1;
505
506
56.5k
  if (clear_skipping_text) {
507
0
    skipping_text = false;
508
0
    clear_skipping_text = false;
509
0
  }
510
511
56.5k
  tr->phonemes_repeat_count = 0;
512
56.5k
  tr->clause_upper_count = 0;
513
56.5k
  tr->clause_lower_count = 0;
514
56.5k
  *tone_type = 0;
515
56.5k
  *voice_change = 0;
516
517
56.5k
  if (ungot_char2 != 0) {
518
899
    c2 = ungot_char2;
519
55.6k
  } else if (Eof()) {
520
0
    c2 = 0;
521
55.6k
  } else {
522
55.6k
    c2 = GetC();
523
55.6k
  }
524
525
3.02M
  while (!Eof() || (ungot_char != 0) || (ungot_char2 != 0) || (ungot_string_ix >= 0)) {
526
3.02M
    if (!iswalnum(c1)) {
527
1.77M
      if ((end_character_position > 0) && (count_characters > end_character_position)) {
528
0
        return CLAUSE_EOF;
529
0
      }
530
531
1.77M
      if ((skip_characters > 0) && (count_characters >= skip_characters)) {
532
        // reached the specified start position
533
        // don't break a word
534
0
        clear_skipping_text = true;
535
0
        skip_characters = 0;
536
0
        UngetC(c2);
537
0
        return CLAUSE_NONE;
538
0
      }
539
1.77M
    }
540
3.02M
    int cprev2 = cprev;
541
3.02M
    cprev = c1;
542
3.02M
    c1 = c2;
543
544
3.02M
    if (ungot_string_ix >= 0) {
545
24.7k
      if (ungot_string[ungot_string_ix] == 0) {
546
5.24k
        MAKE_MEM_UNDEFINED(&ungot_string, sizeof(ungot_string));
547
5.24k
        ungot_string_ix = -1;
548
5.24k
      }
549
24.7k
    }
550
551
3.02M
    if ((ungot_string_ix == 0) && (ungot_char2 == 0))
552
5.59k
      c1 = ungot_string[ungot_string_ix++];
553
3.02M
    if (ungot_string_ix >= 0) {
554
19.5k
      c2 = ungot_string[ungot_string_ix++];
555
3.00M
    } else if (Eof()) {
556
1
      c2 = ' ';
557
3.00M
    } else {
558
3.00M
      c2 = GetC();
559
3.00M
    }
560
561
3.02M
    ungot_char2 = 0;
562
563
3.02M
    if ((option_ssml) && (phoneme_mode == 0)) {
564
2.97M
      if ((c1 == '&') && ((c2 == '#') || ((c2 >= 'a') && (c2 <= 'z')))) {
565
9.83k
        n_xml_buf = 0;
566
9.83k
        c1 = c2;
567
61.2k
        while (!Eof() && (iswalnum(c1) || (c1 == '#')) && (n_xml_buf < N_XML_BUF2)) {
568
51.3k
          xml_buf2[n_xml_buf++] = c1;
569
51.3k
          c1 = GetC();
570
51.3k
        }
571
9.83k
        xml_buf2[n_xml_buf] = 0;
572
9.83k
        if (Eof()) {
573
0
          c2 = '\0';
574
9.83k
        } else {
575
9.83k
          c2 = GetC();
576
9.83k
        }
577
9.83k
        sprintf(ungot_string, "%s%c%c", &xml_buf2[0], c1, c2);
578
579
9.83k
        int found = -1;
580
9.83k
        if (c1 == ';') {
581
5.27k
          found = ParseSsmlReference(xml_buf2, &c1, &c2);
582
5.27k
        }
583
584
9.83k
        if (found <= 0) {
585
5.59k
          ungot_string_ix = 0;
586
5.59k
          c1 = '&';
587
5.59k
          c2 = ' ';
588
5.59k
        }
589
590
9.83k
        if ((c1 <= 0x20) && ((sayas_mode == SAYAS_SINGLE_CHARS) || (sayas_mode == SAYAS_KEY)))
591
0
          c1 += 0xe000; // move into unicode private usage area
592
2.96M
      } else if (c1 == '<') {
593
203k
        if ((c2 == '/') || iswalpha(c2) || c2 == '!' || c2 == '?') {
594
          // check for space in the output buffer for embedded commands produced by the SSML tag
595
195k
          if (ix > (n_buf - 20)) {
596
            // Perhaps not enough room, end the clause before the SSML tag
597
199
            ungot_char2 = c1;
598
199
            TerminateBufWithSpaceAndZero(buf, ix, &c2);
599
199
            return CLAUSE_NONE;
600
199
          }
601
602
          // SSML Tag
603
195k
          n_xml_buf = 0;
604
195k
          c1 = c2;
605
1.05M
          while (!Eof() && (c1 != '>') && (n_xml_buf < N_XML_BUF)) {
606
856k
            xml_buf[n_xml_buf++] = c1;
607
856k
            c1 = GetC();
608
856k
          }
609
195k
          xml_buf[n_xml_buf] = 0;
610
195k
          c2 = ' ';
611
612
195k
          if (base_voice.identifier)
613
195k
            strcpy(current_voice_id, base_voice.identifier);
614
195k
          terminator = ProcessSsmlTag(xml_buf, buf, &ix, n_buf, xmlbase, &audio_text, current_voice_id, &base_voice, base_voice_variant_name, &ignore_text, &clear_skipping_text, &sayas_mode, &sayas_start, ssml_stack, &n_ssml_stack, &n_param_stack, (int *)speech_parameters);
615
616
195k
          if (terminator != 0) {
617
17.9k
            TerminateBufWithSpaceAndZero(buf, ix, NULL);
618
619
17.9k
            if (terminator & CLAUSE_TYPE_VOICE_CHANGE)
620
9.16k
              strcpy(voice_change, current_voice_id);
621
17.9k
            return terminator;
622
17.9k
          }
623
177k
          c1 = ' ';
624
177k
          if (!Eof()) {
625
177k
            c2 = GetC();
626
177k
          }
627
177k
          continue;
628
195k
        }
629
203k
      }
630
2.97M
    }
631
632
2.83M
    if (ignore_text)
633
46.7k
      continue;
634
635
2.78M
    if ((c2 == '\n') && (option_linelength == -1)) {
636
      // single-line mode, return immediately on NL
637
0
      if ((terminator = clause_type_from_codepoint(c1)) == CLAUSE_NONE) {
638
0
        charix[ix] = count_characters - clause_start_char;
639
0
        *charix_top = ix;
640
0
        ix += utf8_out(c1, &buf[ix]);
641
0
        terminator = CLAUSE_PERIOD; // line doesn't end in punctuation, assume period
642
0
      }
643
0
      TerminateBufWithSpaceAndZero(buf, ix, NULL);
644
0
      return terminator;
645
0
    }
646
647
2.78M
    if (c1 == CTRL_EMBEDDED) {
648
      // an embedded command. If it's a voice change, end the clause
649
29.8k
      if (c2 == 'V') {
650
1.97k
        buf[ix++] = 0; // end the clause at this point
651
14.4k
        while (!Eof() && !iswspace(c1 = GetC()) && (ix < (n_buf-1)))
652
12.4k
          buf[ix++] = c1; // add voice name to end of buffer, after the text
653
1.97k
        buf[ix++] = 0;
654
1.97k
        return CLAUSE_VOICE;
655
27.8k
      } else if (c2 == 'B') {
656
        // set the punctuation option from an embedded command
657
        //  B0     B1     B<punct list><space>
658
1.69k
        strcpy(&buf[ix], "   ");
659
1.69k
        ix += 3;
660
661
1.69k
        if (!Eof() && (c2 = GetC()) == '0')
662
212
          option_punctuation = 0;
663
1.48k
        else {
664
1.48k
          option_punctuation = 1;
665
1.48k
          option_punctlist[0] = 0;
666
1.48k
          if (c2 != '1') {
667
            // a list of punctuation characters to be spoken, terminated by space
668
22
            j = 0;
669
118
            while (!Eof() && !iswspace(c2) && (j < N_PUNCTLIST-1)) {
670
96
              option_punctlist[j++] = c2;
671
96
              c2 = GetC();
672
96
              buf[ix++] = ' ';
673
96
            }
674
22
            option_punctlist[j] = 0; // terminate punctuation list
675
22
            option_punctuation = 2;
676
22
          }
677
1.48k
        }
678
1.69k
        if (!Eof())
679
1.69k
          c2 = GetC();
680
1.69k
        continue;
681
1.69k
      }
682
29.8k
    }
683
684
2.78M
    linelength++;
685
686
2.78M
     if (IgnoreOrReplaceChar(tr, &c1) == true)
687
738
        continue;
688
689
690
2.77M
    if (iswalnum(c1))
691
1.23M
      any_alnum = true;
692
1.54M
    else {
693
1.54M
      if (stressed_word) {
694
1.73k
        stressed_word = false;
695
1.73k
        c1 = CHAR_EMPHASIS; // indicate this word is stressed
696
1.73k
        UngetC(c2);
697
1.73k
        c2 = ' ';
698
1.73k
      }
699
700
1.54M
      if (c1 == 0xf0b)
701
0
        c1 = ' '; // Tibet inter-syllabic mark, ?? replace by space ??
702
703
1.54M
      if (c1 == 0xd4d) {
704
        // Malayalam virama, check if next character is Zero-width-joiner
705
206
        if (c2 == 0x200d)
706
0
          c1 = 0xd4e; // use this unofficial code for chillu-virama
707
206
      }
708
709
1.54M
      if (c1 == 0xdca && c2 == 0x200d && !Eof()) {
710
        // Sinhala virama followed by Zero-width-joiner: the ZWJ only
711
        // selects a touching-conjunct glyph (e.g. ක්‍ර) and has no effect
712
        // on pronunciation, so drop it and let the cluster be read as one
713
        // conjunct. (Emoji ZWJ sequences are unaffected: there the joiner
714
        // follows an emoji, not the virama.)
715
1
        c2 = GetC();
716
1
      }
717
1.54M
    }
718
719
2.77M
    if (iswupper(c1)) {
720
285k
      tr->clause_upper_count++;
721
722
285k
      if ((option_capitals == 2) && (sayas_mode == 0) && !iswupper(cprev)) {
723
0
        char text_buf[30];
724
0
        if (LookupSpecial(tr, "_cap", text_buf) != NULL) {
725
0
          j = strlen(text_buf);
726
0
          if ((ix + j) < n_buf) {
727
0
            strcpy(&buf[ix], text_buf);
728
0
            ix += j;
729
0
          }
730
0
        }
731
0
      }
732
2.49M
    } else if (iswalpha(c1))
733
594k
      tr->clause_lower_count++;
734
735
2.77M
    phoneme_mode = CheckPhonemeMode(option_phoneme_input, phoneme_mode, c1, c2);
736
737
2.77M
    if (c1 == '\n') {
738
28.8k
      parag = 0;
739
740
      // count consecutive newlines, ignoring other spaces
741
3.74M
      while (!Eof() && iswspace(c2)) {
742
3.71M
        if (c2 == '\n')
743
3.59M
          parag++;
744
3.71M
        c2 = GetC();
745
3.71M
      }
746
28.8k
      if (parag > 0) {
747
        // 2nd newline, assume paragraph
748
16.9k
        if (end_clause_after_tag)
749
57
          RemoveChar(&buf[end_clause_index]); // delete clause-end punctiation
750
16.9k
        TerminateBufWithSpaceAndZero(buf, ix, &c2);
751
16.9k
        if (parag > 3)
752
744
          parag = 3;
753
16.9k
        if (option_ssml) parag = 1;
754
16.9k
        return (CLAUSE_PARAGRAPH-30) + 30*parag; // several blank lines, longer pause
755
16.9k
      }
756
757
11.8k
      if (linelength <= option_linelength) {
758
        // treat lines shorter than a specified length as end-of-clause
759
0
        TerminateBufWithSpaceAndZero(buf, ix, &c2);
760
0
        return CLAUSE_COLON;
761
0
      }
762
763
11.8k
      linelength = 0;
764
11.8k
    }
765
766
2.76M
    announced_punctuation = 0;
767
768
2.76M
    if ((phoneme_mode == 0) && (sayas_mode == 0)) {
769
2.71M
      is_end_clause = false;
770
771
2.71M
      if (end_clause_after_tag) {
772
        // Because of an xml tag, we are waiting for the
773
        // next non-blank character to decide whether to end the clause
774
        // i.e. is dot followed by an upper-case letter?
775
776
778
        if (!iswspace(c1)) {
777
366
          if (!IsAlpha(c1) || !iswlower(c1)) {
778
180
            ungot_char2 = c1;
779
180
            TerminateBufWithSpaceAndZero(buf, end_clause_index, &c2);
780
180
            return end_clause_after_tag;
781
180
          }
782
186
          end_clause_after_tag = 0;
783
186
        }
784
778
      }
785
786
2.71M
      if ((c1 == '.') && (c2 == '.')) {
787
66.4k
        while (!Eof() && (c_next = GetC()) == '.') {
788
          // 3 or more dots, replace by elipsis
789
64.8k
          c1 = 0x2026;
790
64.8k
          c2 = ' ';
791
64.8k
        }
792
1.59k
        if (c1 == 0x2026)
793
444
          c2 = c_next;
794
1.15k
        else
795
1.15k
          UngetC(c_next);
796
1.59k
      }
797
798
2.71M
      punct_data = 0;
799
2.71M
      if ((punct_data = clause_type_from_codepoint(c1)) != CLAUSE_NONE) {
800
801
        // Handling of sequences of ? and ! like ??!?, !!??!, ?!! etc
802
        // Use only first char as determinant
803
82.6k
        if(punct_data & (CLAUSE_QUESTION | CLAUSE_EXCLAMATION)) {
804
3.13M
          while(!Eof() && clause_type_from_codepoint(c2) & (CLAUSE_QUESTION | CLAUSE_EXCLAMATION)) {
805
3.05M
            c_next = GetC();
806
3.05M
            c2 = c_next;
807
3.05M
          }
808
82.6k
        }
809
810
82.6k
        if (punct_data & CLAUSE_PUNCTUATION_IN_WORD) {
811
          // Armenian punctuation inside a word
812
1.74k
          stressed_word = true;
813
1.74k
          *tone_type = punct_data >> 12 & 0xf; // override the end-of-sentence type
814
1.74k
          continue;
815
1.74k
        }
816
817
80.9k
        if (iswspace(c2) || (punct_data & CLAUSE_OPTIONAL_SPACE_AFTER) || IsBracket(c2) || (c2 == '?') || Eof() || c2 == CTRL_EMBEDDED) { // don't check for '-' because it prevents recognizing ':-)'
818
          // note: (c2='?') is for when a smart-quote has been replaced by '?'
819
20.1k
          is_end_clause = true;
820
20.1k
        }
821
80.9k
      }
822
823
      // don't announce punctuation for the alternative text inside inside <audio> ... </audio>
824
2.70M
      if (c1 == 0xe000+'<')  c1 = '<';
825
2.70M
      if (option_punctuation && iswpunct(c1) && (audio_text == false)) {
826
        // option is set to explicitly speak punctuation characters
827
        // if a list of allowed punctuation has been set up, check whether the character is in it
828
24.0k
        if ((option_punctuation == 1) || (wcschr(option_punctlist, c1) != NULL)) {
829
23.1k
          tr->phonemes_repeat_count = 0;
830
23.1k
          if ((terminator = AnnouncePunctuation(tr, c1, &c2, buf, &ix, is_end_clause)) >= 0)
831
1.01k
            return terminator;
832
22.1k
          announced_punctuation = c1;
833
22.1k
        }
834
24.0k
      }
835
836
2.70M
      if ((punct_data & CLAUSE_SPEAK_PUNCTUATION_NAME) && (announced_punctuation == 0)) {
837
        // used for elipsis (and 3 dots) if a pronunciation for elipsis is given in *_list
838
443
        char *p2;
839
840
443
        p2 = &buf[ix];
841
443
        char cn_buf[74];
842
443
        sprintf(p2, "%s", LookupCharName(cn_buf, tr, c1, true));
843
443
        if (p2[0] != 0) {
844
41
          ix += strlen(p2);
845
41
          announced_punctuation = c1;
846
41
          punct_data = punct_data & ~CLAUSE_INTONATION_TYPE; // change intonation type to 0 (full-stop)
847
41
        }
848
443
      }
849
850
2.70M
      if (is_end_clause) {
851
19.1k
        nl_count = 0;
852
19.1k
        c_next = c2;
853
854
19.1k
        if (iswspace(c_next)) {
855
2.18M
          while (!Eof() && iswspace(c_next)) {
856
2.17M
            if (c_next == '\n')
857
1.02M
              nl_count++;
858
2.17M
            c_next = GetC(); // skip past space(s)
859
2.17M
          }
860
11.5k
        }
861
862
19.1k
        if ((c1 == '.') && (nl_count < 2))
863
8.67k
          punct_data |= CLAUSE_DOT_AFTER_LAST_WORD;
864
865
19.1k
        if (nl_count == 0) {
866
17.0k
          if ((c1 == ',') && (cprev == '.') && (tr->translator_name == L('h', 'u')) && iswdigit(cprev2) && (iswdigit(c_next) || (iswlower(c_next)))) {
867
            // lang=hu, fix for ordinal numbers, eg:  "december 2., szerda", ignore ',' after ordinal number
868
0
            c1 = CHAR_COMMA_BREAK;
869
0
            is_end_clause = false;
870
0
          }
871
872
17.0k
          if (c1 == '.' && c_next == '\'' && text_decoder_peekc(p_decoder) == 's') {
873
            // A special case to handle english acronym + genitive, eg. u.s.a.'s
874
            // But avoid breaking clause handling if anything else follows the apostrophe.
875
418
            is_end_clause = false;
876
418
          }
877
878
17.0k
          if (c1 == '.') {
879
8.49k
            if ((tr->langopts.numbers & NUM_ORDINAL_DOT) &&
880
4.71k
                (iswdigit(cprev) || (IsRomanU(cprev) && (IsRomanU(cprev2) || iswspace(cprev2))))) { // lang=hu
881
              // dot after a number indicates an ordinal number
882
2.54k
              if (!iswdigit(cprev))
883
2.21k
                is_end_clause = false; // Roman number followed by dot
884
336
              else if (iswlower(c_next) || (c_next == '-')) // hyphen is needed for lang-hu (eg. 2.-kal)
885
313
                is_end_clause = false; // only if followed by lower-case, (or if there is a XML tag)
886
2.54k
            } 
887
8.49k
            if (iswlower(c_next) && tr->langopts.lowercase_sentence == false) {
888
              // next word has no capital letter, this dot is probably from an abbreviation
889
895
              is_end_clause = false;
890
895
            }
891
8.49k
            if (any_alnum == false) {
892
              // no letters or digits yet, so probably not a sentence terminator
893
              // Here, dot is followed by space or bracket
894
868
              c1 = ' ';
895
868
              is_end_clause = false;
896
868
            }
897
8.56k
          } else {
898
8.56k
            if (any_alnum == false) {
899
              // no letters or digits yet, so probably not a sentence terminator
900
1.33k
              is_end_clause = false;
901
1.33k
            }
902
8.56k
          }
903
904
17.0k
          if (is_end_clause && (c1 == '.') && (c_next == '<') && option_ssml) {
905
            // wait until after the end of the xml tag, then look for upper-case letter
906
706
            is_end_clause = false;
907
706
            end_clause_index = ix;
908
706
            end_clause_after_tag = punct_data;
909
706
          }
910
17.0k
        }
911
912
19.1k
        if (is_end_clause) {
913
12.8k
          TerminateBufWithSpaceAndZero(buf, ix, &c_next);
914
915
12.8k
          if (iswdigit(cprev) && !IsAlpha(c_next)) // ????
916
1.28k
            punct_data &= ~CLAUSE_DOT_AFTER_LAST_WORD;
917
12.8k
          if (nl_count > 1) {
918
889
            if ((punct_data == CLAUSE_QUESTION) || (punct_data == CLAUSE_EXCLAMATION))
919
582
              return punct_data + 35; // with a longer pause
920
307
            return CLAUSE_PARAGRAPH;
921
889
          }
922
12.0k
          return punct_data; // only recognise punctuation if followed by a blank or bracket/quote
923
12.8k
        } else if (!Eof()) {
924
6.27k
          if (iswspace(c2))
925
4.09k
            UngetC(c_next);
926
6.27k
        }
927
19.1k
      }
928
2.70M
    }
929
930
2.74M
    if (speech_parameters[espeakSILENCE] == 1)
931
0
      continue;
932
933
2.74M
    if (c1 == announced_punctuation) {
934
      // This character has already been announced, so delete it so that it isn't spoken a second time.
935
      // Unless it's a hyphen or apostrophe (which is used by TranslateClause() )
936
22.2k
      if (IsBracket(c1))
937
5.32k
        c1 = 0xe000 + '('; // Unicode private useage area.  So TranslateRules() knows the bracket name has been spoken
938
16.9k
      else if (c1 != '-')
939
16.7k
        c1 = ' ';
940
22.2k
    }
941
942
2.74M
    j = ix+1;
943
944
2.74M
    if (c1 == 0xe000 + '<') c1 = '<';
945
946
2.74M
    ix += utf8_out(c1, &buf[ix]);
947
2.74M
    if (!iswspace(c1) && !IsBracket(c1)) {
948
2.51M
      charix[ix] = count_characters - clause_start_char;
949
4.07M
      while (j < ix)
950
1.55M
        charix[j++] = -1; // subsequent bytes of a multibyte character
951
2.51M
    }
952
2.74M
    *charix_top = ix;
953
954
2.74M
    if (((ix > (n_buf-75)) && !IsAlpha(c1) && !iswdigit(c1))  ||  (ix >= (n_buf-4))) {
955
      // clause too long, getting near end of buffer, so break here
956
      // try to break at a word boundary (unless we actually reach the end of buffer).
957
      // (n_buf-4) is to allow for 3 bytes of multibyte character plus terminator.
958
4.86k
      TerminateBufWithSpaceAndZero(buf, ix, &c2);
959
4.86k
      return CLAUSE_NONE;
960
4.86k
    }
961
2.74M
  }
962
963
505
  if (stressed_word)
964
3
    ix += utf8_out(CHAR_EMPHASIS, &buf[ix]);
965
505
  if (end_clause_after_tag)
966
4
    RemoveChar(&buf[end_clause_index]); // delete clause-end punctiation
967
505
  TerminateBufWithSpaceAndZero(buf, ix, NULL);
968
505
  return CLAUSE_EOF; // end of file
969
56.5k
}
970
971
void InitNamedata(void)
972
628
{
973
628
  namedata_ix = 0;
974
628
  if (namedata != NULL) {
975
0
    free(namedata);
976
0
    namedata = NULL;
977
0
    n_namedata = 0;
978
0
  }
979
628
}
980
981
void InitText2(void)
982
627
{
983
627
  int param;
984
985
627
  ungot_char = 0;
986
627
  ungot_char2 = 0;
987
988
627
  n_ssml_stack = 1;
989
627
  MAKE_MEM_UNDEFINED(&ssml_stack[1], sizeof(ssml_stack) - sizeof(ssml_stack[0]));
990
627
  n_param_stack = 1;
991
627
  MAKE_MEM_UNDEFINED(&param_stack[1], sizeof(param_stack) - sizeof(param_stack[0]));
992
627
  ssml_stack[0].tag_type = 0;
993
994
10.0k
  for (param = 0; param < N_SPEECH_PARAM; param++)
995
9.40k
    speech_parameters[param] = param_stack[0].parameter[param]; // set all speech parameters to defaults
996
997
627
  option_punctuation = speech_parameters[espeakPUNCTUATION];
998
627
  option_capitals = speech_parameters[espeakCAPITALS];
999
1000
627
  current_voice_id[0] = 0;
1001
1002
627
  ignore_text = false;
1003
627
  audio_text = false;
1004
627
  clear_skipping_text = false;
1005
627
  count_characters = -1;
1006
627
  sayas_mode = 0;
1007
1008
627
  xmlbase = NULL;
1009
627
}
1010
1011
54.0k
static void TerminateBufWithSpaceAndZero(char *buf, int index, int *ungetc) {
1012
54.0k
  buf[index] = ' ';
1013
54.0k
  buf[index+1] = 0;
1014
1015
54.0k
  if (ungetc != NULL) {
1016
35.6k
    UngetC(*ungetc);
1017
35.6k
  }
1018
54.0k
}
1019
1020
10.2k
static void DecodeWithPhonemeMode(char *buf, char *phonemes, Translator *tr, Translator *tr2, unsigned int flags[]) {
1021
10.2k
  char phonemes2[55];
1022
10.2k
  if (tr2 == NULL) {
1023
7.06k
    SetWordStress(tr, phonemes, flags, -1, 0);
1024
7.06k
    DecodePhonemes(phonemes, phonemes2);
1025
7.06k
    sprintf(buf, "[\002%s]]", phonemes2);
1026
7.06k
  } else {
1027
3.17k
    SetWordStress(tr2, phonemes, flags, -1, 0);
1028
3.17k
      DecodePhonemes(phonemes, phonemes2);
1029
3.17k
      char wbuf[5];
1030
3.17k
      sprintf(buf, "[\002_^_%s %s _^_%s]]", ESPEAKNG_DEFAULT_VOICE, phonemes2, WordToString2(wbuf, tr->translator_name));
1031
3.17k
    }
1032
10.2k
}