A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 620 lines 18 kB view raw
1/*************************************************************************** 2 * __________ __ ___. 3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 * \/ \/ \/ \/ \/ 8 * $Id$ 9 * 10 * Copyright (C) 2007 Nicolas Pennequin, Jonathan Gordon 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#include <stdio.h> 23#include <stdlib.h> 24#include <stdbool.h> 25#include <ctype.h> 26#include <string.h> 27#include "system.h" 28#include "audio.h" 29#include "kernel.h" 30#include "logf.h" 31#include "misc.h" 32#include "screens.h" 33#include "list.h" 34#include "action.h" 35#include "lang.h" 36#include "debug.h" 37#include "settings.h" 38#include "plugin.h" 39#include "playback.h" 40#include "cuesheet.h" 41#include "gui/wps.h" 42 43#define CUE_DIR ROCKBOX_DIR "/cue" 44 45static bool search_for_cuesheet(const char *path, struct cuesheet_file *cue_file) 46{ 47 size_t len; 48 char cuepath[MAX_PATH]; 49 char *dot, *slash, *slash_cuepath; 50 51 cue_file->pos = 0; 52 cue_file->size = 0; 53 cue_file->path[0] = '\0'; 54 slash = strrchr(path, '/'); 55 if (!slash) 56 return false; 57 len = strlcpy(cuepath, path, MAX_PATH); 58 slash_cuepath = &cuepath[slash - path]; 59 dot = strrchr(slash_cuepath, '.'); 60 if (dot) 61 strmemccpy(dot, ".cue", MAX_PATH - (dot-cuepath)); 62 63 if (!dot || !file_exists(cuepath)) 64 { 65 strcpy(cuepath, CUE_DIR); 66 if (strlcat(cuepath, slash, MAX_PATH) >= MAX_PATH) 67 goto skip; /* overflow */ 68 dot = strrchr(cuepath, '.'); 69 if (dot) 70 strcpy(dot, ".cue"); 71 if (!file_exists(cuepath)) 72 { 73skip: 74 if ((len+4) >= MAX_PATH) 75 return false; 76 strmemccpy(cuepath, path, MAX_PATH); 77 strlcat(cuepath, ".cue", MAX_PATH); 78 if (!file_exists(cuepath)) 79 return false; 80 } 81 } 82 83 strmemccpy(cue_file->path, cuepath, MAX_PATH); 84 return true; 85} 86 87bool look_for_cuesheet_file(struct mp3entry *track_id3, struct cuesheet_file *cue_file) 88{ 89 /* DEBUGF("look for cue file\n"); */ 90 if (track_id3->has_embedded_cuesheet) 91 { 92 cue_file->pos = track_id3->embedded_cuesheet.pos; 93 cue_file->size = track_id3->embedded_cuesheet.size; 94 cue_file->encoding = track_id3->embedded_cuesheet.encoding; 95 strmemccpy(cue_file->path, track_id3->path, MAX_PATH); 96 return true; 97 } 98 99 return search_for_cuesheet(track_id3->path, cue_file); 100} 101 102static char *get_string(const char *line) 103{ 104 char *start, *end; 105 106 start = strchr(line, '"'); 107 if (!start) 108 { 109 start = strchr(line, ' '); 110 111 if (!start) 112 return NULL; 113 } 114 115 end = strchr(++start, '"'); 116 if (end) 117 *end = '\0'; 118 119 return start; 120} 121 122static unsigned long parse_cue_index(const char *line) 123{ 124 /* assumes strncmp(line, "INDEX 01", 8) & NULL terminated string */ 125 /* INDEX 01 MM:SS:FF\0 (00:00:00\0 - 99:99:99\0)*/ 126 const unsigned field_m[3] = {60 * 1000, 1000, 13}; /* MM:SS:~FF*/ 127 const unsigned field_max[3] = {30000, 59, 74}; /* MM, SS, FF */ 128 const char f_sep = ':'; 129 int field = -1; 130 unsigned long offset = 0; /* ms from start of track */ 131 unsigned long value = 0; 132 while (*line) 133 { 134 if (!isdigit(*line)) /* search for numbers */ 135 { 136 line++; 137 continue; 138 } 139 140 while (isdigit(*line)) 141 { 142 value = 10 * value + (*line - '0'); 143 if (field >= 0 && value > field_max[field]) /* Sanity check bail early */ 144 return 0; 145 line++; 146 } 147 148 if (field < 0) /*Filter INDEX 01*/ 149 { 150 /* safe to assume value == 1 */ 151 } 152 else if (field <= 2) 153 { 154 while(*line && *line != f_sep) 155 line++; 156 157 if (*line || field == 2) /* if *line valid we found f_sep */ 158 offset += (unsigned long) field_m[field] * value; 159 } 160 else 161 break; 162 163 value = 0; 164 field++; 165 } 166 167 return offset; 168} 169 170enum eCS_SUPPORTED_TAGS { 171 eCS_TRACK = 0, eCS_INDEX_01, eCS_TITLE, 172 eCS_PERFORMER, eCS_SONGWRITER, eCS_FILE, 173 eCS_COUNT_TAGS_COUNT, eCS_NOTFOUND = -1 174}; 175 176static enum eCS_SUPPORTED_TAGS cuesheet_tag_get_option(const char *option) 177{ 178 #define CS_OPTN(str) {str, sizeof(str)-1} 179 static const struct cs_option_t { 180 const char *str; 181 const int len; 182 } cs_options[eCS_COUNT_TAGS_COUNT + 1] = { 183 [eCS_TRACK] = CS_OPTN("TRACK"), 184 [eCS_INDEX_01] = CS_OPTN("INDEX 01"), 185 [eCS_TITLE] =CS_OPTN("TITLE"), 186 [eCS_PERFORMER] =CS_OPTN("PERFORMER"), 187 [eCS_SONGWRITER] =CS_OPTN("SONGWRITER"), 188 [eCS_FILE] =CS_OPTN("FILE"), 189 [eCS_COUNT_TAGS_COUNT] = {NULL, 0} /*SENTINEL*/ 190 }; 191 192 const struct cs_option_t *op; 193 for (int i=0; ((op=&cs_options[i]))->str != NULL; i++) 194 { 195 if (strncmp(option, op->str, op->len) == 0) 196 return i; 197 } 198 return eCS_NOTFOUND; 199#undef CS_OPTN 200} 201 202/* parse cuesheet "cue_file" and store the information in "cue" */ 203bool parse_cuesheet(struct cuesheet_file *cue_file, struct cuesheet *cue) 204{ 205 char line[MAX_PATH]; 206 char *s; 207 unsigned char char_enc = CHAR_ENC_ISO_8859_1; 208 bool is_embedded = false; 209 int line_len; 210 int bytes_left = 0; 211 int read_bytes = MAX_PATH; 212 unsigned char utf16_buf[MAX_PATH]; 213 214 int fd = open(cue_file->path, O_RDONLY, 0644); 215 if(fd < 0) 216 return false; 217 if (cue_file->pos > 0) 218 { 219 is_embedded = true; 220 lseek(fd, cue_file->pos, SEEK_SET); 221 bytes_left = cue_file->size; 222 char_enc = cue_file->encoding; 223 } 224 225 /* Look for a Unicode BOM */ 226 unsigned char bom_read = 0; 227 if (read(fd, line, BOM_UTF_8_SIZE) > 0) 228 { 229 if(!memcmp(line, BOM_UTF_8, BOM_UTF_8_SIZE)) 230 { 231 char_enc = CHAR_ENC_UTF_8; 232 bom_read = BOM_UTF_8_SIZE; 233 } 234 else 235 { 236 bool le; 237 if (utf16_has_bom(line, &le)) 238 { 239 char_enc = le ? CHAR_ENC_UTF_16_LE : CHAR_ENC_UTF_16_BE; 240 bom_read = BOM_UTF_16_SIZE; 241 } 242 } 243 } 244 245 if (bom_read < BOM_UTF_8_SIZE) 246 lseek(fd, cue_file->pos + bom_read, SEEK_SET); 247 if (is_embedded) 248 { 249 if (bom_read > 0) 250 bytes_left -= bom_read; 251 if (read_bytes > bytes_left) 252 read_bytes = bytes_left; 253 } 254 255 /* Initialization */ 256 memset(cue, 0, sizeof(struct cuesheet)); 257 strcpy(cue->path, cue_file->path); 258 cue->curr_track = cue->tracks; 259 260 if (is_embedded) 261 strcpy(cue->file, cue->path); 262 263 while ((line_len = read_line(fd, line, read_bytes)) > 0 264 && cue->track_count < MAX_TRACKS ) 265 { 266 if (char_enc == CHAR_ENC_UTF_16_LE) 267 { 268 s = utf16decode(line, utf16_buf, line_len>>1, sizeof(utf16_buf) - 1, true); 269 /* terminate the string at the newline */ 270 *s = '\0'; 271 strcpy(line, utf16_buf); 272 /* chomp the trailing 0 after the newline */ 273 lseek(fd, 1, SEEK_CUR); 274 line_len++; 275 } 276 else if (char_enc == CHAR_ENC_UTF_16_BE) 277 { 278 s = utf16decode(line, utf16_buf, line_len>>1, sizeof(utf16_buf) - 1, false); 279 *s = '\0'; 280 strcpy(line, utf16_buf); 281 } 282 s = skip_whitespace(line); 283 284/* RECOGNIZED TAGS *********************** 285* eCS_TRACK = 0, eCS_INDEX_01, eCS_TITLE, 286* eCS_PERFORMER, eCS_SONGWRITER, eCS_FILE, 287*/ 288 enum eCS_SUPPORTED_TAGS option = cuesheet_tag_get_option(s); 289 if (option == eCS_TRACK) 290 { 291 cue->track_count++; 292 } 293 else if (option == eCS_INDEX_01) 294 { 295#if 0 296 s = strchr(s,' '); 297 s = skip_whitespace(s); 298 s = strchr(s,' '); 299 s = skip_whitespace(s); 300 cue->tracks[cue->track_count-1].offset = 60*1000 * atoi(s); 301 s = strchr(s,':') + 1; 302 cue->tracks[cue->track_count-1].offset += 1000 * atoi(s); 303 s = strchr(s,':') + 1; 304 cue->tracks[cue->track_count-1].offset += 13 * atoi(s); 305#else 306 cue->tracks[cue->track_count-1].offset = parse_cue_index(s); 307#endif 308 } 309 else if (option != eCS_NOTFOUND) 310 { 311 char *dest = NULL; 312 char *string = get_string(s); 313 if (!string) 314 break; 315 316 size_t count = MAX_NAME*3 + 1; 317 318 switch (option) 319 { 320 case eCS_TITLE: /* TITLE */ 321 dest = (cue->track_count <= 0) ? cue->title : 322 cue->tracks[cue->track_count-1].title; 323 break; 324 325 case eCS_PERFORMER: /* PERFORMER */ 326 dest = (cue->track_count <= 0) ? cue->performer : 327 cue->tracks[cue->track_count-1].performer; 328 break; 329 330 case eCS_SONGWRITER: /* SONGWRITER */ 331 dest = (cue->track_count <= 0) ? cue->songwriter : 332 cue->tracks[cue->track_count-1].songwriter; 333 break; 334 335 case eCS_FILE: /* FILE */ 336 if (is_embedded || cue->track_count > 0) 337 break; 338 339 dest = cue->file; 340 count = MAX_PATH; 341 break; 342 case eCS_TRACK: 343 /*Fall-Through*/ 344 case eCS_INDEX_01: 345 /*Fall-Through*/ 346 case eCS_COUNT_TAGS_COUNT: 347 /*Fall-Through*/ 348 case eCS_NOTFOUND: /*Shouldn't happen*/ 349 logf(HZ * 2, "Bad Tag %d @ %s", (int) option, __func__); 350 dest = NULL; 351 break; 352 } 353 354 if (dest) 355 { 356 if (char_enc == CHAR_ENC_ISO_8859_1) 357 { 358 dest = iso_decode_ex(string, dest, -1, 359 strlen(string), count - 1); 360 *dest = '\0'; 361 } 362 else 363 { 364 strmemccpy(dest, string, count); 365 } 366 } 367 } 368 369 if (is_embedded) 370 { 371 bytes_left -= line_len; 372 if (bytes_left <= 0) 373 break; 374 if (bytes_left < read_bytes) 375 read_bytes = bytes_left; 376 } 377 } 378 close(fd); 379 380 /* If just a filename, add path information from cuesheet path */ 381 if (*cue->file && !strrchr(cue->file, '/')) 382 { 383 strcpy(line, cue->file); 384 strcpy(cue->file, cue->path); 385 char *slash = strrchr(cue->file, '/'); 386 if (!slash++) slash = cue->file; 387 strmemccpy(slash, line, MAX_PATH - (slash - cue->file)); 388 } 389 390 /* If some songs don't have performer info, we copy the cuesheet performer */ 391 int i; 392 for (i = 0; i < cue->track_count; i++) 393 { 394 if (*(cue->tracks[i].performer) == '\0') 395 strmemccpy(cue->tracks[i].performer, cue->performer, MAX_NAME*3); 396 397 if (*(cue->tracks[i].songwriter) == '\0') 398 strmemccpy(cue->tracks[i].songwriter, cue->songwriter, MAX_NAME*3); 399 } 400 401 return true; 402} 403 404/* takes care of seeking to a track in a playlist 405 * returns false if audio isn't playing */ 406static bool seek(unsigned long pos) 407{ 408 if (!(audio_status() & AUDIO_STATUS_PLAY)) 409 { 410 return false; 411 } 412 else 413 { 414 audio_pre_ff_rewind(); 415 audio_ff_rewind(pos); 416 return true; 417 } 418} 419 420/* returns the index of the track currently being played 421 and updates the information about the current track. */ 422int cue_find_current_track(struct cuesheet *cue, unsigned long curpos) 423{ 424 int i=0; 425 while (i < cue->track_count-1 && cue->tracks[i+1].offset < curpos) 426 i++; 427 428 cue->curr_track_idx = i; 429 cue->curr_track = cue->tracks + i; 430 return i; 431} 432 433/* callback that gives list item titles for the cuesheet browser */ 434static const char* list_get_name_cb(int selected_item, 435 void *data, 436 char *buffer, 437 size_t buffer_len) 438{ 439 struct cuesheet *cue = (struct cuesheet *)data; 440 441 if (selected_item & 1) 442 strmemccpy(buffer, cue->tracks[selected_item/2].title, buffer_len); 443 else 444 snprintf(buffer, buffer_len, "%02d. %s", selected_item/2+1, 445 cue->tracks[selected_item/2].performer); 446 447 return buffer; 448} 449 450void browse_cuesheet(struct cuesheet *cue) 451{ 452 struct gui_synclist lists; 453 int action; 454 bool done = false; 455 char title[MAX_PATH]; 456 int len; 457 458 struct cuesheet_file cue_file; 459 struct mp3entry *id3 = audio_current_track(); 460 461 len = snprintf(title, sizeof(title), "%s: %s", cue->performer, cue->title); 462 463 if ((unsigned) len > sizeof(title)) 464 title[sizeof(title) - 2] = '~'; /* give indication of truncation */ 465 466 467 gui_synclist_init(&lists, list_get_name_cb, cue, false, 2, NULL); 468 gui_synclist_set_nb_items(&lists, 2*cue->track_count); 469 gui_synclist_set_title(&lists, title, 0); 470 471 472 if (id3) 473 { 474 gui_synclist_select_item(&lists, 475 2*cue_find_current_track(cue, id3->elapsed)); 476 } 477 478 while (!done) 479 { 480 gui_synclist_draw(&lists); 481 action = get_action(CONTEXT_LIST,TIMEOUT_BLOCK); 482 if (gui_synclist_do_button(&lists, &action)) 483 continue; 484 switch (action) 485 { 486 case ACTION_STD_OK: 487 { 488 bool startit = true; 489 unsigned long elapsed = 490 cue->tracks[gui_synclist_get_sel_pos(&lists)/2].offset; 491 492 id3 = audio_current_track(); 493 if (id3 && *id3->path) 494 { 495 look_for_cuesheet_file(id3, &cue_file); 496 if (!strcmp(cue->path, cue_file.path)) 497 startit = false; 498 } 499 500 if (!startit) 501 startit = !seek(elapsed); 502 503 if (!startit || !*cue->file) 504 break; 505 506 /* check that this cue is the same one that would be found by 507 a search from playback */ 508 char file[MAX_PATH]; 509 strmemccpy(file, cue->file, MAX_PATH); 510 511 if (!strcmp(cue->path, file) || /* if embedded */ 512 (search_for_cuesheet(file, &cue_file) && 513 !strcmp(cue->path, cue_file.path))) 514 { 515 char *fname = strrsplt(file, '/'); 516 char *dirname = fname <= file + 1 ? "/" : file; 517 bookmark_play(dirname, 0, elapsed, 0, current_tick, fname); 518 } 519 break; 520 } /* ACTION_STD_OK */ 521 522 case ACTION_STD_CANCEL: 523 done = true; 524 default: 525 break; 526 } 527 } 528} 529 530bool display_cuesheet_content(char* filename) 531{ 532 size_t bufsize = 0; 533 struct cuesheet_file cue_file; 534 struct cuesheet *cue = (struct cuesheet *)plugin_get_buffer(&bufsize); 535 if (!cue || bufsize < sizeof(struct cuesheet)) 536 return false; 537 538 strmemccpy(cue_file.path, filename, MAX_PATH); 539 cue_file.pos = 0; 540 cue_file.size = 0; 541 542 if (!parse_cuesheet(&cue_file, cue)) 543 return false; 544 545 browse_cuesheet(cue); 546 return true; 547} 548 549/* skips backwards or forward in the current cuesheet 550 * the return value indicates whether we're still in a cusheet after skipping 551 * it also returns false if we weren't in a cuesheet. 552 * direction should be 1 or -1. 553 */ 554bool curr_cuesheet_skip(struct cuesheet *cue, int direction, unsigned long curr_pos) 555{ 556 int track = cue_find_current_track(cue, curr_pos); 557 558 if (direction >= 0 && track == cue->track_count - 1) 559 { 560 /* we want to get out of the cuesheet */ 561 return false; 562 } 563 else 564 { 565 if (!(direction <= 0 && track == 0)) 566 { 567 /* If skipping forward, skip to next cuesheet segment. If skipping 568 backward before DEFAULT_SKIP_THRESH milliseconds have elapsed, skip 569 to previous cuesheet segment. If skipping backward after 570 DEFAULT_SKIP_THRESH seconds have elapsed, skip to the start of the 571 current cuesheet segment */ 572 if (direction == 1 || 573 ((curr_pos - cue->tracks[track].offset) < DEFAULT_SKIP_THRESH)) 574 { 575 track += direction; 576 } 577 } 578 579 seek(cue->tracks[track].offset); 580 return true; 581 } 582 583} 584 585static inline void draw_veritcal_line_mark(struct screen * screen, 586 int x, int y, int h) 587{ 588 screen->set_drawmode(DRMODE_COMPLEMENT); 589 screen->vline(x, y, y+h-1); 590} 591 592/* draw the cuesheet markers for a track of length "tracklen", 593 between (x,y) and (x+w,y) */ 594void cue_draw_markers(struct screen *screen, struct cuesheet *cue, 595 unsigned long tracklen, 596 int x, int y, int w, int h) 597{ 598 int i,xi; 599 unsigned long tracklen_seconds = tracklen/1000; /* duration in seconds */ 600 601 for (i=1; i < cue->track_count; i++) 602 { 603 /* Convert seconds prior to multiplication to avoid overflow. */ 604 xi = x + (w * (cue->tracks[i].offset/1000)) / tracklen_seconds; 605 draw_veritcal_line_mark(screen, xi, y, h); 606 } 607} 608 609bool cuesheet_subtrack_changed(struct mp3entry *id3) 610{ 611 struct cuesheet *cue = id3->cuesheet; 612 if (cue && (id3->elapsed < cue->curr_track->offset 613 || (cue->curr_track_idx < cue->track_count - 1 614 && id3->elapsed >= (cue->curr_track+1)->offset))) 615 { 616 cue_find_current_track(cue, id3->elapsed); 617 return true; 618 } 619 return false; 620}