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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/lib/wahwah/asf/object.rb +34 -0
  4. data/lib/wahwah/asf_tag.rb +221 -0
  5. data/lib/wahwah/errors.rb +7 -0
  6. data/lib/wahwah/flac/block.rb +51 -0
  7. data/lib/wahwah/flac/streaminfo_block.rb +51 -0
  8. data/lib/wahwah/flac_tag.rb +86 -0
  9. data/lib/wahwah/helper.rb +37 -0
  10. data/lib/wahwah/id3/comment_frame_body.rb +21 -0
  11. data/lib/wahwah/id3/frame.rb +176 -0
  12. data/lib/wahwah/id3/frame_body.rb +35 -0
  13. data/lib/wahwah/id3/genre_frame_body.rb +15 -0
  14. data/lib/wahwah/id3/image_frame_body.rb +60 -0
  15. data/lib/wahwah/id3/text_frame_body.rb +16 -0
  16. data/lib/wahwah/id3/v1.rb +97 -0
  17. data/lib/wahwah/id3/v2.rb +67 -0
  18. data/lib/wahwah/id3/v2_header.rb +53 -0
  19. data/lib/wahwah/lazy_read.rb +40 -0
  20. data/lib/wahwah/mp3/mpeg_frame_header.rb +143 -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 +111 -0
  24. data/lib/wahwah/mp4/atom.rb +101 -0
  25. data/lib/wahwah/mp4_tag.rb +137 -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 +50 -0
  35. data/lib/wahwah/riff_tag.rb +142 -0
  36. data/lib/wahwah/tag.rb +71 -0
  37. data/lib/wahwah/tag_delegate.rb +16 -0
  38. data/lib/wahwah/version.rb +4 -2
  39. data/lib/wahwah.rb +78 -2
  40. metadata +109 -24
  41. data/.gitignore +0 -8
  42. data/.travis.yml +0 -5
  43. data/Gemfile +0 -6
  44. data/Gemfile.lock +0 -22
  45. data/README.md +0 -35
  46. data/Rakefile +0 -10
  47. data/bin/console +0 -14
  48. data/bin/setup +0 -8
  49. data/wahwah.gemspec +0 -27
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class Frame
6
+ prepend LazyRead
7
+
8
+ ID_MAPPING = {
9
+ # ID3v2.2 frame id
10
+ COM: :comment,
11
+ TRK: :track,
12
+ TYE: :year,
13
+ TAL: :album,
14
+ TP1: :artist,
15
+ TT2: :title,
16
+ TCO: :genre,
17
+ TPA: :disc,
18
+ TP2: :albumartist,
19
+ TCM: :composer,
20
+ PIC: :image,
21
+
22
+ # ID3v2.3 and ID3v2.4 frame id
23
+ COMM: :comment,
24
+ TRCK: :track,
25
+ TYER: :year,
26
+ TALB: :album,
27
+ TPE1: :artist,
28
+ TIT2: :title,
29
+ TCON: :genre,
30
+ TPOS: :disc,
31
+ TPE2: :albumartist,
32
+ TCOM: :composer,
33
+ APIC: :image,
34
+
35
+ # ID3v2.4 use TDRC replace TYER
36
+ TDRC: :year
37
+ }
38
+
39
+ # ID3v2.3 frame flags field is defined as follows.
40
+ #
41
+ # %abc00000 %ijk00000
42
+ #
43
+ # a - Tag alter preservation
44
+ # b - File alter preservation
45
+ # c - Read only
46
+ # i - Compression
47
+ # j - Encryption
48
+ # k - Grouping identity
49
+ V3_HEADER_FLAGS_INDICATIONS = Array.new(16).tap do |array|
50
+ array[0] = :tag_alter_preservation
51
+ array[1] = :file_alter_preservation
52
+ array[2] = :read_only
53
+ array[8] = :compression
54
+ array[9] = :encryption
55
+ array[10] = :grouping_identity
56
+ end
57
+
58
+ # ID3v2.4 frame flags field is defined as follows.
59
+ #
60
+ # %0abc0000 %0h00kmnp
61
+ #
62
+ # a - Tag alter preservation
63
+ # b - File alter preservation
64
+ # c - Read only
65
+ # h - Grouping identity
66
+ # k - Compression
67
+ # m - Encryption
68
+ # n - Unsynchronisation
69
+ # p - Data length indicator
70
+ V4_HEADER_FLAGS_INDICATIONS = Array.new(16).tap do |array|
71
+ array[1] = :tag_alter_preservation
72
+ array[2] = :file_alter_preservation
73
+ array[3] = :read_only
74
+ array[9] = :grouping_identity
75
+ array[12] = :compression
76
+ array[13] = :encryption
77
+ array[14] = :unsynchronisation
78
+ array[15] = :data_length_indicator
79
+ end
80
+
81
+ attr_reader :name
82
+
83
+ def initialize(version)
84
+ @version = version
85
+
86
+ parse_frame_header
87
+
88
+ # In ID3v2.3 when frame is compressed using zlib
89
+ # with 4 bytes for 'decompressed size' appended to the frame header.
90
+ #
91
+ # In ID3v2.4 A 'Data Length Indicator' byte MUST be included in the frame
92
+ # when frame is compressed, and 'Data Length Indicator'represented as a 32 bit
93
+ # synchsafe integer
94
+ #
95
+ # So skip those 4 byte.
96
+ if compressed? || data_length_indicator?
97
+ @file_io.seek(4, IO::SEEK_CUR)
98
+ @size -= 4
99
+ end
100
+ end
101
+
102
+ def valid?
103
+ @size > 0 && !@name.nil?
104
+ end
105
+
106
+ def compressed?
107
+ @flags.include? :compression
108
+ end
109
+
110
+ def data_length_indicator?
111
+ @flags.include? :data_length_indicator
112
+ end
113
+
114
+ def value
115
+ return unless @size > 0
116
+
117
+ content = compressed? ? Zlib.inflate(data) : data
118
+ frame_body = frame_body_class.new(content, @version)
119
+ frame_body.value
120
+ end
121
+
122
+ private
123
+
124
+ # ID3v2.2 frame header structure:
125
+ #
126
+ # Frame ID $xx xx xx(tree characters)
127
+ # Size 3 * %xxxxxxxx
128
+ #
129
+ # ID3v2.3 frame header structure:
130
+ #
131
+ # Frame ID $xx xx xx xx (four characters)
132
+ # Size 4 * %xxxxxxxx
133
+ # Flags $xx xx
134
+ #
135
+ # ID3v2.4 frame header structure:
136
+ #
137
+ # Frame ID $xx xx xx xx (four characters)
138
+ # Size 4 * %0xxxxxxx
139
+ # Flags $xx xx
140
+ def parse_frame_header
141
+ header_size = @version == 2 ? 6 : 10
142
+ header_formate = @version == 2 ? "A3B24" : "A4B32B16"
143
+ id, size_bits, flags_bits = @file_io.read(header_size).unpack(header_formate)
144
+
145
+ @name = ID_MAPPING[id.to_sym]
146
+ @size = Helper.id3_size_caculate(size_bits, has_zero_bit: @version == 4)
147
+ @flags = parse_flags(flags_bits)
148
+ end
149
+
150
+ def parse_flags(flags_bits)
151
+ return [] if flags_bits.nil?
152
+
153
+ frame_flags_indications = @version == 4 ?
154
+ V4_HEADER_FLAGS_INDICATIONS :
155
+ V3_HEADER_FLAGS_INDICATIONS
156
+
157
+ flags_bits.chars.map.with_index do |flag_bit, index|
158
+ frame_flags_indications[index] if flag_bit == "1"
159
+ end.compact
160
+ end
161
+
162
+ def frame_body_class
163
+ case @name
164
+ when :comment
165
+ CommentFrameBody
166
+ when :genre
167
+ GenreFrameBody
168
+ when :image
169
+ ImageFrameBody
170
+ else
171
+ TextFrameBody
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class FrameBody
6
+ # Textual frames are marked with an encoding byte.
7
+ #
8
+ # $00 ISO-8859-1 [ISO-8859-1]. Terminated with $00.
9
+ # $01 UTF-16 [UTF-16] encoded Unicode [UNICODE] with BOM.
10
+ # $02 UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
11
+ # $03 UTF-8 [UTF-8] encoded Unicode [UNICODE].
12
+ ENCODING_MAPPING = %w[ISO-8859-1 UTF-16 UTF-16BE UTF-8]
13
+
14
+ ENCODING_TERMINATOR_SIZE = {
15
+ "ISO-8859-1" => 1,
16
+ "UTF-16" => 2,
17
+ "UTF-16BE" => 2,
18
+ "UTF-8" => 1
19
+ }
20
+
21
+ attr_reader :value
22
+
23
+ def initialize(content, version)
24
+ @content = content
25
+ @version = version
26
+
27
+ parse
28
+ end
29
+
30
+ def parse
31
+ raise WahWahNotImplementedError, "The parse method is not implemented"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class GenreFrameBody < TextFrameBody
6
+ def parse
7
+ super
8
+
9
+ # If value is numeric value, or contain numeric value in parens
10
+ # can use as index for ID3v1 genre list
11
+ @value = ID3::V1::GENRES[$1.to_i] if @value =~ /^\((\d+)\)$/ || @value =~ /^(\d+)$/
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class ImageFrameBody < FrameBody
6
+ TYPES = %i[
7
+ other
8
+ file_icon
9
+ other_file_icon
10
+ cover_front
11
+ cover_back
12
+ leaflet
13
+ media
14
+ lead_artist
15
+ artist
16
+ conductor
17
+ band
18
+ composer
19
+ lyricist
20
+ recording_location
21
+ during_recording
22
+ during_performance
23
+ movie_screen_capture
24
+ bright_coloured_fish
25
+ illustration
26
+ band_logotype
27
+ publisher_logotype
28
+ ]
29
+
30
+ def mime_type
31
+ mime_type = @mime_type.downcase.yield_self { |type| type == "jpg" ? "jpeg" : type }
32
+ @version > 2 ? mime_type : "image/#{mime_type}"
33
+ end
34
+
35
+ # ID3v2.2 image frame structure:
36
+ #
37
+ # Text encoding $xx
38
+ # Image format $xx xx xx
39
+ # Picture type $xx
40
+ # Description <text string according to encoding> $00 (00)
41
+ # Picture data <binary data>
42
+ #
43
+ # ID3v2.3 and ID3v2.4 image frame structure:
44
+ #
45
+ # Text encoding $xx
46
+ # MIME type <text string> $00
47
+ # Picture type $xx
48
+ # Description <text string according to encoding> $00 (00)
49
+ # Picture data <binary data>
50
+ def parse
51
+ frame_format = @version > 2 ? "CZ*Ca*" : "Ca3Ca*"
52
+ encoding_id, @mime_type, type_index, reset_content = @content.unpack(frame_format)
53
+ encoding = ENCODING_MAPPING[encoding_id]
54
+ _description, data = Helper.split_with_terminator(reset_content, ENCODING_TERMINATOR_SIZE[encoding])
55
+
56
+ @value = {data: data, mime_type: mime_type, type: TYPES[type_index]}
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class TextFrameBody < FrameBody
6
+ # Text frame boby structure:
7
+ #
8
+ # Text encoding $xx
9
+ # Information <text string according to encoding>
10
+ def parse
11
+ encoding_id, text = @content.unpack("Ca*")
12
+ @value = Helper.encode_to_utf8(text, source_encoding: ENCODING_MAPPING[encoding_id])
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,97 @@
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
+
61
+ # For ID3v1 info, see here https://en.wikipedia.org/wiki/ID3#ID3v1
62
+ #
63
+ # header 3 "TAG"
64
+ # title 30 30 characters of the title
65
+ # artist 30 30 characters of the artist name
66
+ # album 30 30 characters of the album name
67
+ # year 4 A four-digit year
68
+ # comment 28 or 30 The comment.
69
+ # zero-byte 1 If a track number is stored, this byte contains a binary 0.
70
+ # track 1 The number of the track on the album, or 0. Invalid, if previous byte is not a binary 0.
71
+ # genre 1 Index in a list of genres, or 255
72
+ def parse
73
+ return unless @file_io.size >= TAG_SIZE
74
+
75
+ @file_io.seek(-TAG_SIZE, IO::SEEK_END)
76
+ @id = Helper.encode_to_utf8(@file_io.read(3), source_encoding: DEFAULT_ENCODING)
77
+
78
+ return unless valid?
79
+
80
+ @title = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
81
+ @artist = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
82
+ @album = Helper.encode_to_utf8(@file_io.read(30), source_encoding: DEFAULT_ENCODING)
83
+ @year = Helper.encode_to_utf8(@file_io.read(4), source_encoding: DEFAULT_ENCODING)
84
+
85
+ comment = @file_io.read(30)
86
+
87
+ if comment.getbyte(-2) == 0
88
+ @track = comment.getbyte(-1)
89
+ comment = comment.byteslice(0..-3)
90
+ end
91
+
92
+ @comments.push(Helper.encode_to_utf8(comment, source_encoding: DEFAULT_ENCODING))
93
+ @genre = GENRES[@file_io.getbyte] || ""
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class V2 < Tag
6
+ extend Forwardable
7
+
8
+ def_delegators :@header, :major_version, :size, :has_extended_header?, :valid?
9
+
10
+ def version
11
+ "v2.#{major_version}"
12
+ end
13
+
14
+ private
15
+
16
+ def parse
17
+ @file_io.rewind
18
+ @header = V2Header.new(@file_io)
19
+
20
+ return unless valid?
21
+
22
+ until end_of_tag?
23
+ frame = ID3::Frame.new(@file_io, major_version)
24
+
25
+ unless frame.valid?
26
+ frame.skip
27
+ next
28
+ end
29
+
30
+ update_attribute(frame)
31
+ end
32
+ end
33
+
34
+ def update_attribute(frame)
35
+ name = frame.name
36
+
37
+ case name
38
+ when :comment
39
+ # Because there may be more than one comment frame in each tag,
40
+ # so push it into a array.
41
+ @comments.push(frame.value)
42
+ when :image
43
+ # Because there may be more than one image frame in each tag,
44
+ # so push it into a array.
45
+ @images_data.push(frame)
46
+ frame.skip
47
+ when :track, :disc
48
+ # Track and disc value may be extended with a "/" character
49
+ # and a numeric string containing the total numer.
50
+ count, total_count = frame.value.split("/", 2)
51
+ instance_variable_set("@#{name}", count)
52
+ instance_variable_set("@#{name}_total", total_count) unless total_count.nil?
53
+ else
54
+ instance_variable_set("@#{name}", frame.value)
55
+ end
56
+ end
57
+
58
+ def end_of_tag?
59
+ size <= @file_io.pos || file_size <= @file_io.pos
60
+ end
61
+
62
+ def parse_image_data(image_frame)
63
+ image_frame.value
64
+ end
65
+ end
66
+ end
67
+ 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).unpack1("B32"))
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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module LazyRead
5
+ def self.prepended(base)
6
+ base.class_eval do
7
+ attr_reader :size
8
+ end
9
+ end
10
+
11
+ def initialize(file_io, *arg)
12
+ @file_io = file_io
13
+ super(*arg)
14
+ @position = @file_io.pos
15
+ @data = get_data if @file_io.is_a?(StringIO)
16
+ end
17
+
18
+ def data
19
+ if @file_io.closed? && @file_io.is_a?(File)
20
+ @file_io = File.open(@file_io.path)
21
+ @data = get_data
22
+ @file_io.close
23
+ end
24
+
25
+ @data ||= get_data
26
+ end
27
+
28
+ def skip
29
+ @file_io.seek(@position)
30
+ @file_io.seek(size, IO::SEEK_CUR)
31
+ end
32
+
33
+ private
34
+
35
+ def get_data
36
+ @file_io.seek(@position)
37
+ @file_io.read(size)
38
+ end
39
+ end
40
+ end