diff options
Diffstat (limited to 'apps/gui/pitchscreen.c')
-rw-r--r-- | apps/gui/pitchscreen.c | 1055 |
1 files changed, 2 insertions, 1053 deletions
diff --git a/apps/gui/pitchscreen.c b/apps/gui/pitchscreen.c index 871921a10f..9f42aedb5d 100644 --- a/apps/gui/pitchscreen.c +++ b/apps/gui/pitchscreen.c | |||
@@ -18,1059 +18,8 @@ | |||
18 | * KIND, either express or implied. | 18 | * KIND, either express or implied. |
19 | * | 19 | * |
20 | ****************************************************************************/ | 20 | ****************************************************************************/ |
21 | 21 | #include "plugin.h" | |
22 | #include <stdbool.h> | ||
23 | #include <string.h> | ||
24 | #include <stdio.h> | ||
25 | #include <math.h> | ||
26 | #include <stdlib.h> /* for abs() */ | ||
27 | #include "config.h" | ||
28 | #include "action.h" | ||
29 | #include "sound.h" | ||
30 | #include "pcmbuf.h" | ||
31 | #include "lang.h" | ||
32 | #include "icons.h" | ||
33 | #include "screens.h" | ||
34 | #include "talk.h" | ||
35 | #include "viewport.h" | ||
36 | #include "font.h" | ||
37 | #include "system.h" | ||
38 | #include "misc.h" | ||
39 | #include "pitchscreen.h" | ||
40 | #include "settings.h" | ||
41 | #include "tdspeed.h" | ||
42 | |||
43 | #define ICON_BORDER 12 /* icons are currently 7x8, so add ~2 pixels */ | ||
44 | /* on both sides when drawing */ | ||
45 | |||
46 | #define PITCH_MAX (200 * PITCH_SPEED_PRECISION) | ||
47 | #define PITCH_MIN (50 * PITCH_SPEED_PRECISION) | ||
48 | #define PITCH_SMALL_DELTA (PITCH_SPEED_PRECISION / 10) /* .1% */ | ||
49 | #define PITCH_BIG_DELTA (PITCH_SPEED_PRECISION) /* 1% */ | ||
50 | #define PITCH_NUDGE_DELTA (2 * PITCH_SPEED_PRECISION) /* 2% */ | ||
51 | |||
52 | #define SPEED_SMALL_DELTA (PITCH_SPEED_PRECISION / 10) /* .1% */ | ||
53 | #define SPEED_BIG_DELTA (PITCH_SPEED_PRECISION) /* 1% */ | ||
54 | |||
55 | #define SEMITONE_SMALL_DELTA (PITCH_SPEED_PRECISION / 10) /* 10 cents */ | ||
56 | #define SEMITONE_BIG_DELTA PITCH_SPEED_PRECISION /* 1 semitone */ | ||
57 | |||
58 | enum | ||
59 | { | ||
60 | PITCH_TOP = 0, | ||
61 | PITCH_MID, | ||
62 | PITCH_BOTTOM, | ||
63 | PITCH_ITEM_COUNT, | ||
64 | }; | ||
65 | |||
66 | |||
67 | /* This is a table of semitone percentage values of the appropriate | ||
68 | precision (based on PITCH_SPEED_PRECISION). Note that these are | ||
69 | all constant expressions, which will be evaluated at compile time, | ||
70 | so no need to worry about how complex the expressions look. | ||
71 | That's just to get the precision right. | ||
72 | |||
73 | I calculated these values, starting from 50, as | ||
74 | |||
75 | x(n) = 50 * 2^(n/12) | ||
76 | |||
77 | All that math in each entry simply converts the float constant | ||
78 | to an integer equal to PITCH_SPEED_PRECISION times the float value, | ||
79 | with as little precision loss as possible (i.e. correctly rounding | ||
80 | the last digit). | ||
81 | */ | ||
82 | #define TO_INT_WITH_PRECISION(x) \ | ||
83 | ( (unsigned short)(((x) * PITCH_SPEED_PRECISION * 10 + 5) / 10) ) | ||
84 | |||
85 | static const unsigned short semitone_table[] = | ||
86 | { | ||
87 | TO_INT_WITH_PRECISION(50.00000000), /* Octave lower */ | ||
88 | TO_INT_WITH_PRECISION(52.97315472), | ||
89 | TO_INT_WITH_PRECISION(56.12310242), | ||
90 | TO_INT_WITH_PRECISION(59.46035575), | ||
91 | TO_INT_WITH_PRECISION(62.99605249), | ||
92 | TO_INT_WITH_PRECISION(66.74199271), | ||
93 | TO_INT_WITH_PRECISION(70.71067812), | ||
94 | TO_INT_WITH_PRECISION(74.91535384), | ||
95 | TO_INT_WITH_PRECISION(79.37005260), | ||
96 | TO_INT_WITH_PRECISION(84.08964153), | ||
97 | TO_INT_WITH_PRECISION(89.08987181), | ||
98 | TO_INT_WITH_PRECISION(94.38743127), | ||
99 | TO_INT_WITH_PRECISION(100.0000000), /* Normal sound */ | ||
100 | TO_INT_WITH_PRECISION(105.9463094), | ||
101 | TO_INT_WITH_PRECISION(112.2462048), | ||
102 | TO_INT_WITH_PRECISION(118.9207115), | ||
103 | TO_INT_WITH_PRECISION(125.9921049), | ||
104 | TO_INT_WITH_PRECISION(133.4839854), | ||
105 | TO_INT_WITH_PRECISION(141.4213562), | ||
106 | TO_INT_WITH_PRECISION(149.8307077), | ||
107 | TO_INT_WITH_PRECISION(158.7401052), | ||
108 | TO_INT_WITH_PRECISION(168.1792831), | ||
109 | TO_INT_WITH_PRECISION(178.1797436), | ||
110 | TO_INT_WITH_PRECISION(188.7748625), | ||
111 | TO_INT_WITH_PRECISION(200.0000000) /* Octave higher */ | ||
112 | }; | ||
113 | |||
114 | #define NUM_SEMITONES ((int)(sizeof(semitone_table)/sizeof(semitone_table[0]))) | ||
115 | #define SEMITONE_END (NUM_SEMITONES/2) | ||
116 | #define SEMITONE_START (-SEMITONE_END) | ||
117 | |||
118 | /* A table of values for approximating the cent curve with | ||
119 | linear interpolation. Multipy the next lowest semitone | ||
120 | by this much to find the corresponding cent percentage. | ||
121 | |||
122 | These values were calculated as | ||
123 | x(n) = 100 * 2^(n * 20/1200) | ||
124 | */ | ||
125 | |||
126 | static const unsigned short cent_interp[] = | ||
127 | { | ||
128 | TO_INT_WITH_PRECISION(100.0000000), | ||
129 | TO_INT_WITH_PRECISION(101.1619440), | ||
130 | TO_INT_WITH_PRECISION(102.3373892), | ||
131 | TO_INT_WITH_PRECISION(103.5264924), | ||
132 | TO_INT_WITH_PRECISION(104.7294123), | ||
133 | /* this one's the next semitone but we have it here for convenience */ | ||
134 | TO_INT_WITH_PRECISION(105.9463094), | ||
135 | }; | ||
136 | |||
137 | /* Number of cents between entries in the cent_interp table */ | ||
138 | #define CENT_INTERP_INTERVAL 20 | ||
139 | #define CENT_INTERP_NUM ((int)(sizeof(cent_interp)/sizeof(cent_interp[0]))) | ||
140 | |||
141 | /* This stores whether the pitch and speed are at their own limits */ | ||
142 | /* or that of the timestretching algorithm */ | ||
143 | static bool at_limit = false; | ||
144 | |||
145 | /* | ||
146 | * | ||
147 | * The pitchscreen is divided into 3 viewports (each row is a viewport) | ||
148 | * Then each viewport is again divided into 3 colums, each showsing some infos | ||
149 | * Additionally, on touchscreen, each cell represents a button | ||
150 | * | ||
151 | * Below a sketch describing what each cell will show (what's drawn on it) | ||
152 | * -------------------------- | ||
153 | * | | | | <-- pitch up in the middle (text and button) | ||
154 | * | | | | <-- arrows for mode toggling on the sides for touchscreen | ||
155 | * |------------------------| | ||
156 | * | | | | <-- semitone/speed up/down on the sides | ||
157 | * | | | | <-- reset pitch&speed in the middle | ||
158 | * |------------------------| | ||
159 | * | | | | <-- pitch down in the middle | ||
160 | * | | | | <-- Two "OK" for exit on the sides for touchscreen | ||
161 | * |------------------------| | ||
162 | * | ||
163 | * | ||
164 | */ | ||
165 | |||
166 | static void speak_pitch_mode(bool enqueue) | ||
167 | { | ||
168 | bool timestretch_mode = global_settings.pitch_mode_timestretch && dsp_timestretch_available(); | ||
169 | if (timestretch_mode) | ||
170 | talk_id(VOICE_PITCH_TIMESTRETCH_MODE, enqueue); | ||
171 | if (global_settings.pitch_mode_semitone) | ||
172 | talk_id(VOICE_PITCH_SEMITONE_MODE, timestretch_mode ? true : enqueue); | ||
173 | else | ||
174 | talk_id(VOICE_PITCH_ABSOLUTE_MODE, timestretch_mode ? true : enqueue); | ||
175 | return; | ||
176 | } | ||
177 | |||
178 | /* | ||
179 | * Fixes the viewports so they represent the 3 rows, and adds a little margin | ||
180 | * on all sides for the icons (which are drawn outside of the grid | ||
181 | * | ||
182 | * The modified viewports need to be passed to the touchscreen handling function | ||
183 | **/ | ||
184 | static void pitchscreen_fix_viewports(struct viewport *parent, | ||
185 | struct viewport pitch_viewports[PITCH_ITEM_COUNT]) | ||
186 | { | ||
187 | int i, font_height; | ||
188 | font_height = font_get(parent->font)->height; | ||
189 | for (i = 0; i < PITCH_ITEM_COUNT; i++) | ||
190 | { | ||
191 | pitch_viewports[i] = *parent; | ||
192 | pitch_viewports[i].height = parent->height / PITCH_ITEM_COUNT; | ||
193 | pitch_viewports[i].x += ICON_BORDER; | ||
194 | pitch_viewports[i].width -= 2*ICON_BORDER; | ||
195 | } | ||
196 | pitch_viewports[PITCH_TOP].y += ICON_BORDER; | ||
197 | pitch_viewports[PITCH_TOP].height -= ICON_BORDER; | ||
198 | |||
199 | if(pitch_viewports[PITCH_MID].height < font_height * 2) | ||
200 | pitch_viewports[PITCH_MID].height = font_height * 2; | ||
201 | |||
202 | pitch_viewports[PITCH_MID].y = pitch_viewports[PITCH_TOP].y | ||
203 | + pitch_viewports[PITCH_TOP].height; | ||
204 | |||
205 | pitch_viewports[PITCH_BOTTOM].y = pitch_viewports[PITCH_MID].y | ||
206 | + pitch_viewports[PITCH_MID].height; | ||
207 | |||
208 | pitch_viewports[PITCH_BOTTOM].height -= ICON_BORDER; | ||
209 | } | ||
210 | |||
211 | /* must be called before pitchscreen_draw, or within | ||
212 | * since it neither clears nor updates the display */ | ||
213 | static void pitchscreen_draw_icons(struct screen *display, | ||
214 | struct viewport *parent) | ||
215 | { | ||
216 | display->set_viewport(parent); | ||
217 | display->mono_bitmap(bitmap_icons_7x8[Icon_UpArrow], | ||
218 | parent->width/2 - 3, | ||
219 | 2, 7, 8); | ||
220 | display->mono_bitmap(bitmap_icons_7x8[Icon_DownArrow], | ||
221 | parent->width /2 - 3, | ||
222 | parent->height - 10, 7, 8); | ||
223 | display->mono_bitmap(bitmap_icons_7x8[Icon_FastForward], | ||
224 | parent->width - 10, | ||
225 | parent->height /2 - 4, 7, 8); | ||
226 | display->mono_bitmap(bitmap_icons_7x8[Icon_FastBackward], | ||
227 | 2, | ||
228 | parent->height /2 - 4, 7, 8); | ||
229 | display->update_viewport(); | ||
230 | } | ||
231 | |||
232 | static void pitchscreen_draw(struct screen *display, int max_lines, | ||
233 | struct viewport pitch_viewports[PITCH_ITEM_COUNT], | ||
234 | int32_t pitch, int32_t semitone | ||
235 | ,int32_t speed | ||
236 | ) | ||
237 | { | ||
238 | const char* ptr; | ||
239 | char buf[32]; | ||
240 | int w, h; | ||
241 | bool show_lang_pitch; | ||
242 | struct viewport *last_vp = NULL; | ||
243 | |||
244 | /* "Pitch up/Pitch down" - hide for a small screen, | ||
245 | * the text is drawn centered automatically | ||
246 | * | ||
247 | * note: this assumes 5 lines always fit on a touchscreen (should be | ||
248 | * reasonable) */ | ||
249 | if (max_lines >= 5) | ||
250 | { | ||
251 | int w, h; | ||
252 | struct viewport *vp = &pitch_viewports[PITCH_TOP]; | ||
253 | last_vp = display->set_viewport(vp); | ||
254 | display->clear_viewport(); | ||
255 | #ifdef HAVE_TOUCHSCREEN | ||
256 | /* two arrows in the top row, left and right column */ | ||
257 | char *arrows[] = { "<", ">"}; | ||
258 | display->getstringsize(arrows[0], &w, &h); | ||
259 | display->putsxy(0, vp->height/2 - h/2, arrows[0]); | ||
260 | display->putsxy(vp->width - w, vp->height/2 - h/2, arrows[1]); | ||
261 | #endif | ||
262 | /* UP: Pitch Up */ | ||
263 | if (global_settings.pitch_mode_semitone) | ||
264 | ptr = str(LANG_PITCH_UP_SEMITONE); | ||
265 | else | ||
266 | ptr = str(LANG_PITCH_UP); | ||
267 | |||
268 | display->getstringsize(ptr, &w, NULL); | ||
269 | /* draw text */ | ||
270 | display->putsxy(vp->width/2 - w/2, 0, ptr); | ||
271 | display->update_viewport(); | ||
272 | |||
273 | /* DOWN: Pitch Down */ | ||
274 | vp = &pitch_viewports[PITCH_BOTTOM]; | ||
275 | display->set_viewport(vp); | ||
276 | display->clear_viewport(); | ||
277 | |||
278 | #ifdef HAVE_TOUCHSCREEN | ||
279 | ptr = str(LANG_KBD_OK); | ||
280 | display->getstringsize(ptr, &w, &h); | ||
281 | /* one OK in the middle first column of the vp (at half height) */ | ||
282 | display->putsxy(vp->width/6 - w/2, vp->height/2 - h/2, ptr); | ||
283 | /* one OK in the middle of the last column of the vp (at half height) */ | ||
284 | display->putsxy(5*vp->width/6 - w/2, vp->height/2 - h/2, ptr); | ||
285 | #endif | ||
286 | if (global_settings.pitch_mode_semitone) | ||
287 | ptr = str(LANG_PITCH_DOWN_SEMITONE); | ||
288 | else | ||
289 | ptr = str(LANG_PITCH_DOWN); | ||
290 | display->getstringsize(ptr, &w, &h); | ||
291 | /* draw text */ | ||
292 | display->putsxy(vp->width/2 - w/2, vp->height - h, ptr); | ||
293 | display->update_viewport(); | ||
294 | } | ||
295 | |||
296 | /* Middle section */ | ||
297 | display->set_viewport(&pitch_viewports[PITCH_MID]); | ||
298 | display->clear_viewport(); | ||
299 | int width_used = 0; | ||
300 | |||
301 | /* Middle section upper line - hide for a small screen */ | ||
302 | if ((show_lang_pitch = (max_lines >= 3))) | ||
303 | { | ||
304 | if(global_settings.pitch_mode_timestretch) | ||
305 | { | ||
306 | /* Pitch:XXX.X% */ | ||
307 | if(global_settings.pitch_mode_semitone) | ||
308 | { | ||
309 | snprintf(buf, sizeof(buf), "%s: %s%d.%02d", str(LANG_PITCH), | ||
310 | semitone >= 0 ? "+" : "-", | ||
311 | abs(semitone / PITCH_SPEED_PRECISION), | ||
312 | abs((semitone % PITCH_SPEED_PRECISION) / | ||
313 | (PITCH_SPEED_PRECISION / 100)) | ||
314 | ); | ||
315 | } | ||
316 | else | ||
317 | { | ||
318 | snprintf(buf, sizeof(buf), "%s: %ld.%ld%%", str(LANG_PITCH), | ||
319 | pitch / PITCH_SPEED_PRECISION, | ||
320 | (pitch % PITCH_SPEED_PRECISION) / | ||
321 | (PITCH_SPEED_PRECISION / 10)); | ||
322 | } | ||
323 | } | ||
324 | else | ||
325 | { | ||
326 | /* Rate */ | ||
327 | snprintf(buf, sizeof(buf), "%s:", str(LANG_PLAYBACK_RATE)); | ||
328 | } | ||
329 | display->getstringsize(buf, &w, &h); | ||
330 | display->putsxy((pitch_viewports[PITCH_MID].width / 2) - (w / 2), | ||
331 | (pitch_viewports[PITCH_MID].height / 2) - h, buf); | ||
332 | if (w > width_used) | ||
333 | width_used = w; | ||
334 | } | ||
335 | |||
336 | /* Middle section lower line */ | ||
337 | /* "Speed:XXX%" */ | ||
338 | if(global_settings.pitch_mode_timestretch) | ||
339 | { | ||
340 | snprintf(buf, sizeof(buf), "%s: %ld.%ld%%", str(LANG_SPEED), | ||
341 | speed / PITCH_SPEED_PRECISION, | ||
342 | (speed % PITCH_SPEED_PRECISION) / (PITCH_SPEED_PRECISION / 10)); | ||
343 | } | ||
344 | else | ||
345 | { | ||
346 | if(global_settings.pitch_mode_semitone) | ||
347 | { | ||
348 | snprintf(buf, sizeof(buf), "%s%d.%02d", | ||
349 | semitone >= 0 ? "+" : "-", | ||
350 | abs(semitone / PITCH_SPEED_PRECISION), | ||
351 | abs((semitone % PITCH_SPEED_PRECISION) / | ||
352 | (PITCH_SPEED_PRECISION / 100)) | ||
353 | ); | ||
354 | } | ||
355 | else | ||
356 | { | ||
357 | snprintf(buf, sizeof(buf), "%ld.%ld%%", | ||
358 | pitch / PITCH_SPEED_PRECISION, | ||
359 | (pitch % PITCH_SPEED_PRECISION) / (PITCH_SPEED_PRECISION / 10)); | ||
360 | } | ||
361 | } | ||
362 | |||
363 | display->getstringsize(buf, &w, &h); | ||
364 | display->putsxy((pitch_viewports[PITCH_MID].width / 2) - (w / 2), | ||
365 | show_lang_pitch ? (pitch_viewports[PITCH_MID].height / 2) : | ||
366 | (pitch_viewports[PITCH_MID].height / 2) - (h / 2), | ||
367 | buf); | ||
368 | if (w > width_used) | ||
369 | width_used = w; | ||
370 | |||
371 | /* "limit" and "timestretch" labels */ | ||
372 | if (max_lines >= 7) | ||
373 | { | ||
374 | if(at_limit) | ||
375 | { | ||
376 | const char * const p = str(LANG_STRETCH_LIMIT); | ||
377 | display->getstringsize(p, &w, &h); | ||
378 | display->putsxy((pitch_viewports[PITCH_MID].width / 2) - (w / 2), | ||
379 | (pitch_viewports[PITCH_MID].height / 2) + h, p); | ||
380 | if (w > width_used) | ||
381 | width_used = w; | ||
382 | } | ||
383 | } | ||
384 | |||
385 | /* Middle section left/right labels */ | ||
386 | const char *leftlabel = "-2%"; | ||
387 | const char *rightlabel = "+2%"; | ||
388 | if (global_settings.pitch_mode_timestretch) | ||
389 | { | ||
390 | leftlabel = "<<"; | ||
391 | rightlabel = ">>"; | ||
392 | } | ||
393 | |||
394 | /* Only display if they fit */ | ||
395 | display->getstringsize(leftlabel, &w, &h); | ||
396 | width_used += w; | ||
397 | display->getstringsize(rightlabel, &w, &h); | ||
398 | width_used += w; | ||
399 | |||
400 | if (width_used <= pitch_viewports[PITCH_MID].width) | ||
401 | { | ||
402 | display->putsxy(0, (pitch_viewports[PITCH_MID].height / 2) - (h / 2), | ||
403 | leftlabel); | ||
404 | display->putsxy((pitch_viewports[PITCH_MID].width - w), | ||
405 | (pitch_viewports[PITCH_MID].height / 2) - (h / 2), | ||
406 | rightlabel); | ||
407 | } | ||
408 | display->update_viewport(); | ||
409 | display->set_viewport(last_vp); | ||
410 | } | ||
411 | |||
412 | static int32_t pitch_increase(int32_t pitch, int32_t pitch_delta, bool allow_cutoff | ||
413 | /* need this to maintain correct pitch/speed caps */ | ||
414 | , int32_t speed | ||
415 | ) | ||
416 | { | ||
417 | int32_t new_pitch; | ||
418 | int32_t new_stretch; | ||
419 | at_limit = false; | ||
420 | |||
421 | if (pitch_delta < 0) | ||
422 | { | ||
423 | /* for large jumps, snap up to whole numbers */ | ||
424 | if(allow_cutoff && pitch_delta <= -PITCH_SPEED_PRECISION && | ||
425 | (pitch + pitch_delta) % PITCH_SPEED_PRECISION != 0) | ||
426 | { | ||
427 | pitch_delta += PITCH_SPEED_PRECISION - ((pitch + pitch_delta) % PITCH_SPEED_PRECISION); | ||
428 | } | ||
429 | |||
430 | new_pitch = pitch + pitch_delta; | ||
431 | |||
432 | if (new_pitch < PITCH_MIN) | ||
433 | { | ||
434 | if (!allow_cutoff) | ||
435 | { | ||
436 | return pitch; | ||
437 | } | ||
438 | new_pitch = PITCH_MIN; | ||
439 | at_limit = true; | ||
440 | } | ||
441 | } | ||
442 | else if (pitch_delta > 0) | ||
443 | { | ||
444 | /* for large jumps, snap down to whole numbers */ | ||
445 | if(allow_cutoff && pitch_delta >= PITCH_SPEED_PRECISION && | ||
446 | (pitch + pitch_delta) % PITCH_SPEED_PRECISION != 0) | ||
447 | { | ||
448 | pitch_delta -= (pitch + pitch_delta) % PITCH_SPEED_PRECISION; | ||
449 | } | ||
450 | |||
451 | new_pitch = pitch + pitch_delta; | ||
452 | |||
453 | if (new_pitch > PITCH_MAX) | ||
454 | { | ||
455 | if (!allow_cutoff) | ||
456 | return pitch; | ||
457 | new_pitch = PITCH_MAX; | ||
458 | at_limit = true; | ||
459 | } | ||
460 | } | ||
461 | else | ||
462 | { | ||
463 | /* pitch_delta == 0 -> no real change */ | ||
464 | return pitch; | ||
465 | } | ||
466 | if (dsp_timestretch_available()) | ||
467 | { | ||
468 | /* increase the multiple to increase precision of this calculation */ | ||
469 | new_stretch = GET_STRETCH(new_pitch, speed); | ||
470 | if(new_stretch < STRETCH_MIN) | ||
471 | { | ||
472 | /* we have to ignore allow_cutoff, because we can't have the */ | ||
473 | /* stretch go higher than STRETCH_MAX */ | ||
474 | new_pitch = GET_PITCH(speed, STRETCH_MIN); | ||
475 | } | ||
476 | else if(new_stretch > STRETCH_MAX) | ||
477 | { | ||
478 | /* we have to ignore allow_cutoff, because we can't have the */ | ||
479 | /* stretch go higher than STRETCH_MAX */ | ||
480 | new_pitch = GET_PITCH(speed, STRETCH_MAX); | ||
481 | } | ||
482 | |||
483 | if(new_stretch >= STRETCH_MAX || | ||
484 | new_stretch <= STRETCH_MIN) | ||
485 | { | ||
486 | at_limit = true; | ||
487 | } | ||
488 | } | ||
489 | |||
490 | sound_set_pitch(new_pitch); | ||
491 | |||
492 | return new_pitch; | ||
493 | } | ||
494 | |||
495 | static int32_t get_semitone_from_pitch(int32_t pitch) | ||
496 | { | ||
497 | int semitone = 0; | ||
498 | int32_t fractional_index = 0; | ||
499 | |||
500 | while(semitone < NUM_SEMITONES - 1 && | ||
501 | pitch >= semitone_table[semitone + 1]) | ||
502 | { | ||
503 | semitone++; | ||
504 | } | ||
505 | |||
506 | |||
507 | /* now find the fractional part */ | ||
508 | while(pitch > (cent_interp[fractional_index + 1] * | ||
509 | semitone_table[semitone] / PITCH_SPEED_100)) | ||
510 | { | ||
511 | /* Check to make sure fractional_index isn't too big */ | ||
512 | /* This should never happen. */ | ||
513 | if(fractional_index >= CENT_INTERP_NUM - 1) | ||
514 | { | ||
515 | break; | ||
516 | } | ||
517 | fractional_index++; | ||
518 | } | ||
519 | |||
520 | int32_t semitone_pitch_a = cent_interp[fractional_index] * | ||
521 | semitone_table[semitone] / | ||
522 | PITCH_SPEED_100; | ||
523 | int32_t semitone_pitch_b = cent_interp[fractional_index + 1] * | ||
524 | semitone_table[semitone] / | ||
525 | PITCH_SPEED_100; | ||
526 | /* this will be the integer offset from the cent_interp entry */ | ||
527 | int32_t semitone_frac_ofs = (pitch - semitone_pitch_a) * CENT_INTERP_INTERVAL / | ||
528 | (semitone_pitch_b - semitone_pitch_a); | ||
529 | semitone = (semitone + SEMITONE_START) * PITCH_SPEED_PRECISION + | ||
530 | fractional_index * CENT_INTERP_INTERVAL + | ||
531 | semitone_frac_ofs; | ||
532 | |||
533 | return semitone; | ||
534 | } | ||
535 | |||
536 | static int32_t get_pitch_from_semitone(int32_t semitone) | ||
537 | { | ||
538 | int32_t adjusted_semitone = semitone - SEMITONE_START * PITCH_SPEED_PRECISION; | ||
539 | |||
540 | /* Find the index into the semitone table */ | ||
541 | int32_t semitone_index = (adjusted_semitone / PITCH_SPEED_PRECISION); | ||
542 | |||
543 | /* set pitch to the semitone's integer part value */ | ||
544 | int32_t pitch = semitone_table[semitone_index]; | ||
545 | /* get the range of the cent modification for future calculation */ | ||
546 | int32_t pitch_mod_a = | ||
547 | cent_interp[(adjusted_semitone % PITCH_SPEED_PRECISION) / | ||
548 | CENT_INTERP_INTERVAL]; | ||
549 | int32_t pitch_mod_b = | ||
550 | cent_interp[(adjusted_semitone % PITCH_SPEED_PRECISION) / | ||
551 | CENT_INTERP_INTERVAL + 1]; | ||
552 | /* figure out the cent mod amount based on the semitone fractional value */ | ||
553 | int32_t pitch_mod = pitch_mod_a + (pitch_mod_b - pitch_mod_a) * | ||
554 | (adjusted_semitone % CENT_INTERP_INTERVAL) / CENT_INTERP_INTERVAL; | ||
555 | |||
556 | /* modify pitch based on the mod amount we just calculated */ | ||
557 | return (pitch * pitch_mod + PITCH_SPEED_100 / 2) / PITCH_SPEED_100; | ||
558 | } | ||
559 | |||
560 | static int32_t pitch_increase_semitone(int32_t pitch, | ||
561 | int32_t current_semitone, | ||
562 | int32_t semitone_delta | ||
563 | , int32_t speed | ||
564 | ) | ||
565 | { | ||
566 | int32_t new_semitone = current_semitone; | ||
567 | |||
568 | /* snap to the delta interval */ | ||
569 | if(current_semitone % semitone_delta != 0) | ||
570 | { | ||
571 | if(current_semitone > 0 && semitone_delta > 0) | ||
572 | new_semitone += semitone_delta; | ||
573 | else if(current_semitone < 0 && semitone_delta < 0) | ||
574 | new_semitone += semitone_delta; | ||
575 | |||
576 | new_semitone -= new_semitone % semitone_delta; | ||
577 | } | ||
578 | else | ||
579 | new_semitone += semitone_delta; | ||
580 | |||
581 | /* clamp the pitch so it doesn't go beyond the pitch limits */ | ||
582 | if(new_semitone < (SEMITONE_START * PITCH_SPEED_PRECISION)) | ||
583 | { | ||
584 | new_semitone = SEMITONE_START * PITCH_SPEED_PRECISION; | ||
585 | at_limit = true; | ||
586 | } | ||
587 | else if(new_semitone > (SEMITONE_END * PITCH_SPEED_PRECISION)) | ||
588 | { | ||
589 | new_semitone = SEMITONE_END * PITCH_SPEED_PRECISION; | ||
590 | at_limit = true; | ||
591 | } | ||
592 | |||
593 | int32_t new_pitch = get_pitch_from_semitone(new_semitone); | ||
594 | |||
595 | int32_t new_stretch = GET_STRETCH(new_pitch, speed); | ||
596 | |||
597 | /* clamp the pitch so it doesn't go beyond the stretch limits */ | ||
598 | if( new_stretch > STRETCH_MAX) | ||
599 | { | ||
600 | new_pitch = GET_PITCH(speed, STRETCH_MAX); | ||
601 | new_semitone = get_semitone_from_pitch(new_pitch); | ||
602 | at_limit = true; | ||
603 | } | ||
604 | else if (new_stretch < STRETCH_MIN) | ||
605 | { | ||
606 | new_pitch = GET_PITCH(speed, STRETCH_MIN); | ||
607 | new_semitone = get_semitone_from_pitch(new_pitch); | ||
608 | at_limit = true; | ||
609 | } | ||
610 | |||
611 | pitch_increase(pitch, new_pitch - pitch, false | ||
612 | , speed | ||
613 | ); | ||
614 | |||
615 | return new_semitone; | ||
616 | } | ||
617 | |||
618 | #ifdef HAVE_TOUCHSCREEN | ||
619 | /* | ||
620 | * Check for touchscreen presses as per sketch above in this file | ||
621 | * | ||
622 | * goes through each row of the, checks whether the touchscreen | ||
623 | * was pressed in it. Then it looks the columns of each row for specific actions | ||
624 | */ | ||
625 | static int pitchscreen_do_touchscreen(struct viewport vps[]) | ||
626 | { | ||
627 | short x, y; | ||
628 | struct viewport *this_vp = &vps[PITCH_TOP]; | ||
629 | int ret; | ||
630 | static bool wait_for_release = false; | ||
631 | ret = action_get_touchscreen_press_in_vp(&x, &y, this_vp); | ||
632 | |||
633 | /* top row */ | ||
634 | if (ret > ACTION_UNKNOWN) | ||
635 | { /* press on top row, left or right column | ||
636 | * only toggle mode if released */ | ||
637 | int column = this_vp->width / 3; | ||
638 | if ((x < column || x > (2*column)) && (ret == BUTTON_REL)) | ||
639 | return ACTION_PS_TOGGLE_MODE; | ||
640 | |||
641 | |||
642 | else if (x >= column && x <= (2*column)) | ||
643 | { /* center column pressed */ | ||
644 | if (ret == BUTTON_REPEAT) | ||
645 | return ACTION_PS_INC_BIG; | ||
646 | else if (ret & BUTTON_REL) | ||
647 | return ACTION_PS_INC_SMALL; | ||
648 | } | ||
649 | return ACTION_NONE; | ||
650 | } | ||
651 | |||
652 | /* now the center row */ | ||
653 | this_vp = &vps[PITCH_MID]; | ||
654 | ret = action_get_touchscreen_press_in_vp(&x, &y, this_vp); | ||
655 | |||
656 | if (ret > ACTION_UNKNOWN) | ||
657 | { | ||
658 | int column = this_vp->width / 3; | ||
659 | |||
660 | if (x < column) | ||
661 | { /* left column */ | ||
662 | if (ret & BUTTON_REL) | ||
663 | { | ||
664 | wait_for_release = false; | ||
665 | return ACTION_PS_NUDGE_LEFTOFF; | ||
666 | } | ||
667 | else if (ret & BUTTON_REPEAT) | ||
668 | return ACTION_PS_SLOWER; | ||
669 | if (!wait_for_release) | ||
670 | { | ||
671 | wait_for_release = true; | ||
672 | return ACTION_PS_NUDGE_LEFT; | ||
673 | } | ||
674 | } | ||
675 | else if (x > (2*column)) | ||
676 | { /* right column */ | ||
677 | if (ret & BUTTON_REL) | ||
678 | { | ||
679 | wait_for_release = false; | ||
680 | return ACTION_PS_NUDGE_RIGHTOFF; | ||
681 | } | ||
682 | else if (ret & BUTTON_REPEAT) | ||
683 | return ACTION_PS_FASTER; | ||
684 | if (!wait_for_release) | ||
685 | { | ||
686 | wait_for_release = true; | ||
687 | return ACTION_PS_NUDGE_RIGHT; | ||
688 | } | ||
689 | } | ||
690 | else | ||
691 | /* center column was pressed */ | ||
692 | return ACTION_PS_RESET; | ||
693 | } | ||
694 | |||
695 | /* now the bottom row */ | ||
696 | this_vp = &vps[PITCH_BOTTOM]; | ||
697 | ret = action_get_touchscreen_press_in_vp(&x, &y, this_vp); | ||
698 | |||
699 | if (ret > ACTION_UNKNOWN) | ||
700 | { | ||
701 | int column = this_vp->width / 3; | ||
702 | |||
703 | /* left or right column is exit */ | ||
704 | if ((x < column || x > (2*column)) && (ret == BUTTON_REL)) | ||
705 | return ACTION_PS_EXIT; | ||
706 | else if (x >= column && x <= (2*column)) | ||
707 | { /* center column was pressed */ | ||
708 | if (ret & BUTTON_REPEAT) | ||
709 | return ACTION_PS_DEC_BIG; | ||
710 | else if (ret & BUTTON_REL) | ||
711 | return ACTION_PS_DEC_SMALL; | ||
712 | } | ||
713 | return ACTION_NONE; | ||
714 | } | ||
715 | return ACTION_NONE; | ||
716 | } | ||
717 | |||
718 | #endif | ||
719 | /* | ||
720 | returns: | ||
721 | 0 on exit | ||
722 | 1 if USB was connected | ||
723 | */ | ||
724 | |||
725 | int gui_syncpitchscreen_run(void) | 22 | int gui_syncpitchscreen_run(void) |
726 | { | 23 | { |
727 | int button; | 24 | return (plugin_load(VIEWERS_DIR"/pitch_screen.rock", NULL) == PLUGIN_USB_CONNECTED); |
728 | int32_t pitch = sound_get_pitch(); | ||
729 | int32_t semitone; | ||
730 | |||
731 | int32_t new_pitch; | ||
732 | int32_t pitch_delta; | ||
733 | bool nudged = false; | ||
734 | int i, updated = 4, decimals = 0; | ||
735 | bool exit = false; | ||
736 | /* should maybe be passed per parameter later, not needed for now */ | ||
737 | struct viewport parent[NB_SCREENS]; | ||
738 | struct viewport pitch_viewports[NB_SCREENS][PITCH_ITEM_COUNT]; | ||
739 | int max_lines[NB_SCREENS]; | ||
740 | |||
741 | push_current_activity(ACTIVITY_PITCHSCREEN); | ||
742 | |||
743 | int32_t new_speed = 0, new_stretch; | ||
744 | |||
745 | /* the speed variable holds the apparent speed of the playback */ | ||
746 | int32_t speed; | ||
747 | if (dsp_timestretch_available()) | ||
748 | { | ||
749 | speed = GET_SPEED(pitch, dsp_get_timestretch()); | ||
750 | } | ||
751 | else | ||
752 | { | ||
753 | speed = pitch; | ||
754 | } | ||
755 | |||
756 | /* Figure out whether to be in timestretch mode */ | ||
757 | if (global_settings.pitch_mode_timestretch && !dsp_timestretch_available()) | ||
758 | { | ||
759 | global_settings.pitch_mode_timestretch = false; | ||
760 | settings_save(); | ||
761 | } | ||
762 | |||
763 | /* Count decimals for speaking */ | ||
764 | for (i = PITCH_SPEED_PRECISION; i >= 10; i /= 10) | ||
765 | decimals++; | ||
766 | |||
767 | /* set the semitone index based on the current pitch */ | ||
768 | semitone = get_semitone_from_pitch(pitch); | ||
769 | |||
770 | /* initialize pitchscreen vps */ | ||
771 | FOR_NB_SCREENS(i) | ||
772 | { | ||
773 | viewport_set_defaults(&parent[i], i); | ||
774 | max_lines[i] = viewport_get_nb_lines(&parent[i]); | ||
775 | pitchscreen_fix_viewports(&parent[i], pitch_viewports[i]); | ||
776 | screens[i].set_viewport(&parent[i]); | ||
777 | screens[i].clear_viewport(); | ||
778 | |||
779 | /* also, draw the icons now, it's only needed once */ | ||
780 | pitchscreen_draw_icons(&screens[i], &parent[i]); | ||
781 | } | ||
782 | pcmbuf_set_low_latency(true); | ||
783 | |||
784 | while (!exit) | ||
785 | { | ||
786 | FOR_NB_SCREENS(i) | ||
787 | pitchscreen_draw(&screens[i], max_lines[i], | ||
788 | pitch_viewports[i], pitch, semitone | ||
789 | , speed | ||
790 | ); | ||
791 | pitch_delta = 0; | ||
792 | new_speed = 0; | ||
793 | |||
794 | if (global_settings.talk_menu && updated) | ||
795 | { | ||
796 | talk_shutup(); | ||
797 | switch (updated) | ||
798 | { | ||
799 | case 1: | ||
800 | if (global_settings.pitch_mode_semitone) | ||
801 | talk_value_decimal(semitone, UNIT_SIGNED, decimals, false); | ||
802 | else | ||
803 | talk_value_decimal(pitch, UNIT_PERCENT, decimals, false); | ||
804 | break; | ||
805 | case 2: | ||
806 | talk_value_decimal(speed, UNIT_PERCENT, decimals, false); | ||
807 | break; | ||
808 | case 3: | ||
809 | speak_pitch_mode(false); | ||
810 | break; | ||
811 | case 4: | ||
812 | if (global_settings.pitch_mode_timestretch && dsp_timestretch_available()) | ||
813 | talk_id(LANG_PITCH, false); | ||
814 | else | ||
815 | talk_id(LANG_PLAYBACK_RATE, false); | ||
816 | talk_value_decimal(pitch, UNIT_PERCENT, decimals, true); | ||
817 | if (global_settings.pitch_mode_timestretch && dsp_timestretch_available()) | ||
818 | { | ||
819 | talk_id(LANG_SPEED, true); | ||
820 | talk_value_decimal(speed, UNIT_PERCENT, decimals, true); | ||
821 | } | ||
822 | speak_pitch_mode(true); | ||
823 | break; | ||
824 | default: | ||
825 | break; | ||
826 | } | ||
827 | } | ||
828 | updated = 0; | ||
829 | |||
830 | button = get_action(CONTEXT_PITCHSCREEN, HZ); | ||
831 | |||
832 | #ifdef HAVE_TOUCHSCREEN | ||
833 | if (button == ACTION_TOUCHSCREEN) | ||
834 | { | ||
835 | FOR_NB_SCREENS(i) | ||
836 | button = pitchscreen_do_touchscreen(pitch_viewports[i]); | ||
837 | } | ||
838 | #endif | ||
839 | switch (button) | ||
840 | { | ||
841 | case ACTION_PS_INC_SMALL: | ||
842 | if(global_settings.pitch_mode_semitone) | ||
843 | pitch_delta = SEMITONE_SMALL_DELTA; | ||
844 | else | ||
845 | pitch_delta = PITCH_SMALL_DELTA; | ||
846 | updated = 1; | ||
847 | break; | ||
848 | |||
849 | case ACTION_PS_INC_BIG: | ||
850 | if(global_settings.pitch_mode_semitone) | ||
851 | pitch_delta = SEMITONE_BIG_DELTA; | ||
852 | else | ||
853 | pitch_delta = PITCH_BIG_DELTA; | ||
854 | updated = 1; | ||
855 | break; | ||
856 | |||
857 | case ACTION_PS_DEC_SMALL: | ||
858 | if(global_settings.pitch_mode_semitone) | ||
859 | pitch_delta = -SEMITONE_SMALL_DELTA; | ||
860 | else | ||
861 | pitch_delta = -PITCH_SMALL_DELTA; | ||
862 | updated = 1; | ||
863 | break; | ||
864 | |||
865 | case ACTION_PS_DEC_BIG: | ||
866 | if(global_settings.pitch_mode_semitone) | ||
867 | pitch_delta = -SEMITONE_BIG_DELTA; | ||
868 | else | ||
869 | pitch_delta = -PITCH_BIG_DELTA; | ||
870 | updated = 1; | ||
871 | break; | ||
872 | |||
873 | case ACTION_PS_NUDGE_RIGHT: | ||
874 | if (!global_settings.pitch_mode_timestretch) | ||
875 | { | ||
876 | new_pitch = pitch_increase(pitch, PITCH_NUDGE_DELTA, false | ||
877 | , speed | ||
878 | ); | ||
879 | nudged = (new_pitch != pitch); | ||
880 | pitch = new_pitch; | ||
881 | semitone = get_semitone_from_pitch(pitch); | ||
882 | speed = pitch; | ||
883 | updated = nudged ? 1 : 0; | ||
884 | break; | ||
885 | } | ||
886 | else | ||
887 | { | ||
888 | new_speed = speed + SPEED_SMALL_DELTA; | ||
889 | at_limit = false; | ||
890 | updated = 2; | ||
891 | } | ||
892 | break; | ||
893 | |||
894 | case ACTION_PS_FASTER: | ||
895 | if (global_settings.pitch_mode_timestretch) | ||
896 | { | ||
897 | new_speed = speed + SPEED_BIG_DELTA; | ||
898 | /* snap to whole numbers */ | ||
899 | if(new_speed % PITCH_SPEED_PRECISION != 0) | ||
900 | new_speed -= new_speed % PITCH_SPEED_PRECISION; | ||
901 | at_limit = false; | ||
902 | updated = 2; | ||
903 | } | ||
904 | break; | ||
905 | |||
906 | case ACTION_PS_NUDGE_RIGHTOFF: | ||
907 | if (nudged) | ||
908 | { | ||
909 | pitch = pitch_increase(pitch, -PITCH_NUDGE_DELTA, false | ||
910 | , speed | ||
911 | ); | ||
912 | speed = pitch; | ||
913 | semitone = get_semitone_from_pitch(pitch); | ||
914 | nudged = false; | ||
915 | updated = 1; | ||
916 | } | ||
917 | break; | ||
918 | |||
919 | case ACTION_PS_NUDGE_LEFT: | ||
920 | if (!global_settings.pitch_mode_timestretch) | ||
921 | { | ||
922 | new_pitch = pitch_increase(pitch, -PITCH_NUDGE_DELTA, false | ||
923 | , speed | ||
924 | ); | ||
925 | nudged = (new_pitch != pitch); | ||
926 | pitch = new_pitch; | ||
927 | semitone = get_semitone_from_pitch(pitch); | ||
928 | speed = pitch; | ||
929 | updated = nudged ? 1 : 0; | ||
930 | break; | ||
931 | } | ||
932 | else | ||
933 | { | ||
934 | new_speed = speed - SPEED_SMALL_DELTA; | ||
935 | at_limit = false; | ||
936 | updated = 2; | ||
937 | } | ||
938 | break; | ||
939 | |||
940 | case ACTION_PS_SLOWER: | ||
941 | if (global_settings.pitch_mode_timestretch) | ||
942 | { | ||
943 | new_speed = speed - SPEED_BIG_DELTA; | ||
944 | /* snap to whole numbers */ | ||
945 | if(new_speed % PITCH_SPEED_PRECISION != 0) | ||
946 | new_speed += PITCH_SPEED_PRECISION - speed % PITCH_SPEED_PRECISION; | ||
947 | at_limit = false; | ||
948 | updated = 2; | ||
949 | } | ||
950 | break; | ||
951 | |||
952 | case ACTION_PS_NUDGE_LEFTOFF: | ||
953 | if (nudged) | ||
954 | { | ||
955 | pitch = pitch_increase(pitch, PITCH_NUDGE_DELTA, false | ||
956 | , speed | ||
957 | ); | ||
958 | speed = pitch; | ||
959 | semitone = get_semitone_from_pitch(pitch); | ||
960 | nudged = false; | ||
961 | updated = 1; | ||
962 | } | ||
963 | break; | ||
964 | |||
965 | case ACTION_PS_RESET: | ||
966 | pitch = PITCH_SPEED_100; | ||
967 | sound_set_pitch(pitch); | ||
968 | speed = PITCH_SPEED_100; | ||
969 | if (dsp_timestretch_available()) | ||
970 | { | ||
971 | dsp_set_timestretch(PITCH_SPEED_100); | ||
972 | at_limit = false; | ||
973 | } | ||
974 | semitone = get_semitone_from_pitch(pitch); | ||
975 | updated = 4; | ||
976 | break; | ||
977 | |||
978 | case ACTION_PS_TOGGLE_MODE: | ||
979 | global_settings.pitch_mode_semitone = !global_settings.pitch_mode_semitone; | ||
980 | |||
981 | if (dsp_timestretch_available() && !global_settings.pitch_mode_semitone) | ||
982 | { | ||
983 | global_settings.pitch_mode_timestretch = !global_settings.pitch_mode_timestretch; | ||
984 | if(!global_settings.pitch_mode_timestretch) | ||
985 | { | ||
986 | /* no longer in timestretch mode. Reset speed */ | ||
987 | speed = pitch; | ||
988 | dsp_set_timestretch(PITCH_SPEED_100); | ||
989 | } | ||
990 | } | ||
991 | settings_save(); | ||
992 | updated = 3; | ||
993 | break; | ||
994 | |||
995 | case ACTION_PS_EXIT: | ||
996 | exit = true; | ||
997 | break; | ||
998 | |||
999 | default: | ||
1000 | if (default_event_handler(button) == SYS_USB_CONNECTED) | ||
1001 | return 1; | ||
1002 | break; | ||
1003 | } | ||
1004 | if (pitch_delta) | ||
1005 | { | ||
1006 | if (global_settings.pitch_mode_semitone) | ||
1007 | { | ||
1008 | semitone = pitch_increase_semitone(pitch, semitone, pitch_delta | ||
1009 | , speed | ||
1010 | ); | ||
1011 | pitch = get_pitch_from_semitone(semitone); | ||
1012 | } | ||
1013 | else | ||
1014 | { | ||
1015 | pitch = pitch_increase(pitch, pitch_delta, true | ||
1016 | , speed | ||
1017 | ); | ||
1018 | semitone = get_semitone_from_pitch(pitch); | ||
1019 | } | ||
1020 | if (global_settings.pitch_mode_timestretch) | ||
1021 | { | ||
1022 | /* do this to make sure we properly obey the stretch limits */ | ||
1023 | new_speed = speed; | ||
1024 | } | ||
1025 | else | ||
1026 | { | ||
1027 | speed = pitch; | ||
1028 | } | ||
1029 | } | ||
1030 | |||
1031 | if(new_speed) | ||
1032 | { | ||
1033 | new_stretch = GET_STRETCH(pitch, new_speed); | ||
1034 | |||
1035 | /* limit the amount of stretch */ | ||
1036 | if(new_stretch > STRETCH_MAX) | ||
1037 | { | ||
1038 | new_stretch = STRETCH_MAX; | ||
1039 | new_speed = GET_SPEED(pitch, new_stretch); | ||
1040 | } | ||
1041 | else if(new_stretch < STRETCH_MIN) | ||
1042 | { | ||
1043 | new_stretch = STRETCH_MIN; | ||
1044 | new_speed = GET_SPEED(pitch, new_stretch); | ||
1045 | } | ||
1046 | |||
1047 | new_stretch = GET_STRETCH(pitch, new_speed); | ||
1048 | if(new_stretch >= STRETCH_MAX || | ||
1049 | new_stretch <= STRETCH_MIN) | ||
1050 | { | ||
1051 | at_limit = true; | ||
1052 | } | ||
1053 | |||
1054 | /* set the amount of stretch */ | ||
1055 | dsp_set_timestretch(new_stretch); | ||
1056 | |||
1057 | /* update the speed variable with the new speed */ | ||
1058 | speed = new_speed; | ||
1059 | |||
1060 | /* Reset new_speed so we only call dsp_set_timestretch */ | ||
1061 | /* when needed */ | ||
1062 | new_speed = 0; | ||
1063 | } | ||
1064 | } | ||
1065 | |||
1066 | pcmbuf_set_low_latency(false); | ||
1067 | pop_current_activity(); | ||
1068 | |||
1069 | /* Clean up */ | ||
1070 | FOR_NB_SCREENS(i) | ||
1071 | { | ||
1072 | screens[i].set_viewport(NULL); | ||
1073 | } | ||
1074 | |||
1075 | return 0; | ||
1076 | } | 25 | } |