1 /*
2 * mod_ssml for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
3 * Copyright (C) 2013-2014,2016 Grasshopper
4 *
5 * Version: MPL 1.1
6 *
7 * The contents of this file are subject to the Mozilla Public License Version
8 * 1.1 (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
10 * http://www.mozilla.org/MPL/
11 *
12 * Software distributed under the License is distributed on an "AS IS" basis,
13 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
14 * for the specific language governing rights and limitations under the
15 * License.
16 *
17 * The Original Code is mod_ssml for FreeSWITCH Modular Media Switching Software Library / Soft-Switch Application
18 *
19 * The Initial Developer of the Original Code is Grasshopper
20 * Portions created by the Initial Developer are Copyright (C)
21 * the Initial Developer. All Rights Reserved.
22 *
23 * Contributor(s):
24 * Chris Rienzo <chris.rienzo@grasshopper.com>
25 *
26 * mod_ssml.c -- SSML audio rendering format
27 *
28 */
29 #include <switch.h>
30 #include <iksemel.h>
31
32 SWITCH_MODULE_LOAD_FUNCTION(mod_ssml_load);
33 SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_ssml_shutdown);
34 SWITCH_MODULE_DEFINITION(mod_ssml, mod_ssml_load, mod_ssml_shutdown, NULL);
35
36 #define MAX_VOICE_FILES 256
37 #define MAX_VOICE_PRIORITY 999
38 #define VOICE_NAME_PRIORITY 1000
39 #define VOICE_GENDER_PRIORITY 1000
40 #define VOICE_LANG_PRIORITY 1000000
41
42 struct ssml_parser;
43
44 /** function to handle tag attributes */
45 typedef int (* tag_attribs_fn)(struct ssml_parser *, char **);
46 /** function to handle tag CDATA */
47 typedef int (* tag_cdata_fn)(struct ssml_parser *, char *, size_t);
48
49 /**
50 * Tag definition
51 */
52 struct tag_def {
53 tag_attribs_fn attribs_fn;
54 tag_cdata_fn cdata_fn;
55 switch_bool_t is_root;
56 switch_hash_t *children_tags;
57 };
58
59 /**
60 * Module configuration
61 */
62 static struct {
63 /** Mapping of mod-name-language-gender to voice */
64 switch_hash_t *voice_cache;
65 /** Mapping of voice names */
66 switch_hash_t *say_voice_map;
67 /** Synchronizes access to say_voice_map */
68 switch_mutex_t *say_voice_map_mutex;
69 /** Mapping of voice names */
70 switch_hash_t *tts_voice_map;
71 /** Synchronizes access to tts_voice_map */
72 switch_mutex_t *tts_voice_map_mutex;
73 /** Mapping of interpret-as value to macro */
74 switch_hash_t *interpret_as_map;
75 /** Mapping of ISO language code to say-module */
76 switch_hash_t *language_map;
77 /** Mapping of tag name to definition */
78 switch_hash_t *tag_defs;
79 /** module memory pool */
80 switch_memory_pool_t *pool;
81 } globals;
82
83 /**
84 * A say language
85 */
86 struct language {
87 /** The ISO language code */
88 char *iso;
89 /** The FreeSWITCH language code */
90 char *language;
91 /** The say module name */
92 char *say_module;
93 };
94
95 /**
96 * A say macro
97 */
98 struct macro {
99 /** interpret-as name (cardinal...) */
100 char *name;
101 /** language (en-US, en-UK, ...) */
102 char *language;
103 /** type (number, items, persons, messages...) */
104 char *type;
105 /** method (pronounced, counted, iterated...) */
106 char *method;
107 };
108
109 /**
110 * A TTS voice
111 */
112 struct voice {
113 /** higher priority = more likely to pick */
114 int priority;
115 /** voice gender */
116 char *gender;
117 /** voice name / macro */
118 char *name;
119 /** voice language */
120 char *language;
121 /** internal file prefix */
122 char *prefix;
123 };
124
125 #define TAG_LEN 32
126 #define NAME_LEN 128
127 #define LANGUAGE_LEN 6
128 #define GENDER_LEN 8
129
130 /**
131 * SSML voice state
132 */
133 struct ssml_node {
134 /** tag name */
135 char tag_name[TAG_LEN];
136 /** requested name */
137 char name[NAME_LEN];
138 /** requested language */
139 char language[LANGUAGE_LEN];
140 /** requested gender */
141 char gender[GENDER_LEN];
142 /** voice to use */
143 struct voice *tts_voice;
144 /** say macro to use */
145 struct macro *say_macro;
146 /** tag handling data */
147 struct tag_def *tag_def;
148 /** previous node */
149 struct ssml_node *parent_node;
150 };
151
152 /**
153 * A file to play
154 */
155 struct ssml_file {
156 /** prefix to add to file handle */
157 char *prefix;
158 /** the file to play */
159 const char *name;
160 };
161
162 /**
163 * SSML parser state
164 */
165 struct ssml_parser {
166 /** current attribs */
167 struct ssml_node *cur_node;
168 /** files to play */
169 struct ssml_file *files;
170 /** number of files */
171 int num_files;
172 /** max files to play */
173 int max_files;
174 /** memory pool to use */
175 switch_memory_pool_t *pool;
176 /** desired sample rate */
177 int sample_rate;
178 };
179
180 /**
181 * SSML playback state
182 */
183 struct ssml_context {
184 /** handle to current file */
185 switch_file_handle_t fh;
186 /** files to play */
187 struct ssml_file *files;
188 /** number of files */
189 int num_files;
190 /** current file being played */
191 int index;
192 };
193
194 /**
195 * Add a definition for a tag
196 * @param tag the name
197 * @param attribs_fn the function to handle the tag attributes
198 * @param cdata_fn the function to handler the tag CDATA
199 * @param children_tags comma-separated list of valid child tag names
200 * @return the definition
201 */
add_tag_def(const char * tag,tag_attribs_fn attribs_fn,tag_cdata_fn cdata_fn,const char * children_tags)202 static struct tag_def *add_tag_def(const char *tag, tag_attribs_fn attribs_fn, tag_cdata_fn cdata_fn, const char *children_tags)
203 {
204 struct tag_def *def = switch_core_alloc(globals.pool, sizeof(*def));
205 switch_core_hash_init(&def->children_tags);
206 if (!zstr(children_tags)) {
207 char *children_tags_dup = switch_core_strdup(globals.pool, children_tags);
208 char *tags[32] = { 0 };
209 int tag_count = switch_separate_string(children_tags_dup, ',', tags, sizeof(tags) / sizeof(tags[0]));
210 if (tag_count) {
211 int i;
212 for (i = 0; i < tag_count; i++) {
213 switch_core_hash_insert(def->children_tags, tags[i], tags[i]);
214 }
215 }
216 }
217 def->attribs_fn = attribs_fn;
218 def->cdata_fn = cdata_fn;
219 def->is_root = SWITCH_FALSE;
220 switch_core_hash_insert(globals.tag_defs, tag, def);
221 return def;
222 }
223
224 /**
225 * Add a definition for a root tag
226 * @param tag the name
227 * @param attribs_fn the function to handle the tag attributes
228 * @param cdata_fn the function to handler the tag CDATA
229 * @param children_tags comma-separated list of valid child tag names
230 * @return the definition
231 */
add_root_tag_def(const char * tag,tag_attribs_fn attribs_fn,tag_cdata_fn cdata_fn,const char * children_tags)232 static struct tag_def *add_root_tag_def(const char *tag, tag_attribs_fn attribs_fn, tag_cdata_fn cdata_fn, const char *children_tags)
233 {
234 struct tag_def *def = add_tag_def(tag, attribs_fn, cdata_fn, children_tags);
235 def->is_root = SWITCH_TRUE;
236 return def;
237 }
238
239 /**
240 * Handle tag attributes
241 * @param parser the parser
242 * @param name the tag name
243 * @param atts the attributes
244 * @return IKS_OK if OK IKS_BADXML on parse failure
245 */
process_tag(struct ssml_parser * parser,const char * name,char ** atts)246 static int process_tag(struct ssml_parser *parser, const char *name, char **atts)
247 {
248 struct tag_def *def = switch_core_hash_find(globals.tag_defs, name);
249 if (def) {
250 parser->cur_node->tag_def = def;
251 if (def->is_root && parser->cur_node->parent_node == NULL) {
252 /* no parent for ROOT tags */
253 return def->attribs_fn(parser, atts);
254 } else if (!def->is_root && parser->cur_node->parent_node) {
255 /* check if this child is allowed by parent node */
256 struct tag_def *parent_def = parser->cur_node->parent_node->tag_def;
257 if (switch_core_hash_find(parent_def->children_tags, "ANY") ||
258 switch_core_hash_find(parent_def->children_tags, name)) {
259 return def->attribs_fn(parser, atts);
260 }
261 }
262 }
263 return IKS_BADXML;
264 }
265
266 /**
267 * Handle CDATA that is ignored
268 * @param parser the parser
269 * @param data the CDATA
270 * @param len the CDATA length
271 * @return IKS_OK
272 */
process_cdata_ignore(struct ssml_parser * parser,char * data,size_t len)273 static int process_cdata_ignore(struct ssml_parser *parser, char *data, size_t len)
274 {
275 return IKS_OK;
276 }
277
278 /**
279 * Handle CDATA that is not allowed
280 * @param parser the parser
281 * @param data the CDATA
282 * @param len the CDATA length
283 * @return IKS_BADXML
284 */
process_cdata_bad(struct ssml_parser * parser,char * data,size_t len)285 static int process_cdata_bad(struct ssml_parser *parser, char *data, size_t len)
286 {
287 int i;
288 for (i = 0; i < len; i++) {
289 if (isgraph(data[i])) {
290 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Unexpected CDATA for <%s>\n", parser->cur_node->tag_name);
291 return IKS_BADXML;
292 }
293 }
294 return IKS_OK;
295 }
296
297 /**
298 * Score the voice on how close it is to desired language, name, and gender
299 * @param voice the voice to score
300 * @param cur_node the desired voice attributes
301 * @param lang_required if true, language must match
302 * @return the score
303 */
score_voice(struct voice * voice,struct ssml_node * cur_node,int lang_required)304 static int score_voice(struct voice *voice, struct ssml_node *cur_node, int lang_required)
305 {
306 /* language > gender,name > priority */
307 int score = voice->priority;
308 if (!zstr_buf(cur_node->gender) && !strcmp(cur_node->gender, voice->gender)) {
309 score += VOICE_GENDER_PRIORITY;
310 }
311 if (!zstr_buf(cur_node->name) && !strcmp(cur_node->name, voice->name)) {
312 score += VOICE_NAME_PRIORITY;
313 }
314 if (!zstr_buf(cur_node->language) && !strcmp(cur_node->language, voice->language)) {
315 score += VOICE_LANG_PRIORITY;
316 } else if (lang_required) {
317 score = 0;
318 }
319 return score;
320 }
321
322 /**
323 * Search for best voice based on attributes
324 * @param cur_node the desired voice attributes
325 * @param map the map to search
326 * @param type "say" or "tts"
327 * @param lang_required if true, language must match
328 * @return the voice or NULL
329 */
find_voice(struct ssml_node * cur_node,switch_hash_t * map,char * type,int lang_required)330 static struct voice *find_voice(struct ssml_node *cur_node, switch_hash_t *map, char *type, int lang_required)
331 {
332 switch_hash_index_t *hi = NULL;
333 struct voice *voice = NULL;
334 char *lang_name_gender = NULL;
335 int best_score = 0;
336
337 /* check cache */
338 lang_name_gender = switch_mprintf("%s-%s-%s-%s", type, cur_node->language, cur_node->name, cur_node->gender);
339 voice = (struct voice *)switch_core_hash_find(globals.voice_cache, lang_name_gender);
340 if (voice) {
341 /* that was easy! */
342 goto done;
343 }
344
345 /* find best language, name, gender match */
346 for (hi = switch_core_hash_first(map); hi; hi = switch_core_hash_next(&hi)) {
347 const void *key;
348 void *val;
349 struct voice *candidate;
350 int candidate_score = 0;
351 switch_core_hash_this(hi, &key, NULL, &val);
352 candidate = (struct voice *)val;
353 candidate_score = score_voice(candidate, cur_node, lang_required);
354 if (candidate_score > 0 && candidate_score > best_score) {
355 voice = candidate;
356 best_score = candidate_score;
357 }
358 }
359
360 /* remember for next time */
361 if (voice) {
362 switch_core_hash_insert(globals.voice_cache, lang_name_gender, voice);
363 }
364
365 done:
366 switch_safe_free(lang_name_gender);
367
368 return voice;
369 }
370
371 /**
372 * Search for best voice based on attributes
373 * @param cur_node the desired voice attributes
374 * @return the voice or NULL
375 */
find_tts_voice(struct ssml_node * cur_node)376 static struct voice *find_tts_voice(struct ssml_node *cur_node)
377 {
378 struct voice *v;
379 switch_mutex_lock(globals.tts_voice_map_mutex);
380 v = find_voice(cur_node, globals.tts_voice_map, "tts", 0);
381 switch_mutex_unlock(globals.tts_voice_map_mutex);
382 return v;
383 }
384
385 /**
386 * Search for best voice based on attributes
387 * @param cur_node the desired voice attributes
388 * @return the voice or NULL
389 */
find_say_voice(struct ssml_node * cur_node)390 static struct voice *find_say_voice(struct ssml_node *cur_node)
391 {
392 struct voice *v;
393 switch_mutex_lock(globals.say_voice_map_mutex);
394 v = find_voice(cur_node, globals.say_voice_map, "say", 1);
395 switch_mutex_unlock(globals.say_voice_map_mutex);
396 return v;
397 }
398
399 /**
400 * Handle tag attributes that are ignored
401 * @param parser the parser
402 * @param atts the attributes
403 * @return IKS_OK
404 */
process_attribs_ignore(struct ssml_parser * parsed_data,char ** atts)405 static int process_attribs_ignore(struct ssml_parser *parsed_data, char **atts)
406 {
407 struct ssml_node *cur_node = parsed_data->cur_node;
408 cur_node->tts_voice = find_tts_voice(cur_node);
409 return IKS_OK;
410 }
411
412 /**
413 * open next file for reading
414 * @param handle the file handle
415 */
next_file(switch_file_handle_t * handle)416 static switch_status_t next_file(switch_file_handle_t *handle)
417 {
418 struct ssml_context *context = handle->private_info;
419 const char *file;
420
421 top:
422
423 context->index++;
424
425 if (switch_test_flag((&context->fh), SWITCH_FILE_OPEN)) {
426 switch_core_file_close(&context->fh);
427 }
428
429 if (context->index >= context->num_files) {
430 return SWITCH_STATUS_FALSE;
431 }
432
433
434 file = context->files[context->index].name;
435 context->fh.prefix = context->files[context->index].prefix;
436
437 if (switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE)) {
438 /* unsupported */
439 return SWITCH_STATUS_FALSE;
440 }
441
442 if (switch_core_file_open(&context->fh, file, handle->channels, handle->samplerate, handle->flags, NULL) != SWITCH_STATUS_SUCCESS) {
443 goto top;
444 }
445
446 handle->samples = context->fh.samples;
447 handle->format = context->fh.format;
448 handle->sections = context->fh.sections;
449 handle->seekable = context->fh.seekable;
450 handle->speed = context->fh.speed;
451 handle->interval = context->fh.interval;
452
453 if (switch_test_flag((&context->fh), SWITCH_FILE_NATIVE)) {
454 switch_set_flag_locked(handle, SWITCH_FILE_NATIVE);
455 } else {
456 switch_clear_flag_locked(handle, SWITCH_FILE_NATIVE);
457 }
458
459 return SWITCH_STATUS_SUCCESS;
460 }
461
462 /**
463 * Process xml:lang attribute
464 */
process_xml_lang(struct ssml_parser * parsed_data,char ** atts)465 static int process_xml_lang(struct ssml_parser *parsed_data, char **atts)
466 {
467 struct ssml_node *cur_node = parsed_data->cur_node;
468
469 /* only allow language change in <speak>, <p>, and <s> */
470 if (atts) {
471 int i = 0;
472 while (atts[i]) {
473 if (!strcmp("xml:lang", atts[i])) {
474 if (!zstr(atts[i + 1])) {
475 strncpy(cur_node->language, atts[i + 1], LANGUAGE_LEN);
476 cur_node->language[LANGUAGE_LEN - 1] = '\0';
477 }
478 }
479 i += 2;
480 }
481 }
482 cur_node->tts_voice = find_tts_voice(cur_node);
483 return IKS_OK;
484 }
485
486 /**
487 * Process <voice>
488 */
process_voice(struct ssml_parser * parsed_data,char ** atts)489 static int process_voice(struct ssml_parser *parsed_data, char **atts)
490 {
491 struct ssml_node *cur_node = parsed_data->cur_node;
492 if (atts) {
493 int i = 0;
494 while (atts[i]) {
495 if (!strcmp("xml:lang", atts[i])) {
496 if (!zstr(atts[i + 1])) {
497 strncpy(cur_node->language, atts[i + 1], LANGUAGE_LEN);
498 cur_node->language[LANGUAGE_LEN - 1] = '\0';
499 }
500 } else if (!strcmp("name", atts[i])) {
501 if (!zstr(atts[i + 1])) {
502 strncpy(cur_node->name, atts[i + 1], NAME_LEN);
503 cur_node->name[NAME_LEN - 1] = '\0';
504 }
505 } else if (!strcmp("gender", atts[i])) {
506 if (!zstr(atts[i + 1])) {
507 strncpy(cur_node->gender, atts[i + 1], GENDER_LEN);
508 cur_node->gender[GENDER_LEN - 1] = '\0';
509 }
510 }
511 i += 2;
512 }
513 }
514 cur_node->tts_voice = find_tts_voice(cur_node);
515 return IKS_OK;
516 }
517
518 /**
519 * Process <say-as>
520 */
process_say_as(struct ssml_parser * parsed_data,char ** atts)521 static int process_say_as(struct ssml_parser *parsed_data, char **atts)
522 {
523 struct ssml_node *cur_node = parsed_data->cur_node;
524 if (atts) {
525 int i = 0;
526 while (atts[i]) {
527 if (!strcmp("interpret-as", atts[i])) {
528 char *interpret_as = atts[i + 1];
529 if (!zstr(interpret_as)) {
530 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "interpret-as: %s\n", atts[i + 1]);
531 cur_node->say_macro = (struct macro *)switch_core_hash_find(globals.interpret_as_map, interpret_as);
532 }
533 break;
534 }
535 i += 2;
536 }
537 }
538 cur_node->tts_voice = find_tts_voice(cur_node);
539 return IKS_OK;
540 }
541
542 /**
543 * Process <break>- this is a period of silence
544 */
process_break(struct ssml_parser * parsed_data,char ** atts)545 static int process_break(struct ssml_parser *parsed_data, char **atts)
546 {
547 if (atts) {
548 int i = 0;
549 while (atts[i]) {
550 if (!strcmp("time", atts[i])) {
551 char *t = atts[i + 1];
552 if (!zstr(t) && parsed_data->num_files < parsed_data->max_files) {
553 int timeout_ms = 0;
554 char *unit;
555 if ((unit = strstr(t, "ms"))) {
556 *unit = '\0';
557 if (switch_is_number(t)) {
558 timeout_ms = atoi(t);
559 }
560 } else if ((unit = strstr(t, "s"))) {
561 *unit = '\0';
562 if (switch_is_number(t)) {
563 timeout_ms = atoi(t) * 1000;
564 }
565 }
566 if (timeout_ms > 0) {
567 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Adding <break>: \"%s\"\n", t);
568 parsed_data->files[parsed_data->num_files].name = switch_core_sprintf(parsed_data->pool, "silence_stream://%i", timeout_ms);
569 parsed_data->files[parsed_data->num_files++].prefix = NULL;
570 }
571 }
572 return IKS_OK;
573 }
574 i += 2;
575 }
576 }
577 return IKS_OK;
578 }
579
580 /**
581 * Process <audio>- this is a URL to play
582 */
process_audio(struct ssml_parser * parsed_data,char ** atts)583 static int process_audio(struct ssml_parser *parsed_data, char **atts)
584 {
585 if (atts) {
586 int i = 0;
587 while (atts[i]) {
588 if (!strcmp("src", atts[i])) {
589 char *src = atts[i + 1];
590 ikstack *stack = NULL;
591 if (!zstr(src) && parsed_data->num_files < parsed_data->max_files) {
592 /* get the URI */
593 if (strchr(src, '&')) {
594 stack = iks_stack_new(256, 0);
595 src = iks_unescape(stack, src, strlen(src));
596 }
597 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Adding <audio>: \"%s\"\n", src);
598 parsed_data->files[parsed_data->num_files].name = switch_core_strdup(parsed_data->pool, src);
599 parsed_data->files[parsed_data->num_files++].prefix = NULL;
600 if (stack) {
601 iks_stack_delete(&stack);
602 }
603 }
604 return IKS_OK;
605 }
606 i += 2;
607 }
608 }
609 return IKS_OK;
610 }
611
612 /**
613 * Process a tag
614 */
tag_hook(void * user_data,char * name,char ** atts,int type)615 static int tag_hook(void *user_data, char *name, char **atts, int type)
616 {
617 int result = IKS_OK;
618 struct ssml_parser *parsed_data = (struct ssml_parser *)user_data;
619 struct ssml_node *parent_node = parsed_data->cur_node;
620
621 if (type == IKS_OPEN || type == IKS_SINGLE) {
622 struct ssml_node *new_node = malloc(sizeof *new_node);
623 switch_assert(new_node);
624 if (parent_node) {
625 /* inherit parent attribs */
626 *new_node = *parent_node;
627 new_node->parent_node = parent_node;
628 } else {
629 new_node->name[0] = '\0';
630 new_node->language[0] = '\0';
631 new_node->gender[0] = '\0';
632 new_node->parent_node = NULL;
633 }
634 new_node->tts_voice = NULL;
635 new_node->say_macro = NULL;
636 strncpy(new_node->tag_name, name, TAG_LEN);
637 new_node->tag_name[TAG_LEN - 1] = '\0';
638 parsed_data->cur_node = new_node;
639 result = process_tag(parsed_data, name, atts);
640 }
641
642 if (type == IKS_CLOSE || type == IKS_SINGLE) {
643 if (parsed_data->cur_node) {
644 struct ssml_node *parent_node = parsed_data->cur_node->parent_node;
645 free(parsed_data->cur_node);
646 parsed_data->cur_node = parent_node;
647 }
648 }
649
650 return result;
651 }
652
653 /**
654 * Try to get file(s) from say module
655 * @param parsed_data
656 * @param to_say
657 * @return 1 if successful
658 */
get_file_from_macro(struct ssml_parser * parsed_data,char * to_say)659 static int get_file_from_macro(struct ssml_parser *parsed_data, char *to_say)
660 {
661 struct ssml_node *cur_node = parsed_data->cur_node;
662 struct macro *say_macro = cur_node->say_macro;
663 struct voice *say_voice = find_say_voice(cur_node);
664 struct language *language;
665 char *file_string = NULL;
666 char *gender = NULL;
667 switch_say_interface_t *si;
668
669 /* voice is required */
670 if (!say_voice) {
671 return 0;
672 }
673
674 language = switch_core_hash_find(globals.language_map, say_voice->language);
675 /* language is required */
676 if (!language) {
677 return 0;
678 }
679
680 /* TODO need to_say gender, not voice gender */
681 gender = "neuter";
682
683 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Trying macro: %s, %s, %s, %s, %s\n", language->language, to_say, say_macro->type, say_macro->method, gender);
684
685 if ((si = switch_loadable_module_get_say_interface(language->say_module)) && si->say_string_function) {
686 switch_say_args_t say_args = {0};
687 say_args.type = switch_ivr_get_say_type_by_name(say_macro->type);
688 say_args.method = switch_ivr_get_say_method_by_name(say_macro->method);
689 say_args.gender = switch_ivr_get_say_gender_by_name(gender);
690 say_args.ext = "wav";
691 si->say_string_function(NULL, to_say, &say_args, &file_string);
692 }
693 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Adding macro: \"%s\", prefix=\"%s\"\n", file_string, say_voice->prefix);
694 if (!zstr(file_string)) {
695 parsed_data->files[parsed_data->num_files].name = switch_core_strdup(parsed_data->pool, file_string);
696 parsed_data->files[parsed_data->num_files++].prefix = switch_core_strdup(parsed_data->pool, say_voice->prefix);
697 return 1;
698 }
699 switch_safe_free(file_string);
700
701 return 0;
702 }
703
704 /**
705 * Get TTS file for voice
706 */
get_file_from_voice(struct ssml_parser * parsed_data,char * to_say)707 static int get_file_from_voice(struct ssml_parser *parsed_data, char *to_say)
708 {
709 struct ssml_node *cur_node = parsed_data->cur_node;
710 if (cur_node->tts_voice) {
711 char *file = switch_core_sprintf(parsed_data->pool, "%s%s", cur_node->tts_voice->prefix, to_say);
712 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Adding <%s>: \"%s\"\n", cur_node->tag_name, file);
713 parsed_data->files[parsed_data->num_files].name = file;
714 parsed_data->files[parsed_data->num_files++].prefix = NULL;
715 return 1;
716 }
717 return 0;
718 }
719
720 /**
721 * Get TTS from CDATA
722 */
process_cdata_tts(struct ssml_parser * parsed_data,char * data,size_t len)723 static int process_cdata_tts(struct ssml_parser *parsed_data, char *data, size_t len)
724 {
725 struct ssml_node *cur_node = parsed_data->cur_node;
726 if (!len) {
727 return IKS_OK;
728 }
729 if (cur_node && parsed_data->num_files < parsed_data->max_files) {
730 int i = 0;
731 int empty = 1;
732 char *to_say;
733
734 /* is CDATA empty? */
735 for (i = 0; i < len && empty; i++) {
736 empty &= !isgraph(data[i]);
737 }
738 if (empty) {
739 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Skipping empty tts\n");
740 return IKS_OK;
741 }
742
743 /* try macro */
744 to_say = malloc(len + 1);
745 switch_assert(to_say);
746 strncpy(to_say, data, len);
747 to_say[len] = '\0';
748 if (!cur_node->say_macro || !get_file_from_macro(parsed_data, to_say)) {
749 /* use voice instead */
750 if (!get_file_from_voice(parsed_data, to_say)) {
751 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "No TTS voices available to render text!\n");
752 }
753 }
754 free(to_say);
755 return IKS_OK;
756 }
757 return IKS_BADXML;
758 }
759
760 /**
761 * Process <sub>- this is an alias for text to speak
762 */
process_sub(struct ssml_parser * parsed_data,char ** atts)763 static int process_sub(struct ssml_parser *parsed_data, char **atts)
764 {
765 if (atts) {
766 int i = 0;
767 while (atts[i]) {
768 if (!strcmp("alias", atts[i])) {
769 char *alias = atts[i + 1];
770 if (!zstr(alias)) {
771 return process_cdata_tts(parsed_data, alias, strlen(alias));
772 }
773 return IKS_BADXML;
774 }
775 i += 2;
776 }
777 }
778 return IKS_OK;
779 }
780
781 /**
782 * Process cdata
783 */
cdata_hook(void * user_data,char * data,size_t len)784 static int cdata_hook(void *user_data, char *data, size_t len)
785 {
786 struct ssml_parser *parsed_data = (struct ssml_parser *)user_data;
787 if (!parsed_data) {
788 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Missing parser\n");
789 return IKS_BADXML;
790 }
791 if (parsed_data->cur_node) {
792 struct tag_def *handler = switch_core_hash_find(globals.tag_defs, parsed_data->cur_node->tag_name);
793 if (handler) {
794 return handler->cdata_fn(parsed_data, data, len);
795 }
796 return IKS_BADXML;
797 }
798 return IKS_OK;
799 }
800
801 /**
802 * Transforms SSML into file_string format and opens file_string.
803 * @param handle
804 * @param path the inline SSML
805 * @return SWITCH_STATUS_SUCCESS if opened
806 */
ssml_file_open(switch_file_handle_t * handle,const char * path)807 static switch_status_t ssml_file_open(switch_file_handle_t *handle, const char *path)
808 {
809 switch_status_t status = SWITCH_STATUS_FALSE;
810 struct ssml_context *context = switch_core_alloc(handle->memory_pool, sizeof(*context));
811 struct ssml_parser *parsed_data = switch_core_alloc(handle->memory_pool, sizeof(*parsed_data));
812 iksparser *parser = iks_sax_new(parsed_data, tag_hook, cdata_hook);
813
814 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Open: %s\n", path);
815
816 parsed_data->cur_node = NULL;
817 parsed_data->files = switch_core_alloc(handle->memory_pool, sizeof(struct ssml_file) * MAX_VOICE_FILES);
818 parsed_data->max_files = MAX_VOICE_FILES;
819 parsed_data->num_files = 0;
820 parsed_data->pool = handle->memory_pool;
821 parsed_data->sample_rate = handle->samplerate;
822
823 if (iks_parse(parser, path, 0, 1) == IKS_OK) {
824 if (parsed_data->num_files) {
825 context->files = parsed_data->files;
826 context->num_files = parsed_data->num_files;
827 context->index = -1;
828 handle->private_info = context;
829 status = next_file(handle);
830 } else {
831 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "No files to play: %s\n", path);
832 }
833 } else {
834 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Parse error: %s, num_files = %i\n", path, parsed_data->num_files);
835 }
836
837 iks_parser_delete(parser);
838
839 return status;
840 }
841
842 /**
843 * Close SSML document.
844 * @param handle
845 * @return SWITCH_STATUS_SUCCESS
846 */
ssml_file_close(switch_file_handle_t * handle)847 static switch_status_t ssml_file_close(switch_file_handle_t *handle)
848 {
849 struct ssml_context *context = (struct ssml_context *)handle->private_info;
850 if (switch_test_flag((&context->fh), SWITCH_FILE_OPEN)) {
851 return switch_core_file_close(&context->fh);
852 }
853
854 return SWITCH_STATUS_SUCCESS;
855 }
856
857 /**
858 * Read from SSML document
859 * @param handle
860 * @param data
861 * @param len
862 * @return
863 */
ssml_file_read(switch_file_handle_t * handle,void * data,size_t * len)864 static switch_status_t ssml_file_read(switch_file_handle_t *handle, void *data, size_t *len)
865 {
866 switch_status_t status;
867 struct ssml_context *context = (struct ssml_context *)handle->private_info;
868 size_t llen = *len;
869
870 status = switch_core_file_read(&context->fh, data, len);
871 if (status != SWITCH_STATUS_SUCCESS) {
872 if ((status = next_file(handle)) != SWITCH_STATUS_SUCCESS) {
873 return status;
874 }
875 *len = llen;
876 status = switch_core_file_read(&context->fh, data, len);
877 }
878 return status;
879 }
880
881 /**
882 * Seek file
883 */
ssml_file_seek(switch_file_handle_t * handle,unsigned int * cur_sample,int64_t samples,int whence)884 static switch_status_t ssml_file_seek(switch_file_handle_t *handle, unsigned int *cur_sample, int64_t samples, int whence)
885 {
886 struct ssml_context *context = handle->private_info;
887
888 if (samples == 0 && whence == SWITCH_SEEK_SET) {
889 /* restart from beginning */
890 context->index = -1;
891 return next_file(handle);
892 }
893
894 if (!handle->seekable) {
895 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "File is not seekable\n");
896 return SWITCH_STATUS_NOTIMPL;
897 }
898
899 return switch_core_file_seek(&context->fh, cur_sample, samples, whence);
900 }
901
902 /**
903 * TTS playback state
904 */
905 struct tts_context {
906 /** handle to TTS engine */
907 switch_speech_handle_t sh;
908 /** TTS flags */
909 switch_speech_flag_t flags;
910 /** maximum number of samples to read at a time */
911 int max_frame_size;
912 /** done flag */
913 int done;
914 };
915
916 /**
917 * Do TTS as file format
918 * @param handle
919 * @param path the inline SSML
920 * @return SWITCH_STATUS_SUCCESS if opened
921 */
tts_file_open(switch_file_handle_t * handle,const char * path)922 static switch_status_t tts_file_open(switch_file_handle_t *handle, const char *path)
923 {
924 switch_status_t status = SWITCH_STATUS_SUCCESS;
925 struct tts_context *context = switch_core_alloc(handle->memory_pool, sizeof(*context));
926 char *arg_string = switch_core_strdup(handle->memory_pool, path);
927 char *args[3] = { 0 };
928 int argc = switch_separate_string(arg_string, '|', args, (sizeof(args) / sizeof(args[0])));
929 char *module;
930 char *voice;
931 char *document;
932
933 /* path is module:(optional)profile|voice|{param1=val1,param2=val2}TTS document */
934 if (argc != 3) {
935 return SWITCH_STATUS_FALSE;
936 }
937 module = args[0];
938 voice = args[1];
939 document = args[2];
940
941 memset(context, 0, sizeof(*context));
942 context->flags = SWITCH_SPEECH_FLAG_NONE;
943 if ((status = switch_core_speech_open(&context->sh, module, voice, handle->samplerate, handle->interval, handle->channels, &context->flags, NULL)) == SWITCH_STATUS_SUCCESS) {
944 if ((status = switch_core_speech_feed_tts(&context->sh, document, &context->flags)) == SWITCH_STATUS_SUCCESS) {
945 handle->channels = 1;
946 handle->samples = 0;
947 handle->format = 0;
948 handle->sections = 0;
949 handle->seekable = 0;
950 handle->speed = 0;
951 context->max_frame_size = handle->samplerate / 1000 * SWITCH_MAX_INTERVAL;
952 } else {
953 switch_core_speech_close(&context->sh, &context->flags);
954 }
955 }
956 handle->private_info = context;
957 return status;
958 }
959
960 /**
961 * Read audio from TTS engine
962 * @param handle
963 * @param data
964 * @param len
965 * @return
966 */
tts_file_read(switch_file_handle_t * handle,void * data,size_t * len)967 static switch_status_t tts_file_read(switch_file_handle_t *handle, void *data, size_t *len)
968 {
969 switch_status_t status = SWITCH_STATUS_SUCCESS;
970 struct tts_context *context = (struct tts_context *)handle->private_info;
971 switch_size_t rlen;
972
973 if (*len > context->max_frame_size) {
974 *len = context->max_frame_size;
975 }
976 rlen = *len * 2; /* rlen (bytes) = len (samples) * 2 */
977
978 if (!context->done) {
979 context->flags = SWITCH_SPEECH_FLAG_BLOCKING;
980 if ((status = switch_core_speech_read_tts(&context->sh, data, &rlen, &context->flags))) {
981 context->done = 1;
982 }
983 } else {
984 switch_core_speech_flush_tts(&context->sh);
985 memset(data, 0, rlen);
986 status = SWITCH_STATUS_FALSE;
987 }
988 *len = rlen / 2; /* len (samples) = rlen (bytes) / 2 */
989 return status;
990 }
991
992 /**
993 * Close TTS engine
994 * @param handle
995 * @return SWITCH_STATUS_SUCCESS
996 */
tts_file_close(switch_file_handle_t * handle)997 static switch_status_t tts_file_close(switch_file_handle_t *handle)
998 {
999 struct tts_context *context = (struct tts_context *)handle->private_info;
1000 switch_core_speech_close(&context->sh, &context->flags);
1001 return SWITCH_STATUS_SUCCESS;
1002 }
1003
1004 /**
1005 * Configure voices
1006 * @param pool memory pool to use
1007 * @param map voice map to load
1008 * @param type type of voices (for logging)
1009 */
do_config_voices(switch_memory_pool_t * pool,switch_xml_t voices,switch_hash_t * map,const char * type)1010 static void do_config_voices(switch_memory_pool_t *pool, switch_xml_t voices, switch_hash_t *map, const char *type)
1011 {
1012 if (voices) {
1013 int priority = MAX_VOICE_PRIORITY;
1014 switch_xml_t voice;
1015 for (voice = switch_xml_child(voices, "voice"); voice; voice = voice->next) {
1016 const char *name = switch_xml_attr_soft(voice, "name");
1017 const char *language = switch_xml_attr_soft(voice, "language");
1018 const char *gender = switch_xml_attr_soft(voice, "gender");
1019 const char *prefix = switch_xml_attr_soft(voice, "prefix");
1020 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "%s map (%s, %s, %s) = %s\n", type, name, language, gender, prefix);
1021 if (!zstr(name) && !zstr(prefix)) {
1022 struct voice *v = (struct voice *)switch_core_alloc(pool, sizeof(*v));
1023 v->name = switch_core_strdup(pool, name);
1024 v->language = switch_core_strdup(pool, language);
1025 v->gender = switch_core_strdup(pool, gender);
1026 v->prefix = switch_core_strdup(pool, prefix);
1027 v->priority = priority--;
1028 switch_core_hash_insert(map, name, v);
1029 }
1030 }
1031 }
1032 }
1033
1034 /**
1035 * Set default configuration when no XML configuration is present.
1036 * @param pool memory pool to use
1037 * @return SWITCH_STATUS_SUCCESS if module is configured
1038 */
do_default_config(switch_memory_pool_t * pool)1039 static switch_status_t do_default_config(switch_memory_pool_t *pool)
1040 {
1041 struct voice *v = NULL;
1042 struct language *l = NULL;
1043 struct macro *m = NULL;
1044 const char *sounds_dir = switch_core_get_variable("sounds_dir");
1045
1046 /* add TTS voice */
1047 v = switch_core_alloc(pool, sizeof(*v));
1048 v->name = "slt";
1049 v->language = "en-US";
1050 v->gender = "female";
1051 v->prefix = "tts://flite|slt|";
1052 v->priority = MAX_VOICE_PRIORITY;
1053 switch_core_hash_insert(globals.tts_voice_map, "slt", v);
1054
1055 /* add Say voice */
1056 v = switch_core_alloc(pool, sizeof(*v));
1057 v->name = "callie";
1058 v->language = "en-US";
1059 v->gender = "female";
1060 v->prefix = switch_core_sprintf(pool, "%s/en/us/callie/", sounds_dir ? sounds_dir : "");
1061 switch_core_hash_insert(globals.say_voice_map, "callie", v);
1062
1063 /* Add ISO language to Say language mapping */
1064 l = switch_core_alloc(pool, sizeof(*l));
1065 l->iso = "en-US";
1066 l->say_module = "en";
1067 l->language = "en";
1068 switch_core_hash_insert(globals.language_map, "en-US", l);
1069
1070 /* Map interpret-as to Say */
1071 m = switch_core_alloc(pool, sizeof(*m));
1072 m->name = "ordinal";
1073 m->method = "counted";
1074 m->type = "number";
1075 switch_core_hash_insert(globals.interpret_as_map, "ordinal", m);
1076
1077 m = switch_core_alloc(pool, sizeof(*m));
1078 m->name = "cardinal";
1079 m->method = "pronounced";
1080 m->type = "number";
1081 switch_core_hash_insert(globals.interpret_as_map, "cardinal", m);
1082
1083 m = switch_core_alloc(pool, sizeof(*m));
1084 m->name = "characters";
1085 m->method = "pronounced";
1086 m->type = "name_spelled";
1087 switch_core_hash_insert(globals.interpret_as_map, "characters", m);
1088
1089 m = switch_core_alloc(pool, sizeof(*m));
1090 m->name = "telephone";
1091 m->method = "pronounced";
1092 m->type = "telephone_number";
1093 switch_core_hash_insert(globals.interpret_as_map, "telephone", m);
1094
1095 return SWITCH_STATUS_SUCCESS;
1096 }
1097
1098
1099 /**
1100 * Configure module
1101 * @param pool memory pool to use
1102 * @return SWITCH_STATUS_SUCCESS if module is configured
1103 */
do_config(switch_memory_pool_t * pool)1104 static switch_status_t do_config(switch_memory_pool_t *pool)
1105 {
1106 char *cf = "ssml.conf";
1107 switch_xml_t cfg, xml;
1108
1109 if (!(xml = switch_xml_open_cfg(cf, &cfg, NULL))) {
1110 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "open of %s failed, using default configuration\n", cf);
1111 return do_default_config(pool);
1112 }
1113
1114 /* get voices */
1115 do_config_voices(pool, switch_xml_child(cfg, "tts-voices"), globals.tts_voice_map, "tts");
1116 do_config_voices(pool, switch_xml_child(cfg, "say-voices"), globals.say_voice_map, "say");
1117
1118 /* get languages */
1119 {
1120 switch_xml_t languages = switch_xml_child(cfg, "language-map");
1121 if (languages) {
1122 switch_xml_t language;
1123 for (language = switch_xml_child(languages, "language"); language; language = language->next) {
1124 const char *iso = switch_xml_attr_soft(language, "iso");
1125 const char *say_module = switch_xml_attr_soft(language, "say-module");
1126 const char *lang = switch_xml_attr_soft(language, "language");
1127 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "language map: %s = (%s, %s) \n", iso, say_module, lang);
1128 if (!zstr(iso) && !zstr(say_module) && !zstr(lang)) {
1129 struct language *l = (struct language *)switch_core_alloc(pool, sizeof(*l));
1130 l->iso = switch_core_strdup(pool, iso);
1131 l->say_module = switch_core_strdup(pool, say_module);
1132 l->language = switch_core_strdup(pool, lang);
1133 switch_core_hash_insert(globals.language_map, iso, l);
1134 }
1135 }
1136 }
1137 }
1138
1139 /* get macros */
1140 {
1141 switch_xml_t macros = switch_xml_child(cfg, "macros");
1142 if (macros) {
1143 switch_xml_t macro;
1144 for (macro = switch_xml_child(macros, "macro"); macro; macro = macro->next) {
1145 const char *name = switch_xml_attr_soft(macro, "name");
1146 const char *method = switch_xml_attr_soft(macro, "method");
1147 const char *type = switch_xml_attr_soft(macro, "type");
1148 switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "macro: %s = (%s, %s) \n", name, method, type);
1149 if (!zstr(name) && !zstr(type)) {
1150 struct macro *m = (struct macro *)switch_core_alloc(pool, sizeof(*m));
1151 m->name = switch_core_strdup(pool, name);
1152 m->method = switch_core_strdup(pool, method);
1153 m->type = switch_core_strdup(pool, type);
1154 switch_core_hash_insert(globals.interpret_as_map, name, m);
1155 }
1156 }
1157 }
1158 }
1159
1160 switch_xml_free(xml);
1161
1162 return SWITCH_STATUS_SUCCESS;
1163 }
1164
1165 static char *ssml_supported_formats[] = { "ssml", NULL };
1166 static char *tts_supported_formats[] = { "tts", NULL };
1167
SWITCH_MODULE_LOAD_FUNCTION(mod_ssml_load)1168 SWITCH_MODULE_LOAD_FUNCTION(mod_ssml_load)
1169 {
1170 switch_file_interface_t *file_interface;
1171
1172 *module_interface = switch_loadable_module_create_module_interface(pool, modname);
1173 file_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_FILE_INTERFACE);
1174 file_interface->interface_name = modname;
1175 file_interface->extens = ssml_supported_formats;
1176 file_interface->file_open = ssml_file_open;
1177 file_interface->file_close = ssml_file_close;
1178 file_interface->file_read = ssml_file_read;
1179 file_interface->file_seek = ssml_file_seek;
1180
1181 file_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_FILE_INTERFACE);
1182 file_interface->interface_name = modname;
1183 file_interface->extens = tts_supported_formats;
1184 file_interface->file_open = tts_file_open;
1185 file_interface->file_close = tts_file_close;
1186 file_interface->file_read = tts_file_read;
1187 /* TODO allow skip ahead if TTS supports it
1188 * file_interface->file_seek = tts_file_seek;
1189 */
1190
1191 globals.pool = pool;
1192 switch_core_hash_init(&globals.voice_cache);
1193 switch_core_hash_init(&globals.tts_voice_map);
1194 switch_mutex_init(&globals.tts_voice_map_mutex, SWITCH_MUTEX_NESTED, pool);
1195 switch_core_hash_init(&globals.say_voice_map);
1196 switch_mutex_init(&globals.say_voice_map_mutex, SWITCH_MUTEX_NESTED, pool);
1197 switch_core_hash_init(&globals.interpret_as_map);
1198 switch_core_hash_init(&globals.language_map);
1199 switch_core_hash_init(&globals.tag_defs);
1200
1201 add_root_tag_def("speak", process_xml_lang, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub,p,s,lexicon,metadata,meta");
1202 add_tag_def("p", process_xml_lang, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub,s");
1203 add_tag_def("s", process_xml_lang, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub");
1204 add_tag_def("voice", process_voice, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub,p,s");
1205 add_tag_def("prosody", process_attribs_ignore, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub,p,s");
1206 add_tag_def("audio", process_audio, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub,p,s,desc");
1207 add_tag_def("desc", process_attribs_ignore, process_cdata_ignore, "");
1208 add_tag_def("emphasis", process_attribs_ignore, process_cdata_tts, "audio,break,emphasis,mark,phoneme,prosody,say-as,voice,sub");
1209 add_tag_def("say-as", process_say_as, process_cdata_tts, "");
1210 add_tag_def("sub", process_sub, process_cdata_ignore, "");
1211 add_tag_def("phoneme", process_attribs_ignore, process_cdata_tts, "");
1212 add_tag_def("break", process_break, process_cdata_bad, "");
1213 add_tag_def("mark", process_attribs_ignore, process_cdata_bad, "");
1214 add_tag_def("lexicon", process_attribs_ignore, process_cdata_bad, "");
1215 add_tag_def("metadata", process_attribs_ignore, process_cdata_ignore, "ANY");
1216 add_tag_def("meta", process_attribs_ignore, process_cdata_bad, "");
1217
1218 return do_config(pool);
1219 }
1220
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_ssml_shutdown)1221 SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_ssml_shutdown)
1222 {
1223 switch_core_hash_destroy(&globals.voice_cache);
1224 switch_core_hash_destroy(&globals.tts_voice_map);
1225 switch_core_hash_destroy(&globals.say_voice_map);
1226 switch_core_hash_destroy(&globals.interpret_as_map);
1227 switch_core_hash_destroy(&globals.language_map);
1228 {
1229 switch_hash_index_t *hi = NULL;
1230 for (hi = switch_core_hash_first(globals.tag_defs); hi; hi = switch_core_hash_next(&hi)) {
1231 const void *key;
1232 struct tag_def *def;
1233 switch_core_hash_this(hi, &key, NULL, (void *)&def);
1234 switch_core_hash_destroy(&def->children_tags);
1235 }
1236 }
1237 switch_core_hash_destroy(&globals.tag_defs);
1238
1239 return SWITCH_STATUS_SUCCESS;
1240 }
1241
1242 /* For Emacs:
1243 * Local Variables:
1244 * mode:c
1245 * indent-tabs-mode:t
1246 * tab-width:4
1247 * c-basic-offset:4
1248 * End:
1249 * For VIM:
1250 * vim:set softtabstop=4 shiftwidth=4 tabstop=4 noet:
1251 */
1252