nu_wav 0.1.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 +20 -0
- data/README.rdoc +21 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/lib/nu_wav.rb +551 -0
- data/test/helper.rb +9 -0
- data/test/test_nu_wav.rb +46 -0
- metadata +72 -0
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2010 Andrew Kuklewicz (kookster)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
= nu_wav
|
|
2
|
+
|
|
3
|
+
NuWav is a pure Ruby audio WAVE file parser and writer.
|
|
4
|
+
|
|
5
|
+
It currently has support for basic WAVE files, Broadcast Wave Format (bext and mext chunks), and cart chunk.
|
|
6
|
+
|
|
7
|
+
Other chunks types in the WAVE are not yet supported.
|
|
8
|
+
|
|
9
|
+
== Note on Patches/Pull Requests
|
|
10
|
+
|
|
11
|
+
* Fork the project.
|
|
12
|
+
* Make your feature addition or bug fix.
|
|
13
|
+
* Add tests for it. This is important so I don't break it in a
|
|
14
|
+
future version unintentionally.
|
|
15
|
+
* Commit, do not mess with rakefile, version, or history.
|
|
16
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
17
|
+
* Send me a pull request. Bonus points for topic branches.
|
|
18
|
+
|
|
19
|
+
== Copyright
|
|
20
|
+
|
|
21
|
+
Copyright (c) 2010 Andrew Kuklewicz kookster. See LICENSE for details.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'jeweler'
|
|
6
|
+
Jeweler::Tasks.new do |gem|
|
|
7
|
+
gem.name = "nu_wav"
|
|
8
|
+
gem.summary = %Q{NuWav is a pure ruby audio WAV file parser and writer.}
|
|
9
|
+
gem.description = %Q{NuWav is a pure ruby audio WAV file parser and writer. It supports Broadcast Wave Format (BWF), inclluding MPEG audio data, and the public radio standard cart chunk.}
|
|
10
|
+
gem.email = "andrew@beginsinwonder.com"
|
|
11
|
+
gem.homepage = "http://github.com/kookster/nu_wav"
|
|
12
|
+
gem.authors = ["kookster"]
|
|
13
|
+
gem.add_dependency('ruby-mp3info', '>= 0.6.13')
|
|
14
|
+
gem.files.exclude ".document"
|
|
15
|
+
gem.files.exclude ".gitignore"
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
Jeweler::GemcutterTasks.new
|
|
19
|
+
rescue LoadError
|
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
require 'rake/testtask'
|
|
24
|
+
Rake::TestTask.new(:test) do |test|
|
|
25
|
+
test.libs << 'lib' << 'test'
|
|
26
|
+
test.pattern = 'test/**/test_*.rb'
|
|
27
|
+
test.verbose = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
require 'rcov/rcovtask'
|
|
32
|
+
Rcov::RcovTask.new do |test|
|
|
33
|
+
test.libs << 'test'
|
|
34
|
+
test.pattern = 'test/**/test_*.rb'
|
|
35
|
+
test.verbose = true
|
|
36
|
+
end
|
|
37
|
+
rescue LoadError
|
|
38
|
+
task :rcov do
|
|
39
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
task :test => :check_dependencies
|
|
44
|
+
|
|
45
|
+
task :default => :test
|
|
46
|
+
|
|
47
|
+
require 'rake/rdoctask'
|
|
48
|
+
Rake::RDocTask.new do |rdoc|
|
|
49
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
|
50
|
+
|
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
52
|
+
rdoc.title = "nu_wav #{version}"
|
|
53
|
+
rdoc.rdoc_files.include('README*')
|
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
55
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
data/lib/nu_wav.rb
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# http://www.prss.org/contentdepot/automation_specifications.cfm
|
|
2
|
+
# Bill Kelly <billk <at> cts.com> http://article.gmane.org/gmane.comp.lang.ruby.general/43110
|
|
3
|
+
|
|
4
|
+
require 'rubygems'
|
|
5
|
+
require 'mp3info'
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
module NuWav
|
|
9
|
+
|
|
10
|
+
DEBUG = false
|
|
11
|
+
|
|
12
|
+
PCM_COMPRESSION = 1
|
|
13
|
+
MPEG_COMPRESSION = 80
|
|
14
|
+
|
|
15
|
+
ACM_MPEG_LAYER1 = 1
|
|
16
|
+
ACM_MPEG_LAYER2 = 2
|
|
17
|
+
ACM_MPEG_LAYER3 = 4
|
|
18
|
+
|
|
19
|
+
ACM_LAYERS = [ACM_MPEG_LAYER1, ACM_MPEG_LAYER2, ACM_MPEG_LAYER3]
|
|
20
|
+
|
|
21
|
+
ACM_MPEG_STEREO = 1
|
|
22
|
+
ACM_MPEG_JOINTSTEREO = 2
|
|
23
|
+
ACM_MPEG_DUALCHANNEL = 4
|
|
24
|
+
ACM_MPEG_SINGLECHANNEL= 8
|
|
25
|
+
|
|
26
|
+
CHANNEL_MODES = {'Stereo'=>ACM_MPEG_STEREO, 'JStereo'=>ACM_MPEG_JOINTSTEREO, 'Dual Channel'=>ACM_MPEG_DUALCHANNEL, 'Single Channel'=>ACM_MPEG_SINGLECHANNEL}
|
|
27
|
+
|
|
28
|
+
CODING_HISTORY_MODE = {'Single Channel'=>'mono', 'Stereo'=>'stereo', 'Dual Channel'=>'dual-mono', 'JStereo'=>'joint-stereo'}
|
|
29
|
+
|
|
30
|
+
class NotRIFFFormat < StandardError; end
|
|
31
|
+
class NotWAVEFormat < StandardError; end
|
|
32
|
+
|
|
33
|
+
class WaveFile
|
|
34
|
+
|
|
35
|
+
attr_accessor :header, :chunks
|
|
36
|
+
|
|
37
|
+
def self.parse(wave_file)
|
|
38
|
+
wf = NuWav::WaveFile.new
|
|
39
|
+
wf.parse(wave_file)
|
|
40
|
+
wf
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
self.chunks = {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse(wave_file)
|
|
48
|
+
NuWav::WaveFile.log "Processing wave file #{wave_file.inspect}...."
|
|
49
|
+
File.open(wave_file, File::RDWR) do |f|
|
|
50
|
+
#only for windows, make sure we are operating in binary mode
|
|
51
|
+
f.binmode
|
|
52
|
+
#start at the very beginning, a very good place to start
|
|
53
|
+
f.seek(0)
|
|
54
|
+
|
|
55
|
+
riff, riff_length = read_chunk_header(f)
|
|
56
|
+
NuWav::WaveFile.log "riff_length: #{riff_length}"
|
|
57
|
+
raise NotRIFFFormat unless riff == 'RIFF'
|
|
58
|
+
riff_end = f.tell + riff_length
|
|
59
|
+
|
|
60
|
+
riff_type = f.read(4)
|
|
61
|
+
raise NotWAVEFormat unless riff_type == 'WAVE'
|
|
62
|
+
|
|
63
|
+
@header = RiffChunk.new(riff, riff_length, riff_type)
|
|
64
|
+
|
|
65
|
+
while f.tell < riff_end
|
|
66
|
+
chunk_name, chunk_length = read_chunk_header(f)
|
|
67
|
+
fpos = f.tell
|
|
68
|
+
|
|
69
|
+
NuWav::WaveFile.log "found chunk: '#{chunk_name}', size #{chunk_length}"
|
|
70
|
+
self.chunks[chunk_name.to_sym] = chunk_class(chunk_name).parse(chunk_name, chunk_length, f)
|
|
71
|
+
|
|
72
|
+
f.seek(fpos + self.chunks[chunk_name.to_sym].size)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
@chunks.each{|k,v| NuWav::WaveFile.log "#{k}: #{v}\n\n" unless k == :data}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def duration
|
|
79
|
+
fmt = @chunks[:fmt]
|
|
80
|
+
|
|
81
|
+
if fmt && (fmt.compression_code.to_i == PCM_COMPRESSION)
|
|
82
|
+
@header.size / (fmt.sample_rate * fmt.number_of_channels * (fmt.sample_bits / 8))
|
|
83
|
+
else
|
|
84
|
+
raise "Duration implemented for WAV files only."
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_s
|
|
89
|
+
out = "NuWav:#{@header}\n"
|
|
90
|
+
out = [:fmt, :fact, :mext, :bext, :cart, :data ].inject(out) do |s, chunk|
|
|
91
|
+
s += "#{self.chunks[chunk]}\n" if self.chunks[chunk]
|
|
92
|
+
s
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_file(file_name, add_extension=false)
|
|
97
|
+
if add_extension && !(file_name =~ /\.wav/)
|
|
98
|
+
file_name += ".wav"
|
|
99
|
+
end
|
|
100
|
+
NuWav::WaveFile.log "NuWav::WaveFile.to_file: file_name = #{file_name}"
|
|
101
|
+
|
|
102
|
+
#get all the chunks together to get final length
|
|
103
|
+
chunks_out = [:fmt, :fact, :mext, :bext, :cart, :data].inject([]) do |list, chunk|
|
|
104
|
+
out = self.chunks[chunk].to_binary
|
|
105
|
+
NuWav::WaveFile.log out.length
|
|
106
|
+
list << out
|
|
107
|
+
list
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
riff_length = chunks_out.inject(0){|sum, chunk| sum += chunk.size}
|
|
111
|
+
NuWav::WaveFile.log "NuWav::WaveFile.to_file: riff_length = #{riff_length}"
|
|
112
|
+
|
|
113
|
+
#open file for writing
|
|
114
|
+
open(file_name, "wb") do |o|
|
|
115
|
+
#write the header
|
|
116
|
+
o << "RIFF"
|
|
117
|
+
o << [(riff_length + 4)].pack('V')
|
|
118
|
+
o << "WAVE"
|
|
119
|
+
#write the chunks
|
|
120
|
+
chunks_out.each{|c| o << c}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def write_data_file(file_name)
|
|
126
|
+
open(file_name, "wb") do |o|
|
|
127
|
+
o << chunks[:data].data
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# method to create a wave file using the
|
|
133
|
+
def self.from_mpeg(file_name)
|
|
134
|
+
# read and display infos & tags
|
|
135
|
+
NuWav::WaveFile.log "NuWav::from_mpeg::file_name:#{file_name}"
|
|
136
|
+
mp3info = Mp3Info.open(file_name)
|
|
137
|
+
NuWav::WaveFile.log mp3info
|
|
138
|
+
file = File.open(file_name)
|
|
139
|
+
wave = WaveFile.new
|
|
140
|
+
|
|
141
|
+
# data chunk
|
|
142
|
+
data = DataChunk.new
|
|
143
|
+
data.data = file.read
|
|
144
|
+
wave.chunks[:data] = data
|
|
145
|
+
|
|
146
|
+
# fmt chunk
|
|
147
|
+
fmt = FmtChunk.new
|
|
148
|
+
fmt.compression_code = MPEG_COMPRESSION
|
|
149
|
+
fmt.number_of_channels = (mp3info.channel_mode == "Single Channel") ? 1 : 2
|
|
150
|
+
fmt.sample_rate = mp3info.samplerate
|
|
151
|
+
fmt.byte_rate = mp3info.bitrate / 8 * 1000
|
|
152
|
+
fmt.block_align = calculate_mpeg_frame_size(mp3info)
|
|
153
|
+
fmt.sample_bits = 65535
|
|
154
|
+
fmt.extra_size = 22
|
|
155
|
+
fmt.head_layer = ACM_LAYERS[mp3info.layer.to_i-1]
|
|
156
|
+
fmt.head_bit_rate = mp3info.bitrate * 1000
|
|
157
|
+
fmt.head_mode = CHANNEL_MODES[mp3info.channel_mode]
|
|
158
|
+
# fmt.head_mode_ext = (mp3info.channel_mode == "JStereo") ? 2**mp3info.mode_extension : 0
|
|
159
|
+
fmt.head_mode_ext = (mp3info.channel_mode == "JStereo") ? 2**mp3info.header[:mode_extension] : 0
|
|
160
|
+
# fmt.head_emphasis = mp3info.emphasis + 1
|
|
161
|
+
fmt.head_emphasis = mp3info.header[:emphasis] + 1
|
|
162
|
+
fmt.head_flags = calculate_mpeg_head_flags(mp3info)
|
|
163
|
+
fmt.pts_low = 0
|
|
164
|
+
fmt.pts_high = 0
|
|
165
|
+
wave.chunks[:fmt] = fmt
|
|
166
|
+
# NuWav::WaveFile.log "fmt: #{fmt}"
|
|
167
|
+
|
|
168
|
+
# fact chunk
|
|
169
|
+
fact = FactChunk.new
|
|
170
|
+
fact.samples_number = calculate_mpeg_samples_number(file, mp3info)
|
|
171
|
+
wave.chunks[:fact] = fact
|
|
172
|
+
# NuWav::WaveFile.log "fact: #{fact}"
|
|
173
|
+
|
|
174
|
+
#mext chunk
|
|
175
|
+
mext = MextChunk.new
|
|
176
|
+
mext.sound_information = 5
|
|
177
|
+
mext.sound_information += 2 if mp3info.header[:padding]
|
|
178
|
+
mext.frame_size = calculate_mpeg_frame_size(mp3info)
|
|
179
|
+
mext.ancillary_data_length = 0
|
|
180
|
+
mext.ancillary_data_def = 0
|
|
181
|
+
wave.chunks[:mext] = mext
|
|
182
|
+
# NuWav::WaveFile.log "mext: #{mext}"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
#bext chunk
|
|
186
|
+
bext = BextChunk.new
|
|
187
|
+
bext.time_reference_high = 0
|
|
188
|
+
bext.time_reference_low = 0
|
|
189
|
+
bext.version = 1
|
|
190
|
+
bext.coding_history = "A=MPEG1L#{mp3info.layer},F=#{mp3info.samplerate},B=#{mp3info.bitrate},M=#{CODING_HISTORY_MODE[mp3info.channel_mode]},T=PRX\r\n\0\0"
|
|
191
|
+
wave.chunks[:bext] = bext
|
|
192
|
+
# NuWav::WaveFile.log "bext: #{bext}"
|
|
193
|
+
|
|
194
|
+
#cart chunk
|
|
195
|
+
cart = CartChunk.new
|
|
196
|
+
now = Time.now
|
|
197
|
+
today = Date.today
|
|
198
|
+
later = today << 12
|
|
199
|
+
cart.version = '0101'
|
|
200
|
+
cart.title = File.basename(file_name) # this is just a default
|
|
201
|
+
cart.start_date = today.strftime("%Y-%m-%d")
|
|
202
|
+
cart.start_time = now.strftime("%H:%M:%S")
|
|
203
|
+
cart.end_date = later.strftime("%Y-%m-%d")
|
|
204
|
+
cart.end_time = now.strftime("%H:%M:%S")
|
|
205
|
+
cart.producer_app_id = 'PRX'
|
|
206
|
+
cart.producer_app_version = '3.0'
|
|
207
|
+
cart.level_reference = 0
|
|
208
|
+
cart.tag_text = "\r\n"
|
|
209
|
+
wave.chunks[:cart] = cart
|
|
210
|
+
# NuWav::WaveFile.log "cart: #{cart}"
|
|
211
|
+
wave
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.calculate_mpeg_samples_number(file, info)
|
|
215
|
+
(File.size(file.path) / calculate_mpeg_frame_size(info)) * Mp3Info::SAMPLES_PER_FRAME[info.layer][info.mpeg_version]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.calculate_mpeg_head_flags(info)
|
|
219
|
+
flags = 0
|
|
220
|
+
flags += 1 if (info.header[:private_bit])
|
|
221
|
+
flags += 2 if (info.header[:copyright])
|
|
222
|
+
flags += 4 if (info.header[:original])
|
|
223
|
+
flags += 8 if (info.header[:error_protection])
|
|
224
|
+
flags += 16 if (info.mpeg_version > 0)
|
|
225
|
+
flags
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def self.calculate_mpeg_frame_size(info)
|
|
229
|
+
samples_per_frame = Mp3Info::SAMPLES_PER_FRAME[info.layer][info.mpeg_version]
|
|
230
|
+
((samples_per_frame / 8) * (info.bitrate * 1000))/info.samplerate
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
protected
|
|
234
|
+
|
|
235
|
+
def read_chunk_header(file)
|
|
236
|
+
hdr = file.read(8)
|
|
237
|
+
chunkName, chunkLen = hdr.unpack("A4V")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def chunk_class(name)
|
|
241
|
+
constantize("NuWav::#{camelize("#{name}_chunk")}")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# File vendor/rails/activesupport/lib/active_support/inflector.rb, line 147
|
|
245
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
|
246
|
+
if first_letter_in_uppercase
|
|
247
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
|
248
|
+
else
|
|
249
|
+
lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# File vendor/rails/activesupport/lib/active_support/inflector.rb, line 252
|
|
254
|
+
def constantize(camel_cased_word)
|
|
255
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
|
|
256
|
+
raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
|
|
257
|
+
end
|
|
258
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def self.log(m)
|
|
262
|
+
if NuWav::DEBUG
|
|
263
|
+
puts "#{Time.now}: NuWav: #{m}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
class Chunk
|
|
270
|
+
attr_accessor :id, :size, :raw
|
|
271
|
+
|
|
272
|
+
def self.parse(id, size, file)
|
|
273
|
+
raw = file.read(size)
|
|
274
|
+
chunk = self.new(id, size, raw)
|
|
275
|
+
chunk.parse
|
|
276
|
+
return chunk
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def initialize(id=nil, size=nil, raw=nil)
|
|
280
|
+
@id, @size, @raw = id, size, raw
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def parse
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def read_dword(start)
|
|
287
|
+
@raw[start..(start+3)].unpack('V').first
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def read_word(start)
|
|
291
|
+
@raw[start..(start+1)].unpack('v').first
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def read_char(start, length=(@raw.length-start))
|
|
295
|
+
@raw[start..(start+length-1)]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def write_dword(val)
|
|
299
|
+
[val].pack('V')
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def write_word(val)
|
|
303
|
+
[val].pack('v')
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def write_char(val, length=nil)
|
|
307
|
+
val ||= ''
|
|
308
|
+
length ||= val.length
|
|
309
|
+
# NuWav::WaveFile.log "length:#{length} val.length:#{val.length} val:#{val}"
|
|
310
|
+
padding = "\0" * [(length - val.length), 0].max
|
|
311
|
+
out = val[0,length] + padding
|
|
312
|
+
# NuWav::WaveFile.log out
|
|
313
|
+
out
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def to_binary
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class RiffChunk
|
|
322
|
+
attr_accessor :id, :size, :riff_type
|
|
323
|
+
|
|
324
|
+
def initialize(riff_name, riff_length, riff_type)
|
|
325
|
+
@id, @size, @riff_type = riff_name, riff_length, riff_type
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def to_s
|
|
329
|
+
"<chunk type:riff id:#{@id} size:#{@size} type:#{@riff_type} />"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
class FmtChunk < Chunk
|
|
335
|
+
|
|
336
|
+
attr_accessor :compression_code, :number_of_channels, :sample_rate, :byte_rate, :block_align, :sample_bits, :extra_size, :extra,
|
|
337
|
+
:head_layer, :head_bit_rate, :head_mode, :head_mode_ext, :head_emphasis, :head_flags, :pts_low, :pts_high
|
|
338
|
+
|
|
339
|
+
def parse
|
|
340
|
+
NuWav::WaveFile.log "@raw.size = #{@raw.size}"
|
|
341
|
+
@compression_code = read_word(0)
|
|
342
|
+
@number_of_channels = read_word(2)
|
|
343
|
+
@sample_rate = read_dword(4)
|
|
344
|
+
@byte_rate = read_dword(8)
|
|
345
|
+
@block_align = read_word(12)
|
|
346
|
+
@sample_bits = read_word(14)
|
|
347
|
+
@extra_size = read_word(16)
|
|
348
|
+
|
|
349
|
+
if (@compression_code.to_i == MPEG_COMPRESSION)
|
|
350
|
+
@head_layer = read_word(18)
|
|
351
|
+
@head_bit_rate = read_dword(20)
|
|
352
|
+
@head_mode = read_word(24)
|
|
353
|
+
@head_mode_ext = read_word(26)
|
|
354
|
+
@head_emphasis = read_word(28)
|
|
355
|
+
@head_flags = read_word(30)
|
|
356
|
+
@pts_low = read_dword(32)
|
|
357
|
+
@pts_high = read_dword(36)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def to_binary
|
|
362
|
+
out = ''
|
|
363
|
+
out += write_word(@compression_code)
|
|
364
|
+
out += write_word(@number_of_channels)
|
|
365
|
+
out += write_dword(@sample_rate)
|
|
366
|
+
out += write_dword(@byte_rate)
|
|
367
|
+
out += write_word(@block_align)
|
|
368
|
+
out += write_word(@sample_bits)
|
|
369
|
+
out += write_word(@extra_size)
|
|
370
|
+
|
|
371
|
+
if (@compression_code.to_i == MPEG_COMPRESSION)
|
|
372
|
+
out += write_word(@head_layer)
|
|
373
|
+
out += write_dword(@head_bit_rate)
|
|
374
|
+
out += write_word(@head_mode)
|
|
375
|
+
out += write_word(@head_mode_ext)
|
|
376
|
+
out += write_word(@head_emphasis)
|
|
377
|
+
out += write_word(@head_flags)
|
|
378
|
+
out += write_dword(@pts_low)
|
|
379
|
+
out += write_dword(@pts_high)
|
|
380
|
+
end
|
|
381
|
+
"fmt " + write_dword(out.size) + out
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def to_s
|
|
385
|
+
extra = if (@compression_code.to_i == MPEG_COMPRESSION)
|
|
386
|
+
", head_layer:#{head_layer}, head_bit_rate:#{head_bit_rate}, head_mode:#{head_mode}, head_mode_ext:#{head_mode_ext}, head_emphasis:#{head_emphasis}, head_flags:#{head_flags}, pts_low:#{pts_low}, pts_high:#{pts_high}"
|
|
387
|
+
else
|
|
388
|
+
""
|
|
389
|
+
end
|
|
390
|
+
"<chunk type:fmt compression_code:#{compression_code}, number_of_channels:#{number_of_channels}, sample_rate:#{sample_rate}, byte_rate:#{byte_rate}, block_align:#{block_align}, sample_bits:#{sample_bits}, extra_size:#{extra_size} #{extra} />"
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
class FactChunk < Chunk
|
|
395
|
+
attr_accessor :samples_number
|
|
396
|
+
|
|
397
|
+
def parse
|
|
398
|
+
@samples_number = read_dword(0)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def to_s
|
|
402
|
+
"<chunk type:fact samples_number:#{@samples_number} />"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def to_binary
|
|
406
|
+
"fact" + write_dword(4) + write_dword(@samples_number)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
class MextChunk < Chunk
|
|
412
|
+
attr_accessor :sound_information, :frame_size, :ancillary_data_length, :ancillary_data_def, :reserved
|
|
413
|
+
|
|
414
|
+
def parse
|
|
415
|
+
@sound_information = read_word(0)
|
|
416
|
+
@frame_size = read_word(2)
|
|
417
|
+
@ancillary_data_length = read_word(4)
|
|
418
|
+
@ancillary_data_def = read_word(6)
|
|
419
|
+
@reserved = read_char(8,4)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def to_s
|
|
423
|
+
"<chunk type:mext sound_information:(#{sound_information}) #{(0..15).inject(''){|s,x| "#{s}#{sound_information[x]}"}}, frame_size:#{frame_size}, ancillary_data_length:#{ancillary_data_length}, ancillary_data_def:#{(0..15).inject(''){|s,x| "#{s}#{ancillary_data_def[x]}"}}, reserved:'#{reserved}' />"
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def to_binary
|
|
427
|
+
out = "mext" + write_dword(12)
|
|
428
|
+
out += write_word(@sound_information)
|
|
429
|
+
out += write_word(@frame_size)
|
|
430
|
+
out += write_word(@ancillary_data_length)
|
|
431
|
+
out += write_word(@ancillary_data_def)
|
|
432
|
+
out += write_char(@reserved, 4)
|
|
433
|
+
out
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
class BextChunk < Chunk
|
|
438
|
+
attr_accessor :description, :originator, :originator_reference, :origination_date, :origination_time, :time_reference_low, :time_reference_high,
|
|
439
|
+
:version, :umid, :reserved, :coding_history
|
|
440
|
+
|
|
441
|
+
def parse
|
|
442
|
+
@description = read_char(0,256)
|
|
443
|
+
@originator = read_char(256,32)
|
|
444
|
+
@originator_reference = read_char(288,32)
|
|
445
|
+
@origination_date = read_char(320,10)
|
|
446
|
+
@origination_time = read_char(330,8)
|
|
447
|
+
@time_reference_low = read_dword(338)
|
|
448
|
+
@time_reference_high = read_dword(342)
|
|
449
|
+
@version = read_word(346)
|
|
450
|
+
@umid = read_char(348,64)
|
|
451
|
+
@reserved = read_char(412,190)
|
|
452
|
+
@coding_history = read_char(602)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def to_s
|
|
456
|
+
"<chunk type:bext description:'#{description}', originator:'#{originator}', originator_reference:'#{originator_reference}', origination_date:'#{origination_date}', origination_time:'#{origination_time}', time_reference_low:#{time_reference_low}, time_reference_high:#{time_reference_high}, version:#{version}, umid:#{umid}, reserved:'#{reserved}', coding_history:#{coding_history} />"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def to_binary
|
|
460
|
+
out = "bext" + write_dword(602 + @coding_history.length )
|
|
461
|
+
out += write_char(@description, 256)
|
|
462
|
+
out += write_char(@originator, 32)
|
|
463
|
+
out += write_char(@originator_reference, 32)
|
|
464
|
+
out += write_char(@origination_date, 10)
|
|
465
|
+
out += write_char(@origination_time, 8)
|
|
466
|
+
out += write_dword(@time_reference_low)
|
|
467
|
+
out += write_dword(@time_reference_high)
|
|
468
|
+
out += write_word(@version)
|
|
469
|
+
out += write_char(@umid, 64)
|
|
470
|
+
out += write_char(@reserved, 190)
|
|
471
|
+
out += write_char(@coding_history)
|
|
472
|
+
# make sure coding history ends in '\r\n'
|
|
473
|
+
out
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
class CartChunk < Chunk
|
|
479
|
+
attr_accessor :version, :title, :artist, :cut_id, :client_id, :category, :classification, :out_cue, :start_date, :start_time, :end_date, :end_time,
|
|
480
|
+
:producer_app_id, :producer_app_version, :user_def, :level_reference, :post_timer, :reserved, :url, :tag_text
|
|
481
|
+
|
|
482
|
+
def parse
|
|
483
|
+
@version = read_char(0,4)
|
|
484
|
+
@title = read_char(4,64)
|
|
485
|
+
@artist = read_char(68,64)
|
|
486
|
+
@cut_id = read_char(132,64)
|
|
487
|
+
@client_id = read_char(196,64)
|
|
488
|
+
@category = read_char(260,64)
|
|
489
|
+
@classification = read_char(324,64)
|
|
490
|
+
@outcue = read_char(388,64)
|
|
491
|
+
@start_date = read_char(452,10)
|
|
492
|
+
@start_time = read_char(462,8)
|
|
493
|
+
@end_date = read_char(470,10)
|
|
494
|
+
@end_time = read_char(480,8)
|
|
495
|
+
@producer_app_id = read_char(488,64)
|
|
496
|
+
@producer_app_version = read_char(552,64)
|
|
497
|
+
@user_def = read_char(616,64)
|
|
498
|
+
@level_reference = read_dword(680)
|
|
499
|
+
@post_timer = read_char(684,64)
|
|
500
|
+
@reserved = read_char(748,276)
|
|
501
|
+
@url = read_char(1024,1024)
|
|
502
|
+
@tag_text = read_char(2048)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def to_s
|
|
506
|
+
"<chunk type:cart version:#{version}, title:#{title}, artist:#{artist}, cut_id:#{cut_id}, client_id:#{client_id}, category:#{category}, classification:#{classification}, out_cue:#{out_cue}, start_date:#{start_date}, start_time:#{start_time}, end_date:#{end_date}, end_time:#{end_time}, producer_app_id:#{producer_app_id}, producer_app_version:#{producer_app_version}, user_def:#{user_def}, level_reference:#{level_reference}, post_timer:#{post_timer}, reserved:#{reserved}, url:#{url}, tag_text:#{tag_text} />"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def to_binary
|
|
510
|
+
out = "cart" + write_dword(2048 + @tag_text.length )
|
|
511
|
+
out += write_char(@version,4)
|
|
512
|
+
out += write_char(@title,64)
|
|
513
|
+
out += write_char(@artist,64)
|
|
514
|
+
out += write_char(@cut_id,64)
|
|
515
|
+
out += write_char(@client_id,64)
|
|
516
|
+
out += write_char(@category,64)
|
|
517
|
+
out += write_char(@classification,64)
|
|
518
|
+
out += write_char(@outcue,64)
|
|
519
|
+
out += write_char(@start_date,10)
|
|
520
|
+
out += write_char(@start_time,8)
|
|
521
|
+
out += write_char(@end_date,10)
|
|
522
|
+
out += write_char(@end_time,8)
|
|
523
|
+
out += write_char(@producer_app_id,64)
|
|
524
|
+
out += write_char(@producer_app_version,64)
|
|
525
|
+
out += write_char(@user_def,64)
|
|
526
|
+
out += write_dword(@level_reference)
|
|
527
|
+
out += write_char(@post_timer,64)
|
|
528
|
+
out += write_char(@reserved,276)
|
|
529
|
+
out += write_char(@url,1024)
|
|
530
|
+
out += write_char(@tag_text)
|
|
531
|
+
out
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
class DataChunk < Chunk
|
|
537
|
+
alias_method :data, :raw
|
|
538
|
+
alias_method :data=, :raw=
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def to_s
|
|
542
|
+
"<chunk type:data (size:#{data.size})/>"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def to_binary
|
|
546
|
+
out = "data" + write_dword(data.size) + data
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
end
|
data/test/helper.rb
ADDED
data/test/test_nu_wav.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'helper'
|
|
2
|
+
|
|
3
|
+
class TestNuWav < Test::Unit::TestCase
|
|
4
|
+
def test_parse_wav
|
|
5
|
+
assert true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_parse_wav_with_bwf
|
|
9
|
+
assert true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_parse_wav_with_bwf_and_cart_chunk
|
|
13
|
+
assert true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# if NuWav::DEBUG
|
|
18
|
+
#
|
|
19
|
+
# wf = NuWav::WaveFile.new
|
|
20
|
+
# # wf.parse('/Users/akuklewicz/dev/testaudio/0330AK_Studded.wav')
|
|
21
|
+
# # puts "wf.duration = #{wf.duration}"
|
|
22
|
+
# # puts "wf = #{wf}"
|
|
23
|
+
#
|
|
24
|
+
# wf.parse('/Users/akuklewicz/dev/workspace/mediajoint/test/fixtures/files/AfropopW_040_SGMT01.wav')
|
|
25
|
+
#
|
|
26
|
+
# puts "--------------------------------------------------------------------------------"
|
|
27
|
+
#
|
|
28
|
+
# wf.write_data_file('/Users/akuklewicz/dev/workspace/mediajoint/test/fixtures/files/AfropopW_040_SGMT01.mp2')
|
|
29
|
+
#
|
|
30
|
+
# # wf.to_file('AK_FreshA05_160_SGMT02')
|
|
31
|
+
# # wf.parse('AK_FreshA05_160_SGMT02.wav')
|
|
32
|
+
#
|
|
33
|
+
# puts "--------------------------------------------------------------------------------"
|
|
34
|
+
# #
|
|
35
|
+
#
|
|
36
|
+
# wv = NuWav::WaveFile.from_mpeg('/Users/akuklewicz/dev/workspace/mediajoint/test/fixtures/files/AK_AfropopW_040_SGMT01.mp2')
|
|
37
|
+
# wv.to_file('AK_AfropopW_040_SGMT01_to_file_test.wav')
|
|
38
|
+
#
|
|
39
|
+
# puts "--------------------------------------------------------------------------------"
|
|
40
|
+
#
|
|
41
|
+
# wf = NuWav::WaveFile.new
|
|
42
|
+
# wf.parse('AK_AfropopW_040_SGMT01_to_file_test.wav')
|
|
43
|
+
#
|
|
44
|
+
#
|
|
45
|
+
#
|
|
46
|
+
# end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nu_wav
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kookster
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2010-04-03 00:00:00 -04:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: ruby-mp3info
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 0.6.13
|
|
24
|
+
version:
|
|
25
|
+
description: NuWav is a pure ruby audio WAV file parser and writer. It supports Broadcast Wave Format (BWF), inclluding MPEG audio data, and the public radio standard cart chunk.
|
|
26
|
+
email: andrew@beginsinwonder.com
|
|
27
|
+
executables: []
|
|
28
|
+
|
|
29
|
+
extensions: []
|
|
30
|
+
|
|
31
|
+
extra_rdoc_files:
|
|
32
|
+
- LICENSE
|
|
33
|
+
- README.rdoc
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.rdoc
|
|
37
|
+
- Rakefile
|
|
38
|
+
- VERSION
|
|
39
|
+
- lib/nu_wav.rb
|
|
40
|
+
- test/helper.rb
|
|
41
|
+
- test/test_nu_wav.rb
|
|
42
|
+
has_rdoc: true
|
|
43
|
+
homepage: http://github.com/kookster/nu_wav
|
|
44
|
+
licenses: []
|
|
45
|
+
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options:
|
|
48
|
+
- --charset=UTF-8
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: "0"
|
|
56
|
+
version:
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: "0"
|
|
62
|
+
version:
|
|
63
|
+
requirements: []
|
|
64
|
+
|
|
65
|
+
rubyforge_project:
|
|
66
|
+
rubygems_version: 1.3.5
|
|
67
|
+
signing_key:
|
|
68
|
+
specification_version: 3
|
|
69
|
+
summary: NuWav is a pure ruby audio WAV file parser and writer.
|
|
70
|
+
test_files:
|
|
71
|
+
- test/helper.rb
|
|
72
|
+
- test/test_nu_wav.rb
|