wahwah 0.1.0 → 1.0.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.rb +74 -3
- 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 +4 -2
- metadata +94 -23
- 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,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
|