about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--Documentation/dtas-player_protocol.txt24
-rw-r--r--lib/dtas/format.rb8
-rw-r--r--lib/dtas/player.rb43
-rw-r--r--lib/dtas/player/client_handler.rb15
-rw-r--r--test/test_format.rb5
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"