mp3info 0.6.18 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZWUzYTIzOTViZTQ1NmRkMjVkMDk4ZDdlZGNmODNhOGNlOWIwMTE5ZQ==
5
+ data.tar.gz: !binary |-
6
+ MzNhMzRmYzBiODdjNzhiYTYwZGZlNDZiZGFlMzJjZWI4NzZmYTAxMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZTlhNGMwNThiNTllMWQ4ZmU4NWI0NTcwNjhjMWY1N2U1MDAwZjc5MTdkZWVi
10
+ ZTE1MmNmYjI3MDFjOTczODEyMGIxNDMzNjNjMTRmMzU0Y2I2ZDM2ZGJkY2Q4
11
+ Mzc5OTM3ZGI2ZTliM2JmM2NlNTg3M2U0MzY3NjBiOWFmZWFjMTc=
12
+ data.tar.gz: !binary |-
13
+ MDIxYjQxMzRhYTAwMjMzNjI3OWRkODQ5MTljMzEwMWJkM2VhMWYzNTE2NTQ2
14
+ YzUwMzVlM2QxZDA5NGVkYjU0OGQwODIxMTI3NjcyYTcyYTFhMmY3OTllYTg4
15
+ ZTk5ZjEwMDQxMjBiMTNlMTFkMmY5YmI3ZTVjZjliMzJjY2M3NWQ=
metadata CHANGED
@@ -1,76 +1,58 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: mp3info
3
- version: !ruby/object:Gem::Version
4
- hash: 35
5
- prerelease:
6
- segments:
7
- - 0
8
- - 6
9
- - 18
10
- version: 0.6.18
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.5
11
5
  platform: ruby
12
- authors:
6
+ authors:
13
7
  - Guillaume Pierronnet
14
8
  - Ivan Kuchin
15
9
  autorequire:
16
10
  bindir: bin
17
11
  cert_chain: []
18
-
19
- date: 2012-02-21 00:00:00 Z
20
- dependencies: []
21
-
12
+ date: 2014-11-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-mp3info
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: 0.8.5
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ! '>='
26
+ - !ruby/object:Gem::Version
27
+ version: 0.8.5
22
28
  description:
23
29
  email:
24
30
  executables: []
25
-
26
31
  extensions: []
27
-
28
32
  extra_rdoc_files: []
29
-
30
- files:
31
- - .gitignore
32
- - History.txt
33
- - README.rdoc
34
- - lib/mp3info.rb
35
- - lib/mp3info/extension_modules.rb
36
- - lib/mp3info/id3v2.rb
37
- - mp3info.gemspec
38
- - test/fixtures.yml
39
- - test/test_ruby-mp3info.rb
33
+ files: []
40
34
  homepage: http://github.com/toy/mp3info
41
- licenses:
35
+ licenses:
42
36
  - ruby
37
+ metadata: {}
43
38
  post_install_message:
44
39
  rdoc_options: []
45
-
46
- require_paths:
40
+ require_paths:
47
41
  - lib
48
- required_ruby_version: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- hash: 3
54
- segments:
55
- - 0
56
- version: "0"
57
- required_rubygems_version: !ruby/object:Gem::Requirement
58
- none: false
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- hash: 3
63
- segments:
64
- - 0
65
- version: "0"
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
66
52
  requirements: []
67
-
68
- rubyforge_project: mp3info
69
- rubygems_version: 1.8.16
53
+ rubyforge_project:
54
+ rubygems_version: 2.4.3
70
55
  signing_key:
71
- specification_version: 3
56
+ specification_version: 4
72
57
  summary: Read low-level informations and manipulate tags on mp3 files.
73
- test_files:
74
- - test/fixtures.yml
75
- - test/test_ruby-mp3info.rb
76
- has_rdoc:
58
+ test_files: []
data/.gitignore DELETED
@@ -1,12 +0,0 @@
1
- /pkg/
2
- /*.gem
3
-
4
- /doc/
5
- /rdoc/
6
- /.yardoc/
7
- /coverage/
8
-
9
- Makefile
10
- *.o
11
- *.bundle
12
- /tmp/
@@ -1,153 +0,0 @@
1
- === 0.6.17 / 2012-01-15
2
-
3
- * fixed stringio related problems
4
- * cleanup project
5
-
6
- === 0.6.16 / 2011-11-10
7
-
8
- * fixed type error when inspecting mp3info (thanks to Jacob Lichner)
9
-
10
- === 0.6.15 / 2011-07-18
11
-
12
- * support for StringIO as input (thanks to Edd Parris)
13
-
14
- === 0.6.14 / 2011-06-17
15
-
16
- * Added a check for nil that was seen causing problems when processing files. (thanks to Carl Hall)
17
- * Fixed reading on win32, requires binary flag. (thanks to Jonas Tingeborn)
18
- * Fixed white spaces. Replaced tabs with spaces to make the source readable on for users other than the original author. (thanks to Jonas Tingeborn)
19
- * Add :parse_mp3 flag to new/open. (thanks to Dave Lee)
20
- * Add benchmark for parsing performance. (thanks to Dave Lee)
21
- * fixed ID3v2#io_position computing, so Mp3Info#audio_content() is correct now
22
-
23
- === 0.6.13 / 2009-05-26
24
-
25
- * fixed bad mapping of artist inside id3 2.2
26
- * adding fusil fuzzer tests
27
- * Improved support for id3v2.2 to id3v2.3 field mapping
28
- * each_frame() iterator
29
- * removed @bitrate & @length computation based on @tag2[TLEN]
30
-
31
- === 0.6.12 / 2009-02-23
32
-
33
- * fixed bug when @tag2["TLEN"] == 0
34
-
35
- === 0.6.11 / 2009-01-27
36
-
37
- * the library doesn't raise an ID3v2Error anymore when a id3v2 tag size is incorrect, but just output a warning. (Fixes bug #23619)
38
-
39
- === 0.6.10 / 2008-11-27
40
-
41
- * processing of tags (read and write) can be disabled with :parse_tags => false
42
-
43
- === 0.6.9 / 2008-09-16
44
-
45
- * now correctly remove trailing whitespaces form tag1 values
46
- * bugfix #21687 included: 'tweaked the MP3 frame synch code to parse certain mp3 files'
47
-
48
- === 0.6.8 / 2008-08-20
49
-
50
- * support for MPEG 2.5 (thanks to Oleguer Huguet Ibars)
51
- * support for vbr files without Xing header
52
-
53
- === 0.6.7 / 2008-06-26
54
-
55
- * Mp3Info#header hash now gives access to additional mpeg attributes (thanks to Andrew Kuklewicz)
56
-
57
- === 0.6.6 / 2008-05-27
58
-
59
- * avoid reading tag that are too big (> 50Mb)
60
- * ruby 1.9 support (thanks to Dave Thomas)
61
- * FIXED: bug #20311 'Multiple APIC frames may be stored incorrectly'
62
- * FIXED: bug #20312 'doesn't use v2.2 frames for extracting meta data'
63
-
64
- === 0.6.5 / 2008-04-19
65
-
66
- * added Mp3Info#audio_content method, to return "audio-only" boundaries from mp3, i.e. data without tags (closes feature request #17230)
67
- * bugfix on reading id3 2.4 (size not syncsafed)
68
- * more robust tag decoding with bad tags
69
-
70
- === 0.6.4 / 2008-04-16
71
-
72
- * added @tag2["disc_number"] and @tag2["disc_total"] mirroring TPOS attribute (thanks to Harry Ohlsen)
73
-
74
- === 0.6.3 / 2008-03-28
75
-
76
- * some internals modifications for the compatibility with ruby-audioinfo
77
-
78
- === 0.6.2 / 2008-03-02
79
-
80
- * better handling of frames: decode and encode as raw string by default, or handle charset decoding/encoding for /^T/ and COMM frames
81
-
82
- === 0.6.1 / 2008-02-28
83
-
84
- * FIXED: fails to read id3v2 tags when iconv fails
85
-
86
- === 0.6 / 2008-02-24
87
-
88
- * FIXED: correct handling of encoding in id3v2 tags
89
-
90
- === 0.5.1 / 2007-09-10
91
-
92
- * ADDED: Mp3Info#reload method to reload the file from the disk
93
- * FIXED: bug [#2604] Not able to delete tag1
94
- * FIXED: bug #3401 'id3v2.rb dies when trying to read a certain mp3'
95
- * FIXED: bug #2957 'Error message "Can't define singleton"'
96
- * FIXED: bug #3068 'require_gem ("ruby-mp3info") doesn't works'
97
- * FIXED: bug #11967 "Leading 'h' from 'http://' gets chopped on URL fields"
98
- * PATCHED: with patch #3157 'Fix for 64 bit Ruby'
99
-
100
- === 0.5 / 2005-12-06
101
-
102
- * id3v2 writing and removing support added. tag2 attribute is r/w now
103
- * max guess size to find a valid frame set to 2Mb
104
- * implemented a new class ID3v2, ID2TAGS moved into it
105
- * Mp3Info.tag is r/w now and has priority over @tag1 and @tag2 when writing
106
- * added Mp3Info#rename() method to change the filename written at close
107
- * clean up: all overloaded standards classes replaced by including modules
108
- * FIXED bug in reading id3v2 tags tagged with olds versions of "mp3ext" ( http://www.mutschler.de/mp3ext/ )
109
- * FIXED bug on calculating id3v2 frame size
110
- * FIXED bug when multiple TLEN tags
111
- * FIXED bug when converting text tag from Unicode
112
- * FIXED bug: file was not closed, causing too many opened files and test failure on win32
113
-
114
- === 0.4 / 2005-04-26
115
-
116
- * fixes in vbr mode
117
- * removed extract_info_from_head() function
118
- * now try several times to find a good header frame before giving up
119
- * correct handling of unicode in v2 tags. Require standard "iconv" library if such tags are used
120
- * FIXED if a tag appears more than one time, create an array with every value found for this tag
121
-
122
- === 0.3 / 2004-05-04
123
-
124
- * massive changes of most of the code to make it easier to read & hopefully run faster
125
- * ID2TAGS hash is just informative now, no use of it in the code. id3v2 tag fields are read in directly
126
- * added support for id3 v2.2 and v2.4 (0.2.1 only supported v2.3)
127
- * much improved vbr duration guessing
128
- * made Mp3Info#to_s output to be prettier
129
- * moved hastag1? and hastag2? to be class booleans instead of functions (now named hastag1 and hastag2)
130
- * fixed a bug on computing "error_protection" attribute
131
- * new attribute "tag", which is a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
132
- * new method hastag?, which test the presence of any tag
133
-
134
- === 0.2.1 / 2003-09-04
135
-
136
- * filename attribute added
137
- * mp3 files are opened read-only now [Alan Davies <alan__DOT_davies__AT__thomson.com>]
138
- * Mp3Info#initialize: bugfixes [Alan Davies <alan__DOT_davies__AT__thomson.com>]
139
- * put NULLs in year field in id3v1 tags instead of zeros [Alan Davies <alan__DOT_davies__AT__thomson.com>]
140
- * Mp3Info#gettag1: remove null at end of strings [Alan Davies <alan__DOT_davies__AT__thomson.com>]
141
- * Mp3Info#extract_infos_from_head(): some brackets missed [Alan Davies <alan__DOT_davies__AT__thomson.com>]
142
-
143
- === 0.2 / 2003-08-18
144
-
145
- * writing, reading and removing of id3v1 tags
146
- * reading of id3v2 tags
147
- * test suite improved
148
- * to_s method added
149
- * length attribute is a Float now
150
-
151
- === 0.1 / 2003-03-17
152
-
153
- * Initial version
@@ -1,68 +0,0 @@
1
- = mp3info
2
-
3
- * http://github.com/toy/mp3info
4
-
5
- == DESCRIPTION:
6
-
7
- mp3info read low-level informations and manipulate tags on mp3 files.
8
-
9
- == FEATURES/PROBLEMS:
10
-
11
- * written in pure ruby
12
- * read low-level informations like bitrate, length, samplerate, etc...
13
- * read, write, remove id3v1 and id3v2 tags
14
- * correctly read VBR files (with or without Xing header)
15
- * only 2.3 version is supported for writings id3v2 tags
16
-
17
- == SYNOPSIS:
18
-
19
- require "mp3info"
20
- # read and display infos & tags
21
- Mp3Info.open("myfile.mp3") do |mp3info|
22
- puts mp3info
23
- end
24
-
25
- # read/write tag1 and tag2 with Mp3Info#tag attribute
26
- # when reading tag2 have priority over tag1
27
- # when writing, each tag is written.
28
- Mp3Info.open("myfile.mp3") do |mp3|
29
- puts mp3.tag.title
30
- puts mp3.tag.artist
31
- puts mp3.tag.album
32
- puts mp3.tag.tracknum
33
- mp3.tag.title = "track title"
34
- mp3.tag.artist = "artist name"
35
- end
36
-
37
- Mp3Info.open("myfile.mp3") do |mp3|
38
- # you can access four letter v2 tags like this
39
- puts mp3.tag2.TIT2
40
- mp3.tag2.TIT2 = "new TIT2"
41
- # or like that
42
- mp3.tag2["TIT2"]
43
- # at this time, only COMM tag is processed after reading and before writing
44
- # according to ID3v2#options hash
45
- mp3.tag2.options[:lang] = "FRE"
46
- mp3.tag2.COMM = "my comment in french, correctly handled when reading and writing"
47
- end
48
-
49
- # tags v2 will be read and written according to the :encoding settings
50
- mp3 = Mp3Info.open("myfile.mp3", :encoding => 'utf-8')
51
-
52
- == REQUIREMENTS:
53
-
54
- * iconv
55
-
56
- == INSTALL:
57
-
58
- * gem install mp3info
59
-
60
- == LICENSE:
61
-
62
- ruby
63
-
64
- == TODO:
65
-
66
- * encoder detection
67
- * support for more tags in id3v2
68
- * generalize id3v2 with other audio formats (APE, MPC, OGG, etc...)
@@ -1,714 +0,0 @@
1
- # coding:utf-8
2
- # License:: Ruby
3
- # Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
4
- # Website:: http://ruby-mp3info.rubyforge.org/
5
-
6
- require 'stringio'
7
- require "fileutils"
8
- require "mp3info/extension_modules"
9
- require "mp3info/id3v2"
10
-
11
- # ruby -d to display debugging infos
12
-
13
- # Raised on any kind of error related to ruby-mp3info
14
- class Mp3InfoError < StandardError ; end
15
-
16
- class Mp3InfoInternalError < StandardError #:nodoc:
17
- end
18
-
19
- class Mp3Info
20
-
21
- VERSION = "0.6.16"
22
-
23
- LAYER = [ nil, 3, 2, 1]
24
- BITRATE = {
25
- 1 =>
26
- [
27
- [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
28
- [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
29
- [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] ],
30
- 2 =>
31
- [
32
- [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
33
- [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
34
- [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
35
- ],
36
- 2.5 =>
37
- [
38
- [32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
39
- [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
40
- [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
41
- ]
42
- }
43
- SAMPLERATE = {
44
- 1 => [ 44100, 48000, 32000 ],
45
- 2 => [ 22050, 24000, 16000 ],
46
- 2.5 => [ 11025, 12000, 8000 ]
47
- }
48
- CHANNEL_MODE = [ "Stereo", "JStereo", "Dual Channel", "Single Channel"]
49
-
50
- GENRES = [
51
- "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
52
- "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
53
- "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
54
- "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
55
- "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
56
- "Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
57
- "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
58
- "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
59
- "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
60
- "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
61
- "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
62
- "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
63
- "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
64
- "Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
65
- "Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
66
- "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
67
- "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
68
- "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
69
- "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
70
- "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
71
- "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
72
- "Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
73
- "Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
74
- "Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
75
- "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
76
- "SynthPop" ]
77
-
78
- TAG1_SIZE = 128
79
- #MAX_FRAME_COUNT = 6 #number of frame to read for encoder detection
80
-
81
- # map to fill the "universal" tag (#tag attribute)
82
- # for id3v2.2
83
- TAG_MAPPING_2_2 = {
84
- "title" => "TT2",
85
- "artist" => "TP1",
86
- "album" => "TAL",
87
- "year" => "TYE",
88
- "tracknum" => "TRK",
89
- "comments" => "COM",
90
- "genre_s" => "TCO"
91
- }
92
-
93
- # for id3v2.3 and 2.4
94
- TAG_MAPPING_2_3 = {
95
- "title" => "TIT2",
96
- "artist" => "TPE1",
97
- "album" => "TALB",
98
- "year" => "TYER",
99
- "tracknum" => "TRCK",
100
- "comments" => "COMM",
101
- "genre_s" => "TCON"
102
- }
103
-
104
- # http://www.codeproject.com/audio/MPEGAudioInfo.asp
105
- SAMPLES_PER_FRAME = [
106
- nil,
107
- {1=>384, 2=>384, 2.5=>384}, # Layer I
108
- {1=>1152, 2=>1152, 2.5=>1152}, # Layer II
109
- {1=>1152, 2=>576, 2.5=>576} # Layer III
110
- ]
111
-
112
- # mpeg version = 1 or 2
113
- attr_reader(:mpeg_version)
114
-
115
- # layer = 1, 2, or 3
116
- attr_reader(:layer)
117
-
118
- # bitrate in kbps
119
- attr_reader(:bitrate)
120
-
121
- # samplerate in Hz
122
- attr_reader(:samplerate)
123
-
124
- # channel mode => "Stereo", "JStereo", "Dual Channel" or "Single Channel"
125
- attr_reader(:channel_mode)
126
-
127
- # variable bitrate => true or false
128
- attr_reader(:vbr)
129
-
130
- # Hash representing values in the MP3 frame header. Keys are one of the following:
131
- # - :private (boolean)
132
- # - :copyright (boolean)
133
- # - :original (boolean)
134
- # - :padding (boolean)
135
- # - :error_protection (boolean)
136
- # - :mode_extension (integer in the 0..3 range)
137
- # - :emphasis (integer in the 0..3 range)
138
- # detailled explanation can be found here: http://www.mp3-tech.org/programmer/frame_header.html
139
- attr_reader(:header)
140
-
141
- # length in seconds as a Float
142
- attr_reader(:length)
143
-
144
- # error protection => true or false
145
- attr_reader(:error_protection)
146
-
147
- #a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
148
- #this tag has priority over @tag1 and @tag2 when writing the tag with #close
149
- attr_reader(:tag)
150
-
151
- # id3v1 tag as a Hash. You can modify it, it will be written when calling
152
- # "close" method.
153
- attr_accessor(:tag1)
154
-
155
- # id3v2 tag attribute as an ID3v2 object. You can modify it, it will be written when calling
156
- # "close" method.
157
- attr_accessor(:tag2)
158
-
159
- # the original filename unless used with a StringIO
160
- attr_reader(:filename)
161
-
162
- # Test the presence of an id3v1 tag in file or StringIO +filename_or_io+
163
- def self.hastag1?(filename_or_io)
164
- if filename_or_io.is_a?(StringIO)
165
- io = filename_or_io
166
- io.rewind
167
- else
168
- io = File.new(filename_or_io, "rb")
169
- end
170
-
171
- hastag1 = false
172
- begin
173
- io.seek(-TAG1_SIZE, File::SEEK_END)
174
- hastag1 = io.read(3) == "TAG"
175
- ensure
176
- io.close if io.is_a?(File)
177
- end
178
- hastag1
179
- end
180
-
181
- # Test the presence of an id3v2 tag in file or StringIO +filename_or_io+
182
- def self.hastag2?(filename_or_io)
183
- if filename_or_io.is_a?(StringIO)
184
- io = filename_or_io
185
- io.rewind
186
- else
187
- io = File.new(filename_or_io,"rb")
188
- end
189
-
190
- hastag2 = false
191
-
192
- begin
193
- hastag2 = io.read(3) == "ID3"
194
- ensure
195
- io.close if io.is_a?(File)
196
- end
197
- hastag2
198
- end
199
-
200
- # Remove id3v1 tag from +filename+
201
- def self.removetag1(filename)
202
- if self.hastag1?(filename)
203
- newsize = File.size(filename) - TAG1_SIZE
204
- File.open(filename, "rb+") { |f| f.truncate(newsize) }
205
- end
206
- end
207
-
208
- # Remove id3v2 tag from +filename+
209
- def self.removetag2(filename)
210
- self.open(filename) do |mp3|
211
- mp3.tag2.clear
212
- end
213
- end
214
-
215
- # Instantiate Mp3Info object with name +filename+.
216
- # options hash is used for ID3v2#new.
217
- # Specify :parse_tags => false to disable the processing
218
- # of the tags (read and write).
219
- # Specify :parse_mp3 => false to disable processing of the mp3
220
- def initialize(filename_or_io, options = {})
221
- warn("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
222
- @filename_or_io = filename_or_io
223
- options = {:parse_mp3 => true, :parse_tags => true}.update(options)
224
- @tag_parsing_enabled = options.delete(:parse_tags)
225
- @mp3_parsing_enabled = options.delete(:parse_mp3)
226
- @id3v2_options = options
227
- reload
228
- end
229
-
230
- # reload (or load for the first time) the file from disk
231
- def reload
232
- @header = {}
233
-
234
- if @filename_or_io.is_a?(StringIO)
235
- @io_is_a_file = false
236
- @io = @filename_or_io
237
- @io_size = @io.size
238
- @filename = nil
239
- else
240
- @io_is_a_file = true
241
- @io = File.new(@filename_or_io, "rb")
242
- @io_size = @io.stat.size
243
- @filename = @filename_or_io
244
- end
245
-
246
- if @io_size == 0
247
- raise(Mp3InfoError, "empty file or IO")
248
- end
249
-
250
-
251
- @io.extend(Mp3FileMethods)
252
- @tag1 = @tag = @tag1_orig = @tag_orig = {}
253
- @tag1.extend(HashKeys)
254
- @tag2 = ID3v2.new(@id3v2_options)
255
-
256
- begin
257
- if @tag_parsing_enabled
258
- parse_tags
259
- @tag1_orig = @tag1.dup
260
-
261
- if hastag1?
262
- @tag = @tag1.dup
263
- end
264
-
265
- if hastag2?
266
- @tag = {}
267
- # creation of a sort of "universal" tag, regardless of the tag version
268
- tag2_mapping = @tag2.version =~ /^2\.2/ ? TAG_MAPPING_2_2 : TAG_MAPPING_2_3
269
- tag2_mapping.each do |key, tag2_name|
270
- tag_value = (@tag2[tag2_name].is_a?(Array) ? @tag2[tag2_name].first : @tag2[tag2_name])
271
- next unless tag_value
272
- @tag[key] = tag_value.is_a?(Array) ? tag_value.first : tag_value
273
-
274
- if %w{year tracknum}.include?(key)
275
- @tag[key] = tag_value.to_i
276
- end
277
- # this is a special case with id3v2.2, which uses
278
- # old fashionned id3v1 genres
279
- if tag2_name == "TCO" && tag_value =~ /^\((\d+)\)$/
280
- @tag["genre_s"] = GENRES[$1.to_i]
281
- end
282
- end
283
- end
284
-
285
- @tag.extend(HashKeys)
286
- @tag_orig = @tag.dup
287
- end
288
-
289
- if @mp3_parsing_enabled
290
- parse_mp3
291
- end
292
-
293
- ensure
294
- if @io_is_a_file
295
- @io.close
296
- end
297
- end
298
- end
299
-
300
- # "block version" of Mp3Info::new()
301
- def self.open(*params)
302
- m = self.new(*params)
303
- ret = nil
304
- if block_given?
305
- begin
306
- ret = yield(m)
307
- ensure
308
- m.close
309
- end
310
- else
311
- ret = m
312
- end
313
- ret
314
- end
315
-
316
- # Remove id3v1 from mp3
317
- def removetag1
318
- @tag1.clear
319
- self
320
- end
321
-
322
- # Remove id3v2 from mp3
323
- def removetag2
324
- @tag2.clear
325
- self
326
- end
327
-
328
- # Does the file has an id3v1 or v2 tag?
329
- def hastag?
330
- hastag1? || hastag2?
331
- end
332
-
333
- # Does the file has an id3v1 tag?
334
- def hastag1?
335
- !@tag1.empty?
336
- end
337
-
338
- # Does the file has an id3v2 tag?
339
- def hastag2?
340
- @tag2.parsed?
341
- end
342
-
343
- # write to another filename at close()
344
- def rename(new_filename)
345
- raise(Mp3InfoError, "cannot rename an IO") unless @io_is_a_file
346
- @filename = new_filename
347
- end
348
-
349
- # this method returns the "audio-only" data boundaries of the file,
350
- # i.e. content stripped form tags. Useful to compare 2 files with the same
351
- # audio content but with differents tags. Returned value is an array
352
- # [position_in_the_file, length_of_the_data]
353
- def audio_content
354
- pos = 0
355
- length = @io_size
356
- if hastag1?
357
- length -= TAG1_SIZE
358
- end
359
- if hastag2?
360
- pos = @tag2.io_position
361
- length -= @tag2.io_position
362
- end
363
- [pos, length]
364
- end
365
-
366
- # return the length in seconds of one frame
367
- def frame_length
368
- SAMPLES_PER_FRAME[@layer][@mpeg_version] / Float(@samplerate)
369
- end
370
-
371
- # Flush pending modifications to tags and close the file
372
- # not used when source IO is a StringIO
373
- def close
374
- puts "close" if $DEBUG
375
- return unless @io_is_a_file
376
- if !@tag_parsing_enabled
377
- return
378
- end
379
- if @tag != @tag_orig
380
- puts "@tag has changed" if $DEBUG
381
-
382
- # @tag1 has precedence over @tag
383
- if @tag1 == @tag1_orig
384
- @tag.each do |k, v|
385
- @tag1[k] = v
386
- end
387
- end
388
-
389
- # ruby-mp3info can only write v2.3 tags
390
- TAG_MAPPING_2_3.each do |key, tag2_name|
391
- @tag2.delete(TAG_MAPPING_2_2[key])
392
- @tag2[tag2_name] = @tag[key] if @tag[key]
393
- end
394
- end
395
-
396
- if @tag1 != @tag1_orig
397
- puts "@tag1 has changed" if $DEBUG
398
- raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename_or_io)
399
- #@tag1_orig.update(@tag1)
400
- @tag1_orig = @tag1.dup
401
- File.open(@filename_or_io, 'rb+') do |file|
402
- if @tag1_orig.empty?
403
- newsize = @io_size - TAG1_SIZE
404
- file.truncate(newsize)
405
- else
406
- file.seek(-TAG1_SIZE, File::SEEK_END)
407
- t = file.read(3)
408
- if t != 'TAG'
409
- #append new tag
410
- file.seek(0, File::SEEK_END)
411
- file.write('TAG')
412
- end
413
- str = [
414
- @tag1_orig["title"]||"",
415
- @tag1_orig["artist"]||"",
416
- @tag1_orig["album"]||"",
417
- ((@tag1_orig["year"] != 0) ? ("%04d" % @tag1_orig["year"].to_i) : "\0\0\0\0"),
418
- @tag1_orig["comments"]||"",
419
- 0,
420
- @tag1_orig["tracknum"]||0,
421
- @tag1_orig["genre"]||255
422
- ].pack("Z30Z30Z30Z4Z28CCC")
423
- file.write(str)
424
- end
425
- end
426
- end
427
-
428
- if @tag2.changed?
429
- puts "@tag2 has changed" if $DEBUG
430
- raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename_or_io)
431
- tempfile_name = nil
432
- File.open(@filename_or_io, 'rb+') do |file|
433
- #if tag2 already exists, seek to end of it
434
- if @tag2.parsed?
435
- file.seek(@tag2.io_position)
436
- end
437
- # if @io.read(3) == "ID3"
438
- # version_maj, version_min, flags = @io.read(3).unpack("CCB4")
439
- # unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
440
- # tag2_len = @io.get_syncsafe
441
- # @io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
442
- # @io.seek(tag2_len, IO::SEEK_CUR)
443
- # end
444
- tempfile_name = @filename_or_io + ".tmp"
445
- File.open(tempfile_name, "wb") do |tempfile|
446
- unless @tag2.empty?
447
- tempfile.write(@tag2.to_bin)
448
- end
449
-
450
- bufsiz = file.stat.blksize || 4096
451
- while buf = file.read(bufsiz)
452
- tempfile.write(buf)
453
- end
454
- end
455
- end
456
- File.rename(tempfile_name, @filename_or_io)
457
- end
458
- end
459
-
460
- # close and reopen the file, i.e. commit changes to disk and
461
- # reload it (only works with "true" files, not StringIO ones)
462
- def flush
463
- return unless @io_is_a_file
464
- close
465
- reload
466
- end
467
-
468
- # inspect inside Mp3Info
469
- def to_s
470
- s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. header #{@header.inspect} "
471
- s << "tag1: "+@tag1.to_hash.inspect+"\n" if hastag1?
472
- s << "tag2: "+@tag2.to_hash.inspect+"\n" if hastag2?
473
- s
474
- end
475
-
476
- # iterates over each mpeg frame over the file, allowing you to
477
- # write some funny things, like an mpeg lossless cutter, or frame
478
- # counter, or whatever you like ;) +frame+ is a hash with the following keys:
479
- # :layer, :bitrate, :samplerate, :mpeg_version, :padding and :size (in bytes)
480
- def each_frame
481
- @io.seek(@first_frame_pos, File::SEEK_SET)
482
- loop do
483
- head = @io.read(4).unpack("N").first
484
- frame = Mp3Info.get_frames_infos(head)
485
- @io.seek(frame[:size] -4, File::SEEK_CUR)
486
- yield frame
487
- #puts "frame #{frame_count} len #{frame[:length]} br #{frame[:bitrate]} @io.pos #{@io.pos}"
488
- break if @io.eof?
489
- end
490
- end
491
-
492
- private
493
-
494
- def Mp3Info.get_frames_infos(head)
495
- # be sure we are in sync
496
- if ((head & 0xffe00000) != 0xffe00000) || # 11 bit MPEG frame sync
497
- ((head & 0x00060000) == 0x00060000) || # 2 bit layer type
498
- ((head & 0x0000f000) == 0x0000f000) || # 4 bit bitrate
499
- ((head & 0x0000f000) == 0x00000000) || # free format bitstream
500
- ((head & 0x00000c00) == 0x00000c00) || # 2 bit frequency
501
- ((head & 0xffff0000) == 0xfffe0000)
502
- raise Mp3InfoInternalError
503
- end
504
- mpeg_version = [2.5, nil, 2, 1][bits(head, 20,19)]
505
-
506
- layer = LAYER[bits(head, 18,17)]
507
- raise Mp3InfoInternalError if layer == nil || mpeg_version == nil
508
-
509
- bitrate = BITRATE[mpeg_version][layer-1][bits(head, 15,12)-1]
510
- samplerate = SAMPLERATE[mpeg_version][bits(head, 11,10)]
511
- padding = (head[9] == 1)
512
- if layer == 1
513
- size = (12 * bitrate*1000.0 / samplerate + (padding ? 1 : 0))*4
514
- else # layer 2 and 3
515
- size = 144 * (bitrate*1000.0 / samplerate) + (padding ? 1 : 0)
516
- end
517
- size = size.to_i
518
- { :layer => layer,
519
- :bitrate => bitrate,
520
- :samplerate => samplerate,
521
- :mpeg_version => mpeg_version,
522
- :padding => padding,
523
- :size => size }
524
- end
525
-
526
- ### parses the id3 tags of the currently open @io
527
- def parse_tags
528
- return if @io_size < TAG1_SIZE # file is too small
529
-
530
- @tag1_parsed = false
531
- @io.seek(0)
532
- f3 = @io.read(3)
533
- # v1 tag at beginning
534
- if f3 == "TAG"
535
- gettag1
536
- @tag1_parsed = true
537
- end
538
-
539
- @tag2.from_io(@io) if f3 == "ID3" # v2 tag at beginning
540
-
541
- unless @tag1_parsed # v1 tag at end
542
- # this preserves the file pos if tag2 found, since gettag2 leaves
543
- # the file at the best guess as to the first MPEG frame
544
- pos = (@tag2.io_position || 0)
545
- # seek to where id3v1 tag should be
546
- @io.seek(-TAG1_SIZE, IO::SEEK_END)
547
- if @io.read(3) == "TAG"
548
- gettag1
549
- end
550
- @io.seek(pos)
551
- end
552
- end
553
-
554
- ### gets id3v1 tag information from @io
555
- ### assumes @io is pointing to char after "TAG" id
556
- def gettag1
557
- @tag1_parsed = true
558
- @tag1["title"] = @io.read(30).unpack("A*").first
559
- @tag1["artist"] = @io.read(30).unpack("A*").first
560
- @tag1["album"] = @io.read(30).unpack("A*").first
561
- year_t = @io.read(4).to_i
562
- @tag1["year"] = year_t unless year_t == 0
563
- comments = @io.read(30)
564
- if comments.getbyte(-2) == 0
565
- @tag1["tracknum"] = comments.getbyte(-1).to_i
566
- comments.chop! #remove the last char
567
- end
568
- @tag1["comments"] = comments.unpack("A*").first
569
- @tag1["genre"] = @io.getbyte
570
- @tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
571
-
572
- # clear empty tags
573
- @tag1.delete_if { |k, v| v.respond_to?(:empty?) && v.empty? }
574
- @tag1.delete("genre") if @tag1["genre"] == 255
575
- @tag1.delete("tracknum") if @tag1["tracknum"] == 0
576
- end
577
-
578
- ### reads through @io from current pos until it finds a valid MPEG header
579
- ### returns the MPEG header as FixNum
580
- def find_next_frame
581
- # @io will now be sitting at the best guess for where the MPEG frame is.
582
- # It should be at byte 0 when there's no id3v2 tag.
583
- # It should be at the end of the id3v2 tag or the zero padding if there
584
- # is a id3v2 tag.
585
- #dummyproof = @io.stat.size - @io.pos => WAS TOO MUCH
586
-
587
- dummyproof = [ @io_size - @io.pos, 2000000 ].min
588
- dummyproof.times do |i|
589
- if @io.getbyte == 0xff
590
- data = @io.read(3)
591
- raise(Mp3InfoError, "end of file reached") if @io.eof?
592
- head = 0xff000000 + (data.getbyte(0) << 16) + (data.getbyte(1) << 8) + data.getbyte(2)
593
- begin
594
- Mp3Info.get_frames_infos(head)
595
- return head
596
- rescue Mp3InfoInternalError
597
- @io.seek(-3, IO::SEEK_CUR)
598
- end
599
- end
600
- end
601
- if @io.eof?
602
- raise Mp3InfoError, "cannot find a valid frame: got EOF"
603
- else
604
- raise Mp3InfoError, "cannot find a valid frame after reading #{dummyproof} bytes"
605
- end
606
- end
607
-
608
- def frame_scan(frame_limit = nil)
609
- frame_count = bitrate_sum = 0
610
- each_frame do |frame|
611
- bitrate_sum += frame[:bitrate]
612
- frame_count += 1
613
- break if frame_limit && (frame_count >= frame_limit)
614
- end
615
-
616
- average_bitrate = bitrate_sum/frame_count.to_f
617
- length = (frame_count-1) * frame_length
618
- [average_bitrate, length]
619
- end
620
-
621
- def parse_mp3
622
- ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
623
- ### head (fixnum) = valid 4 byte MPEG header
624
-
625
- found = false
626
-
627
- head = nil
628
- 5.times do
629
- head = find_next_frame()
630
- @first_frame_pos = @io.pos - 4
631
- current_frame = Mp3Info.get_frames_infos(head)
632
- @mpeg_version = current_frame[:mpeg_version]
633
- @layer = current_frame[:layer]
634
- @header[:error_protection] = head[16] == 0 ? true : false
635
- @bitrate = current_frame[:bitrate]
636
- @samplerate = current_frame[:samplerate]
637
- @header[:padding] = current_frame[:padding]
638
- @header[:private] = head[8] == 0 ? true : false
639
- @channel_mode = CHANNEL_MODE[@channel_num = Mp3Info.bits(head, 7,6)]
640
- @header[:mode_extension] = Mp3Info.bits(head, 5,4)
641
- @header[:copyright] = (head[3] == 1 ? true : false)
642
- @header[:original] = (head[2] == 1 ? true : false)
643
- @header[:emphasis] = Mp3Info.bits(head, 1,0)
644
- @vbr = false
645
- found = true
646
- break
647
- end
648
-
649
- raise(Mp3InfoError, "Cannot find good frame") unless found
650
-
651
- seek = @mpeg_version == 1 ?
652
- (@channel_num == 3 ? 17 : 32) :
653
- (@channel_num == 3 ? 9 : 17)
654
-
655
- @io.seek(seek, IO::SEEK_CUR)
656
-
657
- vbr_head = @io.read(4)
658
- if vbr_head == "Xing"
659
- puts "Xing header (VBR) detected" if $DEBUG
660
- flags = @io.get32bits
661
- stream_size = frame_count = 0
662
- flags[1] == 1 and frame_count = @io.get32bits
663
- flags[2] == 1 and stream_size = @io.get32bits
664
- puts "#{frame_count} frames" if $DEBUG
665
- raise(Mp3InfoError, "bad VBR header") if frame_count.zero?
666
- # currently this just skips the TOC entries if they're found
667
- @io.seek(100, IO::SEEK_CUR) if flags[0] == 1
668
- #@vbr_quality = @io.get32bits if flags[3] == 1
669
-
670
- samples_per_frame = SAMPLES_PER_FRAME[@layer][@mpeg_version]
671
- @length = frame_count * samples_per_frame / Float(@samplerate)
672
-
673
- @bitrate = (((stream_size/frame_count)*@samplerate)/144) / 1024
674
- @vbr = true
675
- else
676
- # for cbr, calculate duration with the given bitrate
677
-
678
- stream_size = @io_size - (hastag1? ? TAG1_SIZE : 0) - (@tag2.io_position || 0)
679
- @length = ((stream_size << 3)/1000.0)/@bitrate
680
- # read the first 100 frames and decide if the mp3 is vbr and needs full scan
681
- begin
682
- bitrate, length = frame_scan(100)
683
- if @bitrate != bitrate
684
- @vbr = true
685
- @bitrate, @length = frame_scan
686
- end
687
- rescue Mp3InfoInternalError
688
- end
689
- end
690
- end
691
-
692
- ### returns the selected bit range (b, a) as a number
693
- ### NOTE: b > a if not, returns 0
694
- def self.bits(number, b, a)
695
- t = 0
696
- b.downto(a) { |i| t += t + number[i] }
697
- t
698
- end
699
- end
700
-
701
- if $0 == __FILE__
702
- while filename = ARGV.shift
703
- begin
704
- info = Mp3Info.new(filename)
705
- puts filename
706
- #puts "MPEG #{info.mpeg_version} Layer #{info.layer} #{info.vbr ? "VBR" : "CBR"} #{info.bitrate} Kbps \
707
- #{info.channel_mode} #{info.samplerate} Hz length #{info.length} sec."
708
- puts info
709
- rescue Mp3InfoError => e
710
- puts "#{filename}\nERROR: #{e}"
711
- end
712
- puts
713
- end
714
- end