wahwah 0.1.0.pre.test

Sign up to get free protection for your applications and to get access to all the features.
@@ -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