wavefile 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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