diff options
author | Jörg Hohensohn <hohensoh@rockbox.org> | 2004-05-09 09:41:23 +0000 |
---|---|---|
committer | Jörg Hohensohn <hohensoh@rockbox.org> | 2004-05-09 09:41:23 +0000 |
commit | 2fef5b7b4a3ab4224de48a5c32b6fb1ae27b5266 (patch) | |
tree | 0c6362816f0bf379253b47ce8a589d15325cf3a9 /apps/talk.c | |
parent | 30c338a4c156bbe7b26fe7942dbd789518fd7818 (diff) | |
download | rockbox-2fef5b7b4a3ab4224de48a5c32b6fb1ae27b5266.tar.gz rockbox-2fef5b7b4a3ab4224de48a5c32b6fb1ae27b5266.zip |
While searching the voice crash like a madman, I made this module a lot more safe, so I can as well commit that.
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@4598 a1c6a512-1295-4272-9138-f99709370657
Diffstat (limited to 'apps/talk.c')
-rw-r--r-- | apps/talk.c | 202 |
1 files changed, 110 insertions, 92 deletions
diff --git a/apps/talk.c b/apps/talk.c index df5c6ef438..2fe3536ead 100644 --- a/apps/talk.c +++ b/apps/talk.c | |||
@@ -11,7 +11,7 @@ | |||
11 | * | 11 | * |
12 | * This module collects the Talkbox and voice UI functions. | 12 | * This module collects the Talkbox and voice UI functions. |
13 | * (Talkbox reads directory names from mp3 clips called thumbnails, | 13 | * (Talkbox reads directory names from mp3 clips called thumbnails, |
14 | * the voice UI lets menus and screens "talk" from a voicefont in memory. | 14 | * the voice UI lets menus and screens "talk" from a voicefile in memory. |
15 | * | 15 | * |
16 | * All files in this archive are subject to the GNU General Public License. | 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. | 17 | * See the file COPYING in the source tree root for full license agreement. |
@@ -36,21 +36,26 @@ extern void bitswap(unsigned char *data, int length); /* no header for this */ | |||
36 | 36 | ||
37 | /***************** Constants *****************/ | 37 | /***************** Constants *****************/ |
38 | 38 | ||
39 | #define QUEUE_SIZE 50 | 39 | #define QUEUE_SIZE 64 /* must be a power of two */ |
40 | #define QUEUE_MASK (QUEUE_SIZE-1) | ||
40 | const char* dir_thumbnail_name = ".dirname.tbx"; | 41 | const char* dir_thumbnail_name = ".dirname.tbx"; |
41 | 42 | ||
43 | /***************** Functional Macros *****************/ | ||
44 | |||
45 | #define QUEUE_LEVEL ((queue_write - queue_read) & QUEUE_MASK) | ||
46 | |||
42 | 47 | ||
43 | /***************** Data types *****************/ | 48 | /***************** Data types *****************/ |
44 | 49 | ||
45 | struct clip_entry /* one entry of the index table */ | 50 | struct clip_entry /* one entry of the index table */ |
46 | { | 51 | { |
47 | int offset; /* offset from start of voicefont file */ | 52 | int offset; /* offset from start of voicefile file */ |
48 | int size; /* size of the clip */ | 53 | int size; /* size of the clip */ |
49 | }; | 54 | }; |
50 | 55 | ||
51 | struct voicefont /* file format of our "voicefont" */ | 56 | struct voicefile /* file format of our voice file */ |
52 | { | 57 | { |
53 | int version; /* version of the voicefont */ | 58 | int version; /* version of the voicefile */ |
54 | int table; /* offset to index table, (=header size) */ | 59 | int table; /* offset to index table, (=header size) */ |
55 | int id1_max; /* number of "normal" clips contained in above index */ | 60 | int id1_max; /* number of "normal" clips contained in above index */ |
56 | int id2_max; /* number of "voice only" clips contained in above index */ | 61 | int id2_max; /* number of "voice only" clips contained in above index */ |
@@ -58,7 +63,6 @@ struct voicefont /* file format of our "voicefont" */ | |||
58 | /* and finally the bitswapped mp3 clips, not visible here */ | 63 | /* and finally the bitswapped mp3 clips, not visible here */ |
59 | }; | 64 | }; |
60 | 65 | ||
61 | |||
62 | struct queue_entry /* one entry of the internal queue */ | 66 | struct queue_entry /* one entry of the internal queue */ |
63 | { | 67 | { |
64 | unsigned char* buf; | 68 | unsigned char* buf; |
@@ -66,23 +70,22 @@ struct queue_entry /* one entry of the internal queue */ | |||
66 | }; | 70 | }; |
67 | 71 | ||
68 | 72 | ||
69 | |||
70 | /***************** Globals *****************/ | 73 | /***************** Globals *****************/ |
71 | 74 | ||
72 | static unsigned char* p_thumbnail; /* buffer for thumbnail */ | 75 | static unsigned char* p_thumbnail; /* buffer for thumbnail */ |
73 | static long size_for_thumbnail; /* leftover buffer size for it */ | 76 | static long size_for_thumbnail; /* leftover buffer size for it */ |
74 | static struct voicefont* p_voicefont; /* loaded voicefont */ | 77 | static struct voicefile* p_voicefile; /* loaded voicefile */ |
75 | static bool has_voicefont; /* a voicefont file is present */ | 78 | static bool has_voicefile; /* a voicefile file is present */ |
76 | static bool is_playing; /* we're currently playing */ | ||
77 | static struct queue_entry queue[QUEUE_SIZE]; /* queue of scheduled clips */ | 79 | static struct queue_entry queue[QUEUE_SIZE]; /* queue of scheduled clips */ |
78 | static int queue_write; /* write index of queue, by application */ | 80 | static int queue_write; /* write index of queue, by application */ |
79 | static int queue_read; /* read index of queue, by ISR context */ | 81 | static int queue_read; /* read index of queue, by ISR context */ |
82 | static int sent; /* how many bytes handed over to playback, owned by ISR */ | ||
80 | static unsigned char curr_hd[3]; /* current frame header, for re-sync */ | 83 | static unsigned char curr_hd[3]; /* current frame header, for re-sync */ |
81 | 84 | ||
82 | 85 | ||
83 | /***************** Private prototypes *****************/ | 86 | /***************** Private prototypes *****************/ |
84 | 87 | ||
85 | static int load_voicefont(void); | 88 | static int load_voicefile(void); |
86 | static void mp3_callback(unsigned char** start, int* size); | 89 | static void mp3_callback(unsigned char** start, int* size); |
87 | static int shutup(void); | 90 | static int shutup(void); |
88 | static int queue_clip(unsigned char* buf, int size, bool enqueue); | 91 | static int queue_clip(unsigned char* buf, int size, bool enqueue); |
@@ -108,28 +111,29 @@ static int open_voicefile(void) | |||
108 | } | 111 | } |
109 | 112 | ||
110 | 113 | ||
111 | 114 | /* load the voice file into the mp3 buffer */ | |
112 | static int load_voicefont(void) | 115 | static int load_voicefile(void) |
113 | { | 116 | { |
114 | int fd; | 117 | int fd; |
115 | int size; | 118 | int size; |
116 | 119 | ||
117 | p_voicefont = NULL; /* indicate no voicefont if we fail below */ | 120 | p_voicefile = NULL; /* indicate no voicefile if we fail below */ |
118 | 121 | ||
119 | fd = open_voicefile(); | 122 | fd = open_voicefile(); |
120 | if (fd < 0) /* failed to open */ | 123 | if (fd < 0) /* failed to open */ |
121 | { | 124 | { |
122 | p_voicefont = NULL; /* indicate no voicefont */ | 125 | p_voicefile = NULL; /* indicate no voicefile */ |
123 | has_voicefont = false; /* don't try again */ | 126 | has_voicefile = false; /* don't try again */ |
124 | return 0; | 127 | return 0; |
125 | } | 128 | } |
126 | 129 | ||
127 | size = read(fd, mp3buf, mp3end - mp3buf); | 130 | size = read(fd, mp3buf, mp3end - mp3buf); |
128 | if (size > 1000 | 131 | if (size > 10000 /* too small is probably invalid */ |
129 | && ((struct voicefont*)mp3buf)->table | 132 | && size == filesize(fd) /* has to fit completely */ |
130 | == offsetof(struct voicefont, index)) | 133 | && ((struct voicefile*)mp3buf)->table /* format check */ |
134 | == offsetof(struct voicefile, index)) | ||
131 | { | 135 | { |
132 | p_voicefont = (struct voicefont*)mp3buf; | 136 | p_voicefile = (struct voicefile*)mp3buf; |
133 | 137 | ||
134 | /* thumbnail buffer is the remaining space behind */ | 138 | /* thumbnail buffer is the remaining space behind */ |
135 | p_thumbnail = mp3buf + size; | 139 | p_thumbnail = mp3buf + size; |
@@ -138,7 +142,7 @@ static int load_voicefont(void) | |||
138 | } | 142 | } |
139 | else | 143 | else |
140 | { | 144 | { |
141 | has_voicefont = false; /* don't try again */ | 145 | has_voicefile = false; /* don't try again */ |
142 | } | 146 | } |
143 | close(fd); | 147 | close(fd); |
144 | 148 | ||
@@ -149,15 +153,14 @@ static int load_voicefont(void) | |||
149 | /* called in ISR context if mp3 data got consumed */ | 153 | /* called in ISR context if mp3 data got consumed */ |
150 | static void mp3_callback(unsigned char** start, int* size) | 154 | static void mp3_callback(unsigned char** start, int* size) |
151 | { | 155 | { |
152 | int play_now; | 156 | queue[queue_read].len -= sent; /* we completed this */ |
157 | queue[queue_read].buf += sent; | ||
153 | 158 | ||
154 | if (queue[queue_read].len > 0) /* current clip not finished? */ | 159 | if (queue[queue_read].len > 0) /* current clip not finished? */ |
155 | { /* feed the next 64K-1 chunk */ | 160 | { /* feed the next 64K-1 chunk */ |
156 | play_now = MIN(queue[queue_read].len, 0xFFFF); | 161 | sent = MIN(queue[queue_read].len, 0xFFFF); |
157 | *start = queue[queue_read].buf; | 162 | *start = queue[queue_read].buf; |
158 | *size = play_now; | 163 | *size = sent; |
159 | queue[queue_read].buf += play_now; | ||
160 | queue[queue_read].len -= play_now; | ||
161 | return; | 164 | return; |
162 | } | 165 | } |
163 | else /* go to next entry */ | 166 | else /* go to next entry */ |
@@ -167,21 +170,18 @@ static void mp3_callback(unsigned char** start, int* size) | |||
167 | queue_read = 0; | 170 | queue_read = 0; |
168 | } | 171 | } |
169 | 172 | ||
170 | if (queue_read != queue_write) /* queue is not empty? */ | 173 | if (QUEUE_LEVEL) /* queue is not empty? */ |
171 | { /* start next clip */ | 174 | { /* start next clip */ |
172 | play_now = MIN(queue[queue_read].len, 0xFFFF); | 175 | sent = MIN(queue[queue_read].len, 0xFFFF); |
173 | *start = queue[queue_read].buf; | 176 | *start = queue[queue_read].buf; |
174 | *size = play_now; | 177 | *size = sent; |
175 | curr_hd[0] = *start[1]; | 178 | curr_hd[0] = *start[1]; |
176 | curr_hd[1] = *start[2]; | 179 | curr_hd[1] = *start[2]; |
177 | curr_hd[2] = *start[3]; | 180 | curr_hd[2] = *start[3]; |
178 | queue[queue_read].buf += play_now; | ||
179 | queue[queue_read].len -= play_now; | ||
180 | } | 181 | } |
181 | else | 182 | else |
182 | { | 183 | { |
183 | *size = 0; /* end of data */ | 184 | *size = 0; /* end of data */ |
184 | is_playing = false; | ||
185 | mp3_play_stop(); /* fixme: should be done by caller */ | 185 | mp3_play_stop(); /* fixme: should be done by caller */ |
186 | } | 186 | } |
187 | } | 187 | } |
@@ -193,49 +193,55 @@ static int shutup(void) | |||
193 | unsigned char* search; | 193 | unsigned char* search; |
194 | unsigned char* end; | 194 | unsigned char* end; |
195 | 195 | ||
196 | if (!is_playing) /* has ended anyway */ | 196 | if (QUEUE_LEVEL == 0) /* has ended anyway */ |
197 | { | ||
197 | return 0; | 198 | return 0; |
199 | } | ||
198 | 200 | ||
199 | CHCR3 &= ~0x0001; /* disable the DMA */ | 201 | CHCR3 &= ~0x0001; /* disable the DMA (and therefore the interrupt also) */ |
200 | 202 | ||
201 | /* search next frame boundary and continue up to there */ | 203 | /* search next frame boundary and continue up to there */ |
202 | pos = search = mp3_get_pos(); | 204 | pos = search = mp3_get_pos(); |
203 | end = queue[queue_read].buf + queue[queue_read].len; | 205 | end = queue[queue_read].buf + queue[queue_read].len; |
204 | 206 | ||
205 | /* Find the next frame boundary */ | 207 | if (pos >= queue[queue_read].buf |
206 | while (search < end) /* search the remaining data */ | 208 | && pos <= end) /* really our clip? */ |
207 | { | 209 | { /* (for strange reasons this isn't nesessarily the case) */ |
208 | if (*search++ != 0xFF) /* quick search for frame sync byte */ | 210 | /* find the next frame boundary */ |
209 | continue; /* (this does the majority of the job) */ | 211 | while (search < end) /* search the remaining data */ |
210 | |||
211 | /* look at the (bitswapped) rest of header candidate */ | ||
212 | if (search[0] == curr_hd[0] /* do the quicker checks first */ | ||
213 | && search[2] == curr_hd[2] | ||
214 | && (search[1] & 0x30) == (curr_hd[1] & 0x30)) /* sample rate */ | ||
215 | { | 212 | { |
216 | search--; /* back to the sync byte */ | 213 | if (*search++ != 0xFF) /* quick search for frame sync byte */ |
217 | break; /* From looking at it, this is our header. */ | 214 | continue; /* (this does the majority of the job) */ |
215 | |||
216 | /* look at the (bitswapped) rest of header candidate */ | ||
217 | if (search[0] == curr_hd[0] /* do the quicker checks first */ | ||
218 | && search[2] == curr_hd[2] | ||
219 | && (search[1] & 0x30) == (curr_hd[1] & 0x30)) /* sample rate */ | ||
220 | { | ||
221 | search--; /* back to the sync byte */ | ||
222 | break; /* From looking at it, this is our header. */ | ||
223 | } | ||
218 | } | 224 | } |
219 | } | ||
220 | 225 | ||
221 | if (search-pos) | 226 | if (search-pos) |
222 | { /* play old data until the frame end, to keep the MAS in sync */ | 227 | { /* play old data until the frame end, to keep the MAS in sync */ |
223 | DTCR3 = search-pos; | 228 | sent = search-pos; |
224 | 229 | ||
225 | queue_write = queue_read + 1; /* will be empty after next callback */ | 230 | queue_write = queue_read + 1; /* will be empty after next callback */ |
226 | if (queue_write >= QUEUE_SIZE) | 231 | if (queue_write >= QUEUE_SIZE) |
227 | queue_write = 0; | 232 | queue_write = 0; |
228 | queue[queue_read].len = 0; /* current one ends now */ | 233 | queue[queue_read].len = sent; /* current one ends after this */ |
229 | 234 | ||
230 | CHCR3 |= 0x0001; /* re-enable DMA */ | 235 | DTCR3 = sent; /* let the DMA finish this frame */ |
231 | } | 236 | CHCR3 |= 0x0001; /* re-enable DMA */ |
232 | else | 237 | return 0; |
233 | { /* by chance we have played to a frame boundary */ | 238 | } |
234 | queue_write = queue_read; /* reset the queue */ | ||
235 | is_playing = false; | ||
236 | mp3_play_stop(); | ||
237 | } | 239 | } |
238 | 240 | ||
241 | /* nothing to do, was frame boundary or not our clip */ | ||
242 | mp3_play_stop(); | ||
243 | queue_write = queue_read = 0; /* reset the queue */ | ||
244 | |||
239 | return 0; | 245 | return 0; |
240 | } | 246 | } |
241 | 247 | ||
@@ -243,30 +249,42 @@ static int shutup(void) | |||
243 | /* schedule a clip, at the end or discard the existing queue */ | 249 | /* schedule a clip, at the end or discard the existing queue */ |
244 | static int queue_clip(unsigned char* buf, int size, bool enqueue) | 250 | static int queue_clip(unsigned char* buf, int size, bool enqueue) |
245 | { | 251 | { |
252 | int queue_level; | ||
253 | |||
246 | if (!enqueue) | 254 | if (!enqueue) |
247 | shutup(); /* cut off all the pending stuff */ | 255 | shutup(); /* cut off all the pending stuff */ |
248 | 256 | ||
249 | queue[queue_write].buf = buf; | 257 | if (!size) |
250 | queue[queue_write].len = size; | 258 | return 0; /* safety check */ |
259 | |||
260 | /* disable the DMA temporarily, to be safe of race condition */ | ||
261 | CHCR3 &= ~0x0001; | ||
262 | |||
263 | queue_level = QUEUE_LEVEL; /* check old level */ | ||
264 | |||
265 | if (queue_level < QUEUE_SIZE - 1) /* space left? */ | ||
266 | { | ||
267 | queue[queue_write].buf = buf; /* populate an entry */ | ||
268 | queue[queue_write].len = size; | ||
251 | 269 | ||
252 | /* FixMe: make this IRQ-safe */ | 270 | queue_write++; /* increase queue */ |
271 | if (queue_write >= QUEUE_SIZE) | ||
272 | queue_write = 0; | ||
273 | } | ||
253 | 274 | ||
254 | if (!is_playing) | 275 | if (queue_level == 0) |
255 | { /* queue empty, we have to do the initial start */ | 276 | { /* queue was empty, we have to do the initial start */ |
256 | int size_now = MIN(size, 0xFFFF); /* DMA can do no more */ | 277 | sent = MIN(size, 0xFFFF); /* DMA can do no more */ |
257 | is_playing = true; | 278 | mp3_play_data(buf, sent, mp3_callback); |
258 | mp3_play_data(buf, size_now, mp3_callback); | ||
259 | curr_hd[0] = buf[1]; | 279 | curr_hd[0] = buf[1]; |
260 | curr_hd[1] = buf[2]; | 280 | curr_hd[1] = buf[2]; |
261 | curr_hd[2] = buf[3]; | 281 | curr_hd[2] = buf[3]; |
262 | mp3_play_pause(true); /* kickoff audio */ | 282 | mp3_play_pause(true); /* kickoff audio */ |
263 | queue[queue_write].buf += size_now; | ||
264 | queue[queue_write].len -= size_now; | ||
265 | } | 283 | } |
266 | 284 | else | |
267 | queue_write++; | 285 | { |
268 | if (queue_write >= QUEUE_SIZE) | 286 | CHCR3 |= 0x0001; /* re-enable DMA */ |
269 | queue_write = 0; | 287 | } |
270 | 288 | ||
271 | return 0; | 289 | return 0; |
272 | } | 290 | } |
@@ -282,29 +300,30 @@ void talk_init(void) | |||
282 | if (fd >= 0) /* success */ | 300 | if (fd >= 0) /* success */ |
283 | { | 301 | { |
284 | close(fd); | 302 | close(fd); |
285 | has_voicefont = true; | 303 | has_voicefile = true; |
286 | } | 304 | } |
287 | else | 305 | else |
288 | { | 306 | { |
289 | has_voicefont = false; /* no voice file available */ | 307 | has_voicefile = false; /* no voice file available */ |
290 | } | 308 | } |
291 | 309 | ||
292 | talk_buffer_steal(); /* abuse this for most of our inits */ | 310 | talk_buffer_steal(); /* abuse this for most of our inits */ |
293 | queue_write = queue_read = 0; | ||
294 | } | 311 | } |
295 | 312 | ||
296 | 313 | ||
297 | /* somebody else claims the mp3 buffer, e.g. for regular play/record */ | 314 | /* somebody else claims the mp3 buffer, e.g. for regular play/record */ |
298 | int talk_buffer_steal(void) | 315 | int talk_buffer_steal(void) |
299 | { | 316 | { |
300 | p_voicefont = NULL; /* indicate no voicefont (trashed) */ | 317 | mp3_play_stop(); |
318 | queue_write = queue_read = 0; /* reset the queue */ | ||
319 | p_voicefile = NULL; /* indicate no voicefile (trashed) */ | ||
301 | p_thumbnail = mp3buf; /* whole space for thumbnail */ | 320 | p_thumbnail = mp3buf; /* whole space for thumbnail */ |
302 | size_for_thumbnail = mp3end - mp3buf; | 321 | size_for_thumbnail = mp3end - mp3buf; |
303 | return 0; | 322 | return 0; |
304 | } | 323 | } |
305 | 324 | ||
306 | 325 | ||
307 | /* play a voice ID from voicefont */ | 326 | /* play a voice ID from voicefile */ |
308 | int talk_id(int id, bool enqueue) | 327 | int talk_id(int id, bool enqueue) |
309 | { | 328 | { |
310 | int clipsize; | 329 | int clipsize; |
@@ -314,10 +333,10 @@ int talk_id(int id, bool enqueue) | |||
314 | if (mpeg_status()) /* busy, buffer in use */ | 333 | if (mpeg_status()) /* busy, buffer in use */ |
315 | return -1; | 334 | return -1; |
316 | 335 | ||
317 | if (p_voicefont == NULL && has_voicefont) | 336 | if (p_voicefile == NULL && has_voicefile) |
318 | load_voicefont(); /* reload needed */ | 337 | load_voicefile(); /* reload needed */ |
319 | 338 | ||
320 | if (p_voicefont == NULL) /* still no voices? */ | 339 | if (p_voicefile == NULL) /* still no voices? */ |
321 | return -1; | 340 | return -1; |
322 | 341 | ||
323 | if (id == -1) /* -1 is an indication for silence */ | 342 | if (id == -1) /* -1 is an indication for silence */ |
@@ -327,7 +346,6 @@ int talk_id(int id, bool enqueue) | |||
327 | unit = ((unsigned)id) >> UNIT_SHIFT; | 346 | unit = ((unsigned)id) >> UNIT_SHIFT; |
328 | if (unit) | 347 | if (unit) |
329 | { /* sign-extend the value */ | 348 | { /* sign-extend the value */ |
330 | //splash(200, true,"unit=%d", unit); | ||
331 | id = (unsigned)id << (32-UNIT_SHIFT); | 349 | id = (unsigned)id << (32-UNIT_SHIFT); |
332 | id >>= (32-UNIT_SHIFT); | 350 | id >>= (32-UNIT_SHIFT); |
333 | talk_value(id, unit, enqueue); /* speak it */ | 351 | talk_value(id, unit, enqueue); /* speak it */ |
@@ -337,21 +355,21 @@ int talk_id(int id, bool enqueue) | |||
337 | if (id > VOICEONLY_DELIMITER) | 355 | if (id > VOICEONLY_DELIMITER) |
338 | { /* voice-only entries use the second part of the table */ | 356 | { /* voice-only entries use the second part of the table */ |
339 | id -= VOICEONLY_DELIMITER + 1; | 357 | id -= VOICEONLY_DELIMITER + 1; |
340 | if (id >= p_voicefont->id2_max) | 358 | if (id >= p_voicefile->id2_max) |
341 | return -1; /* must be newer than we have */ | 359 | return -1; /* must be newer than we have */ |
342 | id += p_voicefont->id1_max; /* table 2 is behind table 1 */ | 360 | id += p_voicefile->id1_max; /* table 2 is behind table 1 */ |
343 | } | 361 | } |
344 | else | 362 | else |
345 | { /* normal use of the first table */ | 363 | { /* normal use of the first table */ |
346 | if (id >= p_voicefont->id1_max) | 364 | if (id >= p_voicefile->id1_max) |
347 | return -1; /* must be newer than we have */ | 365 | return -1; /* must be newer than we have */ |
348 | } | 366 | } |
349 | 367 | ||
350 | clipsize = p_voicefont->index[id].size; | 368 | clipsize = p_voicefile->index[id].size; |
351 | if (clipsize == 0) /* clip not included in voicefont */ | 369 | if (clipsize == 0) /* clip not included in voicefile */ |
352 | return -1; | 370 | return -1; |
353 | 371 | ||
354 | clipbuf = mp3buf + p_voicefont->index[id].offset; | 372 | clipbuf = mp3buf + p_voicefile->index[id].offset; |
355 | 373 | ||
356 | queue_clip(clipbuf, clipsize, enqueue); | 374 | queue_clip(clipbuf, clipsize, enqueue); |
357 | 375 | ||