Module: DTAS::Source::AvFfCommon

Includes:
File, XS
Included in:
Av, Ff
Defined in:
lib/dtas/source/av_ff_common.rb

Overview

Common code for libav (avconv/avprobe) and ffmpeg (and ffprobe) TODO: newer versions of both *probes support JSON, which will be easier to parse. However, the packaged libav version in Debian 7.0 does not support JSON, so we have an ugly parser…

Defined Under Namespace

Classes: AStream

Constant Summary collapse

AV_FF_TRYORDER =
1

Constants included from File

File::FILE_SIVS, File::SRC_SIVS

Constants included from Process

Process::PIDS

Constants included from Command

Command::COMMAND_DEFAULTS

Instance Attribute Summary collapse

Attributes included from File

#infile, #offset, #tryorder

Attributes included from Common

#dst, #dst_zero_byte, #requeued

Attributes included from Command

#command, #env, #pid, #spawn_at, #to_io

Instance Method Summary collapse

Methods included from XS

#xs

Methods included from File

#__file_init, #__offset_samples, #comments, #cuebreakpoints, #load!, #offset_samples, #offset_us, #replaygain, #samples!, #source_file_dup, #to_hash, #to_source_cat, #to_state_hash, #trimfx

Methods included from Mp3gain

#__mp3gain_peak, #mp3gain_comments

Methods included from Process

#dtas_spawn, #env_expand, #env_expand_ary, #env_expand_i, #qx, reaper

Methods included from DTAS::SpawnFix

#spawn

Methods included from Common

#dst_assoc

Methods included from Command

#command_init, #command_string, #on_death

Methods included from DTAS::Serialize

#ivars_to_hash

Instance Attribute Details

#durationObject (readonly)

Returns the value of attribute duration.


22
23
24
# File 'lib/dtas/source/av_ff_common.rb', line 22

def duration
  @duration
end

#formatObject (readonly)

Returns the value of attribute format.


21
22
23
# File 'lib/dtas/source/av_ff_common.rb', line 21

def format
  @format
end

#precisionObject (readonly)

always 32


20
21
22
# File 'lib/dtas/source/av_ff_common.rb', line 20

def precision
  @precision
end

Instance Method Details

#__parse_astream(cmd, stream) {|index, as| ... } ⇒ Object

Yields:

  • (index, as)

30
31
32
33
34
35
36
37
38
39
40
# File 'lib/dtas/source/av_ff_common.rb', line 30

def __parse_astream(cmd, stream)
  stream =~ /^codec_type=audio$/ or return
  as = AStream.new
  index = nil
  stream =~ /^index=(\d+)\s*$/nm and index = $1.to_i
  stream =~ /^duration=([\d\.]+)\s*$/nm and as.duration = $1.to_f
  stream =~ /^channels=(\d)\s*$/nm and as.channels = $1.to_i
  stream =~ /^sample_rate=([\d\.]+)\s*$/nm and as.rate = $1.to_i
  index or raise "BUG: no audio index from #{xs(cmd)}"
  yield(index, as)
end

#amap_fallbackObject


158
159
160
161
162
163
164
165
166
# File 'lib/dtas/source/av_ff_common.rb', line 158

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:#{index}"
  end
  raise "BUG: no audio stream in #@infile"
end

#av_ff_ok?Boolean

Returns:

  • (Boolean)

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/dtas/source/av_ff_common.rb', line 48

def av_ff_ok?
  @duration = nil
  @format = DTAS::Format.new
  @format.bits = 32 # always, since we still use the "sox" format
  @comments = {}
  @astreams = []

  # needed for VOB and other formats which scatter metadata all over the
  # place and
  @probe_harder = nil
  incomplete = []
  prev_cmd = []

  begin # loop
    cmd = %W(#@av_ff_probe)

    # using the max known duration as a analyzeduration seems to work
    # for the few VOBs I've tested, but seeking is still broken.
    max_duration = 0
    incomplete.each do |as|
      as && as.duration or next
      max_duration = as.duration if as.duration > max_duration
    end
    if max_duration > 0
      usec = max_duration.round * 1000000
      usec = "2G" if usec >= 0x7fffffff # limited to INT_MAX :<
      @probe_harder = %W(-analyzeduration #{usec} -probesize 2G)
      cmd.concat(@probe_harder)
    end
    cmd.concat(%W(-show_streams -show_format #@infile))
    break if cmd == prev_cmd

    err = "".b
    begin
      s = qx(@env, cmd, err_str: err, no_raise: true)
    rescue Errno::ENOENT # avprobe/ffprobe not installed
      return false
    end
    return false unless probe_ok?(s, err)

    # old avprobe
    [ %r{^\[STREAM\]\n(.*?)\n\[/STREAM\]\n}mn,
      %r{^\[streams\.stream\.\d+\]\n(.*?)\n\n}mn ].each do |re|
      s.scan(re) do |_|
        __parse_astream(cmd, $1) do |index, as|
          # incomplete streams may have zero channels
          if as.channels > 0 && as.rate > 0
            @astreams[index] = as
            incomplete[index] = nil
          else
            incomplete[index] = as
          end
        end
      end
    end

    prev_cmd = cmd
  end while incomplete.compact[0]

  # old avprobe
  s.scan(%r{^\[FORMAT\]\n(.*?)\n\[/FORMAT\]\n}m) do |_|
    f = $1.dup
    f =~ /^duration=([\d\.]+)\s*$/nm and @duration = $1.to_f
    # TODO: multi-line/multi-value/repeated tags
    f.gsub!(/^TAG:([^=]+)=(.*)$/ni) { |_|
      @comments[DTAS.dedupe_str($1.upcase)] = DTAS.dedupe_str($2)
    }
  end

  # new avprobe
  s.scan(%r{^\[format\.tags\]\n(.*?)\n\n}m) do |_|
    f = $1.dup
    f.gsub!(/^([^=]+)=(.*)$/ni) { |_|
      @comments[DTAS.dedupe_str($1.upcase)] = DTAS.dedupe_str($2)
    }
  end
  s.scan(%r{^\[format\]\n(.*?)\n\n}m) do |_|
    f = $1.dup
    f =~ /^duration=([\d\.]+)\s*$/nm and @duration = $1.to_f
  end

  ! @astreams.compact.empty?
end

#av_ff_trimfxObject

for sox


138
139
140
141
142
143
144
145
146
# File 'lib/dtas/source/av_ff_common.rb', line 138

def av_ff_trimfx # for sox
  return unless @trim
  tbeg, tlen = @trim # Floats
  tend = tbeg + tlen
  off = offset_samples / @format.rate.to_f
  tlen = tend - off
  tlen = 0 if tlen < 0
  sprintf('trim 0 %0.9g', tlen)
end

#probe_ok?(status, err_str) ⇒ Boolean

Returns:

  • (Boolean)

42
43
44
45
46
# File 'lib/dtas/source/av_ff_common.rb', line 42

def probe_ok?(status, err_str)
  return false if Process::Status === status
  return false if err_str =~ /Unable to find a suitable output format for/
  true
end

#samplesObject

This is the number of samples according to the samples in the source file itself, not the decoded output


201
202
203
204
205
# File 'lib/dtas/source/av_ff_common.rb', line 201

def samples
  @samples ||= (@duration * @format.rate).round
rescue
  0
end

#select_astream(as) ⇒ Object


148
149
150
151
152
153
154
155
156
# File 'lib/dtas/source/av_ff_common.rb', line 148

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

#src_spawn(player_format, rg_state, opts) ⇒ Object


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/dtas/source/av_ff_common.rb', line 168

def src_spawn(player_format, rg_state, opts)
  raise "BUG: #{self.inspect}#src_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)

  e["PROBE"] = @probe_harder ? @probe_harder.join(' ') : nil
  # make sure these are visible to the source command...
  e["INFILE"] = xs(@infile)
  e["AMAP"] = amap
  e["SSPOS"] = sspos
  e["RGFX"] = rg_state.effect(self) || nil
  e["TRIMFX"] = av_ff_trimfx
  e.merge!(@rg.to_env) if @rg

  @pid = dtas_spawn(e, command_string, opts)
end

#ssposObject


132
133
134
135
136
# File 'lib/dtas/source/av_ff_common.rb', line 132

def sspos
  return unless @offset || @trim
  off = offset_samples / @format.rate.to_f
  sprintf('-ss %0.9g', off)
end

#to_hshObject


207
208
209
210
# File 'lib/dtas/source/av_ff_common.rb', line 207

def to_hsh
  sd = source_defaults
  to_hash.delete_if { |k,v| v == sd[k] }
end

#try(infile, offset = nil, trim = nil) ⇒ Object


24
25
26
27
28
# File 'lib/dtas/source/av_ff_common.rb', line 24

def try(infile, offset = nil, trim = nil)
  rv = source_file_dup(infile, offset, trim)
  rv.av_ff_ok? or return
  rv
end