Class: FormatParser::AIFFParser

Inherits:
Object
  • Object
show all
Includes:
IOUtils
Defined in:
lib/parsers/aiff_parser.rb

Constant Summary collapse

AIFF_MIME_TYPE =
'audio/x-aiff'
KNOWN_CHUNKS =

Known chunk types we can omit when parsing, grossly lifted from www.muratnkonar.com/aiff/

[
  'COMT',
  'INST',
  'MARK',
  'SKIP',
  'SSND',
  'MIDI',
  'AESD',
  'APPL',
  'NAME',
  'AUTH',
  '(c) ', # yes it is a thing
  'ANNO',
]

Constants included from IOUtils

IOUtils::INTEGER_DIRECTIVES

Instance Method Summary collapse

Methods included from IOUtils

#read_bytes, #read_fixed_point, #read_int, #safe_read, #safe_skip, #skip_bytes

Instance Method Details

#call(io) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/parsers/aiff_parser.rb', line 27

def call(io)
  io = FormatParser::IOConstraint.new(io)
  form_chunk_type, chunk_size = safe_read(io, 8).unpack('a4N')
  return unless form_chunk_type == 'FORM' && chunk_size > 4

  fmt_chunk_type = safe_read(io, 4)

  return unless fmt_chunk_type == 'AIFF'

  # There might be COMT chunks, for example in Logic exports
  loop do
    chunk_type, chunk_size = safe_read(io, 8).unpack('a4N')
    case chunk_type
    when 'COMM'
      # The ID is always COMM. The chunkSize field is the number of bytes in the
      # chunk. This does not include the 8 bytes used by ID and Size fields. For
      # the Common Chunk, chunkSize should always 18 since there are no fields of
      # variable length (but to maintain compatibility with possible future
      # extensions, if the chunkSize is > 18, you should always treat those extra
      # bytes as pad bytes).
      return unpack_comm_chunk(io)
    when *KNOWN_CHUNKS
      # We continue looping only if we encountered something that looks like
      # a valid AIFF chunk type - skip the size and continue
      safe_skip(io, chunk_size)
      next
    else # This most likely not an AIFF
      return
    end
  end
end

#likely_match?(filename) ⇒ Boolean

Returns:

  • (Boolean)


23
24
25
# File 'lib/parsers/aiff_parser.rb', line 23

def likely_match?(filename)
  filename =~ /\.aiff?$/i
end

#unpack_comm_chunk(io) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/parsers/aiff_parser.rb', line 59

def unpack_comm_chunk(io)
  # Parse the COMM chunk
  channels, sample_frames, _sample_size, sample_rate_extended = safe_read(io, 2 + 4 + 2 + 10).unpack('nNna10')
  sample_rate = unpack_extended_float(sample_rate_extended)

  return unless sample_frames > 0

  # The sample rate is in Hz, so to get duration in seconds, as a float...
  duration_in_seconds = sample_frames / sample_rate
  return unless duration_in_seconds > 0

  FormatParser::Audio.new(
    format: :aiff,
    num_audio_channels: channels,
    audio_sample_rate_hz: sample_rate.to_i,
    media_duration_frames: sample_frames,
    media_duration_seconds: duration_in_seconds,
    content_type: AIFF_MIME_TYPE,
  )
end

#unpack_extended_float(ten_bytes_string) ⇒ Object



80
81
82
83
84
85
86
87
88
# File 'lib/parsers/aiff_parser.rb', line 80

def unpack_extended_float(ten_bytes_string)
  extended = ten_bytes_string.unpack('B80')[0]

  sign = extended[0, 1]
  exponent = extended[1, 15].to_i(2) - ((1 << 14) - 1)
  fraction = extended[16, 64].to_i(2)

  (sign == '1' ? -1.0 : 1.0) * (fraction.to_f / ((1 << 63) - 1)) * (2**exponent)
end