format_parser 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3f9d1c51523af6efea2ed6f8cf63f0f573e2b0b140c1af0435e6bb26961953c
4
- data.tar.gz: 84b73895b8924a0cffa286a4adaa3c270951e4074ec09db718f1a8d482b8e14b
3
+ metadata.gz: 54b56b24c97b2532bc5d7f8521aa38714111a05f2bedd5e15b7391e1005d9795
4
+ data.tar.gz: e33a026ab2c611a86d6ba7e35fd413455f7b99c423651fa09dcebf08ad543e0a
5
5
  SHA512:
6
- metadata.gz: cfa0a69fd35d8d3c05fff79cca1b550af8b8c6876fece5443fc41f7967fed15fe20d494ce5678e16cb2a1cdd1b0918b8ce61ef4b731f3e47b18c736ca21cdf0c
7
- data.tar.gz: 0f7747775606681981367432322bb7b63e2e3c862fcd506d0b843bfc2280ae3d8dcdf334f49a9150f27a181329cf993f9cf79036d8124e75d41ca01f26edebd6
6
+ metadata.gz: e27ec936c4b43cc6c82f896846ef3e8044a0639b18c1c2accf7bbdeffd0fe73d9c03fb2f2e9b4a477ef1f98a1043047042b8521f186e3406ca1c482be9a66abd
7
+ data.tar.gz: a7927cfb5fbf0a41980465186809fb830b322e83db56bab15b5a2ad897031f7269eeae85d7e61f489a5cc2e6737348309e4f93c5dee27238d06a3ed0979f0d43
data/README.md CHANGED
@@ -149,6 +149,14 @@ Unless specified otherwise in this section the fixture files are MIT licensed an
149
149
  ### MP3
150
150
  - Cassy.mp3 has been produced by WeTransfer and may be used with the library for the purposes of testing
151
151
 
152
+ ### AAC
153
+ - Originals music files: “Furious Freak” and “Galway”, Kevin MacLeod (incompetech.com), Licensed under Creative Commons: By Attribution 3.0, http://creativecommons.org/licenses/by/3.0/
154
+ - The AAC samples were converted from 'wav' format and made available [here](https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/design-guide/audio-samples.html) by Espressif Systems, as part of their audio development framework (under the ESPRESSIF MIT License).
155
+ - Files:
156
+ - ff-16b-2c-44100hz.aac
157
+ - ff-16b-1c-44100hz.aac
158
+ - gs-16b-2c-44100hz.aac
159
+ - gs-16b-1c-44100hz.aac
152
160
  ### FDX
153
161
  - fixture.fdx was created by one of the project maintainers and is MIT licensed
154
162
 
@@ -1,3 +1,3 @@
1
1
  module FormatParser
2
- VERSION = '1.5.0'
2
+ VERSION = '1.6.0'
3
3
  end
@@ -0,0 +1,138 @@
1
+ # This is a representation of the relevant information found in an Audio Data Transport Stream (ADTS) file header.
2
+ class FormatParser::AdtsHeaderInfo
3
+ attr_accessor :mpeg_version, :layer, :protection_absence, :profile, :mpeg4_sampling_frequency_index,
4
+ :mpeg4_channel_config, :originality, :home_usage, :frame_length, :buffer_fullness,
5
+ :aac_frames_per_adts_frame
6
+
7
+ # An ADTS header has the following format, when represented in bits:
8
+ # AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
9
+ # The chunks represented by these letters have specific meanings, as described here:
10
+ # https://wiki.multimedia.cx/index.php/ADTS
11
+
12
+ AAC_ADTS_HEADER_BITS_CHUNK_SIZES = [
13
+ ['A', 12], ['B', 1], ['C', 2], ['D', 1],
14
+ ['E', 2], ['F', 4], ['G', 1], ['H', 3],
15
+ ['I', 1], ['J', 1], ['K', 1], ['L', 1],
16
+ ['M', 13], ['O', 11], ['P', 2], ['Q', 16]
17
+ ]
18
+ MPEG4_AUDIO_OBJECT_TYPE_RANGE = 0..45
19
+ MPEG4_AUDIO_SAMPLING_FREQUENCY_RANGE = 0..14
20
+ MPEG4_AUDIO_SAMPLING_FREQUENCY_HASH = {
21
+ 0 => 96000, 1 => 88200, 2 => 64000,
22
+ 3 => 48000, 4 => 44100, 5 => 32000,
23
+ 6 => 24000, 7 => 22050, 8 => 16000,
24
+ 9 => 12000, 10 => 11025, 11 => 8000,
25
+ 12 => 7350, 13 => 'Reserved', 14 => 'Reserved'
26
+ }
27
+ AAC_PROFILE_DESCRIPTION_HASH = {
28
+ 0 => 'AAC_MAIN',
29
+ 1 => 'AAC_LC (Low Complexity)',
30
+ 2 => 'AAC_SSR (Scaleable Sampling Rate)',
31
+ 3 => 'AAC_LTP (Long Term Prediction)'
32
+ }
33
+ MPEG_VERSION_HASH = { 0 => 'MPEG-4', 1 => 'MPEG-2'}
34
+
35
+ def mpeg4_sampling_frequency
36
+ if !@mpeg4_sampling_frequency_index.nil? && MPEG4_AUDIO_SAMPLING_FREQUENCY_HASH.key?(@mpeg4_sampling_frequency_index)
37
+ return MPEG4_AUDIO_SAMPLING_FREQUENCY_HASH[@mpeg4_sampling_frequency_index]
38
+ end
39
+ nil
40
+ end
41
+
42
+ def profile_description
43
+ if !@profile.nil? && AAC_PROFILE_DESCRIPTION_HASH.key?(@profile)
44
+ return AAC_PROFILE_DESCRIPTION_HASH[@profile]
45
+ end
46
+ nil
47
+ end
48
+
49
+ def mpeg_version_description
50
+ if !@mpeg_version.nil? && MPEG_VERSION_HASH.key?(@mpeg_version)
51
+ return MPEG_VERSION_HASH[@mpeg_version]
52
+ end
53
+ nil
54
+ end
55
+
56
+ def number_of_audio_channels
57
+ case @mpeg4_channel_config
58
+ when 1..6
59
+ @mpeg4_channel_config
60
+ when 7
61
+ 8
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ def fixed_bitrate?
68
+ # A buffer fullness value of 0x7FF (decimal: 2047) denotes a variable bitrate, for which buffer fullness isn't applicable
69
+ @buffer_fullness != 2047
70
+ end
71
+
72
+ # The frame rate - i.e. frames per second
73
+ def frame_rate
74
+ # An AAC sample uncompresses to 1024 PCM samples
75
+ mpeg4_sampling_frequency.to_f / 1024
76
+ end
77
+
78
+ # If the given bit array is a valid ADTS header, this method will parse it and return an instance of AdtsHeaderInfo.
79
+ # Will return nil if the header does not match the ADTS specifications.
80
+ def self.parse_adts_header(header_bits)
81
+ result = FormatParser::AdtsHeaderInfo.new
82
+
83
+ AAC_ADTS_HEADER_BITS_CHUNK_SIZES.each do |letter_size|
84
+ letter = letter_size[0]
85
+ chunk_size = letter_size[1]
86
+ chunk = header_bits.shift(chunk_size)
87
+ decimal_number = chunk.join.to_i(2)
88
+
89
+ # Skipping data represented by the letters G, K, L, Q, as we are not interested in those values.
90
+ case letter
91
+ when 'A'
92
+ # Syncword, all bits must be set to 1
93
+ return nil unless chunk.all? { |bit| bit == '1' }
94
+ when 'B'
95
+ # MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2
96
+ result.mpeg_version = decimal_number
97
+ when 'C'
98
+ # Layer, always set to 0
99
+ return nil unless decimal_number == 0
100
+ when 'D'
101
+ # Protection absence, set to 1 if there is no CRC and 0 if there is CRC
102
+ result.protection_absence = decimal_number == 1
103
+ when 'E'
104
+ # AAC Profile
105
+ return nil unless MPEG4_AUDIO_OBJECT_TYPE_RANGE.include?(decimal_number + 1)
106
+ result.profile = decimal_number
107
+ when 'F'
108
+ # MPEG-4 Sampling Frequency Index (15 is forbidden)
109
+ return nil unless MPEG4_AUDIO_SAMPLING_FREQUENCY_RANGE.include?(decimal_number)
110
+ result.mpeg4_sampling_frequency_index = decimal_number
111
+ when 'H'
112
+ # MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an in-band PCE (Program Config Element))
113
+ result.mpeg4_channel_config = decimal_number
114
+ when 'I'
115
+ # Originality, set to 1 to signal originality of the audio and 0 otherwise
116
+ result.originality = decimal_number == 1
117
+ when 'J'
118
+ # Home, set to 1 to signal home usage of the audio and 0 otherwise
119
+ result.home_usage = decimal_number == 1
120
+ when 'M'
121
+ # Frame length, length of the ADTS frame including headers and CRC check (protectionabsent == 1? 7: 9)
122
+ # We expect this to be higher than the header length, but we won't impose any other restrictions
123
+ header_length = result.protection_absence ? 7 : 9
124
+ return nil unless decimal_number > header_length
125
+ result.frame_length = decimal_number
126
+ when 'O'
127
+ # Buffer fullness, states the bit-reservoir per frame.
128
+ # It is merely an informative field with no clear use case defined in the specification.
129
+ result.buffer_fullness = decimal_number
130
+ when 'P'
131
+ # Number of AAC frames (RDBs (Raw Data Blocks)) in ADTS frame minus 1. For maximum compatibility always use one AAC frame per ADTS frame.
132
+ result.aac_frames_per_adts_frame = decimal_number + 1
133
+ end
134
+ end
135
+
136
+ result
137
+ end
138
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'aac_parser/adts_header_info'
2
+
3
+ class FormatParser::AACParser
4
+ include FormatParser::IOUtils
5
+
6
+ AAC_MIME_TYPE = 'audio/aac'
7
+
8
+ def likely_match?(filename)
9
+ filename =~ /\.aac$/i
10
+ end
11
+
12
+ def call(raw_io)
13
+ io = FormatParser::IOConstraint.new(raw_io)
14
+ header = safe_read(io, 9)
15
+ header_bits = header.unpack('B*').first.split('')
16
+
17
+ header_info = FormatParser::AdtsHeaderInfo.parse_adts_header(header_bits)
18
+ return if header_info.nil?
19
+
20
+ FormatParser::Audio.new(
21
+ title: nil,
22
+ album: nil,
23
+ artist: nil,
24
+ format: :aac,
25
+ num_audio_channels: header_info.number_of_audio_channels,
26
+ audio_sample_rate_hz: header_info.mpeg4_sampling_frequency,
27
+ media_duration_seconds: nil,
28
+ media_duration_frames: nil,
29
+ intrinsics: nil,
30
+ content_type: AAC_MIME_TYPE
31
+ )
32
+ end
33
+
34
+ FormatParser.register_parser new, natures: :audio, formats: :aac
35
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe FormatParser::AACParser do
4
+ it 'should match filenames with valid AAC extensions' do
5
+ filenames = ['audiofile', 'audio_file', 'audio-file', 'audio file', 'audio.file']
6
+ extensions = ['.aac', '.AAC', '.Aac', '.AAc', '.aAc', '.aAC', '.aaC']
7
+ filenames.each do |filename|
8
+ extensions.each do |extension|
9
+ expect(subject.likely_match?(filename + extension)).to be_truthy
10
+ end
11
+ end
12
+ end
13
+
14
+ it 'should not match filenames with invalid AAC extensions' do
15
+ extensions = ['.aa', '.ac', '.acc', '.mp3', '.ogg', '.wav', '.flac', '.m4a', '.m4b', '.m4p', '.m4r', '.3gp']
16
+ extensions.each do |extension|
17
+ expect(subject.likely_match?('audiofile' + extension)).to be_falsey
18
+ end
19
+ end
20
+
21
+ it 'should parse a short sample, single channel audio, 16 kb/s, 44100 HZ' do
22
+ file_path = fixtures_dir + '/AAC/gs-16b-1c-44100hz.aac'
23
+ parsed = subject.call(File.open(file_path, 'rb'))
24
+
25
+ expect(parsed).not_to be_nil
26
+
27
+ expect(parsed.nature).to eq(:audio)
28
+ expect(parsed.format).to eq(:aac)
29
+ expect(parsed.num_audio_channels).to eq(1)
30
+ expect(parsed.audio_sample_rate_hz).to eq(44100)
31
+ expect(parsed.content_type).to eq('audio/aac')
32
+ end
33
+
34
+ it 'should parse a short sample, two channel audio, 16 kb/s, 44100 HZ' do
35
+ file_path = fixtures_dir + '/AAC/gs-16b-2c-44100hz.aac'
36
+ parsed = subject.call(File.open(file_path, 'rb'))
37
+
38
+ expect(parsed).not_to be_nil
39
+
40
+ expect(parsed.nature).to eq(:audio)
41
+ expect(parsed.format).to eq(:aac)
42
+ expect(parsed.num_audio_channels).to eq(2)
43
+ expect(parsed.audio_sample_rate_hz).to eq(44100)
44
+ expect(parsed.content_type).to eq('audio/aac')
45
+ end
46
+
47
+ it 'should parse a long sample, single channel audio, 16 kb/s, 44100 HZ' do
48
+ file_path = fixtures_dir + '/AAC/ff-16b-1c-44100hz.aac'
49
+ parsed = subject.call(File.open(file_path, 'rb'))
50
+
51
+ expect(parsed).not_to be_nil
52
+
53
+ expect(parsed.nature).to eq(:audio)
54
+ expect(parsed.format).to eq(:aac)
55
+ expect(parsed.num_audio_channels).to eq(1)
56
+ expect(parsed.audio_sample_rate_hz).to eq(44100)
57
+ expect(parsed.content_type).to eq('audio/aac')
58
+ end
59
+
60
+ it 'should parse a long sample, two channel audio, 16 kb/s, 44100 HZ' do
61
+ file_path = fixtures_dir + '/AAC/ff-16b-2c-44100hz.aac'
62
+ parsed = subject.call(File.open(file_path, 'rb'))
63
+
64
+ expect(parsed).not_to be_nil
65
+
66
+ expect(parsed.nature).to eq(:audio)
67
+ expect(parsed.format).to eq(:aac)
68
+ expect(parsed.num_audio_channels).to eq(2)
69
+ expect(parsed.audio_sample_rate_hz).to eq(44100)
70
+ expect(parsed.content_type).to eq('audio/aac')
71
+ end
72
+
73
+ shared_examples 'invalid filetype' do |filetype, fixture_path|
74
+ it "should fail to parse #{filetype}" do
75
+ file_path = fixtures_dir + fixture_path
76
+ parsed = subject.call(File.open(file_path, 'rb'))
77
+ expect(parsed).to be_nil
78
+ end
79
+ end
80
+
81
+ include_examples 'invalid filetype', 'AIFF', '/AIFF/fixture.aiff'
82
+ include_examples 'invalid filetype', 'FLAC', '/FLAC/atc_fixture_vbr.flac'
83
+ include_examples 'invalid filetype', 'MP3', '/MP3/Cassy.mp3'
84
+ include_examples 'invalid filetype', 'MPG', '/MPG/video1.mpg'
85
+ include_examples 'invalid filetype', 'OGG', '/Ogg/hi.ogg'
86
+ include_examples 'invalid filetype', 'WAV', '/WAV/c_8kmp316.wav'
87
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe FormatParser::AdtsHeaderInfo do
4
+ shared_examples 'parsed header' do |header_bits, expected_mpeg_version_description, expected_protection_absence, expected_profile_description, expected_mpeg4_sampling_frequency, expected_mpeg4_channel_config, expected_number_of_audio_channels, expected_originality, expected_home_usage, expected_frame_length, expected_aac_frames_per_adts_frame, expected_has_fixed_bitrate|
5
+ it "extracts correct values for header #{header_bits}" do
6
+ result = FormatParser::AdtsHeaderInfo.parse_adts_header(header_bits.split(''))
7
+ expect(result).not_to be_nil
8
+ expect(result.mpeg_version_description).to eq(expected_mpeg_version_description)
9
+ expect(result.protection_absence).to eq(expected_protection_absence)
10
+ expect(result.profile_description).to eq(expected_profile_description)
11
+ expect(result.mpeg4_sampling_frequency).to eq(expected_mpeg4_sampling_frequency)
12
+ expect(result.mpeg4_channel_config).to eq(expected_mpeg4_channel_config)
13
+ expect(result.number_of_audio_channels).to eq(expected_number_of_audio_channels)
14
+ expect(result.originality).to eq(expected_originality)
15
+ expect(result.home_usage).to eq(expected_home_usage)
16
+ expect(result.frame_length).to eq(expected_frame_length)
17
+ expect(result.aac_frames_per_adts_frame).to eq(expected_aac_frames_per_adts_frame)
18
+ expect(result.fixed_bitrate?).to eq(expected_has_fixed_bitrate)
19
+ end
20
+ end
21
+
22
+ shared_examples 'invalid header' do |failure_reason, header_bits|
23
+ it "fails on #{failure_reason} for header #{header_bits}" do
24
+ result = FormatParser::AdtsHeaderInfo.parse_adts_header(header_bits.split(''))
25
+ expect(result).to be_nil
26
+ end
27
+ end
28
+
29
+ # These headers have been validated here: https://www.p23.nl/projects/aac-header/
30
+ include_examples 'parsed header', '1111111111110001010111001000000000101110011111111111110000100001', 'MPEG-4', true, 'AAC_LC (Low Complexity)', 22050, 2, 2, false, false, 371, 1, false
31
+ include_examples 'parsed header', '111111111111000101010000010000000000011110011111111111001101111000000010', 'MPEG-4', true, 'AAC_LC (Low Complexity)', 44100, 1, 1, false, false, 60, 1, false
32
+
33
+ include_examples 'invalid header', 'invalid syncword', '1111110111110001010111001000000000101110011111111111110000100001'
34
+ include_examples 'invalid header', 'invalid layer value', '1111111111110011010111001000000000101110011111111111110000100001'
35
+ include_examples 'invalid header', 'invalid sampling frequency index 15', '1111111111110001011111001000000000101110011111111111110000100001'
36
+ include_examples 'invalid header', 'zero frame length', '1111111111110001010111001000000000000000011111111111110000100001'
37
+ include_examples 'invalid header', 'random header', '101000101011010101010101111010101010101011001010101010101111000000011101'
38
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: format_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2022-08-23 00:00:00.000000000 Z
12
+ date: 2022-09-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ks
@@ -229,6 +229,8 @@ files:
229
229
  - lib/image.rb
230
230
  - lib/io_constraint.rb
231
231
  - lib/io_utils.rb
232
+ - lib/parsers/aac_parser.rb
233
+ - lib/parsers/aac_parser/adts_header_info.rb
232
234
  - lib/parsers/aiff_parser.rb
233
235
  - lib/parsers/bmp_parser.rb
234
236
  - lib/parsers/cr2_parser.rb
@@ -273,6 +275,8 @@ files:
273
275
  - spec/hash_utils_spec.rb
274
276
  - spec/integration/active_storage/rails_app.rb
275
277
  - spec/io_utils_spec.rb
278
+ - spec/parsers/aac_parser_spec.rb
279
+ - spec/parsers/adts_header_info_spec.rb
276
280
  - spec/parsers/aiff_parser_spec.rb
277
281
  - spec/parsers/bmp_parser_spec.rb
278
282
  - spec/parsers/cr2_parser_spec.rb