class FormatParser::WAVParser include FormatParser::IOUtils include FormatParser::DSL natures :audio formats :wav def call(io) # Read the RIFF header. Chunk descriptor should be RIFF, the size should # contain the size of the entire file in bytes minus 8 bytes for the # two fields not included in this count: chunk_id and size. chunk_id, _size, riff_type = safe_read(io, 12).unpack('a4la4') # The chunk_id and riff_type should be RIFF and WAVE respectively return unless chunk_id == 'RIFF' && riff_type == 'WAVE' # There are no restrictions upon the order of the chunks within a WAVE file, # with the exception that the Format chunk must precede the Data chunk. # The specification does not require the Format chunk to be the first chunk # after the RIFF header. # http://soundfile.sapp.org/doc/WaveFormat/ # For WAVE files containing PCM audio format we parse the 'fmt ' and # 'data' chunks while for non PCM audio formats the 'fmt ' and 'fact' # chunks. In the latter case the order fo appearence of the chunks is # arbitrary. fmt_processed = false fact_processed = false fmt_data = {} total_sample_frames = 0 loop do chunk_type, chunk_size = safe_read(io, 8).unpack('a4l') case chunk_type when 'fmt ' # watch out: the chunk ID of the format chunk ends with a space fmt_data = unpack_fmt_chunk(io, chunk_size) if fmt_data[:audio_format] != 1 and fact_processed return process_non_pcm(fmt_data, total_sample_frames) end fmt_processed = true when 'data' return unless fmt_processed # the 'data' chunk cannot preceed the 'fmt ' chunk return process_pcm(fmt_data, chunk_size) if fmt_data[:audio_format] == 1 safe_skip(io, chunk_size) when 'fact' total_sample_frames = safe_read(io, 4).unpack('l').first safe_skip(io, chunk_size - 4) if fmt_processed and fmt_data[:audio_format] != 1 return process_non_pcm(fmt_data, total_sample_frames) end fact_processed = true else # Skip this chunk until a known chunk is encountered safe_skip(io, chunk_size) end end end def unpack_fmt_chunk(io, chunk_size) # The size of the fmt chunk is at least 16 bytes. If the format tag's value is not # 1 compression might be in use for storing the data # and the fmt chunk might contain extra fields appended to it. # The last 4 fields of the fmt tag are always: # * unsigned short channels # * unsigned long samples per sec # * unsigned long average bytes per sec # * unsigned short block align # * unsigned short bits per sample fmt_info = safe_read(io, 16).unpack('S_2I2S_2') safe_skip(io, chunk_size - 16) # skip the extra fields { audio_format: fmt_info[0], channels: fmt_info[1], sample_rate: fmt_info[2], byte_rate: fmt_info[3], block_align: fmt_info[4], bits_per_sample: fmt_info[5], } end def process_pcm(fmt_data, data_size) return unless fmt_data[:channels] > 0 and fmt_data[:bits_per_sample] > 0 sample_frames = data_size / (fmt_data[:channels] * fmt_data[:bits_per_sample] / 8) file_info(fmt_data, sample_frames) end def process_non_pcm(fmt_data, total_sample_frames) file_info(fmt_data, total_sample_frames) end def file_info(fmt_data, sample_frames) return unless fmt_data[:sample_rate] > 0 duration_in_seconds = sample_frames / fmt_data[:sample_rate].to_f FormatParser::Audio.new( format: :wav, num_audio_channels: fmt_data[:channels], audio_sample_rate_hz: fmt_data[:sample_rate], media_duration_frames: sample_frames, media_duration_seconds: duration_in_seconds, ) end FormatParser.register_parser_constructor self end