[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH] player: support bypass for rate, bits, channel
From: |
Eric Wong |
Subject: |
[PATCH] player: support bypass for rate, bits, channel |
Date: |
Sat, 28 Sep 2013 07:11:26 +0000 |
This may be used to avoid automatic:
* resampling (rate)
* down/upmixing (channel)
* dither/truncation (bits)
Using any bypass mode means we can no longer guarantee gapless
playback for audio collections where rate, channel, or bits vary.
This can however be useful when CPU usage is too high. This may
also be useful in audio engineering situations.
---
Documentation/dtas-player_protocol.txt | 24 +++++++++++++------
lib/dtas/format.rb | 8 +++++++
lib/dtas/player.rb | 43 +++++++++++++++++++++++++++-------
lib/dtas/player/client_handler.rb | 15 +++++++++---
test/test_format.rb | 5 ++++
5 files changed, 77 insertions(+), 18 deletions(-)
diff --git a/Documentation/dtas-player_protocol.txt
b/Documentation/dtas-player_protocol.txt
index 7fecec1..be1063a 100644
--- a/Documentation/dtas-player_protocol.txt
+++ b/Documentation/dtas-player_protocol.txt
@@ -111,20 +111,30 @@ Commands here should be alphabetized according to
`LC_ALL=C sort'
Changing this will affect the $SOXFMT and $ECAFMT environments passed
to source and sink commands. Changing this implies a "restart"
Changing rate to 48000 is probably useful if you plan on playing to some
- laptop sound cards.
+ laptop sound cards. In all cases where "bypass" is supported, it
+ removes the guarantee of gapless playback as the audio device(s)
+ will likely need to be restarted.
- + channels=UNSIGNED - (default: 2 (stereo)) - number of channels
- to use internally. sox will internally invoke the remix effect
- when decoding.
+
+ + channels=(UNSIGNED|bypass) - (default: 2 (stereo)) - number of
+ channels to use internally. sox will internally invoke the remix
+ effect when decoding. This supports the value "bypass" (without
+ quotes) to avoid the automatic remix effect. Using "bypass" mode
+ removes the guarantee of gapless playback, as the audio device will
+ likely need to be restarted, introducing an audible gap.
+ endian=(big|little|swap) - (default: native) - there is probably no
point in changing this unless you output over a network sink to
a machine of different endianess.
- + bits=UNSIGNED - (default: implied from type) - sample precision (decoded)
+ + bits=(UNSIGNED|bypass) - (default: implied from type) - sample precision
+ (decoded)
This may be pointless and removed in the future, since the sample
- precision is implied from type.
- + rate=UNSIGNED - (default: 44100) - sample rate of audio
+ precision is implied from type. This supports the value of "bypass"
+ to avoid dither/truncation in later stages.
+ + rate=(UNSIGNED|bypass) - (default: 44100) - sample rate of audio
Typical values of rate are 44100, 48000, 88200, 96000. Not all
DSP effects are compatible with all sampling rates/channels.
+ This supports the value of "bypass" as well to avoid introducing
+ software resamplers into the playback chain.
+ type=(s16|s24|s32|u16|u24|u32|f32|f64) - (default: s32)
change the raw PCM format. s32 currently offers the best performance
when only sox/play are used. f32 may offer better performance if
diff --git a/lib/dtas/format.rb b/lib/dtas/format.rb
index e9da16f..223d9c0 100644
--- a/lib/dtas/format.rb
+++ b/lib/dtas/format.rb
@@ -81,6 +81,14 @@ class DTAS::Format # :nodoc:
ivars_to_hash(SIVS)
end
+ def ==(other)
+ a = to_hash
+ b = other.to_hash
+ a["bits"] ||= bits_per_sample
+ b["bits"] ||= other.bits_per_sample
+ a == b
+ end
+
# for the _decoded_ output
def bits_per_sample
return @bits if @bits
diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb
index 273e56b..7567dfc 100644
--- a/lib/dtas/player.rb
+++ b/lib/dtas/player.rb
@@ -33,6 +33,7 @@ class DTAS::Player # :nodoc:
@queue = [] # files for sources, or commands
@paused = false
@format = DTAS::Format.new
+ @bypass = [] # %w(rate bits channels) (not worth Hash overhead)
@sinks = {} # { user-defined name => sink }
@targets = [] # order matters
@@ -55,7 +56,10 @@ class DTAS::Player # :nodoc:
end
def wall(msg)
- msg = xs(Array(msg))
+ __wall(xs(Array(msg)))
+ end
+
+ def __wall(msg)
@watchers.delete_if do |io, _|
if io.closed?
true
@@ -84,6 +88,7 @@ class DTAS::Player # :nodoc:
# Arrays
rv["queue"] = @queue
+ rv["bypass"] = @bypass.sort!
%w(rg sink_buf format).each do |k|
rv[k] = instance_variable_get("@#{k}").to_hsh
@@ -123,7 +128,7 @@ class DTAS::Player # :nodoc:
v = v["buffer_size"]
@sink_buf.buffer_size = v
end
- %w(socket queue paused).each do |k|
+ %w(socket queue paused bypass).each do |k|
v = hash[k] or next
instance_variable_set("@#{k}", v)
end
@@ -366,31 +371,46 @@ class DTAS::Player # :nodoc:
def next_source(source_spec)
@current = nil
if source_spec
- # restart sinks iff we were idle
- spawn_sinks(source_spec) or return
-
case source_spec
when String
pending = try_file(source_spec) or return
- wall(%W(file #{pending.infile}))
+ msg = %W(file #{pending.infile})
when Array
pending = try_file(*source_spec) or return
- wall(%W(file #{pending.infile} #{pending.offset_samples}s))
+ msg = %W(file #{pending.infile} #{pending.offset_samples}s)
else
pending = DTAS::Source::Cmd.new(source_spec["command"])
- wall(%W(command #{pending.command_string}))
+ msg = %W(command #{pending.command_string})
+ end
+
+ unless @bypass.empty?
+ new_fmt = bypass_match!(@format.dup, pending.format)
+ if new_fmt != @format
+ stop_sinks # we may fail to start below
+ format_update!(new_fmt)
+ end
end
+ # restart sinks iff we were idle
+ spawn_sinks(source_spec) or return
+
dst = @sink_buf
pending.dst_assoc(dst)
pending.spawn(@format, @rg, out: dst.wr, in: "/dev/null")
@current = pending
@srv.wait_ctl(dst, :wait_readable)
+ wall(msg)
else
player_idle
end
end
+ def format_update!(fmt)
+ ary = fmt.to_hash.inject(%w(format)) { |m,(k,v)| v ? m << "#{k}=#{v}" : m }
+ @format = fmt
+ __wall(ary.join(' ')) # do not escape '='
+ end
+
def player_idle
stop_sinks if @sink_buf.inflight == 0
@tl.reset unless @paused
@@ -457,4 +477,11 @@ class DTAS::Player # :nodoc:
@sink_buf.close!
@state_file.dump(self, true) if @state_file
end
+
+ def bypass_match!(dst_fmt, src_fmt)
+ @bypass.each do |k|
+ dst_fmt.__send__("#{k}=", src_fmt.__send__(k))
+ end
+ dst_fmt
+ end
end
diff --git a/lib/dtas/player/client_handler.rb
b/lib/dtas/player/client_handler.rb
index 9c28486..a44c1b7 100644
--- a/lib/dtas/player/client_handler.rb
+++ b/lib/dtas/player/client_handler.rb
@@ -295,6 +295,7 @@ module DTAS::Player::ClientHandler # :nodoc:
end
tmp["current_inflight"] = @sink_buf.inflight
tmp["format"] = @format.to_hash.delete_if { |_,v| v.nil? }
+ tmp["bypass"] = @bypass.sort!
tmp["paused"] = @paused
rg = @rg.to_hsh
tmp["rg"] = rg unless rg.empty?
@@ -402,20 +403,28 @@ module DTAS::Player::ClientHandler # :nodoc:
new_fmt.valid_type?(v) or return io.emit("ERR invalid file type")
new_fmt.type = v
when "channels", "bits", "rate"
- rv = set_uint(io, kv, v, false) { |u| new_fmt.__send__("#{k}=", u) }
- rv == true or return rv
+ case v
+ when "bypass"
+ @bypass << k unless @bypass.include?(k)
+ else
+ rv = set_uint(io, kv, v, false) { |u| new_fmt.__send__("#{k}=", u) }
+ rv == true or return rv
+ @bypass.delete(k)
+ end
when "endian"
new_fmt.valid_endian?(v) or return io.emit("ERR invalid endian")
new_fmt.endian = v
end
end
+ bypass_match!(new_fmt, @current.format) if @current
+
if new_fmt != @format
restart_pipeline # calls __current_requeue
# we must assign this after __current_requeue since __current_requeue
# relies on the old @format for calculation
- @format = new_fmt
+ format_update!(new_fmt)
end
io.emit("OK")
end
diff --git a/test/test_format.rb b/test/test_format.rb
index f251e6b..0441d9a 100644
--- a/test/test_format.rb
+++ b/test/test_format.rb
@@ -12,6 +12,11 @@ class TestFormat < Testcase
assert_equal({}, hash)
end
+ def test_equal
+ fmt = DTAS::Format.new
+ assert_equal fmt, fmt.dup
+ end
+
def test_nonstandard
fmt = DTAS::Format.new
fmt.type = "s16"
--
1.8.4
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [PATCH] player: support bypass for rate, bits, channel,
Eric Wong <=