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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
| | # -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <dtas-all@nongnu.org>
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
# frozen_string_literal: true
require_relative '../dtas'
require_relative 'xs'
require_relative 'process'
require_relative 'sigevent'
# backend for the dtas-partstats(1) command
# Unlike the stuff for dtas-player, dtas-partstats is fairly tied to sox
class DTAS::PartStats # :nodoc:
CMD = 'sox "$INFILE" -n $TRIMFX $SOXFX stats $STATSOPTS'
include DTAS::Process
include DTAS::SpawnFix
attr_reader :key_idx
attr_reader :key_width
class TrimPart < Struct.new(:tbeg, :tlen, :rate) # :nodoc:
def sec
tbeg / rate
end
def hhmmss
Time.at(sec).strftime("%H:%M:%S")
end
end
def initialize(infile)
@infile = infile
%w(samples rate channels).each do |iv|
sw = iv[0] # -s, -r, -c
i = qx(%W(soxi -#{sw} #@infile)).to_i
raise ArgumentError, "invalid #{iv}: #{i}" if i <= 0
instance_variable_set("@#{iv}", i)
end
# "Pk lev dB" => 1, "RMS lev dB" => 2, ...
@key_nr = 0
@key_idx = Hash.new { |h,k| h[k] = (@key_nr += 1) }
@key_width = {}
end
def partitions(chunk_sec)
n = 0
part_samples = chunk_sec * @rate
rv = []
begin
rv << TrimPart.new(n, part_samples, @rate)
n += part_samples
end while n < @samples
rv
end
def partstats_spawn(trim_part, opts)
rd, wr = IO.pipe
env = opts[:env]
env = env ? env.dup : {}
env["INFILE"] = xs(@infile)
env["TRIMFX"] = "trim #{trim_part.tbeg}s #{trim_part.tlen}s"
opts = { pgroup: true, close_others: true, err: wr }
pid = spawn(env, CMD, opts)
wr.close
[ pid, rd ]
end
def run(opts = {})
sev = DTAS::Sigevent.new
trap(:CHLD) { sev.signal }
jobs = opts[:jobs] || 2
pids = {}
rset = {}
stats = []
fails = []
do_spawn = lambda do |trim_part|
pid, rpipe = partstats_spawn(trim_part, opts)
rset[rpipe] = [ trim_part, ''.b ]
pids[pid] = [ trim_part, rpipe ]
end
parts = partitions(opts[:chunk_length] || 10)
jobs.times do
trim_part = parts.shift or break
do_spawn.call(trim_part)
end
rset[sev] = true
while pids.size > 0
r = IO.select(rset.keys) or next
r[0].each do |rd|
if DTAS::Sigevent === rd
rd.readable_iter do |_,_|
begin
pid, status = Process.waitpid2(-1, Process::WNOHANG)
pid or break
done = pids.delete(pid)
done_part = done[0]
if status.success?
trim_part = parts.shift and do_spawn.call(trim_part)
puts "DONE #{done_part}" if $DEBUG
else
fails << [ done_part, status ]
end
rescue Errno::ECHILD
break
end while true
end
else
# spurious wakeup should not happen on local pipes,
# so readpartial should be safe
trim_part, buf = rset[rd]
begin
buf << rd.readpartial(666)
rescue EOFError
rset.delete(rd)
rd.close
parse_stats(stats, trim_part, buf)
end
end
end
end
return stats if fails.empty? && parts.empty?
fails.each do |(trim_part,status)|
warn "FAIL #{status.inspect} #{trim_part}"
end
false
ensure
sev.close
end
# "sox INFILE -n stats" example output
=begin
Overall Left Right
DC offset 0.001074 0.000938 0.001074
Min level -0.997711 -0.997711 -0.997711
Max level 0.997681 0.997681 0.997681
Pk lev dB -0.02 -0.02 -0.02
RMS lev dB -10.38 -9.90 -10.92
RMS Pk dB -4.62 -4.62 -5.10
RMS Tr dB -87.25 -86.58 -87.25
Crest factor - 3.12 3.51
Flat factor 19.41 19.66 18.89
Pk count 117k 156k 77.4k
Bit-depth 16/16 16/16 16/16
Num samples 17.2M
Length s 389.373
Scale max 1.000000
Window s 0.050
becomes:
[
TrimPart,
[ -0.02, -0.02, -0.02 ], # Pk lev dB
[ -10.38, -9.90, -10.92 ], # RMS lev dB
...
]
=end
def parse_stats(stats, trim_part, buf)
trim_row = [ trim_part ]
buf.split("\n").each do |line|
do_map = true
case line
when /\A(\S+ \S+ dB)\s/, /\A(Crest factor)\s+-\s/
nshift = 3
when /\A(Flat factor)\s/
nshift = 2
when /\A(Pk count)\s/
nshift = 2
do_map = false
else
next
end
key = DTAS.dedupe_str($1)
key_idx = @key_idx[key]
parts = line.split(/\s+/)
nshift.times { parts.shift } # remove stuff we don't need
@key_width[key] = parts.size
trim_row[key_idx] = do_map ? parts.map!(&:to_f) : parts
end
stats[trim_part.tbeg / trim_part.tlen] = trim_row
end
end
|