wahwah 0.1.0.pre.test
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/wahwah.rb +76 -0
- data/lib/wahwah/asf/object.rb +39 -0
- data/lib/wahwah/asf_tag.rb +220 -0
- data/lib/wahwah/errors.rb +6 -0
- data/lib/wahwah/flac/block.rb +57 -0
- data/lib/wahwah/flac/streaminfo_block.rb +51 -0
- data/lib/wahwah/flac_tag.rb +84 -0
- data/lib/wahwah/helper.rb +37 -0
- data/lib/wahwah/id3/comment_frame_body.rb +21 -0
- data/lib/wahwah/id3/frame.rb +180 -0
- data/lib/wahwah/id3/frame_body.rb +36 -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 +96 -0
- data/lib/wahwah/id3/v2.rb +60 -0
- data/lib/wahwah/id3/v2_header.rb +53 -0
- data/lib/wahwah/mp3/mpeg_frame_header.rb +141 -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 +110 -0
- data/lib/wahwah/mp4/atom.rb +105 -0
- data/lib/wahwah/mp4_tag.rb +126 -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 +54 -0
- data/lib/wahwah/riff_tag.rb +140 -0
- data/lib/wahwah/tag.rb +59 -0
- data/lib/wahwah/tag_delegate.rb +16 -0
- data/lib/wahwah/version.rb +5 -0
- metadata +167 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
class V1 < Tag
|
6
|
+
TAG_SIZE = 128
|
7
|
+
TAG_ID = 'TAG'
|
8
|
+
DEFAULT_ENCODING = 'iso-8859-1'
|
9
|
+
GENRES = [
|
10
|
+
# Standard Genres
|
11
|
+
'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge',
|
12
|
+
'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop',
|
13
|
+
'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative',
|
14
|
+
'Ska', 'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop',
|
15
|
+
'Vocal', 'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid',
|
16
|
+
'House', 'Game', 'Sound Clip', 'Gospel', 'Noise', 'AlternRock', 'Bass',
|
17
|
+
'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', 'Ethnic',
|
18
|
+
'Gothic', 'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream',
|
19
|
+
'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 'Christian Rap', 'Pop/Funk',
|
20
|
+
'Jungle', 'Native American', 'Cabaret', 'New Wave', 'Psychadelic', 'Rave', 'Showtunes',
|
21
|
+
'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro',
|
22
|
+
'Musical', 'Rock & Roll', 'Hard Rock',
|
23
|
+
|
24
|
+
# Winamp Extended Genres
|
25
|
+
'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin',
|
26
|
+
'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock',
|
27
|
+
'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour',
|
28
|
+
'Speech', 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass',
|
29
|
+
'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba',
|
30
|
+
'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle', 'Duet', 'Punk Rock',
|
31
|
+
'Drum Solo', 'A capella', 'Euro-House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club-House',
|
32
|
+
'Hardcore Techno', 'Terror', 'Indie', 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat',
|
33
|
+
'Christian Gangsta Rap', 'Heavy Metal', 'Black Metal', 'Contemporary Christian', 'Christian Rock',
|
34
|
+
|
35
|
+
# Added on WinAmp 1.91
|
36
|
+
'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'Jpop', 'Synthpop',
|
37
|
+
|
38
|
+
# Added on WinAmp 5.6
|
39
|
+
'Abstract', 'Art Rock', 'Baroque', 'Bhangra', 'Big Beat', 'Breakbeat', 'Chillout',
|
40
|
+
'Downtempo', 'Dub', 'EBM', 'Eclectic', 'Electro', 'Electroclash', 'Emo',
|
41
|
+
'Experimental', 'Garage', 'Illbient', 'Industro-Goth', 'Jam Band', 'Krautrock', 'Leftfield',
|
42
|
+
'Lounge', 'Math Rock', 'New Romantic', 'Nu-Breakz', 'Post-Punk', 'Post-Rock', 'Psytrance',
|
43
|
+
'Shoegaze', 'Space Rock', 'Trop Rock', 'World Music', 'Neoclassical', 'Audiobook', 'Audio Theatre',
|
44
|
+
'Neue Deutsche Welle', 'Podcast', 'Indie Rock', 'G-Funk', 'Dubstep', 'Garage Rock', 'Psybient'
|
45
|
+
]
|
46
|
+
|
47
|
+
def size
|
48
|
+
TAG_SIZE
|
49
|
+
end
|
50
|
+
|
51
|
+
def version
|
52
|
+
'v1'
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid?
|
56
|
+
@id == TAG_ID
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
# For ID3v1 info, see here https://en.wikipedia.org/wiki/ID3#ID3v1
|
61
|
+
#
|
62
|
+
# header 3 "TAG"
|
63
|
+
# title 30 30 characters of the title
|
64
|
+
# artist 30 30 characters of the artist name
|
65
|
+
# album 30 30 characters of the album name
|
66
|
+
# year 4 A four-digit year
|
67
|
+
# comment 28 or 30 The comment.
|
68
|
+
# zero-byte 1 If a track number is stored, this byte contains a binary 0.
|
69
|
+
# track 1 The number of the track on the album, or 0. Invalid, if previous byte is not a binary 0.
|
70
|
+
# genre 1 Index in a list of genres, or 255
|
71
|
+
def parse
|
72
|
+
return unless @file_io.size >= TAG_SIZE
|
73
|
+
|
74
|
+
@file_io.seek(-TAG_SIZE, IO::SEEK_END)
|
75
|
+
@id = Helper.encode_to_utf8(@file_io.read(3), source_encoding: DEFAULT_ENCODING)
|
76
|
+
|
77
|
+
return unless valid?
|
78
|
+
|
79
|
+
@title = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
|
80
|
+
@artist = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
|
81
|
+
@album = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
|
82
|
+
@year = Helper.encode_to_utf8(@file_io.read(4), source_encoding: DEFAULT_ENCODING)
|
83
|
+
|
84
|
+
comment = @file_io.read(30)
|
85
|
+
|
86
|
+
if comment.getbyte(-2) == 0
|
87
|
+
@track = comment.getbyte(-1)
|
88
|
+
comment = comment.byteslice(0..-3)
|
89
|
+
end
|
90
|
+
|
91
|
+
@comments.push(Helper.encode_to_utf8(comment, source_encoding: DEFAULT_ENCODING))
|
92
|
+
@genre = GENRES[@file_io.getbyte] || ''
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module WahWah
|
6
|
+
module ID3
|
7
|
+
class V2 < Tag
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@header, :major_version, :size, :has_extended_header?, :valid?
|
11
|
+
|
12
|
+
def version
|
13
|
+
"v2.#{major_version}"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def parse
|
18
|
+
@file_io.rewind
|
19
|
+
@header = V2Header.new(@file_io)
|
20
|
+
|
21
|
+
return unless valid?
|
22
|
+
|
23
|
+
until end_of_tag? do
|
24
|
+
frame = ID3::Frame.new(@file_io, major_version)
|
25
|
+
next unless frame.valid?
|
26
|
+
|
27
|
+
update_attribute(frame)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_attribute(frame)
|
32
|
+
name = frame.name
|
33
|
+
value = frame.value
|
34
|
+
|
35
|
+
case name
|
36
|
+
when :comment
|
37
|
+
# Because there may be more than one comment frame in each tag,
|
38
|
+
# so push it into a array.
|
39
|
+
@comments.push(value)
|
40
|
+
when :image
|
41
|
+
# Because there may be more than one image frame in each tag,
|
42
|
+
# so push it into a array.
|
43
|
+
@images.push(value)
|
44
|
+
when :track, :disc
|
45
|
+
# Track and disc value may be extended with a "/" character
|
46
|
+
# and a numeric string containing the total numer.
|
47
|
+
count, total_count = value.split('/', 2)
|
48
|
+
instance_variable_set("@#{name}", count)
|
49
|
+
instance_variable_set("@#{name}_total", total_count) unless total_count.nil?
|
50
|
+
else
|
51
|
+
instance_variable_set("@#{name}", value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def end_of_tag?
|
56
|
+
size <= @file_io.pos || file_size <= @file_io.pos
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module ID3
|
5
|
+
# The ID3v2 tag header, which should be the first information in the file,
|
6
|
+
# is 10 bytes as follows:
|
7
|
+
|
8
|
+
# ID3v2/file identifier "ID3"
|
9
|
+
# ID3v2 version $03 00
|
10
|
+
# ID3v2 flags %abc00000
|
11
|
+
# ID3v2 size 4 * %0xxxxxxx
|
12
|
+
class V2Header
|
13
|
+
TAG_ID = 'ID3'
|
14
|
+
HEADER_SIZE = 10
|
15
|
+
HEADER_FORMAT = 'A3CxB8B*'
|
16
|
+
|
17
|
+
attr_reader :major_version, :size
|
18
|
+
|
19
|
+
def initialize(file_io)
|
20
|
+
header_content = file_io.read(HEADER_SIZE)
|
21
|
+
@id, @major_version, @flags, size_bits = header_content.unpack(HEADER_FORMAT) if header_content.size >= HEADER_SIZE
|
22
|
+
|
23
|
+
return unless valid?
|
24
|
+
|
25
|
+
# Tag size is the size excluding the header size,
|
26
|
+
# so add header size back to get total size.
|
27
|
+
@size = Helper.id3_size_caculate(size_bits) + HEADER_SIZE
|
28
|
+
|
29
|
+
if has_extended_header?
|
30
|
+
# Extended header structure:
|
31
|
+
#
|
32
|
+
# Extended header size $xx xx xx xx
|
33
|
+
# Extended Flags $xx xx
|
34
|
+
# Size of padding $xx xx xx xx
|
35
|
+
|
36
|
+
# Skip extended_header
|
37
|
+
extended_header_size = Helper.id3_size_caculate(file_io.read(4).unpack('B32').first)
|
38
|
+
file_io.seek(extended_header_size - 4, IO::SEEK_CUR)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?
|
43
|
+
@id == TAG_ID
|
44
|
+
end
|
45
|
+
|
46
|
+
# The second bit in flags byte indicates whether or not the header
|
47
|
+
# is followed by an extended header.
|
48
|
+
def has_extended_header?
|
49
|
+
@flags[1] == '1'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,141 @@
|
|
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.unpack('B11').first
|
99
|
+
|
100
|
+
if sync_bits == "#{'1' * 11}".b
|
101
|
+
@header = header.unpack('B*').first
|
102
|
+
@position = offset
|
103
|
+
|
104
|
+
parse; break
|
105
|
+
end
|
106
|
+
|
107
|
+
offset += 1
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def valid?
|
112
|
+
!@header.nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
def position
|
116
|
+
return 0 unless valid?
|
117
|
+
@position
|
118
|
+
end
|
119
|
+
|
120
|
+
def kind
|
121
|
+
return if @version.nil? && @layer.nil?
|
122
|
+
"#{@version} #{@layer}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def samples_per_frame
|
126
|
+
SAMPLES_PER_FRAME_INDEX[kind]
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
def parse
|
131
|
+
return unless valid?
|
132
|
+
|
133
|
+
@version = VERSIONS_INDEX[@header[11..12].to_i(2)]
|
134
|
+
@layer = LAYER_INDEX[@header[13..14].to_i(2)]
|
135
|
+
@frame_bitrate = FRAME_BITRATE_INDEX[kind]&.fetch(@header[16..19].to_i(2))
|
136
|
+
@channel_mode = CHANNEL_MODE_INDEX[@header[24..25].to_i(2)]
|
137
|
+
@sample_rate = SAMPLE_RATE_INDEX[@version]&.fetch(@header[20..21].to_i(2))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
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).unpack('N').first : 0
|
37
|
+
@bytes_count = @flags & 2 == 2 ? file_io.read(4).unpack('N').first : 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid?
|
41
|
+
%w(Xing Info).include? @id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|