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 +4 -4
- data/README.md +8 -0
- data/lib/format_parser/version.rb +1 -1
- data/lib/parsers/aac_parser/adts_header_info.rb +138 -0
- data/lib/parsers/aac_parser.rb +35 -0
- data/spec/parsers/aac_parser_spec.rb +87 -0
- data/spec/parsers/adts_header_info_spec.rb +38 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54b56b24c97b2532bc5d7f8521aa38714111a05f2bedd5e15b7391e1005d9795
|
4
|
+
data.tar.gz: e33a026ab2c611a86d6ba7e35fd413455f7b99c423651fa09dcebf08ad543e0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
@@ -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.
|
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-
|
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
|