A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 740 lines 19 kB view raw
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}