Class: DTAS::PartStats

Inherits:
Object
  • Object
show all
Includes:
Process, SpawnFix
Defined in:
lib/dtas/partstats.rb

Overview

backend for the dtas-partstats(1) command Unlike the stuff for dtas-player, dtas-partstats is fairly tied to sox

Defined Under Namespace

Classes: TrimPart

Constant Summary collapse

CMD =

:nodoc:

'sox "$INFILE" -n $TRIMFX $SOXFX stats $STATSOPTS'

Constants included from Process

DTAS::Process::PIDS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from SpawnFix

#spawn

Methods included from Process

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

Methods included from XS

#xs

Constructor Details

#initialize(infile) ⇒ PartStats

Returns a new instance of PartStats.


29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/dtas/partstats.rb', line 29

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

Instance Attribute Details

#key_idxObject (readonly)

Returns the value of attribute key_idx.


16
17
18
# File 'lib/dtas/partstats.rb', line 16

def key_idx
  @key_idx
end

#key_widthObject (readonly)

Returns the value of attribute key_width.


17
18
19
# File 'lib/dtas/partstats.rb', line 17

def key_width
  @key_width
end

Instance Method Details

#parse_stats(stats, trim_part, buf) ⇒ Object

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
  ...
]

161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/dtas/partstats.rb', line 161

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

#partitions(chunk_sec) ⇒ Object


44
45
46
47
48
49
50
51
52
53
# File 'lib/dtas/partstats.rb', line 44

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

#partstats_spawn(trim_part, opts) ⇒ Object


55
56
57
58
59
60
61
62
63
64
65
# File 'lib/dtas/partstats.rb', line 55

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

#run(opts = {}) ⇒ Object


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
# File 'lib/dtas/partstats.rb', line 67

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