jstrait-wavefile 0.2.1 → 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 (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)