A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2017 Franklin Wei
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
19 *
20 ***************************************************************************/
21
22/* ideas for improvement:
23 * hyphenation of long words */
24
25#include "plugin.h"
26
27#include "fixedpoint.h"
28
29#include "lib/helper.h"
30#include "lib/pluginlib_actions.h"
31#include "lib/pluginlib_exit.h"
32
33#define LINE_LEN 1024
34#define WORD_MAX 64
35
36#define MIN_WPM 100
37#define MAX_WPM 1000
38#define DEF_WPM 250
39#define WPM_INCREMENT 25
40
41/* mininum bytes to skip when seeking */
42#define SEEK_INTERVAL 100
43
44#define FOCUS_X (7 * LCD_WIDTH / 20)
45#define FOCUS_Y 0
46
47#define FRAME_COLOR LCD_BLACK
48#define BACKGROUND_COLOR LCD_WHITE /* inside frame */
49
50#ifdef HAVE_LCD_COLOR
51#define WORD_COLOR LCD_RGBPACK(48,48,48)
52#define FOCUS_COLOR LCD_RGBPACK(204,0,0)
53#define OUTSIDE_COLOR LCD_RGBPACK(128,128,128)
54#define BAR_COLOR LCD_RGBPACK(230,230,230)
55#else
56#define WORD_COLOR LCD_BLACK
57#define OUTSIDE_COLOR BACKGROUND_COLOR
58#endif
59
60#define BOOKMARK_FILE VIEWERS_DATA_DIR "/speedread.dat"
61#define CONFIG_FILE VIEWERS_DATA_DIR "/speedread.cfg"
62
63#define ANIM_TIME (75 * HZ / 100)
64
65int fd = -1; /* -1 = prescripted demo */
66
67int word_num; /* which word on a line */
68off_t line_offs, begin_offs; /* offsets from the "real" beginning of the file to the current line and end of BOM */
69
70int line_len = -1, custom_font = FONT_UI;
71
72const char *last_word = NULL;
73
74static const char *get_next_word(void)
75{
76 if(fd >= 0)
77 {
78 static char line_buf[LINE_LEN];
79 static char *end = NULL;
80
81 next_line:
82
83 if(line_len < 0)
84 {
85 line_offs = rb->lseek(fd, 0, SEEK_CUR);
86 line_len = rb->read_line(fd, line_buf, LINE_LEN);
87 if(line_len <= 0)
88 return NULL;
89
90 char *word = rb->strtok_r(line_buf, " ", &end);
91
92 word_num = 0;
93
94 if(!word)
95 goto next_line;
96 else
97 {
98 last_word = word;
99 return word;
100 }
101 }
102
103 char *word = rb->strtok_r(NULL, " ", &end);
104 if(!word)
105 {
106 /* end of line */
107 line_len = -1;
108 goto next_line;
109 }
110 ++word_num;
111
112 last_word = word;
113 return word;
114 }
115 else
116 {
117 /* feed the user a quick demo */
118 static const char *words[] = { "This", "plugin", "is", "for", "speed-reading", "plain", "text", "files.",
119 "Please", "open", "a", "plain", "text", "file", "to", "read", "by", "using", "the", "context", "menu.",
120 "Have", "a", "nice", "day!" };
121 static unsigned idx = 0;
122 if(idx + 1 > ARRAYLEN(words))
123 return NULL;
124 last_word = words[idx++];
125 return last_word;
126 }
127}
128
129static const char *get_last_word(void)
130{
131 if(last_word)
132 return last_word;
133 else
134 {
135 last_word = get_next_word();
136 return last_word;
137 }
138}
139
140static void cleanup(void)
141{
142 if(custom_font != FONT_UI)
143 rb->font_unload(custom_font);
144
145 backlight_use_settings();
146
147}
148
149/* returns height of drawn area */
150static int reset_drawing(long proportion) /* 16.16 fixed point, goes from 0 --> 1 over time */
151{
152 int w, h;
153 rb->lcd_getstringsize("X", &w, &h);
154
155 /* clear word area */
156 rb->lcd_set_foreground(BACKGROUND_COLOR);
157 rb->lcd_fillrect(0, 0, LCD_WIDTH, h * 3 / 2);
158
159 if(proportion)
160 {
161#ifdef HAVE_LCD_COLOR
162 rb->lcd_set_foreground(BAR_COLOR);
163#endif
164 rb->lcd_fillrect(fp_mul(proportion, FOCUS_X << 16, 16) >> 16, FOCUS_Y, fp_mul((1 << 16) - proportion, LCD_WIDTH << 16, 16) >> 16, h * 3 / 2);
165 }
166
167 rb->lcd_set_foreground(FRAME_COLOR);
168
169 /* draw frame */
170 rb->lcd_fillrect(0, h * 3 / 2, LCD_WIDTH, w / 2);
171 rb->lcd_fillrect(FOCUS_X - w / 4, FOCUS_Y + h * 5 / 4, w / 2, h / 2);
172
173 rb->lcd_set_foreground(WORD_COLOR);
174
175 return h * 3 / 2 + w / 2;
176}
177
178static void render_word(const char *word, int focus)
179{
180 /* focus char first */
181 char buf[5] = { 0, 0, 0, 0, 0 };
182 int idx = rb->utf8seek(word, focus);
183 rb->memcpy(buf, word + idx, MIN(rb->utf8seek(word, focus + 1) - idx, 4));
184
185 int focus_w;
186 rb->lcd_getstringsize(buf, &focus_w, NULL);
187
188#ifdef HAVE_LCD_COLOR
189 rb->lcd_set_foreground(FOCUS_COLOR);
190#endif
191
192 rb->lcd_putsxy(FOCUS_X - focus_w / 2, FOCUS_Y, buf);
193
194#ifdef HAVE_LCD_COLOR
195 rb->lcd_set_foreground(WORD_COLOR);
196#endif
197
198 /* figure out how far left to shift */
199 static char half[WORD_MAX];
200 rb->strlcpy(half, word, rb->utf8seek(word, focus + 1));
201 int w;
202 rb->lcd_getstringsize(half, &w, NULL);
203
204 int x = FOCUS_X - focus_w / 2 - w;
205
206 /* first half */
207 rb->lcd_putsxy(x, FOCUS_Y, half);
208
209 /* second half */
210 x = FOCUS_X + focus_w / 2;
211 rb->lcd_putsxy(x, FOCUS_Y, word + rb->utf8seek(word, focus + 1));
212}
213
214static int calculate_focus(const char *word)
215{
216#if 0
217 int len = rb->utf8length(word);
218 int focus = -1;
219 for(int i = len / 5; i < len / 2; ++i)
220 {
221 switch(tolower(word[rb->utf8seek(word, i)]))
222 {
223 case 'a': case 'e': case 'i': case 'o': case 'u':
224 focus = i;
225 break;
226 default:
227 break;
228 }
229 }
230
231 if(focus < 0)
232 focus = len / 2;
233 return focus;
234#else
235 int len = rb->utf8length(word);
236 if(rb->utf8length(word) > 13)
237 return 4;
238 else
239 {
240 int tab[] = {0,1,1,1,1,2,2,2,2,3,3,3,3};
241 return tab[len - 1];
242 }
243#endif
244}
245
246static int calculate_delay(const char *word, int wpm)
247{
248 long base = 60 * HZ / wpm;
249 long timeout = base;
250 int len = rb->utf8length(word);
251
252 if(len > 6)
253 timeout += base / 5 * (len - 6);
254
255 if(rb->strchr(word, ',') || rb->strchr(word, '-'))
256 timeout += base / 2;
257
258 if(rb->strchr(word, '.') || rb->strchr(word, '!') || rb->strchr(word, '?') || rb->strchr(word, ';'))
259 timeout += 3 * base / 2;
260 return timeout;
261}
262
263static long render_screen(const char *word, int wpm)
264{
265 /* significant inspiration taken from spread0r */
266 long timeout = calculate_delay(word, wpm);
267 int focus = calculate_focus(word);
268
269 rb->lcd_setfont(custom_font);
270
271 int h = reset_drawing(0);
272
273 render_word(word, focus);
274
275 rb->lcd_setfont(FONT_UI);
276
277 rb->lcd_update_rect(0, 0, LCD_WIDTH, h);
278 return timeout;
279}
280
281static void begin_anim(void)
282{
283 long start = *rb->current_tick;
284 long end = start + ANIM_TIME;
285
286 const char *word = get_last_word();
287
288 int focus = calculate_focus(word);
289
290 rb->lcd_setfont(custom_font);
291
292 while(*rb->current_tick < end)
293 {
294 int h = reset_drawing(fp_div((*rb->current_tick - start) << 16, ANIM_TIME << 16, 16));
295
296 render_word(word, focus);
297 rb->lcd_update_rect(0, 0, LCD_WIDTH, h);
298 }
299
300 rb->lcd_setfont(FONT_UI);
301}
302
303static void init_drawing(void)
304{
305 backlight_ignore_timeout();
306
307 atexit(cleanup);
308
309 rb->lcd_set_background(OUTSIDE_COLOR);
310 rb->lcd_set_backdrop(NULL);
311 rb->lcd_set_drawmode(DRMODE_FG);
312 rb->lcd_clear_display();
313
314 rb->lcd_update();
315}
316
317enum { NOTHING = 0, SLOWER, FASTER, FFWD, BACK, PAUSE, QUIT };
318
319static const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
320
321static int get_useraction(void)
322{
323 int button = pluginlib_getaction(0, plugin_contexts, ARRAYLEN(plugin_contexts));
324
325 switch(button)
326 {
327#ifdef HAVE_SCROLLWHEEL
328 case PLA_SCROLL_FWD:
329 case PLA_SCROLL_FWD_REPEAT:
330#else
331 case PLA_UP:
332#endif
333 return FASTER;
334#ifdef HAVE_SCROLLWHEEL
335 case PLA_SCROLL_BACK:
336 case PLA_SCROLL_BACK_REPEAT:
337#else
338 case PLA_DOWN:
339#endif
340 return SLOWER;
341 case PLA_SELECT:
342 return PAUSE;
343 case PLA_CANCEL:
344 return QUIT;
345 case PLA_LEFT_REPEAT:
346 case PLA_LEFT:
347 return BACK;
348 case PLA_RIGHT_REPEAT:
349 case PLA_RIGHT:
350 return FFWD;
351 default:
352 exit_on_usb(button); /* handle poweroff and USB events */
353 return 0;
354 }
355}
356
357static void save_bookmark(const char *fname, int wpm)
358{
359 if(!fname)
360 return;
361 rb->splash(0, "Saving...");
362 /* copy every line except the one to be changed */
363 int bookmark_fd = rb->open(BOOKMARK_FILE, O_RDONLY);
364 int tmp_fd = rb->open(BOOKMARK_FILE ".tmp", O_WRONLY | O_CREAT | O_TRUNC, 0666);
365 if(bookmark_fd >= 0)
366 {
367 while(1)
368 {
369 /* space for the filename, 3, integers, and a null */
370 static char line[MAX_PATH + 1 + 10 + 1 + 10 + 1 + 10 + 1];
371 int len = rb->read_line(bookmark_fd, line, sizeof(line));
372 if(len <= 0)
373 break;
374
375 char *end;
376 rb->strtok_r(line, " ", &end);
377 rb->strtok_r(NULL, " ", &end);
378 rb->strtok_r(NULL, " ", &end);
379 char *bookmark_name = rb->strtok_r(NULL, "", &end);
380
381 if(!bookmark_name)
382 continue; /* avoid crash */
383 if(rb->strcmp(fname, bookmark_name))
384 {
385 /* go back and clean up after strtok */
386 for(int i = 0; i < len - 1; ++i)
387 if(!line[i])
388 line[i] = ' ';
389
390 rb->write(tmp_fd, line, len);
391 rb->fdprintf(tmp_fd, "\n");
392 }
393 }
394 rb->close(bookmark_fd);
395 }
396 rb->fdprintf(tmp_fd, "%jd %d %d %s\n",
397 (intmax_t) line_offs, word_num, wpm, fname);
398 rb->close(tmp_fd);
399 rb->rename(BOOKMARK_FILE ".tmp", BOOKMARK_FILE);
400}
401
402static bool load_bookmark(const char *fname, int *wpm)
403{
404 int bookmark_fd = rb->open(BOOKMARK_FILE, O_RDONLY);
405 if(bookmark_fd >= 0)
406 {
407 while(1)
408 {
409 /* space for the filename, 2 integers, and a null */
410 char line[MAX_PATH + 1 + 10 + 1 + 10 + 1];
411 int len = rb->read_line(bookmark_fd, line, sizeof(line));
412 if(len <= 0)
413 break;
414
415 char *end;
416 char *tok = rb->strtok_r(line, " ", &end);
417 if(!tok)
418 continue;
419 off_t offs = rb->atoi(tok);
420
421 tok = rb->strtok_r(NULL, " ", &end);
422 if(!tok)
423 continue;
424 int word = rb->atoi(tok);
425
426 tok = rb->strtok_r(NULL, " ", &end);
427 if(!tok)
428 continue;
429 *wpm = rb->atoi(tok);
430 if(*wpm < MIN_WPM)
431 *wpm = MIN_WPM;
432 if(*wpm > MAX_WPM)
433 *wpm = MAX_WPM;
434
435 char *bookmark_name = rb->strtok_r(NULL, "", &end);
436
437 if(!bookmark_name)
438 continue;
439
440 if(!rb->strcmp(fname, bookmark_name))
441 {
442 rb->lseek(fd, offs, SEEK_SET);
443 for(int i = 0; i < word; ++i)
444 get_next_word();
445 rb->close(bookmark_fd);
446 return true;
447 }
448 }
449 rb->close(bookmark_fd);
450 }
451 return false;
452}
453
454static void new_font(const char *path)
455{
456 if(custom_font != FONT_UI)
457 rb->font_unload(custom_font);
458 custom_font = rb->font_load(path);
459 if(custom_font < 0)
460 custom_font = FONT_UI;
461}
462
463static void save_font(const char *path)
464{
465 int font_fd = rb->open(CONFIG_FILE, O_WRONLY | O_TRUNC | O_CREAT, 0666);
466 rb->write(font_fd, path, rb->strlen(path));
467 rb->close(font_fd);
468}
469
470static char font_buf[MAX_PATH + 1];
471
472static void load_font(void)
473{
474 int font_fd = rb->open(CONFIG_FILE, O_RDONLY);
475 if(font_fd < 0)
476 return;
477 int len = rb->read(font_fd, font_buf, MAX_PATH);
478 font_buf[len] = '\0';
479 rb->close(font_fd);
480 new_font(font_buf);
481}
482
483static void font_menu(void)
484{
485 /* taken from text_viewer */
486 char font[MAX_PATH], name[MAX_FILENAME+10];
487 rb->snprintf(name, sizeof(name), "%s.fnt", rb->global_settings->font_file);
488
489 struct browse_context browse = {
490 .dirfilter = SHOW_FONT,
491 .flags = BROWSE_SELECTONLY | BROWSE_NO_CONTEXT_MENU,
492 .title = rb->str(LANG_CUSTOM_FONT),
493 .icon = Icon_Menu_setting,
494 .root = FONT_DIR,
495 .selected = name,
496 .buf = font,
497 .bufsize = sizeof(font),
498 };
499
500 rb->rockbox_browse(&browse);
501
502 if (browse.flags & BROWSE_SELECTED)
503 {
504 new_font(font);
505 save_font(font);
506 }
507}
508
509static bool confirm_restart(void)
510{
511 const struct text_message prompt = { (const char*[]) {"Are you sure?", "This will erase your current position."}, 2};
512 enum yesno_res response = rb->gui_syncyesno_run(&prompt, NULL, NULL);
513 if(response == YESNO_NO)
514 return false;
515 else
516 return true;
517}
518
519static int config_menu(void)
520{
521 MENUITEM_STRINGLIST(menu, "Speedread Menu", NULL,
522 "Resume Reading",
523 "Restart from Beginning",
524 "Change Font",
525 "Quit");
526 int rc = 0;
527 int sel = 0;
528 while(!rc)
529 {
530 switch(rb->do_menu(&menu, &sel, NULL, false))
531 {
532 case 0:
533 rc = 1;
534 break;
535 case 1:
536 if(fd >= 0 && confirm_restart())
537 {
538 rb->lseek(fd, begin_offs, SEEK_SET);
539 line_len = -1;
540 get_next_word();
541 rc = 1;
542 }
543 break;
544 case 2:
545 font_menu();
546 break;
547 case 3:
548 rc = 2;
549 break;
550 default:
551 break;
552 }
553 }
554 return rc - 1;
555}
556
557enum { SKIP = -1, FINISH = -2 };
558
559static int poll_input(int *wpm, long *clear, const char *fname, off_t file_size)
560{
561 switch(get_useraction())
562 {
563 case FASTER:
564 if(*wpm + WPM_INCREMENT <= MAX_WPM)
565 *wpm += WPM_INCREMENT;
566 rb->splashf(0, "%d wpm", *wpm);
567 *clear = *rb->current_tick + HZ;
568 break;
569 case SLOWER:
570 if(*wpm - WPM_INCREMENT >= MIN_WPM)
571 *wpm -= WPM_INCREMENT;
572 rb->splashf(0, "%d wpm", *wpm);
573 *clear = *rb->current_tick + HZ;
574 break;
575 case FFWD:
576 if(fd >= 0)
577 {
578 off_t base_offs = rb->lseek(fd, 0, SEEK_CUR);
579 off_t offs = 0;
580
581 do {
582 offs += SEEK_INTERVAL;
583 if(offs >= 1000 * SEEK_INTERVAL)
584 offs += 199 * SEEK_INTERVAL;
585 else if(offs >= 100 * SEEK_INTERVAL)
586 offs += 99 * SEEK_INTERVAL;
587 else if(offs >= 10 * SEEK_INTERVAL)
588 offs += 9 * SEEK_INTERVAL;
589 rb->splashf(0, "%jd/%jd bytes",
590 (intmax_t) (offs + base_offs), (intmax_t) file_size);
591 rb->sleep(HZ/20);
592 } while(get_useraction() == FFWD && offs + base_offs < file_size && offs + base_offs >= 0);
593
594 *clear = *rb->current_tick + HZ;
595
596 rb->lseek(fd, offs, SEEK_CUR);
597
598 /* discard the next word (or more likely, portion of a word) */
599 line_len = -1;
600 get_next_word();
601
602 return SKIP;
603 }
604 break;
605 case BACK:
606 if(fd >= 0)
607 {
608 off_t base_offs = rb->lseek(fd, 0, SEEK_CUR);
609 off_t offs = 0;
610
611 do {
612 offs -= SEEK_INTERVAL;
613 if(offs <= -1000 * SEEK_INTERVAL)
614 offs -= 199 * SEEK_INTERVAL;
615 else if(offs <= -100 * SEEK_INTERVAL)
616 offs -= 99 * SEEK_INTERVAL;
617 else if(offs <= -10 * SEEK_INTERVAL)
618 offs -= 9 * SEEK_INTERVAL;
619 rb->splashf(0, "%jd/%jd bytes",
620 (intmax_t) (offs + base_offs), (intmax_t) file_size);
621 rb->sleep(HZ/20);
622 } while(get_useraction() == FFWD && offs + base_offs < file_size && offs + base_offs >= 0);
623
624 *clear = *rb->current_tick + HZ;
625
626 rb->lseek(fd, offs, SEEK_CUR);
627
628 /* discard the next word (or more likely, portion of a word) */
629 line_len = -1;
630 get_next_word();
631
632 return SKIP;
633 }
634 break;
635 case PAUSE:
636 case QUIT:
637 if(config_menu())
638 {
639 save_bookmark(fname, *wpm);
640 return FINISH;
641 }
642 else
643 {
644 init_drawing();
645 begin_anim();
646 }
647 break;
648 case NOTHING:
649 default:
650 break;
651 }
652 return 0;
653}
654
655enum plugin_status plugin_start(const void *param)
656{
657 const char *fname = param;
658
659 off_t file_size = 0;
660
661 load_font();
662
663 bool loaded = false;
664
665 int wpm = DEF_WPM;
666
667 if(fname)
668 {
669 fd = rb->open_utf8(fname, O_RDONLY);
670
671 begin_offs = rb->lseek(fd, 0, SEEK_CUR); /* skip BOM */
672 file_size = rb->lseek(fd, 0, SEEK_END);
673 rb->lseek(fd, begin_offs, SEEK_SET);
674
675 loaded = load_bookmark(fname, &wpm);
676 }
677
678 init_drawing();
679
680 long clear = -1;
681 if(loaded)
682 {
683 rb->splash(0, "Loaded bookmark.");
684 clear = *rb->current_tick + HZ;
685 }
686
687 begin_anim();
688
689 /* main loop */
690 while(1)
691 {
692 switch(poll_input(&wpm, &clear, fname, file_size))
693 {
694 case SKIP:
695 continue;
696 case FINISH:
697 goto done;
698 default:
699 break;
700 }
701
702 const char *word = get_next_word();
703 if(!word)
704 break;
705 bool want_full_update = false;
706 if(TIME_AFTER(*rb->current_tick, clear) && clear != -1)
707 {
708 clear = -1;
709 rb->lcd_clear_display();
710 want_full_update = true;
711 }
712 long interval = render_screen(word, wpm);
713
714 long frame_done = *rb->current_tick + interval;
715
716 if(want_full_update)
717 rb->lcd_update();
718
719 while(!TIME_AFTER(*rb->current_tick, frame_done))
720 {
721 switch(poll_input(&wpm, &clear, fname, file_size))
722 {
723 case SKIP:
724 goto next_word;
725 case FINISH:
726 goto done;
727 default:
728 break;
729 }
730 rb->yield();
731 }
732 next_word:
733 ;
734 }
735
736done:
737 rb->close(fd);
738
739 return PLUGIN_OK;
740}