framy_mp3 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: df66f43f7b69142cbcf47b685af5818122f20a8d8f89418a324fae40118b25e2
4
+ data.tar.gz: bbe6bd841b4ea275bbfa5c3be04234ae2cd255819fc13f826e72eb9ae9ef1a5b
5
+ SHA512:
6
+ metadata.gz: 4159ee84fb1f841a24fe2f0736652d4bec04e1b82a17c792897f712d932b032c014faf96a10ce9d91db97b1d856d7e1f6674209b28a7d144a8a6dcb8349aab37
7
+ data.tar.gz: 227b895ccc1187f952d2f87bd5afc359ba10780d05cc2e7908bf991af93ea2353b8759fb12947cf35ddda124611bac6b33accd297deb35616e314ff3461503c6
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in framy_mp3.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ framy_mp3 (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (10.5.0)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ bundler (~> 1.16)
16
+ framy_mp3!
17
+ rake (~> 10.0)
18
+
19
+ BUNDLED WITH
20
+ 1.16.1
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 Matthew Kobs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # FramyMp3
2
+
3
+ A low-level library for interacting with mp3 files at the frame level. Largely a Ruby port of the Go library [mp3lib](dmulholland/mp3lib) by dmuholland.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'framy_mp3'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install framy_mp3
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Development
26
+
27
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
+
29
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/framy_mp3.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "framy_mp3"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/framy_mp3.gemspec ADDED
@@ -0,0 +1,24 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "framy_mp3/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "framy_mp3"
8
+ spec.version = FramyMP3::VERSION
9
+ spec.authors = ["Matthew Kobs"]
10
+ spec.email = ["matt.kobs@cph.org"]
11
+
12
+ spec.summary = %q{FramyMP3 is a simple library for working with and merging mp3 files in Ruby.}
13
+ spec.homepage = "https://github.com/kobsy/framy_mp3"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.16"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ end
@@ -0,0 +1,139 @@
1
+ require "framy_mp3/frame"
2
+ require "framy_mp3/id3_tag"
3
+
4
+ module FramyMP3
5
+ class File
6
+ attr_reader :frames, :tags
7
+
8
+ def initialize(stream)
9
+ @stream = stream
10
+ @frames = []
11
+ @tags = []
12
+
13
+ while (object = next_object)
14
+ frames << object if object.frame?
15
+ tags << object if object.tag?
16
+ end
17
+ end
18
+
19
+ def to_blob(options={})
20
+ blob = ""
21
+
22
+ keep_xing_headers = vbr? ? false : options.fetch(:keep_xing_headers, false)
23
+
24
+ StringIO.open(blob, "wb") do |outfile|
25
+ initial_tag = id3v2_tag
26
+ outfile << initial_tag.data unless initial_tag.nil?
27
+ outfile << xing_header.data unless keep_xing_headers
28
+ (keep_xing_headers ? frames : frames.reject(&:xing_header?)).each do |frame|
29
+ outfile << frame.data
30
+ end
31
+ closing_tag = id3v1_tag
32
+ outfile << closing_tag.data unless closing_tag.nil?
33
+ end
34
+
35
+ blob
36
+ end
37
+ alias to_s to_blob
38
+
39
+ def variable_bitrate?
40
+ initial_bitrate = frames.first.bitrate
41
+ frames[1..-1].any? do |frame|
42
+ frame.bitrate != initial_bitrate
43
+ end
44
+ end
45
+ alias vbr? variable_bitrate?
46
+
47
+ def id3v2_tag
48
+ tags.find(&:v2?)
49
+ end
50
+
51
+ def id3v1_tag
52
+ tags.find(&:v1?)
53
+ end
54
+
55
+ def total_frames
56
+ frames.count
57
+ end
58
+
59
+ def total_frame_bytes
60
+ frames.map { |frame| frame.data.bytesize }.sum
61
+ end
62
+
63
+ private
64
+ attr_reader :stream
65
+
66
+ def next_frame
67
+ while (object = next_object)
68
+ return object if object.frame?
69
+ end
70
+ end
71
+
72
+ def next_id3v2_tag
73
+ while (object = next_object)
74
+ return object if object.tag? && object.v2?
75
+ end
76
+ end
77
+
78
+ def next_object
79
+ buffer = stream.read(4)&.unpack("C*")
80
+ return if buffer.nil? # EOF
81
+
82
+ loop do
83
+ # Check for an ID3v1 Tag
84
+ if buffer[0] == 84 && buffer[1] == 65 && buffer[2] == 71
85
+ tag_data = buffer.pack("C*")
86
+ remaining_data = stream.read(124)
87
+ return if remaining_data.nil?
88
+ tag_data << remaining_data
89
+ return ID3Tag.new(1, tag_data)
90
+ end
91
+
92
+ # Check for an ID3v2 Tag
93
+ if buffer[0] == 73 && buffer[1] == 68 && buffer[2] == 51
94
+ # Read the remainder of the 10-byte tag header
95
+ remainder = stream.read(6).unpack("C*")
96
+ return nil if remainder.nil?
97
+
98
+ # The last 4 bytes of the header indicate the length of the tag.
99
+ # This length does not include the header itself.
100
+ tag_length =
101
+ (remainder[2] << (7 * 3)) |
102
+ (remainder[3] << (7 * 2)) |
103
+ (remainder[4] << (7 * 1)) |
104
+ (remainder[5] << (7 * 0))
105
+
106
+ tag_data = buffer.pack("C*")
107
+ tag_data << remainder.pack("C*")
108
+ remaining_data = stream.read(tag_length)
109
+ return if remaining_data.nil?
110
+ tag_data << remaining_data
111
+ return ID3Tag.new(2, tag_data)
112
+ end
113
+
114
+ # Check for a frame header, indicated by an 11-bit frame-sync sequence.
115
+ if buffer[0] == 0xFF && (buffer[1] & 0xE0) == 0xE0
116
+ begin
117
+ frame = Frame.new(buffer)
118
+ frame.data = buffer.pack("C*")
119
+ frame.data << stream.read(frame.length - 4)
120
+ return frame
121
+ rescue InvalidFrameError
122
+ # For the time being, we'll simply ignore invalid frames...
123
+ end
124
+ end
125
+
126
+ # Nothing found. Shift the buffer forward by one byte and try again.
127
+ buffer.shift
128
+ next_byte = stream.read(1)&.unpack("C")
129
+ return if next_byte.nil?
130
+ buffer << next_byte
131
+ end
132
+ end
133
+
134
+ def xing_header
135
+ frames.first&.to_xing_header(total_frames: total_frames, total_bytes: total_frame_bytes)
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,257 @@
1
+ module FramyMP3
2
+
3
+ class InvalidFrameError < ArgumentError; end
4
+
5
+ class Frame
6
+ attr_reader :header
7
+ attr_accessor :data
8
+
9
+ MPEG_VERSION_2_5 = 0
10
+ MPEG_VERSION_RESERVED = 1
11
+ MPEG_VERSION_2 = 2
12
+ MPEG_VERSION_1 = 3
13
+
14
+ MPEG_LAYER_RESERVED = 0
15
+ MPEG_LAYER_III = 1
16
+ MPEG_LAYER_II = 2
17
+ MPEG_LAYER_I = 3
18
+
19
+ CHANNEL_MODE_STEREO = 0
20
+ CHANNEL_MODE_JOINT_STEREO = 1
21
+ CHANNEL_MODE_DUAL = 2
22
+ CHANNEL_MODE_MONO = 3
23
+
24
+ BITRATES = {
25
+ MPEG_VERSION_1 => {
26
+ MPEG_LAYER_I => [ 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 ],
27
+ MPEG_LAYER_II => [ 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 ],
28
+ MPEG_LAYER_III => [ 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 ]
29
+ },
30
+ MPEG_VERSION_2 => {
31
+ MPEG_LAYER_I => [ 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 ],
32
+ MPEG_LAYER_II => [ 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ],
33
+ MPEG_LAYER_III => [ 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ]
34
+ }
35
+ }.freeze
36
+
37
+ SAMPLE_RATES = {
38
+ MPEG_VERSION_1 => [ 44_100, 48_000, 32_000 ],
39
+ MPEG_VERSION_2 => [ 22_050, 24_000, 16_000 ],
40
+ MPEG_VERSION_2_5 => [ 11_025, 12_000, 8_000 ]
41
+ }.freeze
42
+
43
+ SAMPLES_PER_FRAME = {
44
+ MPEG_VERSION_1 => {
45
+ MPEG_LAYER_I => 384,
46
+ MPEG_LAYER_II => 1152,
47
+ MPEG_LAYER_III => 1152
48
+ },
49
+ MPEG_VERSION_2 => {
50
+ MPEG_LAYER_I => 384,
51
+ MPEG_LAYER_II => 1152,
52
+ MPEG_LAYER_III => 576
53
+ }
54
+ }.freeze
55
+
56
+ def initialize(header)
57
+ @header = header
58
+ raise InvalidFrameError, "Invalid MPEG version" unless valid_mpeg_version?
59
+ raise InvalidFrameError, "Invalid MPEG layer" unless valid_mpeg_layer?
60
+ raise InvalidFrameError, "Bitrate index out of range" unless valid_bitrate_index?
61
+ raise InvalidFrameError, "Sample rate index out of range" unless valid_sample_rate_index?
62
+ raise InvalidFrameError, "Mode extension can only be used with joint stereo mode" unless valid_mode_extension?
63
+ raise InvalidFrameError, "Invalid emphasis" unless valid_emphasis?
64
+ end
65
+
66
+ def frame?
67
+ true
68
+ end
69
+
70
+ def tag?
71
+ false
72
+ end
73
+
74
+ def mpeg_version
75
+ return @mpeg_version if defined?(@mpeg_version)
76
+ @mpeg_version = (header[1] & 0x18) >> 3 # 2 bits
77
+ end
78
+
79
+ def valid_mpeg_version?
80
+ mpeg_version != MPEG_VERSION_RESERVED
81
+ end
82
+
83
+ def major_mpeg_version
84
+ return @major_mpeg_version if defined?(@major_mpeg_version)
85
+ @major_mpeg_version = mpeg_version == MPEG_VERSION_1 ? MPEG_VERSION_1 : MPEG_VERSION_2
86
+ end
87
+
88
+ def mpeg_layer
89
+ return @mpeg_layer if defined?(@mpeg_layer)
90
+ @mpeg_layer = (header[1] & 0x06) >> 1 # 2 bits
91
+ end
92
+
93
+ def valid_mpeg_layer?
94
+ mpeg_layer != MPEG_LAYER_RESERVED
95
+ end
96
+
97
+ def crc_protection?
98
+ return @crc_protection if defined?(@crc_protection)
99
+ @crc_protection = (header[1] & 0x01).zero? # 1 bit
100
+ end
101
+
102
+ def bitrate_index
103
+ return @bitrate_index if defined?(@bitrate_index)
104
+ @bitrate_index = (header[2] & 0xF0) >> 4 # 4 bits
105
+ end
106
+
107
+ def valid_bitrate_index?
108
+ bitrate_index.positive? && bitrate_index < 15
109
+ end
110
+
111
+ def sample_rate_index
112
+ return @sample_rate_index if defined?(@sample_rate_index)
113
+ @sample_rate_index = (header[2] & 0x0C) >> 2 # 2 bits
114
+ end
115
+
116
+ def valid_sample_rate_index?
117
+ sample_rate_index < 3
118
+ end
119
+
120
+ def padded?
121
+ return @padding_bit if defined?(@padding_bit)
122
+ @padding_bit = (header[2] & 0x02) == 2 # 1 bit
123
+ end
124
+
125
+ def private_bit
126
+ return @private_bit if defined?(@private_bit)
127
+ @private_bit = (header[2] & 0x01) == 1 # 1 bit
128
+ end
129
+
130
+ def channel_mode
131
+ return @channel_mode if defined?(@channel_mode)
132
+ @channel_mode = (header[3] & 0xC0) >> 6 # 2 bits
133
+ end
134
+
135
+ def mode_extension
136
+ return @mode_extension if defined?(@mode_extension)
137
+ @mode_extension = (header[3] & 0x30) >> 4 # 2 bits
138
+ end
139
+
140
+ def valid_mode_extension?
141
+ # Mode extension. Valid only for Joint Stereo mode
142
+ mode_extension.zero? || channel_mode == CHANNEL_MODE_JOINT_STEREO
143
+ end
144
+
145
+ def copyrighted?
146
+ return @copyright_bit if defined?(@copyright_bit)
147
+ @copyright_bit = (header[3] & 0x08) == 0x08 # 1 bit
148
+ end
149
+
150
+ def original?
151
+ return @original_bit if defined?(@original_bit)
152
+ @original_bit = (header[3] & 0x04) == 0x04 # 1 bit
153
+ end
154
+
155
+ def emphasis
156
+ return @emphasis if defined?(@emphasis)
157
+ @emphasis = header[3] & 0x03 # 2 bits
158
+ end
159
+
160
+ def valid_emphasis?
161
+ emphasis < 2
162
+ end
163
+
164
+ def bitrate
165
+ BITRATES[major_mpeg_version][mpeg_layer][bitrate_index] * 1000
166
+ end
167
+
168
+ def sample_rate
169
+ SAMPLE_RATES[mpeg_version][sample_rate_index]
170
+ end
171
+
172
+ def sample_count
173
+ # Number of samples in the frame; we need this to determine the frame size
174
+ SAMPLES_PER_FRAME[major_mpeg_version][mpeg_layer]
175
+ end
176
+
177
+ def padding
178
+ # If the padding bit is set we add an extra 'slot' to the frame length.
179
+ # A layer I slot is 4 bytes long; layer II and III slots are 1 byte long.
180
+ return 0 unless padded?
181
+ mpeg_layer == MPEG_LAYER_I ? 4 : 1
182
+ end
183
+
184
+ def length
185
+ # From mp3lib:
186
+ # Calculate the frame length in bytes. There's a lot of confusion online
187
+ # about how to do this and definitive documentation is hard to find. The
188
+ # basic formula seems to boil down to:
189
+ #
190
+ # bytes_per_sample = (bit_rate / sampling_rate) / 8
191
+ # frame_length = sample_count * bytes_per_sample + padding
192
+ #
193
+ # In practice we need to rearrange this formula to avoid rounding errors.
194
+ #
195
+ # I can't find any definitive statement on whether this length is
196
+ # supposed to include the 4-byte header and the optional 2-byte CRC.
197
+ # Experimentation on mp3 files captured from the wild indicates that it
198
+ # includes the header at least.
199
+ (sample_count / 8) * bitrate / sample_rate + padding
200
+ end
201
+
202
+ def side_information_size
203
+ return unless mpeg_layer == MPEG_LAYER_III
204
+ return channel_mode == CHANNEL_MODE_MONO ? 17 : 32 if mpeg_version == MPEG_VERSION_1
205
+ channel_mode == CHANNEL_MODE_MONO ? 9 : 17
206
+ end
207
+
208
+ def xing_header?
209
+ # The Xing header begins directly after the side information block. We
210
+ # also need to allow 4 bytes for the frame header
211
+ offset = 4 + side_information_size
212
+ identifier = data.unpack("C#{offset + 4}")[-4, 4].pack("C4")
213
+ identifier == "Xing" || identifier == "Info"
214
+ end
215
+
216
+ def vbri_header?
217
+ # The VBRI header begins after a fixed 32-byte offset. We also need to
218
+ # allow 4 bytes for the frame header
219
+ data.unpack("C40")[-4, 4].pack("C4") == "VBRI"
220
+ end
221
+
222
+ def to_xing_header(total_frames:, total_bytes:)
223
+ # Make a shallow copy
224
+ xing_frame = dup
225
+
226
+ # Make a new zeroed-out data slice
227
+ xing_frame.data = ([ 0 ] * length).pack("C")
228
+ StringIO.open(xing_frame.data, "r+b") do |xing_data|
229
+ # Copy over the frame header
230
+ xing_data.write header
231
+
232
+ # Determine the Xing header offset
233
+ offset = 4 + side_information_size
234
+
235
+ # Write the Xing header ID
236
+ xing_data.seek(offset, IO::SEEK_SET)
237
+ xing_data.write "Xing"
238
+
239
+ # Write a flag indicating that the number-of-frames and number-of-bytes
240
+ # fields are present
241
+ xing_data.seek(offset + 7, IO::SEEK_SET)
242
+ xing_data.write [ 3 ].pack("C")
243
+
244
+ # Write the number of frames as a 32-bit big endian unsigned integer
245
+ xing_data.seek(offset + 8, IO::SEEK_SET)
246
+ xing_data.write [ total_frames ].pack("N")
247
+
248
+ # Write the number of bytes as a 32-bit big endian unsigned integer
249
+ xing_data.seek(offset + 12, IO::SEEK_SET)
250
+ xing_data.write [ total_bytes ].pack("N")
251
+ end
252
+
253
+ xing_frame
254
+ end
255
+
256
+ end
257
+ end
@@ -0,0 +1,30 @@
1
+ module FramyMP3
2
+ class ID3Tag
3
+ attr_reader :version, :data
4
+
5
+ def initialize(version, data)
6
+ raise ArgumentError, "version must be either 1 or 2" unless [ 1, 2 ].include?(version)
7
+ @version = version
8
+ @data = data
9
+ end
10
+
11
+ def tag?
12
+ true
13
+ end
14
+
15
+ def frame?
16
+ false
17
+ end
18
+
19
+ def version_1?
20
+ version == 1
21
+ end
22
+ alias v1? version_1?
23
+
24
+ def version_2?
25
+ version == 2
26
+ end
27
+ alias v2? version_2?
28
+
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module FramyMP3
2
+ VERSION = "0.1.0".freeze
3
+
4
+ end
data/lib/framy_mp3.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "framy_mp3/version"
2
+ require "framy_mp3/frame"
3
+ require "framy_mp3/id3_tag"
4
+ require "framy_mp3/file"
5
+
6
+ module FramyMP3
7
+
8
+ def self.merge(*files)
9
+ raise ArgumentError, "all files must be FramyMP3::File" if files.any? { |file| !file.is_a?(FramyMP3::File) }
10
+ raise ArgumentError, "expected at least one file" unless files.count.positive?
11
+ outfile = files.first.dup
12
+ files[1..-1].each do |file|
13
+ outfile.frames.push(*file.frames)
14
+ end
15
+ outfile
16
+ end
17
+
18
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: framy_mp3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Kobs
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description:
42
+ email:
43
+ - matt.kobs@cph.org
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - Gemfile.lock
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - bin/console
55
+ - bin/setup
56
+ - framy_mp3.gemspec
57
+ - lib/framy_mp3.rb
58
+ - lib/framy_mp3/file.rb
59
+ - lib/framy_mp3/frame.rb
60
+ - lib/framy_mp3/id3_tag.rb
61
+ - lib/framy_mp3/version.rb
62
+ homepage: https://github.com/kobsy/framy_mp3
63
+ licenses: []
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.7.3
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: FramyMP3 is a simple library for working with and merging mp3 files in Ruby.
85
+ test_files: []