Skip to content

Commit ef3f8bc

Browse files
committed
refactor wav reader #12
1 parent aaec00b commit ef3f8bc

File tree

1 file changed

+66
-47
lines changed

1 file changed

+66
-47
lines changed

src/Spectrogram/WavFile.cs

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ namespace Spectrogram
99
{
1010
public static class WavFile
1111
{
12+
private static (string id, uint length) ChunkInfo(BinaryReader br, long position)
13+
{
14+
br.BaseStream.Seek(position, SeekOrigin.Begin);
15+
string chunkID = new string(br.ReadChars(4));
16+
uint chunkBytes = br.ReadUInt32();
17+
return (chunkID, chunkBytes);
18+
}
19+
1220
public static (int sampleRate, double[] L) ReadMono(string filePath)
1321
{
1422
(int sampleRate, double[] L, _) = ReadStereo(filePath);
@@ -20,84 +28,95 @@ public static (int sampleRate, double[] L, double[] R) ReadStereo(string filePat
2028
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
2129
using (BinaryReader br = new BinaryReader(fs))
2230
{
23-
// read and verify content of the header
24-
if (new string(br.ReadChars(4)) != "RIFF")
25-
throw new ArgumentException("invalid WAV header (no RIFF)");
26-
27-
uint chunkSize = br.ReadUInt32();
28-
if (new string(br.ReadChars(4)) != "WAVE")
29-
throw new ArgumentException("invalid WAV file (expected 'WAVE' at byte 8)");
30-
31-
string formatString = new string(br.ReadChars(4));
32-
if (formatString != "fmt ")
33-
throw new NotImplementedException("unsupported WAV header (expected 'fmt ' at byte 12)");
34-
35-
int chunkOneSize = (int)br.ReadUInt32();
36-
int firstByteAfterChunk1 = (int)br.BaseStream.Position + chunkOneSize;
37-
if (chunkOneSize != 16)
38-
throw new NotImplementedException("unsupported WAV header (chunk 1 must be 16 bytes in length)");
39-
31+
// The first chunk is RIFF section
32+
// Length should be the number of bytes in the file minus 4
33+
var riffChunk = ChunkInfo(br, 0);
34+
Console.WriteLine($"First chunk '{riffChunk.id}' indicates {riffChunk.length:N0} bytes");
35+
if (riffChunk.id != "RIFF")
36+
throw new InvalidOperationException($"Unsupported WAV format (first chunk ID was '{riffChunk.id}', not 'RIFF')");
37+
38+
// The second chunk is FORMAT section
39+
var fmtChunk = ChunkInfo(br, 12);
40+
Console.WriteLine($"Format chunk '{fmtChunk.id}' indicates {fmtChunk.length:N0} bytes");
41+
if (fmtChunk.id != "fmt ")
42+
throw new InvalidOperationException($"Unsupported WAV format (first chunk ID was '{fmtChunk.id}', not 'fmt ')");
43+
if (fmtChunk.length != 16)
44+
throw new InvalidOperationException($"Unsupported WAV format (expect 16 byte 'fmt' chunk, got {fmtChunk.length} bytes)");
45+
46+
// By now we verified this is probably a valid FORMAT section, so read its values.
4047
int audioFormat = br.ReadUInt16();
41-
Debug.WriteLine($"audio format: {audioFormat}");
48+
Console.WriteLine($"audio format: {audioFormat}");
4249
if (audioFormat != 1)
43-
throw new NotImplementedException("unsupported WAV header (audio format must be 1, indicating uncompressed PCM data)");
50+
throw new NotImplementedException("Unsupported WAV format (audio format must be 1, indicating uncompressed PCM data)");
4451

4552
int channelCount = br.ReadUInt16();
46-
Debug.WriteLine($"channel count: {channelCount}");
53+
Console.WriteLine($"channel count: {channelCount}");
54+
if (channelCount < 0 || channelCount > 2)
55+
throw new NotImplementedException($"Unsupported WAV format (must be 1 or 2 channel, file has {channelCount})");
4756

4857
int sampleRate = (int)br.ReadUInt32();
49-
Debug.WriteLine($"sample rate: {sampleRate} Hz");
58+
Console.WriteLine($"sample rate: {sampleRate} Hz");
5059

5160
int byteRate = (int)br.ReadUInt32();
52-
Debug.WriteLine($"byteRate: {byteRate}");
61+
Console.WriteLine($"byteRate: {byteRate}");
5362

5463
ushort blockSize = br.ReadUInt16();
55-
Debug.WriteLine($"block size: {blockSize} bytes per sample");
64+
Console.WriteLine($"block size: {blockSize} bytes per sample");
5665

5766
ushort bitsPerSample = br.ReadUInt16();
58-
Debug.WriteLine($"resolution: {bitsPerSample}-bit");
67+
Console.WriteLine($"resolution: {bitsPerSample}-bit");
5968
if (bitsPerSample != 16)
6069
throw new NotImplementedException("Only 16-bit WAV files are supported");
6170

62-
string dataChars = new string(br.ReadChars(4));
63-
Debug.WriteLine($"Data characters: {dataChars}");
64-
65-
// this may be the number of data bytes, but don't rely on it to be.
66-
int finalNumber = (int)br.ReadUInt32();
67-
Debug.WriteLine($"Final UInt32: {finalNumber}");
71+
// Cycle custom chunks until we get to the DATA chunk
72+
// Various chunks may exist until the data chunk appears
73+
long nextChunkPosition = 36;
74+
int maximumChunkNumber = 42;
75+
long firstDataByte = 0;
76+
long dataByteCount = 0;
77+
for (int i = 0; i < maximumChunkNumber; i++)
78+
{
79+
var chunk = ChunkInfo(br, nextChunkPosition);
80+
Console.WriteLine($"Chunk at {nextChunkPosition} ('{chunk.id}') indicates {chunk.length:N0} bytes");
81+
if (chunk.id == "data")
82+
{
83+
firstDataByte = nextChunkPosition + 8;
84+
dataByteCount = chunk.length;
85+
break;
86+
}
87+
nextChunkPosition += chunk.length + 8;
88+
}
89+
if (firstDataByte == 0 || dataByteCount == 0)
90+
throw new InvalidOperationException("Unsupported WAV format (no 'data' chunk found)");
91+
Console.WriteLine($"PCM data starts at {firstDataByte} and contains {dataByteCount} bytes");
6892

69-
int bytesRemaining = (int)(fs.Length - br.BaseStream.Position);
70-
Debug.WriteLine($"Bytes remaining: {bytesRemaining}");
71-
int sampleCount = bytesRemaining / blockSize;
93+
// Now read PCM data values into an array and return it
94+
long sampleCount = dataByteCount / blockSize;
7295
Debug.WriteLine($"Samples in file: {sampleCount}");
73-
int timePoints = sampleCount / channelCount;
74-
Debug.WriteLine($"Time points in file: {timePoints}");
75-
Debug.WriteLine($"First data byte: {br.BaseStream.Position}");
96+
97+
double[] L = null;
98+
double[] R = null;
7699

77100
if (channelCount == 1)
78101
{
79-
double[] L = new double[timePoints];
80-
for (int i = 0; i < timePoints; i++)
102+
L = new double[sampleCount];
103+
for (int i = 0; i < sampleCount; i++)
81104
{
82105
L[i] = br.ReadInt16();
83106
}
84-
return (sampleRate, L, null);
85107
}
86108
else if (channelCount == 2)
87109
{
88-
double[] L = new double[timePoints];
89-
double[] R = new double[timePoints];
90-
for (int i = 0; i < timePoints; i++)
110+
L = new double[sampleCount];
111+
R = new double[sampleCount];
112+
for (int i = 0; i < sampleCount; i++)
91113
{
92114
L[i] = br.ReadInt16();
93115
R[i] = br.ReadInt16();
94116
}
95-
return (sampleRate, L, R);
96-
}
97-
else
98-
{
99-
throw new InvalidOperationException("channel must be 1 or 2");
100117
}
118+
119+
return (sampleRate, L, R);
101120
}
102121
}
103122
}

0 commit comments

Comments
 (0)