From 69cfcd22a28c14abafbc0b41d8b30bb64a226bdd Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Fri, 21 May 2021 14:46:02 +1000 Subject: [PATCH] CDImageCue: Replace libcue with in-house cue parser --- src/common/CMakeLists.txt | 4 +- src/common/cd_image_cue.cpp | 154 ++++--- src/common/cd_image_memory.cpp | 2 - src/common/common.vcxproj | 29 +- src/common/common.vcxproj.filters | 2 + src/common/cue_parser.cpp | 474 ++++++++++++++++++++ src/common/cue_parser.h | 86 ++++ src/frontend-common/frontend-common.vcxproj | 3 - src/scmversion/scmversion.vcxproj | 3 - 9 files changed, 664 insertions(+), 93 deletions(-) create mode 100644 src/common/cue_parser.cpp create mode 100644 src/common/cue_parser.h diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d58f75e51..cfc10490e 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -26,6 +26,8 @@ add_library(common cd_xa.h crash_handler.cpp crash_handler.h + cue_parser.cpp + cue_parser.h dimensional_array.h error.cpp error.h @@ -114,7 +116,7 @@ add_library(common target_include_directories(common PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(common PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") -target_link_libraries(common PRIVATE glad libcue stb Threads::Threads libchdr glslang vulkan-loader zlib minizip samplerate) +target_link_libraries(common PRIVATE glad stb Threads::Threads libchdr glslang vulkan-loader zlib minizip samplerate) if(WIN32) target_sources(common PRIVATE diff --git a/src/common/cd_image_cue.cpp b/src/common/cd_image_cue.cpp index 9a2d64fee..e5a301e0c 100644 --- a/src/common/cd_image_cue.cpp +++ b/src/common/cd_image_cue.cpp @@ -1,12 +1,12 @@ #include "assert.h" #include "cd_image.h" #include "cd_subchannel_replacement.h" +#include "cue_parser.h" #include "error.h" #include "file_system.h" #include "log.h" #include #include -#include #include Log_SetChannel(CDImageCueSheet); @@ -25,8 +25,6 @@ protected: bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; private: - Cd* m_cd = nullptr; - struct TrackFile { std::string filename; @@ -43,13 +41,12 @@ CDImageCueSheet::CDImageCueSheet() = default; CDImageCueSheet::~CDImageCueSheet() { std::for_each(m_files.begin(), m_files.end(), [](TrackFile& t) { std::fclose(t.file); }); - cd_delete(m_cd); } bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error) { - std::optional cuesheet_string = FileSystem::ReadFileToString(filename); - if (!cuesheet_string.has_value()) + std::FILE* fp = FileSystem::OpenCFile(filename, "rb"); + if (!fp) { Log_ErrorPrintf("Failed to open cuesheet '%s': errno %d", filename, errno); if (error) @@ -58,41 +55,28 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error) return false; } - // work around cuesheet parsing issue - ensure the last character is a newline. - if (!cuesheet_string->empty() && cuesheet_string->at(cuesheet_string->size() - 1) != '\n') - *cuesheet_string += '\n'; - - m_cd = cue_parse_string(cuesheet_string->c_str()); - if (!m_cd) + CueParser::File parser; + if (!parser.Parse(fp, error)) { - Log_ErrorPrintf("Failed to parse cuesheet '%s'", filename); - if (error) - error->SetMessage("Failed to parse cuesheet"); - + std::fclose(fp); return false; } + std::fclose(fp); + m_filename = filename; u32 disc_lba = 0; // for each track.. - const int num_tracks = cd_get_ntrack(m_cd); - for (int track_num = 1; track_num <= num_tracks; track_num++) + for (u32 track_num = 1; track_num <= CueParser::MAX_TRACK_NUMBER; track_num++) { - const ::Track* track = cd_get_track(m_cd, track_num); - if (!track || !track_get_filename(track)) - { - Log_ErrorPrintf("Track/filename missing for track %d", track_num); - if (error) - error->SetFormattedMessage("Track/filename missing for track %d", track_num); + const CueParser::Track* track = parser.GetTrack(track_num); + if (!track) + break; - return false; - } - - const std::string track_filename = track_get_filename(track); - long track_start = track_get_start(track); - long track_length = track_get_length(track); + const std::string track_filename(track->file); + LBA track_start = track->start.ToLBA(); u32 track_file_index = 0; for (; track_file_index < m_files.size(); track_file_index++) @@ -135,22 +119,23 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error) } // data type determines the sector size - const TrackMode mode = static_cast(track_get_mode(track)); + const TrackMode mode = track->mode; const u32 track_sector_size = GetBytesPerSector(mode); // precompute subchannel q flags for the whole track SubChannelQ::Control control{}; control.data = mode != TrackMode::Audio; - control.audio_preemphasis = track_is_set_flag(track, FLAG_PRE_EMPHASIS); - control.digital_copy_permitted = track_is_set_flag(track, FLAG_COPY_PERMITTED); - control.four_channel_audio = track_is_set_flag(track, FLAG_FOUR_CHANNEL); + control.audio_preemphasis = track->HasFlag(CueParser::TrackFlag::PreEmphasis); + control.digital_copy_permitted = track->HasFlag(CueParser::TrackFlag::CopyPermitted); + control.four_channel_audio = track->HasFlag(CueParser::TrackFlag::FourChannelAudio); // determine the length from the file - if (track_length < 0) + LBA track_length; + if (!track->length.has_value()) { - std::fseek(m_files[track_file_index].file, 0, SEEK_END); - long file_size = std::ftell(m_files[track_file_index].file); - std::fseek(m_files[track_file_index].file, 0, SEEK_SET); + FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_END); + u64 file_size = static_cast(FileSystem::FTell64(m_files[track_file_index].file)); + FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_SET); file_size /= track_sector_size; if (track_start >= file_size) @@ -165,49 +150,78 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error) return false; } - track_length = file_size - track_start; + track_length = static_cast(file_size - track_start); + } + else + { + track_length = track->length.value().ToLBA(); } - // Two seconds pregap for track 1 is assumed if not specified. - // Some people have broken (older) dumps where a two second pregap was implicit but not specified in the cuesheet. - // The problem is we can't tell between a missing implicit two second pregap and a zero second pregap. Most of these - // seem to be a single bin file for all tracks. So if this is the case, we add the two seconds in if it's not - // specified. If this is an audio CD (likely when track 1 is not data), we don't add these pregaps, and rely on the - // cuesheet. If we did add them, it causes issues in some games (e.g. Dancing Stage featuring DREAMS COME TRUE). - long pregap_frames = track_get_zero_pre(track); - const bool pregap_in_file = pregap_frames > 0 && track_start >= pregap_frames; - const bool is_multi_track_bin = (track_num > 1 && track_file_index == m_indices[0].file_index); - const bool likely_audio_cd = static_cast(track_get_mode(cd_get_track(m_cd, 1))) == TrackMode::Audio; - if ((track_num == 1 || is_multi_track_bin) && pregap_frames < 0 && (track_num == 1 || !likely_audio_cd)) - pregap_frames = 2 * FRAMES_PER_SECOND; - - // create the index for the pregap - if (pregap_frames > 0) + const Position* index0 = track->GetIndex(0); + LBA pregap_frames; + if (index0) { + // index 1 is always present, so this is safe + pregap_frames = track->GetIndex(1)->ToLBA() - index0->ToLBA(); + + // Pregap/index 0 is in the file, easy. Index pregap_index = {}; pregap_index.start_lba_on_disc = disc_lba; - pregap_index.start_lba_in_track = static_cast(static_cast(-pregap_frames)); + pregap_index.start_lba_in_track = static_cast(-static_cast(pregap_frames)); pregap_index.length = pregap_frames; pregap_index.track_number = track_num; pregap_index.index_number = 0; pregap_index.mode = mode; pregap_index.control.bits = control.bits; pregap_index.is_pregap = true; - if (pregap_in_file) - { - pregap_index.file_index = track_file_index; - pregap_index.file_offset = static_cast(static_cast(track_start - pregap_frames)) * track_sector_size; - pregap_index.file_sector_size = track_sector_size; - } + pregap_index.file_index = track_file_index; + pregap_index.file_offset = static_cast(static_cast(track_start - pregap_frames)) * track_sector_size; + pregap_index.file_sector_size = track_sector_size; m_indices.push_back(pregap_index); disc_lba += pregap_index.length; } + else + { + // Two seconds pregap for track 1 is assumed if not specified. + // Some people have broken (older) dumps where a two second pregap was implicit but not specified in the cuesheet. + // The problem is we can't tell between a missing implicit two second pregap and a zero second pregap. Most of + // these seem to be a single bin file for all tracks. So if this is the case, we add the two seconds in if it's + // not specified. If this is an audio CD (likely when track 1 is not data), we don't add these pregaps, and rely + // on the cuesheet. If we did add them, it causes issues in some games (e.g. Dancing Stage featuring DREAMS COME + // TRUE). + const bool is_multi_track_bin = (track_num > 1 && track_file_index == m_indices[0].file_index); + const bool likely_audio_cd = (parser.GetTrack(1)->mode == TrackMode::Audio); + + pregap_frames = track->zero_pregap.has_value() ? track->zero_pregap->ToLBA() : 0; + if ((track_num == 1 || is_multi_track_bin) && !track->zero_pregap.has_value() && + (track_num == 1 || !likely_audio_cd)) + { + pregap_frames = 2 * FRAMES_PER_SECOND; + } + + // create the index for the pregap + if (pregap_frames > 0) + { + Index pregap_index = {}; + pregap_index.start_lba_on_disc = disc_lba; + pregap_index.start_lba_in_track = static_cast(-static_cast(pregap_frames)); + pregap_index.length = pregap_frames; + pregap_index.track_number = track_num; + pregap_index.index_number = 0; + pregap_index.mode = mode; + pregap_index.control.bits = control.bits; + pregap_index.is_pregap = true; + m_indices.push_back(pregap_index); + + disc_lba += pregap_index.length; + } + } // add the track itself - m_tracks.push_back(Track{static_cast(track_num), disc_lba, static_cast(m_indices.size()), - static_cast(track_length + pregap_frames), mode, control}); + m_tracks.push_back( + Track{track_num, disc_lba, static_cast(m_indices.size()), track_length + pregap_frames, mode, control}); // how many indices in this track? Index last_index; @@ -217,18 +231,20 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error) last_index.index_number = 1; last_index.file_index = track_file_index; last_index.file_sector_size = track_sector_size; - last_index.file_offset = static_cast(static_cast(track_start)) * track_sector_size; + last_index.file_offset = static_cast(track_start) * track_sector_size; last_index.mode = mode; last_index.control.bits = control.bits; last_index.is_pregap = false; - long last_index_offset = track_start; - for (int index_num = 1;; index_num++) + u32 last_index_offset = track_start; + for (u32 index_num = 1;; index_num++) { - long index_offset = track_get_index(track, index_num); - if (index_offset < 0) + const Position* pos = track->GetIndex(index_num); + if (!pos) break; + const u32 index_offset = pos->ToLBA(); + // add an index between the track indices if (index_offset > last_index_offset) { @@ -247,7 +263,7 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Common::Error* error) } // and the last index is added here - const long track_end_index = track_start + track_length; + const u32 track_end_index = track_start + track_length; DebugAssert(track_end_index >= last_index_offset); if (track_end_index > last_index_offset) { diff --git a/src/common/cd_image_memory.cpp b/src/common/cd_image_memory.cpp index fa1d1121c..1ce5eba83 100644 --- a/src/common/cd_image_memory.cpp +++ b/src/common/cd_image_memory.cpp @@ -5,8 +5,6 @@ #include "log.h" #include #include -#include -#include Log_SetChannel(CDImageMemory); class CDImageMemory : public CDImage diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 6443e036d..040ea4558 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -60,6 +60,7 @@ + @@ -139,6 +140,7 @@ + @@ -204,9 +206,6 @@ {425d6c99-d1c8-43c2-b8ac-4d7b1d941017} - - {6a4208ed-e3dc-41e1-81cd-f61025fc285a} - {39f0adff-3a84-470d-9cf0-ca49e164f2f3} @@ -412,7 +411,7 @@ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -439,7 +438,7 @@ _ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -469,7 +468,7 @@ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -496,7 +495,7 @@ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 true @@ -523,7 +522,7 @@ _ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -553,7 +552,7 @@ _ITERATOR_DEBUG_LEVEL=1;WIN32;_DEBUGFAST;_DEBUG;_LIB;%(PreprocessorDefinitions) true ProgramDatabase - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) Default false true @@ -584,7 +583,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -615,7 +614,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -646,7 +645,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -677,7 +676,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true stdcpp17 false @@ -708,7 +707,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 @@ -739,7 +738,7 @@ true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true - $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libcue\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\libchdr\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\glslang;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\minizip\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) true true stdcpp17 diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 69b8eaf9f..2e4b7d145 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -112,6 +112,7 @@ + @@ -218,6 +219,7 @@ + diff --git a/src/common/cue_parser.cpp b/src/common/cue_parser.cpp new file mode 100644 index 000000000..72cdcd7ab --- /dev/null +++ b/src/common/cue_parser.cpp @@ -0,0 +1,474 @@ +#include "cue_parser.h" +#include "error.h" +#include "log.h" +#include "string_util.h" +#include +Log_SetChannel(CueParser); + +namespace CueParser { + +static bool TokenMatch(const std::string_view& s1, const char* token) +{ + const size_t token_len = std::strlen(token); + if (s1.length() != token_len) + return false; + + return (StringUtil::Strncasecmp(s1.data(), token, token_len) == 0); +} + +File::File() = default; + +File::~File() = default; + +const Track* File::GetTrack(u32 n) const +{ + for (const auto& it : m_tracks) + { + if (it.number == n) + return ⁢ + } + + return nullptr; +} + +Track* File::GetMutableTrack(u32 n) +{ + for (auto& it : m_tracks) + { + if (it.number == n) + return ⁢ + } + + return nullptr; +} + +bool File::Parse(std::FILE* fp, Common::Error* error) +{ + char line[1024]; + u32 line_number = 1; + while (std::fgets(line, sizeof(line), fp)) + { + if (!ParseLine(line, line_number, error)) + return false; + + line_number++; + } + + if (!CompleteLastTrack(line_number, error)) + return false; + + if (!SetTrackLengths(line_number, error)) + return false; + + return true; +} + +void File::SetError(u32 line_number, Common::Error* error, const char* format, ...) +{ + std::va_list ap; + SmallString str; + va_start(ap, format); + str.FormatVA(format, ap); + va_end(ap); + + Log_ErrorPrintf("Cue parse error at line %u: %s", line_number, str.GetCharArray()); + + if (error) + error->SetFormattedMessage("Cue parse error at line %u: %s", line_number, str.GetCharArray()); +} + +std::string_view File::GetToken(const char*& line) +{ + std::string_view ret; + + const char* start = line; + while (std::isspace(*start) && *start != '\0') + start++; + + if (*start == '\0') + return ret; + + const char* end; + const bool quoted = *start == '\"'; + if (quoted) + { + start++; + end = start; + while (*end != '\"' && *end != '\0') + end++; + + if (*end != '\"') + return ret; + + ret = std::string_view(start, static_cast(end - start)); + + // eat closing " + end++; + } + else + { + end = start; + while (!std::isspace(*end) && *end != '\0') + end++; + + ret = std::string_view(start, static_cast(end - start)); + } + + line = end; + return ret; +} + +std::optional File::GetMSF(const std::string_view& token) +{ + const u32 len = static_cast(token.length()); + + static const s32 max_values[] = {60, 60, 75}; + + u32 parts[3] = {}; + u32 part = 0; + + u32 start = 0; + for (;;) + { + while (start < token.length() && token[start] < '0' && token[start] <= '9') + start++; + + if (start == token.length()) + return std::nullopt; + + u32 end = start; + while (end < token.length() && token[end] >= '0' && token[end] <= '9') + end++; + + const std::optional value = StringUtil::FromChars(token.substr(start, end - start)); + if (!value.has_value() || value.value() < 0 || value.value() > max_values[part]) + return std::nullopt; + + parts[part] = static_cast(value.value()); + part++; + + if (part == 3) + break; + + while (end < token.length() && std::isspace(token[end])) + end++; + if (end == token.length() || token[end] != ':') + return std::nullopt; + + start = end + 1; + } + + MSF ret; + ret.minute = static_cast(parts[0]); + ret.second = static_cast(parts[1]); + ret.frame = static_cast(parts[2]); + return ret; +} + +bool File::ParseLine(const char* line, u32 line_number, Common::Error* error) +{ + const std::string_view command(GetToken(line)); + if (command.empty()) + return true; + + if (TokenMatch(command, "REM")) + { + // comment, eat it + return true; + } + + if (TokenMatch(command, "FILE")) + return HandleFileCommand(line, line_number, error); + else if (TokenMatch(command, "TRACK")) + return HandleTrackCommand(line, line_number, error); + else if (TokenMatch(command, "INDEX")) + return HandleIndexCommand(line, line_number, error); + else if (TokenMatch(command, "PREGAP")) + return HandlePregapCommand(line, line_number, error); + else if (TokenMatch(command, "FLAGS")) + return HandleFlagCommand(line, line_number, error); + + if (TokenMatch(command, "POSTGAP")) + { + Log_WarningPrintf("Ignoring '*%s' command", static_cast(command.size()), command.data()); + return true; + } + + // stuff we definitely ignore + if (TokenMatch(command, "CATALOG") || TokenMatch(command, "CDTEXTFILE") || TokenMatch(command, "CATALOG") || + TokenMatch(command, "ISRC") || TokenMatch("command", "TRACK_ISRC") || TokenMatch(command, "TITLE") || + TokenMatch(command, "PERFORMER") || TokenMatch(command, "SONGWRITER") || TokenMatch(command, "COMPOSER") || + TokenMatch(command, "ARRANGER") || TokenMatch(command, "MESSAGE") || TokenMatch(command, "DISC_ID") || + TokenMatch(command, "GENRE") || TokenMatch(command, "TOC_INFO1") || TokenMatch(command, "TOC_INFO2") || + TokenMatch(command, "UPC_EAN") || TokenMatch(command, "SIZE_INFO")) + { + return true; + } + + SetError(line_number, error, "Invalid command '%*s'", static_cast(command.size()), command.data()); + return false; +} + +bool File::HandleFileCommand(const char* line, u32 line_number, Common::Error* error) +{ + const std::string_view filename(GetToken(line)); + const std::string_view mode(GetToken(line)); + + if (filename.empty()) + { + SetError(line_number, error, "Missing filename"); + return false; + } + + if (!TokenMatch(mode, "BINARY")) + { + SetError(line_number, error, "Only BINARY modes are supported"); + return false; + } + + m_current_file = filename; + Log_DebugPrintf("File '%s'", m_current_file->c_str()); + return true; +} + +bool File::HandleTrackCommand(const char* line, u32 line_number, Common::Error* error) +{ + if (!CompleteLastTrack(line_number, error)) + return false; + + if (!m_current_file.has_value()) + { + SetError(line_number, error, "Starting a track declaration without a file set"); + return false; + } + + const std::string_view track_number_str(GetToken(line)); + if (track_number_str.empty()) + { + SetError(line_number, error, "Missing track number"); + return false; + } + + const std::optional track_number = StringUtil::FromChars(track_number_str); + if (track_number.value_or(0) < MIN_TRACK_NUMBER || track_number.value_or(0) > MAX_TRACK_NUMBER) + { + SetError(line_number, error, "Invalid track number %d", track_number.value_or(0)); + return false; + } + + const std::string_view mode_str = GetToken(line); + TrackMode mode; + if (TokenMatch(mode_str, "AUDIO")) + mode = TrackMode::Audio; + else if (TokenMatch(mode_str, "MODE1/2048")) + mode = TrackMode::Mode1; + else if (TokenMatch(mode_str, "MODE1/2352")) + mode = TrackMode::Mode1Raw; + else if (TokenMatch(mode_str, "MODE2/2336")) + mode = TrackMode::Mode2; + else if (TokenMatch(mode_str, "MODE2/2048")) + mode = TrackMode::Mode2Form1; + else if (TokenMatch(mode_str, "MODE2/2342")) + mode = TrackMode::Mode2Form2; + else if (TokenMatch(mode_str, "MODE2/2332")) + mode = TrackMode::Mode2FormMix; + else if (TokenMatch(mode_str, "MODE2/2352")) + mode = TrackMode::Mode2Raw; + else + { + SetError(line_number, error, "Invalid mode: '*%s'", static_cast(mode_str.length()), mode_str.data()); + return false; + } + + m_current_track = Track(); + m_current_track->number = static_cast(track_number.value()); + m_current_track->file = m_current_file.value(); + m_current_track->mode = mode; + return true; +} + +bool File::HandleIndexCommand(const char* line, u32 line_number, Common::Error* error) +{ + if (!m_current_track.has_value()) + { + SetError(line_number, error, "Setting index without track"); + return false; + } + + const std::string_view index_number_str(GetToken(line)); + if (index_number_str.empty()) + { + SetError(line_number, error, "Missing index number"); + return false; + } + + const std::optional index_number = StringUtil::FromChars(index_number_str); + if (index_number.value_or(-1) < MIN_INDEX_NUMBER || index_number.value_or(-1) > MAX_INDEX_NUMBER) + { + SetError(line_number, error, "Invalid index number %d", index_number.value_or(-1)); + return false; + } + + if (m_current_track->GetIndex(static_cast(index_number.value())) != nullptr) + { + SetError(line_number, error, "Duplicate index %d", index_number.value()); + return false; + } + + const std::string_view msf_str(GetToken(line)); + if (msf_str.empty()) + { + SetError(line_number, error, "Missing index location"); + return false; + } + + const std::optional msf(GetMSF(msf_str)); + if (!msf.has_value()) + { + SetError(line_number, error, "Invalid index location '%*s'", static_cast(msf_str.size()), msf_str.data()); + return false; + } + + m_current_track->indices.emplace_back(static_cast(index_number.value()), msf.value()); + return true; +} + +bool File::HandlePregapCommand(const char* line, u32 line_number, Common::Error* error) +{ + if (!m_current_track.has_value()) + { + SetError(line_number, error, "Setting pregap without track"); + return false; + } + + if (m_current_track->zero_pregap.has_value()) + { + SetError(line_number, error, "Pregap already specified for track %u", m_current_track->number); + return false; + } + + const std::string_view msf_str(GetToken(line)); + if (msf_str.empty()) + { + SetError(line_number, error, "Missing pregap location"); + return false; + } + + const std::optional msf(GetMSF(msf_str)); + if (!msf.has_value()) + { + SetError(line_number, error, "Invalid pregap location '%*s'", static_cast(msf_str.size()), msf_str.data()); + return false; + } + + m_current_track->zero_pregap = std::move(msf); + return true; +} + +bool File::HandleFlagCommand(const char* line, u32 line_number, Common::Error* error) +{ + if (!m_current_track.has_value()) + { + SetError(line_number, error, "Flags command outside of track"); + return false; + } + + for (;;) + { + const std::string_view token(GetToken(line)); + if (token.empty()) + break; + + if (TokenMatch(token, "PRE")) + m_current_track->SetFlag(TrackFlag::PreEmphasis); + else if (TokenMatch(token, "DCP")) + m_current_track->SetFlag(TrackFlag::CopyPermitted); + else if (TokenMatch(token, "4CH")) + m_current_track->SetFlag(TrackFlag::FourChannelAudio); + else if (TokenMatch(token, "SCMS")) + m_current_track->SetFlag(TrackFlag::SerialCopyManagement); + else + Log_WarningPrintf("Unknown track flag '%*s'", static_cast(token.size()), token.data()); + } + + return true; +} + +bool File::CompleteLastTrack(u32 line_number, Common::Error* error) +{ + if (!m_current_track.has_value()) + return true; + + const MSF* index1 = m_current_track->GetIndex(1); + if (!index1) + { + SetError(line_number, error, "Track %u is missing index 1", m_current_track->number); + return false; + } + + const MSF* index0 = m_current_track->GetIndex(0); + if (index0 && m_current_track->zero_pregap.has_value()) + { + SetError(line_number, error, "Zero pregap and index 0 specified in track %u", m_current_track->number); + return false; + } + + // check indices + for (const auto& [index_number, index_msf] : m_current_track->indices) + { + if (index_number == 0) + continue; + + const MSF* prev_index = m_current_track->GetIndex(index_number - 1); + if (prev_index && *prev_index > index_msf) + { + SetError(line_number, error, "Index %u is after index %u in track %u", index_number - 1, index_number, + m_current_track->number); + return false; + } + } + + m_current_track->start = *index1; + + m_tracks.push_back(std::move(m_current_track.value())); + m_current_track.reset(); + return true; +} + +bool File::SetTrackLengths(u32 line_number, Common::Error* error) +{ + for (const Track& track : m_tracks) + { + if (track.number > 1) + { + // set the length of the previous track based on this track's start, if they're the same file + Track* previous_track = GetMutableTrack(track.number - 1); + if (previous_track && previous_track->file == track.file) + { + if (previous_track->start > track.start) + { + SetError(line_number, error, "Track %u start greater than track %u start", previous_track->number, + track.number); + return false; + } + + previous_track->length = MSF::FromLBA(track.start.ToLBA() - previous_track->start.ToLBA()); + } + } + } + + return true; +} + +const CueParser::MSF* Track::GetIndex(u32 n) const +{ + for (const auto& it : indices) + { + if (it.first == n) + return &it.second; + } + + return nullptr; +} + +} // namespace CueParser \ No newline at end of file diff --git a/src/common/cue_parser.h b/src/common/cue_parser.h new file mode 100644 index 000000000..ef5e12e85 --- /dev/null +++ b/src/common/cue_parser.h @@ -0,0 +1,86 @@ +#pragma once +#include "types.h" +#include "cd_image.h" +#include +#include +#include +#include + +namespace Common { +class Error; +} + +namespace CueParser { + +using TrackMode = CDImage::TrackMode; +using MSF = CDImage::Position; + +enum : s32 +{ + MIN_TRACK_NUMBER = 1, + MAX_TRACK_NUMBER = 99, + MIN_INDEX_NUMBER = 0, + MAX_INDEX_NUMBER = 99 +}; + +enum class TrackFlag : u32 +{ + PreEmphasis = (1 << 0), + CopyPermitted = (1 << 1), + FourChannelAudio = (1 << 2), + SerialCopyManagement = (1 << 3), +}; + +struct Track +{ + u32 number; + u32 flags; + std::string file; + std::vector> indices; + TrackMode mode; + MSF start; + std::optional length; + std::optional zero_pregap; + + const MSF* GetIndex(u32 n) const; + + ALWAYS_INLINE bool HasFlag(TrackFlag flag) const { return (flags & static_cast(flag)) != 0; } + ALWAYS_INLINE void SetFlag(TrackFlag flag) { flags |= static_cast(flag); } + ALWAYS_INLINE void RemoveFlag(TrackFlag flag) { flags &= ~static_cast(flag); } +}; + +class File +{ +public: + File(); + ~File(); + + const Track* GetTrack(u32 n) const; + + bool Parse(std::FILE* fp, Common::Error* error); + +private: + Track* GetMutableTrack(u32 n); + + void SetError(u32 line_number, Common::Error* error, const char* format, ...); + + static std::string_view GetToken(const char*& line); + static std::optional GetMSF(const std::string_view& token); + + bool ParseLine(const char* line, u32 line_number, Common::Error* error); + + bool HandleFileCommand(const char* line, u32 line_number, Common::Error* error); + bool HandleTrackCommand(const char* line, u32 line_number, Common::Error* error); + bool HandleIndexCommand(const char* line, u32 line_number, Common::Error* error); + bool HandlePregapCommand(const char* line, u32 line_number, Common::Error* error); + bool HandleFlagCommand(const char* line, u32 line_number, Common::Error* error); + + bool CompleteLastTrack(u32 line_number, Common::Error* error); + bool SetTrackLengths(u32 line_number, Common::Error* error); + + std::vector m_tracks; + std::optional m_current_file; + std::optional m_current_track; +}; + +} // namespace CueParser \ No newline at end of file diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index b45997f00..9523a558f 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -63,9 +63,6 @@ {bb08260f-6fbc-46af-8924-090ee71360c6} - - {6a4208ed-e3dc-41e1-81cd-f61025fc285a} - {4ba0a6d4-3ae1-42b2-9347-096fd023ff64} diff --git a/src/scmversion/scmversion.vcxproj b/src/scmversion/scmversion.vcxproj index 4dcfae9e3..79b37e7e6 100644 --- a/src/scmversion/scmversion.vcxproj +++ b/src/scmversion/scmversion.vcxproj @@ -60,9 +60,6 @@ {425d6c99-d1c8-43c2-b8ac-4d7b1d941017} - - {6a4208ed-e3dc-41e1-81cd-f61025fc285a} -