From 77cbb5e71e466749f4966ef0caca6a5077ad72c9 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 28 Sep 2013 06:50:04 +0000 Subject: player: support bypass for rate, bits, channel 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. --- lib/dtas/format.rb | 8 ++++++++ lib/dtas/player.rb | 43 +++++++++++++++++++++++++++++++-------- lib/dtas/player/client_handler.rb | 15 +++++++++++--- 3 files changed, 55 insertions(+), 11 deletions(-) (limited to 'lib') 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 -- cgit v1.2.3-24-ge0c7