wavefile 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.markdown +29 -56
- data/Rakefile +6 -0
- data/lib/wavefile.rb +28 -452
- data/lib/wavefile/buffer.rb +147 -0
- data/lib/wavefile/format.rb +69 -0
- data/lib/wavefile/info.rb +53 -0
- data/lib/wavefile/reader.rb +296 -0
- data/lib/wavefile/writer.rb +128 -0
- data/test/buffer_test.rb +121 -0
- data/test/fixtures/actual_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/expected_output/no_samples.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_16_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_32_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_8_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_mono_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/expected_output/valid_stereo_16_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_stereo_32_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_stereo_8_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_tri_16_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_tri_32_44100.wav +0 -0
- data/test/fixtures/expected_output/valid_tri_8_44100.wav +0 -0
- data/test/fixtures/invalid/README.markdown +10 -0
- data/test/fixtures/invalid/bad_riff_header.wav +1 -0
- data/test/fixtures/invalid/bad_wavefile_format.wav +0 -0
- data/test/fixtures/invalid/empty.wav +0 -0
- data/test/fixtures/invalid/empty_format_chunk.wav +0 -0
- data/test/fixtures/invalid/incomplete_riff_header.wav +1 -0
- data/test/fixtures/invalid/insufficient_format_chunk.wav +0 -0
- data/test/fixtures/invalid/no_data_chunk.wav +0 -0
- data/test/fixtures/invalid/no_format_chunk.wav +0 -0
- data/test/fixtures/unsupported/README.markdown +6 -0
- data/test/fixtures/unsupported/bad_audio_format.wav +0 -0
- data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
- data/test/fixtures/unsupported/bad_sample_rate.wav +0 -0
- data/test/fixtures/unsupported/unsupported_audio_format.wav +0 -0
- data/test/fixtures/unsupported/unsupported_bits_per_sample.wav +0 -0
- data/test/fixtures/valid/README.markdown +3 -0
- data/test/fixtures/valid/valid_mono_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_mono_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_mono_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_mono_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/valid/valid_stereo_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_stereo_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_stereo_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_tri_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_tri_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_tri_8_44100.wav +0 -0
- data/test/format_test.rb +105 -0
- data/test/info_test.rb +60 -0
- data/test/reader_test.rb +222 -0
- data/test/wavefile_io_test_helper.rb +47 -0
- data/test/writer_test.rb +118 -0
- metadata +72 -33
- data/test/wavefile_test.rb +0 -339
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -1,79 +1,52 @@
|
|
1
|
-
A Ruby gem for reading and writing
|
1
|
+
A pure Ruby gem for reading and writing sound files in Wave format (*.wav). You can use this gem to create Ruby programs that produce audio, such as [drum machine](http://beatsdrummachine.com).
|
2
2
|
|
3
|
-
#
|
4
|
-
|
5
|
-
First, install the WaveFile gem...
|
6
|
-
|
7
|
-
gem install wavefile
|
3
|
+
# What's New in v0.4.0?
|
8
4
|
|
9
|
-
|
5
|
+
This version is a re-write with a completely new, much improved API. (The old API has been removed). Some improvements due to the new API include:
|
10
6
|
|
11
|
-
|
7
|
+
* Reduced memory consumption, due to not having to load the entire file into memory. In practice, this allows the gem to read/write files that previously would have been prohibitively large.
|
8
|
+
* Better performance for large files, for the same reason as above.
|
9
|
+
* Ability to progressively append data to the end of a file, instead of writing the entire file at once.
|
10
|
+
* Ability to easily read and write data in an arbitrary format, regardless of the file's native format. For example, you can transparently read data out of a 16-bit stereo file as 8-bit mono.
|
11
|
+
* Automatic file management, similar to how IO.open() works. It's easy to continually read the sample data from a file, passing each buffer to a block, and have the file automatically close when there is no more data left.
|
12
12
|
|
13
|
-
|
13
|
+
Other improvements include:
|
14
14
|
|
15
|
-
|
15
|
+
* Ability to query format metadata of files without opening them, even for formats that this gem can't read or write.
|
16
|
+
* Support for reading and writing 32-bit PCM files.
|
16
17
|
|
17
|
-
|
18
|
-
samples = w.sample_data
|
18
|
+
However, reading or writing data as floating point (i.e. values between -1.0 and 1.0) won't be supported in v0.4.0 to keep the scope in check. It might be re-added in the future.
|
19
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
20
|
|
22
|
-
|
23
|
-
[0, 128, 255, 128]
|
24
|
-
|
25
|
-
# Stereo example
|
26
|
-
[[0, 255], [128, 128], [255, 0], [128, 128]]
|
21
|
+
# Compatibility
|
27
22
|
|
28
|
-
|
23
|
+
WaveFile has been tested with these Ruby versions, and appears to be compatible with them:
|
29
24
|
|
30
|
-
|
25
|
+
* MRI 1.9.3, 1.9.2, 1.9.1, 1.8.7
|
26
|
+
* JRuby 1.6.5
|
27
|
+
* Rubinius 1.2.4
|
28
|
+
* MacRuby 0.10
|
31
29
|
|
32
|
-
|
30
|
+
If you find any compatibility issues, please let me know by opening a GitHub issue.
|
33
31
|
|
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
32
|
|
41
|
-
|
33
|
+
# Dependencies
|
42
34
|
|
43
|
-
|
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
|
35
|
+
WaveFile has no external dependencies. It is written in pure Ruby, and is entirely self-contained.
|
53
36
|
|
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
37
|
|
56
|
-
|
57
|
-
w.num_channels = :stereo // Equivalent to line above
|
58
|
-
w.sample_rate = 22050
|
59
|
-
w.bits_per_sample = 16
|
38
|
+
# Installation
|
60
39
|
|
61
|
-
|
40
|
+
First, install the WaveFile gem from rubygems.org:
|
62
41
|
|
63
|
-
|
42
|
+
gem install wavefile
|
64
43
|
|
65
|
-
|
44
|
+
...and include it in your Ruby program:
|
66
45
|
|
67
|
-
|
68
|
-
# sample_rate,
|
69
|
-
# bits_per_sample
|
70
|
-
w.sample_data = <array of samples goes here>
|
71
|
-
w.save("myfile.wav")
|
46
|
+
require 'wavefile'
|
72
47
|
|
73
|
-
|
48
|
+
Note that if you're installing the gem into the default Ruby that comes pre-installed on MacOS (as opposed to a Ruby installed via [RVM](http://beginrescueend.com/)), you should used `sudo gem install wavefile`. Otherwise you might run into a file permission error.
|
74
49
|
|
75
|
-
|
50
|
+
# Usage
|
76
51
|
|
77
|
-
|
78
|
-
w.reverse()
|
79
|
-
w.save("myfile_reversed.wav")
|
52
|
+
For usage instructions with examples, check out the [wiki](https://github.com/jstrait/wavefile/wiki).
|
data/Rakefile
ADDED
data/lib/wavefile.rb
CHANGED
@@ -1,456 +1,32 @@
|
|
1
|
-
|
1
|
+
require 'wavefile/buffer'
|
2
|
+
require 'wavefile/format'
|
3
|
+
require 'wavefile/info'
|
4
|
+
require 'wavefile/reader'
|
5
|
+
require 'wavefile/writer'
|
2
6
|
|
3
|
-
|
4
|
-
|
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).
|
7
|
+
module WaveFile
|
8
|
+
VERSION = "0.4.0"
|
18
9
|
|
19
|
-
|
20
|
-
|
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
|
10
|
+
WAVEFILE_FORMAT_CODE = "WAVE"
|
11
|
+
FORMAT_CHUNK_BYTE_LENGTH = 16
|
53
12
|
PCM = 1
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
13
|
+
CHUNK_IDS = {:riff => "RIFF",
|
14
|
+
:format => "fmt ",
|
15
|
+
:data => "data",
|
16
|
+
:fact => "fact",
|
17
|
+
:silence => "slnt",
|
18
|
+
:cue => "cue ",
|
19
|
+
:playlist => "plst",
|
20
|
+
:list => "list",
|
21
|
+
:label => "labl",
|
22
|
+
:labeled_text => "ltxt",
|
23
|
+
:note => "note",
|
24
|
+
:sample => "smpl",
|
25
|
+
:instrument => "inst" }
|
26
|
+
|
27
|
+
PACK_CODES = {8 => "C*", 16 => "s*", 32 => "l*"}
|
28
|
+
|
29
|
+
UNSIGNED_INT_16 = "v"
|
30
|
+
UNSIGNED_INT_32 = "V"
|
31
|
+
end
|
397
32
|
|
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
|