From f51c19702058da4287ff1006f614c38607f3f79a Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 22 Nov 2024 15:48:35 +1000 Subject: [PATCH] CDImageCue: Support reading .wav files (WAVE cuesheet files) --- src/util/cd_image.h | 2 + src/util/cd_image_cue.cpp | 226 +++++++++++++++++++++++++++------ src/util/cue_parser.cpp | 20 ++- src/util/cue_parser.h | 18 ++- src/util/wav_reader_writer.cpp | 53 +++++++- src/util/wav_reader_writer.h | 16 ++- 6 files changed, 282 insertions(+), 53 deletions(-) diff --git a/src/util/cd_image.h b/src/util/cd_image.h index a14850803..a7c739201 100644 --- a/src/util/cd_image.h +++ b/src/util/cd_image.h @@ -38,6 +38,8 @@ public: SUBCHANNEL_BYTES_PER_FRAME = 12, LEAD_OUT_SECTOR_COUNT = 6750, ALL_SUBCODE_SIZE = 96, + AUDIO_SAMPLE_RATE = 44100, + AUDIO_CHANNELS = 2, }; enum : u8 diff --git a/src/util/cd_image_cue.cpp b/src/util/cd_image_cue.cpp index c4e52c84e..82d3c99c8 100644 --- a/src/util/cd_image_cue.cpp +++ b/src/util/cd_image_cue.cpp @@ -3,7 +3,9 @@ #include "cd_image.h" #include "cue_parser.h" +#include "wav_reader_writer.h" +#include "common/align.h" #include "common/assert.h" #include "common/error.h" #include "common/file_system.h" @@ -34,24 +36,153 @@ protected: bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; private: - struct TrackFile + class TrackFileInterface { - std::string filename; - std::FILE* file; - u64 file_position; + public: + TrackFileInterface(std::string filename); + virtual ~TrackFileInterface(); + + ALWAYS_INLINE const std::string& GetFilename() const { return m_filename; } + + virtual u64 GetSize() = 0; + virtual u64 GetDiskSize() = 0; + + virtual bool Read(void* buffer, u64 offset, u32 size, Error* error) = 0; + + private: + std::string m_filename; }; - std::vector m_files; + struct BinaryTrackFileInterface final : public TrackFileInterface + { + public: + BinaryTrackFileInterface(std::string filename, FileSystem::ManagedCFilePtr file); + ~BinaryTrackFileInterface() override; + + u64 GetSize() override; + u64 GetDiskSize() override; + + bool Read(void* buffer, u64 offset, u32 size, Error* error) override; + + private: + FileSystem::ManagedCFilePtr m_file; + u64 m_file_position = 0; + }; + + struct WaveTrackFileInterface final : public TrackFileInterface + { + public: + WaveTrackFileInterface(std::string filename, WAVReader reader); + ~WaveTrackFileInterface() override; + + u64 GetSize() override; + u64 GetDiskSize() override; + + bool Read(void* buffer, u64 offset, u32 size, Error* error) override; + + private: + WAVReader m_reader; + }; + + std::vector> m_files; }; } // namespace +CDImageCueSheet::TrackFileInterface::TrackFileInterface(std::string filename) : m_filename(std::move(filename)) +{ +} + +CDImageCueSheet::TrackFileInterface::~TrackFileInterface() = default; + +CDImageCueSheet::BinaryTrackFileInterface::BinaryTrackFileInterface(std::string filename, + FileSystem::ManagedCFilePtr file) + : TrackFileInterface(std::move(filename)), m_file(std::move(file)) +{ +} + +CDImageCueSheet::BinaryTrackFileInterface::~BinaryTrackFileInterface() = default; + +bool CDImageCueSheet::BinaryTrackFileInterface::Read(void* buffer, u64 offset, u32 size, Error* error) +{ + if (m_file_position != offset) + { + if (!FileSystem::FSeek64(m_file.get(), static_cast(offset), SEEK_SET, error)) [[unlikely]] + return false; + + m_file_position = offset; + } + + if (std::fread(buffer, size, 1, m_file.get()) != 1) [[unlikely]] + { + Error::SetErrno(error, "fread() failed: ", errno); + return false; + } + + return true; +} + +u64 CDImageCueSheet::BinaryTrackFileInterface::GetSize() +{ + return static_cast(std::max(FileSystem::FSize64(m_file.get()), 0)); +} + +u64 CDImageCueSheet::BinaryTrackFileInterface::GetDiskSize() +{ + return static_cast(std::max(FileSystem::FSize64(m_file.get()), 0)); +} + +CDImageCueSheet::WaveTrackFileInterface::WaveTrackFileInterface(std::string filename, WAVReader reader) + : TrackFileInterface(std::move(filename)), m_reader(std::move(reader)) +{ +} + +CDImageCueSheet::WaveTrackFileInterface::~WaveTrackFileInterface() = default; + +bool CDImageCueSheet::WaveTrackFileInterface::Read(void* buffer, u64 offset, u32 size, Error* error) +{ + // Should always be a multiple of 4 (sizeof frame). + if ((offset & 3) != 0 || (size & 3) != 0) [[unlikely]] + return false; + + // We shouldn't have any extra CD frames. + const u32 frame_number = Truncate32(offset / 4); + if (frame_number >= m_reader.GetNumFrames()) [[unlikely]] + { + Error::SetStringView(error, "Attempted read past end of WAV file"); + return false; + } + + // Do we need to pad the read? + const u32 num_frames = size / 4; + const u32 num_frames_to_read = std::min(num_frames, m_reader.GetNumFrames() - frame_number); + if (num_frames_to_read > 0) + { + if (!m_reader.SeekToFrame(frame_number, error) || !m_reader.ReadFrames(buffer, num_frames_to_read, error)) + return false; + } + + // Padding. + const u32 padding = num_frames - num_frames_to_read; + if (padding > 0) + std::memset(static_cast(buffer) + (num_frames_to_read * 4), 0, 4 * padding); + + return true; +} + +u64 CDImageCueSheet::WaveTrackFileInterface::GetSize() +{ + return Common::AlignUp(static_cast(m_reader.GetNumFrames()) * 4, 2352); +} + +u64 CDImageCueSheet::WaveTrackFileInterface::GetDiskSize() +{ + return m_reader.GetFileSize(); +} + CDImageCueSheet::CDImageCueSheet() = default; -CDImageCueSheet::~CDImageCueSheet() -{ - std::for_each(m_files.begin(), m_files.end(), [](TrackFile& t) { std::fclose(t.file); }); -} +CDImageCueSheet::~CDImageCueSheet() = default; bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error) { @@ -88,30 +219,53 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error) u32 track_file_index = 0; for (; track_file_index < m_files.size(); track_file_index++) { - const TrackFile& t = m_files[track_file_index]; - if (t.filename == track_filename) + if (m_files[track_file_index]->GetFilename() == track_filename) break; } if (track_file_index == m_files.size()) { - const std::string track_full_filename( - !Path::IsAbsolute(track_filename) ? Path::BuildRelativePath(m_filename, track_filename) : track_filename); + std::string track_full_filename = + !Path::IsAbsolute(track_filename) ? Path::BuildRelativePath(m_filename, track_filename) : track_filename; Error track_error; - std::FILE* track_fp = FileSystem::OpenCFile(track_full_filename.c_str(), "rb", &track_error); - if (!track_fp && track_file_index == 0) + std::unique_ptr track_file; + + if (track->file_format == CueParser::FileFormat::Binary) { - // many users have bad cuesheets, or they're renamed the files without updating the cuesheet. - // so, try searching for a bin with the same name as the cue, but only for the first referenced file. - const std::string alternative_filename(Path::ReplaceExtension(filename, "bin")); - track_fp = FileSystem::OpenCFile(alternative_filename.c_str(), "rb"); - if (track_fp) + FileSystem::ManagedCFilePtr track_fp = + FileSystem::OpenManagedCFile(track_full_filename.c_str(), "rb", &track_error); + if (!track_fp && track_file_index == 0) { - WARNING_LOG("Your cue sheet references an invalid file '{}', but this was found at '{}' instead.", - track_filename, alternative_filename); + // many users have bad cuesheets, or they're renamed the files without updating the cuesheet. + // so, try searching for a bin with the same name as the cue, but only for the first referenced file. + std::string alternative_filename = Path::ReplaceExtension(filename, "bin"); + track_fp = FileSystem::OpenManagedCFile(alternative_filename.c_str(), "rb"); + if (track_fp) + { + WARNING_LOG("Your cue sheet references an invalid file '{}', but this was found at '{}' instead.", + track_filename, alternative_filename); + track_full_filename = std::move(alternative_filename); + } + } + if (track_fp) + track_file = std::make_unique(std::move(track_full_filename), std::move(track_fp)); + } + else if (track->file_format == CueParser::FileFormat::Wave) + { + // Since all the frames are packed tightly in the wave file, we only need to get the start offset. + WAVReader reader; + if (reader.Open(track_full_filename.c_str(), &track_error)) + { + if (reader.GetNumChannels() != AUDIO_CHANNELS || reader.GetSampleRate() != AUDIO_SAMPLE_RATE) + { + Error::SetStringFmt(error, "WAV files must be stereo and use a sample rate of 44100hz."); + return false; + } + + track_file = std::make_unique(std::move(track_full_filename), std::move(reader)); } } - if (!track_fp) + if (!track_file) { ERROR_LOG("Failed to open track filename '{}' (from '{}' and '{}'): {}", track_full_filename, track_filename, filename, track_error.GetDescription()); @@ -120,7 +274,7 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error) return false; } - m_files.push_back(TrackFile{track_filename, track_fp, 0}); + m_files.push_back(std::move(track_file)); } // data type determines the sector size @@ -138,9 +292,7 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error) LBA track_length; if (!track->length.has_value()) { - 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); + u64 file_size = m_files[track_file_index]->GetSize(); file_size /= track_sector_size; if (track_start >= file_size) @@ -296,23 +448,15 @@ bool CDImageCueSheet::ReadSectorFromIndex(void* buffer, const Index& index, LBA { DebugAssert(index.file_index < m_files.size()); - TrackFile& tf = m_files[index.file_index]; + TrackFileInterface* tf = m_files[index.file_index].get(); const u64 file_position = index.file_offset + (static_cast(lba_in_index) * index.file_sector_size); - if (tf.file_position != file_position) + Error error; + if (!tf->Read(buffer, file_position, index.file_sector_size, &error)) [[unlikely]] { - if (std::fseek(tf.file, static_cast(file_position), SEEK_SET) != 0) - return false; - - tf.file_position = file_position; - } - - if (std::fread(buffer, index.file_sector_size, 1, tf.file) != 1) - { - std::fseek(tf.file, static_cast(tf.file_position), SEEK_SET); + ERROR_LOG("Failed to read LBA {}: {}", lba_in_index, error.GetDescription()); return false; } - tf.file_position += index.file_sector_size; return true; } @@ -320,8 +464,8 @@ s64 CDImageCueSheet::GetSizeOnDisk() const { // Doesn't include the cue.. but they're tiny anyway, whatever. u64 size = 0; - for (const TrackFile& tf : m_files) - size += FileSystem::FSize64(tf.file); + for (const std::unique_ptr& tf : m_files) + size += tf->GetDiskSize(); return size; } diff --git a/src/util/cue_parser.cpp b/src/util/cue_parser.cpp index f5fbf5a3b..a67d23382 100644 --- a/src/util/cue_parser.cpp +++ b/src/util/cue_parser.cpp @@ -224,13 +224,22 @@ bool CueParser::File::HandleFileCommand(const char* line, u32 line_number, Error return false; } - if (!TokenMatch(mode, "BINARY")) + FileFormat format; + if (TokenMatch(mode, "BINARY")) { - SetError(line_number, error, "Only BINARY modes are supported"); + format = FileFormat::Binary; + } + else if (TokenMatch(mode, "WAVE")) + { + format = FileFormat::Wave; + } + else + { + SetError(line_number, error, "Only BINARY and WAVE modes are supported"); return false; } - m_current_file = filename; + m_current_file = {std::string(filename), format}; DEBUG_LOG("File '{}'", filename); return true; } @@ -285,8 +294,9 @@ bool CueParser::File::HandleTrackCommand(const char* line, u32 line_number, Erro } m_current_track = Track(); - m_current_track->number = static_cast(track_number.value()); - m_current_track->file = m_current_file.value(); + m_current_track->number = static_cast(track_number.value()); + m_current_track->file = m_current_file->first; + m_current_track->file_format = m_current_file->second; m_current_track->mode = mode; return true; } diff --git a/src/util/cue_parser.h b/src/util/cue_parser.h index 397b181e3..8080d511f 100644 --- a/src/util/cue_parser.h +++ b/src/util/cue_parser.h @@ -26,7 +26,7 @@ enum : s32 MAX_INDEX_NUMBER = 99 }; -enum class TrackFlag : u32 +enum class TrackFlag : u8 { PreEmphasis = (1 << 0), CopyPermitted = (1 << 1), @@ -34,13 +34,21 @@ enum class TrackFlag : u32 SerialCopyManagement = (1 << 3), }; +enum class FileFormat : u8 +{ + Binary, + Wave, + MaxCount +}; + struct Track { - u32 number; - u32 flags; + u8 number; + u8 flags; + TrackMode mode; + FileFormat file_format; std::string file; std::vector> indices; - TrackMode mode; MSF start; std::optional length; std::optional zero_pregap; @@ -82,7 +90,7 @@ private: bool SetTrackLengths(u32 line_number, Error* error); std::vector m_tracks; - std::optional m_current_file; + std::optional> m_current_file; std::optional m_current_track; }; diff --git a/src/util/wav_reader_writer.cpp b/src/util/wav_reader_writer.cpp index 0ebb559a0..b72f7cba3 100644 --- a/src/util/wav_reader_writer.cpp +++ b/src/util/wav_reader_writer.cpp @@ -60,12 +60,31 @@ static constexpr u32 WAVE_VALUE = 0x45564157; // 0x57415645 WAVReader::WAVReader() = default; +WAVReader::WAVReader(WAVReader&& move) +{ + m_file = std::exchange(move.m_file, nullptr); + m_frames_start = std::exchange(move.m_frames_start, 0); + m_sample_rate = std::exchange(move.m_sample_rate, 0); + m_num_channels = std::exchange(move.m_num_channels, 0); + m_num_frames = std::exchange(move.m_num_frames, 0); +} + WAVReader::~WAVReader() { if (IsOpen()) Close(); } +WAVReader& WAVReader::operator=(WAVReader&& move) +{ + m_file = std::exchange(move.m_file, nullptr); + m_frames_start = std::exchange(move.m_frames_start, 0); + m_sample_rate = std::exchange(move.m_sample_rate, 0); + m_num_channels = std::exchange(move.m_num_channels, 0); + m_num_frames = std::exchange(move.m_num_frames, 0); + return *this; +} + template static bool FindChunk(std::FILE* fp, T* chunk, u32 tag, Error* error, bool skip_extra_bytes) { @@ -180,13 +199,28 @@ void WAVReader::Close() m_num_frames = 0; } +std::FILE* WAVReader::TakeFile() +{ + std::FILE* ret = std::exchange(m_file, nullptr); + m_sample_rate = 0; + m_frames_start = 0; + m_num_channels = 0; + m_num_frames = 0; + return ret; +} + +u64 WAVReader::GetFileSize() +{ + return static_cast(std::max(FileSystem::FSize64(m_file), 1)); +} + bool WAVReader::SeekToFrame(u32 num, Error* error) { const s64 offset = m_frames_start + (static_cast(num) * (sizeof(s16) * m_num_channels)); return FileSystem::FSeek64(m_file, offset, SEEK_SET, error); } -bool WAVReader::ReadFrames(s16* samples, u32 num_frames, Error* error /*= nullptr*/) +bool WAVReader::ReadFrames(void* samples, u32 num_frames, Error* error /*= nullptr*/) { if (std::fread(samples, sizeof(s16) * m_num_channels, num_frames, m_file) != num_frames) { @@ -199,12 +233,29 @@ bool WAVReader::ReadFrames(s16* samples, u32 num_frames, Error* error /*= nullpt WAVWriter::WAVWriter() = default; +WAVWriter::WAVWriter(WAVWriter&& move) +{ + m_file = std::exchange(move.m_file, nullptr); + m_sample_rate = std::exchange(move.m_sample_rate, 0); + m_num_channels = std::exchange(move.m_num_channels, 0); + m_num_frames = std::exchange(move.m_num_frames, 0); +} + WAVWriter::~WAVWriter() { if (IsOpen()) Close(nullptr); } +WAVWriter& WAVWriter::operator=(WAVWriter&& move) +{ + m_file = std::exchange(move.m_file, nullptr); + m_sample_rate = std::exchange(move.m_sample_rate, 0); + m_num_channels = std::exchange(move.m_num_channels, 0); + m_num_frames = std::exchange(move.m_num_frames, 0); + return *this; +} + bool WAVWriter::Open(const char* path, u32 sample_rate, u32 num_channels, Error* error) { if (IsOpen()) diff --git a/src/util/wav_reader_writer.h b/src/util/wav_reader_writer.h index 46a6dfa51..7149bd2ea 100644 --- a/src/util/wav_reader_writer.h +++ b/src/util/wav_reader_writer.h @@ -13,19 +13,28 @@ class WAVReader { public: WAVReader(); + WAVReader(WAVReader&& move); + WAVReader(const WAVReader&) = delete; ~WAVReader(); + WAVReader& operator=(WAVReader&& move); + WAVReader& operator=(const WAVReader&) = delete; + ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; } ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; } ALWAYS_INLINE u32 GetNumFrames() const { return m_num_frames; } + ALWAYS_INLINE u64 GetFramesStartOffset() const { return m_frames_start; } ALWAYS_INLINE bool IsOpen() const { return (m_file != nullptr); } bool Open(const char* path, Error* error = nullptr); void Close(); + std::FILE* TakeFile(); + u64 GetFileSize(); + bool SeekToFrame(u32 num, Error* error = nullptr); - bool ReadFrames(s16* samples, u32 num_frames, Error* error = nullptr); + bool ReadFrames(void* samples, u32 num_frames, Error* error = nullptr); private: using SampleType = s16; @@ -41,8 +50,13 @@ class WAVWriter { public: WAVWriter(); + WAVWriter(WAVWriter&& move); + WAVWriter(const WAVWriter&) = delete; ~WAVWriter(); + WAVWriter& operator=(WAVWriter&& move); + WAVWriter& operator=(const WAVWriter&) = delete; + ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; } ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; } ALWAYS_INLINE u32 GetNumFrames() const { return m_num_frames; }