diff --git a/src/debug_utils.h b/src/debug_utils.h
index b4d3d183e9a8aba6ea35200ecd8fb8d6f5bfcd5a..eeb633c524922565329a11b324f45bcf381ed0bc 100644
--- a/src/debug_utils.h
+++ b/src/debug_utils.h
@@ -22,7 +22,11 @@
 
 #include "config.h"
 
+#include "libav_deps.h"
+
 #include <chrono>
+#include <fstream>
+#include <ios>
 #include <ratio>
 
 #warning Debug utilities included in build
@@ -53,4 +57,94 @@ private:
     std::chrono::time_point<Clock> start_;
 };
 
+/**
+ * Minimally invasive audio logger. Writes a wav file from raw PCM or AVFrame. Helps debug what goes wrong with audio.
+ */
+class WavWriter
+{
+public:
+    WavWriter(std::string filename, int channels, int sampleRate, int bytesPerSample)
+    {
+        f_ = std::ofstream(filename, std::ios::binary);
+        f_ << "RIFF----WAVEfmt ";
+        write(16, 4); // no extension data
+        write(1, 2); // PCM integer samples
+        write(channels, 2); // channels
+        write(sampleRate, 4); // sample rate
+        write(sampleRate * channels * bytesPerSample, 4); // sample size
+        write(4, 2); // data block size
+        write(bytesPerSample * 8, 2); // bits per sample
+        dataChunk_ = f_.tellp();
+        f_ << "data----";
+    }
+
+    ~WavWriter()
+    {
+        length_ = f_.tellp();
+        f_.seekp(dataChunk_ + 4);
+        write(length_ - dataChunk_ + 8, 4);
+        f_.seekp(4);
+        write(length_ - 8, 4);
+    }
+
+    template<typename Word>
+    void write(Word value, unsigned size)
+    {
+        for (; size; --size, value >>= 8)
+            f_.put(static_cast<char>(value & 0xFF));
+    }
+
+    void write(AVFrame* frame)
+    {
+        AVSampleFormat fmt = (AVSampleFormat)frame->format;
+        int channels = frame->channels;
+        int depth = av_get_bytes_per_sample(fmt);
+        int linesize = frame->linesize[0];
+        int planar = av_sample_fmt_is_planar(fmt);
+        int step = (planar ? depth : depth * channels);
+        for (int i = 0; i < linesize; i += step) {
+            for (int ch = 0; ch < channels; ++ch) {
+                int c = (planar ? ch : 0);
+                int offset = (planar ? i : i + depth * ch);
+                writeSample(&frame->extended_data[c][offset], fmt, depth);
+            }
+        }
+    }
+
+private:
+    void writeSample(uint8_t* p, AVSampleFormat format, int size)
+    {
+        switch (format) {
+        case AV_SAMPLE_FMT_U8:
+        case AV_SAMPLE_FMT_U8P:
+            write(*(uint8_t*)p, size);
+            break;
+        case AV_SAMPLE_FMT_S16:
+        case AV_SAMPLE_FMT_S16P:
+            write(*(int16_t*)p, size);
+            break;
+        case AV_SAMPLE_FMT_S32:
+        case AV_SAMPLE_FMT_S32P:
+        case AV_SAMPLE_FMT_FLT:
+        case AV_SAMPLE_FMT_FLTP:
+            // float samples are always 32 bits in FFmpeg
+            write(*(int32_t*)p, size);
+            break;
+        case AV_SAMPLE_FMT_S64:
+        case AV_SAMPLE_FMT_S64P:
+        case AV_SAMPLE_FMT_DBL:
+        case AV_SAMPLE_FMT_DBLP:
+            // dbl samples are always 64 bits in FFmpeg
+            write(*(int64_t*)p, size);
+            break;
+        default:
+            break;
+        }
+    }
+
+    std::ofstream f_;
+    size_t dataChunk_;
+    size_t length_;
+};
+
 }} // namespace ring::debug