everything related to duct tape audio suite (dtas)
 help / color / mirror / code / Atom feed
9d3312baa8e3110b4a9d8f3e79c9b55b70ec5af5 blob 4871 bytes (raw)

  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-2015 all contributors <dtas-all@nongnu.org>
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
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
  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)
    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"] = @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, "" ]
      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 = $1
      key.freeze
      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
debug log:

solving 9d3312b ...
found 9d3312b in dtas.git

everything related to duct tape audio suite (dtas)

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://80x24.org/dtas-all
	git clone --mirror http://ou63pmih66umazou.onion/dtas-all

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V1 dtas-all dtas-all/ https://80x24.org/dtas-all \
		dtas-all@nongnu.org
	public-inbox-index dtas-all

Example config snippet for mirrors.
Newsgroups are available over NNTP:
	nntp://news.public-inbox.org/inbox.comp.audio.dtas
	nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.audio.dtas
 note: .onion URLs require Tor: https://www.torproject.org/

code repositories for project(s) associated with this inbox:

	../../../dtas.git

AGPL code for this site: git clone http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/public-inbox.git