about summary refs log tree commit homepage
path: root/lib/dtas
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dtas')
-rw-r--r--lib/dtas/player.rb21
-rw-r--r--lib/dtas/process.rb4
-rw-r--r--lib/dtas/source/av.rb97
-rw-r--r--lib/dtas/source/file.rb8
-rw-r--r--lib/dtas/source/sox.rb16
5 files changed, 137 insertions, 9 deletions
diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb
index 57976c2..99c7400 100644
--- a/lib/dtas/player.rb
+++ b/lib/dtas/player.rb
@@ -6,6 +6,7 @@ require 'shellwords'
 require_relative '../dtas'
 require_relative 'source'
 require_relative 'source/sox'
+require_relative 'source/av'
 require_relative 'source/cmd'
 require_relative 'sink'
 require_relative 'unix_server'
@@ -297,6 +298,21 @@ class DTAS::Player # :nodoc:
     end
   end
 
+  def try_file(*args)
+    [ DTAS::Source::Sox, DTAS::Source::Av ].each do |klass|
+      rv = klass.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
+      end
+    end
+    echo "idle"
+    nil
+  end
+
   def next_source(source_spec)
     @current = nil
     if source_spec
@@ -305,16 +321,17 @@ class DTAS::Player # :nodoc:
 
       case source_spec
       when String
-        @current = DTAS::Source::Sox.new(source_spec)
+        @current = try_file(source_spec)
         echo(%W(file #{@current.infile}))
       when Array
-        @current = DTAS::Source::Sox.new(*source_spec)
+        @current = try_file(*source_spec)
         echo(%W(file #{@current.infile} #{@current.offset_samples}s))
       else
         @current = DTAS::Source::Cmd.new(source_spec["command"])
         echo(%W(command #{@current.command_string}))
       end
 
+      # FIXME, support Av overrides
       if DTAS::Source::Sox === @current
         @current.command = @srccmd if @srccmd
         @current.env = @srcenv.dup unless @srcenv.empty?
diff --git a/lib/dtas/process.rb b/lib/dtas/process.rb
index 2806fbf..2d7dcb0 100644
--- a/lib/dtas/process.rb
+++ b/lib/dtas/process.rb
@@ -84,4 +84,8 @@ module DTAS::Process # :nodoc:
     return res if status.success?
     raise RuntimeError, "`#{xs(cmd)}' failed: #{status.inspect}"
   end
+
+  # XXX only for DTAS::Source::{Sox,Av}.try
+  module_function :qx
+  module_function :xs
 end
diff --git a/lib/dtas/source/av.rb b/lib/dtas/source/av.rb
new file mode 100644
index 0000000..804a66b
--- /dev/null
+++ b/lib/dtas/source/av.rb
@@ -0,0 +1,97 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative '../../dtas'
+require_relative '../source'
+require_relative '../replaygain'
+
+# this is usually one input file
+class DTAS::Source::Av # :nodoc:
+  require_relative 'file'
+
+  include DTAS::Source::File
+
+  AV_DEFAULTS = COMMAND_DEFAULTS.merge(
+    "command" =>
+      'avconv -v error $SSPOS -i "$INFILE" -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: err)
+    return if err =~ /Unable to find a suitable output format for/
+    new(infile, offset)
+  rescue
+  end
+
+  def initialize(infile, offset = nil)
+    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
+    @duration = nil
+    @format = DTAS::Format.new
+    @format.bits = @precision
+    @comments = {}
+    err = ""
+    s = qx(%W(avprobe -show_streams -show_format #@infile), err: err)
+    s.scan(%r{^\[STREAM\]\n(.*?)\n\[/STREAM\]\n}m) do |_|
+      stream = $1
+      # XXX what to do about multiple streams?
+      if stream =~ /^codec_type=audio$/
+        duration = channels = rate = nil
+        stream =~ /^duration=([\d\.]+)\s*$/m and duration = $1.to_f
+        stream =~ /^channels=(\d)\s*$/m and channels = $1.to_i
+        stream =~ /^sample_rate=([\d\.]+)\s*$/m and rate = $1.to_i
+        if channels > 0 && rate > 0
+          @duration = duration
+          @format.channels = channels
+          @format.rate = rate
+        end
+      end
+    end
+    s.scan(%r{^\[FORMAT\]\n(.*?)\n\[/FORMAT\]\n}m) do |_|
+      f = $1
+      f =~ /^duration=([\d\.]+)\s*$/m and @duration ||= $1.to_f
+      # TODO: multi-line/multi-value/repeated tags
+      f.gsub!(/^TAG:([^=]+)=(.*)$/i) { |_| @comments[$1.upcase] = $2 }
+    end
+  end
+
+  def sspos(offset)
+    offset =~ /\A(\d+)s\z/ or return "-ss #{offset}"
+    samples = $1.to_f
+    sprintf("-ss %0.9g", samples / @format.rate)
+  end
+
+  def spawn(format, rg_state, opts)
+    raise "BUG: #{self.inspect}#spawn called twice" if @to_io
+    e = @format.to_env
+    e["INFILE"] = @infile
+
+    # make sure these are visible to the "current" command...
+    @env["SSPOS"] = @offset ? sspos(@offset) : nil
+    @env["RGFX"] = rg_state.effect(self) || nil
+    e.merge!(@rg.to_env) if @rg
+
+    @pid = dtas_spawn(e.merge!(@env), 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
+    @samples ||= (@duration * @format.rate).round
+  end
+
+  def to_hsh
+    to_hash.delete_if { |k,v| v == AV_DEFAULTS[k] }
+  end
+end
diff --git a/lib/dtas/source/file.rb b/lib/dtas/source/file.rb
index 472cb3d..a66308b 100644
--- a/lib/dtas/source/file.rb
+++ b/lib/dtas/source/file.rb
@@ -11,9 +11,11 @@ module DTAS::Source::File # :nodoc:
   attr_reader :infile
   attr_reader :offset
   require_relative 'common' # dtas/source/common
+  require_relative 'mp3gain'
   include DTAS::Command
   include DTAS::Process
   include DTAS::Source::Common
+  include DTAS::Source::Mp3gain
 
   FILE_SIVS = %w(infile comments command env)
 
@@ -60,4 +62,10 @@ module DTAS::Source::File # :nodoc:
     rv["samples"] = samples
     rv
   end
+
+  def replaygain
+    @rg ||= DTAS::ReplayGain.new(comments) ||
+            DTAS::ReplayGain.new(mp3gain_comments)
+  end
+
 end
diff --git a/lib/dtas/source/sox.rb b/lib/dtas/source/sox.rb
index 64ce095..954c1a5 100644
--- a/lib/dtas/source/sox.rb
+++ b/lib/dtas/source/sox.rb
@@ -8,16 +8,23 @@ require_relative '../replaygain'
 # this is usually one input file
 class DTAS::Source::Sox # :nodoc:
   require_relative 'file'
-  require_relative 'mp3gain'
 
   include DTAS::Source::File
-  include DTAS::Source::Mp3gain
 
   SOX_DEFAULTS = COMMAND_DEFAULTS.merge(
     "command" => 'exec sox "$INFILE" $SOXFMT - $TRIMFX $RGFX',
     "comments" => nil,
   )
 
+
+  def self.try(infile, offset = nil)
+    err = ""
+    DTAS::Process.qx(%W(soxi #{infile}), err: err)
+    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)
@@ -79,11 +86,6 @@ class DTAS::Source::Sox # :nodoc:
     tmp
   end
 
-  def replaygain
-    @rg = DTAS::ReplayGain.new(comments) ||
-          DTAS::ReplayGain.new(mp3gain_comments)
-  end
-
   def spawn(format, rg_state, opts)
     raise "BUG: #{self.inspect}#spawn called twice" if @to_io
     e = format.to_env