jstrait-wavefile 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.markdown +25 -0
  2. data/lib/wavefile.rb +225 -89
  3. data/test/wavefile_test.rb +339 -0
  4. metadata +4 -2
@@ -36,6 +36,31 @@ You can get basic metadata:
36
36
  w.stereo? # Alias for num_channels == 2
37
37
  w.sample_rate # 11025, 22050, 44100, etc.
38
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")
39
64
 
40
65
  To create and save a new wave file:
41
66
 
@@ -48,10 +48,10 @@ The "data" subchunk contains the size of the data and the actual sound:
48
48
  class WaveFile
49
49
  CHUNK_ID = "RIFF"
50
50
  FORMAT = "WAVE"
51
- SUB_CHUNK1_ID = "fmt "
51
+ FORMAT_CHUNK_ID = "fmt "
52
52
  SUB_CHUNK1_SIZE = 16
53
- AUDIO_FORMAT = 1
54
- SUB_CHUNK2_ID = "data"
53
+ PCM = 1
54
+ DATA_CHUNK_ID = "data"
55
55
  HEADER_SIZE = 36
56
56
 
57
57
  def initialize(num_channels, sample_rate, bits_per_sample, sample_data = [])
@@ -71,21 +71,31 @@ class WaveFile
71
71
  end
72
72
 
73
73
  def self.open(path)
74
- header = read_header(path)
74
+ file = File.open(path, "rb")
75
75
 
76
- if valid_header?(header)
77
- sample_data = read_sample_data(path,
78
- header[:sub_chunk1_size],
79
- header[:num_channels],
80
- header[:bits_per_sample],
81
- header[:sub_chunk2_size])
82
-
83
- wave_file = self.new(header[:num_channels],
84
- header[:sample_rate],
85
- header[:bits_per_sample],
86
- sample_data)
87
- else
88
- raise StandardError, "#{path} is either not a valid wave file, or is in an unsupported format"
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()
89
99
  end
90
100
 
91
101
  return wave_file
@@ -100,15 +110,15 @@ class WaveFile
100
110
  file_contents = CHUNK_ID
101
111
  file_contents += [HEADER_SIZE + sample_data_size].pack("V")
102
112
  file_contents += FORMAT
103
- file_contents += SUB_CHUNK1_ID
113
+ file_contents += FORMAT_CHUNK_ID
104
114
  file_contents += [SUB_CHUNK1_SIZE].pack("V")
105
- file_contents += [AUDIO_FORMAT].pack("v")
115
+ file_contents += [PCM].pack("v")
106
116
  file_contents += [@num_channels].pack("v")
107
117
  file_contents += [@sample_rate].pack("V")
108
118
  file_contents += [@byte_rate].pack("V")
109
119
  file_contents += [@block_align].pack("v")
110
120
  file_contents += [@bits_per_sample].pack("v")
111
- file_contents += SUB_CHUNK2_ID
121
+ file_contents += DATA_CHUNK_ID
112
122
  file_contents += [sample_data_size].pack("V")
113
123
 
114
124
  # Write the sample data
@@ -140,7 +150,7 @@ class WaveFile
140
150
  return @sample_data
141
151
  end
142
152
 
143
- def normalized_sample_data()
153
+ def normalized_sample_data()
144
154
  if @bits_per_sample == 8
145
155
  min_value = 128.0
146
156
  max_value = 127.0
@@ -232,87 +242,213 @@ class WaveFile
232
242
  def reverse()
233
243
  sample_data.reverse!()
234
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
235
339
 
236
- attr_reader :num_channels, :sample_rate, :bits_per_sample, :byte_rate, :block_align
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
237
354
 
238
355
  private
239
356
 
240
- def self.read_header(path)
357
+ def self.read_header(file)
241
358
  header = {}
242
- file = File.open(path, "rb")
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
243
382
 
244
- begin
245
- # Read RIFF header
246
- riff_header = file.sysread(12).unpack("a4Va4")
247
- header[:chunk_id] = riff_header[0]
248
- header[:chunk_size] = riff_header[1]
249
- header[:format] = riff_header[2]
250
-
251
- # Read format subchunk
252
- header[:sub_chunk1_id] = file.sysread(4)
253
- header[:sub_chunk1_size] = file.sysread(4).unpack("V")[0]
254
- format_subchunk_str = file.sysread(header[:sub_chunk1_size])
255
- format_subchunk = format_subchunk_str.unpack("vvVVvv") # Any extra parameters are ignored
256
- header[:audio_format] = format_subchunk[0]
257
- header[:num_channels] = format_subchunk[1]
258
- header[:sample_rate] = format_subchunk[2]
259
- header[:byte_rate] = format_subchunk[3]
260
- header[:block_align] = format_subchunk[4]
261
- header[:bits_per_sample] = format_subchunk[5]
262
-
263
- # Read data subchunk
264
- header[:sub_chunk2_id] = file.sysread(4)
265
- header[:sub_chunk2_size] = file.sysread(4).unpack("V")[0]
266
- rescue EOFError
267
- file.close()
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]
268
393
  end
269
394
 
270
- return header
395
+ return chunk_id, chunk_size
271
396
  end
272
-
273
- def self.valid_header?(header)
274
- valid_bits_per_sample = header[:bits_per_sample] == 8 ||
275
- header[:bits_per_sample] == 16
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
276
424
 
277
- valid_num_channels = (1..65535) === header[:num_channels]
425
+ unless header[:sub_chunk2_id] == DATA_CHUNK_ID
426
+ errors << "Unsupported chunk id: '#{header[:sub_chunk2_id]}'"
427
+ end
278
428
 
279
- return valid_bits_per_sample &&
280
- valid_num_channels &&
281
- header[:chunk_id] == CHUNK_ID &&
282
- header[:format] == FORMAT &&
283
- header[:sub_chunk1_id] == SUB_CHUNK1_ID &&
284
- header[:audio_format] == AUDIO_FORMAT &&
285
- header[:sub_chunk2_id] == SUB_CHUNK2_ID
429
+ return errors
286
430
  end
287
431
 
288
- def self.read_sample_data(path, sub_chunk1_size, num_channels, bits_per_sample, sample_data_size)
289
- offset = 20 + sub_chunk1_size + 8
290
- file = File.open(path, "rb")
291
-
292
- begin
293
- data = file.sysread(offset)
294
-
295
- if(bits_per_sample == 8)
296
- data = file.sysread(sample_data_size).unpack("C*")
297
- elsif(bits_per_sample == 16)
298
- data = file.sysread(sample_data_size).unpack("s*")
299
- else
300
- data = []
301
- end
302
-
303
- if(num_channels > 1)
304
- multichannel_data = []
305
-
306
- i = 0
307
- while i < data.length
308
- multichannel_data << data[i...(num_channels + i)]
309
- i += num_channels
310
- end
311
-
312
- data = multichannel_data
313
- end
314
- rescue EOFError
315
- file.close()
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
316
452
  end
317
453
 
318
454
  return data
@@ -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 CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jstrait-wavefile
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Strait
@@ -25,8 +25,10 @@ files:
25
25
  - LICENSE
26
26
  - README.markdown
27
27
  - lib/wavefile.rb
28
+ - test/wavefile_test.rb
28
29
  has_rdoc: false
29
30
  homepage: http://www.joelstrait.com/
31
+ licenses:
30
32
  post_install_message:
31
33
  rdoc_options: []
32
34
 
@@ -47,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
49
  requirements: []
48
50
 
49
51
  rubyforge_project:
50
- rubygems_version: 1.2.0
52
+ rubygems_version: 1.3.5
51
53
  signing_key:
52
54
  specification_version: 2
53
55
  summary: A class for reading and writing Wave files (*.wav)