#!/usr/bin/ruby class ChunkList attr_accessor :fd, :len, :pos def initialize(fd, len=nil) @fd, @len = fd, len @pos = 0 if len end def eos? @len ? (@pos >= @len) : fd.eof? end # reads len bytes from @raw, increments @ptr def read(len) len = @len-@pos if @len and @pos+len > @len return '' if eos? @pos += len if @len @fd.read(len) || '' end # read the chunk signature def readchunktype read 4 end # return the chunk content (length-prefixed) def readchunk len = read(4).unpack('L').first || 0 read(len) end # returns a new ChunkList containing the current chunk data def makesubchunk len = read(4).unpack('L').first ChunkList.new(@fd, len) end end class WavReader attr_accessor :raw, :wave, :fmt, :data def initialize(fd) @raw = ChunkList.new(fd) seekwave raise 'unsupported file type or empty file' if not @fmt or @fmt['format'] != 1 or not @data end # loop through all RIFF blocks until it finds one with fmt+data def seekwave loop do @wave = nil while @raw.readchunktype != 'RIFF' return if @raw.eos? end @wave = @raw.makesubchunk if @wave.readchunktype == 'WAVE' seekfmt if @fmt seekdata break end end end end # loop through all WAVE blocks until it finds a fmt def seekfmt @fmt = nil while @wave.readchunktype != 'fmt ' return if @wave.eos? @wave.readchunk end fmtraw = @wave.readchunk @fmt = {} %w[format channels samplepersec bytepersec blockalign bitpersample].zip(fmtraw.unpack('sSLLSS')) { |k, v| @fmt[k] = v } p @fmt if $DEBUG end # loop through all WAVE blocks until it finds a data, if not calls seekwave def seekdata @data = nil while (sig = @wave.readchunktype) != 'data' return seekwave if @wave.eos? ck = @wave.readchunk p sig, ck if $DEBUG end @data = @wave.makesubchunk end # return an array of (nchannel) samples, nil on eof def readsample seekdata if @data.eos? return if not @data bps = @fmt['bitpersample'] @data.read(@fmt['channels']*bps/8).unpack({8 => 'C*', 16 => 's*'}[bps]) end # readsample optimized for 16bits mono (returns just 1 sample, no array) def readsampleopt @data.read(2).unpack('s').first end end class WaveWriter attr_reader :fmt, :data def initialize(fmt = {}) @fmt = fmt @fmt['format'] ||= 1 @fmt['channels'] ||= 1 @fmt['samplepersec'] ||= 44100 @fmt['bitpersample'] ||= 16 @fmt['bytepersec'] ||= @fmt['samplepersec'] * @fmt['bitpersample'] / 8 @fmt['blockalign'] ||= 2 @data = '' end def dumpchunk(sig, chk) c = sig + [chk.length].pack('L') c << chk c << 0 while c.length & (@fmt['blockalign'] - 1) != 0 c end def to_s fmt = dumpchunk('fmt ', %w[format channels samplepersec bytepersec blockalign bitpersample].map { |k| @fmt[k] }.pack('sSLLSS')) data = dumpchunk('data', @data) dumpchunk('RIFF', 'WAVE' + fmt + data) end end if $0 == __FILE__ require 'stringio' ARGV.each { |f| puts f w = WavReader.new StringIO.new(File.open(f, 'rb') { |fd| fd.read }) h = Hash.new(0) ww = WaveWriter.new min, max = -2377, 7446 rng = max-min min += rng/4 max -= rng/4 begin while s = w.readsampleopt $stderr.print "#{(w.data.pos * 100.0 / w.data.len).to_i}% \r" if $DEBUG and w.data.pos & 0xffff == 0 h[s] += 1 if s < min: s = -32000 elsif s > max: s = 32000 else s = (s-min) * 64000 / (max-min) - 32000 end ww.data << [s].pack('s') end rescue Interrupt end File.open('out.wav', 'wb') { |fd| fd.write ww } if f != 'out.wav' p [h.keys.min, h.keys.max] all = h.values.inject(0) { |sum, v| sum + v } (-34000...34000).step(2000) { |i| min = i max = i+2000 this = h.find_all { |k, v| k >= min and k <= max }.inject(0) { |sum, (k, v)| sum + v } puts "#{min}:#{max}".ljust(15) + " #{this} (#{(this*10000/all)/100.0}%)" } puts } end