jstrait-wavefile 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +24 -0
- data/README +34 -0
- data/lib/wavefile.rb +227 -0
- metadata +55 -0
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
== WaveFile
|
2
|
+
|
3
|
+
# Copyright (c) 2009 Joel Strait
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person
|
6
|
+
# obtaining a copy of this software and associated documentation
|
7
|
+
# files (the "Software"), to deal in the Software without
|
8
|
+
# restriction, including without limitation the rights to use,
|
9
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the
|
11
|
+
# Software is furnished to do so, subject to the following
|
12
|
+
# conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
19
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
21
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
22
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
23
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
A class for reading and writing *.wav files.
|
2
|
+
|
3
|
+
First, install the WaveFile gem...
|
4
|
+
|
5
|
+
sudo gem install jstrait-wavefile -s http://gems.github.com
|
6
|
+
|
7
|
+
...and include it in your Ruby program:
|
8
|
+
|
9
|
+
require 'WaveFile'
|
10
|
+
|
11
|
+
To open a wave file and get the raw sample data:
|
12
|
+
|
13
|
+
w = WaveFile.open("myfile.wav")
|
14
|
+
samples = w.sample_data
|
15
|
+
|
16
|
+
You can also get the sample data in a normalized form, with each sample between -1.0 and 1.0:
|
17
|
+
|
18
|
+
normalized_samples = w.normalized_sample_data
|
19
|
+
|
20
|
+
You can get basic metadata:
|
21
|
+
|
22
|
+
w.num_channels # 1 for mono, 2 for stereo
|
23
|
+
w.sample_rate # 11025, 22050, 44100, etc.
|
24
|
+
w.bits_per_sample # 8 or 16
|
25
|
+
|
26
|
+
To create and save a new wave file:
|
27
|
+
|
28
|
+
w = WaveFile.new(1, 44100, 16) # num_channels,
|
29
|
+
# sample_rate,
|
30
|
+
# bits_per_sample
|
31
|
+
w.sample_data = <array of samples goes here>
|
32
|
+
w.save("myfile.wav")
|
33
|
+
|
34
|
+
When calling the sample_data=() method, the passed in array can contain either raw samples or normalized samples. If the first item in the array is a Float, the entire array is assumed to be normalized. Normalized samples are automatically converted into raw samples when saving.
|
data/lib/wavefile.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
WAV File Specification
|
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).
|
18
|
+
|
19
|
+
The "WAVE" format consists of two subchunks: "fmt " and "data":
|
20
|
+
The "fmt " subchunk describes the sound data's format:
|
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
|
+
SUB_CHUNK1_ID = "fmt "
|
52
|
+
SUB_CHUNK1_SIZE = 16
|
53
|
+
AUDIO_FORMAT = 1
|
54
|
+
SUB_CHUNK2_ID = "data"
|
55
|
+
HEADER_SIZE = 36
|
56
|
+
|
57
|
+
def initialize(num_channels, sample_rate, bits_per_sample, sample_data = [])
|
58
|
+
@num_channels = num_channels
|
59
|
+
@sample_rate = sample_rate
|
60
|
+
@bits_per_sample = bits_per_sample
|
61
|
+
@sample_data = sample_data
|
62
|
+
|
63
|
+
@byte_rate = sample_rate * num_channels * (bits_per_sample / 8)
|
64
|
+
@block_align = num_channels * (bits_per_sample / 8)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.open(path)
|
68
|
+
header = read_header(path)
|
69
|
+
|
70
|
+
if valid_header?(header)
|
71
|
+
sample_data = read_sample_data(path,
|
72
|
+
header[:sub_chunk1_size],
|
73
|
+
header[:bits_per_sample],
|
74
|
+
header[:sub_chunk2_size])
|
75
|
+
|
76
|
+
wave_file = self.new(header[:num_channels],
|
77
|
+
header[:sample_rate],
|
78
|
+
header[:bits_per_sample],
|
79
|
+
sample_data)
|
80
|
+
else
|
81
|
+
raise StandardError, "#{path} is not a valid wave file"
|
82
|
+
end
|
83
|
+
|
84
|
+
return wave_file
|
85
|
+
end
|
86
|
+
|
87
|
+
def save(path)
|
88
|
+
# All numeric values should be saved in little-endian format
|
89
|
+
|
90
|
+
sample_data_size = @sample_data.length * @num_channels * (@bits_per_sample / 8)
|
91
|
+
|
92
|
+
# Write the header
|
93
|
+
file_contents = CHUNK_ID
|
94
|
+
file_contents += [HEADER_SIZE + sample_data_size].pack("V")
|
95
|
+
file_contents += FORMAT
|
96
|
+
file_contents += SUB_CHUNK1_ID
|
97
|
+
file_contents += [SUB_CHUNK1_SIZE].pack("V")
|
98
|
+
file_contents += [AUDIO_FORMAT].pack("v")
|
99
|
+
file_contents += [@num_channels].pack("v")
|
100
|
+
file_contents += [@sample_rate].pack("V")
|
101
|
+
file_contents += [@byte_rate].pack("V")
|
102
|
+
file_contents += [@block_align].pack("v")
|
103
|
+
file_contents += [@bits_per_sample].pack("v")
|
104
|
+
file_contents += SUB_CHUNK2_ID
|
105
|
+
file_contents += [sample_data_size].pack("V")
|
106
|
+
|
107
|
+
# Write the sample data
|
108
|
+
if @bits_per_sample == 8
|
109
|
+
file_contents += @sample_data.pack("C*")
|
110
|
+
elsif @bits_per_sample == 16
|
111
|
+
file_contents += @sample_data.pack("s*")
|
112
|
+
else
|
113
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
114
|
+
end
|
115
|
+
|
116
|
+
file = File.open(path, "w")
|
117
|
+
file.syswrite(file_contents)
|
118
|
+
file.close
|
119
|
+
end
|
120
|
+
|
121
|
+
def sample_data()
|
122
|
+
return @sample_data
|
123
|
+
end
|
124
|
+
|
125
|
+
def normalized_sample_data()
|
126
|
+
if @bits_per_sample == 8
|
127
|
+
normalized_sample_data = @sample_data.map {|sample| (sample.to_f / 511.0) * 2.0 }
|
128
|
+
elsif @bits_per_sample == 16
|
129
|
+
normalized_sample_data = @sample_data.map {|sample| (sample.to_f / 32767.0) }
|
130
|
+
else
|
131
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
132
|
+
end
|
133
|
+
|
134
|
+
return normalized_sample_data
|
135
|
+
end
|
136
|
+
|
137
|
+
def sample_data=(sample_data)
|
138
|
+
if sample_data.length > 0 && sample_data[0].class == Float
|
139
|
+
if @bits_per_sample == 8
|
140
|
+
# Samples in 8-bit wave files are stored as a unsigned byte
|
141
|
+
# Effective values are 0 to 255
|
142
|
+
@sample_data = sample_data.map {|sample| ((sample * 127.0).to_i) + 127 }
|
143
|
+
elsif @bits_per_sample == 16
|
144
|
+
# Samples in 16-bit wave files are stored as a signed little-endian short
|
145
|
+
# Effective values are -32768 to 32767
|
146
|
+
@sample_data = sample_data.map {|sample| (sample * 32767.0).to_i }
|
147
|
+
else
|
148
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
149
|
+
end
|
150
|
+
else
|
151
|
+
@sample_data = sample_data
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
attr_reader :num_channels, :sample_rate, :bits_per_sample, :byte_rate, :block_align
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def self.read_header(path)
|
160
|
+
header = {}
|
161
|
+
file = File.open(path, "rb")
|
162
|
+
|
163
|
+
begin
|
164
|
+
# Read RIFF header
|
165
|
+
riff_header = file.sysread(12).unpack("a4Va4")
|
166
|
+
header[:chunk_id] = riff_header[0]
|
167
|
+
header[:chunk_size] = riff_header[1]
|
168
|
+
header[:format] = riff_header[2]
|
169
|
+
|
170
|
+
# Read format subchunk
|
171
|
+
header[:sub_chunk1_id] = file.sysread(4)
|
172
|
+
header[:sub_chunk1_size] = file.sysread(4).unpack("V")[0]
|
173
|
+
format_subchunk_str = file.sysread(header[:sub_chunk1_size])
|
174
|
+
format_subchunk = format_subchunk_str.unpack("vvVVvv") # Any extra parameters are ignored
|
175
|
+
header[:audio_format] = format_subchunk[0]
|
176
|
+
header[:num_channels] = format_subchunk[1]
|
177
|
+
header[:sample_rate] = format_subchunk[2]
|
178
|
+
header[:byte_rate] = format_subchunk[3]
|
179
|
+
header[:block_align] = format_subchunk[4]
|
180
|
+
header[:bits_per_sample] = format_subchunk[5]
|
181
|
+
|
182
|
+
# Read data subchunk
|
183
|
+
header[:sub_chunk2_id] = file.sysread(4)
|
184
|
+
header[:sub_chunk2_size] = file.sysread(4).unpack("V")[0]
|
185
|
+
rescue EOFError
|
186
|
+
file.close()
|
187
|
+
end
|
188
|
+
|
189
|
+
return header
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.valid_header?(header)
|
193
|
+
valid_bits_per_sample = header[:bits_per_sample] == 8 ||
|
194
|
+
header[:bits_per_sample] == 16
|
195
|
+
|
196
|
+
valid_num_channels = header[:num_channels] == 1 # Only mono files supported for now
|
197
|
+
|
198
|
+
return valid_bits_per_sample &&
|
199
|
+
valid_num_channels &&
|
200
|
+
header[:chunk_id] == CHUNK_ID &&
|
201
|
+
header[:format] == FORMAT &&
|
202
|
+
header[:sub_chunk1_id] == SUB_CHUNK1_ID &&
|
203
|
+
header[:audio_format] == AUDIO_FORMAT &&
|
204
|
+
header[:sub_chunk2_id] == SUB_CHUNK2_ID
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.read_sample_data(path, sub_chunk1_size, bits_per_sample, sample_data_size)
|
208
|
+
offset = 20 + sub_chunk1_size + 8
|
209
|
+
file = File.open(path, "rb")
|
210
|
+
|
211
|
+
begin
|
212
|
+
data = file.sysread(offset)
|
213
|
+
|
214
|
+
if(bits_per_sample == 8)
|
215
|
+
data = file.sysread(sample_data_size).unpack("C*")
|
216
|
+
elsif(bits_per_sample == 16)
|
217
|
+
data = file.sysread(sample_data_size).unpack("s*")
|
218
|
+
else
|
219
|
+
data = []
|
220
|
+
end
|
221
|
+
rescue EOFError
|
222
|
+
file.close()
|
223
|
+
end
|
224
|
+
|
225
|
+
return data
|
226
|
+
end
|
227
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jstrait-wavefile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joel Strait
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-16 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: joel.strait at gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- LICENSE
|
26
|
+
- README
|
27
|
+
- lib/wavefile.rb
|
28
|
+
has_rdoc: false
|
29
|
+
homepage: http://www.joelstrait.com/
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: "0"
|
40
|
+
version:
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
requirements: []
|
48
|
+
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.2.0
|
51
|
+
signing_key:
|
52
|
+
specification_version: 2
|
53
|
+
summary: A class for reading and writing Wave files (*.wav)
|
54
|
+
test_files: []
|
55
|
+
|