wavefile 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
+