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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/lib/wahwah.rb +74 -3
  4. data/lib/wahwah/asf/object.rb +39 -0
  5. data/lib/wahwah/asf_tag.rb +220 -0
  6. data/lib/wahwah/errors.rb +6 -0
  7. data/lib/wahwah/flac/block.rb +57 -0
  8. data/lib/wahwah/flac/streaminfo_block.rb +51 -0
  9. data/lib/wahwah/flac_tag.rb +84 -0
  10. data/lib/wahwah/helper.rb +37 -0
  11. data/lib/wahwah/id3/comment_frame_body.rb +21 -0
  12. data/lib/wahwah/id3/frame.rb +180 -0
  13. data/lib/wahwah/id3/frame_body.rb +36 -0
  14. data/lib/wahwah/id3/genre_frame_body.rb +15 -0
  15. data/lib/wahwah/id3/image_frame_body.rb +60 -0
  16. data/lib/wahwah/id3/text_frame_body.rb +16 -0
  17. data/lib/wahwah/id3/v1.rb +96 -0
  18. data/lib/wahwah/id3/v2.rb +60 -0
  19. data/lib/wahwah/id3/v2_header.rb +53 -0
  20. data/lib/wahwah/mp3/mpeg_frame_header.rb +141 -0
  21. data/lib/wahwah/mp3/vbri_header.rb +47 -0
  22. data/lib/wahwah/mp3/xing_header.rb +45 -0
  23. data/lib/wahwah/mp3_tag.rb +110 -0
  24. data/lib/wahwah/mp4/atom.rb +105 -0
  25. data/lib/wahwah/mp4_tag.rb +126 -0
  26. data/lib/wahwah/ogg/flac_tag.rb +37 -0
  27. data/lib/wahwah/ogg/opus_tag.rb +33 -0
  28. data/lib/wahwah/ogg/packets.rb +41 -0
  29. data/lib/wahwah/ogg/page.rb +121 -0
  30. data/lib/wahwah/ogg/pages.rb +24 -0
  31. data/lib/wahwah/ogg/vorbis_comment.rb +51 -0
  32. data/lib/wahwah/ogg/vorbis_tag.rb +35 -0
  33. data/lib/wahwah/ogg_tag.rb +66 -0
  34. data/lib/wahwah/riff/chunk.rb +54 -0
  35. data/lib/wahwah/riff_tag.rb +140 -0
  36. data/lib/wahwah/tag.rb +59 -0
  37. data/lib/wahwah/tag_delegate.rb +16 -0
  38. data/lib/wahwah/version.rb +4 -2
  39. metadata +94 -23
  40. data/.gitignore +0 -8
  41. data/.travis.yml +0 -5
  42. data/Gemfile +0 -6
  43. data/Gemfile.lock +0 -22
  44. data/README.md +0 -35
  45. data/Rakefile +0 -10
  46. data/bin/console +0 -14
  47. data/bin/setup +0 -8
  48. 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