[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH] player: support the "trim" parameter
From: |
Eric Wong |
Subject: |
[PATCH] player: support the "trim" parameter |
Date: |
Wed, 28 Jan 2015 07:48:33 +0000 |
This feature is intended to allow users to "zoom-in" on a
particular portion of a track to tweak parameters (either
with dtas-sourceedit(1) or via playback of splitfx YAML files).
This may be combined with looping the tracklist
(via "tl repeat").
---
Documentation/dtas-player_protocol.txt | 12 ++++++++++++
lib/dtas/player.rb | 15 ++++++++++-----
lib/dtas/player/client_handler.rb | 29 +++++++++++++++++++++++++++++
lib/dtas/source/av.rb | 2 +-
lib/dtas/source/av_ff_common.rb | 25 ++++++++++++++++++-------
lib/dtas/source/ff.rb | 2 +-
lib/dtas/source/file.rb | 24 +++++++++++++++++++++---
lib/dtas/source/sox.rb | 6 +++---
lib/dtas/source/splitfx.rb | 8 ++++----
9 files changed, 99 insertions(+), 24 deletions(-)
diff --git a/Documentation/dtas-player_protocol.txt
b/Documentation/dtas-player_protocol.txt
index 82986cf..e1b2487 100644
--- a/Documentation/dtas-player_protocol.txt
+++ b/Documentation/dtas-player_protocol.txt
@@ -283,6 +283,18 @@ Commands here should be alphabetized according to
`LC_ALL=C sort'
* tl tracks
returns a list of all TRACKIDS in the tracklist
+* trim [off|TBEG [TLEN]]
+ Limits playback of all tracks in the tracklist to the time starting
+ at TBEG and ending after TLEN has elapsed. Not specifying TLEN will
+ cause sox. Like the sox "trim" effect, prefixing TLEN with a '='
+ will be interpreted as an absolute stop time.
+ Both TBEG and TLEN should be specified in seconds, not sample counts.
+ Specifying "off" (without quotes) disables trim.
+ This feature is intended to allow users to "zoom-in" on a particular
+ portion of a track to tweak parameters (either with
+ dtas-sourceedit(1) or via playback of splitfx YAML files) and often
+ combined with looping the tracklist (via "tl repeat").
+
* watch - adds the client to the passive watch list for notifications.
It is recommended clients issue no further commands and open
another client socket to issue non-watch commands.
diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb
index d6707c3..2920dcd 100644
--- a/lib/dtas/player.rb
+++ b/lib/dtas/player.rb
@@ -45,6 +45,7 @@ class DTAS::Player # :nodoc:
@sink_buf = DTAS::Buffer.new
@current = nil
@watchers = {}
+ @trim = nil
@source_map = {
"sox" => (sox = DTAS::Source::Sox.new),
"av" => DTAS::Source::Av.new,
@@ -83,6 +84,7 @@ class DTAS::Player # :nodoc:
rv = {}
rv["socket"] = @socket
rv["paused"] = @paused if @paused
+ rv["trim"] = @trim if @trim
src_map = rv["source"] = {}
@source_map.each do |name, src|
src_hsh = src.to_state_hash
@@ -135,7 +137,7 @@ class DTAS::Player # :nodoc:
v = v["buffer_size"]
@sink_buf.buffer_size = v
end
- %w(socket queue paused bypass).each do |k|
+ %w(socket queue paused bypass trim).each do |k|
v = hash[k] or next
instance_variable_set("@#{k}", v)
end
@@ -247,6 +249,8 @@ class DTAS::Player # :nodoc:
io.emit(Dir.pwd)
when "tl"
tl_handler(io, msg)
+ when "trim"
+ trim_handler(io, msg)
end
end
@@ -367,23 +371,24 @@ class DTAS::Player # :nodoc:
end
end
- def try_file(*args)
+ def try_file(file, offset = nil)
@sources.each do |src|
- rv = src.try(*args) and return rv
+ rv = src.try(file, offset, @trim) and return rv
end
# keep going down the list until we find something
while source_spec = @queue.shift
@sources.each do |src|
- rv = src.try(*source_spec) and return rv
+ rv = src.try(file, offset, @trim) and return rv
end
end
# don't get stuck in an infinite loop if @tl.repeat==true and we can't
# decode anything (FS errors, sox uninstalled, etc...)
while path_off = @tl.advance_track(false)
+ path, off = path_off
@sources.each do |src|
- rv = src.try(*path_off) and return rv
+ rv = src.try(path, off, @trim) and return rv
end
end
diff --git a/lib/dtas/player/client_handler.rb
b/lib/dtas/player/client_handler.rb
index 68f1d80..a867265 100644
--- a/lib/dtas/player/client_handler.rb
+++ b/lib/dtas/player/client_handler.rb
@@ -1,10 +1,12 @@
# Copyright (C) 2013-2015 all contributors <address@hidden>
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
require_relative '../xs'
+require_relative '../parse_time'
# client protocol handling for -player
module DTAS::Player::ClientHandler # :nodoc:
include DTAS::XS
+ include DTAS::ParseTime
# returns true on success, wait_ctl arg on error
def set_bool(io, kv, v)
@@ -183,6 +185,7 @@ module DTAS::Player::ClientHandler # :nodoc:
bytes = bytes < 0 ? 0 : bytes # maybe negative in case of sink errors
end
+ # returns seek offset as an Integer in sample count
def __seek_offset_adj(dir, offset)
if offset.sub!(/s\z/, '')
offset = offset.to_i
@@ -680,5 +683,31 @@ module DTAS::Player::ClientHandler # :nodoc:
io.emit("NOCUE")
end
end
+
+ def trim_handler(io, msg)
+ case msg.size
+ when 0
+ io.emit({ 'trim' => @trim }.to_yaml)
+ when 1, 2
+ case msg[0]
+ when 'off'
+ @trim = nil
+ else
+ begin
+ tbeg = parse_time(msg[0])
+ if tlen = msg[1]
+ absolute = tlen.sub!(/\A=/, '') # 44:00 =44:55
+ tlen = parse_time(tlen)
+ tlen -= tbeg if absolute
+ end
+ @trim = [ tbeg, tlen ] # seconds as float, since we don't know rate
+ rescue => e
+ return io.emit("ERR #{e.message}")
+ end
+ end
+ __current_requeue
+ io.emit('OK')
+ end
+ end
end
# :startdoc:
diff --git a/lib/dtas/source/av.rb b/lib/dtas/source/av.rb
index 722c798..c86b5d2 100644
--- a/lib/dtas/source/av.rb
+++ b/lib/dtas/source/av.rb
@@ -10,7 +10,7 @@ class DTAS::Source::Av # :nodoc:
AV_DEFAULTS = COMMAND_DEFAULTS.merge(
"command" =>
'avconv -v error $SSPOS $PROBE -i "$INFILE" $AMAP -f sox - |' \
- 'sox -p $SOXFMT - $RGFX',
+ 'sox -p $SOXFMT - $TRIMFX $RGFX',
# this is above ffmpeg because this av is the Debian default and
# it's easier for me to test av than ff
diff --git a/lib/dtas/source/av_ff_common.rb b/lib/dtas/source/av_ff_common.rb
index 03526d2..189e135 100644
--- a/lib/dtas/source/av_ff_common.rb
+++ b/lib/dtas/source/av_ff_common.rb
@@ -19,8 +19,8 @@ module DTAS::Source::AvFfCommon # :nodoc:
attr_reader :precision # always 32
attr_reader :format
- def try(infile, offset = nil)
- rv = source_file_dup(infile, offset)
+ def try(infile, offset = nil, trim = nil)
+ rv = source_file_dup(infile, offset, trim)
rv.av_ff_ok? or return
rv
end
@@ -101,10 +101,20 @@ module DTAS::Source::AvFfCommon # :nodoc:
! @astreams.compact.empty?
end
- def sspos(offset)
- offset =~ /\A(\d+)s\z/ or return "-ss #{offset}"
- samples = $1.to_f
- sprintf("-ss %0.9g", samples / @format.rate)
+ def sspos
+ return unless @offset || @trim
+ off = offset_samples / @format.rate.to_f
+ sprintf('-ss %0.9g', off)
+ end
+
+ def av_ff_trimfx # for sox
+ return unless @trim
+ tbeg, tlen = @trim # Floats
+ tend = tbeg + tlen
+ off = offset_samples / @format.rate.to_f
+ tlen = tend - off
+ tlen = 0 if tlen < 0
+ sprintf('trim 0 %0.9g', tlen)
end
def select_astream(as)
@@ -150,8 +160,9 @@ module DTAS::Source::AvFfCommon # :nodoc:
# make sure these are visible to the source command...
e["INFILE"] = @infile
e["AMAP"] = amap
- e["SSPOS"] = @offset ? sspos(@offset) : nil
+ e["SSPOS"] = sspos
e["RGFX"] = rg_state.effect(self) || nil
+ e["TRIMFX"] = av_ff_trimfx
e.merge!(@rg.to_env) if @rg
@pid = dtas_spawn(e, command_string, opts)
diff --git a/lib/dtas/source/ff.rb b/lib/dtas/source/ff.rb
index c59de40..d5e744c 100644
--- a/lib/dtas/source/ff.rb
+++ b/lib/dtas/source/ff.rb
@@ -12,7 +12,7 @@ class DTAS::Source::Ff # :nodoc:
FF_DEFAULTS = COMMAND_DEFAULTS.merge(
"command" =>
'ffmpeg -v error $SSPOS $PROBE -i "$INFILE" $AMAP -f sox - |' \
- 'sox -p $SOXFMT - $RGFX',
+ 'sox -p $SOXFMT - $TRIMFX $RGFX',
# I haven't tested this much since av is in Debian stable and ff is not
"tryorder" => 2,
diff --git a/lib/dtas/source/file.rb b/lib/dtas/source/file.rb
index 8a9fa34..75b4a43 100644
--- a/lib/dtas/source/file.rb
+++ b/lib/dtas/source/file.rb
@@ -21,17 +21,18 @@ module DTAS::Source::File # :nodoc:
FILE_SIVS = %w(infile comments command env) # for the "current" command
SRC_SIVS = %w(command env tryorder)
- def source_file_dup(infile, offset)
+ def source_file_dup(infile, offset, trim)
rv = dup
- rv.__file_init(infile, offset)
+ rv.__file_init(infile, offset, trim)
rv
end
- def __file_init(infile, offset)
+ def __file_init(infile, offset, trim)
@env = @env.dup
@format = nil
@infile = infile
@offset = offset
+ @trim = trim
@comments = nil
@samples = nil
@cuebp = nil
@@ -47,6 +48,13 @@ module DTAS::Source::File # :nodoc:
# returns any offset in samples (relative to the original source file),
# likely zero unless seek was used
def offset_samples
+ off = __offset_samples
+ return off unless @trim
+ tbeg = @trim[0] * format.rate
+ tbeg < off ? off : tbeg
+ end
+
+ def __offset_samples
return 0 unless @offset
case @offset
when /\A\d+s\z/
@@ -56,6 +64,16 @@ module DTAS::Source::File # :nodoc:
end
end
+ # creates the effect to fill the TRIMFX env
+ def trimfx
+ return unless @offset || @trim
+ fx = "trim #{offset_samples}s"
+ if @trim && @trim[1]
+ fx << sprintf(' =%0.9gs', (@trim[0] + @trim[1]) * format.rate)
+ end
+ fx
+ end
+
# A user may be downloading the file and start playing
# it before the download completes, this refreshes
def samples!
diff --git a/lib/dtas/source/sox.rb b/lib/dtas/source/sox.rb
index 99dfe35..91a3c40 100644
--- a/lib/dtas/source/sox.rb
+++ b/lib/dtas/source/sox.rb
@@ -38,13 +38,13 @@ class DTAS::Source::Sox # :nodoc:
command_init(SOX_DEFAULTS)
end
- def try(infile, offset = nil)
+ def try(infile, offset = nil, trim = nil)
err = ""
cmd = %W(soxi -s #{infile})
s = qx(@env.dup, cmd, err_str: err, no_raise: true)
return if err =~ /soxi FAIL formats:/
self.class.try_to_fail_harder(infile, s, cmd) or return
- source_file_dup(infile, offset)
+ source_file_dup(infile, offset, trim)
end
def format
@@ -81,7 +81,7 @@ class DTAS::Source::Sox # :nodoc:
e["INFILE"] = @infile
# make sure these are visible to the "current" command...
- e["TRIMFX"] = @offset ? "trim address@hidden" : nil
+ e["TRIMFX"] = trimfx
e["RGFX"] = rg_state.effect(self) || nil
e.merge!(@rg.to_env) if @rg
diff --git a/lib/dtas/source/splitfx.rb b/lib/dtas/source/splitfx.rb
index 680ad8b..b7b9b86 100644
--- a/lib/dtas/source/splitfx.rb
+++ b/lib/dtas/source/splitfx.rb
@@ -20,7 +20,7 @@ class DTAS::Source::SplitFX < DTAS::Source::Sox # :nodoc:
@sox = sox
end
- def try(ymlfile, offset = nil)
+ def try(ymlfile, offset = nil, trim = nil)
@splitfx = @ymlhash = nil
st = File.stat(ymlfile)
return false if !st.file? || st.size > MAX_YAML_SIZE
@@ -41,8 +41,8 @@ class DTAS::Source::SplitFX < DTAS::Source::Sox # :nodoc:
end
@splitfx = sfx
@infile = ymlfile
- sox = @sox.try(sfx.infile, offset) or return false
- rv = source_file_dup(ymlfile, offset)
+ sox = @sox.try(sfx.infile, offset, trim) or return false
+ rv = source_file_dup(ymlfile, offset, trim)
rv.sox = sox
rv.env = sfx.env
rv.sfx = sfx
@@ -66,7 +66,7 @@ class DTAS::Source::SplitFX < DTAS::Source::Sox # :nodoc:
@sfx.infile_env(e, @sox.infile)
# make sure these are visible to the "current" command...
- e["TRIMFX"] = @offset ? "trim address@hidden" : nil
+ e["TRIMFX"] = trimfx
e["RGFX"] = rg_state.effect(self) || nil
e.merge!(@rg.to_env) if @rg
--
EW
- [PATCH] player: support the "trim" parameter,
Eric Wong <=