jstrait-wavefile 0.0.1
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 +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
|
+
|