vjt-ruby-audioinfo 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ === 0.1.5 / 2009-03-29
2
+
3
+ * flac parsing more robust
4
+ * musicbrainz_infos support for flac
5
+ * quick fixes in mpcinfo for id3v2 tag parsing
6
+
7
+ === 0.1.4 / 2008-07-04
8
+
9
+ * charset correctly set when commiting MP3 tags too
10
+ * file is written only if tags have really changed
11
+
12
+ === 0.1.3 / 2008-07-03
13
+
14
+ * #tracknum= added
15
+ * charset correctly set when commiting Ogg tags
16
+
17
+ === 0.1.2 / 2008-04-25
18
+
19
+ * fix on parsing of MusicBrainz tags of .wma files
20
+
21
+ === 0.1.1 / 2008-04-17
22
+
23
+ * updated gem dependency on MP4Info >= 0.3.2, which fixes utf-8 handling
24
+ * added lib/shell_escape.rb that lacks from previous version
25
+
26
+ === 0.1 / 2008-03-28
27
+
28
+ * first release
@@ -0,0 +1,44 @@
1
+ = ruby-audioinfo
2
+
3
+ by Guillaume Pierronnet
4
+ * http://ruby-audioinfo.rubyforge.org
5
+ * http://rubyforge.org/projects/ruby-audioinfo/
6
+
7
+ == DESCRIPTION:
8
+
9
+ ruby-audioinfo glue together various audio ruby libraries and presents a unified
10
+ API to the developper. Currently, supported formats are: mp3, ogg, mpc, ape,
11
+ wma, flac, aac, mp4, m4a.
12
+
13
+ == FEATURES/PROBLEMS:
14
+
15
+ * beta write support for mp3 and ogg tags (other to be written)
16
+ * unified support for tag text-encoding. AudioInfo.new("file", "utf-8") and you're done!
17
+ * support for MusicBrainz tags
18
+ * AudioInfo::Album class included, which gives an unified way to manage an album in a given directory.
19
+
20
+ == SYNOPSIS:
21
+
22
+ AudioInfo.open("audio_file.one_of_supported_extensions") do |info|
23
+ info.artist # or info["artist"]
24
+ info.title # or info["title"]
25
+ info.length # playing time of the file
26
+ info.bitrate # average bitrate
27
+ info.to_h # { "artist" => "artist", "title" => "title", etc... }
28
+ end
29
+
30
+ == REQUIREMENTS:
31
+
32
+ * ruby-mp3info[http://ruby-mp3info.rubyforge.org/]
33
+ * ruby-ogginfo[http://ruby-ogginfo.rubyforge.org/]
34
+ * MP4Info[http://mp4info.rubyforge.org/]
35
+ * flacinfo-rb[http://rubyforge.org/projects/flacinfo-rb/]
36
+ * wmainfo-rb[http://rubyforge.org/projects/wmainfo/]
37
+
38
+ == INSTALL:
39
+
40
+ * sudo gem install ruby-audioinfo
41
+
42
+ == LICENSE:
43
+
44
+ Ruby
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'lib', 'audioinfo')
3
+
4
+ begin
5
+ require 'jeweler'
6
+
7
+ Jeweler::Tasks.new do |gemspec|
8
+ gemspec.name = 'vjt-ruby-audioinfo'
9
+ gemspec.version = AudioInfo::VERSION
10
+ gemspec.authors = ['Guillaume Pierronnet', 'Marcello Barnaba']
11
+ gemspec.email = 'moumar@rubyforge.org'
12
+ gemspec.date = '2010-03-20'
13
+
14
+ gemspec.homepage = 'http://ruby-audioinfo.rubyforge.org'
15
+ gemspec.summary = 'Unified audio info access library'
16
+ gemspec.description = 'ruby-audioinfo glues together various audio libraries and presents a single API to the developer.'
17
+ 'Currently, supported formats are: mp3, ogg, mpc, ape, wma, flac, aac, mp4, m4a.'
18
+
19
+ gemspec.files = %w( README.txt Rakefile History.txt) + Dir['{lib,test}/**/*']
20
+ gemspec.extra_rdoc_files = %w( README.txt )
21
+ gemspec.has_rdoc = true
22
+ gemspec.require_path = 'lib'
23
+
24
+ gemspec.add_dependency 'ruby-mp3info', '>= 0.6.3'
25
+ gemspec.add_dependency 'ruby-ogginfo', '>= 0.3.1'
26
+ gemspec.add_dependency 'mp4info', '>= 1.7.3'
27
+ gemspec.add_dependency 'wmainfo-rb', '>= 0.5'
28
+ gemspec.add_dependency 'flacinfo-rb', '>= 0.4'
29
+ end
30
+ rescue LoadError
31
+ puts 'Jeweler not available. Install it with: gem install jeweler'
32
+ end
33
+
34
+ #task :tag_svn do
35
+ # svn_repo = "svn+ssh://rubyforge.org/var/svn/ruby-audioinfo"
36
+ # sh "svn copy -m 'tagged version #{hoe.version}' #{svn_repo}/trunk #{svn_repo}/tags/REL-#{hoe.version}"
37
+ #end
@@ -0,0 +1,349 @@
1
+ require "iconv"
2
+ require "stringio"
3
+
4
+ require "mp3info"
5
+ require "ogginfo"
6
+ require "wmainfo"
7
+ require "mp4info"
8
+ require "flacinfo"
9
+
10
+ $: << File.expand_path(File.dirname(__FILE__))
11
+
12
+ require "audioinfo/mpcinfo"
13
+ require "audioinfo/apetag"
14
+
15
+ class AudioInfoError < Exception ; end
16
+
17
+ class AudioInfo
18
+ MUSICBRAINZ_FIELDS = {
19
+ "trmid" => "TRM Id",
20
+ "artistid" => "Artist Id",
21
+ "albumid" => "Album Id",
22
+ "albumtype" => "Album Type",
23
+ "albumstatus" => "Album Status",
24
+ "albumartistid" => "Album Artist Id",
25
+ "sortname" => "Sort Name",
26
+ "trackid" => "Track Id"
27
+ }
28
+
29
+ SUPPORTED_EXTENSIONS = %w{mp3 ogg mpc wma mp4 aac m4a flac}
30
+
31
+ VERSION = "0.1.6"
32
+
33
+ attr_reader :path, :extension, :musicbrainz_infos, :tracknum, :bitrate, :vbr
34
+ attr_reader :artist, :album, :title, :length, :date
35
+
36
+ # "block version" of #new()
37
+ def self.open(*args)
38
+ audio_info = self.new(*args)
39
+ ret = nil
40
+ if block_given?
41
+ begin
42
+ ret = yield(audio_info)
43
+ ensure
44
+ audio_info.close
45
+ end
46
+ else
47
+ ret = audio_info
48
+ end
49
+ ret
50
+ end
51
+
52
+ # open the file with path +fn+ and convert all tags from/to specified +encoding+
53
+ def initialize(filename, encoding = 'utf-8')
54
+ raise(AudioInfoError, "path is nil") if filename.nil?
55
+ @path = filename
56
+ ext = File.extname(@path)
57
+ raise(AudioInfoError, "cannot find extension") if ext.empty?
58
+ @extension = ext[1..-1].downcase
59
+ @musicbrainz_infos = {}
60
+ @encoding = encoding
61
+
62
+ begin
63
+ case @extension
64
+ when 'mp3'
65
+ @info = Mp3Info.new(filename, :encoding => @encoding)
66
+ default_tag_fill
67
+ #"TXXX"=>
68
+ #["MusicBrainz TRM Id\000",
69
+ #"MusicBrainz Artist Id\000aba64937-3334-4c65-90a1-4e6b9d4d7ada",
70
+ #"MusicBrainz Album Id\000e1a223c1-cbc2-427f-a192-5d22fefd7c4c",
71
+ #"MusicBrainz Album Type\000album",
72
+ #"MusicBrainz Album Status\000official",
73
+ #"MusicBrainz Album Artist Id\000"]
74
+
75
+ if (arr = @info.tag2["TXXX"]).is_a?(Array)
76
+ fields = MUSICBRAINZ_FIELDS.invert
77
+ arr.each do |val|
78
+ if val =~ /^MusicBrainz (.+)\000(.*)$/
79
+ short_name = fields[$1]
80
+ @musicbrainz_infos[short_name] = $2
81
+ end
82
+ end
83
+ end
84
+ @bitrate = @info.bitrate
85
+ i = @info.tag.tracknum
86
+ @tracknum = (i.is_a?(Array) ? i.last : i).to_i
87
+ @length = @info.length.to_i
88
+ @date = @info.tag["date"]
89
+ @vbr = @info.vbr
90
+ @info.close
91
+
92
+ when 'ogg'
93
+ @info = OggInfo.new(filename, @encoding)
94
+ default_fill_musicbrainz_fields
95
+ default_tag_fill
96
+ @bitrate = @info.bitrate/1000
97
+ @tracknum = @info.tag.tracknumber.to_i
98
+ @length = @info.length.to_i
99
+ @date = @info.tag["date"]
100
+ @vbr = true
101
+ @info.close
102
+
103
+ when 'mpc'
104
+ fill_ape_tag(filename)
105
+
106
+ mpc_info = MpcInfo.new(filename)
107
+ @bitrate = mpc_info.infos['bitrate']/1000
108
+ @length = mpc_info.infos['length']
109
+
110
+ when 'ape'
111
+ fill_ape_tag(filename)
112
+
113
+ when 'wma'
114
+ @info = WmaInfo.new(filename, :encoding => @encoding)
115
+ @artist = @info.tags["Author"]
116
+ @album = @info.tags["AlbumTitle"]
117
+ @title = @info.tags["Title"]
118
+ @tracknum = @info.tags["TrackNumber"].to_i
119
+ @date = @info.tags["Year"]
120
+ @bitrate = @info.info["bitrate"]
121
+ @length = @info.info["playtime_seconds"]
122
+ MUSICBRAINZ_FIELDS.each do |key, original_key|
123
+ @musicbrainz_infos[key] =
124
+ @info.info["MusicBrainz/" + original_key.tr(" ", "")] ||
125
+ @info.info["MusicBrainz/" + original_key]
126
+ end
127
+
128
+ when 'aac', 'mp4', 'm4a'
129
+ @info = MP4Info.open(filename)
130
+ @artist = @info.ART
131
+ @album = @info.ALB
132
+ @title = @info.NAM
133
+ @tracknum = ( t = @info.TRKN ) ? t.first : 0
134
+ @date = @info.DAY
135
+ @bitrate = @info.BITRATE
136
+ @length = @info.SECS
137
+ mapping = MUSICBRAINZ_FIELDS.invert
138
+
139
+ faad_info(filename).match(/^MusicBrainz (.+)$/) do
140
+ name, value = $1.split(/: /, 2)
141
+ key = mapping[name]
142
+ @musicbrainz_infos[key] = value
143
+ end
144
+
145
+ when 'flac'
146
+ @info = FlacInfo.new(filename)
147
+ tags = convert_tags_encoding(@info.tags, "UTF-8")
148
+ @artist = tags["ARTIST"] || tags["artist"]
149
+ @album = tags["ALBUM"] || tags["album"]
150
+ @title = tags["TITLE"] || tags["title"]
151
+ @tracknum = (tags["TRACKNUMBER"]||tags["tracknumber"]).to_i
152
+ @date = tags["DATE"]||tags["date"]
153
+ @length = @info.streaminfo["total_samples"] / @info.streaminfo["samplerate"].to_f
154
+ @bitrate = File.size(filename).to_f*8/@length/1024
155
+ tags.each do |tagname, tagvalue|
156
+ next unless tagname =~ /^musicbrainz_(.+)$/
157
+ @musicbrainz_infos[$1] = tags[tagname]
158
+ end
159
+ @musicbrainz_infos["trmid"] = tags["musicip_puid"]
160
+ #default_fill_musicbrainz_fields
161
+
162
+ else
163
+ raise(AudioInfoError, "unsupported extension '.#{@extension}'")
164
+ end
165
+
166
+ if @tracknum == 0
167
+ @tracknum = nil
168
+ end
169
+
170
+ @musicbrainz_infos.delete_if { |k, v| v.nil? }
171
+ @hash = { "artist" => @artist,
172
+ "album" => @album,
173
+ "title" => @title,
174
+ "tracknum" => @tracknum,
175
+ "date" => @date,
176
+ "length" => @length,
177
+ "bitrate" => @bitrate,
178
+ }
179
+
180
+ rescue Exception, Mp3InfoError, OggInfoError, ApeTagError => e
181
+ raise AudioInfoError, e.to_s, e.backtrace
182
+ end
183
+
184
+ @needs_commit = false
185
+
186
+ end
187
+
188
+ # set the title of the file
189
+ def title=(v)
190
+ if @title != v
191
+ @needs_commit = true
192
+ @title = v
193
+ end
194
+ end
195
+
196
+ # set the artist of the file
197
+ def artist=(v)
198
+ if @artist != v
199
+ @needs_commit = true
200
+ @artist = v
201
+ end
202
+ end
203
+
204
+ # set the album of the file
205
+ def album=(v)
206
+ if @album != v
207
+ @needs_commit = true
208
+ @album = v
209
+ end
210
+ end
211
+
212
+ # set the track number of the file
213
+ def tracknum=(v)
214
+ v = v.to_i
215
+ if @tracknum != v
216
+ @needs_commit = true
217
+ @tracknum = v
218
+ end
219
+ end
220
+
221
+ # hash-like access to tag
222
+ def [](key)
223
+ @hash[key]
224
+ end
225
+
226
+ # convert tags to hash
227
+ def to_h
228
+ @hash
229
+ end
230
+
231
+ # close the file and commits changes to disk
232
+ def close
233
+ if @needs_commit
234
+ case @info
235
+ when Mp3Info
236
+ Mp3Info.open(@path, :encoding => @encoding) do |info|
237
+ info.tag.artist = @artist
238
+ info.tag.title = @title
239
+ info.tag.album = @album
240
+ info.tag.tracknum = @tracknum
241
+ end
242
+ when OggInfo
243
+ OggInfo.open(@path, @encoding) do |ogg|
244
+ { "artist" => @artist,
245
+ "album" => @album,
246
+ "title" => @title,
247
+ "tracknumber" => @tracknum}.each do |k,v|
248
+ ogg.tag[k] = v.to_s
249
+ end
250
+ end
251
+
252
+ else
253
+ raise(AudioInfoError, "implement me")
254
+ end
255
+
256
+ end
257
+ @needs_commit
258
+ end
259
+ =begin
260
+ {"musicbrainz_albumstatus"=>"official",
261
+ "artist"=>"Jill Scott",
262
+ "replaygain_track_gain"=>"-3.29 dB",
263
+ "tracknumber"=>"1",
264
+ "title"=>"A long walk (A touch of Jazz Mix)..Jazzanova Love Beats...",
265
+ "musicbrainz_sortname"=>"Scott, Jill",
266
+ "musicbrainz_artistid"=>"b1fb6a18-1626-4011-80fb-eaf83dfebcb6",
267
+ "musicbrainz_albumid"=>"cb2ad8c7-4a02-4e46-ae9a-c7c2463c7235",
268
+ "replaygain_track_peak"=>"0.82040048",
269
+ "musicbrainz_albumtype"=>"compilation",
270
+ "album"=>"...Mixing (Jazzanova)",
271
+ "musicbrainz_trmid"=>"1ecec0a6-c7c3-4179-abea-ef12dabc7cbd",
272
+ "musicbrainz_trackid"=>"0a368e63-dddf-441f-849c-ca23f9cb2d49",
273
+ "musicbrainz_albumartistid"=>"89ad4ac3-39f7-470e-963a-56509c546377"}>
274
+ =end
275
+
276
+ # check if the file is correctly tagged by MusicBrainz
277
+ def mb_tagged?
278
+ ! @musicbrainz_infos.empty?
279
+ end
280
+
281
+ private
282
+
283
+ def sanitize(input)
284
+ s = input.is_a?(Array) ? input.first : input
285
+ s.gsub("\000", "")
286
+ end
287
+
288
+ def default_fill_musicbrainz_fields
289
+ MUSICBRAINZ_FIELDS.keys.each do |field|
290
+ val = @info.tag["musicbrainz_#{field}"]
291
+ @musicbrainz_infos[field] = val if val
292
+ end
293
+ end
294
+
295
+ def default_tag_fill(tag = @info.tag)
296
+ %w{artist album title}.each do |v|
297
+ instance_variable_set( "@#{v}".to_sym, sanitize(tag[v].to_s) )
298
+ end
299
+ end
300
+
301
+ def fill_ape_tag(filename)
302
+ begin
303
+ @info = ApeTag.new(filename)
304
+ tags = convert_tags_encoding(@info.tag, "UTF-8")
305
+ default_tag_fill(tags)
306
+ default_fill_musicbrainz_fields
307
+ @date = @info.tag["year"]
308
+ @tracknum = 0
309
+
310
+ if track = @info.tag['track']
311
+ @tracknum = @info.tag['track'].split("/").first.to_i
312
+ end
313
+ rescue ApeTagError
314
+ end
315
+ end
316
+
317
+ def convert_tags_encoding(tags_orig, from_encoding)
318
+ tags = {}
319
+ Iconv.open(@encoding, from_encoding) do |ic|
320
+ tags_orig.inject(tags) do |hash, (k, v)|
321
+ if v.is_a?(String)
322
+ hash[ic.iconv(k)] = ic.iconv(v)
323
+ end
324
+ hash
325
+ end
326
+ end
327
+ tags
328
+ end
329
+
330
+ def faad_info(file)
331
+ stdout, stdout_w = IO.pipe
332
+ stderr, stderr_w = IO.pipe
333
+
334
+ fork {
335
+ stdout.close; STDOUT.reopen(stdout_w)
336
+ stderr.close; STDERR.reopen(stderr_w)
337
+ exec 'faad', '-i', file
338
+ }
339
+
340
+ stdout_w.close; stderr_w.close
341
+ pid, status = Process.wait2
342
+
343
+ out, err = stdout.read.chomp, stderr.read.chomp
344
+ stdout.close; stderr.close
345
+
346
+ # Return the stderr because faad prints info on that fd...
347
+ status.exitstatus.zero? ? err : ''
348
+ end
349
+ end
@@ -0,0 +1,151 @@
1
+ require "audioinfo"
2
+
3
+ class AudioInfo::Album
4
+
5
+ IMAGE_EXTENSIONS = %w{jpg jpeg gif png}
6
+
7
+ # a regexp to match the "multicd" suffix of a "multicd" string
8
+ # example: "toto (disc 1)" will match ' (disc 1)'
9
+ MULTICD_REGEXP = /\s*(\(|\[)?\s*(disc|cd):?-?\s*(\d+).*(\)|\])?\s*$/i
10
+
11
+ attr_reader :files, :files_on_error, :discnum, :multicd, :basename, :infos, :path
12
+
13
+ # return the list of images in the album directory, with "folder.*" in first
14
+ def self.images(path)
15
+ arr = Dir.glob( File.join(path, "*.{#{IMAGE_EXTENSIONS.join(",")}}"), File::FNM_CASEFOLD).collect do |f|
16
+ File.expand_path(f)
17
+ end
18
+ # move "folder.*" image on top of the array
19
+ if folder = arr.detect { |f| f =~ /folder\.[^.]+$/ }
20
+ arr.delete(folder)
21
+ arr.unshift(folder)
22
+ end
23
+ arr
24
+ end
25
+
26
+ # strip the "multicd" string from the given +name+
27
+ def self.basename(name)
28
+ name.sub(MULTICD_REGEXP, '')
29
+ end
30
+
31
+ # return the number of the disc in the box or 0
32
+ def self.discnum(name)
33
+ if name =~ MULTICD_REGEXP
34
+ $3.to_i
35
+ else
36
+ 0
37
+ end
38
+ end
39
+
40
+ # open the Album with +path+. +fast_lookup+ will only check
41
+ # first and last file of the directory
42
+ def initialize(path, fast_lookup = false)
43
+ @path = path
44
+ @multicd = false
45
+ @basename = @path
46
+ exts = AudioInfo::SUPPORTED_EXTENSIONS.join(",")
47
+
48
+ # need to escape the glob path
49
+ glob_escaped_path = @path.gsub(/([{}?*\[\]])/) { |s| '\\' << s }
50
+
51
+ file_names = Dir.glob( File.join(glob_escaped_path, "*.{#{exts}}") , File::FNM_CASEFOLD).sort
52
+
53
+ if fast_lookup
54
+ file_names = [file_names.first, file_names.last]
55
+ end
56
+
57
+ @files_on_error = []
58
+
59
+ @files = file_names.collect do |f|
60
+ begin
61
+ AudioInfo.new(f)
62
+ rescue AudioInfoError
63
+ @files_on_error << f
64
+ nil
65
+ end
66
+ end.compact
67
+
68
+ if @files_on_error.empty?
69
+ @files_on_error = nil
70
+ end
71
+
72
+ @infos = {}
73
+ @infos["album"] = @files.collect { |i| i.album }.uniq
74
+ @infos["album"] = @infos["album"].first if @infos["album"].size == 1
75
+ artists = @files.collect { |i| i.artist }.uniq
76
+ @infos["artist"] = artists.size > 1 ? "various" : artists.first
77
+ @discnum = self.class.discnum(@infos["album"])
78
+
79
+ if not @discnum.zero?
80
+ @multicd = true
81
+ @basename = self.class.basename(@infos["album"])
82
+ end
83
+ end
84
+
85
+ # is the album empty?
86
+ def empty?
87
+ @files.empty?
88
+ end
89
+
90
+ # are all the files of the album MusicBrainz tagged ?
91
+ def mb_tagged?
92
+ return false if @files.empty?
93
+ mb = true
94
+ @files.each do |f|
95
+ mb &&= f.mb_tagged?
96
+ end
97
+ mb
98
+ end
99
+
100
+ # return an array of images with "folder.*" in first
101
+ def images
102
+ self.class.images(@path)
103
+ end
104
+
105
+ # title of the album
106
+ def title
107
+ albums = @files.collect { |f| f.album }.uniq
108
+ #if albums.size > 1
109
+ # "#{albums.first} others candidates: '" + albums[1..-1].join("', '") + "'"
110
+ #else
111
+ albums.first
112
+ #end
113
+ end
114
+
115
+ # mbid (MusicBrainz ID) of the album
116
+ def mbid
117
+ return nil unless mb_tagged?
118
+ @files.collect { |f| f.musicbrainz_infos["albumid"] }.uniq.first
119
+ end
120
+
121
+ # is the album multi-artist?
122
+ def va?
123
+ @files.collect { |f| f.artist }.uniq.size > 1
124
+ end
125
+
126
+ # pretty print
127
+ def to_s
128
+ out = StringIO.new
129
+ out.puts(@path)
130
+ out.print "'#{title}'"
131
+
132
+ unless va?
133
+ out.print " by '#{@files.first.artist}' "
134
+ end
135
+
136
+ out.puts
137
+
138
+ @files.sort_by { |f| f.tracknum }.each do |f|
139
+ out.printf("%02d %s %3d %s", f.tracknum, f.extension, f.bitrate, f.title)
140
+ if va?
141
+ out.print(" "+f.artist)
142
+ end
143
+ out.puts
144
+ end
145
+
146
+ out.string
147
+ end
148
+
149
+ end
150
+
151
+
@@ -0,0 +1,53 @@
1
+ # see http://www.personal.uni-jena.de/~pfk/mpp/sv8/apetag.html for specs
2
+
3
+ class ApeTagError < StandardError ; end
4
+
5
+ class ApeTag
6
+ attr_reader :tag, :version
7
+
8
+ def initialize(filename)
9
+ @tag = {}
10
+
11
+ begin
12
+ @file = File.new(filename, "rb")
13
+ @file.seek(-32, IO::SEEK_END)
14
+
15
+ preamble, version, tagsize, itemcount, flags =
16
+ @file.read(24).unpack("A8VVVV")
17
+ @version = version/1000
18
+
19
+ raise(ApeTagError, "cannot find preamble") if preamble != 'APETAGEX'
20
+ @file.seek(-tagsize, IO::SEEK_END)
21
+ itemcount.times do |i|
22
+ len, flags = @file.read(8).unpack("VV")
23
+ key = ""
24
+ loop do
25
+ c = @file.getc
26
+ break if c == 0
27
+ key << c
28
+ end
29
+ #ugly FIX
30
+ @tag[key.downcase] = @file.read(len) unless len > 100_000
31
+ end
32
+ ensure
33
+ @file.close
34
+ end
35
+ end
36
+ end
37
+
38
+ if $0 == __FILE__
39
+ while filename = ARGV.shift
40
+ puts "Getting info from #{filename}"
41
+ begin
42
+ ape = ApeTag.new(filename)
43
+ rescue ApeTagError
44
+ puts "error: doesn't appear to be an ape tagged file"
45
+ else
46
+ puts ape
47
+ ape.tag.each do |key, value|
48
+ puts "#{key} => #{value}"
49
+ end
50
+ end
51
+ puts
52
+ end
53
+ end
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "stringio"
4
+ require "mp3info/id3v2"
5
+
6
+ class MpcInfoError < StandardError; end
7
+
8
+ class MpcInfo
9
+
10
+ PROFILES_NAMES = [
11
+ 'no profile',
12
+ 'Experimental',
13
+ 'unused',
14
+ 'unused',
15
+ 'unused',
16
+ 'below Telephone (q = 0.0)',
17
+ 'below Telephone (q = 1.0)',
18
+ 'Telephone (q = 2.0)',
19
+ 'Thumb (q = 3.0)',
20
+ 'Radio (q = 4.0)',
21
+ 'Standard (q = 5.0)',
22
+ 'Extreme (q = 6.0)',
23
+ 'Insane (q = 7.0)',
24
+ 'BrainDead (q = 8.0)',
25
+ 'above BrainDead (q = 9.0)',
26
+ 'above BrainDead (q = 10.0)'
27
+ ]
28
+
29
+ FREQUENCIES = [ 44100, 48000, 37800, 32000 ]
30
+
31
+ SV4_6_HEADER = Regexp.new('^[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0]/', nil, 'n')
32
+
33
+ attr_reader :infos
34
+ attr_reader :id3v2_tag
35
+
36
+ def initialize(filename)
37
+ @file = File.open(filename, "rb")
38
+
39
+ @infos = {}
40
+ @infos['raw'] = {}
41
+ parse_infos
42
+ end
43
+
44
+ private
45
+
46
+ def parse_infos
47
+ mpc_header = @file.read(3)
48
+
49
+ if mpc_header == "MP+"
50
+ # this is SV7+
51
+
52
+ header = StringIO.new(@file.read(25))
53
+ header_size = 28
54
+ #stream_version_byte = header.read(4).unpack("V").first
55
+ stream_version_byte = header.read(1)[0] #.unpack("c").first
56
+
57
+ @infos['stream_major_version'] = (stream_version_byte & 0x0F)
58
+ @infos['stream_minor_version'] = (stream_version_byte & 0xF0) >> 4
59
+
60
+ @infos['frame_count'] = read32(header)
61
+ if @infos['stream_major_version'] != 7
62
+ raise(MpcInfoError, "Only Musepack SV7 supported")
63
+ end
64
+
65
+ flags_dword1 = read32(header)
66
+
67
+ @infos['intensity_stereo'] = ((flags_dword1 & 0x80000000) >> 31) == 1
68
+ @infos['mid_side_stereo'] = ((flags_dword1 & 0x40000000) >> 30) == 1
69
+ @infos['max_subband'] = (flags_dword1 & 0x3F000000) >> 24
70
+ @infos['raw']['profile'] = (flags_dword1 & 0x00F00000) >> 20
71
+ @infos['begin_loud'] = ((flags_dword1 & 0x00080000) >> 19) == 1
72
+ @infos['end_loud'] = ((flags_dword1 & 0x00040000) >> 18) == 1
73
+ @infos['raw']['sample_rate'] = (flags_dword1 & 0x00030000) >> 16
74
+ @infos['max_level'] = (flags_dword1 & 0x0000FFFF)
75
+
76
+ @infos['raw']['title_peak'] = read16(header)
77
+ @infos['raw']['title_gain'] = read16(header)
78
+
79
+ @infos['raw']['album_peak'] = read16(header)
80
+ @infos['raw']['album_gain'] = read16(header)
81
+
82
+ flags_dword2 = read32(header)
83
+ @infos['true_gapless'] = ((flags_dword2 & 0x80000000) >> 31) == 1
84
+ @infos['last_frame_length'] = (flags_dword2 & 0x7FF00000) >> 20
85
+
86
+
87
+ not_sure_what = read32(header, 3)
88
+ @infos['raw']['encoder_version'] = read8(header)
89
+
90
+ @infos['profile'] = PROFILES_NAMES[ @infos['raw']['profile'] ] || 'invalid'
91
+ @infos['sample_rate'] = FREQUENCIES[ @infos['raw']['sample_rate'] ]
92
+
93
+ if @infos['sample_rate'] == 0
94
+ raise(MpcInfoError, 'Corrupt MPC file: frequency == zero')
95
+ end
96
+
97
+ sample_rate = @infos['sample_rate'];
98
+ channels = 2 #appears to be hardcoded
99
+ @infos['samples'] = (((@infos['frame_count'] - 1) * 1152) + @infos['last_frame_length']) * channels
100
+ @infos['length'] = (((@infos['frame_count'] - 1) * 1152) + @infos['last_frame_length']) * channels
101
+
102
+ @infos['length'] = (@infos['samples'] / channels) / @infos['sample_rate'].to_f
103
+ if @infos['length'] == 0
104
+ raise(MpcInfoError, 'Corrupt MPC file: playtime_seconds == zero')
105
+ end
106
+
107
+ # add size of file header to avdataoffset - calc bitrate correctly + MD5 data
108
+ avdataoffset = header_size
109
+
110
+ # FIXME is $ThisFileInfo['avdataend'] == File.size ????
111
+ @infos['bitrate'] = ((@file.stat.size - avdataoffset) * 8) / @infos['length']
112
+
113
+ @infos['title_peak'] = @infos['raw']['title_peak']
114
+ @infos['title_peak_db'] = @infos['title_peak'].zero? ? 0 : peak_db(@infos['title_peak'])
115
+ if @infos['raw']['title_gain'] < 0
116
+ @infos['title_gain_db'] = (32768 + @infos['raw']['title_gain']) / -100.0
117
+ else
118
+ @infos['title_gain_db'] = @infos['raw']['title_gain'] / 100.0
119
+ end
120
+
121
+ @infos['album_peak'] = @infos['raw']['album_peak'];
122
+ @infos['album_peak_db'] = @infos['album_peak'].zero? ? 0 : peak_db(@infos['album_peak'])
123
+
124
+ if @infos['raw']['album_gain'] < 0
125
+ @infos['album_gain_db'] = (32768 + @infos['raw']['album_gain']) / -100.0
126
+ else
127
+ @infos['album_gain_db'] = @infos['raw']['album_gain'] / 100.0
128
+ end
129
+ @infos['encoder_version'] = encoder_version(@infos['raw']['encoder_version'])
130
+
131
+ =begin
132
+ #FIXME
133
+ $ThisFileInfo['replay_gain']['track']['adjustment'] = @infos['title_gain_db'];
134
+ $ThisFileInfo['replay_gain']['album']['adjustment'] = @infos['album_gain_db'];
135
+ if @infos['title_peak'] > 0
136
+ #$ThisFileInfo['replay_gain']['track']['peak'] = @infos['title_peak']
137
+ elsif round(@infos['max_level'] * 1.18) > 0)
138
+ # ThisFileInfo['replay_gain']['track']['peak'] = getid3_lib::CastAsInt(round(@infos['max_level'] * 1.18)); // why? I don't know - see mppdec.c
139
+ end
140
+
141
+ if @infos['album_peak'] > 0
142
+ #$ThisFileInfo['replay_gain']['album']['peak'] = @infos['album_peak'];
143
+ end
144
+
145
+ #ThisFileInfo['audio']['encoder'] = 'SV'.@infos['stream_major_version'].'.'.@infos['stream_minor_version'].', '.@infos['encoder_version'];
146
+ #$ThisFileInfo['audio']['encoder'] = @infos['encoder_version'];
147
+ #$ThisFileInfo['audio']['encoder_options'] = @infos['profile'];
148
+ =end
149
+ elsif mpc_header =~ SV4_6_HEADER
150
+ # this is SV4 - SV6, handle seperately
151
+ header_size = 8
152
+ elsif mpc_header == "ID3"
153
+ @id3v2_tag = ID3v2.new
154
+ @id3v2_tag.from_io(@file)
155
+ @file.seek(@id3v2_tag.io_position)
156
+ # very dirty hack to allow parsing of mpc infos after id3v2 tag
157
+ while @file.read(1) != "M"; end
158
+ if @file.read(2) == "P+"
159
+ @file.seek(-3, IO::SEEK_CUR)
160
+ # we need to reparse the tag, since we have the beggining of the mpc file
161
+ parse_infos
162
+ else
163
+ raise(MpcInfoError, "cannot find MPC header after id3 tag")
164
+ end
165
+ else
166
+ raise(MpcInfoError, "cannot find MPC header")
167
+ end
168
+ end
169
+
170
+ def read8(io)
171
+ io.read(1)[0]
172
+ end
173
+
174
+ def read16(io)
175
+ io.read(2).unpack("v").first
176
+ end
177
+
178
+ def read32(io, size = 4)
179
+ io.read(size).unpack("V").first
180
+ end
181
+
182
+ def peak_db(i)
183
+ ((Math.log10(i) / Math.log10(2)) - 15) * 6
184
+ end
185
+
186
+ def encoder_version(encoderversion)
187
+ # Encoder version * 100 (106 = 1.06)
188
+ # EncoderVersion % 10 == 0 Release (1.0)
189
+ # EncoderVersion % 2 == 0 Beta (1.06)
190
+ # EncoderVersion % 2 == 1 Alpha (1.05a...z)
191
+
192
+ if encoderversion == 0
193
+ # very old version, not known exactly which
194
+ 'Buschmann v1.7.0-v1.7.9 or Klemm v0.90-v1.05';
195
+ elsif encoderversion % 10 == 0
196
+ # release version
197
+ sprintf("%.2f", encoderversion/100.0)
198
+ elsif encoderversion % 2 == 0
199
+ sprintf("%.2f beta", encoderversion / 100.0)
200
+ else
201
+ sprintf("%.2f alpha", encoderversion/100.0)
202
+ end
203
+ end
204
+
205
+ end
206
+
207
+ if __FILE__ == $0
208
+ require "pp"
209
+
210
+ mpcinfo = MpcInfo.new(ARGV[0])
211
+ pp mpcinfo.infos.sort
212
+
213
+ end
@@ -0,0 +1,8 @@
1
+ require File.dirname(__FILE__)+"/../lib/audioinfo/mpcinfo"
2
+
3
+ require "pp"
4
+
5
+ fn = "file.mpc"
6
+ mpc = MpcInfo.new(fn)
7
+ pp mpc.id3v2_tag
8
+ pp mpc.infos
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vjt-ruby-audioinfo
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 6
9
+ version: 0.1.6
10
+ platform: ruby
11
+ authors:
12
+ - Guillaume Pierronnet
13
+ - Marcello Barnaba
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-03-20 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: ruby-mp3info
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 6
31
+ - 3
32
+ version: 0.6.3
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: ruby-ogginfo
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ - 3
45
+ - 1
46
+ version: 0.3.1
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: mp4info
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 1
58
+ - 7
59
+ - 3
60
+ version: 1.7.3
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: wmainfo-rb
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
72
+ - 5
73
+ version: "0.5"
74
+ type: :runtime
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: flacinfo-rb
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ segments:
84
+ - 0
85
+ - 4
86
+ version: "0.4"
87
+ type: :runtime
88
+ version_requirements: *id005
89
+ description: ruby-audioinfo glues together various audio libraries and presents a single API to the developer.
90
+ email: moumar@rubyforge.org
91
+ executables: []
92
+
93
+ extensions: []
94
+
95
+ extra_rdoc_files:
96
+ - README.txt
97
+ files:
98
+ - History.txt
99
+ - README.txt
100
+ - Rakefile
101
+ - lib/audioinfo.rb
102
+ - lib/audioinfo/album.rb
103
+ - lib/audioinfo/apetag.rb
104
+ - lib/audioinfo/mpcinfo.rb
105
+ - test/mpcinfo.rb
106
+ has_rdoc: true
107
+ homepage: http://ruby-audioinfo.rubyforge.org
108
+ licenses: []
109
+
110
+ post_install_message:
111
+ rdoc_options:
112
+ - --charset=UTF-8
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ segments:
127
+ - 0
128
+ version: "0"
129
+ requirements: []
130
+
131
+ rubyforge_project:
132
+ rubygems_version: 1.3.6
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: Unified audio info access library
136
+ test_files:
137
+ - test/mpcinfo.rb