about summary refs log tree commit homepage
path: root/lib/dtas
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2013-09-28 06:50:04 +0000
committerEric Wong <normalperson@yhbt.net>2013-09-28 07:10:53 +0000
commit77cbb5e71e466749f4966ef0caca6a5077ad72c9 (patch)
treeab5112f6a538e9bab91b0bbe6d5b7c1bceb819b2 /lib/dtas
parent2118e3aa78ebbd91b9d10ec524186820cbfa9c50 (diff)
downloaddtas-77cbb5e71e466749f4966ef0caca6a5077ad72c9.tar.gz
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.
Diffstat (limited to 'lib/dtas')
-rw-r--r--lib/dtas/format.rb8
-rw-r--r--lib/dtas/player.rb43
-rw-r--r--lib/dtas/player/client_handler.rb15
3 files changed, 55 insertions, 11 deletions
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