summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Wilgus <wilgus.william@gmail.com>2024-07-15 23:33:29 -0400
committerWilliam Wilgus <wilgus.william@gmail.com>2024-07-20 07:27:01 -0400
commit7e90760a48a0dcd0e6d7133022ffb1736083dd46 (patch)
tree6f67f1a66b7c4b65a767ee078971b20ac6ab2a79
parent072228bb70c71bd05eff848d15c36d7f540972b3 (diff)
downloadrockbox-7e90760a48.tar.gz
rockbox-7e90760a48.zip
[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
-rw-r--r--apps/gui/skin_engine/skin_tokens.c2
-rw-r--r--apps/plugin.c1
-rw-r--r--apps/plugin.h1
-rw-r--r--apps/plugins/CATEGORIES1
-rw-r--r--apps/plugins/SOURCES1
-rw-r--r--apps/plugins/cue_playlist.c375
-rw-r--r--apps/plugins/viewers.config1
-rw-r--r--lib/rbcodec/metadata/metadata.c8
-rw-r--r--lib/rbcodec/metadata/metadata.h1
9 files changed, 390 insertions, 1 deletions
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
diff --git a/lib/rbcodec/metadata/metadata.c b/lib/rbcodec/metadata/metadata.c
index 28cef46d92..670c37fcfd 100644
--- a/lib/rbcodec/metadata/metadata.c
+++ b/lib/rbcodec/metadata/metadata.c
@@ -304,6 +304,14 @@ int get_audio_base_codec_type(int type)
return base_type;
}
+const char * get_codec_string(int type)
+{
+ if (type < 0 || type >= AFMT_NUM_CODECS)
+ type = AFMT_UNKNOWN;
+
+ return audio_formats[type].label;
+}
+
/* Get the basic audio type */
bool rbcodec_format_is_atomic(int afmt)
{
diff --git a/lib/rbcodec/metadata/metadata.h b/lib/rbcodec/metadata/metadata.h
index a0ba0376c6..ce1b153833 100644
--- a/lib/rbcodec/metadata/metadata.h
+++ b/lib/rbcodec/metadata/metadata.h
@@ -333,6 +333,7 @@ void wipe_mp3entry(struct mp3entry *id3);
void fill_metadata_from_path(struct mp3entry *id3, const char *trackname);
int get_audio_base_codec_type(int type);
+const char * get_codec_string(int type);
bool rbcodec_format_is_atomic(int afmt);
bool format_buffers_with_offset(int afmt);