diff --git a/src/common/log_channels.h b/src/common/log_channels.h
index e5b6b74a2..aaa8b7842 100644
--- a/src/common/log_channels.h
+++ b/src/common/log_channels.h
@@ -78,7 +78,6 @@
X(Timers) \
X(TimingEvents) \
X(Ungrouped) \
- X(WAVWriter) \
X(Win32RawInputSource) \
X(WindowInfo) \
X(XInputSource)
diff --git a/src/core/spu.cpp b/src/core/spu.cpp
index b6b147cdd..4800ec264 100644
--- a/src/core/spu.cpp
+++ b/src/core/spu.cpp
@@ -14,7 +14,7 @@
#include "util/imgui_manager.h"
#include "util/media_capture.h"
#include "util/state_wrapper.h"
-#include "util/wav_writer.h"
+#include "util/wav_reader_writer.h"
#include "common/bitfield.h"
#include "common/bitutils.h"
diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt
index ee8d4c230..ceb5be2a4 100644
--- a/src/util/CMakeLists.txt
+++ b/src/util/CMakeLists.txt
@@ -67,8 +67,8 @@ add_library(util
sockets.h
state_wrapper.cpp
state_wrapper.h
- wav_writer.cpp
- wav_writer.h
+ wav_reader_writer.cpp
+ wav_reader_writer.h
window_info.cpp
window_info.h
)
diff --git a/src/util/util.vcxproj b/src/util/util.vcxproj
index 51371b21d..c56d19431 100644
--- a/src/util/util.vcxproj
+++ b/src/util/util.vcxproj
@@ -98,7 +98,7 @@
-
+
@@ -199,7 +199,7 @@
-
+
diff --git a/src/util/util.vcxproj.filters b/src/util/util.vcxproj.filters
index a93e198ef..83622b0b9 100644
--- a/src/util/util.vcxproj.filters
+++ b/src/util/util.vcxproj.filters
@@ -5,7 +5,7 @@
-
+
@@ -82,7 +82,7 @@
-
+
diff --git a/src/util/wav_reader_writer.cpp b/src/util/wav_reader_writer.cpp
new file mode 100644
index 000000000..0ebb559a0
--- /dev/null
+++ b/src/util/wav_reader_writer.cpp
@@ -0,0 +1,303 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#include "wav_reader_writer.h"
+
+#include "common/error.h"
+#include "common/file_system.h"
+#include "common/log.h"
+
+#include
+
+namespace {
+
+#pragma pack(push, 1)
+struct WAV_HEADER
+{
+ u32 chunk_id;
+ u32 chunk_size;
+ u32 format;
+};
+
+struct WAV_CHUNK_HEADER
+{
+ u32 chunk_id;
+ u32 chunk_size;
+};
+
+struct WAV_FULL_HEADER
+{
+ u32 chunk_id; // RIFF
+ u32 chunk_size;
+ u32 format; // WAVE
+
+ struct FormatChunk
+ {
+ u32 chunk_id; // "fmt "
+ u32 chunk_size;
+ u16 audio_format; // pcm = 1
+ u16 num_channels;
+ u32 sample_rate;
+ u32 byte_rate;
+ u16 block_align;
+ u16 bits_per_sample;
+ } fmt_chunk;
+
+ struct DataChunkHeader
+ {
+ u32 chunk_id; // "data "
+ u32 chunk_size;
+ } data_chunk_header;
+};
+#pragma pack(pop)
+
+static constexpr u32 RIFF_VALUE = 0x46464952; // 0x52494646
+static constexpr u32 FMT_VALUE = 0x20746d66; // 0x666d7420
+static constexpr u32 DATA_VALUE = 0x61746164; // 0x64617461
+static constexpr u32 WAVE_VALUE = 0x45564157; // 0x57415645
+
+} // namespace
+
+WAVReader::WAVReader() = default;
+
+WAVReader::~WAVReader()
+{
+ if (IsOpen())
+ Close();
+}
+
+template
+static bool FindChunk(std::FILE* fp, T* chunk, u32 tag, Error* error, bool skip_extra_bytes)
+{
+ for (;;)
+ {
+ WAV_CHUNK_HEADER header;
+ if (std::fread(&header, sizeof(header), 1, fp) != 1)
+ {
+ Error::SetErrno(error, "fread() failed: ", errno);
+ return false;
+ }
+
+ if (header.chunk_id != tag)
+ {
+ if (!FileSystem::FSeek64(fp, header.chunk_size, SEEK_CUR, error))
+ return false;
+
+ continue;
+ }
+
+ if (header.chunk_size < (sizeof(T) - sizeof(header)))
+ {
+ Error::SetStringFmt(error, "Chunk is too small (required {} got {})", sizeof(T) - sizeof(header),
+ header.chunk_size);
+ return false;
+ }
+
+ std::memcpy(chunk, &header, sizeof(header));
+ if constexpr (sizeof(T) > sizeof(header))
+ {
+ if (std::fread(reinterpret_cast(chunk) + sizeof(header), sizeof(T) - sizeof(header), 1, fp) != 1)
+ {
+ Error::SetErrno(error, "fread() for data failed: ", errno);
+ return false;
+ }
+ }
+
+ // skip over additional bytes
+ const u32 extra_bytes = header.chunk_size - (sizeof(T) - sizeof(header));
+ if (skip_extra_bytes && !FileSystem::FSeek64(fp, extra_bytes, SEEK_CUR, error))
+ return false;
+
+ return true;
+ }
+}
+
+bool WAVReader::Open(const char* path, Error* error /*= nullptr*/)
+{
+ auto fp = FileSystem::OpenManagedCFile(path, "rb", error);
+ if (!fp)
+ return false;
+
+ WAV_HEADER file_header;
+ if (std::fread(&file_header, sizeof(file_header), 1, fp.get()) != 1 || file_header.chunk_id != RIFF_VALUE ||
+ file_header.format != WAVE_VALUE)
+ {
+ Error::SetStringView(error, "Invalid file header, must be RIFF");
+ return false;
+ }
+
+ WAV_FULL_HEADER::FormatChunk format;
+ if (!FindChunk(fp.get(), &format, FMT_VALUE, error, true))
+ {
+ Error::AddPrefix(error, "Failed to get FMT chunk: ");
+ return false;
+ }
+
+ if (format.audio_format != 1) // PCM
+ {
+ Error::SetStringFmt(error, "Unsupported audio format {}", format.audio_format);
+ return false;
+ }
+
+ if (format.sample_rate == 0 || format.num_channels == 0 || format.bits_per_sample != 16)
+ {
+ Error::SetStringFmt(error, "Unsupported file format samplerate={} channels={} bits={}", format.sample_rate,
+ format.num_channels, format.bits_per_sample);
+ return false;
+ }
+
+ WAV_CHUNK_HEADER data;
+ if (!FindChunk(fp.get(), &data, DATA_VALUE, error, false))
+ {
+ Error::AddPrefix(error, "Failed to get DATA chunk: ");
+ return false;
+ }
+
+ const u32 num_frames = data.chunk_size / (sizeof(s16) * format.num_channels);
+ if (num_frames == 0)
+ {
+ Error::SetStringFmt(error, "File has no frames");
+ return false;
+ }
+
+ m_file = fp.release();
+ m_frames_start = FileSystem::FTell64(m_file);
+ m_sample_rate = format.sample_rate;
+ m_num_channels = format.num_channels;
+ m_num_frames = num_frames;
+ return true;
+}
+
+void WAVReader::Close()
+{
+ if (!IsOpen())
+ return;
+
+ std::fclose(m_file);
+ m_file = nullptr;
+ m_sample_rate = 0;
+ m_num_channels = 0;
+ m_num_frames = 0;
+}
+
+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*/)
+{
+ if (std::fread(samples, sizeof(s16) * m_num_channels, num_frames, m_file) != num_frames)
+ {
+ Error::SetErrno(error, "fread() failed: ", errno);
+ return false;
+ }
+
+ return true;
+}
+
+WAVWriter::WAVWriter() = default;
+
+WAVWriter::~WAVWriter()
+{
+ if (IsOpen())
+ Close(nullptr);
+}
+
+bool WAVWriter::Open(const char* path, u32 sample_rate, u32 num_channels, Error* error)
+{
+ if (IsOpen())
+ Close(nullptr);
+
+ m_file = FileSystem::OpenCFile(path, "wb", error);
+ if (!m_file)
+ return false;
+
+ m_sample_rate = sample_rate;
+ m_num_channels = num_channels;
+ m_num_frames = 0;
+
+ if (!WriteHeader(error))
+ {
+ m_sample_rate = 0;
+ m_num_channels = 0;
+ std::fclose(m_file);
+ m_file = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+bool WAVWriter::Close(Error* error)
+{
+ if (!IsOpen())
+ return true;
+
+ bool res = (m_num_frames != std::numeric_limits::max());
+ if (res)
+ {
+ res = FileSystem::FSeek64(m_file, 0, SEEK_SET, error) && WriteHeader(error);
+ if (std::fclose(m_file) != 0)
+ {
+ Error::SetErrno(error, "fclose() failed: ", errno);
+ res = false;
+ }
+ }
+
+ m_file = nullptr;
+ m_sample_rate = 0;
+ m_num_channels = 0;
+ m_num_frames = 0;
+ return res;
+}
+
+bool WAVWriter::WriteFrames(const s16* samples, u32 num_frames, Error* error)
+{
+ if (m_num_frames == std::numeric_limits::max())
+ {
+ Error::SetStringView(error, "Previous write failed.");
+ return false;
+ }
+
+ const u32 num_frames_written =
+ static_cast(std::fwrite(samples, sizeof(s16) * m_num_channels, num_frames, m_file));
+ if (num_frames_written != num_frames)
+ {
+ Error::SetErrno(error, "fwrite() failed: ", errno);
+ m_num_frames = std::numeric_limits::max();
+ return false;
+ }
+
+ m_num_frames += num_frames_written;
+ return true;
+}
+
+bool WAVWriter::WriteHeader(Error* error)
+{
+ const u32 data_size = sizeof(SampleType) * m_num_channels * m_num_frames;
+
+ WAV_FULL_HEADER header = {};
+ header.chunk_id = RIFF_VALUE;
+ header.chunk_size = sizeof(WAV_FULL_HEADER) - 8 + data_size;
+ header.format = WAVE_VALUE;
+ header.fmt_chunk.chunk_id = FMT_VALUE;
+ header.fmt_chunk.chunk_size = sizeof(header.fmt_chunk) - 8;
+ header.fmt_chunk.audio_format = 1;
+ header.fmt_chunk.num_channels = static_cast(m_num_channels);
+ header.fmt_chunk.sample_rate = m_sample_rate;
+ header.fmt_chunk.byte_rate = m_sample_rate * m_num_channels * sizeof(SampleType);
+ header.fmt_chunk.block_align = static_cast(m_num_channels * sizeof(SampleType));
+ header.fmt_chunk.bits_per_sample = 16;
+ header.data_chunk_header.chunk_id = DATA_VALUE;
+ header.data_chunk_header.chunk_size = data_size;
+
+ if (std::fwrite(&header, sizeof(header), 1, m_file) != 1)
+ {
+ Error::SetErrno(error, "fwrite() failed: ", errno);
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/util/wav_reader_writer.h b/src/util/wav_reader_writer.h
new file mode 100644
index 000000000..46a6dfa51
--- /dev/null
+++ b/src/util/wav_reader_writer.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin
+// SPDX-License-Identifier: CC-BY-NC-ND-4.0
+
+#pragma once
+
+#include "common/types.h"
+
+#include
+
+class Error;
+
+class WAVReader
+{
+public:
+ WAVReader();
+ ~WAVReader();
+
+ 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 bool IsOpen() const { return (m_file != nullptr); }
+
+ bool Open(const char* path, Error* error = nullptr);
+ void Close();
+
+ bool SeekToFrame(u32 num, Error* error = nullptr);
+
+ bool ReadFrames(s16* samples, u32 num_frames, Error* error = nullptr);
+
+private:
+ using SampleType = s16;
+
+ std::FILE* m_file = nullptr;
+ s64 m_frames_start = 0;
+ u32 m_sample_rate = 0;
+ u32 m_num_channels = 0;
+ u32 m_num_frames = 0;
+};
+
+class WAVWriter
+{
+public:
+ WAVWriter();
+ ~WAVWriter();
+
+ 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 bool IsOpen() const { return (m_file != nullptr); }
+
+ bool Open(const char* path, u32 sample_rate, u32 num_channels, Error* error = nullptr);
+ bool Close(Error* error);
+
+ bool WriteFrames(const s16* samples, u32 num_frames, Error* error = nullptr);
+
+private:
+ using SampleType = s16;
+
+ bool WriteHeader(Error* error);
+
+ std::FILE* m_file = nullptr;
+ u32 m_sample_rate = 0;
+ u32 m_num_channels = 0;
+ u32 m_num_frames = 0;
+};
diff --git a/src/util/wav_writer.cpp b/src/util/wav_writer.cpp
deleted file mode 100644
index 877918467..000000000
--- a/src/util/wav_writer.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin
-// SPDX-License-Identifier: CC-BY-NC-ND-4.0
-
-#include "wav_writer.h"
-#include "common/file_system.h"
-#include "common/log.h"
-LOG_CHANNEL(WAVWriter);
-
-namespace {
-#pragma pack(push, 1)
-struct WAV_HEADER
-{
- u32 chunk_id; // RIFF
- u32 chunk_size;
- u32 format; // WAVE
-
- struct FormatChunk
- {
- u32 chunk_id; // "fmt "
- u32 chunk_size;
- u16 audio_format; // pcm = 1
- u16 num_channels;
- u32 sample_rate;
- u32 byte_rate;
- u16 block_align;
- u16 bits_per_sample;
- } fmt_chunk;
-
- struct DataChunkHeader
- {
- u32 chunk_id; // "data "
- u32 chunk_size;
- } data_chunk_header;
-};
-#pragma pack(pop)
-} // namespace
-
-WAVWriter::WAVWriter() = default;
-
-WAVWriter::~WAVWriter()
-{
- if (IsOpen())
- Close();
-}
-
-bool WAVWriter::Open(const char* filename, u32 sample_rate, u32 num_channels)
-{
- if (IsOpen())
- Close();
-
- m_file = FileSystem::OpenCFile(filename, "wb");
- if (!m_file)
- return false;
-
- m_sample_rate = sample_rate;
- m_num_channels = num_channels;
-
- if (!WriteHeader())
- {
- ERROR_LOG("Failed to write header to file");
- m_sample_rate = 0;
- m_num_channels = 0;
- std::fclose(m_file);
- m_file = nullptr;
- return false;
- }
-
- return true;
-}
-
-void WAVWriter::Close()
-{
- if (!IsOpen())
- return;
-
- if (std::fseek(m_file, 0, SEEK_SET) != 0 || !WriteHeader())
- ERROR_LOG("Failed to re-write header on file, file may be unplayable");
-
- std::fclose(m_file);
- m_file = nullptr;
- m_sample_rate = 0;
- m_num_channels = 0;
- m_num_frames = 0;
-}
-
-void WAVWriter::WriteFrames(const s16* samples, u32 num_frames)
-{
- const u32 num_frames_written =
- static_cast(std::fwrite(samples, sizeof(s16) * m_num_channels, num_frames, m_file));
- if (num_frames_written != num_frames)
- ERROR_LOG("Only wrote {} of {} frames to output file", num_frames_written, num_frames);
-
- m_num_frames += num_frames_written;
-}
-
-bool WAVWriter::WriteHeader()
-{
- const u32 data_size = sizeof(SampleType) * m_num_channels * m_num_frames;
-
- WAV_HEADER header = {};
- header.chunk_id = 0x46464952; // 0x52494646
- header.chunk_size = sizeof(WAV_HEADER) - 8 + data_size;
- header.format = 0x45564157; // 0x57415645
- header.fmt_chunk.chunk_id = 0x20746d66; // 0x666d7420
- header.fmt_chunk.chunk_size = sizeof(header.fmt_chunk) - 8;
- header.fmt_chunk.audio_format = 1;
- header.fmt_chunk.num_channels = static_cast(m_num_channels);
- header.fmt_chunk.sample_rate = m_sample_rate;
- header.fmt_chunk.byte_rate = m_sample_rate * m_num_channels * sizeof(SampleType);
- header.fmt_chunk.block_align = static_cast(m_num_channels * sizeof(SampleType));
- header.fmt_chunk.bits_per_sample = 16;
- header.data_chunk_header.chunk_id = 0x61746164; // 0x64617461
- header.data_chunk_header.chunk_size = data_size;
-
- return (std::fwrite(&header, sizeof(header), 1, m_file) == 1);
-}
diff --git a/src/util/wav_writer.h b/src/util/wav_writer.h
deleted file mode 100644
index 1202bf8cf..000000000
--- a/src/util/wav_writer.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin
-// SPDX-License-Identifier: CC-BY-NC-ND-4.0
-
-#pragma once
-#include "common/types.h"
-#include
-
-class WAVWriter
-{
-public:
- WAVWriter();
- ~WAVWriter();
-
- 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 bool IsOpen() const { return (m_file != nullptr); }
-
- bool Open(const char* filename, u32 sample_rate, u32 num_channels);
- void Close();
-
- void WriteFrames(const s16* samples, u32 num_frames);
-
-private:
- using SampleType = s16;
-
- bool WriteHeader();
-
- std::FILE* m_file = nullptr;
- u32 m_sample_rate = 0;
- u32 m_num_channels = 0;
- u32 m_num_frames = 0;
-};