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
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == WaveFile
2
2
 
3
- # Copyright (c) 2009 Joel Strait
3
+ # Copyright (c) 2009-12 Joel Strait
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person
6
6
  # obtaining a copy of this software and associated documentation
data/README.markdown CHANGED
@@ -1,79 +1,52 @@
1
- A Ruby gem for reading and writing wave files (*.wav).
1
+ A pure Ruby gem for reading and writing sound files in Wave format (*.wav). You can use this gem to create Ruby programs that produce audio, such as [drum machine](http://beatsdrummachine.com).
2
2
 
3
- # Installation
4
-
5
- First, install the WaveFile gem...
6
-
7
- gem install wavefile
3
+ # What's New in v0.4.0?
8
4
 
9
- ...and include it in your Ruby program:
5
+ This version is a re-write with a completely new, much improved API. (The old API has been removed). Some improvements due to the new API include:
10
6
 
11
- require 'wavefile'
7
+ * Reduced memory consumption, due to not having to load the entire file into memory. In practice, this allows the gem to read/write files that previously would have been prohibitively large.
8
+ * Better performance for large files, for the same reason as above.
9
+ * Ability to progressively append data to the end of a file, instead of writing the entire file at once.
10
+ * Ability to easily read and write data in an arbitrary format, regardless of the file's native format. For example, you can transparently read data out of a 16-bit stereo file as 8-bit mono.
11
+ * Automatic file management, similar to how IO.open() works. It's easy to continually read the sample data from a file, passing each buffer to a block, and have the file automatically close when there is no more data left.
12
12
 
13
- # Usage
13
+ Other improvements include:
14
14
 
15
- To open a wave file and get the raw sample data:
15
+ * Ability to query format metadata of files without opening them, even for formats that this gem can't read or write.
16
+ * Support for reading and writing 32-bit PCM files.
16
17
 
17
- w = WaveFile.open("myfile.wav")
18
- samples = w.sample_data
18
+ However, reading or writing data as floating point (i.e. values between -1.0 and 1.0) won't be supported in v0.4.0 to keep the scope in check. It might be re-added in the future.
19
19
 
20
- Sample data is stored in an array. For mono files, each sample is a single number. For stereo files, each sample is represented by an array containing a value for the left and right channel.
21
20
 
22
- # Mono example
23
- [0, 128, 255, 128]
24
-
25
- # Stereo example
26
- [[0, 255], [128, 128], [255, 0], [128, 128]]
21
+ # Compatibility
27
22
 
28
- You can also get the sample data in a normalized form, with each sample between -1.0 and 1.0:
23
+ WaveFile has been tested with these Ruby versions, and appears to be compatible with them:
29
24
 
30
- normalized_samples = w.normalized_sample_data
25
+ * MRI 1.9.3, 1.9.2, 1.9.1, 1.8.7
26
+ * JRuby 1.6.5
27
+ * Rubinius 1.2.4
28
+ * MacRuby 0.10
31
29
 
32
- You can get basic metadata:
30
+ If you find any compatibility issues, please let me know by opening a GitHub issue.
33
31
 
34
- w.num_channels # 1 for mono, 2 for stereo
35
- w.mono? # Alias for num_channels == 1
36
- w.stereo? # Alias for num_channels == 2
37
- w.sample_rate # 11025, 22050, 44100, etc.
38
- w.bits_per_sample # 8 or 16
39
- w.duration # Example: {:hours => 0, :minutes => 3, :seconds => 12, :milliseconds => 345 }
40
32
 
41
- You can view all of the metadata at once using the `inspect()` method. It returns a multi-line string:
33
+ # Dependencies
42
34
 
43
- w.inspect()
44
-
45
- # Example result:
46
- # Channels: 2
47
- # Sample rate: 44100
48
- # Bits per sample: 16
49
- # Block align: 4
50
- # Byte rate: 176400
51
- # Sample count: 498070
52
- # Duration: 0h:0m:11s:294ms
35
+ WaveFile has no external dependencies. It is written in pure Ruby, and is entirely self-contained.
53
36
 
54
- You can use setter methods to convert a file to a different format. For example, you can convert a mono file to stereo, or down-sample a 16-bit file to 8-bit.
55
37
 
56
- w.num_channels = 2
57
- w.num_channels = :stereo // Equivalent to line above
58
- w.sample_rate = 22050
59
- w.bits_per_sample = 16
38
+ # Installation
60
39
 
61
- Changes are not saved to disk until you call the `save()` method.
40
+ First, install the WaveFile gem from rubygems.org:
62
41
 
63
- w.save("myfile.wav")
42
+ gem install wavefile
64
43
 
65
- To create and save a new wave file:
44
+ ...and include it in your Ruby program:
66
45
 
67
- w = WaveFile.new(1, 44100, 16) # num_channels,
68
- # sample_rate,
69
- # bits_per_sample
70
- w.sample_data = <array of samples goes here>
71
- w.save("myfile.wav")
46
+ require 'wavefile'
72
47
 
73
- When calling the `sample_data=()` method, the passed in array can contain either raw samples or normalized samples. If the first item in the array is a Float, the entire array is assumed to be normalized. Normalized samples are automatically converted into raw samples when saving.
48
+ Note that if you're installing the gem into the default Ruby that comes pre-installed on MacOS (as opposed to a Ruby installed via [RVM](http://beginrescueend.com/)), you should used `sudo gem install wavefile`. Otherwise you might run into a file permission error.
74
49
 
75
- You can reverse a file with the `reverse()` method:
50
+ # Usage
76
51
 
77
- w = WaveFile.open("myfile.wav")
78
- w.reverse()
79
- w.save("myfile_reversed.wav")
52
+ For usage instructions with examples, check out the [wiki](https://github.com/jstrait/wavefile/wiki).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList['test/**/*_test.rb']
6
+ end
data/lib/wavefile.rb CHANGED
@@ -1,456 +1,32 @@
1
- #!/usr/bin/env ruby
1
+ require 'wavefile/buffer'
2
+ require 'wavefile/format'
3
+ require 'wavefile/info'
4
+ require 'wavefile/reader'
5
+ require 'wavefile/writer'
2
6
 
3
- =begin
4
- WAV File Specification
5
- FROM http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
6
- The canonical WAVE format starts with the RIFF header:
7
- 0 4 ChunkID Contains the letters "RIFF" in ASCII form
8
- (0x52494646 big-endian form).
9
- 4 4 ChunkSize 36 + SubChunk2Size, or more precisely:
10
- 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)
11
- This is the size of the rest of the chunk
12
- following this number. This is the size of the
13
- entire file in bytes minus 8 bytes for the
14
- two fields not included in this count:
15
- ChunkID and ChunkSize.
16
- 8 4 Format Contains the letters "WAVE"
17
- (0x57415645 big-endian form).
7
+ module WaveFile
8
+ VERSION = "0.4.0"
18
9
 
19
- The "WAVE" format consists of two subchunks: "fmt " and "data":
20
- The "fmt " subchunk describes the sound data's format:
21
- 12 4 Subchunk1ID Contains the letters "fmt "
22
- (0x666d7420 big-endian form).
23
- 16 4 Subchunk1Size 16 for PCM. This is the size of the
24
- rest of the Subchunk which follows this number.
25
- 20 2 AudioFormat PCM = 1 (i.e. Linear quantization)
26
- Values other than 1 indicate some
27
- form of compression.
28
- 22 2 NumChannels Mono = 1, Stereo = 2, etc.
29
- 24 4 SampleRate 8000, 44100, etc.
30
- 28 4 ByteRate == SampleRate * NumChannels * BitsPerSample/8
31
- 32 2 BlockAlign == NumChannels * BitsPerSample/8
32
- The number of bytes for one sample including
33
- all channels. I wonder what happens when
34
- this number isn't an integer?
35
- 34 2 BitsPerSample 8 bits = 8, 16 bits = 16, etc.
36
-
37
- The "data" subchunk contains the size of the data and the actual sound:
38
- 36 4 Subchunk2ID Contains the letters "data"
39
- (0x64617461 big-endian form).
40
- 40 4 Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8
41
- This is the number of bytes in the data.
42
- You can also think of this as the size
43
- of the read of the subchunk following this
44
- number.
45
- 44 * Data The actual sound data.
46
- =end
47
-
48
- class WaveFile
49
- CHUNK_ID = "RIFF"
50
- FORMAT = "WAVE"
51
- FORMAT_CHUNK_ID = "fmt "
52
- SUB_CHUNK1_SIZE = 16
10
+ WAVEFILE_FORMAT_CODE = "WAVE"
11
+ FORMAT_CHUNK_BYTE_LENGTH = 16
53
12
  PCM = 1
54
- DATA_CHUNK_ID = "data"
55
- HEADER_SIZE = 36
56
-
57
- def initialize(num_channels, sample_rate, bits_per_sample, sample_data = [])
58
- if num_channels == :mono
59
- @num_channels = 1
60
- elsif num_channels == :stereo
61
- @num_channels = 2
62
- else
63
- @num_channels = num_channels
64
- end
65
- @sample_rate = sample_rate
66
- @bits_per_sample = bits_per_sample
67
- @sample_data = sample_data
68
-
69
- @byte_rate = sample_rate * @num_channels * (bits_per_sample / 8)
70
- @block_align = @num_channels * (bits_per_sample / 8)
71
- end
72
-
73
- def self.open(path)
74
- file = File.open(path, "rb")
75
-
76
- begin
77
- header = read_header(file)
78
- errors = validate_header(header)
79
-
80
- if errors == []
81
- sample_data = read_sample_data(file,
82
- header[:num_channels],
83
- header[:bits_per_sample],
84
- header[:sub_chunk2_size])
85
-
86
- wave_file = self.new(header[:num_channels],
87
- header[:sample_rate],
88
- header[:bits_per_sample],
89
- sample_data)
90
- else
91
- error_msg = "#{path} can't be opened, due to the following errors:\n"
92
- errors.each {|error| error_msg += " * #{error}\n" }
93
- raise StandardError, error_msg
94
- end
95
- rescue EOFError
96
- raise StandardError, "An error occured while reading #{path}."
97
- ensure
98
- file.close()
99
- end
100
-
101
- return wave_file
102
- end
103
-
104
- def save(path)
105
- # All numeric values should be saved in little-endian format
106
-
107
- sample_data_size = @sample_data.length * @num_channels * (@bits_per_sample / 8)
108
-
109
- # Write the header
110
- file_contents = CHUNK_ID
111
- file_contents += [HEADER_SIZE + sample_data_size].pack("V")
112
- file_contents += FORMAT
113
- file_contents += FORMAT_CHUNK_ID
114
- file_contents += [SUB_CHUNK1_SIZE].pack("V")
115
- file_contents += [PCM].pack("v")
116
- file_contents += [@num_channels].pack("v")
117
- file_contents += [@sample_rate].pack("V")
118
- file_contents += [@byte_rate].pack("V")
119
- file_contents += [@block_align].pack("v")
120
- file_contents += [@bits_per_sample].pack("v")
121
- file_contents += DATA_CHUNK_ID
122
- file_contents += [sample_data_size].pack("V")
123
-
124
- # Write the sample data
125
- if !mono?
126
- output_sample_data = []
127
- @sample_data.each{|sample|
128
- sample.each{|sub_sample|
129
- output_sample_data << sub_sample
130
- }
131
- }
132
- else
133
- output_sample_data = @sample_data
134
- end
135
-
136
- if @bits_per_sample == 8
137
- file_contents += output_sample_data.pack("C*")
138
- elsif @bits_per_sample == 16
139
- file_contents += output_sample_data.pack("s*")
140
- else
141
- raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
142
- end
143
-
144
- file = File.open(path, "w")
145
- file.syswrite(file_contents)
146
- file.close
147
- end
148
-
149
- def sample_data()
150
- return @sample_data
151
- end
152
-
153
- def normalized_sample_data()
154
- if @bits_per_sample == 8
155
- min_value = 128.0
156
- max_value = 127.0
157
- midpoint = 128
158
- elsif @bits_per_sample == 16
159
- min_value = 32768.0
160
- max_value = 32767.0
161
- midpoint = 0
162
- else
163
- raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
164
- end
165
-
166
- if mono?
167
- normalized_sample_data = @sample_data.map {|sample|
168
- sample -= midpoint
169
- if sample < 0
170
- sample.to_f / min_value
171
- else
172
- sample.to_f / max_value
173
- end
174
- }
175
- else
176
- normalized_sample_data = @sample_data.map {|sample|
177
- sample.map {|sub_sample|
178
- sub_sample -= midpoint
179
- if sub_sample < 0
180
- sub_sample.to_f / min_value
181
- else
182
- sub_sample.to_f / max_value
183
- end
184
- }
185
- }
186
- end
187
-
188
- return normalized_sample_data
189
- end
190
-
191
- def sample_data=(sample_data)
192
- if sample_data.length > 0 && ((mono? && sample_data[0].class == Float) ||
193
- (!mono? && sample_data[0][0].class == Float))
194
- if @bits_per_sample == 8
195
- # Samples in 8-bit wave files are stored as a unsigned byte
196
- # Effective values are 0 to 255, midpoint at 128
197
- min_value = 128.0
198
- max_value = 127.0
199
- midpoint = 128
200
- elsif @bits_per_sample == 16
201
- # Samples in 16-bit wave files are stored as a signed little-endian short
202
- # Effective values are -32768 to 32767, midpoint at 0
203
- min_value = 32768.0
204
- max_value = 32767.0
205
- midpoint = 0
206
- else
207
- raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
208
- end
209
-
210
- if mono?
211
- @sample_data = sample_data.map {|sample|
212
- if(sample < 0.0)
213
- (sample * min_value).round + midpoint
214
- else
215
- (sample * max_value).round + midpoint
216
- end
217
- }
218
- else
219
- @sample_data = sample_data.map {|sample|
220
- sample.map {|sub_sample|
221
- if(sub_sample < 0.0)
222
- (sub_sample * min_value).round + midpoint
223
- else
224
- (sub_sample * max_value).round + midpoint
225
- end
226
- }
227
- }
228
- end
229
- else
230
- @sample_data = sample_data
231
- end
232
- end
233
-
234
- def mono?()
235
- return num_channels == 1
236
- end
237
-
238
- def stereo?()
239
- return num_channels == 2
240
- end
241
-
242
- def reverse()
243
- sample_data.reverse!()
244
- end
245
-
246
- def duration()
247
- total_samples = sample_data.length
248
- samples_per_millisecond = @sample_rate / 1000.0
249
- samples_per_second = @sample_rate
250
- samples_per_minute = samples_per_second * 60
251
- samples_per_hour = samples_per_minute * 60
252
- hours, minutes, seconds, milliseconds = 0, 0, 0, 0
253
-
254
- if(total_samples >= samples_per_hour)
255
- hours = total_samples / samples_per_hour
256
- total_samples -= samples_per_hour * hours
257
- end
258
-
259
- if(total_samples >= samples_per_minute)
260
- minutes = total_samples / samples_per_minute
261
- total_samples -= samples_per_minute * minutes
262
- end
263
-
264
- if(total_samples >= samples_per_second)
265
- seconds = total_samples / samples_per_second
266
- total_samples -= samples_per_second * seconds
267
- end
268
-
269
- milliseconds = (total_samples / samples_per_millisecond).floor
270
-
271
- return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
272
- end
273
-
274
- def bits_per_sample=(new_bits_per_sample)
275
- if new_bits_per_sample != 8 && new_bits_per_sample != 16
276
- raise StandardError, "Bits per sample of #{@bits_per_samples} is invalid, only 8 or 16 are supported"
277
- end
278
-
279
- if @bits_per_sample == 16 && new_bits_per_sample == 8
280
- conversion_func = lambda {|sample|
281
- if(sample < 0)
282
- (sample / 256) + 128
283
- else
284
- # Faster to just divide by integer 258?
285
- (sample / 258.007874015748031).round + 128
286
- end
287
- }
288
-
289
- if mono?
290
- @sample_data.map! &conversion_func
291
- else
292
- sample_data.map! {|sample| sample.map! &conversion_func }
293
- end
294
- elsif @bits_per_sample == 8 && new_bits_per_sample == 16
295
- conversion_func = lambda {|sample|
296
- sample -= 128
297
- if(sample < 0)
298
- sample * 256
299
- else
300
- # Faster to just multiply by integer 258?
301
- (sample * 258.007874015748031).round
302
- end
303
- }
304
-
305
- if mono?
306
- @sample_data.map! &conversion_func
307
- else
308
- sample_data.map! {|sample| sample.map! &conversion_func }
309
- end
310
- end
311
-
312
- @bits_per_sample = new_bits_per_sample
313
- end
314
-
315
- def num_channels=(new_num_channels)
316
- if new_num_channels == :mono
317
- new_num_channels = 1
318
- elsif new_num_channels == :stereo
319
- new_num_channels = 2
320
- end
321
-
322
- # The cases of mono -> stereo and vice-versa are handled in specially,
323
- # because those conversion methods are faster than the general methods,
324
- # and the large majority of wave files are expected to be either mono or stereo.
325
- if @num_channels == 1 && new_num_channels == 2
326
- sample_data.map! {|sample| [sample, sample]}
327
- elsif @num_channels == 2 && new_num_channels == 1
328
- sample_data.map! {|sample| (sample[0] + sample[1]) / 2}
329
- elsif @num_channels == 1 && new_num_channels >= 2
330
- sample_data.map! {|sample| [].fill(sample, 0, new_num_channels)}
331
- elsif @num_channels >= 2 && new_num_channels == 1
332
- sample_data.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / @num_channels }
333
- elsif @num_channels > 2 && new_num_channels == 2
334
- sample_data.map! {|sample| [sample[0], sample[1]]}
335
- end
336
-
337
- @num_channels = new_num_channels
338
- end
339
-
340
- def inspect()
341
- duration = self.duration()
342
-
343
- result = "Channels: #{@num_channels}\n" +
344
- "Sample rate: #{@sample_rate}\n" +
345
- "Bits per sample: #{@bits_per_sample}\n" +
346
- "Block align: #{@block_align}\n" +
347
- "Byte rate: #{@byte_rate}\n" +
348
- "Sample count: #{@sample_data.length}\n" +
349
- "Duration: #{duration[:hours]}h:#{duration[:minutes]}m:#{duration[:seconds]}s:#{duration[:milliseconds]}ms\n"
350
- end
351
-
352
- attr_reader :num_channels, :bits_per_sample, :byte_rate, :block_align
353
- attr_accessor :sample_rate
354
-
355
- private
356
-
357
- def self.read_header(file)
358
- header = {}
359
-
360
- # Read RIFF header
361
- riff_header = file.sysread(12).unpack("a4Va4")
362
- header[:chunk_id] = riff_header[0]
363
- header[:chunk_size] = riff_header[1]
364
- header[:format] = riff_header[2]
365
-
366
- # Read format subchunk
367
- header[:sub_chunk1_id], header[:sub_chunk1_size] = self.read_to_chunk(file, FORMAT_CHUNK_ID)
368
- format_subchunk_str = file.sysread(header[:sub_chunk1_size])
369
- format_subchunk = format_subchunk_str.unpack("vvVVvv") # Any extra parameters are ignored
370
- header[:audio_format] = format_subchunk[0]
371
- header[:num_channels] = format_subchunk[1]
372
- header[:sample_rate] = format_subchunk[2]
373
- header[:byte_rate] = format_subchunk[3]
374
- header[:block_align] = format_subchunk[4]
375
- header[:bits_per_sample] = format_subchunk[5]
376
-
377
- # Read data subchunk
378
- header[:sub_chunk2_id], header[:sub_chunk2_size] = self.read_to_chunk(file, DATA_CHUNK_ID)
379
-
380
- return header
381
- end
382
-
383
- def self.read_to_chunk(file, expected_chunk_id)
384
- chunk_id = file.sysread(4)
385
- chunk_size = file.sysread(4).unpack("V")[0]
386
-
387
- while chunk_id != expected_chunk_id
388
- # Skip chunk
389
- file.sysread(chunk_size)
390
-
391
- chunk_id = file.sysread(4)
392
- chunk_size = file.sysread(4).unpack("V")[0]
393
- end
394
-
395
- return chunk_id, chunk_size
396
- end
13
+ CHUNK_IDS = {:riff => "RIFF",
14
+ :format => "fmt ",
15
+ :data => "data",
16
+ :fact => "fact",
17
+ :silence => "slnt",
18
+ :cue => "cue ",
19
+ :playlist => "plst",
20
+ :list => "list",
21
+ :label => "labl",
22
+ :labeled_text => "ltxt",
23
+ :note => "note",
24
+ :sample => "smpl",
25
+ :instrument => "inst" }
26
+
27
+ PACK_CODES = {8 => "C*", 16 => "s*", 32 => "l*"}
28
+
29
+ UNSIGNED_INT_16 = "v"
30
+ UNSIGNED_INT_32 = "V"
31
+ end
397
32
 
398
- def self.validate_header(header)
399
- errors = []
400
-
401
- unless header[:bits_per_sample] == 8 || header[:bits_per_sample] == 16
402
- errors << "Invalid bits per sample of #{header[:bits_per_sample]}. Only 8 and 16 are supported."
403
- end
404
-
405
- unless (1..65535) === header[:num_channels]
406
- errors << "Invalid number of channels. Must be between 1 and 65535."
407
- end
408
-
409
- unless header[:chunk_id] == CHUNK_ID
410
- errors << "Unsupported chunk ID: '#{header[:chunk_id]}'"
411
- end
412
-
413
- unless header[:format] == FORMAT
414
- errors << "Unsupported format: '#{header[:format]}'"
415
- end
416
-
417
- unless header[:sub_chunk1_id] == FORMAT_CHUNK_ID
418
- errors << "Unsupported chunk id: '#{header[:sub_chunk1_id]}'"
419
- end
420
-
421
- unless header[:audio_format] == PCM
422
- errors << "Unsupported audio format code: '#{header[:audio_format]}'"
423
- end
424
-
425
- unless header[:sub_chunk2_id] == DATA_CHUNK_ID
426
- errors << "Unsupported chunk id: '#{header[:sub_chunk2_id]}'"
427
- end
428
-
429
- return errors
430
- end
431
-
432
- # Assumes that file is "queued up" to the first sample
433
- def self.read_sample_data(file, num_channels, bits_per_sample, sample_data_size)
434
- if(bits_per_sample == 8)
435
- data = file.sysread(sample_data_size).unpack("C*")
436
- elsif(bits_per_sample == 16)
437
- data = file.sysread(sample_data_size).unpack("s*")
438
- else
439
- data = []
440
- end
441
-
442
- if(num_channels > 1)
443
- multichannel_data = []
444
-
445
- i = 0
446
- while i < data.length
447
- multichannel_data << data[i...(num_channels + i)]
448
- i += num_channels
449
- end
450
-
451
- data = multichannel_data
452
- end
453
-
454
- return data
455
- end
456
- end