mp3info 0.6.17
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.
- data/.gitignore +12 -0
- data/History.txt +153 -0
- data/README.rdoc +68 -0
- data/lib/mp3info.rb +714 -0
- data/lib/mp3info/extension_modules.rb +46 -0
- data/lib/mp3info/id3v2.rb +437 -0
- data/mp3info.gemspec +17 -0
- data/test/fixtures.yml +86 -0
- data/test/test_ruby-mp3info.rb +572 -0
- metadata +76 -0
data/.gitignore
ADDED
data/History.txt
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
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
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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...)
|
data/lib/mp3info.rb
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
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
|