class AudioFingerprint::WaveFile

Constants

CHUNK_ID
DATA_CHUNK_ID
FORMAT
FORMAT_CHUNK_ID
HEADER_SIZE
PCM
SUB_CHUNK1_SIZE

Attributes

bits_per_sample[R]
block_align[R]
byte_rate[R]
num_channels[R]
sample_rate[RW]

Public Class Methods

new(num_channels, sample_rate, bits_per_sample, sample_data = []) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 16
def initialize(num_channels, sample_rate, bits_per_sample, sample_data = [])
    if num_channels == :mono
        @num_channels = 1
    elsif num_channels == :stereo
        @num_channels = 2
    else
        @num_channels = num_channels
    end
    @sample_rate = sample_rate
    @bits_per_sample = bits_per_sample
    @sample_data = sample_data

    @byte_rate = sample_rate * @num_channels * (bits_per_sample / 8)
    @block_align = @num_channels * (bits_per_sample / 8)
end
open(path) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 32
def self.open(path)
    file = File.open(path, "rb")

    begin
        header = read_header(file)
        errors = validate_header(header)

        if errors == []
            sample_data = read_sample_data(file,
            header[:num_channels],
            header[:bits_per_sample],
            header[:sub_chunk2_size])

            wave_file = self.new(header[:num_channels],
            header[:sample_rate],
            header[:bits_per_sample],
            sample_data)
        else
            error_msg = "#{path} can't be opened, due to the following errors:\n"
            errors.each {|error| error_msg += "  * #{error}\n" }
            raise StandardError, error_msg
        end
    rescue EOFError
        raise StandardError, "An error occured while reading #{path}."
    ensure
        file.close()
    end

    return wave_file
end

Private Class Methods

read_header(file) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 316
def self.read_header(file)
    header = {}

    # Read RIFF header
    riff_header = file.sysread(12).unpack("a4Va4")
    header[:chunk_id] = riff_header[0]
    header[:chunk_size] = riff_header[1]
    header[:format] = riff_header[2]

    # Read format subchunk
    header[:sub_chunk1_id], header[:sub_chunk1_size] = self.read_to_chunk(file, FORMAT_CHUNK_ID)
    format_subchunk_str = file.sysread(header[:sub_chunk1_size])
    format_subchunk = format_subchunk_str.unpack("vvVVvv")  # Any extra parameters are ignored
    header[:audio_format] = format_subchunk[0]
    header[:num_channels] = format_subchunk[1]
    header[:sample_rate] = format_subchunk[2]
    header[:byte_rate] = format_subchunk[3]
    header[:block_align] = format_subchunk[4]
    header[:bits_per_sample] = format_subchunk[5]

    # Read data subchunk
    header[:sub_chunk2_id], header[:sub_chunk2_size] = self.read_to_chunk(file, DATA_CHUNK_ID)

    return header
end
read_sample_data(file, num_channels, bits_per_sample, sample_data_size) click to toggle source

Assumes that file is “queued up” to the first sample

# File lib/audio_fingerprint/wave_file.rb, line 392
def self.read_sample_data(file, num_channels, bits_per_sample, sample_data_size)
    if(bits_per_sample == 8)
        data = file.sysread(sample_data_size).unpack("C*")
    elsif(bits_per_sample == 16)
        data = file.sysread(sample_data_size).unpack("s*")
    else
        data = []
    end

    if(num_channels > 1)
        multichannel_data = []

        i = 0
        while i < data.length
            multichannel_data << data[i...(num_channels + i)]
            i += num_channels
        end

        data = multichannel_data
    end

    return data
end
read_to_chunk(file, expected_chunk_id) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 342
def self.read_to_chunk(file, expected_chunk_id)
    chunk_id = file.sysread(4)
    chunk_size = file.sysread(4).unpack("V")[0]

    while chunk_id != expected_chunk_id
        # Skip chunk
        file.sysread(chunk_size)

        chunk_id = file.sysread(4)
        chunk_size = file.sysread(4).unpack("V")[0]
    end

    return chunk_id, chunk_size
end
validate_header(header) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 357
def self.validate_header(header)
    errors = []

    unless header[:bits_per_sample] == 8  ||  header[:bits_per_sample] == 16
        errors << "Invalid bits per sample of #{header[:bits_per_sample]}. Only 8 and 16 are supported."
    end

    unless (1..65535) === header[:num_channels]
        errors << "Invalid number of channels. Must be between 1 and 65535."
    end

    unless header[:chunk_id] == CHUNK_ID
        errors << "Unsupported chunk ID: '#{header[:chunk_id]}'"
    end

    unless header[:format] == FORMAT
        errors << "Unsupported format: '#{header[:format]}'"
    end

    unless header[:sub_chunk1_id] == FORMAT_CHUNK_ID
        errors << "Unsupported chunk id: '#{header[:sub_chunk1_id]}'"
    end

    unless header[:audio_format] == PCM
        errors << "Unsupported audio format code: '#{header[:audio_format]}'"
    end

    unless header[:sub_chunk2_id] == DATA_CHUNK_ID
        errors << "Unsupported chunk id: '#{header[:sub_chunk2_id]}'"
    end

    return errors
end

Public Instance Methods

bits_per_sample=(new_bits_per_sample) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 233
def bits_per_sample=(new_bits_per_sample)
    if new_bits_per_sample != 8 && new_bits_per_sample != 16
        raise StandardError, "Bits per sample of #{@bits_per_samples} is invalid, only 8 or 16 are supported"
    end

    if @bits_per_sample == 16 && new_bits_per_sample == 8
        conversion_func = lambda {|sample|
            if(sample < 0)
                (sample / 256) + 128
            else
                # Faster to just divide by integer 258?
                (sample / 258.007874015748031).round + 128
            end
        }

        if mono?
            @sample_data.map! &conversion_func
        else
            sample_data.map! {|sample| sample.map! &conversion_func }
        end
        elsif @bits_per_sample == 8 && new_bits_per_sample == 16
            conversion_func = lambda {|sample|
                sample -= 128
                if(sample < 0)
                    sample * 256
                else
                    # Faster to just multiply by integer 258?
                    (sample * 258.007874015748031).round
                end
            }

        if mono?
            @sample_data.map! &conversion_func
        else
            sample_data.map! {|sample| sample.map! &conversion_func }
        end
    end

    @bits_per_sample = new_bits_per_sample
end
duration() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 205
def duration()
    total_samples = sample_data.length
    samples_per_millisecond = @sample_rate / 1000.0
    samples_per_second = @sample_rate
    samples_per_minute = samples_per_second * 60
    samples_per_hour = samples_per_minute * 60
    hours, minutes, seconds, milliseconds = 0, 0, 0, 0

    if(total_samples >= samples_per_hour)
        hours = total_samples / samples_per_hour
        total_samples -= samples_per_hour * hours
    end

    if(total_samples >= samples_per_minute)
        minutes = total_samples / samples_per_minute
        total_samples -= samples_per_minute * minutes
    end

    if(total_samples >= samples_per_second)
        seconds = total_samples / samples_per_second
        total_samples -= samples_per_second * seconds
    end

    milliseconds = (total_samples / samples_per_millisecond).floor

    return  { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
end
inspect() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 299
def inspect()
    duration = self.duration()

    result =  "Channels:        #{@num_channels}\n" +
                "Sample rate:     #{@sample_rate}\n" +
                "Bits per sample: #{@bits_per_sample}\n" +
                "Block align:     #{@block_align}\n" +
                "Byte rate:       #{@byte_rate}\n" +
                "Sample count:    #{@sample_data.length}\n" +
                "Duration:        #{duration[:hours]}h:#{duration[:minutes]}m:#{duration[:seconds]}s:#{duration[:milliseconds]}ms\n"
end
mono?() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 193
def mono?()
    return num_channels == 1
end
normalized_sample_data() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 112
def normalized_sample_data()    
    if @bits_per_sample == 8
        min_value = 128.0
        max_value = 127.0
        midpoint = 128
    elsif @bits_per_sample == 16
        min_value = 32768.0
        max_value = 32767.0
        midpoint = 0
    else
        raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
    end

    if mono?
        normalized_sample_data = @sample_data.map {|sample|
            sample -= midpoint
            if sample < 0
                sample.to_f / min_value
            else
                sample.to_f / max_value
            end
        }
    else
        normalized_sample_data = @sample_data.map {|sample|
            sample.map {|sub_sample|
                sub_sample -= midpoint
                if sub_sample < 0
                    sub_sample.to_f / min_value
                else
                    sub_sample.to_f / max_value
                end
            }
        }
    end

    return normalized_sample_data
end
num_channels=(new_num_channels) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 274
def num_channels=(new_num_channels)
    if new_num_channels == :mono
        new_num_channels = 1
    elsif new_num_channels == :stereo
        new_num_channels = 2
    end

    # The cases of mono -> stereo and vice-versa are handled in specially,
    # because those conversion methods are faster than the general methods,
    # and the large majority of wave files are expected to be either mono or stereo.
    if @num_channels == 1 && new_num_channels == 2
        sample_data.map! {|sample| [sample, sample]}
    elsif @num_channels == 2 && new_num_channels == 1
        sample_data.map! {|sample| (sample[0] + sample[1]) / 2}
    elsif @num_channels == 1 && new_num_channels >= 2
        sample_data.map! {|sample| [].fill(sample, 0, new_num_channels)}
    elsif @num_channels >= 2 && new_num_channels == 1
        sample_data.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / @num_channels }
    elsif @num_channels > 2 && new_num_channels == 2
        sample_data.map! {|sample| [sample[0], sample[1]]}
    end

    @num_channels = new_num_channels
end
reverse() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 201
def reverse()
    sample_data.reverse!()
end
sample_data() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 108
def sample_data()
    return @sample_data
end
sample_data=(sample_data) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 150
def sample_data=(sample_data)
    if sample_data.length > 0 && ((mono? && sample_data[0].class == Float) ||
                                (!mono? && sample_data[0][0].class == Float))
    if @bits_per_sample == 8
        # Samples in 8-bit wave files are stored as a unsigned byte
        # Effective values are 0 to 255, midpoint at 128
        min_value = 128.0
        max_value = 127.0
        midpoint = 128
    elsif @bits_per_sample == 16
        # Samples in 16-bit wave files are stored as a signed little-endian short
        # Effective values are -32768 to 32767, midpoint at 0
        min_value = 32768.0
        max_value = 32767.0
        midpoint = 0
    else
        raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
    end

    if mono?
        @sample_data = sample_data.map {|sample|
            if(sample < 0.0)
            (sample * min_value).round + midpoint
            else
            (sample * max_value).round + midpoint
            end
        }
    else
        @sample_data = sample_data.map {|sample|
            sample.map {|sub_sample|
                if(sub_sample < 0.0)
                    (sub_sample * min_value).round + midpoint
                else
                    (sub_sample * max_value).round + midpoint
                end
            }
        }
    end
    else
        @sample_data = sample_data
    end
end
save(path) click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 63
def save(path)
    # All numeric values should be saved in little-endian format

    sample_data_size = @sample_data.length * @num_channels * (@bits_per_sample / 8)

    # Write the header
    file_contents = CHUNK_ID
    file_contents += [HEADER_SIZE + sample_data_size].pack("V")
    file_contents += FORMAT
    file_contents += FORMAT_CHUNK_ID
    file_contents += [SUB_CHUNK1_SIZE].pack("V")
    file_contents += [PCM].pack("v")
    file_contents += [@num_channels].pack("v")
    file_contents += [@sample_rate].pack("V")
    file_contents += [@byte_rate].pack("V")
    file_contents += [@block_align].pack("v")
    file_contents += [@bits_per_sample].pack("v")
    file_contents += DATA_CHUNK_ID
    file_contents += [sample_data_size].pack("V")

    # Write the sample data
    if !mono?
        output_sample_data = []
        @sample_data.each{|sample|
            sample.each{|sub_sample|
                output_sample_data << sub_sample
            }
        }
    else
        output_sample_data = @sample_data
    end

    if @bits_per_sample == 8
        file_contents += output_sample_data.pack("C*")
    elsif @bits_per_sample == 16
        file_contents += output_sample_data.pack("s*")
    else
        raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
    end

    file = File.open(path, "w")
    file.syswrite(file_contents)
    file.close
end
stereo?() click to toggle source
# File lib/audio_fingerprint/wave_file.rb, line 197
def stereo?()
    return num_channels == 2
end