/*
* 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 .
*/
// 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
#include
#include
#include
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)
{
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.track_start = time;
got_pause = true;
}
else if (index == 1)
{
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;
}
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;
}
}