lib/nu_wav.rb in nu_wav-0.3.4 vs lib/nu_wav.rb in nu_wav-0.4.2
- old
+ new
@@ -8,13 +8,17 @@
require 'mp3info'
require 'date'
require 'tempfile'
require 'fileutils'
+require "nu_wav/version"
+require "nu_wav/chunk"
+require "nu_wav/wave_file"
+
module NuWav
- DEBUG = false
+ DEBUG = ENV['NU_WAV_DEBUG']
# 1 is standard integer based, 3 is the floating point PCM
PCM_INTEGER_COMPRESSION = 1
PCM_FLOATING_COMPRESSION = 3
PCM_COMPRESSION = [PCM_INTEGER_COMPRESSION, PCM_FLOATING_COMPRESSION]
@@ -36,614 +40,7 @@
CODING_HISTORY_MODE = {'Single Channel'=>'mono', 'Stereo'=>'stereo', 'Dual Channel'=>'dual-mono', 'JStereo'=>'joint-stereo'}
class NotRIFFFormat < StandardError; end
class NotWAVEFormat < StandardError; end
-
- class WaveFile
-
- attr_accessor :header, :chunks
- def self.parse(wave_file)
- NuWav::WaveFile.new.parse(wave_file)
- end
-
- def initialize
- self.chunks = {}
- end
-
- def parse(wave_file)
- NuWav::WaveFile.log "Processing wave file #{wave_file.inspect}...."
- wave_file_size = File.size(wave_file)
-
- File.open(wave_file, File::RDWR) do |f|
-
- #only for windows, make sure we are operating in binary mode
- f.binmode
- #start at the very beginning, a very good place to start
- f.seek(0)
-
- riff, riff_length = read_chunk_header(f)
- NuWav::WaveFile.log "riff: #{riff}"
- NuWav::WaveFile.log "riff_length: #{riff_length}"
- NuWav::WaveFile.log "wave_file_size: #{wave_file_size}"
-
- raise NotRIFFFormat unless riff == 'RIFF'
- riff_end = [f.tell + riff_length, wave_file_size].min
-
- riff_type = f.read(4)
- raise NotWAVEFormat unless riff_type == 'WAVE'
-
- @header = RiffChunk.new(riff, riff_length, riff_type)
-
- while (f.tell + 8) <= riff_end
- NuWav::WaveFile.log "while #{f.tell} < #{riff_end}"
- chunk_name, chunk_length = read_chunk_header(f)
- fpos = f.tell
-
- NuWav::WaveFile.log "found chunk: '#{chunk_name}', size #{chunk_length}"
-
- if chunk_name && chunk_length
-
- self.chunks[chunk_name.to_sym] = chunk_class(chunk_name).parse(chunk_name, chunk_length, f)
-
- NuWav::WaveFile.log "about to do a seek..."
- NuWav::WaveFile.log "f.seek #{fpos} + #{self.chunks[chunk_name.to_sym].size}"
- f.seek(fpos + self.chunks[chunk_name.to_sym].size)
- NuWav::WaveFile.log "seek done"
- else
- NuWav::WaveFile.log "chunk or length was off - remainder of file does not parse properly: #{riff_end} - #{fpos} = #{riff_end - fpos}"
- f.seek(riff_end)
- end
- end
- end
- @chunks.each{|k,v| NuWav::WaveFile.log "#{k}: #{v}\n\n" unless k.to_s == 'data'}
- NuWav::WaveFile.log "parse done"
- self
- end
-
- def duration
- fmt = @chunks[:fmt]
-
- if (PCM_COMPRESSION.include?(fmt.compression_code.to_i))
- data = @chunks[:data]
- data.size / (fmt.sample_rate * fmt.number_of_channels * (fmt.sample_bits / 8))
- elsif (fmt.compression_code.to_i == MPEG_COMPRESSION)
- # <chunk type:fact samples_number:78695424 />
- fact = @chunks[:fact]
- fact.samples_number / fmt.sample_rate
- else
- raise "Duration implemented for PCM and MEPG files only."
- end
- end
-
- def is_mpeg?
- (@chunks[:fmt] && (@chunks[:fmt].compression_code.to_i == MPEG_COMPRESSION))
- end
-
- def is_pcm?
- (@chunks[:fmt] && (PCM_COMPRESSION.include?(@chunks[:fmt].compression_code.to_i)))
- end
-
- def to_s
- out = "NuWav:#{@header}\n"
- out = [:fmt, :fact, :mext, :bext, :cart, :data ].inject(out) do |s, chunk|
- s += "#{self.chunks[chunk]}\n" if self.chunks[chunk]
- s
- end
- end
-
- def to_file(file_name, add_extension=false)
- if add_extension && !(file_name =~ /\.wav/)
- file_name += ".wav"
- end
- NuWav::WaveFile.log "NuWav::WaveFile.to_file: file_name = #{file_name}"
-
- #get all the chunks together to get final length
- chunks_out = [:fmt, :fact, :mext, :bext, :cart, :data].inject([]) do |list, chunk|
- if self.chunks[chunk]
- out = self.chunks[chunk].to_binary
- NuWav::WaveFile.log out.length
- list << out
- end
- list
- end
-
- # TODO: handle other chunks not in the above list, but that might have been in a parsed wav
-
- riff_length = chunks_out.inject(0){|sum, chunk| sum += chunk.size}
- NuWav::WaveFile.log "NuWav::WaveFile.to_file: riff_length = #{riff_length}"
-
- #open file for writing
- open(file_name, "wb") do |o|
- #write the header
- o << "RIFF"
- o << [(riff_length + 4)].pack('V')
- o << "WAVE"
- #write the chunks
- chunks_out.each{|c| o << c}
- end
-
- end
-
- def write_data_file(file_name)
- open(file_name, "wb") do |o|
- o << chunks[:data].data
- end
- end
-
-
- # method to create a wave file using the
- def self.from_mpeg(file_name)
- # read and display infos & tags
- NuWav::WaveFile.log "NuWav::from_mpeg::file_name:#{file_name}"
- mp3info = Mp3Info.open(file_name)
- NuWav::WaveFile.log mp3info
- file = File.open(file_name)
- wave = WaveFile.new
-
- # data chunk
- data = DataChunk.new_from_file(file)
- wave.chunks[:data] = data
-
- # fmt chunk
- fmt = FmtChunk.new
- fmt.compression_code = MPEG_COMPRESSION
- fmt.number_of_channels = (mp3info.channel_mode == "Single Channel") ? 1 : 2
- fmt.sample_rate = mp3info.samplerate
- fmt.byte_rate = mp3info.bitrate / 8 * 1000
- fmt.block_align = calculate_mpeg_frame_size(mp3info)
- fmt.sample_bits = 65535
- fmt.extra_size = 22
- fmt.head_layer = ACM_LAYERS[mp3info.layer.to_i-1]
- fmt.head_bit_rate = mp3info.bitrate * 1000
- fmt.head_mode = CHANNEL_MODES[mp3info.channel_mode]
- # fmt.head_mode_ext = (mp3info.channel_mode == "JStereo") ? 2**mp3info.mode_extension : 0
- fmt.head_mode_ext = (mp3info.channel_mode == "JStereo") ? 2**mp3info.header[:mode_extension] : 0
- # fmt.head_emphasis = mp3info.emphasis + 1
- fmt.head_emphasis = mp3info.header[:emphasis] + 1
- fmt.head_flags = calculate_mpeg_head_flags(mp3info)
- fmt.pts_low = 0
- fmt.pts_high = 0
- wave.chunks[:fmt] = fmt
- # NuWav::WaveFile.log "fmt: #{fmt}"
-
- # fact chunk
- fact = FactChunk.new
- fact.samples_number = calculate_mpeg_samples_number(file, mp3info)
- wave.chunks[:fact] = fact
- # NuWav::WaveFile.log "fact: #{fact}"
-
- #mext chunk
- mext = MextChunk.new
- mext.sound_information = 5
- mext.sound_information += 2 if mp3info.header[:padding]
- mext.frame_size = calculate_mpeg_frame_size(mp3info)
- mext.ancillary_data_length = 0
- mext.ancillary_data_def = 0
- wave.chunks[:mext] = mext
- # NuWav::WaveFile.log "mext: #{mext}"
-
-
- #bext chunk
- bext = BextChunk.new
- bext.time_reference_high = 0
- bext.time_reference_low = 0
- bext.version = 1
- 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"
- wave.chunks[:bext] = bext
- # NuWav::WaveFile.log "bext: #{bext}"
-
- #cart chunk
- cart = CartChunk.new
- now = Time.now
- today = Date.today
- later = today << 12
- cart.version = '0101'
- cart.title = File.basename(file_name) # this is just a default
- cart.start_date = today.strftime("%Y-%m-%d")
- cart.start_time = now.strftime("%H:%M:%S")
- cart.end_date = later.strftime("%Y-%m-%d")
- cart.end_time = now.strftime("%H:%M:%S")
- cart.producer_app_id = 'PRX'
- cart.producer_app_version = '3.0'
- cart.level_reference = 0
- cart.tag_text = "\r\n"
- wave.chunks[:cart] = cart
- # NuWav::WaveFile.log "cart: #{cart}"
- wave
- end
-
- def self.calculate_mpeg_samples_number(file, info)
- (File.size(file.path) / calculate_mpeg_frame_size(info)) * Mp3Info::SAMPLES_PER_FRAME[info.layer][info.mpeg_version]
- end
-
- def self.calculate_mpeg_head_flags(info)
- flags = 0
- flags += 1 if (info.header[:private_bit])
- flags += 2 if (info.header[:copyright])
- flags += 4 if (info.header[:original])
- flags += 8 if (info.header[:error_protection])
- flags += 16 if (info.mpeg_version > 0)
- flags
- end
-
- def self.calculate_mpeg_frame_size(info)
- samples_per_frame = Mp3Info::SAMPLES_PER_FRAME[info.layer][info.mpeg_version]
- ((samples_per_frame / 8) * (info.bitrate * 1000))/info.samplerate
- end
-
- protected
-
- def read_chunk_header(file)
- hdr = file.read(8)
- # puts "hdr: #{hdr}"
- chunkName, chunkLen = hdr.unpack("A4V") rescue [nil, nil]
- # puts "chunkName: '#{chunkName}', chunkLen: '#{chunkLen}'"
- [chunkName, chunkLen]
- end
-
- def chunk_class(name)
- begin
- constantize("NuWav::#{camelize("#{name}_chunk")}")
- rescue NameError
- NuWav::Chunk
- end
-
- end
-
- # File vendor/rails/activesupport/lib/active_support/inflector.rb, line 147
- def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
- if first_letter_in_uppercase
- lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
- else
- lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
- end
- end
-
- # File vendor/rails/activesupport/lib/active_support/inflector.rb, line 252
- def constantize(camel_cased_word)
- unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
- raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
- end
- Object.module_eval("::#{$1}", __FILE__, __LINE__)
- end
-
- def self.log(m)
- if NuWav::DEBUG
- puts "#{Time.now}: NuWav: #{m}"
- end
- end
-
- end
-
- class Chunk
- attr_accessor :id, :size, :raw
-
- def self.parse(id, size, file)
- raw = file.read(size)
- chunk = self.new(id, size, raw)
- chunk.parse
- return chunk
- end
-
- def initialize(id=nil, size=nil, raw=nil)
- @id, @size, @raw = id, size, raw
- end
-
- def parse
- end
-
- def read_dword(start)
- @raw[start..(start+3)].unpack('V').first
- end
-
- def read_word(start)
- @raw[start..(start+1)].unpack('v').first
- end
-
- def read_char(start, length=(@raw.length-start))
- (@raw[start..(start+length-1)] || '').strip
- end
-
- def write_dword(val)
- val ||= 0
- [val].pack('V')
- end
-
- def write_word(val)
- val ||= 0
- [val].pack('v')
- end
-
- def write_char(val, length=nil)
- val ||= ''
- val = val.to_s
- length ||= val.length
- # NuWav::WaveFile.log "length:#{length} val.length:#{val.length} val:#{val}"
- padding = "\0" * [(length - val.length), 0].max
- out = val[0,length] + padding
- # NuWav::WaveFile.log out
- out
- end
-
- def to_binary
- end
- end
-
-
- class RiffChunk
- attr_accessor :id, :size, :riff_type
-
- def initialize(riff_name, riff_length, riff_type)
- @id, @size, @riff_type = riff_name, riff_length, riff_type
- end
-
- def to_s
- "<chunk type:riff id:#{@id} size:#{@size} type:#{@riff_type} />"
- end
-
- end
-
- class FmtChunk < Chunk
-
- attr_accessor :compression_code, :number_of_channels, :sample_rate, :byte_rate, :block_align, :sample_bits, :extra_size, :extra,
- :head_layer, :head_bit_rate, :head_mode, :head_mode_ext, :head_emphasis, :head_flags, :pts_low, :pts_high
-
- def parse
- NuWav::WaveFile.log "@raw.size = #{@raw.size}"
- @compression_code = read_word(0)
- @number_of_channels = read_word(2)
- @sample_rate = read_dword(4)
- @byte_rate = read_dword(8)
- @block_align = read_word(12)
- @sample_bits = read_word(14)
- @extra_size = read_word(16)
-
- if (@compression_code.to_i == MPEG_COMPRESSION)
- @head_layer = read_word(18)
- @head_bit_rate = read_dword(20)
- @head_mode = read_word(24)
- @head_mode_ext = read_word(26)
- @head_emphasis = read_word(28)
- @head_flags = read_word(30)
- @pts_low = read_dword(32)
- @pts_high = read_dword(36)
- end
- end
-
- def to_binary
- out = ''
- out += write_word(@compression_code)
- out += write_word(@number_of_channels)
- out += write_dword(@sample_rate)
- out += write_dword(@byte_rate)
- out += write_word(@block_align)
- out += write_word(@sample_bits)
- out += write_word(@extra_size)
-
- if (@compression_code.to_i == MPEG_COMPRESSION)
- out += write_word(@head_layer)
- out += write_dword(@head_bit_rate)
- out += write_word(@head_mode)
- out += write_word(@head_mode_ext)
- out += write_word(@head_emphasis)
- out += write_word(@head_flags)
- out += write_dword(@pts_low)
- out += write_dword(@pts_high)
- end
- "fmt " + write_dword(out.size) + out
- end
-
- def to_s
- extra = if (@compression_code.to_i == MPEG_COMPRESSION)
- ", 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}"
- else
- ""
- end
- "<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} />"
- end
- end
-
- class FactChunk < Chunk
- attr_accessor :samples_number
-
- def parse
- @samples_number = read_dword(0)
- end
-
- def to_s
- "<chunk type:fact samples_number:#{@samples_number} />"
- end
-
- def to_binary
- "fact" + write_dword(4) + write_dword(@samples_number)
- end
-
- end
-
- class MextChunk < Chunk
- attr_accessor :sound_information, :frame_size, :ancillary_data_length, :ancillary_data_def, :reserved
-
- def parse
- @sound_information = read_word(0)
- @frame_size = read_word(2)
- @ancillary_data_length = read_word(4)
- @ancillary_data_def = read_word(6)
- @reserved = read_char(8,4)
- end
-
- def to_s
- "<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}' />"
- end
-
- def to_binary
- out = "mext" + write_dword(12)
- out += write_word(@sound_information)
- out += write_word(@frame_size)
- out += write_word(@ancillary_data_length)
- out += write_word(@ancillary_data_def)
- out += write_char(@reserved, 4)
- out
- end
- end
-
- class BextChunk < Chunk
- attr_accessor :description, :originator, :originator_reference, :origination_date, :origination_time, :time_reference_low, :time_reference_high,
- :version, :umid, :reserved, :coding_history
-
- def parse
- @description = read_char(0,256)
- @originator = read_char(256,32)
- @originator_reference = read_char(288,32)
- @origination_date = read_char(320,10)
- @origination_time = read_char(330,8)
- @time_reference_low = read_dword(338)
- @time_reference_high = read_dword(342)
- @version = read_word(346)
- @umid = read_char(348,64)
- @reserved = read_char(412,190)
- @coding_history = read_char(602)
- end
-
- def to_s
- "<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} />"
- end
-
- def to_binary
- out = "bext" + write_dword(602 + @coding_history.length )
- out += write_char(@description, 256)
- out += write_char(@originator, 32)
- out += write_char(@originator_reference, 32)
- out += write_char(@origination_date, 10)
- out += write_char(@origination_time, 8)
- out += write_dword(@time_reference_low)
- out += write_dword(@time_reference_high)
- out += write_word(@version)
- out += write_char(@umid, 64)
- out += write_char(@reserved, 190)
- out += write_char(@coding_history)
- # make sure coding history ends in '\r\n'
- out
- end
-
- end
-
- class CartChunk < Chunk
- attr_accessor :version, :title, :artist, :cut_id, :client_id, :category, :classification, :out_cue, :start_date, :start_time, :end_date, :end_time,
- :producer_app_id, :producer_app_version, :user_def, :level_reference, :post_timer, :reserved, :url, :tag_text
-
- def parse
- @version = read_char(0,4)
- @title = read_char(4,64)
- @artist = read_char(68,64)
- @cut_id = read_char(132,64)
- @client_id = read_char(196,64)
- @category = read_char(260,64)
- @classification = read_char(324,64)
- @outcue = read_char(388,64)
- @start_date = read_char(452,10)
- @start_time = read_char(462,8)
- @end_date = read_char(470,10)
- @end_time = read_char(480,8)
- @producer_app_id = read_char(488,64)
- @producer_app_version = read_char(552,64)
- @user_def = read_char(616,64)
- @level_reference = read_dword(680)
- @post_timer = read_char(684,64)
- @reserved = read_char(748,276)
- @url = read_char(1024,1024)
- @tag_text = read_char(2048)
- end
-
- def to_s
- "<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} />"
- end
-
- def to_binary
- out = "cart" + write_dword(2048 + @tag_text.length )
- out += write_char(@version,4)
- out += write_char(@title,64)
- out += write_char(@artist,64)
- out += write_char(@cut_id,64)
- out += write_char(@client_id,64)
- out += write_char(@category,64)
- out += write_char(@classification,64)
- out += write_char(@outcue,64)
- out += write_char(@start_date,10)
- out += write_char(@start_time,8)
- out += write_char(@end_date,10)
- out += write_char(@end_time,8)
- out += write_char(@producer_app_id,64)
- out += write_char(@producer_app_version,64)
- out += write_char(@user_def,64)
- out += write_dword(@level_reference)
- out += write_char(@post_timer,64)
- out += write_char(@reserved,276)
- out += write_char(@url,1024)
- out += write_char(@tag_text)
- out
- end
-
- end
-
- class DataChunk < Chunk
- attr_accessor :tmp_data_file
-
- def self.parse(id, size, file)
-
- # tmp_data = File.open('./data_chunk.mp2', 'wb')
- tmp_data = Tempfile.open('data_chunk')
- tmp_data.binmode
-
- remaining = size
- while (remaining > 0 && !file.eof?)
- read_bytes = [128, remaining].min
- tmp_data << file.read(read_bytes)
- remaining -= read_bytes
- end
- tmp_data.rewind
- chunk = self.new(id, size, tmp_data)
-
- return chunk
- end
-
- def self.new_from_file(file)
- tmp_data = Tempfile.open('data_chunk')
- tmp_data.binmode
- FileUtils.cp(file.path, tmp_data.path)
- tmp_data.rewind
- self.new('data', File.size(tmp_data.path).to_s, tmp_data)
- end
-
- def initialize(id=nil, size=nil, tmp_data_file=nil)
- @id, @size, @tmp_data_file = id, size, tmp_data_file
- end
-
- def data
- f = ''
- if self.tmp_data_file
- NuWav::WaveFile.log "we have a tmp_data_file!"
- self.tmp_data_file.rewind
- f = self.tmp_data_file.read
- self.tmp_data_file.rewind
- else
- NuWav::WaveFile.log "we have NO tmp_data_file!"
- end
- f
- end
-
- def to_s
- "<chunk type:data (size:#{data.size})/>"
- end
-
- def to_binary
- NuWav::WaveFile.log "data chunk to_binary"
- d = self.data
- NuWav::WaveFile.log "got data size = #{d.size} #{d[0,10]}"
- out = "data" + write_dword(d.size) + d
- out
- end
-
- end
-
end
\ No newline at end of file