#!/usr/bin/ruby CNT = 3 EIGHTS = 1 # Counting 8's instead of beats SKIP = 40 require 'delegate' ################################################## # Updates to common methods ################################################## class String # I'm surprised that ruby doesn't have this built in for system calls @@quote_chars = %w(\\ & ; | * < > \( \) ' ") + [' '] def quote! @@quote_chars.each { |q| self.gsub!(q) { |g| "\\"+g } } self end def quote new = self.dup new.quote! end end ################################################## # Song handlers (mp3/ogg/..) ################################################## class SongMP3 @@tool = "id3tool" # A frame is about 3200bytes? So that's ~40 frames/s at 128kb/s @@play = %w(mpg123 --loop -1 -k)+[(SKIP*40).to_s] @path = '' @playing = nil def initialize(path) @path = path end def comment c = @@tool+" "+@path.quote comment = '' IO.popen(c).each { |l| comment += $1 if l =~ /^Note:\s+(\S.*?)\s*$/ } comment end def comment=(val) c = @@tool+" "+@path.quote+" -n "+val.quote #puts "SET #{c}" raise RuntimeError, "Trouble running #{@@tool}" unless system(c) end def play @playing = fork return if @playing $stdout.close() $stderr.close() $stdin.close() exec(*@@play+[@path]) exit # Shouldn't get here end def stop return unless @playing Process.kill(9,@playing) end def self.is(path) return path =~ /\.mp3$/i ? true : false end end class SongOGG @@tool = "vorbiscomment" @@play = %w(ogg123 -k)+[SKIP.to_s] @path = '' @playing = nil @tags = nil def initialize(path) @path = path end def comment c = @@tool+" "+@path.quote @tags = Hash.new IO.popen(c).each { |l| @tags[$1] = $2 if l =~ /^([^=]+)=(.+)/ } @tags['comment'] # There's a description tag, but xmms uses 'comment' end def comment=(val) comment unless @tags @tags['comment'] = val c = @@tool+" -w "+@path.quote @tags.each_pair { |k,v| c += " -t #{k}=#{v.quote}" } #puts c raise RuntimeError, "Trouble running #{@@tool}" unless system(c) #puts "SYS #{c} gave #{$?}" end def play @playing = fork return if @playing $stdout.close() $stderr.close() $stdin.close() exec(*@@play+[@path]) puts "YO!" exit # Shouldn't get here end def stop return unless @playing Process.kill(9,@playing) end def self.is(path) return path =~ /\.ogg$/i ? true : false end end # Delegate to the right song object class Song < SimpleDelegator attr_reader :name, :path @actual = nil def initialize(path) @path = path @name = path.sub(/.*\//,'') case true when SongMP3.is(path) @actual = SongMP3.new(path) when SongOGG.is(path) @actual = SongOGG.new(path) else raise RuntimeError, "Unknown song type:\n #{path}\n" end __setobj__(@actual) # Delegate end end ################################################## # BPM code ################################################## def getBPM times = Array.new puts "\nHit return for every " + (EIGHTS ? "8 beats" : "beat") + "\n 'b' for back, 's' for skip, 'r' for retry, 'q' for quit" CNT.times { |i| print "#{CNT-i} " g = $stdin.gets.chomp return false if g =~ /q/; return -1 if g =~ /b/; return -2 if g =~ /s/; return $1.to_i if g =~ /=(\d+)/; if (g =~ /r/) then puts " Try again.." return getBPM end times.push Time.now } ave = 0 (CNT-1).times { |t| ave += times[t+1]-times[t] } # We really only need to look at the first and last time, but whatever.. ave /= (CNT-1) ave /= 8 if EIGHTS (60/ave).to_i end ################################################## # Manage songs ################################################## # Figure out which songs already have BPM def usage puts "Usage: #{$0.sub(/.*\//,'')} " puts " Tag songs with BPMs (in comments)" puts puts " -f Force tagging, even if song already has BPMs" puts " -clear Clear any prior comments" puts exit -1 end def songList songs = Array.new force = false tmpforce = false clear = false usage if ARGV.empty? ARGV.each { |path| case path when '-h' usage when '-?' usage when '-f' force = true when '-clear' clear = true else songs.push(path) end } count = 0 loop { count += 1 break if count > songs.size path = songs[count-1] song = Song.new(path) c = song.comment if (!tmpforce && !force && c =~ /(\d+)bpm/) then # Already tagged puts "Tagged (#{$1} bpm): #{song.name}" next end tmpforce = false c = "" if c =~ /^\s+$/ c && c.gsub!(/^(\d+bpm\s*)+/,'') # In case of force puts "Name: #{song.name} #{count}/#{songs.length}" puts " comment: <#{c}>" if c && !c.empty? c= "" if clear song.play bpm = getBPM song.stop exit unless bpm if (bpm<0) then count -= 2 if bpm==-1 tmpforce = true next end puts "BPM: #{bpm}" c = c && !c.empty? ? "#{bpm}bpm #{c}" : "#{bpm}bpm" song.comment=c } end #puts "#{bpm} BPM" songList