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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f0442dc7cdc627cfe026cd772f5c244089b4d3ab0ac2c1343381b4462efe7de
4
- data.tar.gz: 69f88a13f1e4aa4df4e76f9ab25d698448468176f74023ee6f9cdc7018236811
3
+ metadata.gz: 9db3f9953fd6a1641859fa4d27912ed6fdd1eec6b5a137498e1d7fa669903ea4
4
+ data.tar.gz: 23f36dddc181188c98d77a546960273ed6dd4c1f2dc548d0214cc3f2feeac060
5
5
  SHA512:
6
- metadata.gz: 2ee68cdad4790218492dbd49545a421a6be8ccdc006ca0eb20254f2f0c083f11b580b5c39778af899e01a341136a84203e89e6f6eb55df3f9ce3d584d364ea24
7
- data.tar.gz: 9493abddeaa6997bd6368e83b3ff225dd579ecfd3a29780cab227aec6a9aefab025cdae73a9684093c5a3ea9fb9559bdcab0e4723ca2326e07ae721a23d11f87
6
+ metadata.gz: 31171f19be0c4777e0fdcaa03c8d02449856847eb205e5867f0d76f0c8e44b221e98c3808834623e4bd943c4f94b8728fef17c0ebc91f3371a5fb2ce636cb5a4
7
+ data.tar.gz: 810d6c29379773f93e61d905be863fbda4eb1b0fa0127b10e52d9357bf1faa267df71f0bebf09d8652733e73c057eecc5e2fd5aea3da2500ac496aa3cb9a774e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Aidewoode <aidewoode@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Asf
5
+ # The base unit of organization for ASF files is called the ASF object.
6
+ # It consists of a 128-bit GUID for the object, a 64-bit integer object size, and the variable-length object data.
7
+ # The value of the object size field is the sum of 24 bytes plus the size of the object data in bytes.
8
+ # The following diagram illustrates the ASF object structure:
9
+ #
10
+ # 16 bytes: Object GUID
11
+ # 8 bytes: Object size
12
+ # variable-sized: Object data
13
+ class Object
14
+ prepend LazyRead
15
+
16
+ HEADER_SIZE = 24
17
+ HEADER_FORMAT = "a16Q<"
18
+
19
+ attr_reader :guid
20
+
21
+ def initialize
22
+ guid_bytes, @size = @file_io.read(HEADER_SIZE)&.unpack(HEADER_FORMAT)
23
+ return unless valid?
24
+
25
+ @size -= HEADER_SIZE
26
+ @guid = Helper.byte_string_to_guid(guid_bytes)
27
+ end
28
+
29
+ def valid?
30
+ !@size.nil? && @size >= HEADER_SIZE
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ class AsfTag < Tag
5
+ HEADER_OBJECT_CONTENT_SIZE = 6
6
+ HEADER_OBJECT_GUID = "75B22630-668E-11CF-A6D9-00AA0062CE6C"
7
+ FILE_PROPERTIES_OBJECT_GUID = "8CABDCA1-A947-11CF-8EE4-00C00C205365"
8
+ EXTENDED_CONTENT_DESCRIPTION_OBJECT_GUID = "D2D0A440-E307-11D2-97F0-00A0C95EA850"
9
+ STREAM_PROPERTIES_OBJECT_GUID = "B7DC0791-A9B7-11CF-8EE6-00C00C205365"
10
+ AUDIO_MEDIA_OBJECT_GUID = "F8699E40-5B4D-11CF-A8FD-00805F5C442B"
11
+ CONTENT_DESCRIPTION_OBJECT_GUID = "75B22633-668E-11CF-A6D9-00AA0062CE6C"
12
+
13
+ EXTENDED_CONTENT_DESCRIPTOR_NAME_MAPPING = {
14
+ "WM/AlbumArtist" => :albumartist,
15
+ "WM/AlbumTitle" => :album,
16
+ "WM/Composer" => :composer,
17
+ "WM/Genre" => :genre,
18
+ "WM/PartOfSet" => :disc,
19
+ "WM/TrackNumber" => :track,
20
+ "WM/Year" => :year
21
+ }
22
+
23
+ private
24
+
25
+ # ASF files are logically composed of three types of top-level objects:
26
+ # the Header Object, the Data Object, and the Index Object(s).
27
+ # The Header Object is mandatory and must be placed at the beginning of every ASF file.
28
+ # Of the three top-level ASF objects, the Header Object is the only one that contains other ASF objects.
29
+ # All Unicode strings in ASF uses UTF-16, little endian, and the Byte-Order Marker (BOM) character is not present.
30
+ def parse
31
+ header_object = Asf::Object.new(@file_io)
32
+ return unless header_object.valid?
33
+
34
+ total_header_object_size = header_object.size + Asf::Object::HEADER_SIZE
35
+
36
+ return unless header_object.guid == HEADER_OBJECT_GUID
37
+
38
+ # Header Object contains 6 bytes useless data, so skip it.
39
+ @file_io.seek(HEADER_OBJECT_CONTENT_SIZE, IO::SEEK_CUR)
40
+
41
+ until total_header_object_size <= @file_io.pos
42
+ sub_object = Asf::Object.new(@file_io)
43
+ parse_sub_object(sub_object)
44
+ end
45
+ end
46
+
47
+ def parse_sub_object(sub_object)
48
+ case sub_object.guid
49
+ when FILE_PROPERTIES_OBJECT_GUID
50
+ parse_file_properties_object(sub_object)
51
+ when EXTENDED_CONTENT_DESCRIPTION_OBJECT_GUID
52
+ parse_extended_content_description_object(sub_object)
53
+ when STREAM_PROPERTIES_OBJECT_GUID
54
+ parse_stream_properties_object(sub_object)
55
+ when CONTENT_DESCRIPTION_OBJECT_GUID
56
+ parse_content_description_object(sub_object)
57
+ else
58
+ sub_object.skip
59
+ end
60
+ end
61
+
62
+ # File Properties Object structure:
63
+ #
64
+ # Field name Field type Size (bits)
65
+ #
66
+ # Object ID GUID 128
67
+ # Object Size QWORD 64
68
+ # File ID GUID 128
69
+ # File Size QWORD 64
70
+ # Creation Date QWORD 64
71
+ # Data Packets Count QWORD 64
72
+ # Play Duration QWORD 64
73
+ # Send Duration QWORD 64
74
+ # Preroll QWORD 64
75
+ # Flags DWORD 32
76
+ # Broadcast Flag 1 (LSB)
77
+ # Seekable Flag 1
78
+ # Reserved 30
79
+ # Minimum Data Packet Size DWORD 32
80
+ # Maximum Data Packet Size DWORD 32
81
+ # Maximum Bitrate DWORD 32
82
+ #
83
+ # Play Duration Specifies the time needed to play the file in 100-nanosecond units.
84
+ # The value of this field is invalid if the Broadcast Flag bit in the Flags field is set to 1.
85
+ #
86
+ # Preroll Specifies the amount of time to buffer data before starting to play the file, in millisecond units.
87
+ # If this value is nonzero, the Play Duration field and all of the payload Presentation Time fields have been offset by this amount.
88
+ def parse_file_properties_object(object)
89
+ play_duration, preroll, flags = object.data.unpack("x40Q<x8Q<b32")
90
+ @duration = play_duration / 10000000.0 - preroll / 1000.0 if flags[0] == "0"
91
+ end
92
+
93
+ # Extended Content Description Object structure:
94
+ #
95
+ # Field name Field type Size (bits)
96
+ #
97
+ # Object ID GUID 128
98
+ # Object Size QWORD 64
99
+ # Content Descriptors Count WORD 16
100
+ # Content Descriptors See text varies
101
+ #
102
+ #
103
+ # The structure of each Content Descriptor:
104
+ #
105
+ # Field Name Field Type Size (bits)
106
+ #
107
+ # Descriptor Name Length WORD 16
108
+ # Descriptor Name WCHAR varies
109
+ # Descriptor Value Data Type WORD 16
110
+ # Descriptor Value Length WORD 16
111
+ # Descriptor Value See text varies
112
+ #
113
+ #
114
+ # Specifies the type of data stored in the Descriptor Value field.
115
+ # The types are defined in the following table.
116
+ #
117
+ # Value Type Descriptor value length
118
+ #
119
+ # 0x0000 Unicode string varies
120
+ # 0x0001 BYTE array varies
121
+ # 0x0002 BOOL 32
122
+ # 0x0003 DWORD 32
123
+ # 0x0004 QWORD 64
124
+ # 0x0005 WORD 16
125
+ def parse_extended_content_description_object(object)
126
+ object_data = StringIO.new(object.data)
127
+ descriptors_count = object_data.read(2).unpack1("v")
128
+
129
+ descriptors_count.times do
130
+ name_length = object_data.read(2).unpack1("v")
131
+ name = Helper.encode_to_utf8(object_data.read(name_length), source_encoding: "UTF-16LE")
132
+ value_type, value_length = object_data.read(4).unpack("vv")
133
+ value = object_data.read(value_length)
134
+
135
+ attr_value = case value_type
136
+ when 0
137
+ Helper.encode_to_utf8(value, source_encoding: "UTF-16LE")
138
+ when 1
139
+ value
140
+ when 2, 3
141
+ value.unpack1("V")
142
+ when 4
143
+ value.unpack1("Q<")
144
+ when 5
145
+ value.unpack1("v")
146
+ end
147
+
148
+ attr_name = EXTENDED_CONTENT_DESCRIPTOR_NAME_MAPPING[name]
149
+ instance_variable_set("@#{attr_name}", attr_value) unless attr_name.nil?
150
+ end
151
+ end
152
+
153
+ # Stream Properties Object structure:
154
+ #
155
+ # Field Name Field Type Size (bits)
156
+ # Object ID GUID 128
157
+ # Object Size QWORD 64
158
+ # Stream Type GUID 128
159
+ # Error Correction Type GUID 128
160
+ # Time Offset QWORD 64
161
+ # Type-Specific Data Length DWORD 32
162
+ # Error Correction Data Length DWORD 32
163
+ # Flags WORD 16
164
+ # Stream Number 7 (LSB)
165
+ # Reserved 8
166
+ # Encrypted Content Flag 1
167
+ # Reserved DWORD 32
168
+ # Type-Specific Data BYTE varies
169
+ # Error Correction Data BYTE varies
170
+ #
171
+ # Stream Type specifies the type of the stream (for example, audio, video, and so on).
172
+ # Any streams with unrecognized Stream Type values should be ignored.
173
+ #
174
+ # Audio media type Object structure:
175
+ #
176
+ # Field name Field type Size (bits)
177
+ #
178
+ # Codec ID / Format Tag WORD 16
179
+ # Number of Channels WORD 16
180
+ # Samples Per Second DWORD 32
181
+ # Average Number of Bytes Per Second DWORD 32
182
+ # Block Alignment WORD 16
183
+ # Bits Per Sample WORD 16
184
+ def parse_stream_properties_object(object)
185
+ object_data = StringIO.new(object.data)
186
+ stream_type, type_specific_data_length = object_data.read(54).unpack("a16x24V")
187
+ stream_type_guid = Helper.byte_string_to_guid(stream_type)
188
+
189
+ return unless stream_type_guid == AUDIO_MEDIA_OBJECT_GUID
190
+
191
+ @sample_rate, bytes_per_second, @bit_depth = object_data.read(type_specific_data_length).unpack("x4VVx2v")
192
+ @bitrate = (bytes_per_second * 8.0 / 1000).round
193
+ end
194
+
195
+ # Content Description Object structure:
196
+ #
197
+ # Field name Field type Size (bits)
198
+ #
199
+ # Object ID GUID 128
200
+ # Object Size QWORD 64
201
+ # Title Length WORD 16
202
+ # Author Length WORD 16
203
+ # Copyright Length WORD 16
204
+ # Description Length WORD 16
205
+ # Rating Length WORD 16
206
+ # Title WCHAR Varies
207
+ # Author WCHAR Varies
208
+ # Copyright WCHAR Varies
209
+ # Description WCHAR Varies
210
+ # Rating WCHAR Varies
211
+ def parse_content_description_object(object)
212
+ object_data = StringIO.new(object.data)
213
+ title_length, author_length, copyright_length, description_length, _ = object_data.read(10).unpack("v" * 5)
214
+
215
+ @title = Helper.encode_to_utf8(object_data.read(title_length), source_encoding: "UTF-16LE")
216
+ @artist = Helper.encode_to_utf8(object_data.read(author_length), source_encoding: "UTF-16LE")
217
+ object_data.seek(copyright_length, IO::SEEK_CUR)
218
+ @comments.push(Helper.encode_to_utf8(object_data.read(description_length), source_encoding: "UTF-16LE"))
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ class WahWahArgumentError < ArgumentError; end
5
+
6
+ class WahWahNotImplementedError < RuntimeError; end
7
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Flac
5
+ class Block
6
+ prepend LazyRead
7
+
8
+ HEADER_SIZE = 4
9
+ HEADER_FORMAT = "B*"
10
+ BLOCK_TYPE_INDEX = %w[STREAMINFO PADDING APPLICATION SEEKTABLE VORBIS_COMMENT CUESHEET PICTURE]
11
+
12
+ attr_reader :type
13
+
14
+ def initialize
15
+ # Block header structure:
16
+ #
17
+ # Length(bit) Meaning
18
+ #
19
+ # 1 Last-metadata-block flag:
20
+ # '1' if this block is the last metadata block before the audio blocks, '0' otherwise.
21
+ #
22
+ # 7 BLOCK_TYPE
23
+ # 0 : STREAMINFO
24
+ # 1 : PADDING
25
+ # 2 : APPLICATION
26
+ # 3 : SEEKTABLE
27
+ # 4 : VORBIS_COMMENT
28
+ # 5 : CUESHEET
29
+ # 6 : PICTURE
30
+ # 7-126 : reserved
31
+ # 127 : invalid, to avoid confusion with a frame sync code
32
+ #
33
+ # 24 Length (in bytes) of metadata to follow
34
+ # (does not include the size of the METADATA_BLOCK_HEADER)
35
+ header_bits = @file_io.read(HEADER_SIZE).unpack1(HEADER_FORMAT)
36
+
37
+ @last_flag = header_bits[0]
38
+ @type = BLOCK_TYPE_INDEX[header_bits[1..7].to_i(2)]
39
+ @size = header_bits[8..].to_i(2)
40
+ end
41
+
42
+ def valid?
43
+ @size > 0
44
+ end
45
+
46
+ def is_last?
47
+ @last_flag.to_i == 1
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Flac
5
+ module StreaminfoBlock
6
+ STREAMINFO_BLOCK_SIZE = 34
7
+
8
+ # STREAMINFO block data structure:
9
+ #
10
+ # Length(bit) Meaning
11
+ #
12
+ # 16 The minimum block size (in samples) used in the stream.
13
+ #
14
+ # 16 The maximum block size (in samples) used in the stream.
15
+ # (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream.
16
+ #
17
+ # 24 The minimum frame size (in bytes) used in the stream.
18
+ # May be 0 to imply the value is not known.
19
+ #
20
+ # 24 The maximum frame size (in bytes) used in the stream.
21
+ # May be 0 to imply the value is not known.
22
+ #
23
+ # 20 Sample rate in Hz. Though 20 bits are available,
24
+ # the maximum sample rate is limited by the structure of frame headers to 655350Hz.
25
+ # Also, a value of 0 is invalid.
26
+ #
27
+ # 3 (number of channels)-1. FLAC supports from 1 to 8 channels
28
+ #
29
+ # 5 (bits per sample)-1. FLAC supports from 4 to 32 bits per sample.
30
+ # Currently the reference encoder and decoders only support up to 24 bits per sample.
31
+ #
32
+ # 36 Total samples in stream. 'Samples' means inter-channel sample,
33
+ # i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels.
34
+ # A value of zero here means the number of total samples is unknown.
35
+ #
36
+ # 128 MD5 signature of the unencoded audio data.
37
+ def parse_streaminfo_block(block_data)
38
+ return unless block_data.size == STREAMINFO_BLOCK_SIZE
39
+
40
+ info_bits = block_data.unpack1("x10B64")
41
+
42
+ @sample_rate = info_bits[0..19].to_i(2)
43
+ @bit_depth = info_bits[23..27].to_i(2) + 1
44
+ total_samples = info_bits[28..].to_i(2)
45
+
46
+ @duration = total_samples.to_f / @sample_rate if @sample_rate > 0
47
+ @bitrate = @sample_rate * @bit_depth / 1000
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ class FlacTag < Tag
5
+ include Ogg::VorbisComment
6
+ include Flac::StreaminfoBlock
7
+
8
+ TAG_ID = "fLaC"
9
+
10
+ private
11
+
12
+ # FLAC structure:
13
+ #
14
+ # The four byte string "fLaC"
15
+ # The STREAMINFO metadata block
16
+ # Zero or more other metadata blocks
17
+ # One or more audio frames
18
+ def parse
19
+ # Flac file maybe contain ID3 header on the start, so skip it if exists
20
+ id3_header = ID3::V2Header.new(@file_io)
21
+ id3_header.valid? ? @file_io.seek(id3_header.size) : @file_io.rewind
22
+
23
+ return if @file_io.read(4) != TAG_ID
24
+
25
+ loop do
26
+ block = Flac::Block.new(@file_io)
27
+ parse_block(block)
28
+
29
+ break if block.is_last? || @file_io.eof?
30
+ end
31
+ end
32
+
33
+ def parse_block(block)
34
+ return unless block.valid?
35
+
36
+ case block.type
37
+ when "STREAMINFO"
38
+ parse_streaminfo_block(block.data)
39
+ when "VORBIS_COMMENT"
40
+ parse_vorbis_comment(block.data)
41
+ when "PICTURE"
42
+ @images_data.push(block)
43
+ block.skip
44
+ else
45
+ block.skip
46
+ end
47
+ end
48
+
49
+ # PICTURE block data structure:
50
+ #
51
+ # Length(bit) Meaning
52
+ #
53
+ # 32 The picture type according to the ID3v2 APIC frame:
54
+ #
55
+ # 32 The length of the MIME type string in bytes.
56
+ #
57
+ # n*8 The MIME type string.
58
+ #
59
+ # 32 The length of the description string in bytes.
60
+ #
61
+ # n*8 The description of the picture, in UTF-8.
62
+ #
63
+ # 32 The width of the picture in pixels.
64
+ #
65
+ # 32 The height of the picture in pixels.
66
+ #
67
+ # 32 The color depth of the picture in bits-per-pixel.
68
+ #
69
+ # 32 For indexed-color pictures (e.g. GIF), the number of colors used, or 0 for non-indexed pictures.
70
+ #
71
+ # 32 The length of the picture data in bytes.
72
+ #
73
+ # n*8 The binary picture data.
74
+ def parse_image_data(picture_block)
75
+ block_content = StringIO.new(picture_block.data)
76
+
77
+ type_index, mime_type_length = block_content.read(8).unpack("NN")
78
+ mime_type = Helper.encode_to_utf8(block_content.read(mime_type_length))
79
+ description_length = block_content.read(4).unpack1("N")
80
+ data_length = block_content.read(description_length + 20).unpack1("#{"x" * (description_length + 16)}N")
81
+ data = block_content.read(data_length)
82
+
83
+ {data: data, mime_type: mime_type, type: ID3::ImageFrameBody::TYPES[type_index]}
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Helper
5
+ def self.encode_to_utf8(string, source_encoding: "")
6
+ encoded_string = source_encoding.empty? ?
7
+ string.force_encoding("utf-8") :
8
+ string.encode("utf-8", source_encoding, invalid: :replace, undef: :replace, replace: "")
9
+
10
+ encoded_string.valid_encoding? ? encoded_string.strip : ""
11
+ end
12
+
13
+ # ID3 size is encoded with four bytes where may the most significant
14
+ # bit (bit 7) is set to zero in every byte,
15
+ # making a total of 28 bits. The zeroed bits are ignored
16
+ def self.id3_size_caculate(bits_string, has_zero_bit: true)
17
+ if has_zero_bit
18
+ bits_string.scan(/.{8}/).map { |byte_string| byte_string[1..] }.join.to_i(2)
19
+ else
20
+ bits_string.to_i(2)
21
+ end
22
+ end
23
+
24
+ def self.split_with_terminator(string, terminator_size)
25
+ string.split(Regexp.new(('\x00' * terminator_size).b), 2)
26
+ end
27
+
28
+ def self.file_format(file_path)
29
+ File.extname(file_path).downcase.delete(".")
30
+ end
31
+
32
+ def self.byte_string_to_guid(byte_string)
33
+ guid = byte_string.unpack("NnnA*").pack("VvvA*").unpack1("H*")
34
+ [guid[0..7], guid[8..11], guid[12..15], guid[16..19], guid[20..]].join("-").upcase
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module ID3
5
+ class CommentFrameBody < FrameBody
6
+ # Comment frame body structure:
7
+ #
8
+ # Text encoding $xx
9
+ # Language $xx xx xx
10
+ # Short content description <textstring> $00 (00)
11
+ # The actual text <textstring>
12
+ def parse
13
+ encoding_id, _language, reset_content = @content.unpack("CA3a*")
14
+ encoding = ENCODING_MAPPING[encoding_id]
15
+ _description, comment_text = Helper.split_with_terminator(reset_content, ENCODING_TERMINATOR_SIZE[encoding])
16
+
17
+ @value = Helper.encode_to_utf8(comment_text, source_encoding: encoding)
18
+ end
19
+ end
20
+ end
21
+ end