Add short explanation on how to build rbutilqt.
[Rockbox.git] / apps / talk.c
blobb9d5bc2aa779d0892e555f263f3f1aec446488bc
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
10 * Copyright (C) 2004 Jörg Hohensohn
12 * This module collects the Talkbox and voice UI functions.
13 * (Talkbox reads directory names from mp3 clips called thumbnails,
14 * the voice UI lets menus and screens "talk" from a voicefile in memory.
16 * All files in this archive are subject to the GNU General Public License.
17 * See the file COPYING in the source tree root for full license agreement.
19 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20 * KIND, either express or implied.
22 ****************************************************************************/
24 #include <stdio.h>
25 #include <stddef.h>
26 #include <string.h>
27 #include "file.h"
28 #include "buffer.h"
29 #include "system.h"
30 #include "kernel.h"
31 #include "settings.h"
32 #include "mp3_playback.h"
33 #include "audio.h"
34 #include "lang.h"
35 #include "talk.h"
36 #include "id3.h"
37 #include "logf.h"
38 #include "bitswap.h"
39 #include "structec.h"
40 #if CONFIG_CODEC == SWCODEC
41 #include "playback.h"
42 #endif
43 #include "debug.h"
46 /* Memory layout varies between targets because the
47 Archos (MASCODEC) devices cannot mix voice and audio playback
49 MASCODEC | MASCODEC | SWCODEC
50 (playing) | (stopped) |
51 audiobuf-----------+-----------+------------
52 audio | voice | thumbnail
53 |-----------|------------
54 | thumbnail | voice
55 | |------------
56 | | filebuf
57 | |------------
58 | | audio
59 | |------------
60 | | codec swap
61 audiobufend----------+-----------+------------
63 SWCODEC allocates dedicated buffers, MASCODEC reuses audiobuf. */
66 /***************** Constants *****************/
68 #define QUEUE_SIZE 64 /* must be a power of two */
69 #define QUEUE_MASK (QUEUE_SIZE-1)
70 const char* const dir_thumbnail_name = "_dirname.talk";
71 const char* const file_thumbnail_ext = ".talk";
73 /***************** Functional Macros *****************/
75 #define QUEUE_LEVEL ((queue_write - queue_read) & QUEUE_MASK)
77 #define LOADED_MASK 0x80000000 /* MSB */
79 #if CONFIG_CODEC == SWCODEC
80 #define MAX_THUMBNAIL_BUFSIZE 32768
81 #endif
83 #ifndef SIMULATOR
84 extern bool audio_is_initialized;
85 #endif
87 /***************** Data types *****************/
89 struct clip_entry /* one entry of the index table */
91 int offset; /* offset from start of voicefile file */
92 int size; /* size of the clip */
95 struct voicefile /* file format of our voice file */
97 int version; /* version of the voicefile */
98 int target_id; /* the rockbox target the file was made for */
99 int table; /* offset to index table, (=header size) */
100 int id1_max; /* number of "normal" clips contained in above index */
101 int id2_max; /* number of "voice only" clips contained in above index */
102 struct clip_entry index[]; /* followed by the index tables */
103 /* and finally the mp3 clips, not visible here, bitswapped
104 for SH based players */
107 struct queue_entry /* one entry of the internal queue */
109 unsigned char* buf;
110 long len;
114 /***************** Globals *****************/
116 static unsigned char* p_thumbnail = NULL; /* buffer for thumbnail */
117 static long size_for_thumbnail; /* leftover buffer size for it */
118 static struct voicefile* p_voicefile; /* loaded voicefile */
119 static bool has_voicefile; /* a voicefile file is present */
120 static struct queue_entry queue[QUEUE_SIZE]; /* queue of scheduled clips */
121 /* enqueue next utterance even if enqueue is false. */
122 static bool force_enqueue_next;
123 static int queue_write; /* write index of queue, by application */
124 static int queue_read; /* read index of queue, by ISR context */
125 static int sent; /* how many bytes handed over to playback, owned by ISR */
126 static unsigned char curr_hd[3]; /* current frame header, for re-sync */
127 static int filehandle = -1; /* global, so the MMC variant can keep the file open */
128 static unsigned char* p_silence; /* VOICE_PAUSE clip, used for termination */
129 static long silence_len; /* length of the VOICE_PAUSE clip */
130 static unsigned char* p_lastclip; /* address of latest clip, for silence add */
131 static unsigned long voicefile_size = 0; /* size of the loaded voice file */
132 static unsigned char last_lang[MAX_FILENAME+1]; /* name of last used lang file (in talk_init) */
133 static bool talk_initialized; /* true if talk_init has been called */
134 static int talk_menu_disable; /* if non-zero, temporarily disable voice UI (not saved) */
136 /***************** Private prototypes *****************/
138 static void load_voicefile(void);
139 static void mp3_callback(unsigned char** start, size_t* size);
140 static int queue_clip(unsigned char* buf, long size, bool enqueue);
141 static int open_voicefile(void);
142 static unsigned char* get_clip(long id, long* p_size);
143 static int shutup(void); /* Interrupt voice, as when enqueue is false */
145 /***************** Private implementation *****************/
147 static int open_voicefile(void)
149 char buf[64];
150 char* p_lang = "english"; /* default */
152 if ( global_settings.lang_file[0] &&
153 global_settings.lang_file[0] != 0xff )
154 { /* try to open the voice file of the selected language */
155 p_lang = (char *)global_settings.lang_file;
158 snprintf(buf, sizeof(buf), LANG_DIR "/%s.voice", p_lang);
160 return open(buf, O_RDONLY);
164 /* load the voice file into the mp3 buffer */
165 static void load_voicefile(void)
167 int load_size;
168 int got_size;
169 int file_size;
170 #ifdef ROCKBOX_LITTLE_ENDIAN
171 int i;
172 #endif
174 filehandle = open_voicefile();
175 if (filehandle < 0) /* failed to open */
176 goto load_err;
178 file_size = filesize(filehandle);
179 if (file_size > audiobufend - audiobuf) /* won't fit? */
180 goto load_err;
182 #ifdef HAVE_MMC /* load only the header for now */
183 load_size = offsetof(struct voicefile, index);
184 #else /* load the full file */
185 load_size = file_size;
186 #endif
188 got_size = read(filehandle, audiobuf, load_size);
189 if (got_size != load_size /* failure */)
190 goto load_err;
192 #ifdef ROCKBOX_LITTLE_ENDIAN
193 logf("Byte swapping voice file");
194 structec_convert(audiobuf, "lllll", 1, true);
195 #endif
197 if (((struct voicefile*)audiobuf)->table /* format check */
198 == offsetof(struct voicefile, index))
200 p_voicefile = (struct voicefile*)audiobuf;
202 if (p_voicefile->target_id != TARGET_ID)
204 logf("Incompatible voice file (wrong target)");
205 goto load_err;
207 #if CONFIG_CODEC != SWCODEC
208 /* MASCODEC: now use audiobuf for voice then thumbnail */
209 p_thumbnail = audiobuf + file_size;
210 p_thumbnail += (long)p_thumbnail % 2; /* 16-bit align */
211 size_for_thumbnail = audiobufend - p_thumbnail;
212 #endif
214 else
215 goto load_err;
217 #ifdef ROCKBOX_LITTLE_ENDIAN
218 for (i = 0; i < p_voicefile->id1_max + p_voicefile->id2_max; i++)
219 structec_convert(&p_voicefile->index[i], "ll", 1, true);
220 #endif
222 #ifdef HAVE_MMC
223 /* load the index table, now that we know its size from the header */
224 load_size = (p_voicefile->id1_max + p_voicefile->id2_max)
225 * sizeof(struct clip_entry);
226 got_size = read(filehandle,
227 (unsigned char *) p_voicefile + offsetof(struct voicefile, index), load_size);
228 if (got_size != load_size) /* read error */
229 goto load_err;
230 #else
231 close(filehandle); /* only the MMC variant leaves it open */
232 filehandle = -1;
233 #endif
235 /* make sure to have the silence clip, if available */
236 p_silence = get_clip(VOICE_PAUSE, &silence_len);
238 return;
240 load_err:
241 p_voicefile = NULL;
242 has_voicefile = false; /* don't try again */
243 if (filehandle >= 0)
245 close(filehandle);
246 filehandle = -1;
248 return;
252 /* Are more voice clips queued and waiting? */
253 bool is_voice_queued()
255 return !!QUEUE_LEVEL;
259 /* called in ISR context if mp3 data got consumed */
260 static void mp3_callback(unsigned char** start, size_t* size)
262 queue[queue_read].len -= sent; /* we completed this */
263 queue[queue_read].buf += sent;
265 if (queue[queue_read].len > 0) /* current clip not finished? */
266 { /* feed the next 64K-1 chunk */
267 #if CONFIG_CODEC != SWCODEC
268 sent = MIN(queue[queue_read].len, 0xFFFF);
269 #else
270 sent = queue[queue_read].len;
271 #endif
272 *start = queue[queue_read].buf;
273 *size = sent;
274 return;
276 else if (sent > 0) /* go to next entry */
278 queue_read = (queue_read + 1) & QUEUE_MASK;
281 re_check:
283 if (QUEUE_LEVEL) /* queue is not empty? */
284 { /* start next clip */
285 #if CONFIG_CODEC != SWCODEC
286 sent = MIN(queue[queue_read].len, 0xFFFF);
287 #else
288 sent = queue[queue_read].len;
289 #endif
290 *start = p_lastclip = queue[queue_read].buf;
291 *size = sent;
292 curr_hd[0] = p_lastclip[1];
293 curr_hd[1] = p_lastclip[2];
294 curr_hd[2] = p_lastclip[3];
296 else if (p_silence != NULL /* silence clip available */
297 && p_lastclip != p_silence /* previous clip wasn't silence */
298 && p_lastclip != p_thumbnail) /* ..or thumbnail */
299 { /* add silence clip when queue runs empty playing a voice clip */
300 queue[queue_write].buf = p_silence;
301 queue[queue_write].len = silence_len;
302 queue_write = (queue_write + 1) & QUEUE_MASK;
304 goto re_check;
306 else
308 *size = 0; /* end of data */
312 /* stop the playback and the pending clips */
313 int do_shutup(void)
315 #if CONFIG_CODEC != SWCODEC
316 unsigned char* pos;
317 unsigned char* search;
318 unsigned char* end;
319 #endif
321 if (QUEUE_LEVEL == 0) /* has ended anyway */
323 #if CONFIG_CODEC == SWCODEC
324 mp3_play_stop();
325 #endif
326 return 0;
328 #if CONFIG_CODEC != SWCODEC
329 #if CONFIG_CPU == SH7034
330 CHCR3 &= ~0x0001; /* disable the DMA (and therefore the interrupt also) */
331 #endif
332 /* search next frame boundary and continue up to there */
333 pos = search = mp3_get_pos();
334 end = queue[queue_read].buf + queue[queue_read].len;
336 if (pos >= queue[queue_read].buf
337 && pos <= end) /* really our clip? */
338 { /* (for strange reasons this isn't nesessarily the case) */
339 /* find the next frame boundary */
340 while (search < end) /* search the remaining data */
342 if (*search++ != 0xFF) /* quick search for frame sync byte */
343 continue; /* (this does the majority of the job) */
345 /* look at the (bitswapped) rest of header candidate */
346 if (search[0] == curr_hd[0] /* do the quicker checks first */
347 && search[2] == curr_hd[2]
348 && (search[1] & 0x30) == (curr_hd[1] & 0x30)) /* sample rate */
350 search--; /* back to the sync byte */
351 break; /* From looking at it, this is our header. */
355 if (search-pos)
356 { /* play old data until the frame end, to keep the MAS in sync */
357 sent = search-pos;
359 queue_write = (queue_read + 1) & QUEUE_MASK; /* will be empty after next callback */
360 queue[queue_read].len = sent; /* current one ends after this */
362 #if CONFIG_CPU == SH7034
363 DTCR3 = sent; /* let the DMA finish this frame */
364 CHCR3 |= 0x0001; /* re-enable DMA */
365 #endif
366 return 0;
369 #endif
371 /* nothing to do, was frame boundary or not our clip */
372 mp3_play_stop();
374 queue_write = queue_read = 0; /* reset the queue */
376 return 0;
379 /* Shutup the voice, except if force_enqueue_next is set. */
380 static int shutup(void)
382 if (!force_enqueue_next)
383 return do_shutup();
384 return 0;
387 /* schedule a clip, at the end or discard the existing queue */
388 static int queue_clip(unsigned char* buf, long size, bool enqueue)
390 int queue_level;
392 if (!enqueue)
393 shutup(); /* cut off all the pending stuff */
394 /* Something is being enqueued, force_enqueue_next override is no
395 longer in effect. */
396 force_enqueue_next = false;
398 if (!size)
399 return 0; /* safety check */
400 #if CONFIG_CPU == SH7034
401 /* disable the DMA temporarily, to be safe of race condition */
402 CHCR3 &= ~0x0001;
403 #endif
404 queue_level = QUEUE_LEVEL; /* check old level */
406 if (queue_level < QUEUE_SIZE - 1) /* space left? */
408 queue[queue_write].buf = buf; /* populate an entry */
409 queue[queue_write].len = size;
410 queue_write = (queue_write + 1) & QUEUE_MASK;
413 if (queue_level == 0)
414 { /* queue was empty, we have to do the initial start */
415 p_lastclip = buf;
416 #if CONFIG_CODEC != SWCODEC
417 sent = MIN(size, 0xFFFF); /* DMA can do no more */
418 #else
419 sent = size;
420 #endif
421 mp3_play_data(buf, sent, mp3_callback);
422 curr_hd[0] = buf[1];
423 curr_hd[1] = buf[2];
424 curr_hd[2] = buf[3];
425 mp3_play_pause(true); /* kickoff audio */
427 else
429 #if CONFIG_CPU == SH7034
430 CHCR3 |= 0x0001; /* re-enable DMA */
431 #endif
434 return 0;
437 /* fetch a clip from the voice file */
438 static unsigned char* get_clip(long id, long* p_size)
440 long clipsize;
441 unsigned char* clipbuf;
443 if (id > VOICEONLY_DELIMITER)
444 { /* voice-only entries use the second part of the table */
445 id -= VOICEONLY_DELIMITER + 1;
446 if (id >= p_voicefile->id2_max)
447 return NULL; /* must be newer than we have */
448 id += p_voicefile->id1_max; /* table 2 is behind table 1 */
450 else
451 { /* normal use of the first table */
452 if (id >= p_voicefile->id1_max)
453 return NULL; /* must be newer than we have */
456 clipsize = p_voicefile->index[id].size;
457 if (clipsize == 0) /* clip not included in voicefile */
458 return NULL;
459 clipbuf = (unsigned char *) p_voicefile + p_voicefile->index[id].offset;
461 #ifdef HAVE_MMC /* dynamic loading, on demand */
462 if (!(clipsize & LOADED_MASK))
463 { /* clip used for the first time, needs loading */
464 lseek(filehandle, p_voicefile->index[id].offset, SEEK_SET);
465 if (read(filehandle, clipbuf, clipsize) != clipsize)
466 return NULL; /* read error */
468 p_voicefile->index[id].size |= LOADED_MASK; /* mark as loaded */
470 else
471 { /* clip is in memory already */
472 clipsize &= ~LOADED_MASK; /* without the extra bit gives true size */
474 #endif
476 *p_size = clipsize;
477 return clipbuf;
481 /* common code for talk_init() and talk_buffer_steal() */
482 static void reset_state(void)
484 queue_write = queue_read = 0; /* reset the queue */
485 p_voicefile = NULL; /* indicate no voicefile (trashed) */
486 #if CONFIG_CODEC == SWCODEC
487 /* Allocate a dedicated thumbnail buffer - once */
488 if (p_thumbnail == NULL)
490 size_for_thumbnail = audiobufend - audiobuf;
491 if (size_for_thumbnail > MAX_THUMBNAIL_BUFSIZE)
492 size_for_thumbnail = MAX_THUMBNAIL_BUFSIZE;
493 p_thumbnail = buffer_alloc(size_for_thumbnail);
495 #else
496 /* Just use the audiobuf, without allocating anything */
497 p_thumbnail = audiobuf;
498 size_for_thumbnail = audiobufend - audiobuf;
499 #endif
500 p_silence = NULL; /* pause clip not accessible */
503 /***************** Public implementation *****************/
505 void talk_init(void)
507 talk_menu_disable = 0;
508 if (talk_initialized && !strcasecmp(last_lang, global_settings.lang_file))
510 /* not a new file, nothing to do */
511 return;
514 #ifdef HAVE_MMC
515 if (filehandle >= 0) /* MMC: An old voice file might still be open */
517 close(filehandle);
518 filehandle = -1;
520 #endif
522 talk_initialized = true;
523 strncpy((char *) last_lang, (char *)global_settings.lang_file,
524 MAX_FILENAME);
526 #if CONFIG_CODEC == SWCODEC
527 audio_get_buffer(false, NULL); /* Must tell audio to reinitialize */
528 #endif
529 reset_state(); /* use this for most of our inits */
531 filehandle = open_voicefile();
532 has_voicefile = (filehandle >= 0); /* test if we can open it */
533 voicefile_size = 0;
535 if (has_voicefile)
537 voicefile_size = filesize(filehandle);
538 close(filehandle); /* close again, this was just to detect presence */
539 filehandle = -1;
544 #if CONFIG_CODEC == SWCODEC
545 /* return if a voice codec is required or not */
546 bool talk_voice_required(void)
548 return (voicefile_size != 0) /* Voice file is available */
549 || (global_settings.talk_dir_clip) /* Thumbnail clips are required */
550 || (global_settings.talk_file_clip);
552 #endif
554 /* return size of voice file */
555 int talk_get_bufsize(void)
557 return voicefile_size;
560 /* somebody else claims the mp3 buffer, e.g. for regular play/record */
561 int talk_buffer_steal(void)
563 #if CONFIG_CODEC != SWCODEC
564 mp3_play_stop();
565 #endif
566 #ifdef HAVE_MMC
567 if (filehandle >= 0) /* only relevant for MMC */
569 close(filehandle);
570 filehandle = -1;
572 #endif
573 reset_state();
575 return 0;
579 /* play a voice ID from voicefile */
580 int talk_id(long id, bool enqueue)
582 long clipsize;
583 unsigned char* clipbuf;
584 int unit;
586 #if CONFIG_CODEC != SWCODEC
587 if (audio_status()) /* busy, buffer in use */
588 return -1;
589 #endif
591 if (p_voicefile == NULL && has_voicefile)
592 load_voicefile(); /* reload needed */
594 if (p_voicefile == NULL) /* still no voices? */
595 return -1;
597 if (id == -1) /* -1 is an indication for silence */
598 return -1;
600 /* check if this is a special ID, with a value */
601 unit = ((unsigned long)id) >> UNIT_SHIFT;
602 if (unit)
603 { /* sign-extend the value */
604 id = (unsigned long)id << (32-UNIT_SHIFT);
605 id >>= (32-UNIT_SHIFT);
606 talk_value(id, unit, enqueue); /* speak it */
607 return 0; /* and stop, end of special case */
610 clipbuf = get_clip(id, &clipsize);
611 if (clipbuf == NULL)
612 return -1; /* not present */
614 queue_clip(clipbuf, clipsize, enqueue);
616 return 0;
619 /* Speaks zero or more IDs (from an array). */
620 int talk_idarray(long *ids, bool enqueue)
622 int r;
623 if(!ids)
624 return 0;
625 while(*ids != TALK_FINAL_ID)
627 if((r = talk_id(*ids++, enqueue)) <0)
628 return r;
629 enqueue = true;
631 return 0;
634 /* Make sure the current utterance is not interrupted by the next one. */
635 void talk_force_enqueue_next(void)
637 force_enqueue_next = true;
640 /* play a thumbnail from file */
641 int talk_file(const char* filename, bool enqueue)
643 int fd;
644 int size;
645 struct mp3entry info;
647 #if CONFIG_CODEC != SWCODEC
648 if (audio_status()) /* busy, buffer in use */
649 return -1;
650 #endif
652 if (p_thumbnail == NULL || size_for_thumbnail <= 0)
653 return -1;
655 if(mp3info(&info, filename, false)) /* use this to find real start */
657 return 0; /* failed to open, or invalid */
660 fd = open(filename, O_RDONLY);
661 if (fd < 0) /* failed to open */
663 return 0;
666 lseek(fd, info.first_frame_offset, SEEK_SET); /* behind ID data */
668 size = read(fd, p_thumbnail, size_for_thumbnail);
669 close(fd);
671 /* ToDo: find audio, skip ID headers and trailers */
673 if (size != 0 && size != size_for_thumbnail) /* Don't play missing or truncated clips */
675 #if CONFIG_CODEC != SWCODEC
676 bitswap(p_thumbnail, size);
677 #endif
678 queue_clip(p_thumbnail, size, enqueue);
681 return size;
685 /* say a numeric value, this word ordering works for english,
686 but not necessarily for other languages (e.g. german) */
687 int talk_number(long n, bool enqueue)
689 int level = 2; /* mille count */
690 long mil = 1000000000; /* highest possible "-illion" */
692 #if CONFIG_CODEC != SWCODEC
693 if (audio_status()) /* busy, buffer in use */
694 return -1;
695 #endif
697 if (!enqueue)
698 shutup(); /* cut off all the pending stuff */
700 if (n==0)
701 { /* special case */
702 talk_id(VOICE_ZERO, true);
703 return 0;
706 if (n<0)
708 talk_id(VOICE_MINUS, true);
709 n = -n;
712 while (n)
714 int segment = n / mil; /* extract in groups of 3 digits */
715 n -= segment * mil; /* remove the used digits from number */
716 mil /= 1000; /* digit place for next round */
718 if (segment)
720 int hundreds = segment / 100;
721 int ones = segment % 100;
723 if (hundreds)
725 talk_id(VOICE_ZERO + hundreds, true);
726 talk_id(VOICE_HUNDRED, true);
729 /* combination indexing */
730 if (ones > 20)
732 int tens = ones/10 + 18;
733 talk_id(VOICE_ZERO + tens, true);
734 ones %= 10;
737 /* direct indexing */
738 if (ones)
739 talk_id(VOICE_ZERO + ones, true);
741 /* add billion, million, thousand */
742 if (mil)
743 talk_id(VOICE_THOUSAND + level, true);
745 level--;
748 return 0;
751 /* singular/plural aware saying of a value */
752 int talk_value(long n, int unit, bool enqueue)
754 int unit_id;
755 static const int unit_voiced[] =
756 { /* lookup table for the voice ID of the units */
757 [0 ... UNIT_LAST-1] = -1, /* regular ID, int, signed */
758 [UNIT_MS]
759 = VOICE_MILLISECONDS, /* here come the "real" units */
760 [UNIT_SEC]
761 = VOICE_SECONDS,
762 [UNIT_MIN]
763 = VOICE_MINUTES,
764 [UNIT_HOUR]
765 = VOICE_HOURS,
766 [UNIT_KHZ]
767 = VOICE_KHZ,
768 [UNIT_DB]
769 = VOICE_DB,
770 [UNIT_PERCENT]
771 = VOICE_PERCENT,
772 [UNIT_MAH]
773 = VOICE_MILLIAMPHOURS,
774 [UNIT_PIXEL]
775 = VOICE_PIXEL,
776 [UNIT_PER_SEC]
777 = VOICE_PER_SEC,
778 [UNIT_HERTZ]
779 = VOICE_HERTZ,
780 [UNIT_MB]
781 = LANG_MEGABYTE,
782 [UNIT_KBIT]
783 = VOICE_KBIT_PER_SEC,
784 [UNIT_PM_TICK]
785 = VOICE_PM_UNITS_PER_TICK,
788 #if CONFIG_CODEC != SWCODEC
789 if (audio_status()) /* busy, buffer in use */
790 return -1;
791 #endif
793 if (unit < 0 || unit >= UNIT_LAST)
794 unit_id = -1;
795 else
796 unit_id = unit_voiced[unit];
798 if ((n==1 || n==-1) /* singular? */
799 && unit_id >= VOICE_SECONDS && unit_id <= VOICE_HOURS)
801 unit_id--; /* use the singular for those units which have */
804 /* special case with a "plus" before */
805 if (n > 0 && (unit == UNIT_SIGNED || unit == UNIT_DB))
807 talk_id(VOICE_PLUS, enqueue);
808 enqueue = true;
811 talk_number(n, enqueue); /* say the number */
812 talk_id(unit_id, true); /* say the unit, if any */
814 return 0;
817 /* spell a string */
818 int talk_spell(const char* spell, bool enqueue)
820 char c; /* currently processed char */
822 #if CONFIG_CODEC != SWCODEC
823 if (audio_status()) /* busy, buffer in use */
824 return -1;
825 #endif
827 if (!enqueue)
828 shutup(); /* cut off all the pending stuff */
830 while ((c = *spell++) != '\0')
832 /* if this grows into too many cases, I should use a table */
833 if (c >= 'A' && c <= 'Z')
834 talk_id(VOICE_CHAR_A + c - 'A', true);
835 else if (c >= 'a' && c <= 'z')
836 talk_id(VOICE_CHAR_A + c - 'a', true);
837 else if (c >= '0' && c <= '9')
838 talk_id(VOICE_ZERO + c - '0', true);
839 else if (c == '-')
840 talk_id(VOICE_MINUS, true);
841 else if (c == '+')
842 talk_id(VOICE_PLUS, true);
843 else if (c == '.')
844 talk_id(VOICE_DOT, true);
845 else if (c == ' ')
846 talk_id(VOICE_PAUSE, true);
849 return 0;
852 bool talk_menus_enabled(void)
854 return (global_settings.talk_menu && talk_menu_disable == 0);
858 void talk_disable_menus(void)
860 talk_menu_disable++;
863 void talk_enable_menus(void)
865 talk_menu_disable--;