wahwah 0.1.0.pre.test

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c4f8d14cd1c6aa34326a6b0bb4e5ac8bed06778a7b981ca0accb86214f7ad9ee
4
+ data.tar.gz: 0763f36ffc903dc02dd47b885c71e11df3248aa3bc513d53d78c41e7f149cda0
5
+ SHA512:
6
+ metadata.gz: 2570bfd1bdaa7705048d23f9e5a89b3f954817b48d2a520ca93901a5b7cad34e32eafa9d46f4d4bb97b371b82fcd1df463ac96a698a10043b65ecc07d470cff1
7
+ data.tar.gz: 045d228d65fbadbe5363a916897f2b812df0e6d2e0fc94e4dc9cd6e09c7f737ad00a96add2fa52564c5c813e6d588e0d4fd1f6d8dfc4b3ed80f4662dee1b68e3
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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wahwah/version'
4
+ require 'wahwah/errors'
5
+ require 'wahwah/helper'
6
+ require 'wahwah/tag_delegate'
7
+ require 'wahwah/tag'
8
+
9
+ require 'wahwah/id3/v1'
10
+ require 'wahwah/id3/v2'
11
+ require 'wahwah/id3/v2_header'
12
+ require 'wahwah/id3/frame'
13
+ require 'wahwah/id3/frame_body'
14
+ require 'wahwah/id3/text_frame_body'
15
+ require 'wahwah/id3/genre_frame_body'
16
+ require 'wahwah/id3/comment_frame_body'
17
+ require 'wahwah/id3/image_frame_body'
18
+
19
+ require 'wahwah/mp3/mpeg_frame_header'
20
+ require 'wahwah/mp3/xing_header'
21
+ require 'wahwah/mp3/vbri_header'
22
+
23
+ require 'wahwah/riff/chunk'
24
+
25
+ require 'wahwah/flac/block'
26
+ require 'wahwah/flac/streaminfo_block'
27
+
28
+ require 'wahwah/ogg/page'
29
+ require 'wahwah/ogg/pages'
30
+ require 'wahwah/ogg/packets'
31
+ require 'wahwah/ogg/vorbis_comment'
32
+ require 'wahwah/ogg/vorbis_tag'
33
+ require 'wahwah/ogg/opus_tag'
34
+ require 'wahwah/ogg/flac_tag'
35
+
36
+ require 'wahwah/asf/object'
37
+
38
+ require 'wahwah/mp4/atom'
39
+
40
+ require 'wahwah/mp3_tag'
41
+ require 'wahwah/mp4_tag'
42
+ require 'wahwah/ogg_tag'
43
+ require 'wahwah/riff_tag'
44
+ require 'wahwah/asf_tag'
45
+ require 'wahwah/flac_tag'
46
+
47
+ module WahWah
48
+ FORMATE_MAPPING = {
49
+ Mp3Tag: ['mp3'],
50
+ OggTag: ['ogg', 'oga', 'opus'],
51
+ RiffTag: ['wav'],
52
+ FlacTag: ['flac'],
53
+ AsfTag: ['wma'],
54
+ Mp4Tag: ['m4a']
55
+ }.freeze
56
+
57
+ def self.open(file_path)
58
+ file_path = file_path.to_path if file_path.respond_to? :to_path
59
+ file_path = file_path.to_str
60
+
61
+ file_format = Helper.file_format(file_path)
62
+
63
+ raise WahWahArgumentError, 'File is not exists' unless File.exist? file_path
64
+ raise WahWahArgumentError, 'File is unreadable' unless File.readable? file_path
65
+ raise WahWahArgumentError, 'File is empty' unless File.size(file_path) > 0
66
+ raise WahWahArgumentError, 'No supported format found' unless support_formats.include? file_format
67
+
68
+ FORMATE_MAPPING.each do |tag, formats|
69
+ break const_get(tag).new(file_path) if formats.include?(file_format)
70
+ end
71
+ end
72
+
73
+ def self.support_formats
74
+ FORMATE_MAPPING.values.flatten
75
+ end
76
+ end
@@ -0,0 +1,39 @@
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
+ HEADER_SIZE = 24
15
+ HEADER_FORMAT = 'a16Q<'
16
+
17
+ attr_reader :size, :guid
18
+
19
+ def initialize(file_io)
20
+ guid_bytes, @size = file_io.read(HEADER_SIZE)&.unpack(HEADER_FORMAT)
21
+ return unless valid?
22
+
23
+ @size = @size - HEADER_SIZE
24
+ @guid = Helper.byte_string_to_guid(guid_bytes)
25
+ @file_io = file_io
26
+ @position = file_io.pos
27
+ end
28
+
29
+ def valid?
30
+ !@size.nil? && @size >= HEADER_SIZE
31
+ end
32
+
33
+ def data
34
+ @file_io.seek(@position)
35
+ @file_io.read(size)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,220 @@
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
+ # ASF files are logically composed of three types of top-level objects:
25
+ # the Header Object, the Data Object, and the Index Object(s).
26
+ # The Header Object is mandatory and must be placed at the beginning of every ASF file.
27
+ # Of the three top-level ASF objects, the Header Object is the only one that contains other ASF objects.
28
+ # All Unicode strings in ASF uses UTF-16, little endian, and the Byte-Order Marker (BOM) character is not present.
29
+ def parse
30
+ header_object = Asf::Object.new(@file_io)
31
+ return unless header_object.valid?
32
+
33
+ total_header_object_size = header_object.size + Asf::Object::HEADER_SIZE
34
+
35
+ return unless header_object.guid == HEADER_OBJECT_GUID
36
+
37
+ # Header Object contains 6 bytes useless data, so skip it.
38
+ @file_io.seek(HEADER_OBJECT_CONTENT_SIZE, IO::SEEK_CUR)
39
+
40
+ until total_header_object_size <= @file_io.pos
41
+ sub_object = Asf::Object.new(@file_io)
42
+ parse_sub_object(sub_object)
43
+ end
44
+ end
45
+
46
+ def parse_sub_object(sub_object)
47
+ case sub_object.guid
48
+ when FILE_PROPERTIES_OBJECT_GUID
49
+ parse_file_properties_object(sub_object)
50
+ when EXTENDED_CONTENT_DESCRIPTION_OBJECT_GUID
51
+ parse_extended_content_description_object(sub_object)
52
+ when STREAM_PROPERTIES_OBJECT_GUID
53
+ parse_stream_properties_object(sub_object)
54
+ when CONTENT_DESCRIPTION_OBJECT_GUID
55
+ parse_content_description_object(sub_object)
56
+ else
57
+ @file_io.seek(sub_object.size, IO::SEEK_CUR)
58
+ end
59
+ end
60
+
61
+ # File Properties Object structure:
62
+ #
63
+ # Field name Field type Size (bits)
64
+ #
65
+ # Object ID GUID 128
66
+ # Object Size QWORD 64
67
+ # File ID GUID 128
68
+ # File Size QWORD 64
69
+ # Creation Date QWORD 64
70
+ # Data Packets Count QWORD 64
71
+ # Play Duration QWORD 64
72
+ # Send Duration QWORD 64
73
+ # Preroll QWORD 64
74
+ # Flags DWORD 32
75
+ # Broadcast Flag 1 (LSB)
76
+ # Seekable Flag 1
77
+ # Reserved 30
78
+ # Minimum Data Packet Size DWORD 32
79
+ # Maximum Data Packet Size DWORD 32
80
+ # Maximum Bitrate DWORD 32
81
+ #
82
+ # Play Duration Specifies the time needed to play the file in 100-nanosecond units.
83
+ # The value of this field is invalid if the Broadcast Flag bit in the Flags field is set to 1.
84
+ #
85
+ # Preroll Specifies the amount of time to buffer data before starting to play the file, in millisecond units.
86
+ # If this value is nonzero, the Play Duration field and all of the payload Presentation Time fields have been offset by this amount.
87
+ def parse_file_properties_object(object)
88
+ play_duration, preroll, flags = object.data.unpack('x40Q<x8Q<b32')
89
+ @duration = (play_duration / 10000000.0 - preroll / 1000.0).round if flags[0] == '0'
90
+ end
91
+
92
+ # Extended Content Description Object structure:
93
+ #
94
+ # Field name Field type Size (bits)
95
+ #
96
+ # Object ID GUID 128
97
+ # Object Size QWORD 64
98
+ # Content Descriptors Count WORD 16
99
+ # Content Descriptors See text varies
100
+ #
101
+ #
102
+ # The structure of each Content Descriptor:
103
+ #
104
+ # Field Name Field Type Size (bits)
105
+ #
106
+ # Descriptor Name Length WORD 16
107
+ # Descriptor Name WCHAR varies
108
+ # Descriptor Value Data Type WORD 16
109
+ # Descriptor Value Length WORD 16
110
+ # Descriptor Value See text varies
111
+ #
112
+ #
113
+ # Specifies the type of data stored in the Descriptor Value field.
114
+ # The types are defined in the following table.
115
+ #
116
+ # Value Type Descriptor value length
117
+ #
118
+ # 0x0000 Unicode string varies
119
+ # 0x0001 BYTE array varies
120
+ # 0x0002 BOOL 32
121
+ # 0x0003 DWORD 32
122
+ # 0x0004 QWORD 64
123
+ # 0x0005 WORD 16
124
+ def parse_extended_content_description_object(object)
125
+ object_data = StringIO.new(object.data)
126
+ descriptors_count = object_data.read(2).unpack('v').first
127
+
128
+ descriptors_count.times do
129
+ name_length = object_data.read(2).unpack('v').first
130
+ name = Helper.encode_to_utf8(object_data.read(name_length), source_encoding: 'UTF-16LE')
131
+ value_type, value_length = object_data.read(4).unpack('vv')
132
+ value = object_data.read(value_length)
133
+
134
+ attr_value = case value_type
135
+ when 0
136
+ Helper.encode_to_utf8(value, source_encoding: 'UTF-16LE')
137
+ when 1
138
+ value
139
+ when 2, 3
140
+ value.unpack('V').first
141
+ when 4
142
+ value.unpack('Q<').first
143
+ when 5
144
+ value.unpack('v').first
145
+ end
146
+
147
+ attr_name = EXTENDED_CONTENT_DESCRIPTOR_NAME_MAPPING[name]
148
+ instance_variable_set("@#{attr_name}", attr_value) unless attr_name.nil?
149
+ end
150
+ end
151
+
152
+ # Stream Properties Object structure:
153
+ #
154
+ # Field Name Field Type Size (bits)
155
+ # Object ID GUID 128
156
+ # Object Size QWORD 64
157
+ # Stream Type GUID 128
158
+ # Error Correction Type GUID 128
159
+ # Time Offset QWORD 64
160
+ # Type-Specific Data Length DWORD 32
161
+ # Error Correction Data Length DWORD 32
162
+ # Flags WORD 16
163
+ # Stream Number 7 (LSB)
164
+ # Reserved 8
165
+ # Encrypted Content Flag 1
166
+ # Reserved DWORD 32
167
+ # Type-Specific Data BYTE varies
168
+ # Error Correction Data BYTE varies
169
+ #
170
+ # Stream Type specifies the type of the stream (for example, audio, video, and so on).
171
+ # Any streams with unrecognized Stream Type values should be ignored.
172
+ #
173
+ # Audio media type Object structure:
174
+ #
175
+ # Field name Field type Size (bits)
176
+ #
177
+ # Codec ID / Format Tag WORD 16
178
+ # Number of Channels WORD 16
179
+ # Samples Per Second DWORD 32
180
+ # Average Number of Bytes Per Second DWORD 32
181
+ # Block Alignment WORD 16
182
+ # Bits Per Sample WORD 16
183
+ def parse_stream_properties_object(object)
184
+ object_data = StringIO.new(object.data)
185
+ stream_type, type_specific_data_length = object_data.read(54).unpack('a16x24V')
186
+ stream_type_guid = Helper.byte_string_to_guid(stream_type)
187
+
188
+ return unless stream_type_guid == AUDIO_MEDIA_OBJECT_GUID
189
+
190
+ @sample_rate, bytes_per_second = object_data.read(type_specific_data_length).unpack('x4VV')
191
+ @bitrate = (bytes_per_second * 8.0 / 1000).round
192
+ end
193
+
194
+ # Content Description Object structure:
195
+ #
196
+ # Field name Field type Size (bits)
197
+ #
198
+ # Object ID GUID 128
199
+ # Object Size QWORD 64
200
+ # Title Length WORD 16
201
+ # Author Length WORD 16
202
+ # Copyright Length WORD 16
203
+ # Description Length WORD 16
204
+ # Rating Length WORD 16
205
+ # Title WCHAR Varies
206
+ # Author WCHAR Varies
207
+ # Copyright WCHAR Varies
208
+ # Description WCHAR Varies
209
+ # Rating WCHAR Varies
210
+ def parse_content_description_object(object)
211
+ object_data = StringIO.new(object.data)
212
+ title_length, author_length, copyright_length, description_length, _ = object_data.read(10).unpack('v' * 5)
213
+
214
+ @title = Helper.encode_to_utf8(object_data.read(title_length), source_encoding: 'UTF-16LE')
215
+ @artist = Helper.encode_to_utf8(object_data.read(author_length), source_encoding: 'UTF-16LE')
216
+ object_data.seek(copyright_length, IO::SEEK_CUR)
217
+ @comments.push(Helper.encode_to_utf8(object_data.read(description_length), source_encoding: 'UTF-16LE'))
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ class WahWahArgumentError < ArgumentError; end
5
+ class WahWahNotImplementedError < NotImplementedError; end
6
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Flac
5
+ class Block
6
+ HEADER_SIZE = 4
7
+ HEADER_FORMAT = 'B*'
8
+ BLOCK_TYPE_INDEX = %w(STREAMINFO PADDING APPLICATION SEEKTABLE VORBIS_COMMENT CUESHEET PICTURE)
9
+
10
+ attr_reader :size, :type
11
+
12
+ def initialize(file_io)
13
+ # Block header structure:
14
+ #
15
+ # Length(bit) Meaning
16
+ #
17
+ # 1 Last-metadata-block flag:
18
+ # '1' if this block is the last metadata block before the audio blocks, '0' otherwise.
19
+ #
20
+ # 7 BLOCK_TYPE
21
+ # 0 : STREAMINFO
22
+ # 1 : PADDING
23
+ # 2 : APPLICATION
24
+ # 3 : SEEKTABLE
25
+ # 4 : VORBIS_COMMENT
26
+ # 5 : CUESHEET
27
+ # 6 : PICTURE
28
+ # 7-126 : reserved
29
+ # 127 : invalid, to avoid confusion with a frame sync code
30
+ #
31
+ # 24 Length (in bytes) of metadata to follow
32
+ # (does not include the size of the METADATA_BLOCK_HEADER)
33
+ header_bits = file_io.read(HEADER_SIZE).unpack(HEADER_FORMAT).first
34
+
35
+ @last_flag = header_bits[0]
36
+ @type = BLOCK_TYPE_INDEX[header_bits[1..7].to_i(2)]
37
+ @size = header_bits[8..-1].to_i(2)
38
+
39
+ @file_io = file_io
40
+ @position = file_io.pos
41
+ end
42
+
43
+ def valid?
44
+ @size > 0
45
+ end
46
+
47
+ def is_last?
48
+ @last_flag.to_i == 1
49
+ end
50
+
51
+ def data
52
+ @file_io.seek(@position)
53
+ @file_io.read(size)
54
+ end
55
+ end
56
+ end
57
+ 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.unpack('x10B64').first
41
+
42
+ @sample_rate = info_bits[0..19].to_i(2)
43
+ bits_per_sample = info_bits[23..27].to_i(2) + 1
44
+ total_samples = info_bits[28..-1].to_i(2)
45
+
46
+ @duration = (total_samples.to_f / @sample_rate).round if @sample_rate > 0
47
+ @bitrate = @sample_rate * bits_per_sample / 1000
48
+ end
49
+ end
50
+ end
51
+ end