wavefile 0.3.0 → 0.4.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/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
|