From 7e90760a48a0dcd0e6d7133022ffb1736083dd46 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Mon, 15 Jul 2024 23:33:29 -0400 Subject: [Feature] Playlis to cue plugin generate valid cue files from a playlist uses remarks to store extra id3 info and display and playlist index Change-Id: I00c9f6389445bb601dde6eb8f36157044024f8cb --- apps/gui/skin_engine/skin_tokens.c | 2 +- apps/plugin.c | 1 + apps/plugin.h | 1 + apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 1 + apps/plugins/cue_playlist.c | 375 +++++++++++++++++++++++++++++++++++++ apps/plugins/viewers.config | 1 + 7 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 apps/plugins/cue_playlist.c (limited to 'apps') diff --git a/apps/gui/skin_engine/skin_tokens.c b/apps/gui/skin_engine/skin_tokens.c index ba9396ae74..082619432f 100644 --- a/apps/gui/skin_engine/skin_tokens.c +++ b/apps/gui/skin_engine/skin_tokens.c @@ -74,7 +74,7 @@ static const char* get_codectype(const struct mp3entry* id3) { if (id3 && id3->codectype < AFMT_NUM_CODECS) { - return audio_formats[id3->codectype].label; + return get_codec_string(id3->codectype); } else { return NULL; } diff --git a/apps/plugin.c b/apps/plugin.c index 7c80e5c6e1..bd849dad50 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -841,6 +841,7 @@ static const struct plugin_api rockbox_api = { filetype_get_plugin, playlist_entries_iterate, lang_is_rtl, + get_codec_string, }; static int plugin_buffer_handle; diff --git a/apps/plugin.h b/apps/plugin.h index 4dbd9d04c0..66c895d0db 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -979,6 +979,7 @@ struct plugin_api { struct playlist_insert_context *pl_context, bool (*action_cb)(const char *file_name)); int (*lang_is_rtl)(void); + const char* (*get_codec_string)(int codectype); }; /* plugin header */ diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index 5e22bea980..366c42c75b 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -23,6 +23,7 @@ clock,apps codebuster,games credits,viewers cube,demos +cue_playlist,viewers dart_scorer,apps db_commit,apps db_folder_select,viewers diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 8028758ef0..0b1d48d5de 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -9,6 +9,7 @@ tagcache/tagcache.c chessclock.c credits.c cube.c +cue_playlist.c dart_scorer.c dict.c jackpot.c diff --git a/apps/plugins/cue_playlist.c b/apps/plugins/cue_playlist.c new file mode 100644 index 0000000000..d3f64fcc50 --- /dev/null +++ b/apps/plugins/cue_playlist.c @@ -0,0 +1,375 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2024 William Wilgus + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* convert supplied playlist file to a .cue file */ + +#include "plugin.h" + +#if defined(DEBUG) || defined(SIMULATOR) + #define logf(...) rb->debugf(__VA_ARGS__); rb->debugf("\n") +#elif defined(ROCKBOX_HAS_LOGF) + #define logf rb->logf +#else + #define logf(...) do { } while(0) +#endif + +#define CPS_MAX_ENTRY_SZ (4 *1024) +#define TDINDENT " " /* prepend spaces for track data formatting */ + +static struct cps +{ + char *buffer; + size_t buffer_sz; + size_t buffer_index; + int cue_fd; + int entries; +} cps; + +static int sprfunc(void *ptr, int letter) +{ + /* callback for vuprintf */ + (void) ptr; + if (cps.buffer_index < cps.buffer_sz - 1) + { + cps.buffer[cps.buffer_index++] = letter; + return 1; + } + return -1; +} + +void cps_printf(const char *fmt, ...) +{ + /* NOTE! this is made for flushing the buffer to disk -- WARNING + * Nothing is NULL terminated here unless explicitly made so.. \0 */ + va_list ap; + va_start(ap, fmt); + rb->vuprintf(sprfunc, NULL, fmt, ap); + va_end(ap); +} + +static uint32_t write_metadata_tags(struct mp3entry *id3) +{ +/* check an ID3 and write any numeric tags and valid string tags (non empty) */ +#define ISVALID(s) (s != NULL && s[0] != '\0') + uint32_t tag_flags = 0; + + const char *performer = rb->str(LANG_TAGNAVI_UNTAGGED); + if (ISVALID(id3->artist)) + performer = id3->artist; + else if (ISVALID(id3->albumartist)) + performer = id3->albumartist; + + const char *title = rb->str(LANG_TAGNAVI_UNTAGGED); + if (ISVALID(id3->title)) + title = id3->title; + +#define PERFORMER_TITLE_SZ TDINDENT "PERFORMER \"%s\"\n" \ + TDINDENT "TITLE \"%s\"\n" \ + TDINDENT "SIZE_INFO %ld\n" + + cps_printf(PERFORMER_TITLE_SZ, performer, title, id3->filesize); + if (ISVALID(id3->composer)) + cps_printf(TDINDENT "COMPOSER \"%s\"\n", id3->composer); + + cps_printf(TDINDENT "INDEX 01 00:00:00\n"); + +#define ID3_TAG_NUM(theid3, TAGID, flag) \ + {cps_printf(TDINDENT "REM %s %lu\n", TAGID, (unsigned long)theid3);tag_flags |= flag;} +#define ID3_TAG_STR(theid3, TAGID, flag) if (ISVALID(theid3)) \ + {cps_printf(TDINDENT "REM %s \"%s\"\n", TAGID, theid3);tag_flags |= flag;} + + ID3_TAG_STR(id3->album, "ALBUM", 0x01); + ID3_TAG_STR(id3->albumartist, "ALBUMARTIST", 0x02); + ID3_TAG_STR(id3->comment, "COMMENT", 0x04); + ID3_TAG_STR(id3->genre_string, "GENRE", 0x08); + ID3_TAG_STR(id3->disc_string, "DISC", 0x10); + ID3_TAG_STR(id3->track_string, "TRACK", 0x20); + ID3_TAG_STR(id3->grouping, "GROUPING", 0x40); + ID3_TAG_STR(id3->mb_track_id, "MB_TRACK_ID", 0x80); + ID3_TAG_STR(rb->get_codec_string(id3->codectype), "ID3_CODEC", 0x100); + + ID3_TAG_NUM(id3->discnum, "DISCNUM", 0x200); + ID3_TAG_NUM(id3->tracknum, "TRACKNUM", 0x400); + ID3_TAG_NUM(id3->length, "LENGTH", 0x800); + ID3_TAG_NUM(id3->bitrate, "BITRATE", 0x1000); + ID3_TAG_NUM(id3->frequency, "FREQUENCY", 0x2000); + ID3_TAG_NUM(id3->track_level, "TRACK_LEVEL",0x4000); + ID3_TAG_NUM(id3->album_level, "ALBUM_LEVEL", 0x8000); +#undef ID3_TAG_STR +#undef ID3_TAG_NUM +#undef IS_VALID + return tag_flags; +} + +static bool current_playlist_filename_cb(const char *filename, int attr, int index, int display_index) +{ + /* worker function for writing the actual cue data */ + int szpos = 0; /* records position of the size string */ + int namepos = 0; /* records position of the end of filename string */ + struct mp3entry id3; + + logf("found: %s", filename); + + uint32_t id3_flags = 0; + bool have_metadata = rb->get_metadata(&id3, -1, filename); + + if (!have_metadata && !rb->file_exists(filename)) + return false; +#define RB_ENTRY_DATA_FMT "REM RB_ENTRY_DATA " \ + "\"DISPLAY_INDEX %012u " \ + "PLAYLIST_INDEX %012u " \ + "SIZE %n%012zu TAGS %012lu\"\n" + + const char *audiotype = "WAVE"; /* everything except MP3 */ + const char *skipped = "";; + const char *queued = ""; + + size_t entry_start = cps.buffer_index; /* get start to calculate final size */ + + cps_printf(RB_ENTRY_DATA_FMT, display_index, index, &szpos, 0, 0UL); + + size_t file_start = cps.buffer_index; + cps_printf("FILE \"%s%n\"", filename, &namepos); + + if (cps.buffer[file_start + namepos - 1] == '3') + audiotype = "MP3"; + + cps_printf(" %s\n", audiotype); + + if (attr & PLAYLIST_ATTR_SKIPPED) + skipped = TDINDENT "REM SKIPPED\n"; + if (attr & PLAYLIST_ATTR_QUEUED) + queued = TDINDENT "REM QUEUED\n"; + + cps_printf(" TRACK %d AUDIO\n%s%s", display_index, skipped, queued); + + if (have_metadata) + id3_flags = write_metadata_tags(&id3); + + if (cps.buffer_index - entry_start < CPS_MAX_ENTRY_SZ) + { + /* place the write pointer at the size entry so we can update size + tags*/ + size_t index = cps.buffer_index; + cps.buffer_index = entry_start + szpos; + cps_printf("%012zu TAGS %012lu", (index - entry_start), id3_flags); + cps.buffer_index = index; /* set the write pointer back at the end */ + } + else + { + rb->splashf(HZ * 3, "Entry too large %s", filename); + cps.buffer_index = entry_start; + return false; + } + + cps.entries++; + return true; +} + +static bool playlist_filename_cb(const char *filename) +{ + /* get entries from an on-disk playlist */ + return current_playlist_filename_cb(filename, 0, + cps.entries, cps.entries); +} + +static bool current_playlist_get_entries(void) +{ + /* get entries from a loaded playlist ( may have queued or skipped tracks ) */ +#if defined(HAVE_ADJUSTABLE_CPU_FREQ) +#define cpuboost(enable) rb->cpu_boost(enable); +#else +#define cpuboost(enable) do{ } while(0) +#endif + struct playlist_track_info info; + int count = rb->playlist_amount(); + int i, res = 0; + logf("current playlist contains %d entries", count); + + cpuboost(true); + + long next_progress_tick = *rb->current_tick; + for (i = 0; i < count; i++) + { + res = rb->playlist_get_track_info(NULL, i, &info); + int attr = info.attr; + int index = info.index; + int display_index = info.display_index; + if (res < 0 || !current_playlist_filename_cb(info.filename, attr, index, display_index)) + break; + + if (cps.buffer_index >= (cps.buffer_sz - CPS_MAX_ENTRY_SZ)) + { + logf("Buffer full, writing to disk"); + rb->write(cps.cue_fd, cps.buffer, cps.buffer_index); + cps.buffer_index = 0; + } + + if (TIME_AFTER(*rb->current_tick, next_progress_tick)) + { + rb->splash_progress(i, count, "Processing current playlist %d", i); + int action = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); + if (action == ACTION_STD_CANCEL) + { + res = -10; + break; + } + if (rb->default_event_handler(action) == SYS_USB_CONNECTED) + { + cpuboost(false); + return PLUGIN_USB_CONNECTED; + } + next_progress_tick = *rb->current_tick + HZ / 2; + } + rb->yield(); + } + + cpuboost(false); + + return res >= 0; +#undef cpuboost +} + +static void init_new_cue(const char *playlist_filename) +{ + if (cps.cue_fd >= 0) + { + rb->lseek(cps.cue_fd, 0, SEEK_SET); + rb->fdprintf(cps.cue_fd, "REM COMMENT \"generated by Rockbox version: " \ + "%s\"\n", rb->rbversion); + + rb->fdprintf(cps.cue_fd, "TITLE \"%s\"\n", playlist_filename); /* top level TITLE */ + } +} + +static void finalize_new_cue(void) +{ + rb->write(cps.cue_fd, "\n", 1); + rb->close(cps.cue_fd); +} + +static int create_new_cue(const char *filename) +{ + char buf[MAX_PATH]; + if (!filename) + filename = "/Playlists/current.cue"; + const char *dot = rb->strrchr(filename, '.'); + int dotpos = 0; + if (dot) + dotpos = dot - filename; + rb->snprintf(buf, sizeof(buf), "%.*s.cue", dotpos, filename); + cps.cue_fd = rb->open(buf, O_WRONLY|O_CREAT|O_TRUNC, 0666); + + init_new_cue(filename); + + return cps.cue_fd; +} + +enum plugin_status plugin_start(const void* parameter) +{ + + bool res; + rb->splash(HZ*2, ID2P(LANG_WAIT)); + + const char *filename = parameter; + + if (create_new_cue(filename) < 0) + { + rb->splashf(HZ, "creat() failed: %d", cps.cue_fd); + return PLUGIN_ERROR; + } + + cps.buffer = rb->plugin_get_buffer(&cps.buffer_sz); + if (cps.buffer != NULL) + { + cps.buffer_index = 0; +#ifdef STORAGE_WANTS_ALIGN + /* align start and length for DMA */ + STORAGE_ALIGN_BUFFER(cps.buffer, cps.buffer_sz); +#else + /* align start and length to 32 bit */ + ALIGN_BUFFER(cps.buffer, cps.buffer_sz, 4); +#endif + } + if (cps.buffer == NULL|| cps.buffer_sz < CPS_MAX_ENTRY_SZ) + { + rb->splashf(HZ, "No Buffers Available :( "); + return PLUGIN_ERROR; + } + + if (filename && filename[0]) + res = rb->playlist_entries_iterate(filename, NULL, &playlist_filename_cb); + else + res = current_playlist_get_entries(); + + if (res) + { + + if (cps.buffer_index > 0) + { + rb->write(cps.cue_fd, cps.buffer, cps.buffer_index); + cps.buffer_index = 0; + + } + rb->splashf(HZ * 2, + "Playist parsing SUCCESS %d entries written", cps.entries); + } + else + { + rb->splashf(HZ * 2, "Playist parsing FAILED after %d entries", cps.entries); + } + + finalize_new_cue(); + + if (!res) + return PLUGIN_ERROR; + return PLUGIN_OK; +} + +/* +#CUE FORMAT + CATALOG + CDTEXTFILE + FILE + FLAGS + INDEX + ISRC + PERFORMER + POSTGAP + PREGAP + REM + SONGWRITER + TITLE + TRACK +#CD-TEXT https://wyday.com/cuesharp/specification.php + ARRANGER + COMPOSER + DISC_ID + GENRE + ISRC + MESSAGE + PERFORMER + SONGWRITER + TITLE + TOC_INFO + TOC_INFO2 + UPC_EAN + SIZE_INFO +*/ diff --git a/apps/plugins/viewers.config b/apps/plugins/viewers.config index 45bbfeef0b..d745a29cfe 100644 --- a/apps/plugins/viewers.config +++ b/apps/plugins/viewers.config @@ -34,6 +34,7 @@ rvf,viewers/video,4 mp3,viewers/vbrfix,5 m3u,viewers/search,- m3u,viewers/iriverify,- +m3u,viewers/cue_playlist,- lrc,apps/lrcplayer,1 lrc8,apps/lrcplayer,1 snc,apps/lrcplayer,1 -- cgit v1.2.3