using System;
using System.IO;
using System.Text;
/// A WAV file decoded to mono floats, plus a per-pixel min/max cache for drawing.
public class WaveData
{
public float[] Samples;
public int SampleRate;
private int _cacheWidth = -1;
private float[] _mins, _maxs;
public double Duration
{
get { return (Samples == null || SampleRate <= 0) ? 0 : (double)Samples.Length / SampleRate; }
}
public static WaveData Load(string path, out string error)
{
float[] s; int rate;
if (!ReadWav(path, out s, out rate, out error)) return null;
return new WaveData { Samples = s, SampleRate = rate };
}
/// Min/max of the samples that fall into each of `width` pixel columns.
public void GetPeaks(int width, out float[] mins, out float[] maxs)
{
if (width != _cacheWidth)
{
_cacheWidth = width;
_mins = new float[width];
_maxs = new float[width];
int n = Samples.Length;
for (int x = 0; x < width; x++)
{
int s0 = (int)((long)x * n / width);
int s1 = (int)((long)(x + 1) * n / width);
if (s1 <= s0) s1 = s0 + 1;
if (s1 > n) s1 = n;
float mn = 1f, mx = -1f;
for (int i = s0; i < s1; i++)
{
float v = Samples[i];
if (v < mn) mn = v;
if (v > mx) mx = v;
}
_mins[x] = mn;
_maxs[x] = mx;
}
}
mins = _mins;
maxs = _maxs;
}
// PCM 8/16/24/32-bit or float32, mono or stereo (left channel), normalized to [-1, 1]
public static bool ReadWav(string path, out float[] samples, out int sampleRate, out string error)
{
samples = null; sampleRate = 0; error = null;
try
{
if (!File.Exists(path)) { error = "not found: " + path; return false; }
using (var br = new BinaryReader(File.OpenRead(path)))
{
long len = br.BaseStream.Length;
if (Id(br) != "RIFF") { error = "not RIFF"; return false; }
br.ReadInt32();
if (Id(br) != "WAVE") { error = "not WAVE"; return false; }
int fmtTag = 0, channels = 0, bits = 0;
byte[] data = null;
while (br.BaseStream.Position + 8 <= len)
{
string id = Id(br);
long size = (uint)br.ReadInt32();
long start = br.BaseStream.Position;
if (id == "fmt ")
{
fmtTag = (ushort)br.ReadInt16();
channels = br.ReadInt16();
sampleRate = br.ReadInt32();
br.ReadInt32(); // byte rate
br.ReadInt16(); // block align
bits = br.ReadInt16();
if (fmtTag == 0xFFFE && size >= 40) // WAVE_FORMAT_EXTENSIBLE
{
br.ReadInt16(); br.ReadInt16(); br.ReadInt32();
fmtTag = (ushort)br.ReadInt16();
}
}
else if (id == "data")
{
data = br.ReadBytes((int)Math.Min(size, len - start));
break;
}
long next = start + size + (size & 1);
if (next > len) break;
br.BaseStream.Position = next;
}
if (data == null || channels <= 0 || sampleRate <= 0 || bits <= 0)
{ error = "missing fmt/data chunk"; return false; }
int frame = (bits / 8) * channels;
int frames = data.Length / frame;
if (frames == 0) { error = "empty data"; return false; }
var s = new float[frames];
float peak = 0f;
for (int i = 0; i < frames; i++)
{
int o = i * frame;
float v;
switch (bits)
{
case 8: v = (data[o] - 128) / 128f; break;
case 16: v = BitConverter.ToInt16(data, o) / 32768f; break;
case 24: v = (data[o] | (data[o + 1] << 8) | ((sbyte)data[o + 2] << 16)) / 8388608f; break;
case 32: v = fmtTag == 3 ? BitConverter.ToSingle(data, o)
: BitConverter.ToInt32(data, o) / 2147483648f; break;
default: error = "unsupported bits: " + bits; return false;
}
s[i] = v;
if (Math.Abs(v) > peak) peak = Math.Abs(v);
}
if (peak > 0f)
for (int i = 0; i < frames; i++) s[i] /= peak;
samples = s;
return true;
}
}
catch (Exception ex) { error = ex.Message; return false; }
}
private static string Id(BinaryReader br)
{
return Encoding.ASCII.GetString(br.ReadBytes(4));
}
}