about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-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
4 files changed, 77 insertions, 65 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