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
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