vjt-ruby-audioinfo 0.1.6

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.
@@ -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