wavefile 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/LICENSE +1 -1
  2. data/README.markdown +29 -56
  3. data/Rakefile +6 -0
  4. data/lib/wavefile.rb +28 -452
  5. data/lib/wavefile/buffer.rb +147 -0
  6. data/lib/wavefile/format.rb +69 -0
  7. data/lib/wavefile/info.rb +53 -0
  8. data/lib/wavefile/reader.rb +296 -0
  9. data/lib/wavefile/writer.rb +128 -0
  10. data/test/buffer_test.rb +121 -0
  11. data/test/fixtures/actual_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
  12. data/test/fixtures/expected_output/no_samples.wav +0 -0
  13. data/test/fixtures/expected_output/valid_mono_16_44100.wav +0 -0
  14. data/test/fixtures/expected_output/valid_mono_32_44100.wav +0 -0
  15. data/test/fixtures/expected_output/valid_mono_8_44100.wav +0 -0
  16. data/test/fixtures/expected_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
  17. data/test/fixtures/expected_output/valid_stereo_16_44100.wav +0 -0
  18. data/test/fixtures/expected_output/valid_stereo_32_44100.wav +0 -0
  19. data/test/fixtures/expected_output/valid_stereo_8_44100.wav +0 -0
  20. data/test/fixtures/expected_output/valid_tri_16_44100.wav +0 -0
  21. data/test/fixtures/expected_output/valid_tri_32_44100.wav +0 -0
  22. data/test/fixtures/expected_output/valid_tri_8_44100.wav +0 -0
  23. data/test/fixtures/invalid/README.markdown +10 -0
  24. data/test/fixtures/invalid/bad_riff_header.wav +1 -0
  25. data/test/fixtures/invalid/bad_wavefile_format.wav +0 -0
  26. data/test/fixtures/invalid/empty.wav +0 -0
  27. data/test/fixtures/invalid/empty_format_chunk.wav +0 -0
  28. data/test/fixtures/invalid/incomplete_riff_header.wav +1 -0
  29. data/test/fixtures/invalid/insufficient_format_chunk.wav +0 -0
  30. data/test/fixtures/invalid/no_data_chunk.wav +0 -0
  31. data/test/fixtures/invalid/no_format_chunk.wav +0 -0
  32. data/test/fixtures/unsupported/README.markdown +6 -0
  33. data/test/fixtures/unsupported/bad_audio_format.wav +0 -0
  34. data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
  35. data/test/fixtures/unsupported/bad_sample_rate.wav +0 -0
  36. data/test/fixtures/unsupported/unsupported_audio_format.wav +0 -0
  37. data/test/fixtures/unsupported/unsupported_bits_per_sample.wav +0 -0
  38. data/test/fixtures/valid/README.markdown +3 -0
  39. data/test/fixtures/valid/valid_mono_16_44100.wav +0 -0
  40. data/test/fixtures/valid/valid_mono_32_44100.wav +0 -0
  41. data/test/fixtures/valid/valid_mono_8_44100.wav +0 -0
  42. data/test/fixtures/valid/valid_mono_8_44100_with_padding_byte.wav +0 -0
  43. data/test/fixtures/valid/valid_stereo_16_44100.wav +0 -0
  44. data/test/fixtures/valid/valid_stereo_32_44100.wav +0 -0
  45. data/test/fixtures/valid/valid_stereo_8_44100.wav +0 -0
  46. data/test/fixtures/valid/valid_tri_16_44100.wav +0 -0
  47. data/test/fixtures/valid/valid_tri_32_44100.wav +0 -0
  48. data/test/fixtures/valid/valid_tri_8_44100.wav +0 -0
  49. data/test/format_test.rb +105 -0
  50. data/test/info_test.rb +60 -0
  51. data/test/reader_test.rb +222 -0
  52. data/test/wavefile_io_test_helper.rb +47 -0
  53. data/test/writer_test.rb +118 -0
  54. metadata +72 -33
  55. data/test/wavefile_test.rb +0 -339
@@ -0,0 +1,147 @@
1
+ module WaveFile
2
+ # Error that is raised when an attempt is made to perform an unsupported or undefined
3
+ # conversion between two sample data formats.
4
+ class BufferConversionError < StandardError; end
5
+
6
+
7
+ # Represents a collection of samples in a certain format (e.g. 16-bit mono).
8
+ # Reader returns sample data contained in Buffers, and Writer expects incoming sample
9
+ # data to be contained in a Buffer as well.
10
+ #
11
+ # Contains methods to convert the sample data in the buffer to a different format.
12
+ class Buffer
13
+
14
+ # Creates a new Buffer. You are on the honor system to make sure that the given
15
+ # sample data matches the given format.
16
+ def initialize(samples, format)
17
+ @samples = samples
18
+ @format = format
19
+ end
20
+
21
+
22
+ # Creates a new Buffer containing the sample data of this Buffer, but converted to
23
+ # a different format.
24
+ #
25
+ # new_format - The format that the sample data should be converted to
26
+ #
27
+ # Examples
28
+ #
29
+ # new_format = Format.new(:mono, 16, 44100)
30
+ # new_buffer = old_buffer.convert(new_format)
31
+ #
32
+ # Returns a new Buffer; the existing Buffer is unmodified.
33
+ def convert(new_format)
34
+ new_samples = convert_buffer(@samples.dup, @format, new_format)
35
+ return Buffer.new(new_samples, new_format)
36
+ end
37
+
38
+
39
+ # Converts the sample data contained in the Buffer to a new format. The sample data
40
+ # is converted in place, so the existing Buffer is modified.
41
+ #
42
+ # new_format - The format that the sample data should be converted to
43
+ #
44
+ # Examples
45
+ #
46
+ # new_format = Format.new(:mono, 16, 44100)
47
+ # old_buffer.convert!(new_format)
48
+ #
49
+ # Returns self.
50
+ def convert!(new_format)
51
+ @samples = convert_buffer(@samples, @format, new_format)
52
+ @format = new_format
53
+ return self
54
+ end
55
+
56
+
57
+ # The number of channels the buffer's sample data has
58
+ def channels
59
+ return @format.channels
60
+ end
61
+
62
+
63
+ # The bits per sample of the buffer's sample data
64
+ def bits_per_sample
65
+ return @format.bits_per_sample
66
+ end
67
+
68
+
69
+ # The sample rate of the buffer's sample data
70
+ def sample_rate
71
+ return @format.sample_rate
72
+ end
73
+
74
+ attr_reader :samples
75
+
76
+ private
77
+
78
+ def convert_buffer(samples, old_format, new_format)
79
+ unless old_format.channels == new_format.channels
80
+ samples = convert_buffer_channels(samples, old_format.channels, new_format.channels)
81
+ end
82
+
83
+ unless old_format.bits_per_sample == new_format.bits_per_sample
84
+ samples = convert_buffer_bits_per_sample(samples, old_format.bits_per_sample, new_format.bits_per_sample)
85
+ end
86
+
87
+ return samples
88
+ end
89
+
90
+ def convert_buffer_channels(samples, old_channels, new_channels)
91
+ # The cases of mono -> stereo and vice-versa are handled specially,
92
+ # because those conversion methods are faster than the general methods,
93
+ # and the large majority of wave files are expected to be either mono or stereo.
94
+ if old_channels == 1 && new_channels == 2
95
+ samples.map! {|sample| [sample, sample]}
96
+ elsif old_channels == 2 && new_channels == 1
97
+ samples.map! {|sample| (sample[0] + sample[1]) / 2}
98
+ elsif old_channels == 1 && new_channels >= 2
99
+ samples.map! {|sample| [].fill(sample, 0, new_channels)}
100
+ elsif old_channels >= 2 && new_channels == 1
101
+ samples.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / old_channels }
102
+ elsif old_channels > 2 && new_channels == 2
103
+ samples.map! {|sample| [sample[0], sample[1]]}
104
+ else
105
+ raise BufferConversionError,
106
+ "Conversion of sample data from #{old_channels} channels to #{new_channels} channels is unsupported"
107
+ end
108
+
109
+ return samples
110
+ end
111
+
112
+ def convert_buffer_bits_per_sample(samples, old_bits_per_sample, new_bits_per_sample)
113
+ shift_amount = (new_bits_per_sample - old_bits_per_sample).abs
114
+ more_than_one_channel = (Array === samples.first)
115
+
116
+ if old_bits_per_sample == 8
117
+ if more_than_one_channel
118
+ samples.map! do |sample|
119
+ sample.map! {|sub_sample| (sub_sample - 128) << shift_amount }
120
+ end
121
+ else
122
+ samples.map! {|sample| (sample - 128) << shift_amount }
123
+ end
124
+ elsif new_bits_per_sample == 8
125
+ if more_than_one_channel
126
+ samples.map! do |sample|
127
+ sample.map! {|sub_sample| (sub_sample >> shift_amount) + 128 }
128
+ end
129
+ else
130
+ samples.map! {|sample| (sample >> shift_amount) + 128 }
131
+ end
132
+ else
133
+ operator = (new_bits_per_sample > old_bits_per_sample) ? :<< : :>>
134
+
135
+ if more_than_one_channel
136
+ samples.map! do |sample|
137
+ sample.map! {|sub_sample| sub_sample.send(operator, shift_amount) }
138
+ end
139
+ else
140
+ samples.map! {|sample| sample.send(operator, shift_amount) }
141
+ end
142
+ end
143
+
144
+ return samples
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,69 @@
1
+ module WaveFile
2
+ class InvalidFormatError < StandardError; end
3
+
4
+ class Format
5
+ # Not using ranges because of 1.8.7 performance problems with Range.max()
6
+ MIN_CHANNELS = 1
7
+ MAX_CHANNELS = 65535
8
+
9
+ MIN_SAMPLE_RATE = 1
10
+ MAX_SAMPLE_RATE = 4_294_967_296
11
+
12
+ SUPPORTED_BITS_PER_SAMPLE = [8, 16, 32]
13
+
14
+ def initialize(channels, bits_per_sample, sample_rate)
15
+ channels = canonicalize_channels(channels)
16
+ validate_channels(channels)
17
+ validate_bits_per_sample(bits_per_sample)
18
+ validate_sample_rate(sample_rate)
19
+
20
+ @channels = channels
21
+ @bits_per_sample = bits_per_sample
22
+ @sample_rate = sample_rate
23
+ @block_align = (@bits_per_sample / 8) * @channels
24
+ @byte_rate = @block_align * @sample_rate
25
+ end
26
+
27
+ def mono?()
28
+ return @channels == 1
29
+ end
30
+
31
+ def stereo?()
32
+ return @channels == 2
33
+ end
34
+
35
+ attr_reader :channels, :bits_per_sample, :sample_rate, :block_align, :byte_rate
36
+
37
+ private
38
+
39
+ def canonicalize_channels(channels)
40
+ if channels == :mono
41
+ return 1
42
+ elsif channels == :stereo
43
+ return 2
44
+ else
45
+ return channels
46
+ end
47
+ end
48
+
49
+ def validate_channels(candidate_channels)
50
+ unless (MIN_CHANNELS..MAX_CHANNELS) === candidate_channels
51
+ raise InvalidFormatError, "Invalid number of channels. Must be between 1 and #{MAX_CHANNELS}."
52
+ end
53
+ end
54
+
55
+ def validate_bits_per_sample(candidate_bits_per_sample)
56
+ unless SUPPORTED_BITS_PER_SAMPLE.member?(candidate_bits_per_sample)
57
+ raise InvalidFormatError,
58
+ "Bits per sample of #{candidate_bits_per_sample} is unsupported. " +
59
+ "Only #{SUPPORTED_BITS_PER_SAMPLE.inspect} are supported."
60
+ end
61
+ end
62
+
63
+ def validate_sample_rate(candidate_sample_rate)
64
+ unless (MIN_SAMPLE_RATE..MAX_SAMPLE_RATE) === candidate_sample_rate
65
+ raise InvalidFormatError, "Invalid sample rate. Must be between 1 and #{MAX_SAMPLE_RATE}"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,53 @@
1
+ module WaveFile
2
+ class Info
3
+ def initialize(file_name, raw_format_chunk, sample_count)
4
+ @file_name = file_name
5
+ @audio_format = raw_format_chunk[:audio_format]
6
+ @channels = raw_format_chunk[:channels]
7
+ @bits_per_sample = raw_format_chunk[:bits_per_sample]
8
+ @sample_rate = raw_format_chunk[:sample_rate]
9
+ @byte_rate = raw_format_chunk[:byte_rate]
10
+ @block_align = raw_format_chunk[:block_align]
11
+ @sample_count = sample_count
12
+ @duration = calculate_duration()
13
+ end
14
+
15
+ attr_reader :file_name,
16
+ :audio_format, :channels, :bits_per_sample, :sample_rate, :byte_rate, :block_align,
17
+ :sample_count, :duration
18
+
19
+ private
20
+
21
+ # Calculates playback time given the number of samples and the sample rate.
22
+ #
23
+ # Returns a hash listing the number of hours, minutes, seconds, and milliseconds of
24
+ # playback time.
25
+ def calculate_duration()
26
+ total_samples = @sample_count
27
+ samples_per_millisecond = @sample_rate / 1000.0
28
+ samples_per_second = @sample_rate
29
+ samples_per_minute = samples_per_second * 60
30
+ samples_per_hour = samples_per_minute * 60
31
+ hours, minutes, seconds, milliseconds = 0, 0, 0, 0
32
+
33
+ if(total_samples >= samples_per_hour)
34
+ hours = total_samples / samples_per_hour
35
+ total_samples -= samples_per_hour * hours
36
+ end
37
+
38
+ if(total_samples >= samples_per_minute)
39
+ minutes = total_samples / samples_per_minute
40
+ total_samples -= samples_per_minute * minutes
41
+ end
42
+
43
+ if(total_samples >= samples_per_second)
44
+ seconds = total_samples / samples_per_second
45
+ total_samples -= samples_per_second * seconds
46
+ end
47
+
48
+ milliseconds = (total_samples / samples_per_millisecond).floor
49
+
50
+ @duration = { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,296 @@
1
+ module WaveFile
2
+ # Error that is raised when trying to read from a file that is either not a wave file,
3
+ # or that is not valid according to the wave file spec.
4
+ class InvalidFormatError < StandardError; end
5
+
6
+ # Error that is raised when trying to read from a valid wave file that has its sample data
7
+ # stored in a format that Reader doesn't understand.
8
+ class UnsupportedFormatError < StandardError; end
9
+
10
+
11
+ # Provides the ability to read sample data out of a wave file, as well as query a
12
+ # wave file about its metadata (e.g. number of channels, sample rate, etc).
13
+ class Reader
14
+ # Returns a Reader object that is ready to start reading the specified file's sample data.
15
+ #
16
+ # file_name - The name of the wave file to read from.
17
+ # format - The format that read sample data should be returned in
18
+ # (default: the wave file's internal format).
19
+ #
20
+ # Returns a Reader object that is ready to start reading the specified file's sample data.
21
+ # Raises Errno::ENOENT if the specified file can't be found
22
+ # Raises InvalidFormatError if the specified file isn't a valid wave file
23
+ # Raises UnsupportedFormatError if the specified file has its sample data stored in a format
24
+ # that Reader doesn't know how to process.
25
+ def initialize(file_name, format=nil)
26
+ @file_name = file_name
27
+ @file = File.open(file_name, "rb")
28
+
29
+ raw_format_chunk, sample_count = HeaderReader.new(@file, @file_name).read_until_data_chunk()
30
+ @sample_count = sample_count
31
+ @samples_remaining = sample_count
32
+
33
+ # Make file is in a format we can actually read
34
+ validate_format_chunk(raw_format_chunk)
35
+
36
+ @native_format = Format.new(raw_format_chunk[:channels],
37
+ raw_format_chunk[:bits_per_sample],
38
+ raw_format_chunk[:sample_rate])
39
+ @pack_code = PACK_CODES[@native_format.bits_per_sample]
40
+
41
+ if format == nil
42
+ @format = @native_format
43
+ else
44
+ @format = format
45
+ end
46
+
47
+ if block_given?
48
+ yield(self)
49
+ close()
50
+ end
51
+ end
52
+
53
+
54
+ # Reads metadata from the specified wave file and returns an Info object with the results.
55
+ # Metadata includes things like the number of channels, bits per sample, number of samples,
56
+ # sample encoding format (i.e. PCM, IEEE float, uLaw etc). See the Info object for more
57
+ # detail on exactly what metadata is available.
58
+ #
59
+ # file_name - The name of the wave file to read from
60
+ #
61
+ # Examples:
62
+ #
63
+ # info = Reader.info("my_docs/my_sounds/my_file.wav")
64
+ #
65
+ # Returns an Info object containing metadata about the wave file.
66
+ # Raises Errno::ENOENT if the specified file can't be found
67
+ # Raises InvalidFormatError if the specified file isn't a valid wave file, or is in a format
68
+ # that WaveFile can't read.
69
+ def self.info(file_name)
70
+ file = File.open(file_name, "rb")
71
+ raw_format_chunk, sample_count = HeaderReader.new(file, file_name).read_until_data_chunk()
72
+ file.close()
73
+
74
+ return Info.new(file_name, raw_format_chunk, sample_count)
75
+ end
76
+
77
+
78
+ # Reads sample data of the into successive Buffers of the specified size, until there is no more
79
+ # sample data to be read. When all sample data has been read, the Reader is automatically closed.
80
+ # Each Buffer is passed to the specified block.
81
+ #
82
+ # Note that the number of sames to read is for *each channel*. That is, if buffer_size is 1024, then
83
+ # for a stereo file 1024 samples will be read from the left channel, and 1024 samples will be read from
84
+ # the right channel.
85
+ #
86
+ # buffer_size - The number of samples to read into each Buffer from each channel. The number of
87
+ # samples read into the final Buffer could be less than this size, if there are not
88
+ # enough remaining samples.
89
+ #
90
+ # Returns nothing.
91
+ def each_buffer(buffer_size)
92
+ begin
93
+ while true do
94
+ yield(read(buffer_size))
95
+ end
96
+ rescue EOFError
97
+ close()
98
+ end
99
+ end
100
+
101
+
102
+ # Reads the specified number of samples from the wave file into a Buffer. Note that the Buffer will have
103
+ # at most buffer_size samples, but could have less if the file doesn't have enough remaining samples.
104
+ #
105
+ # buffer_size - The number of samples to read. Note that for multi-channel files, this number of samples
106
+ # will be read from each channel.
107
+ #
108
+ # Returns a Buffer containing buffer_size samples
109
+ # Raises EOFError if no samples could be read due to reaching the end of the file
110
+ def read(buffer_size)
111
+ if @samples_remaining == 0
112
+ #FIXME: Do something different here, because the end of the file has not actually necessarily been reached
113
+ raise EOFError
114
+ elsif buffer_size > @samples_remaining
115
+ buffer_size = @samples_remaining
116
+ end
117
+
118
+ samples = @file.sysread(buffer_size * @native_format.block_align).unpack(@pack_code)
119
+ @samples_remaining -= buffer_size
120
+
121
+ if @native_format.channels > 1
122
+ num_multichannel_samples = samples.length / @native_format.channels
123
+ multichannel_data = Array.new(num_multichannel_samples)
124
+
125
+ if(@native_format.channels == 2)
126
+ # Files with more than 2 channels are expected to be less common, so if there are 2 channels
127
+ # using a faster specific algorithm instead of a general one.
128
+ num_multichannel_samples.times {|i| multichannel_data[i] = [samples.pop(), samples.pop()].reverse!() }
129
+ else
130
+ # General algorithm that works for any number of channels, 2 or greater.
131
+ num_multichannel_samples.times do |i|
132
+ sample = Array.new(@native_format.channels)
133
+ @native_format.channels.times {|j| sample[j] = samples.pop() }
134
+ multichannel_data[i] = sample.reverse!()
135
+ end
136
+ end
137
+
138
+ samples = multichannel_data.reverse!()
139
+ end
140
+
141
+ buffer = Buffer.new(samples, @native_format)
142
+ return buffer.convert(@format)
143
+ end
144
+
145
+
146
+ # Returns true if the Reader is closed, and false if it is open and available for reading.
147
+ def closed?()
148
+ return @file.closed?
149
+ end
150
+
151
+
152
+ # Closes the Reader. After a Reader is closed, no more sample data can be read from it.
153
+ #
154
+ # Returns nothing.
155
+ # Raises IOError if the Reader is already closed.
156
+ def close()
157
+ @file.close()
158
+ end
159
+
160
+ attr_reader :file_name, :format
161
+
162
+ private
163
+
164
+ def validate_format_chunk(raw_format_chunk)
165
+ # :byte_rate and :block_align are not checked to make sure that match :channels/:sample_rate/bits_per_sample
166
+ # because this library doesn't use them.
167
+
168
+ unless raw_format_chunk[:audio_format] == PCM
169
+ raise UnsupportedFormatError, "Audio format is #{raw_format_chunk[:audio_format]}, " +
170
+ "but only format code 1 (i.e. PCM) is supported."
171
+ end
172
+
173
+ unless Format::SUPPORTED_BITS_PER_SAMPLE.include?(raw_format_chunk[:bits_per_sample])
174
+ raise UnsupportedFormatError, "Bits per sample is #{raw_format_chunk[:bits_per_sample]}, " +
175
+ "but only #{Format::SUPPORTED_BITS_PER_SAMPLE.inspect} are supported."
176
+ end
177
+
178
+ unless raw_format_chunk[:channels] > 0
179
+ raise UnsupportedFormatError, "Number of channels is #{raw_format_chunk[:channels]}, " +
180
+ "but only #{Format::MIN_CHANNELS}-#{Format::MAX_CHANNELS} are supported."
181
+ end
182
+
183
+ unless raw_format_chunk[:sample_rate] > 0
184
+ raise UnsupportedFormatError, "Sample rate is #{raw_format_chunk[:sample_rate]}, " +
185
+ "but only #{Format::MIN_SAMPLE_RATE}-#{Format::MAX_SAMPLE_RATE} are supported."
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ # Used to read the RIFF chunks in a wave file up until the data chunk. Thus is can be used
192
+ # to open a wave file and "queue it up" to the start of the actual sample data, as well as
193
+ # extract information out of pre-data chunks, such as the format chunk.
194
+ class HeaderReader
195
+ RIFF_CHUNK_HEADER_SIZE = 12
196
+ FORMAT_CHUNK_MINIMUM_SIZE = 16
197
+
198
+ def initialize(file, file_name)
199
+ @file = file
200
+ @file_name = file_name
201
+ end
202
+
203
+ def read_until_data_chunk()
204
+ read_riff_chunk()
205
+
206
+ begin
207
+ chunk_id = @file.sysread(4)
208
+ chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
209
+ while chunk_id != CHUNK_IDS[:data]
210
+ if chunk_id == CHUNK_IDS[:format]
211
+ format_chunk = read_format_chunk(chunk_id, chunk_size)
212
+ else
213
+ # Other chunk types besides the format chunk are ignored. This may change in the future.
214
+ @file.sysread(chunk_size)
215
+ end
216
+
217
+ chunk_id = @file.sysread(4)
218
+ chunk_size = @file.sysread(4).unpack(UNSIGNED_INT_32).first
219
+ end
220
+ rescue EOFError
221
+ raise_error InvalidFormatError, "It doesn't have a data chunk."
222
+ end
223
+
224
+ if format_chunk == nil
225
+ raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
226
+ end
227
+
228
+ sample_count = chunk_size / format_chunk[:block_align]
229
+
230
+ return format_chunk, sample_count
231
+ end
232
+
233
+ private
234
+
235
+ def read_riff_chunk()
236
+ riff_header = {}
237
+ riff_header[:chunk_id],
238
+ riff_header[:chunk_size],
239
+ riff_header[:riff_format] = read_chunk_body(CHUNK_IDS[:riff], RIFF_CHUNK_HEADER_SIZE).unpack("a4Va4")
240
+
241
+ unless riff_header[:chunk_id] == CHUNK_IDS[:riff]
242
+ raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{riff_header[:chunk_id]}'"
243
+ end
244
+
245
+ unless riff_header[:riff_format] == WAVEFILE_FORMAT_CODE
246
+ raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_header[:riff_format]}'"
247
+ end
248
+
249
+ return riff_header
250
+ end
251
+
252
+ def read_format_chunk(chunk_id, chunk_size)
253
+ if chunk_size < FORMAT_CHUNK_MINIMUM_SIZE
254
+ raise_error InvalidFormatError, "The format chunk is incomplete."
255
+ end
256
+
257
+ raw_bytes = read_chunk_body(CHUNK_IDS[:format], chunk_size)
258
+
259
+ format_chunk = {}
260
+ format_chunk[:audio_format],
261
+ format_chunk[:channels],
262
+ format_chunk[:sample_rate],
263
+ format_chunk[:byte_rate],
264
+ format_chunk[:block_align],
265
+ format_chunk[:bits_per_sample] = raw_bytes.slice!(0...FORMAT_CHUNK_MINIMUM_SIZE).unpack("vvVVvv")
266
+
267
+ if chunk_size > FORMAT_CHUNK_MINIMUM_SIZE
268
+ format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
269
+
270
+ if format_chunk[:extension_size] == nil
271
+ raise_error InvalidFormatError, "The format chunk is missing an expected extension."
272
+ end
273
+
274
+ if format_chunk[:extension_size] != raw_bytes.length
275
+ raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
276
+ end
277
+
278
+ # TODO: Parse the extension
279
+ end
280
+
281
+ return format_chunk
282
+ end
283
+
284
+ def read_chunk_body(chunk_id, chunk_size)
285
+ begin
286
+ return @file.sysread(chunk_size)
287
+ rescue EOFError
288
+ raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
289
+ end
290
+ end
291
+
292
+ def raise_error(exception_class, message)
293
+ raise exception_class, "File '#{@file_name}' is not a supported wave file. #{message}"
294
+ end
295
+ end
296
+ end