diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index a25a7d8b1..b9605cf39 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -54,6 +54,8 @@ add_library(common
timestamp.cpp
timestamp.h
types.h
+ wav_writer.cpp
+ wav_Writer.h
)
target_include_directories(common PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj
index 9e5559329..21941c4b6 100644
--- a/src/common/common.vcxproj
+++ b/src/common/common.vcxproj
@@ -70,6 +70,7 @@
+
@@ -102,6 +103,7 @@
+
diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters
index 450591a77..631173c7a 100644
--- a/src/common/common.vcxproj.filters
+++ b/src/common/common.vcxproj.filters
@@ -52,6 +52,7 @@
+
@@ -100,6 +101,7 @@
+
diff --git a/src/common/wav_writer.cpp b/src/common/wav_writer.cpp
new file mode 100644
index 000000000..5f51e9c30
--- /dev/null
+++ b/src/common/wav_writer.cpp
@@ -0,0 +1,115 @@
+#include "wav_writer.h"
+#include "file_system.h"
+#include "log.h"
+Log_SetChannel(WAVWriter);
+
+#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 Common {
+
+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())
+ {
+ Log_ErrorPrintf("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())
+ Log_ErrorPrintf("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)
+ Log_ErrorPrintf("Only wrote %u of %u frames to output file", num_frames_written);
+
+ 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);
+}
+
+} // namespace Common
\ No newline at end of file
diff --git a/src/common/wav_writer.h b/src/common/wav_writer.h
new file mode 100644
index 000000000..7c2647843
--- /dev/null
+++ b/src/common/wav_writer.h
@@ -0,0 +1,31 @@
+#pragma once
+#include "types.h"
+#include
+
+namespace Common {
+
+class WAVWriter
+{
+public:
+ WAVWriter();
+ ~WAVWriter();
+
+ 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;
+};
+
+} // namespace Common
\ No newline at end of file