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.
- data/README.markdown +25 -0
- data/lib/wavefile.rb +225 -89
- data/test/wavefile_test.rb +339 -0
- metadata +4 -2
data/README.markdown
CHANGED
@@ -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
|
|
data/lib/wavefile.rb
CHANGED
@@ -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
|
-
|
51
|
+
FORMAT_CHUNK_ID = "fmt "
|
52
52
|
SUB_CHUNK1_SIZE = 16
|
53
|
-
|
54
|
-
|
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
|
-
|
74
|
+
file = File.open(path, "rb")
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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 +=
|
113
|
+
file_contents += FORMAT_CHUNK_ID
|
104
114
|
file_contents += [SUB_CHUNK1_SIZE].pack("V")
|
105
|
-
file_contents += [
|
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 +=
|
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
|
-
|
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(
|
357
|
+
def self.read_header(file)
|
241
358
|
header = {}
|
242
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
395
|
+
return chunk_id, chunk_size
|
271
396
|
end
|
272
|
-
|
273
|
-
def self.
|
274
|
-
|
275
|
-
|
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
|
-
|
425
|
+
unless header[:sub_chunk2_id] == DATA_CHUNK_ID
|
426
|
+
errors << "Unsupported chunk id: '#{header[:sub_chunk2_id]}'"
|
427
|
+
end
|
278
428
|
|
279
|
-
return
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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.
|
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.
|
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)
|