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.
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
+