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) 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}