jstrait-wavefile 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/LICENSE +24 -0
  2. data/README +34 -0
  3. data/lib/wavefile.rb +227 -0
  4. 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
+