diff options
Diffstat (limited to 'apps/talk.c')
-rw-r--r-- | apps/talk.c | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/apps/talk.c b/apps/talk.c new file mode 100644 index 0000000000..d208c7296c --- /dev/null +++ b/apps/talk.c | |||
@@ -0,0 +1,398 @@ | |||
1 | /*************************************************************************** | ||
2 | * __________ __ ___. | ||
3 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | * \/ \/ \/ \/ \/ | ||
8 | * $Id$ | ||
9 | * | ||
10 | * Copyright (C) 2004 Jörg Hohensohn | ||
11 | * | ||
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 voicefont in memory. | ||
15 | * | ||
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. | ||
18 | * | ||
19 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
20 | * KIND, either express or implied. | ||
21 | * | ||
22 | ****************************************************************************/ | ||
23 | |||
24 | #include <stdio.h> | ||
25 | #include <stddef.h> | ||
26 | #include "file.h" | ||
27 | #include "buffer.h" | ||
28 | #include "system.h" | ||
29 | #include "mp3_playback.h" | ||
30 | #include "mpeg.h" | ||
31 | #include "lang.h" | ||
32 | #include "talk.h" | ||
33 | #include "screens.h" /* test hack */ | ||
34 | extern void bitswap(unsigned char *data, int length); /* no header for this */ | ||
35 | |||
36 | /***************** Constants *****************/ | ||
37 | |||
38 | #define VOICEFONT_FILENAME "/.rockbox/langs/english.voice" | ||
39 | #define QUEUE_SIZE 32 | ||
40 | |||
41 | |||
42 | /***************** Data types *****************/ | ||
43 | |||
44 | struct clip_entry /* one entry of the index table */ | ||
45 | { | ||
46 | int offset; /* offset from start of voicefont file */ | ||
47 | int size; /* size of the clip */ | ||
48 | }; | ||
49 | |||
50 | struct voicefont /* file format of our "voicefont" */ | ||
51 | { | ||
52 | int version; /* version of the voicefont */ | ||
53 | int headersize; /* size of the header, =offset to index */ | ||
54 | int id_max; /* number of clips contained */ | ||
55 | struct clip_entry index[]; /* followed by the index table */ | ||
56 | /* and finally the bitswapped mp3 clips, not visible here */ | ||
57 | }; | ||
58 | |||
59 | |||
60 | struct queue_entry /* one entry of the internal queue */ | ||
61 | { | ||
62 | unsigned char* buf; | ||
63 | int len; | ||
64 | }; | ||
65 | |||
66 | |||
67 | |||
68 | /***************** Globals *****************/ | ||
69 | |||
70 | static unsigned char* p_thumbnail; /* buffer for thumbnail */ | ||
71 | static long size_for_thumbnail; /* leftover buffer size for it */ | ||
72 | static struct voicefont* p_voicefont; /* loaded voicefont */ | ||
73 | static bool has_voicefont; /* a voicefont file is present */ | ||
74 | static bool is_playing; /* we're currently playing */ | ||
75 | static struct queue_entry queue[QUEUE_SIZE]; /* queue of scheduled clips */ | ||
76 | static int queue_write; /* write index of queue, by application */ | ||
77 | static int queue_read; /* read index of queue, by ISR context */ | ||
78 | |||
79 | |||
80 | |||
81 | /***************** Private implementation *****************/ | ||
82 | |||
83 | static int load_voicefont(void) | ||
84 | { | ||
85 | int fd; | ||
86 | int size; | ||
87 | |||
88 | p_voicefont = NULL; /* indicate no voicefont if we fail below */ | ||
89 | |||
90 | fd = open(VOICEFONT_FILENAME, O_RDONLY); | ||
91 | if (fd < 0) /* failed to open */ | ||
92 | { | ||
93 | p_voicefont = NULL; /* indicate no voicefont */ | ||
94 | has_voicefont = false; /* don't try again */ | ||
95 | return 0; | ||
96 | } | ||
97 | |||
98 | size = read(fd, mp3buf, mp3end - mp3buf); | ||
99 | if (size > 1000 | ||
100 | && ((struct voicefont*)mp3buf)->headersize | ||
101 | == offsetof(struct voicefont, index)) | ||
102 | { | ||
103 | p_voicefont = (struct voicefont*)mp3buf; | ||
104 | |||
105 | /* thumbnail buffer is the remaining space behind */ | ||
106 | p_thumbnail = mp3buf + size; | ||
107 | p_thumbnail += (int)p_thumbnail % 2; /* 16-bit align */ | ||
108 | size_for_thumbnail = mp3end - p_thumbnail; | ||
109 | } | ||
110 | else | ||
111 | { | ||
112 | has_voicefont = false; /* don't try again */ | ||
113 | } | ||
114 | close(fd); | ||
115 | |||
116 | return size; | ||
117 | } | ||
118 | |||
119 | |||
120 | /* called in ISR context if mp3 data got consumed */ | ||
121 | static void mp3_callback(unsigned char** start, int* size) | ||
122 | { | ||
123 | int play_now; | ||
124 | |||
125 | if (queue[queue_read].len > 0) /* current clip not finished? */ | ||
126 | { /* feed the next 64K-1 chunk */ | ||
127 | play_now = MIN(queue[queue_read].len, 0xFFFF); | ||
128 | *start = queue[queue_read].buf; | ||
129 | *size = play_now; | ||
130 | queue[queue_read].buf += play_now; | ||
131 | queue[queue_read].len -= play_now; | ||
132 | return; | ||
133 | } | ||
134 | else /* go to next entry */ | ||
135 | { | ||
136 | queue_read++; | ||
137 | if (queue_read >= QUEUE_SIZE) | ||
138 | queue_read = 0; | ||
139 | } | ||
140 | |||
141 | if (queue_read != queue_write) /* queue is not empty? */ | ||
142 | { /* start next clip */ | ||
143 | play_now = MIN(queue[queue_read].len, 0xFFFF); | ||
144 | *start = queue[queue_read].buf; | ||
145 | *size = play_now; | ||
146 | queue[queue_read].buf += play_now; | ||
147 | queue[queue_read].len -= play_now; | ||
148 | } | ||
149 | else | ||
150 | { | ||
151 | *size = 0; /* end of data */ | ||
152 | is_playing = false; | ||
153 | mp3_play_stop(); /* fixme: should be done by caller */ | ||
154 | } | ||
155 | } | ||
156 | |||
157 | |||
158 | /* stop the playback and the pending clips, but at frame boundary */ | ||
159 | static int shutup(void) | ||
160 | { | ||
161 | mp3_play_pause(false); /* pause */ | ||
162 | |||
163 | /* ToDo: search next frame boundary and continue up to there */ | ||
164 | |||
165 | queue_write = queue_read; | ||
166 | is_playing = false; | ||
167 | mp3_play_stop(); | ||
168 | |||
169 | return 0; | ||
170 | } | ||
171 | |||
172 | |||
173 | /* schedule a clip, at the end or discard the existing queue */ | ||
174 | static int queue_clip(unsigned char* buf, int size, bool enqueue) | ||
175 | { | ||
176 | if (!enqueue) | ||
177 | shutup(); /* cut off all the pending stuff */ | ||
178 | |||
179 | queue[queue_write].buf = buf; | ||
180 | queue[queue_write].len = size; | ||
181 | |||
182 | /* FixMe: make this IRQ-safe */ | ||
183 | |||
184 | if (!is_playing) | ||
185 | { /* queue empty, we have to do the initial start */ | ||
186 | int size_now = MIN(size, 0xFFFF); /* DMA can do no more */ | ||
187 | is_playing = true; | ||
188 | mp3_play_data(buf, size_now, mp3_callback); | ||
189 | mp3_play_pause(true); /* kickoff audio */ | ||
190 | queue[queue_write].buf += size_now; | ||
191 | queue[queue_write].len -= size_now; | ||
192 | } | ||
193 | |||
194 | queue_write++; | ||
195 | if (queue_write >= QUEUE_SIZE) | ||
196 | queue_write = 0; | ||
197 | |||
198 | return 0; | ||
199 | } | ||
200 | |||
201 | |||
202 | /***************** Public implementation *****************/ | ||
203 | |||
204 | void talk_init(void) | ||
205 | { | ||
206 | has_voicefont = true; /* unless we fail later, assume we have one */ | ||
207 | talk_buffer_steal(); | ||
208 | queue_write = queue_read = 0; | ||
209 | } | ||
210 | |||
211 | |||
212 | /* somebody else claims the mp3 buffer, e.g. for regular play/record */ | ||
213 | int talk_buffer_steal(void) | ||
214 | { | ||
215 | p_voicefont = NULL; /* indicate no voicefont (trashed) */ | ||
216 | p_thumbnail = mp3buf; /* whole space for thumbnail */ | ||
217 | size_for_thumbnail = mp3end - mp3buf; | ||
218 | return 0; | ||
219 | } | ||
220 | |||
221 | |||
222 | /* play a voice ID from voicefont */ | ||
223 | int talk_id(int id, bool enqueue) | ||
224 | { | ||
225 | int clipsize; | ||
226 | unsigned char* clipbuf; | ||
227 | int unit; | ||
228 | |||
229 | if (mpeg_status()) /* busy, buffer in use */ | ||
230 | return -1; | ||
231 | |||
232 | if (p_voicefont == NULL && has_voicefont) | ||
233 | load_voicefont(); /* reload needed */ | ||
234 | |||
235 | if (p_voicefont == NULL) /* still no voices? */ | ||
236 | return -1; | ||
237 | |||
238 | /* check if this is a special ID, with a value */ | ||
239 | unit = ((unsigned)id) >> UNIT_SHIFT; | ||
240 | if (id != -1 && unit) | ||
241 | { /* sign-extend the value */ | ||
242 | //splash(200, true,"unit=%d", unit); | ||
243 | id = (unsigned)id << (32-UNIT_SHIFT); | ||
244 | id >>= (32-UNIT_SHIFT); | ||
245 | talk_value(id, unit, enqueue); /* speak it */ | ||
246 | return 0; /* and stop, end of special case */ | ||
247 | } | ||
248 | |||
249 | if (id < 0 || id >= p_voicefont->id_max) | ||
250 | return -1; | ||
251 | |||
252 | clipsize = p_voicefont->index[id].size; | ||
253 | if (clipsize == 0) /* clip not included in voicefont */ | ||
254 | return -1; | ||
255 | |||
256 | clipbuf = mp3buf + p_voicefont->index[id].offset; | ||
257 | |||
258 | queue_clip(clipbuf, clipsize, enqueue); | ||
259 | |||
260 | return 0; | ||
261 | } | ||
262 | |||
263 | |||
264 | /* play a thumbnail from file */ | ||
265 | int talk_file(char* filename, bool enqueue) | ||
266 | { | ||
267 | int fd; | ||
268 | int size; | ||
269 | |||
270 | if (mpeg_status()) /* busy, buffer in use */ | ||
271 | return -1; | ||
272 | |||
273 | if (p_thumbnail == NULL || size_for_thumbnail <= 0) | ||
274 | return -1; | ||
275 | |||
276 | fd = open(filename, O_RDONLY); | ||
277 | if (fd < 0) /* failed to open */ | ||
278 | { | ||
279 | return 0; | ||
280 | } | ||
281 | |||
282 | size = read(fd, p_thumbnail, size_for_thumbnail); | ||
283 | close(fd); | ||
284 | |||
285 | /* ToDo: find audio, skip ID headers and trailers */ | ||
286 | |||
287 | if (size) | ||
288 | { | ||
289 | bitswap(p_thumbnail, size); | ||
290 | queue_clip(p_thumbnail, size, enqueue); | ||
291 | } | ||
292 | |||
293 | return size; | ||
294 | } | ||
295 | |||
296 | |||
297 | /* say a numeric value, this works for english, | ||
298 | but not necessarily for other languages */ | ||
299 | int talk_number(int n, bool enqueue) | ||
300 | { | ||
301 | int level = 0; // mille count | ||
302 | int mil = 1000000000; // highest possible "-illion" | ||
303 | |||
304 | if (!enqueue) | ||
305 | shutup(); /* cut off all the pending stuff */ | ||
306 | |||
307 | if (n==0) | ||
308 | { // special case | ||
309 | talk_id(VOICE_ZERO, true); | ||
310 | return 0; | ||
311 | } | ||
312 | |||
313 | if (n<0) | ||
314 | { | ||
315 | talk_id(VOICE_MINUS, true); | ||
316 | n = -n; | ||
317 | } | ||
318 | |||
319 | while (n) | ||
320 | { | ||
321 | int segment = n / mil; // extract in groups of 3 digits | ||
322 | n -= segment * mil; // remove the used digits from number | ||
323 | mil /= 1000; // digit place for next round | ||
324 | |||
325 | if (segment) | ||
326 | { | ||
327 | int hundreds = segment / 100; | ||
328 | int ones = segment % 100; | ||
329 | |||
330 | if (hundreds) | ||
331 | { | ||
332 | talk_id(VOICE_ZERO + hundreds, true); | ||
333 | talk_id(VOICE_HUNDRED, true); | ||
334 | } | ||
335 | |||
336 | // combination indexing | ||
337 | if (ones > 20) | ||
338 | { | ||
339 | int tens = ones/10 + 18; | ||
340 | talk_id(VOICE_ZERO + tens, true); | ||
341 | ones %= 10; | ||
342 | } | ||
343 | |||
344 | // direct indexing | ||
345 | if (ones) | ||
346 | talk_id(VOICE_ZERO + ones, true); | ||
347 | |||
348 | // add billion, million, thousand | ||
349 | if (mil) | ||
350 | talk_id(VOICE_BILLION + level, true); | ||
351 | } | ||
352 | level++; | ||
353 | } | ||
354 | |||
355 | return 0; | ||
356 | } | ||
357 | |||
358 | int talk_value(int n, int unit, bool enqueue) | ||
359 | { | ||
360 | int unit_id; | ||
361 | const int unit_voiced[] = | ||
362 | { /* lookup table for the voice ID of the units */ | ||
363 | -1, -1, -1, /* regular ID, int, signed */ | ||
364 | VOICE_MILLISECONDS, /* here come the "real" units */ | ||
365 | VOICE_SECONDS, | ||
366 | VOICE_MINUTES, | ||
367 | VOICE_HOURS, | ||
368 | VOICE_KHZ, | ||
369 | VOICE_DB, | ||
370 | VOICE_PERCENT, | ||
371 | VOICE_MEGABYTE, | ||
372 | VOICE_GIGABYTE | ||
373 | }; | ||
374 | |||
375 | if (unit < 0 || unit >= UNIT_LAST) | ||
376 | unit_id = -1; | ||
377 | else | ||
378 | unit_id = unit_voiced[unit]; | ||
379 | |||
380 | if ((n==1 || n==-1) // singular? | ||
381 | && unit_id >= VOICE_SECONDS && unit_id <= VOICE_HOURS) | ||
382 | { | ||
383 | unit_id--; /* use the singular for those units which have */ | ||
384 | } | ||
385 | |||
386 | /* special case with a "plus" before */ | ||
387 | if (n > 0 && (unit == UNIT_SIGNED || unit == UNIT_DB)) | ||
388 | { | ||
389 | talk_id(VOICE_PLUS, enqueue); | ||
390 | enqueue = true; | ||
391 | } | ||
392 | |||
393 | talk_number(n, enqueue); /* say the number */ | ||
394 | talk_id(unit_id, true); /* say the unit, if any */ | ||
395 | |||
396 | return 0; | ||
397 | } | ||
398 | |||