[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH] player: support seeking based on embedded cuesheet (FLAC)
From: |
Eric Wong |
Subject: |
[PATCH] player: support seeking based on embedded cuesheet (FLAC) |
Date: |
Mon, 30 Sep 2013 03:40:41 +0000 |
This adds the ability to seek internally within FLAC file
based on the internal CUE sheet. Other formats may be supported
in the future, but FLAC is the only one I know of which supports
embedded cue sheets.
Note: flac 1.3.0 is recommended for users of non-CDDA-compatible
formats.
See updates to dtas-player_protocol(7) for details.
---
Documentation/dtas-player_protocol.txt | 17 ++++++++
lib/dtas/cue_index.rb | 39 +++++++++++++++++
lib/dtas/player.rb | 2 +
lib/dtas/player/client_handler.rb | 79 +++++++++++++++++++++++++++++++---
lib/dtas/source/file.rb | 29 +++++++++++++
test/test_source_sox.rb | 43 ++++++++++++++++++
6 files changed, 202 insertions(+), 7 deletions(-)
create mode 100644 lib/dtas/cue_index.rb
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 <address@hidden> 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=- address@hidden))
+ 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
--
1.8.4
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [PATCH] player: support seeking based on embedded cuesheet (FLAC),
Eric Wong <=