From 3d96e3d4a3ad7fbf45b38c81478f04cffb329e25 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 26 Aug 2013 02:00:05 +0000 Subject: cleanup multi-source handling between sox and av This should better prepare us to make "source ed" into "source ed" and set per-source priorities. We also now treat @env consistently for all per-source commands (such as soxi/avprobe) so we can be sure we're using the same installation of sox or libav if using a non-standard PATH, or if we want to set AV_LOG_FORCE_NOCOLOR --- lib/dtas/player.rb | 11 +++--- lib/dtas/process.rb | 3 -- lib/dtas/source/av.rb | 88 ++++++++++++++++++++++++++------------------- lib/dtas/source/sox.rb | 40 ++++++++++----------- test/test_rg_integration.rb | 2 +- test/test_source_av.rb | 18 +++++----- test/test_source_sox.rb | 20 +++++------ 7 files changed, 97 insertions(+), 85 deletions(-) diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb index 2a3420a..f1d3507 100644 --- a/lib/dtas/player.rb +++ b/lib/dtas/player.rb @@ -26,7 +26,7 @@ class DTAS::Player # :nodoc: @state_file = nil @socket = nil @srv = nil - @queue = [] # sources + @queue = [] # files for sources, or commands @paused = false @format = DTAS::Format.new @srccmd = nil @@ -40,6 +40,7 @@ class DTAS::Player # :nodoc: @sink_buf = DTAS::Buffer.new @current = nil @watchers = {} + @sources = [ DTAS::Source::Sox.new, DTAS::Source::Av.new ] end def echo(msg) @@ -298,14 +299,14 @@ class DTAS::Player # :nodoc: end def try_file(*args) - [ DTAS::Source::Sox, DTAS::Source::Av ].each do |klass| - rv = klass.try(*args) and return rv + @sources.each do |src| + rv = src.try(*args) and return rv end # keep going down the list until we find something while source_spec = @queue.shift - [ DTAS::Source::Sox, DTAS::Source::Av ].each do |klass| - rv = klass.try(*source_spec) and return rv + @sources.each do |src| + rv = src.try(*source_spec) and return rv end end echo "idle" diff --git a/lib/dtas/process.rb b/lib/dtas/process.rb index 2cf59c7..8e2ca1b 100644 --- a/lib/dtas/process.rb +++ b/lib/dtas/process.rb @@ -85,7 +85,4 @@ module DTAS::Process # :nodoc: raise RuntimeError, "`#{Shellwords.join(Array(cmd))}' failed: #{status.inspect}" end - - # XXX only for DTAS::Source::{Sox,Av}.try - module_function :qx end diff --git a/lib/dtas/source/av.rb b/lib/dtas/source/av.rb index 66ee320..a6a0e3a 100644 --- a/lib/dtas/source/av.rb +++ b/lib/dtas/source/av.rb @@ -17,38 +17,36 @@ class DTAS::Source::Av # :nodoc: "command" => 'avconv -v error $SSPOS -i "$INFILE" $AMAP -f sox - |' \ 'sox -p $SOXFMT - $RGFX', - "comments" => nil, ) attr_reader :precision # always 32 attr_reader :format - def self.try(infile, offset = nil) - err = "" - DTAS::Process.qx(%W(avprobe #{infile}), err_str: err) - return if err =~ /Unable to find a suitable output format for/ - new(infile, offset) - rescue - end - - def initialize(infile, offset = nil) + def initialize command_init(AV_DEFAULTS) - source_file_init(infile, offset) @precision = 32 # this still goes through sox, which is 32-bit - do_avprobe end - def do_avprobe + def try(infile, offset = nil) + rv = dup + rv.source_file_init(infile, offset) + rv.av_ok? or return + rv + end + + def av_ok? @duration = nil @format = DTAS::Format.new @format.bits = @precision @comments = {} - err = "" - s = qx(%W(avprobe -show_streams -show_format #@infile), err_str: err) @astreams = [] + cmd = %W(avprobe -show_streams -show_format #@infile) + err = "" + s = qx(@env, cmd, err_str: err, no_raise: true) + return false if Process::Status === s + return false if err =~ /Unable to find a suitable output format for/ s.scan(%r{^\[STREAM\]\n(.*?)\n\[/STREAM\]\n}m) do |_| stream = $1 - # XXX what to do about multiple streams? if stream =~ /^codec_type=audio$/ as = AStream.new index = nil @@ -56,6 +54,9 @@ class DTAS::Source::Av # :nodoc: stream =~ /^duration=([\d\.]+)\s*$/m and as.duration = $1.to_f stream =~ /^channels=(\d)\s*$/m and as.channels = $1.to_i stream =~ /^sample_rate=([\d\.]+)\s*$/m and as.rate = $1.to_i + index or raise "BUG: no audio index from #{Shellwords.join(cmd)}" + + # some streams have zero channels @astreams[index] = as if as.channels > 0 && as.rate > 0 end end @@ -65,6 +66,7 @@ class DTAS::Source::Av # :nodoc: # TODO: multi-line/multi-value/repeated tags f.gsub!(/^TAG:([^=]+)=(.*)$/i) { |_| @comments[$1.upcase] = $2 } end + ! @astreams.empty? end def sspos(offset) @@ -73,41 +75,55 @@ class DTAS::Source::Av # :nodoc: sprintf("-ss %0.9g", samples / @format.rate) end + def select_astream(as) + @format.channels = as.channels + @format.rate = as.rate + + # favor the duration of the stream we're playing instead of + # duration we got from [FORMAT]. However, some streams may not have + # a duration and only have it in [FORMAT] + @duration = as.duration if as.duration + end + + def amap_fallback + @astreams.each_with_index do |as, index| + as or next + select_astream(as) + warn "no suitable audio stream in #@infile, trying stream=#{index}" + return "-map 0:#{i}" + end + raise "BUG: no audio stream in #@infile" + end + def spawn(player_format, rg_state, opts) raise "BUG: #{self.inspect}#spawn called twice" if @to_io amap = nil - found_as = nil # try to find an audio stream which matches our channel count # we need to set @format for sspos() down below - @astreams.each_with_index do |as, index| + @astreams.each_with_index do |as, i| if as && as.channels == player_format.channels - @format.channels = as.channels - @format.rate = as.rate - found_as = as - amap = "-map 0:#{index}" - end - end - unless found_as - first_as = @astreams.compact[0] - if first_as - @format.channels = found_as.channels - @format.rate = found_as.rate + select_astream(as) + amap = "-map 0:#{i}" end end - e = player_format.to_env + + # fall back to the first audio stream + # we must call select_astream before sspos + amap ||= amap_fallback + + e = @env.merge!(player_format.to_env) + + # make sure these are visible to the source command... e["INFILE"] = @infile e["AMAP"] = amap - - # make sure these are visible to the "current" command... - @env["SSPOS"] = @offset ? sspos(@offset) : nil - @env["RGFX"] = rg_state.effect(self) || nil + e["SSPOS"] = @offset ? sspos(@offset) : nil + e["RGFX"] = rg_state.effect(self) || nil e.merge!(@rg.to_env) if @rg - @pid = dtas_spawn(e.merge!(@env), command_string, opts) + @pid = dtas_spawn(e, command_string, opts) end - # This is the number of samples according to the samples in the source # file itself, not the decoded output def samples diff --git a/lib/dtas/source/sox.rb b/lib/dtas/source/sox.rb index 30e7f18..fa6192d 100644 --- a/lib/dtas/source/sox.rb +++ b/lib/dtas/source/sox.rb @@ -13,27 +13,25 @@ class DTAS::Source::Sox # :nodoc: SOX_DEFAULTS = COMMAND_DEFAULTS.merge( "command" => 'exec sox "$INFILE" $SOXFMT - $TRIMFX $RGFX', - "comments" => nil, ) + def initialize + command_init(SOX_DEFAULTS) + end - def self.try(infile, offset = nil) + def try(infile, offset = nil) err = "" - DTAS::Process.qx(%W(soxi #{infile}), err_str: err) + qx(@env, %W(soxi #{infile}), err_str: err, no_raise: true) return if err =~ /soxi FAIL formats:/ - new(infile, offset) - rescue - end - - def initialize(infile, offset = nil) - command_init(SOX_DEFAULTS) - source_file_init(infile, offset) + rv = dup + rv.source_file_init(infile, offset) + rv end def precision - qx(%W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3 + qx(@env, %W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3 rescue # fallback to parsing the whole output - s = qx(%W(soxi #@infile), err: "/dev/null") + s = qx(@env, %W(soxi #@infile), err: "/dev/null") s =~ /Precision\s+:\s*(\d+)-bit/ v = $1.to_i return v if v > 0 @@ -44,9 +42,9 @@ class DTAS::Source::Sox # :nodoc: @format ||= begin fmt = DTAS::Format.new path = @infile - fmt.channels = qx(%W(soxi -c #{path})).to_i - fmt.type = qx(%W(soxi -t #{path})).strip - fmt.rate = qx(%W(soxi -r #{path})).to_i + fmt.channels = qx(@env, %W(soxi -c #{path})).to_i + fmt.type = qx(@env, %W(soxi -t #{path})).strip + fmt.rate = qx(@env, %W(soxi -r #{path})).to_i fmt.bits ||= precision fmt end @@ -55,7 +53,7 @@ class DTAS::Source::Sox # :nodoc: # This is the number of samples according to the samples in the source # file itself, not the decoded output def samples - @samples ||= qx(%W(soxi -s #@infile)).to_i + @samples ||= qx(@env, %W(soxi -s #@infile)).to_i rescue => e warn e.message 0 @@ -69,7 +67,7 @@ class DTAS::Source::Sox # :nodoc: err = "" cmd = %W(soxi -a #@infile) begin - qx(cmd, err_str: err).split(/\n/).each do |line| + qx(@env, cmd, err_str: err).split(/\n/).each do |line| key, value = line.split(/=/, 2) key && value or next # TODO: multi-line/multi-value/repeated tags @@ -88,15 +86,15 @@ class DTAS::Source::Sox # :nodoc: def spawn(player_format, rg_state, opts) raise "BUG: #{self.inspect}#spawn called twice" if @to_io - e = player_format.to_env + e = @env.merge!(player_format.to_env) e["INFILE"] = @infile # make sure these are visible to the "current" command... - @env["TRIMFX"] = @offset ? "trim #@offset" : nil - @env["RGFX"] = rg_state.effect(self) || nil + e["TRIMFX"] = @offset ? "trim #@offset" : nil + e["RGFX"] = rg_state.effect(self) || nil e.merge!(@rg.to_env) if @rg - @pid = dtas_spawn(e.merge!(@env), command_string, opts) + @pid = dtas_spawn(e, command_string, opts) end def to_hsh diff --git a/test/test_rg_integration.rb b/test/test_rg_integration.rb index 79a6563..3b78e07 100644 --- a/test/test_rg_integration.rb +++ b/test/test_rg_integration.rb @@ -41,7 +41,7 @@ class TestRgIntegration < Minitest::Unit::TestCase end while cur["current_offset"] == 0 && sleep(0.01) end - assert_nil cur["current"]["env"]["RGFX"] + assert_empty cur["current"]["env"]["RGFX"] assert_equal DTAS::Format.new.rate * len, cur["current_expect"] diff --git a/test/test_source_av.rb b/test/test_source_av.rb index 4dba559..78015b3 100644 --- a/test/test_source_av.rb +++ b/test/test_source_av.rb @@ -33,7 +33,7 @@ class TestSourceAv < Minitest::Unit::TestCase x(%W(metaflac --set-tag=FOO=BAR #{tmp.path})) x(%W(metaflac --add-replay-gain #{tmp.path})) - source = DTAS::Source::Av.new(tmp.path) + source = DTAS::Source::Av.new.try(tmp.path) assert_equal source.comments["FOO"], "BAR", source.inspect rg = source.replaygain assert_kind_of DTAS::ReplayGain, rg @@ -60,7 +60,7 @@ class TestSourceAv < Minitest::Unit::TestCase end end - source = DTAS::Source::Av.new(a.path) + source = DTAS::Source::Av.new.try(a.path) rg = source.replaygain assert_kind_of DTAS::ReplayGain, rg assert_in_delta 0.0, rg.track_peak.to_f, 0.00000001 @@ -71,31 +71,31 @@ class TestSourceAv < Minitest::Unit::TestCase def test_offset tmp = new_file('flac') or return - source = DTAS::Source::Av.new(*%W(#{tmp.path} 5s)) + source = DTAS::Source::Av.new.try(*%W(#{tmp.path} 5s)) assert_equal 5, source.offset_samples - source = DTAS::Source::Av.new(*%W(#{tmp.path} 1:00:00.5)) + source = DTAS::Source::Av.new.try(*%W(#{tmp.path} 1:00:00.5)) expect = 1 * 60 * 60 * 44100 + (44100/2) assert_equal expect, source.offset_samples - source = DTAS::Source::Av.new(*%W(#{tmp.path} 1:10.5)) + source = DTAS::Source::Av.new.try(*%W(#{tmp.path} 1:10.5)) expect = 1 * 60 * 44100 + (10 * 44100) + (44100/2) assert_equal expect, source.offset_samples - source = DTAS::Source::Av.new(*%W(#{tmp.path} 10.03)) + source = DTAS::Source::Av.new.try(*%W(#{tmp.path} 10.03)) expect = (10 * 44100) + (44100 * 3/100.0) assert_equal expect, source.offset_samples end def test_offset_us tmp = new_file('flac') or return - source = DTAS::Source::Av.new(*%W(#{tmp.path} 441s)) + source = DTAS::Source::Av.new.try(*%W(#{tmp.path} 441s)) assert_equal 10000.0, source.offset_us - source = DTAS::Source::Av.new(*%W(#{tmp.path} 22050s)) + source = DTAS::Source::Av.new.try(*%W(#{tmp.path} 22050s)) assert_equal 500000.0, source.offset_us - source = DTAS::Source::Av.new(tmp.path, '1') + source = DTAS::Source::Av.new.try(tmp.path, '1') assert_equal 1000000.0, source.offset_us end end diff --git a/test/test_source_sox.rb b/test/test_source_sox.rb index f2e0699..4dec808 100644 --- a/test/test_source_sox.rb +++ b/test/test_source_sox.rb @@ -31,7 +31,7 @@ class TestSource < Minitest::Unit::TestCase return if `which metaflac`.strip.size == 0 tmp = new_file('flac') or return - source = DTAS::Source::Sox.new(tmp.path) + source = DTAS::Source::Sox.new.try(tmp.path) x(%W(metaflac --set-tag=FOO=BAR #{tmp.path})) x(%W(metaflac --add-replay-gain #{tmp.path})) assert_equal source.comments["FOO"], "BAR" @@ -48,7 +48,7 @@ class TestSource < Minitest::Unit::TestCase a = new_file('mp3') or return b = new_file('mp3') or return - source = DTAS::Source::Sox.new(a.path) + source = DTAS::Source::Sox.new.try(a.path) # redirect stdout to /dev/null temporarily, mp3gain is noisy File.open("/dev/null", "w") do |null| @@ -72,31 +72,31 @@ class TestSource < Minitest::Unit::TestCase def test_offset tmp = new_file('sox') or return - source = DTAS::Source::Sox.new(*%W(#{tmp.path} 5s)) + source = DTAS::Source::Sox.new.try(*%W(#{tmp.path} 5s)) assert_equal 5, source.offset_samples - source = DTAS::Source::Sox.new(*%W(#{tmp.path} 1:00:00.5)) + source = DTAS::Source::Sox.new.try(*%W(#{tmp.path} 1:00:00.5)) expect = 1 * 60 * 60 * 44100 + (44100/2) assert_equal expect, source.offset_samples - source = DTAS::Source::Sox.new(*%W(#{tmp.path} 1:10.5)) + source = DTAS::Source::Sox.new.try(*%W(#{tmp.path} 1:10.5)) expect = 1 * 60 * 44100 + (10 * 44100) + (44100/2) assert_equal expect, source.offset_samples - source = DTAS::Source::Sox.new(*%W(#{tmp.path} 10.03)) + source = DTAS::Source::Sox.new.try(*%W(#{tmp.path} 10.03)) expect = (10 * 44100) + (44100 * 3/100.0) assert_equal expect, source.offset_samples end def test_offset_us tmp = new_file('sox') or return - source = DTAS::Source::Sox.new(*%W(#{tmp.path} 441s)) + source = DTAS::Source::Sox.new.try(*%W(#{tmp.path} 441s)) assert_equal 10000.0, source.offset_us - source = DTAS::Source::Sox.new(*%W(#{tmp.path} 22050s)) + source = DTAS::Source::Sox.new.try(*%W(#{tmp.path} 22050s)) assert_equal 500000.0, source.offset_us - source = DTAS::Source::Sox.new(tmp.path, '1') + source = DTAS::Source::Sox.new.try(tmp.path, '1') assert_equal 1000000.0, source.offset_us end @@ -106,7 +106,7 @@ class TestSource < Minitest::Unit::TestCase cmd = %W(sox -r 96000 -b 24 -c 2 -n #{tmp.path} trim 0 1) system(*cmd) assert $?.success?, "#{cmd.inspect} failed: #$?" - fmt = DTAS::Source::Sox.new(tmp.path).format + fmt = DTAS::Source::Sox.new.try(tmp.path).format assert_equal 96000, fmt.rate assert_equal 2, fmt.channels tmp.unlink -- cgit v1.2.3-24-ge0c7