wahwah 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/lib/wahwah.rb +74 -3
  4. data/lib/wahwah/asf/object.rb +39 -0
  5. data/lib/wahwah/asf_tag.rb +220 -0
  6. data/lib/wahwah/errors.rb +6 -0
  7. data/lib/wahwah/flac/block.rb +57 -0
  8. data/lib/wahwah/flac/streaminfo_block.rb +51 -0
  9. data/lib/wahwah/flac_tag.rb +84 -0
  10. data/lib/wahwah/helper.rb +37 -0
  11. data/lib/wahwah/id3/comment_frame_body.rb +21 -0
  12. data/lib/wahwah/id3/frame.rb +180 -0
  13. data/lib/wahwah/id3/frame_body.rb +36 -0
  14. data/lib/wahwah/id3/genre_frame_body.rb +15 -0
  15. data/lib/wahwah/id3/image_frame_body.rb +60 -0
  16. data/lib/wahwah/id3/text_frame_body.rb +16 -0
  17. data/lib/wahwah/id3/v1.rb +96 -0
  18. data/lib/wahwah/id3/v2.rb +60 -0
  19. data/lib/wahwah/id3/v2_header.rb +53 -0
  20. data/lib/wahwah/mp3/mpeg_frame_header.rb +141 -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 +110 -0
  24. data/lib/wahwah/mp4/atom.rb +105 -0
  25. data/lib/wahwah/mp4_tag.rb +126 -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 +54 -0
  35. data/lib/wahwah/riff_tag.rb +140 -0
  36. data/lib/wahwah/tag.rb +59 -0
  37. data/lib/wahwah/tag_delegate.rb +16 -0
  38. data/lib/wahwah/version.rb +4 -2
  39. metadata +94 -23
  40. data/.gitignore +0 -8
  41. data/.travis.yml +0 -5
  42. data/Gemfile +0 -6
  43. data/Gemfile.lock +0 -22
  44. data/README.md +0 -35
  45. data/Rakefile +0 -10
  46. data/bin/console +0 -14
  47. data/bin/setup +0 -8
  48. 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: a5677433a8471c7ed8ba480c0c7d42804e0ad3494edc88135728766b2778bc01
4
+ data.tar.gz: 3163af32c9dc394f8da2b538a6356c111aeb85981b33ec6c42babbe4634de1c0
5
5
  SHA512:
6
- metadata.gz: 2ee68cdad4790218492dbd49545a421a6be8ccdc006ca0eb20254f2f0c083f11b580b5c39778af899e01a341136a84203e89e6f6eb55df3f9ce3d584d364ea24
7
- data.tar.gz: 9493abddeaa6997bd6368e83b3ff225dd579ecfd3a29780cab227aec6a9aefab025cdae73a9684093c5a3ea9fb9559bdcab0e4723ca2326e07ae721a23d11f87
6
+ metadata.gz: dc8742ff453a6f1826b2458b2daf78bdef6825dbb0ce4e208d11b5c1456ff998ab2f0ff33223393a9a2366e42fc39652930866c1316434859444f016529ad9bb
7
+ data.tar.gz: 1c0a2099a2df566f755f12074ce5628ebaa831d01c679509965cff9b6b950aaa6af0e0c7bd5e6dd6dd770d3868595225645a6f79a32ba5e377eeb01a2ac4cc0c
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.
@@ -1,5 +1,76 @@
1
- require "wahwah/version"
1
+ # frozen_string_literal: true
2
2
 
3
- module Wahwah
4
- # Your code goes here...
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
5
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