1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
| | # -*- encoding: binary -*-
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
# 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'
# 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...
module DTAS::Source::AvFfCommon # :nodoc:
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
rescue
0
end
def to_hsh
sd = source_defaults
to_hash.delete_if { |k,v| v == sd[k] }
end
end
|