Permalink
Switch branches/tags
Nothing to show
Find file Copy path
aa23025 Oct 1, 2018
1 contributor

Users who have contributed to this file

executable file 149 lines (130 sloc) 4.17 KB
#!/usr/bin/env ruby
require "pp"
require "shellwords"
require "pathname"
require "fileutils"
class Pathname
def shellescape
to_s.shellescape
end
end
class SpeedUpMp3
def initialize(factor)
@factor = factor
end
def known_tags
%W[TPE1 TIT2 TALB TCON TIT3 TRCK TYER COMM]
end
def read_id3_tags_raw(source)
# Most ID3 tags in the wild are not UTF-8
`id3v2 -l #{source.to_s.shellescape}`.force_encoding("ISO-8859-1")
end
def read_id3_tags(source)
read_id3_tags_raw(source).scan(/^([a-zA-Z0-9]{4}) \(.*?\): (.*)$/).select{|tag, value|
known_tags.include?(tag)
}
end
def copy_id3_tags!(source, target)
tags = read_id3_tags(source).map{|tag, value| ["--#{tag}", value]}.flatten
return if tags.empty?
cmd = ["id3v2"]
cmd += tags
cmd += [target.to_s]
system *cmd
end
def copy_timestamp!(source, target)
FileUtils.touch(target, mtime: source.mtime)
end
def sox_m4a!(source, target)
part_target = "#{target}.part"
system %Q[ffmpeg -loglevel panic -i #{source.shellescape} -f wav pipe: | sox -t wav - -t mp3 #{part_target.shellescape} tempo -s #{@factor}] or raise "#{source} conversion failed"
copy_timestamp!(source, part_target)
system *%W[mv #{part_target} #{target}]
end
def sox!(source, target)
part_target = "#{target}.part"
system *%W[sox #{source} -t mp3 #{part_target} tempo -s #{@factor}] or raise "#{source} conversion failed"
copy_timestamp!(source, part_target)
system *%W[mv #{part_target} #{target}]
end
def speedup_video!(source, target)
# TODO: there are ways to stack filters for other speeds
raise "ffmpeg only supports speeds from 0.5 to 2.0" unless (0.5..2.0).include?(@factor)
filter = "[0:v]setpts=PTS/#{@factor}[v];[0:a]atempo=#{@factor}[a]"
part_target = target.dirname + "part-#{target.basename}"
system *%W[ffmpeg -i #{source} -filter_complex #{filter} -map [v] -map [a] #{part_target}]
copy_timestamp!(source, part_target)
system *%W[mv #{part_target} #{target}]
end
def speedup_file!(source, target)
target.dirname.mkpath
return if source.basename.to_s == ".DS_Store" # OSX junk
case source.extname.downcase
when ".mp3"
sox!(source, target) and copy_id3_tags!(source, target)
when ".m4a", ".m4b", ".opus", ".ogg", ".aac"
sox_m4a!(source, target.to_s.sub(/\.(?:m4a|m4b|mp4|opus|ogg)\z/i, ".mp3"))
when ".mp4", ".mkv", ".m4v"
speedup_video!(source, target)
when ".wav"
sox!(source, target.to_s.sub(/\.(?:wav)\z/i, ".mp3"))
else
warn "Don't know how to convert '#{source}' to '#{target}'"
end
end
def speedup_to_directory!(source, target)
if Pathname(source).directory?
source.children.each do |child|
target_child = target+child.basename
if child.directory?
speedup_to_directory!(child, target_child)
else
speedup_file!(child, target_child)
end
end
else
speedup_file!(source, target+source.basename)
end
end
def run!(args)
target = args.pop
sources = args
# TODO: Support autocreating target directory if it doesn't exist,
# and better error messages?
if target.directory?
sources.each do |source|
speedup_to_directory!(source, target)
end
elsif sources.size == 1
source = sources[0]
if source.directory?
source.find do |source_path|
target_path = target + source_path.relative_path_from(source)
if source_path.directory?
target_path.mkpath
else
speedup_file!(source_path, target_path)
end
end
else
speedup_file!(source, target)
end
else
STDERR.puts "#{target} must be a directory to speedup multiple source files to it"
exit 1
end
end
end
speed = 1.4
if ARGV[0] =~ /\A-([\d\.]+)\z/
speed = $1.to_f
ARGV.shift
end
if ARGV.empty?
STDERR.puts "Usage: #{$0} [-factor] file_in.mp3 file_out.mp3"
STDERR.puts " #{$0} [-factor] file1.mp3 file2.mp3 dir"
STDERR.puts " #{$0} [-factor] dir_in dir_out"
STDERR.puts "Default factor is 1.4 (40% faster)"
exit 1
end
SpeedUpMp3.new(speed).run!(ARGV.map{|x| Pathname(x)})