CDImageCue: Support reading .wav files (WAVE cuesheet files)

This commit is contained in:
Stenzek 2024-11-22 15:48:35 +10:00
parent e6892e0a54
commit 0079f7a285
No known key found for this signature in database
6 changed files with 286 additions and 53 deletions

View File

@ -38,6 +38,8 @@ public:
SUBCHANNEL_BYTES_PER_FRAME = 12, SUBCHANNEL_BYTES_PER_FRAME = 12,
LEAD_OUT_SECTOR_COUNT = 6750, LEAD_OUT_SECTOR_COUNT = 6750,
ALL_SUBCODE_SIZE = 96, ALL_SUBCODE_SIZE = 96,
AUDIO_SAMPLE_RATE = 44100,
AUDIO_CHANNELS = 2,
}; };
enum : u8 enum : u8

View File

@ -3,7 +3,9 @@
#include "cd_image.h" #include "cd_image.h"
#include "cue_parser.h" #include "cue_parser.h"
#include "wav_reader_writer.h"
#include "common/align.h"
#include "common/assert.h" #include "common/assert.h"
#include "common/error.h" #include "common/error.h"
#include "common/file_system.h" #include "common/file_system.h"
@ -34,24 +36,157 @@ protected:
bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override;
private: private:
struct TrackFile class TrackFileInterface
{ {
std::string filename; public:
std::FILE* file; TrackFileInterface(std::string filename);
u64 file_position; 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<TrackFile> 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<std::unique_ptr<TrackFileInterface>> m_files;
}; };
} // namespace } // 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<s64>(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);
// position is indeterminate now
m_file_position = std::numeric_limits<decltype(m_file_position)>::max();
return false;
}
m_file_position += size;
return true;
}
u64 CDImageCueSheet::BinaryTrackFileInterface::GetSize()
{
return static_cast<u64>(std::max<s64>(FileSystem::FSize64(m_file.get()), 0));
}
u64 CDImageCueSheet::BinaryTrackFileInterface::GetDiskSize()
{
return static_cast<u64>(std::max<s64>(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<u8*>(buffer) + (num_frames_to_read * 4), 0, 4 * padding);
return true;
}
u64 CDImageCueSheet::WaveTrackFileInterface::GetSize()
{
return Common::AlignUp(static_cast<u64>(m_reader.GetNumFrames()) * 4, 2352);
}
u64 CDImageCueSheet::WaveTrackFileInterface::GetDiskSize()
{
return m_reader.GetFileSize();
}
CDImageCueSheet::CDImageCueSheet() = default; CDImageCueSheet::CDImageCueSheet() = default;
CDImageCueSheet::~CDImageCueSheet() CDImageCueSheet::~CDImageCueSheet() = default;
{
std::for_each(m_files.begin(), m_files.end(), [](TrackFile& t) { std::fclose(t.file); });
}
bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error) bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error)
{ {
@ -88,30 +223,53 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error)
u32 track_file_index = 0; u32 track_file_index = 0;
for (; track_file_index < m_files.size(); track_file_index++) for (; track_file_index < m_files.size(); track_file_index++)
{ {
const TrackFile& t = m_files[track_file_index]; if (m_files[track_file_index]->GetFilename() == track_filename)
if (t.filename == track_filename)
break; break;
} }
if (track_file_index == m_files.size()) if (track_file_index == m_files.size())
{ {
const std::string track_full_filename( std::string track_full_filename =
!Path::IsAbsolute(track_filename) ? Path::BuildRelativePath(m_filename, track_filename) : track_filename); !Path::IsAbsolute(track_filename) ? Path::BuildRelativePath(m_filename, track_filename) : track_filename;
Error track_error; Error track_error;
std::FILE* track_fp = FileSystem::OpenCFile(track_full_filename.c_str(), "rb", &track_error); std::unique_ptr<TrackFileInterface> track_file;
if (!track_fp && track_file_index == 0)
if (track->file_format == CueParser::FileFormat::Binary)
{ {
// many users have bad cuesheets, or they're renamed the files without updating the cuesheet. FileSystem::ManagedCFilePtr track_fp =
// so, try searching for a bin with the same name as the cue, but only for the first referenced file. FileSystem::OpenManagedCFile(track_full_filename.c_str(), "rb", &track_error);
const std::string alternative_filename(Path::ReplaceExtension(filename, "bin")); if (!track_fp && track_file_index == 0)
track_fp = FileSystem::OpenCFile(alternative_filename.c_str(), "rb");
if (track_fp)
{ {
WARNING_LOG("Your cue sheet references an invalid file '{}', but this was found at '{}' instead.", // many users have bad cuesheets, or they're renamed the files without updating the cuesheet.
track_filename, alternative_filename); // 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<BinaryTrackFileInterface>(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<WaveTrackFileInterface>(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, ERROR_LOG("Failed to open track filename '{}' (from '{}' and '{}'): {}", track_full_filename, track_filename,
filename, track_error.GetDescription()); filename, track_error.GetDescription());
@ -120,7 +278,7 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error)
return false; 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 // data type determines the sector size
@ -138,9 +296,7 @@ bool CDImageCueSheet::OpenAndParse(const char* filename, Error* error)
LBA track_length; LBA track_length;
if (!track->length.has_value()) if (!track->length.has_value())
{ {
FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_END); u64 file_size = m_files[track_file_index]->GetSize();
u64 file_size = static_cast<u64>(FileSystem::FTell64(m_files[track_file_index].file));
FileSystem::FSeek64(m_files[track_file_index].file, 0, SEEK_SET);
file_size /= track_sector_size; file_size /= track_sector_size;
if (track_start >= file_size) if (track_start >= file_size)
@ -296,23 +452,15 @@ bool CDImageCueSheet::ReadSectorFromIndex(void* buffer, const Index& index, LBA
{ {
DebugAssert(index.file_index < m_files.size()); 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<u64>(lba_in_index) * index.file_sector_size); const u64 file_position = index.file_offset + (static_cast<u64>(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<long>(file_position), SEEK_SET) != 0) ERROR_LOG("Failed to read LBA {}: {}", lba_in_index, error.GetDescription());
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<long>(tf.file_position), SEEK_SET);
return false; return false;
} }
tf.file_position += index.file_sector_size;
return true; return true;
} }
@ -320,8 +468,8 @@ s64 CDImageCueSheet::GetSizeOnDisk() const
{ {
// Doesn't include the cue.. but they're tiny anyway, whatever. // Doesn't include the cue.. but they're tiny anyway, whatever.
u64 size = 0; u64 size = 0;
for (const TrackFile& tf : m_files) for (const std::unique_ptr<TrackFileInterface>& tf : m_files)
size += FileSystem::FSize64(tf.file); size += tf->GetDiskSize();
return size; return size;
} }

View File

@ -224,13 +224,22 @@ bool CueParser::File::HandleFileCommand(const char* line, u32 line_number, Error
return false; 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; return false;
} }
m_current_file = filename; m_current_file = {std::string(filename), format};
DEBUG_LOG("File '{}'", filename); DEBUG_LOG("File '{}'", filename);
return true; return true;
} }
@ -285,8 +294,9 @@ bool CueParser::File::HandleTrackCommand(const char* line, u32 line_number, Erro
} }
m_current_track = Track(); m_current_track = Track();
m_current_track->number = static_cast<u32>(track_number.value()); m_current_track->number = static_cast<u8>(track_number.value());
m_current_track->file = m_current_file.value(); m_current_track->file = m_current_file->first;
m_current_track->file_format = m_current_file->second;
m_current_track->mode = mode; m_current_track->mode = mode;
return true; return true;
} }

View File

@ -26,7 +26,7 @@ enum : s32
MAX_INDEX_NUMBER = 99 MAX_INDEX_NUMBER = 99
}; };
enum class TrackFlag : u32 enum class TrackFlag : u8
{ {
PreEmphasis = (1 << 0), PreEmphasis = (1 << 0),
CopyPermitted = (1 << 1), CopyPermitted = (1 << 1),
@ -34,13 +34,21 @@ enum class TrackFlag : u32
SerialCopyManagement = (1 << 3), SerialCopyManagement = (1 << 3),
}; };
enum class FileFormat : u8
{
Binary,
Wave,
MaxCount
};
struct Track struct Track
{ {
u32 number; u8 number;
u32 flags; u8 flags;
TrackMode mode;
FileFormat file_format;
std::string file; std::string file;
std::vector<std::pair<u32, MSF>> indices; std::vector<std::pair<u32, MSF>> indices;
TrackMode mode;
MSF start; MSF start;
std::optional<MSF> length; std::optional<MSF> length;
std::optional<MSF> zero_pregap; std::optional<MSF> zero_pregap;
@ -82,7 +90,7 @@ private:
bool SetTrackLengths(u32 line_number, Error* error); bool SetTrackLengths(u32 line_number, Error* error);
std::vector<Track> m_tracks; std::vector<Track> m_tracks;
std::optional<std::string> m_current_file; std::optional<std::pair<std::string, FileFormat>> m_current_file;
std::optional<Track> m_current_track; std::optional<Track> m_current_track;
}; };

View File

@ -60,12 +60,31 @@ static constexpr u32 WAVE_VALUE = 0x45564157; // 0x57415645
WAVReader::WAVReader() = default; 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() WAVReader::~WAVReader()
{ {
if (IsOpen()) if (IsOpen())
Close(); 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<typename T> template<typename T>
static bool FindChunk(std::FILE* fp, T* chunk, u32 tag, Error* error, bool skip_extra_bytes) 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; 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<u64>(std::max<s64>(FileSystem::FSize64(m_file), 1));
}
bool WAVReader::SeekToFrame(u32 num, Error* error) bool WAVReader::SeekToFrame(u32 num, Error* error)
{ {
const s64 offset = m_frames_start + (static_cast<s64>(num) * (sizeof(s16) * m_num_channels)); const s64 offset = m_frames_start + (static_cast<s64>(num) * (sizeof(s16) * m_num_channels));
return FileSystem::FSeek64(m_file, offset, SEEK_SET, error); 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) 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() = 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() WAVWriter::~WAVWriter()
{ {
if (IsOpen()) if (IsOpen())
Close(nullptr); 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) bool WAVWriter::Open(const char* path, u32 sample_rate, u32 num_channels, Error* error)
{ {
if (IsOpen()) if (IsOpen())

View File

@ -13,19 +13,28 @@ class WAVReader
{ {
public: public:
WAVReader(); WAVReader();
WAVReader(WAVReader&& move);
WAVReader(const WAVReader&) = delete;
~WAVReader(); ~WAVReader();
WAVReader& operator=(WAVReader&& move);
WAVReader& operator=(const WAVReader&) = delete;
ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; } ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; }
ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; } ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; }
ALWAYS_INLINE u32 GetNumFrames() const { return m_num_frames; } 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); } ALWAYS_INLINE bool IsOpen() const { return (m_file != nullptr); }
bool Open(const char* path, Error* error = nullptr); bool Open(const char* path, Error* error = nullptr);
void Close(); void Close();
std::FILE* TakeFile();
u64 GetFileSize();
bool SeekToFrame(u32 num, Error* error = nullptr); 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: private:
using SampleType = s16; using SampleType = s16;
@ -41,8 +50,13 @@ class WAVWriter
{ {
public: public:
WAVWriter(); WAVWriter();
WAVWriter(WAVWriter&& move);
WAVWriter(const WAVWriter&) = delete;
~WAVWriter(); ~WAVWriter();
WAVWriter& operator=(WAVWriter&& move);
WAVWriter& operator=(const WAVWriter&) = delete;
ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; } ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; }
ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; } ALWAYS_INLINE u32 GetNumChannels() const { return m_num_channels; }
ALWAYS_INLINE u32 GetNumFrames() const { return m_num_frames; } ALWAYS_INLINE u32 GetNumFrames() const { return m_num_frames; }