diff options
-rw-r--r-- | Documentation/dtas-player_protocol.txt | 24 | ||||
-rw-r--r-- | lib/dtas/format.rb | 8 | ||||
-rw-r--r-- | lib/dtas/player.rb | 43 | ||||
-rw-r--r-- | lib/dtas/player/client_handler.rb | 15 | ||||
-rw-r--r-- | 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" |