diff --git a/Runtime/Scripts/Internal/AudioClipDump.cs b/Runtime/Scripts/Internal/AudioClipDump.cs
new file mode 100644
index 00000000..626bbce8
--- /dev/null
+++ b/Runtime/Scripts/Internal/AudioClipDump.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections;
+using System.IO;
+using UnityEngine;
+
+namespace LiveKit.Internal
+{
+ ///
+ /// Debugging utility for dumping audio buffers to WAV files so they can be inspected offline
+ /// (in an audio editor, or analyzed programmatically).
+ ///
+ ///
+ /// This proved decisive when diagnosing microphone capture issues: a raw dump of the mic clip
+ /// on macOS with a Bluetooth HFP headset revealed that FMOD writes valid 320-sample audio
+ /// fragments at a 1024-sample stride with exact-zero padding between them, while
+ /// Microphone.GetPosition advances ~3.2x faster than the data rate. Inspecting the actual
+ /// buffer contents settles questions that API values (clip.frequency, GetPosition) cannot.
+ ///
+ internal static class AudioClipDump
+ {
+ ///
+ /// Snapshots the full contents of a clip to a 16-bit PCM WAV file and returns the path.
+ /// For looping microphone clips, call a few seconds after capture started so the ring
+ /// buffer contains audio, and produce sound continuously while it fills.
+ ///
+ public static string DumpClip(AudioClip clip, string fileName = "lk_clip_dump.wav")
+ {
+ var data = new float[clip.samples * clip.channels];
+ clip.GetData(data, 0);
+ var path = Path.Combine(Application.temporaryCachePath, fileName);
+ WriteWav(path, data, clip.channels, clip.frequency);
+ Utils.Info($"AudioClipDump: wrote {path} ({clip.samples} frames @ {clip.frequency}Hz/{clip.channels}ch)");
+ return path;
+ }
+
+ ///
+ /// Coroutine that waits, then dumps the clip. Convenient to start alongside capture:
+ /// MonoBehaviourContext.RunCoroutine(AudioClipDump.DumpClipAfter(clip, 4f));
+ ///
+ public static IEnumerator DumpClipAfter(AudioClip clip, float delaySeconds, string fileName = "lk_clip_dump.wav")
+ {
+ yield return new WaitForSeconds(delaySeconds);
+ if (clip == null) yield break;
+ try
+ {
+ DumpClip(clip, fileName);
+ }
+ catch (Exception e)
+ {
+ Utils.Warning($"AudioClipDump: dump failed: {e.Message}");
+ }
+ }
+
+ ///
+ /// Writes interleaved float samples as a 16-bit PCM WAV file.
+ ///
+ public static void WriteWav(string path, float[] samples, int channels, int sampleRate)
+ {
+ using var fs = new FileStream(path, FileMode.Create);
+ using var w = new BinaryWriter(fs);
+ int dataBytes = samples.Length * 2;
+ w.Write(System.Text.Encoding.ASCII.GetBytes("RIFF"));
+ w.Write(36 + dataBytes);
+ w.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt "));
+ w.Write(16);
+ w.Write((short)1); // PCM
+ w.Write((short)channels);
+ w.Write(sampleRate);
+ w.Write(sampleRate * channels * 2);
+ w.Write((short)(channels * 2)); // block align
+ w.Write((short)16); // bits per sample
+ w.Write(System.Text.Encoding.ASCII.GetBytes("data"));
+ w.Write(dataBytes);
+ foreach (var s in samples)
+ w.Write((short)(Mathf.Clamp(s, -1f, 1f) * 32767f));
+ }
+ }
+}
diff --git a/Runtime/Scripts/Internal/AudioClipDump.cs.meta b/Runtime/Scripts/Internal/AudioClipDump.cs.meta
new file mode 100644
index 00000000..26a9ff0c
--- /dev/null
+++ b/Runtime/Scripts/Internal/AudioClipDump.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 366ae3d162f2460fa2de6a859cafc508
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: