@@ -9,6 +9,14 @@ namespace Spectrogram
9
9
{
10
10
public static class WavFile
11
11
{
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
+
12
20
public static ( int sampleRate , double [ ] L ) ReadMono ( string filePath )
13
21
{
14
22
( int sampleRate , double [ ] L , _ ) = ReadStereo ( filePath ) ;
@@ -20,84 +28,95 @@ public static (int sampleRate, double[] L, double[] R) ReadStereo(string filePat
20
28
using ( FileStream fs = new FileStream ( filePath , FileMode . Open , FileAccess . Read ) )
21
29
using ( BinaryReader br = new BinaryReader ( fs ) )
22
30
{
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.
40
47
int audioFormat = br . ReadUInt16 ( ) ;
41
- Debug . WriteLine ( $ "audio format: { audioFormat } ") ;
48
+ Console . WriteLine ( $ "audio format: { audioFormat } ") ;
42
49
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)" ) ;
44
51
45
52
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 } )") ;
47
56
48
57
int sampleRate = ( int ) br . ReadUInt32 ( ) ;
49
- Debug . WriteLine ( $ "sample rate: { sampleRate } Hz") ;
58
+ Console . WriteLine ( $ "sample rate: { sampleRate } Hz") ;
50
59
51
60
int byteRate = ( int ) br . ReadUInt32 ( ) ;
52
- Debug . WriteLine ( $ "byteRate: { byteRate } ") ;
61
+ Console . WriteLine ( $ "byteRate: { byteRate } ") ;
53
62
54
63
ushort blockSize = br . ReadUInt16 ( ) ;
55
- Debug . WriteLine ( $ "block size: { blockSize } bytes per sample") ;
64
+ Console . WriteLine ( $ "block size: { blockSize } bytes per sample") ;
56
65
57
66
ushort bitsPerSample = br . ReadUInt16 ( ) ;
58
- Debug . WriteLine ( $ "resolution: { bitsPerSample } -bit") ;
67
+ Console . WriteLine ( $ "resolution: { bitsPerSample } -bit") ;
59
68
if ( bitsPerSample != 16 )
60
69
throw new NotImplementedException ( "Only 16-bit WAV files are supported" ) ;
61
70
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") ;
68
92
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 ;
72
95
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 ;
76
99
77
100
if ( channelCount == 1 )
78
101
{
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 ++ )
81
104
{
82
105
L [ i ] = br . ReadInt16 ( ) ;
83
106
}
84
- return ( sampleRate , L , null ) ;
85
107
}
86
108
else if ( channelCount == 2 )
87
109
{
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 ++ )
91
113
{
92
114
L [ i ] = br . ReadInt16 ( ) ;
93
115
R [ i ] = br . ReadInt16 ( ) ;
94
116
}
95
- return ( sampleRate , L , R ) ;
96
- }
97
- else
98
- {
99
- throw new InvalidOperationException ( "channel must be 1 or 2" ) ;
100
117
}
118
+
119
+ return ( sampleRate , L , R ) ;
101
120
}
102
121
}
103
122
}
0 commit comments