about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--Documentation/dtas-player_protocol.txt17
-rw-r--r--lib/dtas/cue_index.rb39
-rw-r--r--lib/dtas/player.rb2
-rw-r--r--lib/dtas/player/client_handler.rb79
-rw-r--r--lib/dtas/source/file.rb29
-rw-r--r--test/test_source_sox.rb43
6 files changed, 202 insertions, 7 deletions
diff --git a/Documentation/dtas-player_protocol.txt b/Documentation/dtas-player_protocol.txt
index be1063a..daafa3e 100644
--- a/Documentation/dtas-player_protocol.txt
+++ b/Documentation/dtas-player_protocol.txt
@@ -80,6 +80,23 @@ Commands here should be alphabetized according to `LC_ALL=C sort'
 * clear - clear current queue (current track/command continues running)
   PENDING: this may be renamed to "queue clear" or "queue-clear"
 
+* cue - display the index/offsets of the file based on the embedded
+  cue sheet, if any
+
+* cue next - skip to the next section of the track based on the
+  embedded cue sheet.  This may skip to the next track if there is
+  no embedded cue sheet or if playing the last (embedded) track
+
+* cue prev - skip to the previous section of the track based on
+  the embedded cue sheet.  This may just seek to the beginning
+  if there is no embedded cue sheet or if we are playing the first
+  (embedded) track.
+
+* cue goto INTEGER - go to the embedded track with cue index denoted
+  by INTEGER (0 is first track as returned by "cue").  Negative
+  values of INTEGER allows selecting track relative to the last
+  track (-1 is the last track, -2 is the penultimate, and so on).
+
 * current - output information about the currently-playing track/command
   in YAML.  The structure of this is unstable and subject to change.
 
diff --git a/lib/dtas/cue_index.rb b/lib/dtas/cue_index.rb
new file mode 100644
index 0000000..09c3709
--- /dev/null
+++ b/lib/dtas/cue_index.rb
@@ -0,0 +1,39 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative '../dtas'
+class DTAS::CueIndex
+  attr_reader :offset
+  attr_reader :index
+
+  def initialize(index, offset)
+    @index = index.to_i
+
+    # must be compatible with the sox "trim" effect
+    @offset = offset # "#{INTEGER}s" (samples) or HH:MM:SS:FRAC
+  end
+
+  def to_hash
+    { "index" => @index, "offset" => @offset }
+  end
+
+  def offset_samples(format)
+    case @offset
+    when /\A(\d+)s\z/
+      $1.to_i
+    else
+      format.hhmmss_to_samples(@offset)
+    end
+  end
+
+  def pregap?
+    @index == 0
+  end
+
+  def track?
+    @index == 1
+  end
+
+  def subindex?
+    @index > 1
+  end
+end
diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb
index 7567dfc..efefcbf 100644
--- a/lib/dtas/player.rb
+++ b/lib/dtas/player.rb
@@ -206,6 +206,8 @@ class DTAS::Player # :nodoc:
       io.emit("OK")
     when "rg"
       rg_handler(io, msg)
+    when "cue"
+      cue_handler(io, msg)
     when "skip"
       skip_handler(io, msg)
     when "sink"
diff --git a/lib/dtas/player/client_handler.rb b/lib/dtas/player/client_handler.rb
index 91dfada..2f356ab 100644
--- a/lib/dtas/player/client_handler.rb
+++ b/lib/dtas/player/client_handler.rb
@@ -351,6 +351,16 @@ module DTAS::Player::ClientHandler # :nodoc:
     @paused ? do_play : do_pause
   end
 
+  def seek_internal(cur, offset)
+    if cur.requeued
+      @queue[0][1] = offset
+    else
+      @queue.unshift([ cur.infile, offset ])
+      cur.requeued = true
+      __buf_reset(cur.dst) # trigger EPIPE
+    end
+  end
+
   def do_seek(io, offset)
     if @current
       if @current.respond_to?(:infile)
@@ -364,13 +374,7 @@ module DTAS::Player::ClientHandler # :nodoc:
         rescue ArgumentError
           return io.emit("ERR bad time format")
         end
-        if @current.requeued
-          @queue[0][1] = offset
-        else
-          @queue.unshift([ @current.infile, offset ])
-          @current.requeued = true
-          __buf_reset(@current.dst) # trigger EPIPE
-        end
+        seek_internal(@current, offset)
       else
         return io.emit("ERR unseekable")
       end
@@ -607,5 +611,66 @@ module DTAS::Player::ClientHandler # :nodoc:
       io.emit("OK")
     end
   end
+
+  def __bp_prev_next(io, msg, cur, bp)
+    case type = msg[1]
+    when nil, "track"
+      bp.keep_if { |ci| ci.track? }
+    when "pregap"
+      bp.keep_if { |ci| ci.pregap? }
+    when "subindex" # any subindex
+      bp.keep_if { |ci| ci.subindex? }
+    when /\A\d+\z/ # exact subindex match
+      si = type.to_i
+      bp.keep_if { |ci| ci.index == si }
+    when "any" # anything goes
+    else
+      return io.emit("INVALID TYPE")
+    end
+    fmt = cur.format
+    case msg[0]
+    when "next"
+      ds = __current_decoded_samples
+      bp.each do |ci|
+        next if ci.offset_samples(fmt) < ds
+        seek_internal(cur, ci.offset)
+        return io.emit("OK")
+      end
+      # go to the next (real) track if not found
+      __current_drop
+    when "prev"
+      os = cur.offset_samples # where we currently started
+      bp.reverse_each do |ci|
+        next if ci.offset_samples(fmt) >= os
+        seek_internal(cur, ci.offset)
+        return io.emit("OK")
+      end
+      # offset may be nil/zero if we couldn't find a previous breakpoint
+      seek_internal(cur, '0')
+    end
+    io.emit("OK")
+  end
+
+  def cue_handler(io, msg)
+    cur = @current
+    if cur.respond_to?(:cuebreakpoints)
+      offset = nil
+      bp = cur.cuebreakpoints
+      case cmd = msg[0]
+      when nil
+        tmp = { "infile" => cur.infile, "cue" => bp.map { |ci| ci.to_hash } }
+        io.emit(tmp.to_yaml)
+      when "next", "prev"
+        return __bp_prev_next(io, msg, cur, bp)
+      when "goto"
+        index = msg[1] or return io.emit("NOINDEX")
+        ci = bp[index.to_i] or return io.emit("BADINDEX")
+        seek_internal(cur, ci.offset)
+        return io.emit("OK")
+      end
+    else
+      io.emit("NOCUE")
+    end
+  end
 end
 # :startdoc:
diff --git a/lib/dtas/source/file.rb b/lib/dtas/source/file.rb
index 3dadc67..8657b0c 100644
--- a/lib/dtas/source/file.rb
+++ b/lib/dtas/source/file.rb
@@ -5,6 +5,7 @@ require_relative '../source'
 require_relative '../command'
 require_relative '../format'
 require_relative '../process'
+require_relative '../cue_index'
 
 module DTAS::Source::File # :nodoc:
   attr_reader :infile
@@ -33,6 +34,7 @@ module DTAS::Source::File # :nodoc:
     @offset = offset
     @comments = nil
     @samples = nil
+    @cuebp = nil
     @rg = nil
   end
 
@@ -90,4 +92,31 @@ module DTAS::Source::File # :nodoc:
     defaults = source_defaults # see dtas/source/{av,sox}.rb
     to_source_cat.delete_if { |k,v| v == defaults[k] }
   end
+
+  def cuebreakpoints
+    rv = @cuebp and return rv
+    rv = []
+    begin
+      str = qx(@env, %W(metaflac --export-cuesheet-to=- #@infile))
+    rescue
+      return rv
+    end
+    str.scan(/^    INDEX (\d+) (\S+)/) do |m|
+      index = m[0]
+      time = m[1].dup
+      case time
+      when /\A\d+\z/
+        time << "s" # sample count (flac 1.3.0)
+      else # HH:MM:SS:FF
+        # FF/75 CDDA frames per second, convert to fractional seconds
+        time.sub!(/:(\d+)\z/, "")
+        frames = $1.to_f
+        if frames > 0
+          time = sprintf("#{time}.%0.6g", frames / 75.0)
+        end
+      end
+      rv << DTAS::CueIndex.new(index, time)
+    end
+    @cuebp = rv
+  end
 end
diff --git a/test/test_source_sox.rb b/test/test_source_sox.rb
index 367f4df..36605a5 100644
--- a/test/test_source_sox.rb
+++ b/test/test_source_sox.rb
@@ -111,4 +111,47 @@ class TestSource < Testcase
       tmp.unlink
     end
   end
+
+  def test_flac_cuesheet_cdda
+    return if `which metaflac`.strip.size == 0
+    tmp = Tempfile.new(%W(tmp .flac))
+    x(%W(sox -n -r44100 -b16 -c2 #{tmp.path} synth 5 pluck vol -1dB))
+    cue = Tempfile.new(%W(tmp .cue))
+    cue.puts %Q(FILE "ignored.flac" FLAC)
+    cue.puts "  TRACK 01 AUDIO"
+    cue.puts "    INDEX 01 00:00:00"
+    cue.puts "  TRACK 02 AUDIO"
+    cue.puts "    INDEX 01 00:01:40"
+    cue.puts "  TRACK 03 AUDIO"
+    cue.puts "    INDEX 01 00:03:00"
+    cue.flush
+    x(%W(metaflac --import-cuesheet-from=#{cue.path} #{tmp.path}))
+    source = DTAS::Source::Sox.new.try(tmp.path)
+    offsets = source.cuebreakpoints.map(&:offset)
+    assert_equal %w(00:00 00:01.0.533333 00:03), offsets
+    source.cuebreakpoints.all?(&:track?)
+  end
+
+  def test_flac_cuesheet_48
+    return if `which metaflac`.strip.size == 0
+    ver = `flac --version`.split(/ /)[1].strip
+    ver.to_f >= 1.3 or return # flac 1.3.0 fixed non-44.1k rate support
+
+    tmp = Tempfile.new(%W(tmp .flac))
+    x(%W(sox -n -r48000 -c2 -b24 #{tmp.path} synth 5 pluck vol -1dB))
+    cue = Tempfile.new(%W(tmp .cue))
+    cue.puts %Q(FILE "ignored.flac" FLAC)
+    cue.puts "  TRACK 01 AUDIO"
+    cue.puts "    INDEX 01 00:00:00"
+    cue.puts "  TRACK 02 AUDIO"
+    cue.puts "    INDEX 01 00:01:00"
+    cue.puts "  TRACK 03 AUDIO"
+    cue.puts "    INDEX 01 00:03:00"
+    cue.flush
+    x(%W(metaflac --import-cuesheet-from=#{cue.path} #{tmp.path}))
+    source = DTAS::Source::Sox.new.try(tmp.path)
+    offsets = source.cuebreakpoints.map(&:offset)
+    assert_equal %w(0s 48000s 144000s), offsets
+    source.cuebreakpoints.all?(&:track?)
+  end
 end