A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 375 lines 11 kB view raw
1/*************************************************************************** 2 * __________ __ ___. 3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 * \/ \/ \/ \/ \/ 8 * $Id$ 9 * 10 * Copyright (C) 2024 William Wilgus 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/* convert supplied playlist file to a .cue file */ 23 24#include "plugin.h" 25 26#if defined(DEBUG) || defined(SIMULATOR) 27 #define logf(...) rb->debugf(__VA_ARGS__); rb->debugf("\n") 28#elif defined(ROCKBOX_HAS_LOGF) 29 #define logf rb->logf 30#else 31 #define logf(...) do { } while(0) 32#endif 33 34#define CPS_MAX_ENTRY_SZ (4 *1024) 35#define TDINDENT " " /* prepend spaces for track data formatting */ 36 37static struct cps 38{ 39 char *buffer; 40 size_t buffer_sz; 41 size_t buffer_index; 42 int cue_fd; 43 int entries; 44} cps; 45 46static int sprfunc(void *ptr, int letter) 47{ 48 /* callback for vuprintf */ 49 (void) ptr; 50 if (cps.buffer_index < cps.buffer_sz - 1) 51 { 52 cps.buffer[cps.buffer_index++] = letter; 53 return 1; 54 } 55 return -1; 56} 57 58void cps_printf(const char *fmt, ...) 59{ 60 /* NOTE! this is made for flushing the buffer to disk -- WARNING 61 * Nothing is NULL terminated here unless explicitly made so.. \0 */ 62 va_list ap; 63 va_start(ap, fmt); 64 rb->vuprintf(sprfunc, NULL, fmt, ap); 65 va_end(ap); 66} 67 68static uint32_t write_metadata_tags(struct mp3entry *id3) 69{ 70/* check an ID3 and write any numeric tags and valid string tags (non empty) */ 71#define ISVALID(s) (s != NULL && s[0] != '\0') 72 uint32_t tag_flags = 0; 73 74 const char *performer = rb->str(LANG_TAGNAVI_UNTAGGED); 75 if (ISVALID(id3->artist)) 76 performer = id3->artist; 77 else if (ISVALID(id3->albumartist)) 78 performer = id3->albumartist; 79 80 const char *title = rb->str(LANG_TAGNAVI_UNTAGGED); 81 if (ISVALID(id3->title)) 82 title = id3->title; 83 84#define PERFORMER_TITLE_SZ TDINDENT "PERFORMER \"%s\"\n" \ 85 TDINDENT "TITLE \"%s\"\n" \ 86 TDINDENT "SIZE_INFO %ld\n" 87 88 cps_printf(PERFORMER_TITLE_SZ, performer, title, id3->filesize); 89 if (ISVALID(id3->composer)) 90 cps_printf(TDINDENT "COMPOSER \"%s\"\n", id3->composer); 91 92 cps_printf(TDINDENT "INDEX 01 00:00:00\n"); 93 94#define ID3_TAG_NUM(theid3, TAGID, flag) \ 95 {cps_printf(TDINDENT "REM %s %lu\n", TAGID, (unsigned long)theid3);tag_flags |= flag;} 96#define ID3_TAG_STR(theid3, TAGID, flag) if (ISVALID(theid3)) \ 97 {cps_printf(TDINDENT "REM %s \"%s\"\n", TAGID, theid3);tag_flags |= flag;} 98 99 ID3_TAG_STR(id3->album, "ALBUM", 0x01); 100 ID3_TAG_STR(id3->albumartist, "ALBUMARTIST", 0x02); 101 ID3_TAG_STR(id3->comment, "COMMENT", 0x04); 102 ID3_TAG_STR(id3->genre_string, "GENRE", 0x08); 103 ID3_TAG_STR(id3->disc_string, "DISC", 0x10); 104 ID3_TAG_STR(id3->track_string, "TRACK", 0x20); 105 ID3_TAG_STR(id3->grouping, "GROUPING", 0x40); 106 ID3_TAG_STR(id3->mb_track_id, "MB_TRACK_ID", 0x80); 107 ID3_TAG_STR(rb->get_codec_string(id3->codectype), "ID3_CODEC", 0x100); 108 109 ID3_TAG_NUM(id3->discnum, "DISCNUM", 0x200); 110 ID3_TAG_NUM(id3->tracknum, "TRACKNUM", 0x400); 111 ID3_TAG_NUM(id3->length, "LENGTH", 0x800); 112 ID3_TAG_NUM(id3->bitrate, "BITRATE", 0x1000); 113 ID3_TAG_NUM(id3->frequency, "FREQUENCY", 0x2000); 114 ID3_TAG_NUM(id3->track_level, "TRACK_LEVEL",0x4000); 115 ID3_TAG_NUM(id3->album_level, "ALBUM_LEVEL", 0x8000); 116#undef ID3_TAG_STR 117#undef ID3_TAG_NUM 118#undef IS_VALID 119 return tag_flags; 120} 121 122static bool current_playlist_filename_cb(const char *filename, int attr, int index, int display_index) 123{ 124 /* worker function for writing the actual cue data */ 125 int szpos = 0; /* records position of the size string */ 126 int namepos = 0; /* records position of the end of filename string */ 127 struct mp3entry id3; 128 129 logf("found: %s", filename); 130 131 uint32_t id3_flags = 0; 132 bool have_metadata = rb->get_metadata(&id3, -1, filename); 133 134 if (!have_metadata && !rb->file_exists(filename)) 135 return false; 136#define RB_ENTRY_DATA_FMT "REM RB_ENTRY_DATA " \ 137 "\"DISPLAY_INDEX %012u " \ 138 "PLAYLIST_INDEX %012u " \ 139 "SIZE %n%012zu TAGS %012lu\"\n" 140 141 const char *audiotype = "WAVE"; /* everything except MP3 */ 142 const char *skipped = "";; 143 const char *queued = ""; 144 145 size_t entry_start = cps.buffer_index; /* get start to calculate final size */ 146 147 cps_printf(RB_ENTRY_DATA_FMT, display_index, index, &szpos, 0, 0UL); 148 149 size_t file_start = cps.buffer_index; 150 cps_printf("FILE \"%s%n\"", filename, &namepos); 151 152 if (cps.buffer[file_start + namepos - 1] == '3') 153 audiotype = "MP3"; 154 155 cps_printf(" %s\n", audiotype); 156 157 if (attr & PLAYLIST_ATTR_SKIPPED) 158 skipped = TDINDENT "REM SKIPPED\n"; 159 if (attr & PLAYLIST_ATTR_QUEUED) 160 queued = TDINDENT "REM QUEUED\n"; 161 162 cps_printf(" TRACK %d AUDIO\n%s%s", display_index, skipped, queued); 163 164 if (have_metadata) 165 id3_flags = write_metadata_tags(&id3); 166 167 if (cps.buffer_index - entry_start < CPS_MAX_ENTRY_SZ) 168 { 169 /* place the write pointer at the size entry so we can update size + tags*/ 170 size_t index = cps.buffer_index; 171 cps.buffer_index = entry_start + szpos; 172 cps_printf("%012zu TAGS %012lu", (index - entry_start), id3_flags); 173 cps.buffer_index = index; /* set the write pointer back at the end */ 174 } 175 else 176 { 177 rb->splashf(HZ * 3, "Entry too large %s", filename); 178 cps.buffer_index = entry_start; 179 return false; 180 } 181 182 cps.entries++; 183 return true; 184} 185 186static bool playlist_filename_cb(const char *filename) 187{ 188 /* get entries from an on-disk playlist */ 189 return current_playlist_filename_cb(filename, 0, 190 cps.entries, cps.entries); 191} 192 193static bool current_playlist_get_entries(void) 194{ 195 /* get entries from a loaded playlist ( may have queued or skipped tracks ) */ 196#if defined(HAVE_ADJUSTABLE_CPU_FREQ) 197#define cpuboost(enable) rb->cpu_boost(enable); 198#else 199#define cpuboost(enable) do{ } while(0) 200#endif 201 struct playlist_track_info info; 202 int count = rb->playlist_amount(); 203 int i, res = 0; 204 logf("current playlist contains %d entries", count); 205 206 cpuboost(true); 207 208 long next_progress_tick = *rb->current_tick; 209 for (i = 0; i < count; i++) 210 { 211 res = rb->playlist_get_track_info(NULL, i, &info); 212 int attr = info.attr; 213 int index = info.index; 214 int display_index = info.display_index; 215 if (res < 0 || !current_playlist_filename_cb(info.filename, attr, index, display_index)) 216 break; 217 218 if (cps.buffer_index >= (cps.buffer_sz - CPS_MAX_ENTRY_SZ)) 219 { 220 logf("Buffer full, writing to disk"); 221 rb->write(cps.cue_fd, cps.buffer, cps.buffer_index); 222 cps.buffer_index = 0; 223 } 224 225 if (TIME_AFTER(*rb->current_tick, next_progress_tick)) 226 { 227 rb->splash_progress(i, count, "Processing current playlist %d", i); 228 int action = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); 229 if (action == ACTION_STD_CANCEL) 230 { 231 res = -10; 232 break; 233 } 234 if (rb->default_event_handler(action) == SYS_USB_CONNECTED) 235 { 236 cpuboost(false); 237 return PLUGIN_USB_CONNECTED; 238 } 239 next_progress_tick = *rb->current_tick + HZ / 2; 240 } 241 rb->yield(); 242 } 243 244 cpuboost(false); 245 246 return res >= 0; 247#undef cpuboost 248} 249 250static void init_new_cue(const char *playlist_filename) 251{ 252 if (cps.cue_fd >= 0) 253 { 254 rb->lseek(cps.cue_fd, 0, SEEK_SET); 255 rb->fdprintf(cps.cue_fd, "REM COMMENT \"generated by Rockbox version: " \ 256 "%s\"\n", rb->rbversion); 257 258 rb->fdprintf(cps.cue_fd, "TITLE \"%s\"\n", playlist_filename); /* top level TITLE */ 259 } 260} 261 262static void finalize_new_cue(void) 263{ 264 rb->write(cps.cue_fd, "\n", 1); 265 rb->close(cps.cue_fd); 266} 267 268static int create_new_cue(const char *filename) 269{ 270 char buf[MAX_PATH]; 271 if (!filename) 272 filename = "/Playlists/current.cue"; 273 const char *dot = rb->strrchr(filename, '.'); 274 int dotpos = 0; 275 if (dot) 276 dotpos = dot - filename; 277 rb->snprintf(buf, sizeof(buf), "%.*s.cue", dotpos, filename); 278 cps.cue_fd = rb->open(buf, O_WRONLY|O_CREAT|O_TRUNC, 0666); 279 280 init_new_cue(filename); 281 282 return cps.cue_fd; 283} 284 285enum plugin_status plugin_start(const void* parameter) 286{ 287 288 bool res; 289 rb->splash(HZ*2, ID2P(LANG_WAIT)); 290 291 const char *filename = parameter; 292 293 if (create_new_cue(filename) < 0) 294 { 295 rb->splashf(HZ, "creat() failed: %d", cps.cue_fd); 296 return PLUGIN_ERROR; 297 } 298 299 cps.buffer = rb->plugin_get_buffer(&cps.buffer_sz); 300 if (cps.buffer != NULL) 301 { 302 cps.buffer_index = 0; 303#ifdef STORAGE_WANTS_ALIGN 304 /* align start and length for DMA */ 305 STORAGE_ALIGN_BUFFER(cps.buffer, cps.buffer_sz); 306#else 307 /* align start and length to 32 bit */ 308 ALIGN_BUFFER(cps.buffer, cps.buffer_sz, 4); 309#endif 310 } 311 if (cps.buffer == NULL|| cps.buffer_sz < CPS_MAX_ENTRY_SZ) 312 { 313 rb->splashf(HZ, "No Buffers Available :( "); 314 return PLUGIN_ERROR; 315 } 316 317 if (filename && filename[0]) 318 res = rb->playlist_entries_iterate(filename, NULL, &playlist_filename_cb); 319 else 320 res = current_playlist_get_entries(); 321 322 if (res) 323 { 324 325 if (cps.buffer_index > 0) 326 { 327 rb->write(cps.cue_fd, cps.buffer, cps.buffer_index); 328 cps.buffer_index = 0; 329 330 } 331 rb->splashf(HZ * 2, 332 "Playist parsing SUCCESS %d entries written", cps.entries); 333 } 334 else 335 { 336 rb->splashf(HZ * 2, "Playist parsing FAILED after %d entries", cps.entries); 337 } 338 339 finalize_new_cue(); 340 341 if (!res) 342 return PLUGIN_ERROR; 343 return PLUGIN_OK; 344} 345 346/* 347#CUE FORMAT 348 CATALOG 349 CDTEXTFILE 350 FILE 351 FLAGS 352 INDEX 353 ISRC 354 PERFORMER 355 POSTGAP 356 PREGAP 357 REM 358 SONGWRITER 359 TITLE 360 TRACK 361#CD-TEXT https://wyday.com/cuesharp/specification.php 362 ARRANGER 363 COMPOSER 364 DISC_ID 365 GENRE 366 ISRC 367 MESSAGE 368 PERFORMER 369 SONGWRITER 370 TITLE 371 TOC_INFO 372 TOC_INFO2 373 UPC_EAN 374 SIZE_INFO 375*/