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: