wahwah 0.1.0 → 1.2.0

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