Class: FormatParser::MP3Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/parsers/mp3_parser.rb

Defined Under Namespace

Modules: ID3V1, ID3V2 Classes: InvalidDeepFetch, MP3Info, MPEGFrame, VBRHeader

Constant Summary collapse

MAX_FRAMES_TO_SCAN =

We limit the number of MPEG frames we scan to obtain our duration estimation

128
SAMPLES_PER_FRAME =

Default frame size for mp3

1152
ZIP_LOCAL_ENTRY_SIGNATURE =

For some edge cases

"PK\x03\x04\x14\x00".b

Instance Method Summary collapse

Instance Method Details

#call(io) ⇒ Object



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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/parsers/mp3_parser.rb', line 29

def call(io)
  # Special case: some ZIPs (Office documents) did detect as MP3s.
  # To avoid having that happen, we check for the PKZIP signature -
  # local entry header signature - at the very start of the file
  return if io.read(6) == ZIP_LOCAL_ENTRY_SIGNATURE
  io.seek(0)

  # Read the last 128 bytes which might contain ID3v1
  id3_v1 = ID3V1.attempt_id3_v1_extraction(io)
  # Read the header bytes that might contain ID3v1
  id3_v2 = ID3V2.attempt_id3_v2_extraction(io)

  # Compute how many bytes are occupied by the actual MPEG frames
  ignore_bytes_at_tail = id3_v1 ? 128 : 0
  ignore_bytes_at_head = id3_v2 ? io.pos : 0
  bytes_used_by_frames = io.size - ignore_bytes_at_tail - ignore_bytes_at_tail

  io.seek(ignore_bytes_at_head)

  maybe_xing_header, initial_frames = parse_mpeg_frames(io)

  return if initial_frames.empty?

  first_frame = initial_frames.first

  file_info = FormatParser::Audio.new(
    format: :mp3,
    num_audio_channels: first_frame.channels,
    audio_sample_rate_hz: first_frame.sample_rate,
    # media_duration_frames is omitted because the frames
    # in MPEG are not the same thing as in a movie file - they
    # do not tell anything of substance
    intrinsics: {
      id3_v1: id3_v1 ? id3_v1.to_h : nil,
      id3_v2: id3_v2 ? id3_v2.map(&:to_h) : nil,
      xing_header: maybe_xing_header.to_h,
      initial_frames: initial_frames.map(&:to_h)
    }
  )

  if maybe_xing_header
    duration = maybe_xing_header.frames * SAMPLES_PER_FRAME / first_frame.sample_rate.to_f
    _bit_rate = maybe_xing_header.byte_count * 8 / duration / 1000
    file_info.media_duration_seconds = duration
    return file_info
  end

  # Estimate duration using the frames we did parse - to have an exact one
  # we would need to have all the frames and thus read most of the file
  _avg_bitrate = float_average_over(initial_frames, :frame_bitrate)
  avg_frame_size = float_average_over(initial_frames, :frame_length)
  avg_sample_rate = float_average_over(initial_frames, :sample_rate)

  est_frame_count = bytes_used_by_frames / avg_frame_size
  est_samples = est_frame_count * SAMPLES_PER_FRAME
  est_duration_seconds = est_samples / avg_sample_rate

  # Safeguard for i.e. some JPEGs being recognized as MP3
  # to prevent ambiguous recognition
  return if est_duration_seconds == Float::INFINITY

  file_info.media_duration_seconds = est_duration_seconds
  file_info
end