Skip to content

Commit

Permalink
Cue-Sheet Splitting
Browse files Browse the repository at this point in the history
1.0.0
  • Loading branch information
rbrooks committed Apr 29, 2012
1 parent 4cbb682 commit 778930b
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 32 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# AlacIt Changelog

## 1.0.0

* Cue-Sheet Splitting (Extraction) - If a matching-named `.cue` sheet is found in the same directory as the audio file, then multiple M4A's are generated based on the Cue Sheet data.

## 0.1.1

* Fix RubyGems summary and description.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -6,6 +6,7 @@ Apple Lossless conversion utility. Converts APE, FLAC, and WAV audio files to A
* No quality loss
* Basic metadata survives: Song, Artist, etc.
* Converts entire directories, single files, or any combination thereof.
* Cue-Sheet splitting / extraction
* Puts converted files in same dir as source.

### Install
Expand Down Expand Up @@ -51,6 +52,10 @@ AlacIt won't overwrite existing files by default. If you need to, just force ove
alacit --force song.flac
alacit -f song.flac

#### Cue-Sheet Splitting

Have you ever downloaded an album and it's a single, large audio file along with a `.cue` file? AlacIt will split that into individual files for you. If a matching-named `.cue` sheet is found in the same directory as the audio file, then multiple M4A's are generated based on the Cue Sheet data.

### Dependencies

* **Ruby 1.9.2+**
Expand Down
4 changes: 2 additions & 2 deletions alacit.gemspec
Expand Up @@ -9,8 +9,8 @@ spec = Gem::Specification.new do |s|
s.email = 'me@russbrooks.com'
s.homepage = 'http://russbrooks.com'
s.platform = Gem::Platform::RUBY
s.summary = 'APE, FLAC, and WAV to Apple Lossless (ALAC) batch conversion utility.'
s.description = 'Quickly convert entire directories of APE, FLAC, and WAV files to Apple Lossless (ALAC) for importation into iTunes, iPhones, iPads, and iPods.'
s.summary = 'APE, FLAC, and WAV to Apple Lossless (ALAC) batch conversion utility and cue-sheet splitter.'
s.description = 'Quickly convert entire directories of APE, FLAC, and WAV files to Apple Lossless (ALAC) for importation into iTunes, iPhones, iPads, and iPods. It does Cue-Sheet splitting too.'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
Expand Down
93 changes: 70 additions & 23 deletions lib/alacit.rb
Expand Up @@ -12,9 +12,11 @@
# On Linux: `sudo apt-get install flac ffmpeg`
# Windows : [untested]

require 'application'
require 'cuesheet'
require 'index'
require 'open3'
require 'version'
require 'application'

module AlacIt
class Converter < Application
Expand Down Expand Up @@ -44,22 +46,28 @@ def convert_dir(source_dir)

unless Dir.glob(source_glob).empty?
Dir.glob(source_glob) do |file|
m4a_file = file.chomp(File.extname(file)) + '.m4a'
cue_file = file.chomp(File.extname(file)) + '.cue'

if File.exists? cue_file
extract_songs file, cue_file
else
m4a_file = file.chomp(File.extname(file)) + '.m4a'

if !File.exists?(m4a_file) || @options[:force]
command = 'ffmpeg -y -i "' + file + '" -c:a alac "' + m4a_file + '"'
stdout_str, stderr_str, status = Open3.capture3(command)
if !File.exists?(m4a_file) || @options[:force]
command = 'ffmpeg -y -i "' + file + '" -c:a alac "' + m4a_file + '"'
stdout_str, stderr_str, status = Open3.capture3(command)

if status.success?
puts "#{file} converted."
if status.success?
puts "#{file} converted."
else
$stderr.puts "Error: #{file}: File could not be converted."
$stderr.puts stderr_str.split("\n").last
next
end
else
$stderr.puts "Error: #{file}: File could not be converted."
$stderr.puts stderr_str.split("\n").last
$stderr.puts "Error: \"#{m4a_file}\" exists. Use --force option to overwrite."
next
end
else
$stderr.puts "Error: #{m4a_file} exists. Use --force option to overwrite."
next
end
end
else
Expand All @@ -71,22 +79,30 @@ def convert_dir(source_dir)
def convert_file(file)
if File.extname(file) =~ /(\.ape|\.flac|\.wav)/i
if File.exists? file
m4a_file = file.chomp(File.extname(file)) + '.m4a'
cue_file = file.chomp(File.extname(file)) + '.cue'

if !File.exists?(m4a_file) || @options[:force]
command = 'ffmpeg -y -i "' + file + '" -acodec alac "' + m4a_file + '"'
stdout_str, stderr_str, status = Open3.capture3(command)
if File.exists? cue_file
extract_songs file, cue_file
else
# File has no Cuesheet. Convert the entire file.
m4a_file = file.chomp(File.extname(file)) + '.m4a'

if !File.exists?(m4a_file) || @options[:force]
command = 'ffmpeg -y -i "' + file + '" -c:a alac "' + m4a_file + '"'

stdout_str, stderr_str, status = Open3.capture3(command)

if status.success?
puts "#{file} converted."
if status.success?
puts "#{file} converted."
else
$stderr.puts "Error: #{file}: File could not be converted."
$stderr.puts stderr_str.split("\n").last
return
end
else
$stderr.puts "Error: #{file}: File could not be converted."
$stderr.puts stderr_str.split("\n").last
$stderr.puts "Error: \"#{m4a_file}\" exists. Use --force option to overwrite."
return
end
else
$stderr.puts "Error: #{m4a_file} exists."
return
end
else
$stderr.puts "Error: #{file}: No such file."
Expand All @@ -97,6 +113,37 @@ def convert_file(file)
return
end
end

def extract_songs(file, cue_file)
cuesheet = AlacIt::Cuesheet.new(File.read(cue_file))
cuesheet.parse!

cuesheet.songs.each do |song|
m4a_filename = song[:track].to_s.rjust(2, '0') + ' - ' + song[:title] + '.m4a'
m4a_file = File.join(File.dirname(file), m4a_filename)

if !File.exists?(m4a_file) || @options[:force]
command = 'ffmpeg -y'
command << ' -i "' + file + '" -c:a alac'
command << ' -ss ' + song[:index].to_human_ms
command << (song[:duration].nil? ? '' : ' -t ' + song[:duration].to_human_ms)
command << ' "' + m4a_file + '"'

stdout_str, stderr_str, status = Open3.capture3(command)

if status.success?
puts "\"#{m4a_filename}\" extracted based on cue sheet."
else
$stderr.puts "Error: \"#{m4a_filename}\": File could not be extracted."
$stderr.puts stderr_str.split("\n").last
next
end
else
$stderr.puts "Error: \"#{m4a_filename}\" exists. Use --force option to overwrite."
next
end
end
end
end
end

Expand Down
92 changes: 92 additions & 0 deletions lib/cuesheet.rb
@@ -0,0 +1,92 @@
module AlacIt
class Cuesheet
attr_reader :cuesheet, :songs, :track_duration

def initialize(cuesheet, track_duration = nil)
@cuesheet = cuesheet
@reg = {
:track => %r(TRACK (\d{1,3}) AUDIO),
:performer => %r(PERFORMER "(.*)"),
:title => %r(TITLE "(.*)"),
:index => %r(INDEX \d{1,3} (\d{1,3}):(\d{1,2}):(\d{1,2}))
}
@track_duration = AlacIt::Index.new(track_duration) if track_duration
end

def parse!
@songs = parse_titles.map{ |title| {:title => title} }
@songs.each_with_index do |song, i|
song[:performer] = parse_performers[i]
song[:track] = parse_tracks[i]
song[:index] = parse_indices[i]
end
raise AlacIt::InvalidCuesheet.new('Cuesheet is malformed!') unless valid?
calculate_song_durations!
true
end

def position(value)
index = Index.new(value)
return @songs.first if index < @songs.first[:index]
@songs.each_with_index do |song, i|
return song if song == @songs.last
return song if between(song[:index], @songs[i + 1][:index], index)
end
end

def valid?
@songs.all? do |song|
[:performer, :track, :index, :title].all? do |key|
song[key] != nil
end
end
end

private

def calculate_song_durations!
@songs.each_with_index do |song, i|
if song == @songs.last
song[:duration] = (@track_duration - song[:index]) if @track_duration
return
end
song[:duration] = @songs[i + 1][:index] - song[:index]
end
end

def between(a, b, position_index)
(position_index > a) && (position_index < b)
end

def parse_titles
unless @titles
@titles = cuesheet_scan(:title).map{ |title| title.first }
@titles.delete_at(0)
end
@titles
end

def parse_performers
unless @performers
@performers = cuesheet_scan(:performer).map{ |performer| performer.first }
@performers.delete_at(0)
end
@performers
end

def parse_tracks
@tracks ||= cuesheet_scan(:track).map{ |track| track.first.to_i }
end

def parse_indices
@indices ||= cuesheet_scan(:index).map{ |index| AlacIt::Index.new([index[0].to_i, index[1].to_i, index[2].to_i]) }
end

def cuesheet_scan(field)
scan = @cuesheet.scan(@reg[field])
raise InvalidCuesheet.new("No fields were found for #{field.to_s}") if scan.empty?
scan
end

end
end
112 changes: 112 additions & 0 deletions lib/index.rb
@@ -0,0 +1,112 @@
module AlacIt
class Index
SECONDS_PER_MINUTE = 60
FRAMES_PER_SECOND = 75
FRAMES_PER_MINUTE = FRAMES_PER_SECOND * 60

attr_reader :minutes, :seconds, :frames

def initialize(value=nil)
case value
when Array
set_from_array!(value)
when Integer
set_from_integer!(value)
end
end

def to_f
((@minutes * SECONDS_PER_MINUTE) + (@seconds) + (@frames.to_f / FRAMES_PER_SECOND)).to_f
end

def to_i
to_f.floor
end

def to_a
[@minutes, @seconds, @frames]
end

def to_s
"#{'%02d' % @minutes}:#{'%02d' % @seconds}:#{'%02d' % @frames}"
end

def to_human_ms
"00:#{'%02d' % @minutes}:#{'%02d' % @seconds}" + (@frames.to_f / 75).round(3).to_s[1..-1]
end

def +(other)
self.class.new(carrying_addition(other))
end

def -(other)
self.class.new(carrying_subtraction(other))
end

def >(other)
self.to_f > other.to_f
end

def <(other)
self.to_f < other.to_f
end

def ==(other)
self.to_a == other.to_a
end

def each
to_a.each { |value| yield value }
end

private

def carrying_addition(other)
minutes, seconds, frames = *[@minutes + other.minutes,
@seconds + other.seconds, @frames + other.frames]

seconds, frames = *convert_with_rate(frames, seconds, FRAMES_PER_SECOND)
minutes, seconds = *convert_with_rate(seconds, minutes, SECONDS_PER_MINUTE)
[minutes, seconds, frames]
end

def carrying_subtraction(other)
seconds = minutes = 0

my_frames = @frames + (@seconds * FRAMES_PER_SECOND) + (@minutes * FRAMES_PER_MINUTE)
other_frames = other.frames + (other.seconds * FRAMES_PER_SECOND) + (other.minutes * FRAMES_PER_MINUTE)
frames = my_frames - other_frames

seconds, frames = *convert_with_rate(frames, seconds, FRAMES_PER_SECOND)
minutes, seconds = *convert_with_rate(seconds, minutes, SECONDS_PER_MINUTE)
[minutes, seconds, frames]
end

def convert_with_rate(from, to, rate, step=1)
while from >= rate
to += step
from -= rate
end
[to, from]
end

def set_from_array!(array)
if array.size != 3 || array.any?{|element| !element.is_a?(Integer)}
raise ArgumentError.new("Must be initialized with an array in the format of [minutes, seconds,frames], all integers")
end
@minutes, @seconds, @frames = *array
end

def set_from_integer!(seconds)
@minutes = 0
@frames = 0
@seconds = seconds

while @seconds >= SECONDS_PER_MINUTE
@minutes += 1
@seconds -= SECONDS_PER_MINUTE
end
end

end
end
2 changes: 1 addition & 1 deletion lib/version.rb
@@ -1,3 +1,3 @@
module AlacIt
VERSION = '0.1.1'
VERSION = '1.0.0'
end
Binary file added pkg/alacit-1.0.0.gem
Binary file not shown.

0 comments on commit 778930b

Please sign in to comment.