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 +7 -0
- data/.gitignore +8 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +20 -0
- data/LICENSE.txt +19 -0
- data/README.md +33 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/framy_mp3.gemspec +24 -0
- data/lib/framy_mp3/file.rb +139 -0
- data/lib/framy_mp3/frame.rb +257 -0
- data/lib/framy_mp3/id3_tag.rb +30 -0
- data/lib/framy_mp3/version.rb +4 -0
- data/lib/framy_mp3.rb +18 -0
- metadata +85 -0
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
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
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
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
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
|
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: []
|