wavefile 0.3.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 (5) hide show
  1. data/LICENSE +24 -0
  2. data/README.markdown +79 -0
  3. data/lib/wavefile.rb +456 -0
  4. data/test/wavefile_test.rb +339 -0
  5. metadata +58 -0
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ == WaveFile
2
+
3
+ # Copyright (c) 2009 Joel Strait
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,79 @@
1
+ A Ruby gem for reading and writing wave files (*.wav).
2
+
3
+ # Installation
4
+
5
+ First, install the WaveFile gem...
6
+
7
+ gem install wavefile
8
+
9
+ ...and include it in your Ruby program:
10
+
11
+ require 'wavefile'
12
+
13
+ # Usage
14
+
15
+ To open a wave file and get the raw sample data:
16
+
17
+ w = WaveFile.open("myfile.wav")
18
+ samples = w.sample_data
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
+
22
+ # Mono example
23
+ [0, 128, 255, 128]
24
+
25
+ # Stereo example
26
+ [[0, 255], [128, 128], [255, 0], [128, 128]]
27
+
28
+ You can also get the sample data in a normalized form, with each sample between -1.0 and 1.0:
29
+
30
+ normalized_samples = w.normalized_sample_data
31
+
32
+ You can get basic metadata:
33
+
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
+
41
+ You can view all of the metadata at once using the `inspect()` method. It returns a multi-line string:
42
+
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
53
+
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
+
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
60
+
61
+ Changes are not saved to disk until you call the `save()` method.
62
+
63
+ w.save("myfile.wav")
64
+
65
+ To create and save a new wave file:
66
+
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")
72
+
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.
74
+
75
+ You can reverse a file with the `reverse()` method:
76
+
77
+ w = WaveFile.open("myfile.wav")
78
+ w.reverse()
79
+ w.save("myfile_reversed.wav")
data/lib/wavefile.rb ADDED
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env ruby
2
+
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).
18
+
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
53
+ 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
397
+
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
@@ -0,0 +1,339 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/unit'
4
+ require 'wavefile'
5
+
6
+ class WaveFileTest < Test::Unit::TestCase
7
+ def test_initialize
8
+
9
+ end
10
+
11
+ def test_read_empty_file
12
+ assert_raise(StandardError) { w = WaveFile.open("examples/invalid/empty.wav") }
13
+ end
14
+
15
+ def test_read_nonexistent_file
16
+ assert_raise(Errno::ENOENT) { w = WaveFile.open("examples/invalid/nonexistent.wav") }
17
+ end
18
+
19
+ def test_read_valid_file
20
+ # Mono file
21
+ w = WaveFile.open("examples/valid/sine-mono-8bit.wav")
22
+ assert_equal(w.num_channels, 1)
23
+ assert_equal(w.mono?, true)
24
+ assert_equal(w.stereo?, false)
25
+ assert_equal(w.sample_rate, 44100)
26
+ assert_equal(w.bits_per_sample, 8)
27
+ assert_equal(w.byte_rate, 44100)
28
+ assert_equal(w.block_align, 1)
29
+ assert_equal(w.sample_data.length, 44100)
30
+ # Test that sample array is in format [sample, sample ... sample]
31
+ valid = true
32
+ w.sample_data.each{|sample| valid &&= (sample.class == Fixnum)}
33
+ assert_equal(valid, true)
34
+
35
+ # Stereo file
36
+ w = WaveFile.open("examples/valid/sine-stereo-8bit.wav")
37
+ assert_equal(w.num_channels, 2)
38
+ assert_equal(w.mono?, false)
39
+ assert_equal(w.stereo?, true)
40
+ assert_equal(w.sample_rate, 44100)
41
+ assert_equal(w.bits_per_sample, 8)
42
+ assert_equal(w.byte_rate, 88200)
43
+ assert_equal(w.block_align, 2)
44
+ assert_equal(w.sample_data.length, 44100)
45
+ # Test that sample array is in format [[left, right], [left, right] ... [left,right]]
46
+ valid = true
47
+ w.sample_data.each{|sample| valid &&= (sample.class == Array) && (sample.length == 2)}
48
+ assert_equal(valid, true)
49
+ end
50
+
51
+ def test_new_file
52
+ # Mono
53
+ w = WaveFile.new(1, 44100, 8)
54
+ assert_equal(w.num_channels, 1)
55
+ assert_equal(w.sample_rate, 44100)
56
+ assert_equal(w.bits_per_sample, 8)
57
+ assert_equal(w.byte_rate, 44100)
58
+ assert_equal(w.block_align, 1)
59
+
60
+ # Mono
61
+ w = WaveFile.new(:mono, 44100, 8)
62
+ assert_equal(w.num_channels, 1)
63
+ assert_equal(w.sample_rate, 44100)
64
+ assert_equal(w.bits_per_sample, 8)
65
+ assert_equal(w.byte_rate, 44100)
66
+ assert_equal(w.block_align, 1)
67
+
68
+ # Stereo
69
+ w = WaveFile.new(2, 44100, 16)
70
+ assert_equal(w.num_channels, 2)
71
+ assert_equal(w.sample_rate, 44100)
72
+ assert_equal(w.bits_per_sample, 16)
73
+ assert_equal(w.byte_rate, 176400)
74
+ assert_equal(w.block_align, 4)
75
+
76
+ # Stereo
77
+ w = WaveFile.new(:stereo, 44100, 16)
78
+ assert_equal(w.num_channels, 2)
79
+ assert_equal(w.sample_rate, 44100)
80
+ assert_equal(w.bits_per_sample, 16)
81
+ assert_equal(w.byte_rate, 176400)
82
+ assert_equal(w.block_align, 4)
83
+
84
+ # Quad
85
+ w = WaveFile.new(4, 44100, 16)
86
+ assert_equal(w.num_channels, 4)
87
+ assert_equal(w.sample_rate, 44100)
88
+ assert_equal(w.bits_per_sample, 16)
89
+ assert_equal(w.byte_rate, 352800)
90
+ assert_equal(w.block_align, 8)
91
+ end
92
+
93
+ def test_normalized_sample_data
94
+ # Mono 8-bit
95
+ w = WaveFile.new(:mono, 44100, 8)
96
+ w.sample_data = [0, 32, 64, 96, 128, 160, 192, 223, 255]
97
+ assert_equal(w.normalized_sample_data, [-1.0, -0.75, -0.5, -0.25, 0.0,
98
+ (32.0 / 127.0), (64.0 / 127.0), (95.0 / 127.0), 1.0])
99
+
100
+ # Mono 16-bit
101
+ w = WaveFile.new(:mono, 44100, 16)
102
+ w.sample_data = [-32768, -24576, -16384, -8192, 0, 8192, 16383, 24575, 32767]
103
+ assert_equal(w.normalized_sample_data, [-1.0, -0.75, -0.5, -0.25, 0.0,
104
+ (8192.0 / 32767.0), (16383.0 / 32767.0), (24575.0 / 32767.0), 1.0])
105
+
106
+ # Stereo 8-bit
107
+ w = WaveFile.new(:stereo, 44100, 8)
108
+ w.sample_data = [[0, 255], [32, 223], [64, 192], [96, 160], [128, 128], [160, 96], [192, 64], [223, 32], [255, 0]]
109
+ assert_equal(w.normalized_sample_data, [[-1.0, 1.0],
110
+ [-0.75, (95.0 / 127.0)],
111
+ [-0.5, (64.0 / 127.0)],
112
+ [-0.25, (32.0 / 127.0)],
113
+ [0.0, 0.0],
114
+ [(32.0 / 127.0), -0.25],
115
+ [(64.0 / 127.0), -0.5],
116
+ [(95.0 / 127.0), -0.75],
117
+ [1.0, -1.0]])
118
+
119
+ # Stereo 16-bit
120
+ w = WaveFile.new(:stereo, 44100, 16)
121
+ w.sample_data = [[-32768, 32767], [-24576, 24575], [-16384, 16384], [-8192, 8192], [0, 0], [8192, -8192], [16384, -16384], [24575, -24576], [32767, -32768]]
122
+ assert_equal(w.normalized_sample_data, [[-1.0, 1.0],
123
+ [-0.75, (24575.0 / 32767.0)],
124
+ [-0.5, (16384.0 / 32767.0)],
125
+ [-0.25, (8192.0 / 32767.0)],
126
+ [0.0, 0.0],
127
+ [(8192.0 / 32767.0), -0.25],
128
+ [(16384.0 / 32767.0), -0.5],
129
+ [(24575.0 / 32767.0), -0.75],
130
+ [1.0, -1.0]])
131
+ end
132
+
133
+ def test_sample_data=
134
+ # Mono 8-bit
135
+ w = WaveFile.new(:mono, 44100, 8)
136
+ w.sample_data = [-1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0]
137
+ assert_equal(w.sample_data, [0, 32, 64, 96, 128, 160, 192, 223, 255])
138
+
139
+ # Mono 16-bit
140
+ w = WaveFile.new(:mono, 44100, 16)
141
+ w.sample_data = [-1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0]
142
+ assert_equal(w.sample_data, [-32768, -24576, -16384, -8192, 0, 8192, 16384, 24575, 32767])
143
+
144
+ # Stereo 8-bit
145
+ w = WaveFile.new(:stereo, 44100, 8)
146
+ w.sample_data = [[-1.0, 1.0], [-0.75, 0.75], [-0.5, 0.5], [-0.25, 0.25], [0.0, 0.0],
147
+ [0.25, -0.25], [0.5, -0.5], [0.75, -0.75], [1.0, -1.0]]
148
+ assert_equal(w.sample_data, [[0, 255], [32, 223], [64, 192], [96, 160], [128, 128],
149
+ [160, 96], [192, 64], [223, 32], [255, 0]])
150
+
151
+ # Stereo 16-bit
152
+ w = WaveFile.new(:stereo, 44100, 16)
153
+ w.sample_data = [[-1.0, 1.0], [-0.75, 0.75], [-0.5, 0.5], [-0.25, 0.25], [0.0, 0.0],
154
+ [0.25, -0.25], [0.5, -0.5], [0.75, -0.75], [1.0, -1.0]]
155
+ assert_equal(w.sample_data, [[-32768, 32767], [-24576, 24575], [-16384, 16384], [-8192, 8192], [0, 0],
156
+ [8192, -8192], [16384, -16384], [24575, -24576], [32767, -32768]])
157
+ end
158
+
159
+ def test_mono?
160
+ w = WaveFile.new(1, 44100, 16)
161
+ assert_equal(w.mono?, true)
162
+
163
+ w = WaveFile.open("examples/valid/sine-mono-8bit.wav")
164
+ assert_equal(w.mono?, true)
165
+
166
+ w = WaveFile.new(2, 44100, 16)
167
+ assert_equal(w.mono?, false)
168
+
169
+ w = WaveFile.new(4, 44100, 16)
170
+ assert_equal(w.mono?, false)
171
+ end
172
+
173
+ def test_stereo?
174
+ w = WaveFile.new(1, 44100, 16)
175
+ assert_equal(w.stereo?, false)
176
+
177
+ w = WaveFile.open("examples/valid/sine-mono-8bit.wav")
178
+ assert_equal(w.stereo?, false)
179
+
180
+ w = WaveFile.new(2, 44100, 16)
181
+ assert_equal(w.stereo?, true)
182
+
183
+ w = WaveFile.new(4, 44100, 16)
184
+ assert_equal(w.stereo?, false)
185
+ end
186
+
187
+ def test_reverse
188
+ # Mono
189
+ w = WaveFile.new(:mono, 44100, 16)
190
+ w.sample_data = [1, 2, 3, 4, 5]
191
+ w.reverse
192
+ assert_equal(w.sample_data, [5, 4, 3, 2, 1])
193
+
194
+ # Stereo
195
+ w = WaveFile.new(:stereo, 44100, 16)
196
+ w.sample_data = [[1, 9], [2, 8], [3, 7], [4, 6], [5, 5]]
197
+ w.reverse
198
+ assert_equal(w.sample_data, [[5, 5], [4, 6], [3, 7], [2, 8], [1, 9]])
199
+ end
200
+
201
+ def test_duration()
202
+ sample_rate = 44100
203
+
204
+ [30001, 22050, 44100].each {|bits_per_sample|
205
+ [8, 16].each {|bits_per_sample|
206
+ [:mono, :stereo].each {|num_channels|
207
+ w = WaveFile.new(num_channels, sample_rate, bits_per_sample)
208
+
209
+ w.sample_data = []
210
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 0, :milliseconds => 0})
211
+ w.sample_data = get_duration_test_samples(num_channels, (sample_rate.to_f / 1000.0).floor)
212
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 0, :milliseconds => 0})
213
+ w.sample_data = get_duration_test_samples(num_channels, (sample_rate.to_f / 1000.0).ceil)
214
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 0, :milliseconds => 1})
215
+ w.sample_data = get_duration_test_samples(num_channels, sample_rate / 2)
216
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 0, :milliseconds => 500})
217
+ w.sample_data = get_duration_test_samples(num_channels, sample_rate - 1)
218
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 0, :milliseconds => 999})
219
+ w.sample_data = get_duration_test_samples(num_channels, sample_rate)
220
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 1, :milliseconds => 0})
221
+ w.sample_data = get_duration_test_samples(num_channels, sample_rate * 2)
222
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 2, :milliseconds => 0})
223
+ w.sample_data = get_duration_test_samples(num_channels, (sample_rate / 2) * 3)
224
+ assert_equal(w.duration, {:hours => 0, :minutes => 0, :seconds => 1, :milliseconds => 500})
225
+
226
+ # These tests currently take too long to run...
227
+ #w.sample_data = [].fill(0.0, 0, sample_rate * 60)
228
+ #assert_equal(w.duration, {:hours => 0, :minutes => 1, :seconds => 0, :milliseconds => 0})
229
+ #w.sample_data = [].fill(0.0, 0, sample_rate * 60 * 60)
230
+ #assert_equal(w.duration, {:hours => 1, :minutes => 0, :seconds => 0, :milliseconds => 0})
231
+ }
232
+ }
233
+ }
234
+ end
235
+
236
+ def get_duration_test_samples(num_channels, num_samples)
237
+ if num_channels == :mono || num_channels == 1
238
+ return [].fill(0.0, 0, num_samples)
239
+ elsif num_channels == :stereo || num_channels == 2
240
+ return [].fill([0.0, 0.0], 0, num_samples)
241
+ else
242
+ return "error"
243
+ end
244
+ end
245
+
246
+ def test_bits_per_sample=()
247
+ # Set bits_per_sample to invalid value (non-8 or non-16)
248
+ w = WaveFile.open("examples/valid/sine-mono-8bit.wav")
249
+ assert_raise(StandardError) { w.bits_per_sample = 20 }
250
+ w = WaveFile.new(:mono, 44100, 16)
251
+ assert_raise(StandardError) { w.bits_per_sample = 4 }
252
+
253
+ w_before = WaveFile.open("examples/valid/sine-mono-8bit.wav")
254
+ w_after = WaveFile.open("examples/valid/sine-mono-8bit.wav")
255
+ w_after.bits_per_sample = 8
256
+ assert_equal(w_before.sample_data, w_after.sample_data)
257
+
258
+ w_before = WaveFile.open("examples/valid/sine-stereo-8bit.wav")
259
+ w_after = WaveFile.open("examples/valid/sine-stereo-8bit.wav")
260
+ w_after.bits_per_sample = 8
261
+ assert_equal(w_before.sample_data, w_after.sample_data)
262
+
263
+ # Open mono 16 bit file, change to 16 bit, still the same
264
+ # Open stereo 16 bit file, change to 16 bit, still the same
265
+
266
+ # Open mono 8 bit file, convert to 16 bit
267
+ w = WaveFile.new(:mono, 44100, 8)
268
+ w.sample_data = [0, 32, 64, 96, 128, 160, 192, 223, 255]
269
+ w.bits_per_sample = 16
270
+ assert_equal(w.sample_data, [-32768, -24576, -16384, -8192, 0, 8256, 16513, 24511, 32767])
271
+
272
+ # Open stereo 8 bit file, convert to 16 bit
273
+ w = WaveFile.new(:stereo, 44100, 8)
274
+ w.sample_data = [[0, 255], [32, 223], [64, 192], [96, 160], [128, 128],
275
+ [160, 96], [192, 64], [223, 32], [255, 0]]
276
+ w.bits_per_sample = 16
277
+ assert_equal(w.sample_data, [[-32768, 32767], [-24576, 24511], [-16384, 16513], [-8192, 8256], [0, 0],
278
+ [8256, -8192], [16513, -16384], [24511, -24576], [32767, -32768]])
279
+
280
+ # Open mono 16 bit file, convert to 8 bit
281
+ w = WaveFile.new(:mono, 44100, 16)
282
+ w.sample_data = [-32768, -24576, -16384, -8192, 0, 8256, 16513, 24511, 32767]
283
+ w.bits_per_sample = 8
284
+ assert_equal(w.sample_data, [0, 32, 64, 96, 128, 160, 192, 223, 255])
285
+
286
+ # Open stereo 16 bit file, convert to 8 bit, conversion successful
287
+ w = WaveFile.new(:stereo, 44100, 16)
288
+ w.sample_data = [[-32768, 32767], [-24576, 24511], [-16384, 16513], [-8192, 8256], [0, 0],
289
+ [8256, -8192], [16513, -16384], [24511, -24576], [32767, -32768]]
290
+ w.bits_per_sample = 8
291
+ assert_equal(w.sample_data, [[0, 255], [32, 223], [64, 192], [96, 160], [128, 128],
292
+ [160, 96], [192, 64], [223, 32], [255, 0]])
293
+
294
+ # Open 8 bit mono, convert to 16 bit, back to 8 bit.
295
+ w_before = WaveFile.open("examples/valid/sine-mono-8bit.wav")
296
+ w_after = WaveFile.open("examples/valid/sine-mono-8bit.wav")
297
+ w_after.bits_per_sample = 16
298
+ assert_not_equal(w_before.sample_data, w_after.sample_data)
299
+ w_after.bits_per_sample = 8
300
+ assert_equal(w_before.sample_data, w_after.sample_data)
301
+
302
+ # Open 8 bit stereo, convert to 16 bit, back to 8 bit.
303
+ w_before = WaveFile.open("examples/valid/sine-stereo-8bit.wav")
304
+ w_after = WaveFile.open("examples/valid/sine-stereo-8bit.wav")
305
+ w_after.bits_per_sample = 16
306
+ assert_not_equal(w_before.sample_data, w_after.sample_data)
307
+ w_after.bits_per_sample = 8
308
+ assert_equal(w_before.sample_data, w_after.sample_data)
309
+
310
+ # Open 16 bit mono, convert to 8 bit, back to 16 bit.
311
+ # Open 16 bit stereo, convert to 8 bit, back to 16 bit.
312
+ end
313
+
314
+ def test_num_channels=()
315
+ w = WaveFile.new(:mono, 44100, 16)
316
+ w.sample_data = [-32768, -24576, -16384, -8192, 0, 8256, 16513, 24511, 32767]
317
+ w.num_channels = 2
318
+ assert_equal(w.sample_data, [[-32768, -32768], [-24576, -24576], [-16384, -16384], [-8192, -8192], [0, 0],
319
+ [8256, 8256], [16513, 16513], [24511, 24511], [32767, 32767]])
320
+
321
+ w = WaveFile.new(:mono, 44100, 16)
322
+ w.sample_data = [-32768, -24576, -16384, -8192, 0, 8256, 16513, 24511, 32767]
323
+ w.num_channels = 3
324
+ assert_equal(w.sample_data, [[-32768, -32768, -32768], [-24576, -24576, -24576], [-16384, -16384, -16384], [-8192, -8192, -8192], [0, 0, 0],
325
+ [8256, 8256, 8256], [16513, 16513, 16513], [24511, 24511, 24511], [32767, 32767, 32767]])
326
+
327
+ w = WaveFile.new(:stereo, 44100, 16)
328
+ w.sample_data = [[-32768, -32768], [-24576, -24576], [-16384, -16384], [-8192, -8192], [0, 0],
329
+ [8256, 8256], [16513, 16513], [24511, 24511], [32767, 32767]]
330
+ w.num_channels = 1
331
+ assert_equal(w.sample_data, [-32768, -24576, -16384, -8192, 0, 8256, 16513, 24511, 32767])
332
+
333
+ w = WaveFile.new(3, 44100, 16)
334
+ w.sample_data = [[-32768, -32768, -32768], [-24576, -24576, -24576], [-16384, -16384, -16384], [-8192, -8192, -8192], [0, 0, 0],
335
+ [8256, 8256, 8256], [16513, 16513, 16513], [24511, 24511, 24511], [32767, 32767, 32767]]
336
+ w.num_channels = 1
337
+ assert_equal(w.sample_data, [-32768, -24576, -16384, -8192, 0, 8256, 16513, 24511, 32767])
338
+ end
339
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wavefile
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Strait
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-12 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: joel.strait at gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - LICENSE
26
+ - README.markdown
27
+ - lib/wavefile.rb
28
+ - test/wavefile_test.rb
29
+ has_rdoc: true
30
+ homepage: http://www.joelstrait.com/
31
+ licenses: []
32
+
33
+ post_install_message:
34
+ rdoc_options: []
35
+
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ version:
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ requirements: []
51
+
52
+ rubyforge_project:
53
+ rubygems_version: 1.3.5
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: A class for reading and writing Wave sound files (*.wav)
57
+ test_files: []
58
+