wahwah 0.1.0 → 1.2.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/LICENSE +21 -0
- data/lib/wahwah/asf/object.rb +34 -0
- data/lib/wahwah/asf_tag.rb +221 -0
- data/lib/wahwah/errors.rb +7 -0
- data/lib/wahwah/flac/block.rb +51 -0
- data/lib/wahwah/flac/streaminfo_block.rb +51 -0
- data/lib/wahwah/flac_tag.rb +86 -0
- data/lib/wahwah/helper.rb +37 -0
- data/lib/wahwah/id3/comment_frame_body.rb +21 -0
- data/lib/wahwah/id3/frame.rb +176 -0
- data/lib/wahwah/id3/frame_body.rb +35 -0
- data/lib/wahwah/id3/genre_frame_body.rb +15 -0
- data/lib/wahwah/id3/image_frame_body.rb +60 -0
- data/lib/wahwah/id3/text_frame_body.rb +16 -0
- data/lib/wahwah/id3/v1.rb +97 -0
- data/lib/wahwah/id3/v2.rb +67 -0
- data/lib/wahwah/id3/v2_header.rb +53 -0
- data/lib/wahwah/lazy_read.rb +40 -0
- data/lib/wahwah/mp3/mpeg_frame_header.rb +143 -0
- data/lib/wahwah/mp3/vbri_header.rb +47 -0
- data/lib/wahwah/mp3/xing_header.rb +45 -0
- data/lib/wahwah/mp3_tag.rb +111 -0
- data/lib/wahwah/mp4/atom.rb +101 -0
- data/lib/wahwah/mp4_tag.rb +137 -0
- data/lib/wahwah/ogg/flac_tag.rb +37 -0
- data/lib/wahwah/ogg/opus_tag.rb +33 -0
- data/lib/wahwah/ogg/packets.rb +41 -0
- data/lib/wahwah/ogg/page.rb +121 -0
- data/lib/wahwah/ogg/pages.rb +24 -0
- data/lib/wahwah/ogg/vorbis_comment.rb +51 -0
- data/lib/wahwah/ogg/vorbis_tag.rb +35 -0
- data/lib/wahwah/ogg_tag.rb +66 -0
- data/lib/wahwah/riff/chunk.rb +50 -0
- data/lib/wahwah/riff_tag.rb +142 -0
- data/lib/wahwah/tag.rb +71 -0
- data/lib/wahwah/tag_delegate.rb +16 -0
- data/lib/wahwah/version.rb +4 -2
- data/lib/wahwah.rb +78 -2
- metadata +109 -24
- data/.gitignore +0 -8
- data/.travis.yml +0 -5
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -22
- data/README.md +0 -35
- data/Rakefile +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/wahwah.gemspec +0 -27
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Mp3
|
5
|
+
# mpeg frame header structure:
|
6
|
+
#
|
7
|
+
# Position Length Meaning
|
8
|
+
# 0 11 Frame sync to find the header (all bits are always set)
|
9
|
+
#
|
10
|
+
# 11 2 Audio version ID
|
11
|
+
# 00 - MPEG Version 2.5 (unofficial extension of MPEG 2)
|
12
|
+
# 01 - reserved
|
13
|
+
# 10 - MPEG Version 2 (ISO/IEC 13818-3)
|
14
|
+
# 11 - MPEG Version 1 (ISO/IEC 11172-3)
|
15
|
+
#
|
16
|
+
# 13 2 Layer index
|
17
|
+
# 00 - reserved
|
18
|
+
# 01 - Layer III
|
19
|
+
# 10 - Layer II
|
20
|
+
# 11 - Layer I
|
21
|
+
#
|
22
|
+
# 15 1 Protection bit
|
23
|
+
#
|
24
|
+
# 16 4 Bitrate index, see FRAME_BITRATE_INDEX constant
|
25
|
+
#
|
26
|
+
# 20 2 Sampling rate index, see SAMPLE_RATE_INDEX constant
|
27
|
+
#
|
28
|
+
# 22 1 Padding bit
|
29
|
+
#
|
30
|
+
# 23 1 Private bit
|
31
|
+
#
|
32
|
+
# 24 2 Channel mode
|
33
|
+
# 00 - Stereo
|
34
|
+
# 01 - Joint Stereo (Stereo)
|
35
|
+
# 10 - Dual channel (Two mono channels)
|
36
|
+
# 11 - Single channel (Mono)
|
37
|
+
#
|
38
|
+
# 26 2 Mode extension (Only used in Joint Stereo)
|
39
|
+
#
|
40
|
+
# 28 1 Copyright bit (only informative)
|
41
|
+
#
|
42
|
+
# 29 1 Original bit (only informative)
|
43
|
+
#
|
44
|
+
# 30 2 Emphasis
|
45
|
+
class MpegFrameHeader
|
46
|
+
HEADER_SIZE = 4
|
47
|
+
|
48
|
+
FRAME_BITRATE_INDEX = {
|
49
|
+
"MPEG1 layer1" => [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
|
50
|
+
"MPEG1 layer2" => [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
|
51
|
+
"MPEG1 layer3" => [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
52
|
+
|
53
|
+
"MPEG2 layer1" => [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
|
54
|
+
"MPEG2 layer2" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
|
55
|
+
"MPEG2 layer3" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
|
56
|
+
|
57
|
+
"MPEG2.5 layer1" => [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
|
58
|
+
"MPEG2.5 layer2" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
|
59
|
+
"MPEG2.5 layer3" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
60
|
+
}
|
61
|
+
|
62
|
+
VERSIONS_INDEX = ["MPEG2.5", nil, "MPEG2", "MPEG1"]
|
63
|
+
LAYER_INDEX = [nil, "layer3", "layer2", "layer1"]
|
64
|
+
CHANNEL_MODE_INDEX = ["Stereo", "Joint Stereo", "Dual Channel", "Single Channel"]
|
65
|
+
|
66
|
+
SAMPLE_RATE_INDEX = {
|
67
|
+
"MPEG1" => [44100, 48000, 32000],
|
68
|
+
"MPEG2" => [22050, 24000, 16000],
|
69
|
+
"MPEG2.5" => [11025, 12000, 8000]
|
70
|
+
}
|
71
|
+
|
72
|
+
SAMPLES_PER_FRAME_INDEX = {
|
73
|
+
"MPEG1 layer1" => 384,
|
74
|
+
"MPEG1 layer2" => 1152,
|
75
|
+
"MPEG1 layer3" => 1152,
|
76
|
+
|
77
|
+
"MPEG2 layer1" => 384,
|
78
|
+
"MPEG2 layer2" => 1152,
|
79
|
+
"MPEG2 layer3" => 576,
|
80
|
+
|
81
|
+
"MPEG2.5 layer1" => 384,
|
82
|
+
"MPEG2.5 layer2" => 1152,
|
83
|
+
"MPEG2.5 layer3" => 576
|
84
|
+
}
|
85
|
+
|
86
|
+
attr_reader :version, :layer, :frame_bitrate, :channel_mode, :sample_rate
|
87
|
+
|
88
|
+
def initialize(file_io, offset = 0)
|
89
|
+
# mpeg frame header start with '11111111111' sync bits,
|
90
|
+
# So look through file until find it.
|
91
|
+
loop do
|
92
|
+
file_io.rewind
|
93
|
+
file_io.seek(offset)
|
94
|
+
|
95
|
+
break if file_io.eof?
|
96
|
+
|
97
|
+
header = file_io.read(HEADER_SIZE)
|
98
|
+
sync_bits = header.unpack1("B11")
|
99
|
+
|
100
|
+
if sync_bits == ("1" * 11).b
|
101
|
+
@header = header.unpack1("B*")
|
102
|
+
@position = offset
|
103
|
+
|
104
|
+
parse
|
105
|
+
break
|
106
|
+
end
|
107
|
+
|
108
|
+
offset += 1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def valid?
|
113
|
+
!@header.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
def position
|
117
|
+
return 0 unless valid?
|
118
|
+
@position
|
119
|
+
end
|
120
|
+
|
121
|
+
def kind
|
122
|
+
return if @version.nil? && @layer.nil?
|
123
|
+
"#{@version} #{@layer}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def samples_per_frame
|
127
|
+
SAMPLES_PER_FRAME_INDEX[kind]
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def parse
|
133
|
+
return unless valid?
|
134
|
+
|
135
|
+
@version = VERSIONS_INDEX[@header[11..12].to_i(2)]
|
136
|
+
@layer = LAYER_INDEX[@header[13..14].to_i(2)]
|
137
|
+
@frame_bitrate = FRAME_BITRATE_INDEX[kind]&.fetch(@header[16..19].to_i(2))
|
138
|
+
@channel_mode = CHANNEL_MODE_INDEX[@header[24..25].to_i(2)]
|
139
|
+
@sample_rate = SAMPLE_RATE_INDEX[@version]&.fetch(@header[20..21].to_i(2))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Mp3
|
5
|
+
# VBRI header structure:
|
6
|
+
#
|
7
|
+
# Position Length Meaning
|
8
|
+
# 0 4 VBR header ID in 4 ASCII chars, always 'VBRI', not NULL-terminated
|
9
|
+
#
|
10
|
+
# 4 2 Version ID as Big-Endian 16-bit unsigned
|
11
|
+
#
|
12
|
+
# 6 2 Delay as Big-Endian float
|
13
|
+
#
|
14
|
+
# 8 2 Quality indicator
|
15
|
+
#
|
16
|
+
# 10 4 Number of Bytes as Big-Endian 32-bit unsigned
|
17
|
+
#
|
18
|
+
# 14 4 Number of Frames as Big-Endian 32-bit unsigned
|
19
|
+
#
|
20
|
+
# 18 2 Number of entries within TOC table as Big-Endian 16-bit unsigned
|
21
|
+
#
|
22
|
+
# 20 2 Scale factor of TOC table entries as Big-Endian 32-bit unsigned
|
23
|
+
#
|
24
|
+
# 22 2 Size per table entry in bytes (max 4) as Big-Endian 16-bit unsigned
|
25
|
+
#
|
26
|
+
# 24 2 Frames per table entry as Big-Endian 16-bit unsigned
|
27
|
+
#
|
28
|
+
# 26 TOC entries for seeking as Big-Endian integral.
|
29
|
+
# From size per table entry and number of entries,
|
30
|
+
# you can calculate the length of this field.
|
31
|
+
class VbriHeader
|
32
|
+
HEADER_SIZE = 32
|
33
|
+
HEADER_FORMAT = "A4x6NN"
|
34
|
+
|
35
|
+
attr_reader :frames_count, :bytes_count
|
36
|
+
|
37
|
+
def initialize(file_io, offset = 0)
|
38
|
+
file_io.seek(offset)
|
39
|
+
@id, @bytes_count, @frames_count = file_io.read(HEADER_SIZE)&.unpack(HEADER_FORMAT)
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?
|
43
|
+
@id == "VBRI"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Mp3
|
5
|
+
# Xing header structure:
|
6
|
+
#
|
7
|
+
# Position Length Meaning
|
8
|
+
# 0 4 VBR header ID in 4 ASCII chars, either 'Xing' or 'Info',
|
9
|
+
# not NULL-terminated
|
10
|
+
#
|
11
|
+
# 4 4 Flags which indicate what fields are present,
|
12
|
+
# flags are combined with a logical OR. Field is mandatory.
|
13
|
+
#
|
14
|
+
# 0x0001 - Frames field is present
|
15
|
+
# 0x0002 - Bytes field is present
|
16
|
+
# 0x0004 - TOC field is present
|
17
|
+
# 0x0008 - Quality indicator field is present
|
18
|
+
#
|
19
|
+
# 8 4 Number of Frames as Big-Endian 32-bit unsigned (optional)
|
20
|
+
#
|
21
|
+
# 8 or 12 4 Number of Bytes in file as Big-Endian 32-bit unsigned (optional)
|
22
|
+
#
|
23
|
+
# 8,12 or 16 100 100 TOC entries for seeking as integral BYTE (optional)
|
24
|
+
#
|
25
|
+
# 8,12,16,108,112 or 116 4 Quality indicator as Big-Endian 32-bit unsigned
|
26
|
+
# from 0 - best quality to 100 - worst quality (optional)
|
27
|
+
class XingHeader
|
28
|
+
attr_reader :frames_count, :bytes_count
|
29
|
+
|
30
|
+
def initialize(file_io, offset = 0)
|
31
|
+
file_io.seek(offset)
|
32
|
+
|
33
|
+
@id, @flags = file_io.read(8)&.unpack("A4N")
|
34
|
+
return unless valid?
|
35
|
+
|
36
|
+
@frames_count = @flags & 1 == 1 ? file_io.read(4).unpack1("N") : 0
|
37
|
+
@bytes_count = @flags & 2 == 2 ? file_io.read(4).unpack1("N") : 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid?
|
41
|
+
%w[Xing Info].include? @id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
class Mp3Tag < Tag
|
5
|
+
extend TagDelegate
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegator :@mpeg_frame_header, :version, :mpeg_version
|
9
|
+
def_delegator :@mpeg_frame_header, :layer, :mpeg_layer
|
10
|
+
def_delegator :@mpeg_frame_header, :kind, :mpeg_kind
|
11
|
+
def_delegators :@mpeg_frame_header, :channel_mode, :sample_rate
|
12
|
+
|
13
|
+
tag_delegate :@id3_tag,
|
14
|
+
:title,
|
15
|
+
:artist,
|
16
|
+
:album,
|
17
|
+
:albumartist,
|
18
|
+
:composer,
|
19
|
+
:comments,
|
20
|
+
:track,
|
21
|
+
:track_total,
|
22
|
+
:genre,
|
23
|
+
:year,
|
24
|
+
:disc,
|
25
|
+
:disc_total,
|
26
|
+
:images
|
27
|
+
|
28
|
+
def id3v2?
|
29
|
+
@id3_tag.instance_of? ID3::V2
|
30
|
+
end
|
31
|
+
|
32
|
+
def invalid_id3?
|
33
|
+
@id3_tag.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def id3_version
|
37
|
+
@id3_tag&.version
|
38
|
+
end
|
39
|
+
|
40
|
+
def is_vbr?
|
41
|
+
mpeg_frame_header.valid? && (xing_header.valid? || vbri_header.valid?)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse
|
47
|
+
@id3_tag = parse_id3_tag
|
48
|
+
parse_duration if mpeg_frame_header.valid?
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_id3_tag
|
52
|
+
id3_v1_tag = ID3::V1.new(@file_io.dup)
|
53
|
+
id3_v2_tag = ID3::V2.new(@file_io.dup)
|
54
|
+
|
55
|
+
return id3_v2_tag if id3_v2_tag.valid?
|
56
|
+
id3_v1_tag if id3_v1_tag.valid?
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_duration
|
60
|
+
if is_vbr?
|
61
|
+
@duration = frames_count * (mpeg_frame_header.samples_per_frame / sample_rate.to_f)
|
62
|
+
@bitrate = (bytes_count * 8 / @duration / 1000).round unless @duration.zero?
|
63
|
+
else
|
64
|
+
@bitrate = mpeg_frame_header.frame_bitrate
|
65
|
+
@duration = (file_size - (@id3_tag&.size || 0)) * 8 / (@bitrate * 1000).to_f unless @bitrate.zero?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def mpeg_frame_header
|
70
|
+
# Because id3v2 tag on the file header so skip id3v2 tag
|
71
|
+
@mpeg_frame_header ||= Mp3::MpegFrameHeader.new(@file_io, id3v2? ? @id3_tag&.size : 0)
|
72
|
+
end
|
73
|
+
|
74
|
+
def xing_header
|
75
|
+
@xing_header ||= Mp3::XingHeader.new(@file_io, xing_header_offset)
|
76
|
+
end
|
77
|
+
|
78
|
+
def vbri_header
|
79
|
+
@vbri_header ||= Mp3::VbriHeader.new(@file_io, vbri_header_offset)
|
80
|
+
end
|
81
|
+
|
82
|
+
def xing_header_offset
|
83
|
+
mpeg_frame_header_position = mpeg_frame_header.position
|
84
|
+
mpeg_frame_header_size = Mp3::MpegFrameHeader::HEADER_SIZE
|
85
|
+
mpeg_frame_side_info_size = if mpeg_version == "MPEG1"
|
86
|
+
channel_mode == "Single Channel" ? 17 : 32
|
87
|
+
else
|
88
|
+
channel_mode == "Single Channel" ? 9 : 17
|
89
|
+
end
|
90
|
+
|
91
|
+
mpeg_frame_header_position + mpeg_frame_header_size + mpeg_frame_side_info_size
|
92
|
+
end
|
93
|
+
|
94
|
+
def vbri_header_offset
|
95
|
+
mpeg_frame_header_position = mpeg_frame_header.position
|
96
|
+
mpeg_frame_header_size = Mp3::MpegFrameHeader::HEADER_SIZE
|
97
|
+
|
98
|
+
mpeg_frame_header_position + mpeg_frame_header_size + 32
|
99
|
+
end
|
100
|
+
|
101
|
+
def frames_count
|
102
|
+
return xing_header.frames_count if xing_header.valid?
|
103
|
+
vbri_header.frames_count if vbri_header.valid?
|
104
|
+
end
|
105
|
+
|
106
|
+
def bytes_count
|
107
|
+
return xing_header.bytes_count if xing_header.valid?
|
108
|
+
vbri_header.bytes_count if vbri_header.valid?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Mp4
|
5
|
+
class Atom
|
6
|
+
prepend LazyRead
|
7
|
+
|
8
|
+
VERSIONED_ATOMS = %w[meta stsd]
|
9
|
+
FLAGGED_ATOMS = %w[stsd]
|
10
|
+
HEADER_SIZE = 8
|
11
|
+
HEADER_SIZE_FIELD_SIZE = 4
|
12
|
+
EXTENDED_HEADER_SIZE = 8
|
13
|
+
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
def self.find(file_io, *atom_path)
|
17
|
+
file_io.rewind
|
18
|
+
|
19
|
+
atom_type = atom_path.shift
|
20
|
+
|
21
|
+
until file_io.eof?
|
22
|
+
atom = new(file_io)
|
23
|
+
|
24
|
+
next unless atom.valid?
|
25
|
+
file_io.seek(atom.size, IO::SEEK_CUR)
|
26
|
+
next unless atom.type == atom_type
|
27
|
+
|
28
|
+
return atom if atom_path.empty?
|
29
|
+
return atom.find(*atom_path)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return empty atom if can not found
|
33
|
+
new(StringIO.new(""))
|
34
|
+
end
|
35
|
+
|
36
|
+
# An atom header consists of the following fields:
|
37
|
+
#
|
38
|
+
# Atom size:
|
39
|
+
# A 32-bit integer that indicates the size of the atom, including both the atom header and the atom’s contents,
|
40
|
+
# including any contained atoms. Normally, the size field contains the actual size of the atom.
|
41
|
+
#
|
42
|
+
# Type:
|
43
|
+
# A 32-bit integer that contains the type of the atom.
|
44
|
+
# This can often be usefully treated as a four-character field with a mnemonic value .
|
45
|
+
def initialize
|
46
|
+
@size, @type = @file_io.read(HEADER_SIZE)&.unpack("Na4")
|
47
|
+
return unless valid?
|
48
|
+
|
49
|
+
# If the size field of an atom is set to 1, the type field is followed by a 64-bit extended size field,
|
50
|
+
# which contains the actual size of the atom as a 64-bit unsigned integer.
|
51
|
+
@size = @file_io.read(EXTENDED_HEADER_SIZE).unpack1("Q>") - EXTENDED_HEADER_SIZE if @size == 1
|
52
|
+
|
53
|
+
# If the size field of an atom is set to 0, which is allowed only for a top-level atom,
|
54
|
+
# designates the last atom in the file and indicates that the atom extends to the end of the file.
|
55
|
+
@size = @file_io.size if @size == 0
|
56
|
+
@size -= HEADER_SIZE
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid?
|
60
|
+
!@size.nil? && @size >= HEADER_SIZE
|
61
|
+
end
|
62
|
+
|
63
|
+
def find(*atom_path)
|
64
|
+
child_atom_index = data.index(atom_path.first)
|
65
|
+
|
66
|
+
# Return empty atom if can not found
|
67
|
+
return self.class.new(StringIO.new("")) if child_atom_index.nil?
|
68
|
+
|
69
|
+
# Because before atom type field there are 4 bytes of size field,
|
70
|
+
# So the child_atom_index should reduce 4.
|
71
|
+
self.class.find(StringIO.new(data[child_atom_index - HEADER_SIZE_FIELD_SIZE..]), *atom_path)
|
72
|
+
end
|
73
|
+
|
74
|
+
def children
|
75
|
+
@children ||= parse_children_atoms
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def parse_children_atoms
|
81
|
+
children_atoms = []
|
82
|
+
atoms_data = data
|
83
|
+
|
84
|
+
# Some atoms data contain extra content before child atom data.
|
85
|
+
# So reduce those extra content to get child atom data.
|
86
|
+
atoms_data = atoms_data[4..] if VERSIONED_ATOMS.include? type # Skip 4 bytes for version
|
87
|
+
atoms_data = atoms_data[4..] if FLAGGED_ATOMS.include? type # Skip 4 bytes for flag
|
88
|
+
atoms_data_io = StringIO.new(atoms_data)
|
89
|
+
|
90
|
+
until atoms_data_io.eof?
|
91
|
+
atom = self.class.new(atoms_data_io)
|
92
|
+
children_atoms.push(atom)
|
93
|
+
|
94
|
+
atom.skip
|
95
|
+
end
|
96
|
+
|
97
|
+
children_atoms
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
class Mp4Tag < Tag
|
5
|
+
META_ATOM_MAPPING = {
|
6
|
+
"\xA9alb".b => :album,
|
7
|
+
"\xA9ART".b => :artist,
|
8
|
+
"\xA9cmt".b => :comment,
|
9
|
+
"\xA9wrt".b => :composer,
|
10
|
+
"\xA9day".b => :year,
|
11
|
+
"\xA9gen".b => :genre,
|
12
|
+
"\xA9nam".b => :title,
|
13
|
+
"covr".b => :image,
|
14
|
+
"disk".b => :disc,
|
15
|
+
"trkn".b => :track,
|
16
|
+
"aART".b => :albumartist
|
17
|
+
}
|
18
|
+
|
19
|
+
META_ATOM_DECODE_BY_TYPE = {
|
20
|
+
0 => ->(data) { data }, # reserved
|
21
|
+
1 => ->(data) { Helper.encode_to_utf8(data) }, # UTF-8
|
22
|
+
2 => ->(data) { Helper.encode_to_utf8(data, "UTF-16BE") }, # UTF-16BE
|
23
|
+
3 => ->(data) { Helper.encode_to_utf8(data, "SJIS") }, # SJIS
|
24
|
+
|
25
|
+
13 => ->(data) { {data: data, mime_type: "image/jpeg", type: :cover} }, # JPEG
|
26
|
+
14 => ->(data) { {data: data, mime_type: "image/png", type: :cover} }, # PNG
|
27
|
+
|
28
|
+
21 => ->(data) { data.unpack1("i>") }, # Big endian signed integer
|
29
|
+
22 => ->(data) { data.unpack1("I>") }, # Big endian unsigned integer
|
30
|
+
23 => ->(data) { data.unpack1("g") }, # Big endian 32-bit floating point value
|
31
|
+
24 => ->(data) { data.unpack1("G") }, # Big endian 64-bit floating point value
|
32
|
+
|
33
|
+
65 => ->(data) { data.unpack1("c") }, # 8-bit signed integer
|
34
|
+
66 => ->(data) { data.unpack1("s>") }, # Big-endian 16-bit signed integer
|
35
|
+
67 => ->(data) { data.unpack1("l>") }, # Big-endian 32-bit signed integer
|
36
|
+
74 => ->(data) { data.unpack1("q>") }, # Big-endian 64-bit signed integer
|
37
|
+
|
38
|
+
75 => ->(data) { data.unpack1("C") }, # 8-bit unsigned integer
|
39
|
+
76 => ->(data) { data.unpack1("S>") }, # Big-endian 16-bit unsigned integer
|
40
|
+
77 => ->(data) { data.unpack1("L>") }, # Big-endian 32-bit unsigned integer
|
41
|
+
78 => ->(data) { data.unpack1("Q>") } # Big-endian 64-bit unsigned integer
|
42
|
+
}
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse
|
47
|
+
movie_atom = Mp4::Atom.find(@file_io, "moov")
|
48
|
+
return unless movie_atom.valid?
|
49
|
+
|
50
|
+
parse_meta_list_atom movie_atom.find("udta", "meta", "ilst")
|
51
|
+
parse_mvhd_atom movie_atom.find("mvhd")
|
52
|
+
parse_stsd_atom movie_atom.find("trak", "mdia", "minf", "stbl", "stsd")
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_meta_list_atom(atom)
|
56
|
+
return unless atom.valid?
|
57
|
+
|
58
|
+
# The metadata item list atom holds a list of actual metadata values that are present in the metadata atom.
|
59
|
+
# The metadata items are formatted as a list of items.
|
60
|
+
# The metadata item list atom is of type ‘ilst’ and contains a number of metadata items, each of which is an atom.
|
61
|
+
# each metadata item atom contains a Value Atom, to hold the value of the metadata item
|
62
|
+
atom.children.each do |child_atom|
|
63
|
+
attr_name = META_ATOM_MAPPING[child_atom.type]
|
64
|
+
|
65
|
+
# The value of the metadata item is expressed as immediate data in a value atom.
|
66
|
+
# The value atom starts with two fields: a type indicator, and a locale indicator.
|
67
|
+
# Both the type and locale indicators are four bytes long.
|
68
|
+
# There may be multiple ‘value’ entries, using different type
|
69
|
+
data_atom = child_atom.find("data")
|
70
|
+
next unless data_atom.valid?
|
71
|
+
|
72
|
+
if attr_name == :image
|
73
|
+
@images_data.push(data_atom)
|
74
|
+
next
|
75
|
+
end
|
76
|
+
|
77
|
+
encoded_data_value = parse_meta_data_atom(data_atom)
|
78
|
+
next if attr_name.nil? || encoded_data_value.nil?
|
79
|
+
|
80
|
+
case attr_name
|
81
|
+
when :comment
|
82
|
+
@comments.push(encoded_data_value)
|
83
|
+
when :track, :disc
|
84
|
+
count, total_count = encoded_data_value.unpack("x2nn")
|
85
|
+
|
86
|
+
instance_variable_set("@#{attr_name}", count) unless count.zero?
|
87
|
+
instance_variable_set("@#{attr_name}_total", total_count) unless total_count.zero?
|
88
|
+
else
|
89
|
+
instance_variable_set("@#{attr_name}", encoded_data_value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_meta_data_atom(atom)
|
95
|
+
data_type, data_value = atom.data.unpack("Nx4a*")
|
96
|
+
META_ATOM_DECODE_BY_TYPE[data_type]&.call(data_value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_mvhd_atom(atom)
|
100
|
+
return unless atom.valid?
|
101
|
+
|
102
|
+
atom_data = StringIO.new(atom.data)
|
103
|
+
version = atom_data.read(1).unpack1("c")
|
104
|
+
|
105
|
+
# Skip flags
|
106
|
+
atom_data.seek(3, IO::SEEK_CUR)
|
107
|
+
|
108
|
+
if version == 0
|
109
|
+
# Skip creation and modification time
|
110
|
+
atom_data.seek(8, IO::SEEK_CUR)
|
111
|
+
|
112
|
+
time_scale, duration = atom_data.read(8).unpack("l>l>")
|
113
|
+
elsif version == 1
|
114
|
+
# Skip creation and modification time
|
115
|
+
atom_data.seek(16, IO::SEEK_CUR)
|
116
|
+
|
117
|
+
time_scale, duration = atom_data.read(12).unpack("l>q>")
|
118
|
+
end
|
119
|
+
|
120
|
+
@duration = duration / time_scale.to_f
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_stsd_atom(atom)
|
124
|
+
return unless atom.valid?
|
125
|
+
|
126
|
+
mp4a_atom = atom.find("mp4a")
|
127
|
+
esds_atom = atom.find("esds")
|
128
|
+
|
129
|
+
@sample_rate = mp4a_atom.data.unpack1("x22I>") if mp4a_atom.valid?
|
130
|
+
@bitrate = esds_atom.data.unpack1("x26I>") / 1000 if esds_atom.valid?
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_image_data(image_data_atom)
|
134
|
+
parse_meta_data_atom(image_data_atom)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
class FlacTag
|
6
|
+
include VorbisComment
|
7
|
+
include Flac::StreaminfoBlock
|
8
|
+
|
9
|
+
attr_reader :bitrate, :duration, :sample_rate, :bit_depth, *COMMET_FIELD_MAPPING.values
|
10
|
+
|
11
|
+
def initialize(identification_packet, comment_packet)
|
12
|
+
# Identification packet structure:
|
13
|
+
#
|
14
|
+
# The one-byte packet type 0x7F
|
15
|
+
# The four-byte ASCII signature "FLAC", i.e. 0x46, 0x4C, 0x41, 0x43
|
16
|
+
# A one-byte binary major version number for the mapping, e.g. 0x01 for mapping version 1.0
|
17
|
+
# A one-byte binary minor version number for the mapping, e.g. 0x00 for mapping version 1.0
|
18
|
+
# A two-byte, big-endian binary number signifying the number of header (non-audio) packets, not including this one.
|
19
|
+
# The four-byte ASCII native FLAC signature "fLaC" according to the FLAC format specification
|
20
|
+
# The STREAMINFO metadata block for the stream.
|
21
|
+
#
|
22
|
+
# The first identification packet is followed by one or more header packets.
|
23
|
+
# Each such packet will contain a single native FLAC metadata block.
|
24
|
+
# The first of these must be a VORBIS_COMMENT block.
|
25
|
+
|
26
|
+
id, streaminfo_block_data = identification_packet.unpack("x9A4A*")
|
27
|
+
|
28
|
+
return unless id == "fLaC"
|
29
|
+
streaminfo_block = Flac::Block.new(StringIO.new(streaminfo_block_data))
|
30
|
+
vorbis_comment_block = Flac::Block.new(StringIO.new(comment_packet))
|
31
|
+
|
32
|
+
parse_streaminfo_block(streaminfo_block.data)
|
33
|
+
parse_vorbis_comment(vorbis_comment_block.data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
class OpusTag
|
6
|
+
include VorbisComment
|
7
|
+
|
8
|
+
attr_reader :sample_rate, :pre_skip, *COMMET_FIELD_MAPPING.values
|
9
|
+
|
10
|
+
def initialize(identification_packet, comment_packet)
|
11
|
+
# Identification packet structure:
|
12
|
+
#
|
13
|
+
# 1) "OpusHead"
|
14
|
+
# 2) [version] = read 8 bits as unsigned integer
|
15
|
+
# 3) [audio_channels] = read 8 bit as unsigned integer
|
16
|
+
# 4) [pre_skip] = read 16 bits as unsigned little endian integer
|
17
|
+
# 5) [input_sample_rate] = read 32 bits as unsigned little endian integer
|
18
|
+
# 6) [output_gain] = read 16 bits as unsigned little endian integer
|
19
|
+
# 7) [channel_mapping_family] = read 8 bit as unsigned integer
|
20
|
+
# 8) [channel_mapping_table]
|
21
|
+
@sample_rate = 48000
|
22
|
+
@pre_skip = identification_packet[10..11].unpack1("v")
|
23
|
+
|
24
|
+
comment_packet_id, comment_packet_body = [comment_packet[0..7], comment_packet[8..]]
|
25
|
+
|
26
|
+
# Opus comment packet start with 'OpusTags'
|
27
|
+
return unless comment_packet_id == "OpusTags"
|
28
|
+
|
29
|
+
parse_vorbis_comment(comment_packet_body)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
# From Ogg's perspective, packets can be of any arbitrary size. A
|
6
|
+
# specific media mapping will define how to group or break up packets
|
7
|
+
# from a specific media encoder. As Ogg pages have a maximum size of
|
8
|
+
# about 64 kBytes, sometimes a packet has to be distributed over
|
9
|
+
# several pages. To simplify that process, Ogg divides each packet
|
10
|
+
# into 255 byte long chunks plus a final shorter chunk. These chunks
|
11
|
+
# are called "Ogg Segments". They are only a logical construct and do
|
12
|
+
# not have a header for themselves.
|
13
|
+
class Packets
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
def initialize(file_io)
|
17
|
+
@file_io = file_io
|
18
|
+
end
|
19
|
+
|
20
|
+
def each
|
21
|
+
@file_io.rewind
|
22
|
+
|
23
|
+
packet = +""
|
24
|
+
pages = Ogg::Pages.new(@file_io)
|
25
|
+
|
26
|
+
pages.each do |page|
|
27
|
+
page.segments.each do |segment|
|
28
|
+
packet << segment
|
29
|
+
|
30
|
+
# Ogg divides each packet into 255 byte long segments plus a final shorter segment.
|
31
|
+
# So when segment length is less than 255 byte, it's the final segment.
|
32
|
+
if segment.length < 255
|
33
|
+
yield packet
|
34
|
+
packet = +""
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|