about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--lib/dtas/player.rb11
-rw-r--r--lib/dtas/process.rb3
-rw-r--r--lib/dtas/source/av.rb88
-rw-r--r--lib/dtas/source/sox.rb40
-rw-r--r--test/test_rg_integration.rb2
-rw-r--r--test/test_source_av.rb18
-rw-r--r--test/test_source_sox.rb20
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