From bc00dd1b3fee22bd91bd713e697b8b31be978d4e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 26 Aug 2013 08:26:58 +0000 Subject: add lightly-tested ffmpeg support Since ffmpeg/ffprobe are wrappers around their libav-variants, I haven't had the chance to actually test with "real" ffmpeg, but the usage is probably similar enough to not matter. --- Documentation/dtas-sourceedit.txt | 6 +- lib/dtas/player.rb | 2 + lib/dtas/source/av.rb | 123 ++------------------------------------ lib/dtas/source/av_ff_common.rb | 123 ++++++++++++++++++++++++++++++++++++++ lib/dtas/source/ff.rb | 30 ++++++++++ test/test_player_integration.rb | 6 +- 6 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 lib/dtas/source/av_ff_common.rb create mode 100644 lib/dtas/source/ff.rb diff --git a/Documentation/dtas-sourceedit.txt b/Documentation/dtas-sourceedit.txt index 6ef876c..2a80dce 100644 --- a/Documentation/dtas-sourceedit.txt +++ b/Documentation/dtas-sourceedit.txt @@ -7,7 +7,7 @@ dtas-sourceedit - edit parameters of a source decoder # SYNOPSYS -dtas-sourceedit {sox | av} +dtas-sourceedit {sox | av | ff} # DESCRIPTION @@ -24,6 +24,10 @@ To change the way dtas-player calls avconv (part of libav): $ dtas-sourceedit av +To change the way dtas-player calls ffmpeg (lightly-tested): + + $ dtas-sourceedit ff + # ENVIRONMENT VISUAL / EDITOR - your favorite *nix text editor, defaults to 'vi' if unset. diff --git a/lib/dtas/player.rb b/lib/dtas/player.rb index d865239..0e7a4b5 100644 --- a/lib/dtas/player.rb +++ b/lib/dtas/player.rb @@ -7,6 +7,7 @@ require_relative '../dtas' require_relative 'source' require_relative 'source/sox' require_relative 'source/av' +require_relative 'source/ff' require_relative 'source/cmd' require_relative 'sink' require_relative 'unix_server' @@ -41,6 +42,7 @@ class DTAS::Player # :nodoc: @source_map = { "sox" => DTAS::Source::Sox.new, "av" => DTAS::Source::Av.new, + "ff" => DTAS::Source::Ff.new, } source_map_reload end diff --git a/lib/dtas/source/av.rb b/lib/dtas/source/av.rb index 61d88b2..403048c 100644 --- a/lib/dtas/source/av.rb +++ b/lib/dtas/source/av.rb @@ -2,136 +2,25 @@ # Copyright (C) 2013, Eric Wong # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) require_relative '../../dtas' -require_relative '../source' -require_relative '../replaygain' +require_relative 'av_ff_common' # this is usually one input file class DTAS::Source::Av # :nodoc: - require_relative 'file' - - include DTAS::Source::File - - AStream = Struct.new(:duration, :channels, :rate) + include DTAS::Source::AvFfCommon AV_DEFAULTS = COMMAND_DEFAULTS.merge( "command" => 'avconv -v error $SSPOS -i "$INFILE" $AMAP -f sox - |' \ 'sox -p $SOXFMT - $RGFX', + + # this is above ffmpeg because this av is the Debian default and + # it's easier for me to test av than ff "tryorder" => 1, ) - attr_reader :precision # always 32 - attr_reader :format - def initialize command_init(AV_DEFAULTS) - @precision = 32 # this still goes through sox, which is 32-bit - end - - def try(infile, offset = nil) - rv = source_file_dup(infile, offset) - rv.av_ok? or return - rv - end - - def av_ok? - @duration = nil - @format = DTAS::Format.new - @format.bits = @precision - @comments = {} - @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 - if stream =~ /^codec_type=audio$/ - as = AStream.new - index = nil - stream =~ /^index=(\d+)\s*$/m and index = $1.to_i - 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 - 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 - ! @astreams.empty? - 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 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 - - # 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, i| - if as && as.channels == player_format.channels - select_astream(as) - amap = "-map 0:#{i}" - end - end - - # 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 - e["SSPOS"] = @offset ? sspos(@offset) : nil - e["RGFX"] = rg_state.effect(self) || nil - e.merge!(@rg.to_env) if @rg - - @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 - @samples ||= (@duration * @format.rate).round - end - - def to_hsh - to_hash.delete_if { |k,v| v == AV_DEFAULTS[k] } + @av_ff_probe = "avprobe" end def source_defaults diff --git a/lib/dtas/source/av_ff_common.rb b/lib/dtas/source/av_ff_common.rb new file mode 100644 index 0000000..08f5b0c --- /dev/null +++ b/lib/dtas/source/av_ff_common.rb @@ -0,0 +1,123 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative '../../dtas' +require_relative '../source' +require_relative '../replaygain' +require_relative 'file' + +module DTAS::Source::AvFfCommon + include DTAS::Source::File + AStream = Struct.new(:duration, :channels, :rate) + AV_FF_TRYORDER = 1 + + attr_reader :precision # always 32 + attr_reader :format + + def try(infile, offset = nil) + rv = source_file_dup(infile, offset) + rv.av_ff_ok? or return + rv + end + + def av_ff_ok? + @duration = nil + @format = DTAS::Format.new + @format.bits = 32 # always, since we still use the "sox" format + @comments = {} + @astreams = [] + cmd = %W(#@av_ff_probe -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 + if stream =~ /^codec_type=audio$/ + as = AStream.new + index = nil + stream =~ /^index=(\d+)\s*$/m and index = $1.to_i + 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 + 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 + ! @astreams.empty? + 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 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 + + # 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, i| + if as && as.channels == player_format.channels + select_astream(as) + amap = "-map 0:#{i}" + end + end + + # 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 + e["SSPOS"] = @offset ? sspos(@offset) : nil + e["RGFX"] = rg_state.effect(self) || nil + e.merge!(@rg.to_env) if @rg + + @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 + @samples ||= (@duration * @format.rate).round + end + + def to_hsh + sd = source_defaults + to_hash.delete_if { |k,v| v == sd[k] } + end +end diff --git a/lib/dtas/source/ff.rb b/lib/dtas/source/ff.rb new file mode 100644 index 0000000..db0e609 --- /dev/null +++ b/lib/dtas/source/ff.rb @@ -0,0 +1,30 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative '../../dtas' +require_relative 'av_ff_common' + +# ffmpeg support +# note: only tested with the compatibility wrapper in the Debian 7.0 package +# (so still using avconv/avprobe) +class DTAS::Source::Ff # :nodoc: + include DTAS::Source::AvFfCommon + + FF_DEFAULTS = COMMAND_DEFAULTS.merge( + "command" => + 'ffmpeg -v error $SSPOS -i "$INFILE" $AMAP -f sox - |' \ + 'sox -p $SOXFMT - $RGFX', + + # I haven't tested this much since av is in Debian stable and ff is not + "tryorder" => 2, + ) + + def initialize + command_init(FF_DEFAULTS) + @av_ff_probe = "ffprobe" + end + + def source_defaults + FF_DEFAULTS + end +end diff --git a/test/test_player_integration.rb b/test/test_player_integration.rb index 757ed35..23a73ab 100644 --- a/test/test_player_integration.rb +++ b/test/test_player_integration.rb @@ -191,11 +191,11 @@ class TestPlayerIntegration < Minitest::Unit::TestCase def test_source_ed s = client_socket - assert_equal "sox av", s.req("source ls") + assert_equal "sox av ff", s.req("source ls") s.req_ok("source ed av tryorder=-1") - assert_equal "av sox", s.req("source ls") + assert_equal "av sox ff", s.req("source ls") s.req_ok("source ed av tryorder=") - assert_equal "sox av", s.req("source ls") + assert_equal "sox av ff", s.req("source ls") s.req_ok("source ed sox command=true") sox = YAML.load(s.req("source cat sox")) -- cgit v1.2.3-24-ge0c7