浏览代码

Merge branch 'main' of github.com:ZuluSCSI/ZuluSCSI-firmware

Morio 1 年之前
父节点
当前提交
b3262feb93

+ 5 - 1
README.md

@@ -24,7 +24,11 @@ To use a BIN/CUE image with ZuluSCSI, name both files with the same part before
 For example `CD3.bin` and `CD3.cue`.
 The cue file contains the original file name, but it doesn't matter for ZuluSCSI.
 
-BIN/CUE support is currently experimental. Supported track types are `AUDIO`, `MODE1/2048` and `MODE1/2352`.
+If the image consists of one `.cue` file and multiple `.bin` files, they need to be placed in a separate subfolder.
+For example, create `CD3` folder, then `MyGame` subfolder and put the `.cue` and `.bin` files there.
+The `.bin` file names must then match the names specified in the `.cue` file.
+
+Supported track types are `AUDIO`, `MODE1/2048` and `MODE1/2352`.
 
 Creating new image files
 ------------------------

+ 0 - 9
lib/CUEParser/library.json

@@ -1,9 +0,0 @@
-{
-    "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": "*"
-}

+ 0 - 287
lib/CUEParser/src/CUEParser.cpp

@@ -1,287 +0,0 @@
-/*
- * 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(): CUEParser("")
-{
-
-}
-
-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()
-{
-    // Previous track info is needed to track file offset
-    uint32_t prev_track_start = m_track_info.track_start;
-    uint32_t prev_sector_length = get_sector_length(m_track_info.file_mode, m_track_info.track_mode); // Defaults to 2352 before first track
-
-    bool got_track = false;
-    bool got_data = false;
-    bool got_pause = false; // true if a period of silence (INDEX 00) was encountered for a track
-    while(!(got_track && got_data) && start_line())
-    {
-        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));
-            m_track_info.file_offset = 0;
-            m_track_info.track_mode = CUETrack_AUDIO;
-            prev_track_start = 0;
-            prev_sector_length = get_sector_length(m_track_info.file_mode, m_track_info.track_mode);
-        }
-        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.sector_length = get_sector_length(m_track_info.file_mode, m_track_info.track_mode);
-            m_track_info.unstored_pregap_length = 0;
-            m_track_info.data_start = 0;
-            m_track_info.track_start = 0;
-            got_track = true;
-            got_data = false;
-            got_pause = false;
-        }
-        else if (strncasecmp(m_parse_pos, "PREGAP ", 7) == 0)
-        {
-            // Unstored pregap, which offsets the data start on CD but does not
-            // affect the offset in data file.
-            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)
-            {
-                // Stored pregap that is present both on CD and in data file
-                m_track_info.track_start = time;
-                got_pause = true;
-            }
-            else if (index == 1)
-            {
-                // Data content of the track
-                m_track_info.data_start = time;
-                got_data = true;
-            }
-        }
-
-        next_line();
-    }
-
-    if (got_data && !got_pause)
-    {
-        m_track_info.track_start = m_track_info.data_start;
-        m_track_info.data_start += m_track_info.unstored_pregap_length;
-    }
-
-    if (got_track && got_data)
-    {
-        m_track_info.file_offset += (uint64_t)(m_track_info.track_start - prev_track_start) * prev_sector_length;
-        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
-}
-
-uint32_t CUEParser::get_sector_length(CUEFileMode filemode, CUETrackMode trackmode)
-{
-    if (filemode == CUEFile_BINARY || filemode == CUEFile_MOTOROLA)
-    {
-        switch (trackmode)
-        {
-            case CUETrack_AUDIO:        return 2352;
-            case CUETrack_CDG:          return 2448;
-            case CUETrack_MODE1_2048:   return 2048;
-            case CUETrack_MODE1_2352:   return 2352;
-            case CUETrack_MODE2_2048:   return 2048;
-            case CUETrack_MODE2_2324:   return 2324;
-            case CUETrack_MODE2_2336:   return 2336;
-            case CUETrack_MODE2_2352:   return 2352;
-            case CUETrack_CDI_2336:     return 2336;
-            case CUETrack_CDI_2352:     return 2352;
-            default:                    return 2048;
-        }
-    }
-    else
-    {
-        return 0;
-    }
-}

+ 0 - 126
lib/CUEParser/src/CUEParser.h

@@ -1,126 +0,0 @@
-/*
- * 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
-{
-    // Source file name and file type, and offset to start of track data in bytes.
-    char filename[CUE_MAX_FILENAME+1];
-    CUEFileMode file_mode;
-    uint64_t file_offset; // corresponds to data_start below
-
-    // Track number and mode in CD format
-    int track_number;
-    CUETrackMode track_mode;
-
-    // Sector length for this track in bytes, assuming BINARY or MOTOROLA file modes.
-    uint32_t sector_length;
-
-    // The CD frames of PREGAP time at the start of this track, or 0 if none are present.
-    // These frames of silence are not stored in the underlying data file.
-    uint32_t unstored_pregap_length;
-
-    // LBA start position of the data area (INDEX 01) of this track (in CD frames)
-    uint32_t data_start;
-
-    // LBA for the beginning of the track, which will be INDEX 00 if that is present.
-    // If there is unstored PREGAP, it's added between track_start and data_start.
-    // Otherwise this will be INDEX 01 matching data_start above.
-    uint32_t track_start;
-};
-
-class CUEParser
-{
-public:
-    CUEParser();
-
-    // 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);
-
-    // Get sector length in file from track mode
-    uint32_t get_sector_length(CUEFileMode filemode, CUETrackMode trackmode);
-};

+ 0 - 245
lib/CUEParser/test/CUEParser_test.cpp

@@ -1,245 +0,0 @@
-#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 = false; \
-    } else { \
-        printf("\033[32;1mOK:\033[22;39m %s\n", #x); \
-    }
-
-bool test_basics()
-{
-    bool status = true;
-    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_basics()");
-    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->file_offset == 0);
-        TEST(track->track_number == 1);
-        TEST(track->track_mode == CUETrack_MODE1_2048);
-        TEST(track->sector_length == 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);
-    uint32_t start2 = ((2 * 60) + 47) * 75 + 20;
-    if (track)
-    {
-        TEST(strcmp(track->filename, "Image Name.bin") == 0);
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 2048 * start2);
-        TEST(track->track_number == 2);
-        TEST(track->track_mode == CUETrack_AUDIO);
-        TEST(track->sector_length == 2352);
-        TEST(track->unstored_pregap_length == 2 * 75);
-        TEST(track->data_start == start2 + 2 * 75);
-    }
-
-    COMMENT("Test TRACK 03 (audio with index 0)");
-    track = parser.next_track();
-    TEST(track != NULL);
-    uint32_t start3_i0 = ((7 * 60) + 55) * 75 + 58;
-    uint32_t start3_i1 = ((7 * 60) + 55) * 75 + 65;
-    if (track)
-    {
-        TEST(strcmp(track->filename, "Image Name.bin") == 0);
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 2048 * start2 + 2352 * (start3_i0 - start2));
-        TEST(track->track_number == 3);
-        TEST(track->track_mode == CUETrack_AUDIO);
-        TEST(track->sector_length == 2352);
-        TEST(track->track_start == start3_i0);
-        TEST(track->data_start == start3_i1);
-    }
-
-    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->file_offset == 0);
-        TEST(track->track_number == 11);
-        TEST(track->track_mode == CUETrack_AUDIO);
-        TEST(track->sector_length == 0);
-        TEST(track->track_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;
-}
-
-bool test_datatracks()
-{
-    bool status = true;
-    const char *cue_sheet = R"(
-FILE "beos-5.0.3-professional-gobe.bin" BINARY
-TRACK 01 MODE1/2352
-    INDEX 01 00:00:00
-TRACK 02 MODE1/2352
-    INDEX 01 10:48:58
-TRACK 03 MODE1/2352
-    INDEX 01 46:07:03
-    )";
-
-    CUEParser parser(cue_sheet);
-
-    COMMENT("test_datatracks()");
-    COMMENT("Test TRACK 01 (data)");
-    const CUETrackInfo *track = parser.next_track();
-    TEST(track != NULL);
-    if (track)
-    {
-        TEST(strcmp(track->filename, "beos-5.0.3-professional-gobe.bin") == 0);
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 0);
-        TEST(track->track_number == 1);
-        TEST(track->track_mode == CUETrack_MODE1_2352);
-        TEST(track->sector_length == 2352);
-        TEST(track->unstored_pregap_length == 0);
-        TEST(track->data_start == 0);
-        TEST(track->track_start == 0);
-    }
-
-    COMMENT("Test TRACK 02 (data)");
-    track = parser.next_track();
-    TEST(track != NULL);
-    if (track)
-    {
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 0x6D24560);
-        TEST(track->track_number == 2);
-        TEST(track->track_mode == CUETrack_MODE1_2352);
-        TEST(track->sector_length == 2352);
-        TEST(track->unstored_pregap_length == 0);
-        TEST(track->data_start == ((10 * 60) + 48) * 75 + 58);
-        TEST(track->track_start == ((10 * 60) + 48) * 75 + 58);
-    }
-
-    COMMENT("Test TRACK 03 (data)");
-    track = parser.next_track();
-    TEST(track != NULL);
-    if (track)
-    {
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 0x1D17E780);
-        TEST(track->track_number == 3);
-        TEST(track->track_mode == CUETrack_MODE1_2352);
-        TEST(track->sector_length == 2352);
-        TEST(track->unstored_pregap_length == 0);
-        TEST(track->data_start == ((46 * 60) + 7) * 75 + 3);
-        TEST(track->track_start == ((46 * 60) + 7) * 75 + 3);
-    }
-
-    track = parser.next_track();
-    TEST(track == NULL);
-
-    return status;
-}
-
-bool test_datatrackpregap()
-{
-    bool status = true;
-    const char *cue_sheet = R"(
-FILE "issue422.bin" BINARY
-  TRACK 01 AUDIO
-    INDEX 01 00:00:00
-  TRACK 02 MODE1/2352
-    PREGAP 00:02:00
-    INDEX 01 01:06:19
-    )";
-
-    CUEParser parser(cue_sheet);
-
-    COMMENT("test_datatrackpregap()");
-    COMMENT("Test TRACK 01 (audio)");
-    const CUETrackInfo *track = parser.next_track();
-    TEST(track != NULL);
-    if (track)
-    {
-        TEST(strcmp(track->filename, "issue422.bin") == 0);
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 0);
-        TEST(track->track_number == 1);
-        TEST(track->track_mode == CUETrack_AUDIO);
-        TEST(track->sector_length == 2352);
-        TEST(track->unstored_pregap_length == 0);
-        TEST(track->data_start == 0);
-        TEST(track->track_start == 0);
-    }
-
-    COMMENT("Test TRACK 02 (data)");
-    track = parser.next_track();
-    TEST(track != NULL);
-    if (track)
-    {
-        TEST(strcmp(track->filename, "issue422.bin") == 0);
-        TEST(track->file_mode == CUEFile_BINARY);
-        TEST(track->file_offset == 0xB254B0);
-        TEST(track->track_number == 2);
-        TEST(track->track_mode == CUETrack_MODE1_2352);
-        TEST(track->sector_length == 2352);
-        TEST(track->unstored_pregap_length == 75 * 2);
-        TEST(track->data_start == (60 + 6 + 2) * 75 + 19);
-        TEST(track->track_start == (60 + 6) * 75 + 19);
-    }
-
-    track = parser.next_track();
-    TEST(track == NULL);
-
-    return status;
-}
-
-
-int main()
-{
-    if (test_basics() && test_datatracks() && test_datatrackpregap())
-    {
-        return 0;
-    }
-    else
-    {
-        printf("Some tests failed\n");
-        return 1;
-    }
-}

+ 0 - 7
lib/CUEParser/test/Makefile

@@ -1,7 +0,0 @@
-# Run basic unit tests for the CUEParser library
-
-all: CUEParser_test
-	./CUEParser_test
-
-CUEParser_test: CUEParser_test.cpp ../src/CUEParser.cpp
-	g++ -Wall -Wextra -g -ggdb -o $@ -I ../src $^

+ 1 - 1
lib/ZuluSCSI_platform_GD32F205/gd32_cdc_acm_core.c

@@ -233,7 +233,7 @@ static const usb_desc_str product_string =
 };
 
 /* USBD serial string */
-static usb_desc_str serial_string = 
+static const usb_desc_str serial_string =
 {
     .header = 
      {

+ 1 - 1
lib/ZuluSCSI_platform_GD32F205/scsi_accel_dma.cpp

@@ -704,4 +704,4 @@ static void greenpak_stop_dma()
     exti_interrupt_disable(GREENPAK_PLD_IO2_EXTI);
 }
 
-#endif
+#endif

+ 1 - 1
lib/ZuluSCSI_platform_GD32F205/usbd_msc_core.c

@@ -167,7 +167,7 @@ static __ALIGN_BEGIN const usb_desc_str product_string __ALIGN_END = {
 };
 
 /* USBD serial string */
-static __ALIGN_BEGIN usb_desc_str serial_string __ALIGN_END = {
+static __ALIGN_BEGIN const usb_desc_str serial_string __ALIGN_END = {
     .header =
     {
         .bLength         = USB_STRING_LEN(12U),

+ 6 - 5
platformio.ini

@@ -22,7 +22,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_template
     SCSI2SD
-    CUEParser
+    CUEParser=https://github.com/rabbitholecomputing/CUEParser
 
 ; ZuluSCSI V1.0 hardware platform with GD32F205 CPU.
 [env:ZuluSCSIv1_0]
@@ -34,12 +34,13 @@ board_build.ldscript = lib/ZuluSCSI_platform_GD32F205/zuluscsi_gd32f205.ld
 ldscript_bootloader = lib/ZuluSCSI_platform_GD32F205/zuluscsi_gd32f205_btldr.ld
 framework = spl
 lib_compat_mode = off
+lib_ldf_mode = chain+
 lib_deps =
     SdFat=https://github.com/rabbitholecomputing/SdFat#2.2.3-gpt
     minIni
     ZuluSCSI_platform_GD32F205
     SCSI2SD
-    CUEParser
+    CUEParser=https://github.com/rabbitholecomputing/CUEParser
     GD32F20x_usbfs_library
 upload_protocol = stlink
 platform_packages = platformio/toolchain-gccarmnoneeabi@1.100301.220327
@@ -115,7 +116,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_RP2040
     SCSI2SD
-    CUEParser
+    CUEParser=https://github.com/rabbitholecomputing/CUEParser
 upload_protocol = cmsis-dap
 debug_tool = cmsis-dap
 debug_build_flags =
@@ -206,7 +207,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_RP2040
     SCSI2SD
-    CUEParser
+    CUEParser=https://github.com/rabbitholecomputing/CUEParser
 build_flags =
     -O2 -Isrc
     -Wall -Wno-sign-compare -Wno-ignored-qualifiers
@@ -279,7 +280,7 @@ lib_deps =
     minIni
     ZuluSCSI_platform_GD32F450
     SCSI2SD
-    CUEParser
+    CUEParser=https://github.com/rabbitholecomputing/CUEParser
 upload_protocol = stlink
 platform_packages = 
     toolchain-gccarmnoneeabi@1.90201.191206

+ 96 - 30
src/ImageBackingStore.cpp

@@ -42,6 +42,8 @@ ImageBackingStore::ImageBackingStore()
     m_isreadonly_attr = false;
     m_blockdev = nullptr;
     m_bgnsector = m_endsector = m_cursector = 0;
+    m_isfolder = false;
+    m_foldername[0] = '\0';
 }
 
 ImageBackingStore::ImageBackingStore(const char *filename, uint32_t scsi_block_size): ImageBackingStore()
@@ -89,47 +91,79 @@ ImageBackingStore::ImageBackingStore(const char *filename, uint32_t scsi_block_s
     }
     else
     {
-        m_isreadonly_attr = !!(FS_ATTRIB_READ_ONLY & SD.attrib(filename));
-        if (m_isreadonly_attr)
+        if (SD.open(filename, O_RDONLY).isDir())
         {
-            m_fsfile = SD.open(filename, O_RDONLY);
-            logmsg("---- Image file is read-only, writes disabled");
+            // Folder that contains .cue sheet and multiple .bin files
+            m_isfolder = true;
+            strncpy(m_foldername, filename, sizeof(m_foldername));
+            m_foldername[sizeof(m_foldername)-1] = '\0';
         }
         else
         {
-            m_fsfile = SD.open(filename, O_RDWR);
+            // Regular image file
+            _internal_open(filename);
         }
+    }
+}
+
+bool ImageBackingStore::_internal_open(const char *filename)
+{
+    m_isreadonly_attr = !!(FS_ATTRIB_READ_ONLY & SD.attrib(filename));
+    oflag_t open_flag = O_RDWR;
+    if (m_isreadonly_attr && !m_isfolder)
+    {
+        open_flag = O_RDONLY;
+        logmsg("---- Image file is read-only, writes disabled");
+    }
+
+    if (m_isfolder)
+    {
+        char fullpath[MAX_FILE_PATH * 2];
+        strncpy(fullpath, m_foldername, sizeof(fullpath) - strlen(fullpath));
+        strncat(fullpath, "/", sizeof(fullpath) - strlen(fullpath));
+        strncat(fullpath, filename, sizeof(fullpath) - strlen(fullpath));
+        m_fsfile = SD.open(fullpath, open_flag);
+    }
+    else
+    {
+        m_fsfile = SD.open(filename, open_flag);
+    }
+
+    if (!m_fsfile.isOpen())
+    {
+        return false;
+    }
 
-        uint32_t sectorcount = m_fsfile.size() / SD_SECTOR_SIZE;
-        uint32_t begin = 0, end = 0;
-        if (m_fsfile.contiguousRange(&begin, &end) && end >= begin + sectorcount
-            && (scsi_block_size % SD_SECTOR_SIZE) == 0)
+    uint32_t sectorcount = m_fsfile.size() / SD_SECTOR_SIZE;
+    uint32_t begin = 0, end = 0;
+    if (m_fsfile.contiguousRange(&begin, &end) && end >= begin + sectorcount)
+    {
+        // Convert to raw mapping, this avoids some unnecessary
+        // access overhead in SdFat library.
+        // If non-aligned offsets are later requested, it automatically falls
+        // back to SdFat access mode.
+        m_iscontiguous = true;
+        m_blockdev = SD.card();
+        m_bgnsector = begin;
+
+        if (end != begin + sectorcount)
         {
-            // Convert to raw mapping, this avoids some unnecessary
-            // access overhead in SdFat library.
-            // If non-aligned offsets are later requested, it automatically falls
-            // back to SdFat access mode.
-            m_iscontiguous = true;
-            m_blockdev = SD.card();
-            m_bgnsector = begin;
-
-            if (end != begin + sectorcount)
+            uint32_t allocsize = end - begin + 1;
+            // Due to issue #80 in ZuluSCSI version 1.0.8 and 1.0.9 the allocated size was mistakenly reported to SCSI controller.
+            // If the drive was formatted using those versions, you may have problems accessing it with newer firmware.
+            // The old behavior can be restored with setting  [SCSI] UseFATAllocSize = 1 in config file.
+
+            if (g_scsi_settings.getSystem()->useFATAllocSize)
             {
-                uint32_t allocsize = end - begin + 1;
-                // Due to issue #80 in ZuluSCSI version 1.0.8 and 1.0.9 the allocated size was mistakenly reported to SCSI controller.
-                // If the drive was formatted using those versions, you may have problems accessing it with newer firmware.
-                // The old behavior can be restored with setting  [SCSI] UseFATAllocSize = 1 in config file.
-
-                if (g_scsi_settings.getSystem()->useFATAllocSize)
-                {
-                    sectorcount = allocsize;
-                }
+                sectorcount = allocsize;
             }
-
-            m_endsector = begin + sectorcount - 1;
-            m_fsfile.flush(); // Note: m_fsfile is also kept open as a fallback.
         }
+
+        m_endsector = begin + sectorcount - 1;
+        m_fsfile.flush(); // Note: m_fsfile is also kept open as a fallback.
     }
+
+    return true;
 }
 
 bool ImageBackingStore::isOpen()
@@ -138,6 +172,8 @@ bool ImageBackingStore::isOpen()
         return (m_blockdev != NULL);
     else if (m_isrom)
         return (m_romhdr.imagesize > 0);
+    else if (m_isfolder)
+        return m_foldername[0] != '\0';
     else
         return m_fsfile.isOpen();
 }
@@ -157,6 +193,10 @@ bool ImageBackingStore::isRom()
     return m_isrom;
 }
 
+bool ImageBackingStore::isFolder()
+{
+    return m_isfolder;
+}
 
 bool ImageBackingStore::isContiguous()
 {
@@ -165,6 +205,7 @@ bool ImageBackingStore::isContiguous()
 
 bool ImageBackingStore::close()
 {
+    m_isfolder = false;
     if (m_iscontiguous)
     {
         m_blockdev = nullptr;
@@ -356,3 +397,28 @@ size_t ImageBackingStore::getFilename(char* buf, size_t buflen)
     }
     return 0;
 }
+
+bool ImageBackingStore::selectImageFile(const char *filename)
+{
+    if (!m_isfolder)
+    {
+        logmsg("Attempted selectImageFile() but image is not a folder");
+        return false;
+    }
+    return _internal_open(filename);
+}
+
+size_t ImageBackingStore::getFoldername(char* buf, size_t buflen)
+{
+    if (m_isfolder)
+    {
+        size_t name_length = strlen(m_foldername);
+        if (name_length + 1 > buflen)
+            return 0;
+
+        strncpy(buf, m_foldername, buflen);
+        return name_length;
+    }
+
+    return 0;
+}

+ 13 - 0
src/ImageBackingStore.h

@@ -33,6 +33,7 @@
 #include <unistd.h>
 #include <SdFat.h>
 #include "ROMDrive.h"
+#include "ZuluSCSI_config.h"
 
 extern "C" {
 #include <scsi.h>
@@ -74,6 +75,9 @@ public:
     // Is this internal ROM drive in microcontroller flash?
     bool isRom();
 
+    // Is the image a folder, which contains multiple files (used for .bin/.cue)
+    bool isFolder();
+
     // Is this a contigious block on the SD card? Allowing less overhead
     bool isContiguous();
 
@@ -105,6 +109,10 @@ public:
 
     size_t getFilename(char* buf, size_t buflen);
 
+    // Change image if the image is a folder (used for .cue with multiple .bin)
+    bool selectImageFile(const char *filename);
+    size_t getFoldername(char* buf, size_t buflen);
+
 protected:
     bool m_iscontiguous;
     bool m_israw;
@@ -116,4 +124,9 @@ protected:
     uint32_t m_bgnsector;
     uint32_t m_endsector;
     uint32_t m_cursector;
+
+    bool m_isfolder;
+    char m_foldername[MAX_FILE_PATH + 1];
+
+    bool _internal_open(const char *filename);
 };

+ 3 - 3
src/ZuluSCSI.cpp

@@ -356,14 +356,14 @@ bool findHDDImages()
 
   logmsg("Finding images in directory ", imgdir, ":");
 
-  SdFile root;
+  FsFile root;
   root.open(imgdir);
   if (!root.isOpen())
   {
     logmsg("Could not open directory: ", imgdir);
   }
 
-  SdFile file;
+  FsFile file;
   bool imageReady;
   bool foundImage = false;
   int usedDefaultId = 0;
@@ -403,7 +403,7 @@ bool findHDDImages()
     }
 
     char name[MAX_FILE_PATH+1];
-    if(!file.isDir()) {
+    if(!file.isDir() || scsiDiskFolderContainsCueSheet(&file)) {
       file.getName(name, MAX_FILE_PATH+1);
       file.close();
 

+ 144 - 40
src/ZuluSCSI_cdrom.cpp

@@ -463,26 +463,85 @@ void doReadTrackInformationSimple(bool track, uint32_t lba, uint16_t allocationL
 /* TOC generation from cue sheet */
 /*********************************/
 
-// Fetch track info based on LBA
-static void getTrackFromLBA(CUEParser &parser, uint32_t lba, CUETrackInfo *result)
+static bool loadCueSheet(image_config_t &img, CUEParser &parser);
+
+// Switch BIN file if the CUE sheet uses multiple separate BIN files in a folder.
+// Otherwise does nothing.
+static bool cdromSelectBinFileForTrack(image_config_t &img, const CUETrackInfo *track)
 {
-    // Track info in case we have no .cue file
-    result->file_mode = CUEFile_BINARY;
-    result->track_mode = CUETrack_MODE1_2048;
-    result->sector_length = 2048;
-    result->track_number = 1;
+    if (track->filename[0] == '\0' || !img.file.isFolder())
+    {
+        // Using a single image, no need to switch anything.
+        return true;
+    }
+    else if (img.cdrom_binfile_index == track->file_index)
+    {
+        // Haven't switched files
+        return true;
+    }
+
+    img.cdrom_binfile_index = track->file_index;
+    bool open_ok = img.file.selectImageFile(track->filename);
 
-    const CUETrackInfo *tmptrack;
-    while ((tmptrack = parser.next_track()) != NULL)
+    if (!open_ok)
+    {
+        logmsg("CUE sheet specified track file '", track->filename, "' not found");
+    }
+
+    return open_ok;
+}
+
+// Fetch track info based on LBA
+// Returns with the requested track already selected for bin file
+static void getTrackFromLBA(image_config_t &img, uint32_t lba, CUETrackInfo *result, CUEParser *parser = nullptr)
+{
+    if (!img.cuesheetfile.isOpen())
+    {
+        // Track info in case we have no .cue file
+        result->file_mode = CUEFile_BINARY;
+        result->track_mode = CUETrack_MODE1_2048;
+        result->sector_length = 2048;
+        result->track_number = 1;
+    }
+    else if (img.cdrom_binfile_index == img.cdrom_trackinfo.file_index &&
+             lba >= img.cdrom_trackinfo.track_start &&
+             lba < img.cdrom_trackinfo.data_start + img.file.size() / img.cdrom_trackinfo.sector_length)
     {
-        if (tmptrack->track_start <= lba)
+        // Same track as previous time
+        *result = img.cdrom_trackinfo;
+    }
+    else
+    {
+        // We can use either temporary parser or parser provided by caller
+        CUEParser tmp_parser;
+        if (!parser)
         {
-            *result = *tmptrack;
+            loadCueSheet(img, tmp_parser);
+            parser = &tmp_parser;
         }
         else
         {
-            break;
+            parser->restart();
         }
+
+        const CUETrackInfo *tmptrack;
+        uint64_t prev_capacity = 0;
+        while ((tmptrack = parser->next_track(prev_capacity)) != NULL)
+        {
+            if (tmptrack->track_start <= lba)
+            {
+                *result = *tmptrack;
+            }
+            else
+            {
+                break;
+            }
+
+            cdromSelectBinFileForTrack(img, tmptrack);
+            prev_capacity = img.file.size();
+        }
+
+        img.cdrom_trackinfo = *result;
     }
 }
 
@@ -560,7 +619,8 @@ static void doReadTOC(bool MSF, uint8_t track, uint16_t allocationLength)
     int firsttrack = -1;
     CUETrackInfo lasttrack = {0};
     const CUETrackInfo *trackinfo;
-    while ((trackinfo = parser.next_track()) != NULL)
+    uint64_t prev_capacity = 0;
+    while ((trackinfo = parser.next_track(prev_capacity)) != NULL)
     {
         if (firsttrack < 0) firsttrack = trackinfo->track_number;
         lasttrack = *trackinfo;
@@ -570,6 +630,9 @@ static void doReadTOC(bool MSF, uint8_t track, uint16_t allocationLength)
             formatTrackInfo(trackinfo, &trackdata[8 * trackcount], MSF);
             trackcount += 1;
         }
+
+        cdromSelectBinFileForTrack(img, trackinfo);
+        prev_capacity = img.file.size();
     }
 
     // Format lead-out track info
@@ -698,7 +761,8 @@ static void doReadFullTOC(uint8_t session, uint16_t allocationLength, bool useBC
     int firsttrack = -1;
     CUETrackInfo lasttrack = {0};
     const CUETrackInfo *trackinfo;
-    while ((trackinfo = parser.next_track()) != NULL)
+    uint64_t prev_capacity = 0;
+    while ((trackinfo = parser.next_track(prev_capacity)) != NULL)
     {
         if (firsttrack < 0)
         {
@@ -713,6 +777,9 @@ static void doReadFullTOC(uint8_t session, uint16_t allocationLength, bool useBC
         formatRawTrackInfo(trackinfo, &scsiDev.data[len], useBCD);
         trackcount += 1;
         len += 11;
+
+        cdromSelectBinFileForTrack(img, trackinfo);
+        prev_capacity = img.file.size();
     }
 
     // First and last track numbers
@@ -762,12 +829,11 @@ void doReadHeader(bool MSF, uint32_t lba, uint16_t allocationLength)
 #endif
 
     uint8_t mode = 1;
-    CUEParser parser;
-    if (loadCueSheet(img, parser))
+    if (img.cuesheetfile.isOpen())
     {
         // Search the track with the requested LBA
         CUETrackInfo trackinfo = {};
-        getTrackFromLBA(parser, lba, &trackinfo);
+        getTrackFromLBA(img, lba, &trackinfo);
 
         // Track mode (audio / data)
         if (trackinfo.track_mode == CUETrack_AUDIO)
@@ -860,7 +926,8 @@ void doReadTrackInformation(bool track, uint32_t lba, uint16_t allocationLength)
     uint32_t tracklen = 0;
     CUETrackInfo mtrack = {0};
     const CUETrackInfo *trackinfo;
-    while ((trackinfo = parser.next_track()) != NULL)
+    uint64_t prev_capacity = 0;
+    while ((trackinfo = parser.next_track(prev_capacity)) != NULL)
     {
         if (mtrack.track_number != 0) // skip 1st track, just store later
         {
@@ -873,6 +940,9 @@ void doReadTrackInformation(bool track, uint32_t lba, uint16_t allocationLength)
             }
         }
         mtrack = *trackinfo;
+
+        cdromSelectBinFileForTrack(img, trackinfo);
+        prev_capacity = img.file.size();
     }
     // try the last track as a final attempt if no match found beforehand
     if (!trackfound)
@@ -1127,7 +1197,8 @@ bool cdromValidateCueSheet(image_config_t &img)
 
     const CUETrackInfo *trackinfo;
     int trackcount = 0;
-    while ((trackinfo = parser.next_track()) != NULL)
+    uint64_t prev_capacity = 0;
+    while ((trackinfo = parser.next_track(prev_capacity)) != NULL)
     {
         trackcount++;
 
@@ -1142,6 +1213,14 @@ bool cdromValidateCueSheet(image_config_t &img)
         {
             logmsg("---- Unsupported CUE data file mode ", (int)trackinfo->file_mode);
         }
+
+        // Check that the bin file is available
+        if (!cdromSelectBinFileForTrack(img, trackinfo))
+        {
+            return false;
+        }
+
+        prev_capacity = img.file.size();
     }
 
     if (trackcount == 0)
@@ -1309,11 +1388,10 @@ static void doPlayAudio(uint32_t lba, uint32_t length)
     }
 
     // if actual playback is requested perform steps to verify prior to playback
-    CUEParser parser;
-    if (loadCueSheet(img, parser))
+    if (img.cuesheetfile.isOpen())
     {
         CUETrackInfo trackinfo = {};
-        getTrackFromLBA(parser, lba, &trackinfo);
+        getTrackFromLBA(img, lba, &trackinfo);
 
         if (lba == 0xFFFFFFFF)
         {
@@ -1450,8 +1528,7 @@ static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
     audio_stop(img.scsiId & 7);
 #endif
 
-    CUEParser parser;
-    if (!loadCueSheet(img, parser)
+    if (!img.cuesheetfile.isOpen()
         && (sector_type == 0 || sector_type == 2)
         && main_channel == 0x10 && sub_channel == 0)
     {
@@ -1463,10 +1540,10 @@ static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
     // Search the track with the requested LBA
     // Supplies dummy data if no cue sheet is active.
     CUETrackInfo trackinfo = {};
-    getTrackFromLBA(parser, lba, &trackinfo);
+    getTrackFromLBA(img, lba, &trackinfo);
 
     // Figure out the data offset in the file
-    uint64_t offset;
+    int64_t offset;
     if (sector_type == SECTOR_TYPE_VENDOR_PLEXTOR &&
          g_scsi_settings.getDevice(img.scsiId & 0x7)->vendorExtensions & VENDOR_EXTENSION_OPTICAL_PLEXTOR)
     {
@@ -1484,7 +1561,7 @@ static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
 
         trackinfo.sector_length = AUDIO_CD_SECTOR_LEN;
         trackinfo.track_mode = CUETrack_AUDIO;
-        offset = (uint64_t)(lba - 2) * trackinfo.sector_length;
+        offset = ((int64_t)lba - 2 - trackinfo.file_start) * trackinfo.sector_length;
         dbgmsg("------ Read CD Vendor Plextor (0xd8): ", (int)length, " sectors starting at ", (int)lba -2,
                ", sector size ", (int) AUDIO_CD_SECTOR_LEN,
                ", data offset in file ", (int)offset);
@@ -1493,30 +1570,48 @@ static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
     {
         trackinfo.sector_length = AUDIO_CD_SECTOR_LEN;
         trackinfo.track_mode = CUETrack_AUDIO;
-        offset = (uint64_t)(lba) * trackinfo.sector_length;
+        offset = ((int64_t)lba - trackinfo.file_start) * trackinfo.sector_length;
         dbgmsg("------ Read CD Vendor Apple CDROM 300 plus (0xd8): ", (int)length, " sectors starting at ", (int)lba,
                ", sector size ", (int) AUDIO_CD_SECTOR_LEN,
                ", data offset in file ", (int)offset);
     }
     else
     {
-        offset = trackinfo.file_offset + trackinfo.sector_length * (lba - trackinfo.data_start);
+        offset = trackinfo.file_offset + trackinfo.sector_length * ((int64_t)lba - trackinfo.data_start);
         dbgmsg("------ Read CD: ", (int)length, " sectors starting at ", (int)lba,
             ", track number ", trackinfo.track_number, ", sector size ", (int)trackinfo.sector_length,
             ", main channel ", main_channel, ", sub channel ", sub_channel,
             ", data offset in file ", (int)offset);
     }
+
     // Ensure read is not out of range of the image
+    uint32_t total_length = length;
     uint64_t readend = offset + trackinfo.sector_length * length;
-    if (readend > img.file.size())
+    if (offset < 0 && -offset > trackinfo.unstored_pregap_length * trackinfo.sector_length)
+    {
+        // It doesn't really matter what data we give for the unstored pregap
+        offset = 0;
+    }
+    else if (readend > img.file.size())
     {
-        logmsg("WARNING: Host attempted CD read at sector ", lba, "+", length,
+        uint32_t sectors_available = (img.file.size() - offset) / trackinfo.sector_length;
+        if (!img.file.isFolder() || sectors_available == 0)
+        {
+            // This is really past the end of the CD
+            logmsg("WARNING: Host attempted CD read at sector ", lba, "+", length,
               ", exceeding image size ", img.file.size());
-        scsiDev.status = CHECK_CONDITION;
-        scsiDev.target->sense.code = ILLEGAL_REQUEST;
-        scsiDev.target->sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
-        scsiDev.phase = STATUS;
-        return;
+            scsiDev.status = CHECK_CONDITION;
+            scsiDev.target->sense.code = ILLEGAL_REQUEST;
+            scsiDev.target->sense.asc = LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
+            scsiDev.phase = STATUS;
+            return;
+        }
+        else
+        {
+            // Read as much as we can and continue with next file
+            dbgmsg("------ Splitting read request at image file end");
+            length = sectors_available;
+        }
     }
 
     // Verify sector type
@@ -1731,6 +1826,14 @@ static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
 
     scsiFinishWrite();
 
+    if (length != total_length)
+    {
+        // This read request was split across multiple .bin files
+        // Tail recurse to read the next track.
+        doReadCD(lba + length, total_length - length, sector_type, main_channel, sub_channel, data_only);
+        return;
+    }
+
     scsiDev.status = 0;
     scsiDev.phase = STATUS;
 }
@@ -1748,10 +1851,8 @@ static void doReadSubchannel(bool time, bool subq, uint8_t parameter, uint8_t tr
 
         // Fetch current track info
         image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
-        CUEParser parser;
         CUETrackInfo trackinfo = {};
-        loadCueSheet(img, parser);
-        getTrackFromLBA(parser, lba, &trackinfo);
+        getTrackFromLBA(img, lba, &trackinfo);
 
         // Request sub channel data at current playback position
         *buf++ = 0; // Reserved
@@ -1837,9 +1938,12 @@ static bool doReadCapacity(uint32_t lba, uint8_t pmi)
     // find the last track on the disk
     CUETrackInfo lasttrack = {0};
     const CUETrackInfo *trackinfo;
-    while ((trackinfo = parser.next_track()) != NULL)
+    uint64_t prev_capacity = 0;
+    while ((trackinfo = parser.next_track(prev_capacity)) != NULL)
     {
         lasttrack = *trackinfo;
+        cdromSelectBinFileForTrack(img, trackinfo);
+        prev_capacity = img.file.size();
     }
 
     uint32_t capacity = 0;

+ 46 - 3
src/ZuluSCSI_disk.cpp

@@ -268,6 +268,7 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_lun, in
 {
     image_config_t &img = g_DiskImages[target_idx];
     img.cuesheetfile.close();
+    img.cdrom_binfile_index = -1;
     scsiDiskSetImageConfig(target_idx);
     img.file = ImageBackingStore(filename, blocksize);
 
@@ -278,14 +279,14 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_lun, in
         img.scsiId = target_idx | S2S_CFG_TARGET_ENABLED;
         img.sdSectorStart = 0;
 
-        if (type != S2S_CFG_NETWORK && img.scsiSectors == 0)
+        if (img.scsiSectors == 0 && type != S2S_CFG_NETWORK && !img.file.isFolder())
         {
             logmsg("---- Error: image file ", filename, " is empty");
             img.file.close();
             return false;
         }
         uint32_t sector_begin = 0, sector_end = 0;
-        if (img.file.isRom() || type == S2S_CFG_NETWORK)
+        if (img.file.isRom() || type == S2S_CFG_NETWORK || img.file.isFolder())
         {
             // ROM is always contiguous, no need to log
         }
@@ -388,6 +389,7 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_lun, in
         if (img.deviceType == S2S_CFG_OPTICAL &&
             strncasecmp(filename + strlen(filename) - 4, ".bin", 4) == 0)
         {
+            // Check for .cue sheet with single .bin file
             char cuesheetname[MAX_FILE_PATH + 1] = {0};
             strncpy(cuesheetname, filename, strlen(filename) - 4);
             strlcat(cuesheetname, ".cue", sizeof(cuesheetname));
@@ -407,6 +409,31 @@ bool scsiDiskOpenHDDImage(int target_idx, const char *filename, int scsi_lun, in
                 logmsg("---- No CUE sheet found at ", cuesheetname, ", using as plain binary image");
             }
         }
+        else if (img.deviceType == S2S_CFG_OPTICAL && img.file.isFolder())
+        {
+            // The folder should contain .cue sheet and one or several .bin files
+            char foldername[MAX_FILE_PATH + 1] = {0};
+            char cuesheetname[MAX_FILE_PATH + 1] = {0};
+            img.file.getFoldername(foldername, sizeof(foldername));
+            FsFile folder = SD.open(foldername, O_RDONLY);
+            bool valid = false;
+            img.cuesheetfile.close();
+            while (!valid && img.cuesheetfile.openNext(&folder, O_RDONLY))
+            {
+                img.cuesheetfile.getName(cuesheetname, sizeof(cuesheetname));
+                if (strncasecmp(cuesheetname + strlen(cuesheetname) - 4, ".cue", 4) == 0)
+                {
+                    valid = cdromValidateCueSheet(img);
+                }
+            }
+
+            if (!valid)
+            {
+                logmsg("No valid .cue sheet found in folder '", foldername, "'");
+                img.cuesheetfile.close();
+            }
+        }
+
         img.use_prefix = use_prefix;
         img.file.getFilename(img.current_image, sizeof(img.current_image));
         return true;
@@ -479,6 +506,22 @@ bool scsiDiskFilenameValid(const char* name)
     return true;
 }
 
+bool scsiDiskFolderContainsCueSheet(FsFile *dir)
+{
+    FsFile file;
+    char filename[MAX_FILE_PATH + 1];
+    while (file.openNext(dir, O_RDONLY))
+    {
+        if (file.getName(filename, sizeof(filename)) &&
+            (strncasecmp(filename + strlen(filename) - 4, ".cue", 4) == 0))
+        {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 static void scsiDiskCheckDir(char * dir_name, int target_idx, image_config_t* img, S2S_CFG_TYPE type, const char* type_name)
 {
     if (SD.exists(dir_name))
@@ -639,7 +682,7 @@ static int findNextImageAfter(image_config_t &img,
     FsFile file;
     while (file.openNext(&dir, O_RDONLY))
     {
-        if (file.isDir()) continue;
+        if (file.isDir() && !scsiDiskFolderContainsCueSheet(&file)) continue;
         if (!file.getName(buf, MAX_FILE_PATH))
         {
             logmsg("Image directory '", dirname, "' had invalid file");

+ 13 - 1
src/ZuluSCSI_disk.h

@@ -34,6 +34,7 @@
 #include <scsiPhy.h>
 #include "ImageBackingStore.h"
 #include "ZuluSCSI_config.h"
+#include <CUEParser.h>
 
 extern "C" {
 #include <disk.h>
@@ -75,6 +76,13 @@ struct image_config_t: public S2S_TargetCfg
     // Negative value forces restart from first image.
     int image_index;
 
+    // Previously accessed CD-ROM track, cached for performance
+    CUETrackInfo cdrom_trackinfo;
+
+    // Loaded .bin file index for .cue/.bin with multiple files
+    // Matches trackinfo.file_index
+    int cdrom_binfile_index;
+
     // Cue sheet file for CD-ROM images
     FsFile cuesheetfile;
 
@@ -124,6 +132,10 @@ void scsiDiskLoadConfig(int target_idx);
 // The current implementation does not check the the filename prefix for validity.
 bool scsiDiskFilenameValid(const char* name);
 
+// Check if a directory contains a .cue sheet file.
+// This is used when single .cue sheet references multiple .bin files.
+bool scsiDiskFolderContainsCueSheet(FsFile *dir);
+
 // Clear the ROM drive header from flash
 bool scsiDiskClearRomDrive();
 // Program ROM drive and rename image file
@@ -157,4 +169,4 @@ bool scsiDiskCheckAnyNetworkDevicesConfigured();
 
 
 // Switch to next Drive image if multiple have been configured
-bool switchNextImage(image_config_t &img, const char* next_filename = nullptr);
+bool switchNextImage(image_config_t &img, const char* next_filename = nullptr);

+ 19 - 19
src/ZuluSCSI_settings.cpp

@@ -104,25 +104,25 @@ void ZuluSCSISettings::setDefaultDriveInfo(uint8_t scsiId, const char *presetNam
     
 
 
-    static const char *driveinfo_fixed[4]     = DRIVEINFO_FIXED;
-    static const char *driveinfo_removable[4] = DRIVEINFO_REMOVABLE;
-    static const char *driveinfo_optical[4]   = DRIVEINFO_OPTICAL;
-    static const char *driveinfo_floppy[4]    = DRIVEINFO_FLOPPY;
-    static const char *driveinfo_magopt[4]    = DRIVEINFO_MAGOPT;
-    static const char *driveinfo_network[4]   = DRIVEINFO_NETWORK;
-    static const char *driveinfo_tape[4]      = DRIVEINFO_TAPE;
-
-    static const char *apl_driveinfo_fixed[4]     = APPLE_DRIVEINFO_FIXED;
-    static const char *apl_driveinfo_removable[4] = APPLE_DRIVEINFO_REMOVABLE;
-    static const char *apl_driveinfo_optical[4]   = APPLE_DRIVEINFO_OPTICAL;
-    static const char *apl_driveinfo_floppy[4]    = APPLE_DRIVEINFO_FLOPPY;
-    static const char *apl_driveinfo_magopt[4]    = APPLE_DRIVEINFO_MAGOPT;
-    static const char *apl_driveinfo_network[4]   = APPLE_DRIVEINFO_NETWORK;
-    static const char *apl_driveinfo_tape[4]      = APPLE_DRIVEINFO_TAPE;
-
-    static const char *iomega_driveinfo_removeable[4] = IOMEGA_DRIVEINFO_ZIP100;
+    static const char * const driveinfo_fixed[4]     = DRIVEINFO_FIXED;
+    static const char * const driveinfo_removable[4] = DRIVEINFO_REMOVABLE;
+    static const char * const driveinfo_optical[4]   = DRIVEINFO_OPTICAL;
+    static const char * const driveinfo_floppy[4]    = DRIVEINFO_FLOPPY;
+    static const char * const driveinfo_magopt[4]    = DRIVEINFO_MAGOPT;
+    static const char * const driveinfo_network[4]   = DRIVEINFO_NETWORK;
+    static const char * const driveinfo_tape[4]      = DRIVEINFO_TAPE;
+
+    static const char * const apl_driveinfo_fixed[4]     = APPLE_DRIVEINFO_FIXED;
+    static const char * const apl_driveinfo_removable[4] = APPLE_DRIVEINFO_REMOVABLE;
+    static const char * const apl_driveinfo_optical[4]   = APPLE_DRIVEINFO_OPTICAL;
+    static const char * const apl_driveinfo_floppy[4]    = APPLE_DRIVEINFO_FLOPPY;
+    static const char * const apl_driveinfo_magopt[4]    = APPLE_DRIVEINFO_MAGOPT;
+    static const char * const apl_driveinfo_network[4]   = APPLE_DRIVEINFO_NETWORK;
+    static const char * const apl_driveinfo_tape[4]      = APPLE_DRIVEINFO_TAPE;
+
+    static const char * const iomega_driveinfo_removeable[4] = IOMEGA_DRIVEINFO_ZIP100;
     
-    const char **driveinfo = NULL;
+    const char * const * driveinfo = NULL;
     bool known_preset = false;
     scsi_system_settings_t& cfgSys = m_sys;
 
@@ -483,4 +483,4 @@ scsi_device_preset_t ZuluSCSISettings::getDevicePreset(uint8_t scsiId)
 const char* ZuluSCSISettings::getDevicePresetName(uint8_t scsiId)
 {
     return devicePresetName[m_devPreset[scsiId]];
-}
+}