Browse Source

Add a simple CUE file parser library

Petteri Aimonen 2 years ago
parent
commit
039f151778
4 changed files with 452 additions and 0 deletions
  1. 230 0
      lib/CUEParser/CUEParser.cpp
  2. 106 0
      lib/CUEParser/CUEParser.h
  3. 104 0
      lib/CUEParser/CUEParser_test.cpp
  4. 12 0
      lib/CUEParser/library.json

+ 230 - 0
lib/CUEParser/CUEParser.cpp

@@ -0,0 +1,230 @@
+/*
+ * Simple CUE sheet parser suitable for embedded systems.
+ *
+ *  Copyright (c) 2023 Rabbit Hole Computing
+ *  
+ *  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 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+// Refer to e.g. https://www.gnu.org/software/ccd2cue/manual/html_node/CUE-sheet-format.html#CUE-sheet-format
+//
+// Example of a CUE file:
+// FILE "foo bar.bin" BINARY
+//   TRACK 01 MODE1/2048
+//     INDEX 01 00:00:00
+//   TRACK 02 AUDIO
+//     PREGAP 00:02:00
+//     INDEX 01 02:47:20
+//   TRACK 03 AUDIO
+//     INDEX 00 07:55:58
+//     INDEX 01 07:55:65
+
+
+#include "CUEParser.h"
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+CUEParser::CUEParser(const char *cue_sheet):
+    m_cue_sheet(cue_sheet)
+{
+    restart();
+}
+
+void CUEParser::restart()
+{
+    m_parse_pos = m_cue_sheet;
+    memset(&m_track_info, 0, sizeof(m_track_info));
+}
+
+const CUETrackInfo *CUEParser::next_track()
+{
+    bool got_track = false;
+    bool got_data = false;
+    while(start_line() && !(got_track && got_data))
+    {
+        if (strncasecmp(m_parse_pos, "FILE ", 5) == 0)
+        {
+            const char *p = read_quoted(m_parse_pos + 5, m_track_info.filename, sizeof(m_track_info.filename));
+            m_track_info.file_mode = parse_file_mode(skip_space(p));
+        }
+        else if (strncasecmp(m_parse_pos, "TRACK ", 6) == 0)
+        {
+            const char *track_num = skip_space(m_parse_pos + 6);
+            char *endptr;
+            m_track_info.track_number = strtoul(track_num, &endptr, 10);
+            m_track_info.track_mode = parse_track_mode(skip_space(endptr));
+            m_track_info.pregap_start = 0;
+            m_track_info.unstored_pregap_length = 0;
+            m_track_info.data_start = 0;
+            got_track = true;
+            got_data = false;
+        }
+        else if (strncasecmp(m_parse_pos, "PREGAP ", 7) == 0)
+        {
+            const char *time_str = skip_space(m_parse_pos + 7);
+            m_track_info.unstored_pregap_length = parse_time(time_str);
+        }
+        else if (strncasecmp(m_parse_pos, "INDEX ", 6) == 0)
+        {
+            const char *index_str = skip_space(m_parse_pos + 6);
+            char *endptr;
+            int index = strtoul(index_str, &endptr, 10);
+
+            const char *time_str = skip_space(endptr);
+            uint32_t time = parse_time(time_str);
+
+            if (index == 0)
+            {
+                m_track_info.pregap_start = time;
+            }
+            else if (index == 1)
+            {
+                m_track_info.data_start = time;
+                got_data = true;
+            }
+        }
+
+        next_line();
+    }
+
+    if (got_track && got_data)
+        return &m_track_info;
+    else
+        return nullptr;
+}
+
+bool CUEParser::start_line()
+{
+    // Skip initial whitespace
+    while (isspace(*m_parse_pos))
+    {
+        m_parse_pos++;
+    }
+    return *m_parse_pos != '\0';
+}
+
+void CUEParser::next_line()
+{
+    // Find end of current line
+    const char *p = m_parse_pos;
+    while (*p != '\n' && *p != '\0')
+    {
+        p++;
+    }
+
+    // Skip any linefeeds
+    while (*p == '\n' || *p == '\r')
+    {
+        p++;
+    }
+
+    m_parse_pos = p;
+}
+
+const char *CUEParser::skip_space(const char *p) const
+{
+    while (isspace(*p)) p++;
+    return p;
+}
+
+const char *CUEParser::read_quoted(const char *src, char *dest, int dest_size)
+{
+    // Search for starting quote
+    while (*src != '"')
+    {
+        if (*src == '\0' || *src == '\n')
+        {
+            // Unexpected end of line / file
+            dest[0] = '\0';
+            return src;
+        }
+
+        src++;
+    }
+    
+    src++;
+
+    // Copy text until ending quote
+    int len = 0;
+    while (*src != '"' && *src != '\0' && *src != '\n')
+    {
+        if (len < dest_size - 1)
+        {
+            dest[len++] = *src;
+        }
+
+        src++;
+    }
+
+    dest[len] = '\0';
+
+    if (*src == '"') src++;
+    return src;
+}
+
+uint32_t CUEParser::parse_time(const char *src)
+{
+    char *endptr;
+    uint32_t minutes = strtoul(src, &endptr, 10);
+    if (*endptr == ':') endptr++;
+    uint32_t seconds = strtoul(endptr, &endptr, 10);
+    if (*endptr == ':') endptr++;
+    uint32_t frames = strtoul(endptr, &endptr, 10);
+
+    return frames + 75 * (seconds + 60 * minutes);
+}
+
+CUEFileMode CUEParser::parse_file_mode(const char *src)
+{
+    if (strncasecmp(src, "BIN", 3) == 0)
+        return CUEFile_BINARY;
+    else if (strncasecmp(src, "MOTOROLA", 8) == 0)
+        return CUEFile_MOTOROLA;
+    else if (strncasecmp(src, "MP3", 3) == 0)
+        return CUEFile_MP3;
+    else if (strncasecmp(src, "WAV", 3) == 0)
+        return CUEFile_WAVE;
+    else if (strncasecmp(src, "AIFF", 4) == 0)
+        return CUEFile_AIFF;
+    else
+        return CUEFile_BINARY; // Default to binary mode
+}
+
+CUETrackMode CUEParser::parse_track_mode(const char *src)
+{
+    if (strncasecmp(src, "AUDIO", 5) == 0)
+        return CUETrack_AUDIO;
+    else if (strncasecmp(src, "CDG", 3) == 0)
+        return CUETrack_CDG;
+    else if (strncasecmp(src, "MODE1/2048", 10) == 0)
+        return CUETrack_MODE1_2048;
+    else if (strncasecmp(src, "MODE1/2352", 10) == 0)
+        return CUETrack_MODE1_2352;
+    else if (strncasecmp(src, "MODE2/2048", 10) == 0)
+        return CUETrack_MODE2_2048;
+    else if (strncasecmp(src, "MODE2/2324", 10) == 0)
+        return CUETrack_MODE2_2324;
+    else if (strncasecmp(src, "MODE2/2336", 10) == 0)
+        return CUETrack_MODE2_2336;
+    else if (strncasecmp(src, "MODE2/2352", 10) == 0)
+        return CUETrack_MODE2_2352;
+    else if (strncasecmp(src, "CDI/2336", 8) == 0)
+        return CUETrack_CDI_2336;
+    else if (strncasecmp(src, "CDI/2352", 8) == 0)
+        return CUETrack_CDI_2352;
+    else
+        return CUETrack_MODE1_2048; // Default to data track
+}

+ 106 - 0
lib/CUEParser/CUEParser.h

@@ -0,0 +1,106 @@
+/*
+ * Simple CUE sheet parser suitable for embedded systems.
+ *
+ *  Copyright (c) 2023 Rabbit Hole Computing
+ *  
+ *  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 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#ifndef CUE_MAX_FILENAME
+#define CUE_MAX_FILENAME 64
+#endif
+
+enum CUEFileMode
+{
+    CUEFile_BINARY = 0,
+    CUEFile_MOTOROLA,
+    CUEFile_MP3,
+    CUEFile_WAVE,
+    CUEFile_AIFF,
+};
+
+enum CUETrackMode
+{
+    CUETrack_AUDIO = 0,
+    CUETrack_CDG,
+    CUETrack_MODE1_2048,
+    CUETrack_MODE1_2352,
+    CUETrack_MODE2_2048,
+    CUETrack_MODE2_2324,
+    CUETrack_MODE2_2336,
+    CUETrack_MODE2_2352,
+    CUETrack_CDI_2336,
+    CUETrack_CDI_2352,
+};
+
+struct CUETrackInfo
+{
+    char filename[CUE_MAX_FILENAME+1];
+    CUEFileMode file_mode;
+    int track_number;
+    CUETrackMode track_mode;
+
+    uint32_t unstored_pregap_length;
+    uint32_t pregap_start;
+    uint32_t data_start;
+};
+
+class CUEParser
+{
+public:
+    // Initialize the class to parse data from string.
+    // The string must remain valid for the lifetime of this object.
+    CUEParser(const char *cue_sheet);
+
+    // Restart parsing from beginning of file
+    void restart();
+
+    // Get information for next track.
+    // Returns nullptr when there are no more tracks.
+    // The returned pointer remains valid until next call to next_track()
+    // or destruction of this object.
+    const CUETrackInfo *next_track();
+
+protected:
+    const char *m_cue_sheet;
+    const char *m_parse_pos;
+    CUETrackInfo m_track_info;
+
+    // Skip any whitespace at beginning of line.
+    // Returns false if at end of string.
+    bool start_line();
+
+    // Advance parser to next line
+    void next_line();
+
+    // Skip spaces in string, return pointer to first non-space character
+    const char *skip_space(const char *p) const;
+
+    // Read text starting with " and ending with next "
+    // Returns pointer to character after ending quote.
+    const char *read_quoted(const char *src, char *dest, int dest_size);
+
+    // Parse time from MM:SS:FF format to frame number
+    uint32_t parse_time(const char *src);
+
+    // Parse file mode into enum
+    CUEFileMode parse_file_mode(const char *src);
+
+    // Parse track mode into enum
+    CUETrackMode parse_track_mode(const char *src);
+};

+ 104 - 0
lib/CUEParser/CUEParser_test.cpp

@@ -0,0 +1,104 @@
+#include "CUEParser.h"
+#include <stdio.h>
+#include <string.h>
+
+/* Unit test helpers */
+#define COMMENT(x) printf("\n----" x "----\n");
+#define TEST(x) \
+    if (!(x)) { \
+        fprintf(stderr, "\033[31;1mFAILED:\033[22;39m %s:%d %s\n", __FILE__, __LINE__, #x); \
+        status = 1; \
+    } else { \
+        printf("\033[32;1mOK:\033[22;39m %s\n", #x); \
+    }
+
+int test_basics()
+{
+    int status = 0;
+    const char *cue_sheet = R"(
+FILE "Image Name.bin" BINARY
+  TRACK 01 MODE1/2048
+    INDEX 01 00:00:00
+  TRACK 02 AUDIO
+    PREGAP 00:02:00
+    INDEX 01 02:47:20
+  TRACK 03 AUDIO
+    INDEX 00 07:55:58
+    INDEX 01 07:55:65
+FILE "Sound.wav" WAVE
+  TRACK 11 AUDIO
+    INDEX 00 00:00:00
+    INDEX 01 00:02:00
+    )";
+
+    CUEParser parser(cue_sheet);
+
+    COMMENT("Test TRACK 01 (data)");
+    const CUETrackInfo *track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Image Name.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->track_number == 1);
+        TEST(track->track_mode == CUETrack_MODE1_2048);
+        TEST(track->unstored_pregap_length == 0);
+        TEST(track->data_start == 0);
+    }
+
+    COMMENT("Test TRACK 02 (audio with pregap)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Image Name.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->track_number == 2);
+        TEST(track->track_mode == CUETrack_AUDIO);
+        TEST(track->unstored_pregap_length == 2 * 75);
+        TEST(track->data_start == ((2 * 60) + 47) * 75 + 20);
+    }
+
+    COMMENT("Test TRACK 03 (audio with index 0)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Image Name.bin") == 0);
+        TEST(track->file_mode == CUEFile_BINARY);
+        TEST(track->track_number == 3);
+        TEST(track->track_mode == CUETrack_AUDIO);
+        TEST(track->pregap_start == ((7 * 60) + 55) * 75 + 58);
+        TEST(track->data_start == ((7 * 60) + 55) * 75 + 65);
+    }
+
+    COMMENT("Test TRACK 11 (audio from wav)");
+    track = parser.next_track();
+    TEST(track != NULL);
+    if (track)
+    {
+        TEST(strcmp(track->filename, "Sound.wav") == 0);
+        TEST(track->file_mode == CUEFile_WAVE);
+        TEST(track->track_number == 11);
+        TEST(track->track_mode == CUETrack_AUDIO);
+        TEST(track->pregap_start == 0);
+        TEST(track->data_start == 2 * 75);
+    }
+
+    COMMENT("Test end of file");
+    track = parser.next_track();
+    TEST(track == NULL);
+
+    COMMENT("Test restart");
+    parser.restart();
+    track = parser.next_track();
+    TEST(track != NULL && track->track_number == 1);
+
+    return status;
+}
+
+
+int main()
+{
+    return test_basics();
+}

+ 12 - 0
lib/CUEParser/library.json

@@ -0,0 +1,12 @@
+{
+    "name": "CUEParser",
+    "version": "1.0.0",
+    "repository": { "type": "git", "url": "https://github.com/ZuluSCSI/ZuluSCSI-firmware.git"},
+    "authors": [{ "name": "Petteri Aimonen", "email": "jpa@git.mail.kapsi.fi" }],
+    "license": "GPL-3.0-or-later",
+    "frameworks": "*",
+    "platforms": "*",
+    "build": {
+        "srcFilter": "CUEParser.cpp"
+    }
+}