mirror of
https://github.com/stenzek/duckstation.git
synced 2024-11-22 21:39:40 +00:00
CDImageCue: Support reading .wav files (WAVE cuesheet files)
This commit is contained in:
parent
e6892e0a54
commit
f51c197020
@ -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
|
||||
|
@ -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<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
|
||||
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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<TrackFileInterface> 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<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,
|
||||
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<u64>(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<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)
|
||||
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);
|
||||
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<TrackFileInterface>& tf : m_files)
|
||||
size += tf->GetDiskSize();
|
||||
return size;
|
||||
}
|
||||
|
||||
|
@ -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<u32>(track_number.value());
|
||||
m_current_track->file = m_current_file.value();
|
||||
m_current_track->number = static_cast<u8>(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;
|
||||
}
|
||||
|
@ -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<std::pair<u32, MSF>> indices;
|
||||
TrackMode mode;
|
||||
MSF start;
|
||||
std::optional<MSF> length;
|
||||
std::optional<MSF> zero_pregap;
|
||||
@ -82,7 +90,7 @@ private:
|
||||
bool SetTrackLengths(u32 line_number, Error* error);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -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<typename T>
|
||||
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<u64>(std::max<s64>(FileSystem::FSize64(m_file), 1));
|
||||
}
|
||||
|
||||
bool WAVReader::SeekToFrame(u32 num, Error* error)
|
||||
{
|
||||
const s64 offset = m_frames_start + (static_cast<s64>(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())
|
||||
|
@ -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; }
|
||||
|
Loading…
Reference in New Issue
Block a user