dtas 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a4ce17f3d71d013e20442d569236e8ea2b56a091
4
- data.tar.gz: f8177dd1cae23bbf4f679b8cfca1062f8583f5e2
3
+ metadata.gz: 95cea9c51fc4befa828c208dabf229b37dbc3582
4
+ data.tar.gz: 4bb3019027d4864ea6d6e9fd8026b236c2f0070f
5
5
  SHA512:
6
- metadata.gz: 7d57e1c9be990a92163771a89075cf0432cbe6f4441f4c79ba7efe09288a41fb76b055faf37339ab6de2247a89ad9b36a0e45da5deff39f14b376e417d5e11ca
7
- data.tar.gz: 5c3ae2d8e5d06f46afe06472b27c9de90c095e7651d69379d45b71589adb3e6d2fffdd24442b095f1e48b0335206ff98a52ff79edcd6845b92bef97fdee364d9
6
+ metadata.gz: 795146ad7d81aeabfa30ad36b1de0bf92402428f1c6388f9ba5e2e51df95a773feaf312d38889aa88ee9ab635d61e6b9f082518a03b9614636107b4ebb5dc331
7
+ data.tar.gz: 75076a786083803900a61adee4847326866f6e08cd8bf624c1b3b4fa021e1f80a991c37be78782caae3888ec5d3c26c6869f29de6bc016c5b65dd90c499e3a6c
@@ -4,7 +4,7 @@
4
4
  CONSTANT = "DTAS::VERSION"
5
5
  RVF = "lib/dtas/version.rb"
6
6
  GVF = "GIT-VERSION-FILE"
7
- DEF_VER = "v0.5.0"
7
+ DEF_VER = "v0.6.0"
8
8
  vn = DEF_VER
9
9
 
10
10
  # First see if there is a version file (included in release tarballs),
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # TODO
5
+ # - option parsing: sox effects, stats effect options
6
+ # - support piping out to external processes
7
+ # - configurable output formatting
8
+ # - Sequel/SQLite support
9
+ require 'dtas/partstats'
10
+ infile = ARGV[0] or abort "usage: #$0 INFILE"
11
+ ps = DTAS::PartStats.new(infile)
12
+ opts = {
13
+ jobs: `nproc 2>/dev/null || echo 2`.to_i
14
+ }
15
+ stats = ps.run(opts)
16
+
17
+ headers = ps.key_idx.to_a
18
+ headers = headers.sort_by! { |(n,i)| i }.map! { |(n,_)| n }
19
+ width = ps.key_width
20
+ print " time "
21
+ puts(headers.map do |h|
22
+ cols = width[h]
23
+ sprintf("% #{(cols * 6)+cols-1}s", h.tr(' ','_'))
24
+ end.join(" | "))
25
+
26
+ stats.each do |row|
27
+ trim_part = row.shift
28
+ print "#{trim_part.hhmmss} "
29
+ puts(row.map do |group|
30
+ group.map do |f|
31
+ case f
32
+ when Float
33
+ sprintf("% 6.2f", f)
34
+ else
35
+ sprintf("% 6s", f)
36
+ end
37
+ end.join(" ")
38
+ end.join(" | "))
39
+ end
@@ -56,7 +56,7 @@ when "reto"
56
56
  re = ignorecase ? %r{#{re}}i : %r{#{re}}
57
57
  get_track_ids(c).each do |track_id|
58
58
  res = c.req("tl get #{track_id}")
59
- res.sub!(/\A1 /, '')
59
+ res.sub!(/\A1 \d+=/, '')
60
60
  if re =~ res
61
61
  req = %W(tl goto #{track_id})
62
62
  req << time if time
@@ -0,0 +1,30 @@
1
+ # To the extent possible under law, Eric Wong has waived all copyright and
2
+ # related or neighboring rights to this example.
3
+ # Note: be sure to update test/test_trimfx.rb if you change this,
4
+ # test_trimfx.rb relies on this.
5
+ ---
6
+ infile: foo.flac
7
+ env:
8
+ PATH: $PATH
9
+ SOX_OPTS: $SOX_OPTS -R
10
+ I2: second.flac
11
+ I3: third.flac
12
+ comments:
13
+ ARTIST: John Smith
14
+ ALBUM: Hello World
15
+ YEAR: 2013
16
+ track_start: 1
17
+ effects:
18
+ # the fade parameter sets the default fade for every subsequent effect
19
+ - fade=t1,t1;t1,t1 # fade-out-prev,fade-in-main;fade-out-main,fade-in-next
20
+
21
+ # the following commands are equivalent
22
+ - trim 52 =53 sh sox $SOXIN $SOXOUT $TRIMFX vol -6dB $FADEFX
23
+ - trim 52 1 sox vol -6dB # shorthand
24
+
25
+ # as are the following (for little endian machines)
26
+ - trim 52 1 eca -eadb:-6
27
+ - trim 52 1 sh sox $SOXIN $SOX2ECA $TRIMFX | ecasound $ECAFMT
28
+ -i stdin -o stdout -eadb:-6 | sox $ECA2SOX - $SOXOUT $FADEFX
29
+ # SOX2ECA='-tf32 -c$CHANNELS -r$RATE'
30
+ # ECAFMT='-f32_le,$CHANNELS,$RATE
@@ -1,6 +1,8 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require 'io/wait'
3
4
  require_relative '../dtas'
5
+ require_relative 'compat_rbx' # IO#nread
4
6
 
5
7
  class DTAS::Buffer # :nodoc:
6
8
  begin
@@ -47,16 +49,13 @@ class DTAS::Buffer # :nodoc:
47
49
  # - some type of StandardError
48
50
  # - nil
49
51
  def broadcast(targets)
50
- bytes = inflight
51
- return :wait_readable if 0 == bytes # spurious wakeup
52
-
53
52
  case targets.size
54
53
  when 0
55
54
  :ignore # this will pause decoders
56
55
  when 1
57
- broadcast_one(targets, bytes)
56
+ broadcast_one(targets)
58
57
  else # infinity
59
- broadcast_inf(targets, bytes)
58
+ broadcast_inf(targets)
60
59
  end
61
60
  end
62
61
 
@@ -1,6 +1,5 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
- require 'io/wait'
4
3
  require 'io/nonblock'
5
4
  require_relative '../../dtas'
6
5
  require_relative '../pipe'
@@ -17,25 +16,30 @@ module DTAS::Buffer::ReadWrite # :nodoc:
17
16
  def discard(bytes)
18
17
  buf = _rbuf
19
18
  begin
20
- @to_io.read(bytes, buf) or break # EOF
19
+ @to_io.readpartial(bytes, buf)
21
20
  bytes -= buf.bytesize
21
+ rescue EOFError
22
+ return
22
23
  end until bytes == 0
23
24
  end
24
25
 
25
26
  # always block when we have a single target
26
- def broadcast_one(targets, bytes)
27
+ def broadcast_one(targets)
27
28
  buf = _rbuf
28
- @to_io.read(bytes, buf)
29
+ @to_io.readpartial(MAX_AT_ONCE, buf)
29
30
  n = targets[0].write(buf) # IO#write has write-in-full behavior
30
31
  @bytes_xfer += n
31
32
  :wait_readable
33
+ rescue EOFError
34
+ nil
32
35
  rescue Errno::EPIPE, IOError => e
33
36
  __dst_error(targets[0], e)
34
37
  targets.clear
35
38
  nil # do not return error here, we already spewed an error message
36
39
  end
37
40
 
38
- def broadcast_inf(targets, bytes)
41
+ def broadcast_inf(targets)
42
+ bytes = inflight
39
43
  nr_nb = targets.count { |sink| sink.nonblock? }
40
44
  if nr_nb == 0 || nr_nb == targets.size
41
45
  # if all targets are full, don't start until they're all writable
@@ -56,7 +60,8 @@ module DTAS::Buffer::ReadWrite # :nodoc:
56
60
  bytes = bytes > MAX_AT_ONCE ? MAX_AT_ONCE : bytes
57
61
  buf = _rbuf
58
62
  @to_io.read(bytes, buf)
59
- @bytes_xfer += buf.bytesize
63
+ n = buf.bytesize
64
+ @bytes_xfer += n
60
65
 
61
66
  targets.delete_if do |dst|
62
67
  begin
@@ -1,6 +1,5 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
- require 'io/wait'
4
3
  require 'io/nonblock'
5
4
  require 'io/splice'
6
5
  require_relative '../../dtas'
@@ -8,6 +7,7 @@ require_relative '../pipe'
8
7
 
9
8
  module DTAS::Buffer::Splice # :nodoc:
10
9
  MAX_AT_ONCE = 4096 # page size in Linux
10
+ MAX_AT_ONCE_1 = 65536
11
11
  MAX_SIZE = File.read("/proc/sys/fs/pipe-max-size").to_i
12
12
  DEVNULL = File.open("/dev/null", "r+")
13
13
  F_MOVE = IO::Splice::F_MOVE
@@ -28,9 +28,9 @@ module DTAS::Buffer::Splice # :nodoc:
28
28
  IO.splice(@to_io, nil, DEVNULL, nil, bytes)
29
29
  end
30
30
 
31
- def broadcast_one(targets, bytes)
31
+ def broadcast_one(targets)
32
32
  # single output is always non-blocking
33
- s = IO.trysplice(@to_io, nil, targets[0], nil, bytes, F_MOVE)
33
+ s = IO.trysplice(@to_io, nil, targets[0], nil, MAX_AT_ONCE_1, F_MOVE)
34
34
  if Symbol === s
35
35
  targets # our one and only target blocked on write
36
36
  else
@@ -48,11 +48,14 @@ module DTAS::Buffer::Splice # :nodoc:
48
48
  most_teed = 0
49
49
  targets.delete_if do |dst|
50
50
  begin
51
- t = dst.nonblock? ?
51
+ t = (dst.nonblock? || most_teed == 0) ?
52
52
  IO.trytee(@to_io, dst, chunk_size) :
53
53
  IO.tee(@to_io, dst, chunk_size, WAITALL)
54
54
  if Integer === t
55
- most_teed = t if t > most_teed
55
+ if t > most_teed
56
+ chunk_size = t if most_teed == 0
57
+ most_teed = t
58
+ end
56
59
  else
57
60
  blocked << dst
58
61
  end
@@ -65,7 +68,7 @@ module DTAS::Buffer::Splice # :nodoc:
65
68
  most_teed
66
69
  end
67
70
 
68
- def broadcast_inf(targets, bytes)
71
+ def broadcast_inf(targets)
69
72
  if targets.none? { |sink| sink.nonblock? }
70
73
  # if all targets are blocking, don't start until they're all writable
71
74
  r = IO.select(nil, targets, nil, 0) or return targets
@@ -80,9 +83,10 @@ module DTAS::Buffer::Splice # :nodoc:
80
83
  end
81
84
 
82
85
  # don't pin too much on one target
83
- bytes = bytes > MAX_AT_ONCE ? MAX_AT_ONCE : bytes
84
-
86
+ bytes = MAX_AT_ONCE
85
87
  last = targets.pop # we splice to the last one, tee to the rest
88
+
89
+ # this may return zero if all targets were non-blocking
86
90
  most_teed = __broadcast_tee(blocked, targets, bytes)
87
91
 
88
92
  # don't splice more than the largest amount we successfully teed
@@ -90,7 +94,7 @@ module DTAS::Buffer::Splice # :nodoc:
90
94
 
91
95
  begin
92
96
  targets << last
93
- if last.nonblock?
97
+ if last.nonblock? || most_teed == 0
94
98
  s = IO.trysplice(@to_io, nil, last, nil, bytes, F_MOVE)
95
99
  if Symbol === s
96
100
  blocked << last
@@ -0,0 +1,12 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+ # ref: https://github.com/rubysl/rubysl-io-wait/issues/1
5
+ # this ignores buffers and is Linux-only
6
+ class IO
7
+ def nread
8
+ buf = "\0" * 8
9
+ ioctl(0x541B, buf)
10
+ buf.unpack("l_")[0]
11
+ end
12
+ end if ! IO.method_defined?(:nread) && RUBY_PLATFORM =~ /linux/
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative '../dtas'
4
+ require_relative 'parse_time'
5
+
6
+ class DTAS::FadeFX
7
+ include DTAS::ParseTime
8
+ attr_reader :out_prev, :in_main, :out_main, :in_next
9
+ F = Struct.new(:type, :len)
10
+
11
+ def initialize(args)
12
+ args =~ /\Afade=([^,]*),([^,]*);([^,]*),([^,]*)\z/ or
13
+ raise ArgumentError, "bad fade format"
14
+ fades = [ $1, $2, $3, $4 ]
15
+ %w(out_prev in_main out_main in_next).each do |iv|
16
+ instance_variable_set("@#{iv}", parse!(fades.shift))
17
+ end
18
+ end
19
+
20
+ # q - quarter of a sine wave
21
+ # h - half a sine wave
22
+ # t - linear (`triangular') slope
23
+ # l - logarithmic
24
+ # p - inverted parabola
25
+ # default is 't' (sox defaults to 'l', but triangular makes more sense
26
+ # when concatenating
27
+ def parse!(str)
28
+ type = "t"
29
+ str.sub!(/\A([a-z])/, "") and type = $1
30
+ F[type, parse_time(str)]
31
+ end
32
+ end
@@ -35,6 +35,32 @@ class DTAS::Format # :nodoc:
35
35
  fmt
36
36
  end
37
37
 
38
+ # some of these are sox-only, but that's what we mainly care about
39
+ # for audio-editing. We only use ffmpeg/avconv for odd files during
40
+ # playback.
41
+
42
+ extend DTAS::Process
43
+
44
+ def self.precision(env, infile)
45
+ # sox.git f4562efd0aa3
46
+ qx(env, %W(soxi -p #{infile}), err: "/dev/null").to_i
47
+ rescue # fallback to parsing the whole output
48
+ s = qx(env, %W(soxi #{infile}), err: "/dev/null")
49
+ s =~ /Precision\s+:\s*(\d+)-bit/n
50
+ v = $1.to_i
51
+ return v if v > 0
52
+ raise TypeError, "could not determine precision for #{infile}"
53
+ end
54
+
55
+ def self.from_file(env, infile)
56
+ fmt = new
57
+ fmt.channels = qx(env, %W(soxi -c #{infile})).to_i
58
+ fmt.type = qx(env, %W(soxi -t #{infile})).strip
59
+ fmt.rate = qx(env, %W(soxi -r #{infile})).to_i
60
+ fmt.bits ||= precision(env, infile)
61
+ fmt
62
+ end
63
+
38
64
  def initialize
39
65
  FORMAT_DEFAULTS.each do |k,v|
40
66
  instance_variable_set("@#{k}", v)
@@ -0,0 +1,29 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative '../dtas'
4
+ module DTAS::ParseTime
5
+ def parse_time(time)
6
+ case time
7
+ when /\A\d+\z/
8
+ time.to_i
9
+ when /\A[\d\.]+\z/
10
+ time.to_f
11
+ when /\A[:\d\.]+\z/
12
+ hhmmss = time.dup
13
+ rv = hhmmss.sub!(/\.(\d+)\z/, "") ? "0.#$1".to_f : 0
14
+
15
+ # deal with HH:MM:SS
16
+ t = hhmmss.split(/:/)
17
+ raise ArgumentError, "Bad time format: #{hhmmss}" if t.size > 3
18
+
19
+ mult = 1
20
+ while part = t.pop
21
+ rv += part.to_i * mult
22
+ mult *= 60
23
+ end
24
+ rv
25
+ else
26
+ raise ArgumentError, "unparseable: #{time.inspect}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,187 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # Unlike the stuff for dtas-player, dtas-partstats is fairly tied to sox
5
+ require_relative '../dtas'
6
+ require_relative 'xs'
7
+ require_relative 'process'
8
+ require_relative 'sigevent'
9
+
10
+ class DTAS::PartStats
11
+ CMD = 'sox "$INFILE" -n $TRIMFX $SOXFX stats $STATSOPTS'
12
+ include DTAS::Process
13
+ attr_reader :key_idx
14
+ attr_reader :key_width
15
+
16
+ class TrimPart < Struct.new(:tbeg, :tlen, :rate)
17
+ def sec
18
+ tbeg / rate
19
+ end
20
+
21
+ def hhmmss
22
+ Time.at(sec).strftime("%H:%M:%S")
23
+ end
24
+ end
25
+
26
+ def initialize(infile)
27
+ @infile = infile
28
+ %w(samples rate channels).each do |iv|
29
+ sw = iv[0] # -s, -r, -c
30
+ i = qx(%W(soxi -#{sw} #@infile)).to_i
31
+ raise ArgumentError, "invalid #{iv}: #{i}" if i <= 0
32
+ instance_variable_set("@#{iv}", i)
33
+ end
34
+
35
+ # "Pk lev dB" => 1, "RMS lev dB" => 2, ...
36
+ @key_nr = 0
37
+ @key_idx = Hash.new { |h,k| h[k] = (@key_nr += 1) }
38
+ @key_width = {}
39
+ end
40
+
41
+ def partitions(chunk_sec)
42
+ n = 0
43
+ part_samples = chunk_sec * @rate
44
+ rv = []
45
+ begin
46
+ rv << TrimPart.new(n, part_samples, @rate)
47
+ n += part_samples
48
+ end while n < @samples
49
+ rv
50
+ end
51
+
52
+ def spawn(trim_part, opts)
53
+ rd, wr = IO.pipe
54
+ env = opts[:env]
55
+ env = env ? env.dup : {}
56
+ env["INFILE"] = @infile
57
+ env["TRIMFX"] = "trim #{trim_part.tbeg}s #{trim_part.tlen}s"
58
+ opts = { pgroup: true, close_others: true, err: wr }
59
+ pid = begin
60
+ Process.spawn(env, CMD, opts)
61
+ rescue Errno::EINTR # Ruby bug?
62
+ retry
63
+ end
64
+ wr.close
65
+ [ pid, rd ]
66
+ end
67
+
68
+ def run(opts = {})
69
+ sev = DTAS::Sigevent.new
70
+ trap(:CHLD) { sev.signal }
71
+ jobs = opts[:jobs] || 2
72
+ pids = {}
73
+ rset = {}
74
+ stats = []
75
+ fails = []
76
+ do_spawn = lambda do |trim_part|
77
+ pid, rpipe = spawn(trim_part, opts)
78
+ rset[rpipe] = [ trim_part, "" ]
79
+ pids[pid] = [ trim_part, rpipe ]
80
+ end
81
+
82
+ parts = partitions(opts[:chunk_length] || 10)
83
+ jobs.times do
84
+ trim_part = parts.shift or break
85
+ do_spawn.call(trim_part)
86
+ end
87
+
88
+ rset[sev] = true
89
+
90
+ while pids.size > 0
91
+ r = IO.select(rset.keys) or next
92
+ r[0].each do |rd|
93
+ if DTAS::Sigevent === rd
94
+ rd.readable_iter do |_,_|
95
+ begin
96
+ pid, status = Process.waitpid2(-1, Process::WNOHANG)
97
+ pid or break
98
+ done = pids.delete(pid)
99
+ done_part = done[0]
100
+ if status.success?
101
+ trim_part = parts.shift and do_spawn.call(trim_part)
102
+ puts "DONE #{done_part}" if $DEBUG
103
+ else
104
+ fails << [ done_part, status ]
105
+ end
106
+ rescue Errno::ECHILD
107
+ break
108
+ end while true
109
+ end
110
+ else
111
+ # spurious wakeup should not happen on local pipes,
112
+ # so readpartial should be safe
113
+ trim_part, buf = rset[rd]
114
+ begin
115
+ buf << rd.readpartial(666)
116
+ rescue EOFError
117
+ rset.delete(rd)
118
+ rd.close
119
+ parse_stats(stats, trim_part, buf)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ return stats if fails.empty? && parts.empty?
126
+ fails.each do |(trim_part,status)|
127
+ warn "FAIL #{status.inspect} #{trim_part}"
128
+ end
129
+ false
130
+ ensure
131
+ sev.close
132
+ end
133
+
134
+ # "sox INFILE -n stats" example output
135
+ =begin
136
+ Overall Left Right
137
+ DC offset 0.001074 0.000938 0.001074
138
+ Min level -0.997711 -0.997711 -0.997711
139
+ Max level 0.997681 0.997681 0.997681
140
+ Pk lev dB -0.02 -0.02 -0.02
141
+ RMS lev dB -10.38 -9.90 -10.92
142
+ RMS Pk dB -4.62 -4.62 -5.10
143
+ RMS Tr dB -87.25 -86.58 -87.25
144
+ Crest factor - 3.12 3.51
145
+ Flat factor 19.41 19.66 18.89
146
+ Pk count 117k 156k 77.4k
147
+ Bit-depth 16/16 16/16 16/16
148
+ Num samples 17.2M
149
+ Length s 389.373
150
+ Scale max 1.000000
151
+ Window s 0.050
152
+
153
+ becomes:
154
+ [
155
+ TrimPart,
156
+ [ -0.02, -0.02, -0.02 ], # Pk lev dB
157
+ [ -10.38, -9.90, -10.92 ], # RMS lev dB
158
+ ...
159
+ ]
160
+ =end
161
+
162
+ def parse_stats(stats, trim_part, buf)
163
+ trim_row = [ trim_part ]
164
+ buf.split(/\n/).each do |line|
165
+ do_map = true
166
+ case line
167
+ when /\A(\S+ \S+ dB)\s/, /\A(Crest factor)\s+-\s/
168
+ nshift = 3
169
+ when /\A(Flat factor)\s/
170
+ nshift = 2
171
+ when /\A(Pk count)\s/
172
+ nshift = 2
173
+ do_map = false
174
+ else
175
+ next
176
+ end
177
+ key = $1
178
+ key.freeze
179
+ key_idx = @key_idx[key]
180
+ parts = line.split(/\s+/)
181
+ nshift.times { parts.shift } # remove stuff we don't need
182
+ @key_width[key] = parts.size
183
+ trim_row[key_idx] = do_map ? parts.map!(&:to_f) : parts
184
+ end
185
+ stats[trim_part.tbeg / trim_part.tlen] = trim_row
186
+ end
187
+ end
@@ -22,6 +22,11 @@ class DTAS::Pipe < IO # :nodoc:
22
22
  def pipe_size=(_)
23
23
  end
24
24
  end
25
+
26
+ # avoid syscall, we never change IO#nonblock= directly
27
+ def nonblock?
28
+ false
29
+ end
25
30
  end
26
31
 
27
32
  # for non-blocking sinks, this avoids extra fcntl(..., F_GETFL) syscalls
@@ -165,9 +165,13 @@ class DTAS::Player # :nodoc:
165
165
  rv
166
166
  end
167
167
 
168
+ def need_to_queue
169
+ @current || @queue[0] || @paused
170
+ end
171
+
168
172
  def enq_handler(io, msg)
169
173
  # check @queue[0] in case we have no sinks
170
- if @current || @queue[0] || @paused
174
+ if need_to_queue
171
175
  @queue << msg
172
176
  else
173
177
  next_source(msg)
@@ -177,7 +181,7 @@ class DTAS::Player # :nodoc:
177
181
 
178
182
  def do_enq_head(io, msg)
179
183
  # check @queue[0] in case we have no sinks
180
- if @current || @queue[0] || @paused
184
+ if need_to_queue
181
185
  @queue.unshift(msg)
182
186
  else
183
187
  next_source(msg)
@@ -415,7 +419,6 @@ class DTAS::Player # :nodoc:
415
419
 
416
420
  def player_idle
417
421
  stop_sinks if @sink_buf.inflight == 0
418
- @tl.reset unless @paused
419
422
  wall("idle")
420
423
  end
421
424
 
@@ -344,7 +344,12 @@ module DTAS::Player::ClientHandler # :nodoc:
344
344
  # no wall, next_source will wall on new track
345
345
  @paused = false
346
346
  return if @current
347
- next_source(_next)
347
+ n = _next
348
+ unless n
349
+ @tl.reset
350
+ n = _next
351
+ end
352
+ next_source(n)
348
353
  end
349
354
 
350
355
  def do_play_pause
@@ -549,12 +554,11 @@ module DTAS::Player::ClientHandler # :nodoc:
549
554
  rescue ArgumentError => e
550
555
  return io.emit("ERR #{e.message}")
551
556
  end
552
- _tl_skip if set_as_current
553
557
 
554
- # start playing if we're the only track
555
- if @tl.size == 1 && !(@current || @queue[0] || @paused)
556
- next_source(_next)
557
- end
558
+ _tl_skip if set_as_current # if @current is playing, it will restart soon
559
+
560
+ # start playing if we're currently idle
561
+ next_source(_next) unless need_to_queue
558
562
  io.emit("#{track_id}")
559
563
  when "repeat"
560
564
  case msg.shift
@@ -568,15 +572,16 @@ module DTAS::Player::ClientHandler # :nodoc:
568
572
  when "remove"
569
573
  track_id = msg.shift or return io.emit("ERR track_id not specified")
570
574
  track_id = track_id.to_i
571
- cur = @tl.cur_track
575
+ @tl.remove_track(track_id) or return io.emit("MISSING")
572
576
 
573
577
  # skip if we're removing the currently playing track
574
- if cur.object_id == track_id && @current &&
575
- @current.respond_to?(:infile) && @current.infile == cur
578
+ if @current && @current.respond_to?(:infile) &&
579
+ @current.infile.object_id == track_id
576
580
  _tl_skip
577
581
  end
578
-
579
- io.emit(@tl.remove_track(track_id) ? "OK" : "MISSING")
582
+ # drop it from the queue, too, in case it just got requeued or paused
583
+ @queue.delete_if { |t| Array === t && t[0].object_id == track_id }
584
+ io.emit("OK")
580
585
  when "get"
581
586
  res = @tl.get_tracks(msg.map! { |i| i.to_i })
582
587
  res.map! { |tid, file| "#{tid}=#{file ? Shellwords.escape(file) : ''}" }
@@ -589,9 +594,7 @@ module DTAS::Player::ClientHandler # :nodoc:
589
594
  offset = msg.shift # may be nil
590
595
  if @tl.go_to(track_id.to_i, offset)
591
596
  _tl_skip
592
- if !(@current || @queue[0] || @paused)
593
- next_source(_next)
594
- end
597
+ next_source(_next) unless need_to_queue
595
598
  io.emit("OK")
596
599
  else
597
600
  io.emit("MISSING")
@@ -47,26 +47,8 @@ class DTAS::Source::Sox # :nodoc:
47
47
  source_file_dup(infile, offset)
48
48
  end
49
49
 
50
- def precision
51
- qx(@env, %W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3
52
- rescue # fallback to parsing the whole output
53
- s = qx(@env, %W(soxi #@infile), err: "/dev/null")
54
- s =~ /Precision\s+:\s*(\d+)-bit/n
55
- v = $1.to_i
56
- return v if v > 0
57
- raise TypeError, "could not determine precision for #@infile"
58
- end
59
-
60
50
  def format
61
- @format ||= begin
62
- fmt = DTAS::Format.new
63
- path = @infile
64
- fmt.channels = qx(@env, %W(soxi -c #{path})).to_i
65
- fmt.type = qx(@env, %W(soxi -t #{path})).strip
66
- fmt.rate = qx(@env, %W(soxi -r #{path})).to_i
67
- fmt.bits ||= precision
68
- fmt
69
- end
51
+ @format ||= DTAS::Format.from_file(@env, @infile)
70
52
  end
71
53
 
72
54
  # This is the number of samples according to the samples in the source
@@ -134,27 +134,12 @@ class DTAS::SplitFX # :nodoc:
134
134
  load_tracks!(hash)
135
135
  end
136
136
 
137
- # FIXME: duplicate from dtas/source/sox
138
- def precision
139
- qx(@env, %W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3
140
- rescue # fallback to parsing the whole output
141
- s = qx(@env, %W(soxi #@infile), err: "/dev/null")
142
- s =~ /Precision\s+:\s*(\d+)-bit/n
143
- v = $1.to_i
144
- return v if v > 0
145
- raise TypeError, "could not determine precision for #@infile"
146
- end
147
-
148
137
  def load_input!(hash)
149
138
  @infile = hash["infile"] or raise ArgumentError, "'infile' not specified"
150
139
  if infmt = hash["infmt"] # rarely needed
151
140
  @infmt = DTAS::Format.load(infmt)
152
141
  else # likely
153
- @infmt = DTAS::Format.new
154
- @infmt.channels = qx(@env, %W(soxi -c #@infile)).to_i
155
- @infmt.rate = qx(@env, %W(soxi -r #@infile)).to_i
156
- @infmt.bits ||= precision
157
- # we don't care for type
142
+ @infmt = DTAS::Format.from_file(@env, @infile)
158
143
  end
159
144
  end
160
145
 
@@ -167,6 +152,12 @@ class DTAS::SplitFX # :nodoc:
167
152
  def spawn(target, t, opts)
168
153
  target = @targets[target] || generic_target(target)
169
154
  outfmt = target["format"]
155
+
156
+ # default format:
157
+ unless outfmt
158
+ outfmt = @infmt.dup
159
+ outfmt.type = "flac"
160
+ end
170
161
  env = outfmt.to_env
171
162
 
172
163
  # set very high quality resampling if using 24-bit or higher output
@@ -91,21 +91,33 @@ class DTAS::Tracklist # :nodoc:
91
91
  idx = by_track_id[after_track_id] or
92
92
  raise ArgumentError, "after_track_id invalid"
93
93
  @list[idx, 1] = [ @list[idx], track ]
94
- @pos = idx + 1 if set_as_current
94
+ if set_as_current
95
+ @pos = idx + 1
96
+ else
97
+ @pos += 1 if @pos > idx
98
+ end
95
99
  else # nil = first_track
96
100
  @list.unshift(track)
97
- @pos = 0 if set_as_current
101
+ if set_as_current
102
+ @pos = 0
103
+ else
104
+ @pos += 1 if @pos >= 0
105
+ end
98
106
  end
99
107
  track.object_id
100
108
  end
101
109
 
102
110
  def remove_track(track_id)
103
111
  by_track_id = _track_id_map
104
- if idx = by_track_id.delete(track_id)
105
- @list[idx] = nil
106
- @list.compact!
107
- # TODO: what do we do with @pos (and the currently-playing track)
112
+ idx = by_track_id.delete(track_id) or return false
113
+ @list[idx] = nil
114
+ @list.compact!
115
+ len = @list.size
116
+ if @pos >= len
117
+ @pos = len == 0 ? TL_DEFAULTS["pos"] : len
108
118
  end
119
+ @goto_pos = @goto_pos = nil # TODO: reposition?
120
+ true
109
121
  end
110
122
 
111
123
  def go_to(track_id, offset_hhmmss = nil)
@@ -0,0 +1,78 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative '../dtas'
4
+ require_relative 'parse_time'
5
+ require 'shellwords'
6
+
7
+ class DTAS::TrimFX
8
+ include DTAS::ParseTime
9
+
10
+ attr_reader :tbeg
11
+ attr_reader :tlen
12
+ attr_reader :cmd
13
+
14
+ def initialize(args)
15
+ args = args.dup
16
+ case args.shift
17
+ when "trim"
18
+ parse_trim!(args)
19
+ when "all"
20
+ @tbeg = 0
21
+ @tlen = nil
22
+ else
23
+ raise ArgumentError, "#{args.inspect} not understood"
24
+ end
25
+ case tmp = args.shift
26
+ when "sh" then @cmd = args
27
+ when "sox" then tfx_sox(args)
28
+ when "eca" then tfx_eca(args)
29
+ when nil
30
+ @cmd = []
31
+ else
32
+ raise ArgumentError, "unknown effect type: #{tmp}"
33
+ end
34
+ end
35
+
36
+ def tfx_sox(args)
37
+ @cmd = %w(sox $SOXIN $SOXOUT $TRIMFX)
38
+ @cmd.concat(args)
39
+ @cmd.concat(%w($FADEFX))
40
+ end
41
+
42
+ def tfx_eca(args)
43
+ @cmd = %w(sox $SOXIN $SOX2ECA $TRIMFX)
44
+ @cmd.concat(%w(| ecasound $ECAFMT -i stdin -o stdout))
45
+ @cmd.concat(args)
46
+ @cmd.concat(%w(| sox $ECA2SOX - $SOXOUT $FADEFX))
47
+ end
48
+
49
+ def to_sox_arg(format)
50
+ if @tbeg && @tlen
51
+ beg = @tbeg * format.rate
52
+ len = @tlen * format.rate
53
+ %W(trim #{beg.round}s #{len.round}s)
54
+ elsif @tbeg
55
+ return [] if @tbeg == 0
56
+ beg = @tbeg * format.rate
57
+ %W(trim #{beg.round}s)
58
+ else
59
+ []
60
+ end
61
+ end
62
+
63
+ def parse_trim!(args)
64
+ tbeg = parse_time(args.shift)
65
+ if args[0] =~ /\A=?[\d\.]+\z/
66
+ tlen = args.shift
67
+ is_stop_time = tlen.sub!(/\A=/, "") ? true : false
68
+ tlen = parse_time(tlen)
69
+ if is_stop_time
70
+ tlen = tlen - tbeg
71
+ end
72
+ else
73
+ tlen = nil
74
+ end
75
+ @tbeg = tbeg
76
+ @tlen = tlen
77
+ end
78
+ end
@@ -2,6 +2,7 @@
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
  require 'socket'
4
4
  require 'io/wait'
5
+ require_relative 'compat_rbx'
5
6
 
6
7
  class DTAS::UNIXAccepted # :nodoc:
7
8
  attr_reader :to_io
@@ -5,6 +5,7 @@ require_relative 'xs'
5
5
  require 'socket'
6
6
  require 'io/wait'
7
7
  require 'shellwords'
8
+ require_relative 'compat_rbx'
8
9
 
9
10
  class DTAS::UNIXClient # :nodoc:
10
11
  attr_reader :to_io
@@ -16,7 +17,7 @@ class DTAS::UNIXClient # :nodoc:
16
17
  end
17
18
 
18
19
  def initialize(path = self.class.default_path)
19
- @to_io = Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0)
20
+ @to_io = Socket.new(:UNIX, :SEQPACKET, 0)
20
21
  @to_io.connect(Socket.pack_sockaddr_un(path))
21
22
  end
22
23
 
@@ -26,13 +26,13 @@ class DTAS::UNIXServer # :nodoc:
26
26
  # lock down access by default, arbitrary commands may run as the
27
27
  # same user dtas-player runs as:
28
28
  old_umask = File.umask(0077)
29
- @to_io = Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0)
29
+ @to_io = Socket.new(:UNIX, :SEQPACKET, 0)
30
30
  addr = Socket.pack_sockaddr_un(path)
31
31
  begin
32
32
  @to_io.bind(addr)
33
33
  rescue Errno::EADDRINUSE
34
34
  # maybe we have an old path leftover from a killed process
35
- tmp = Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0)
35
+ tmp = Socket.new(:UNIX, :SEQPACKET, 0)
36
36
  begin
37
37
  tmp.connect(addr)
38
38
  raise RuntimeError, "socket `#{path}' is in use", []
@@ -51,13 +51,9 @@ class TestBuffer < Testcase
51
51
  def test_broadcast_1
52
52
  buf = new_buffer
53
53
  r, w = IO.pipe
54
- assert_equal :wait_readable, buf.broadcast([w])
55
- assert_equal 0, buf.bytes_xfer
56
54
  buf.wr.write "HIHI"
57
55
  assert_equal :wait_readable, buf.broadcast([w])
58
56
  assert_equal 4, buf.bytes_xfer
59
- assert_equal :wait_readable, buf.broadcast([w])
60
- assert_equal 4, buf.bytes_xfer
61
57
  tmp = [w]
62
58
  r.close
63
59
  buf.wr.write "HIHI"
@@ -90,20 +86,6 @@ class TestBuffer < Testcase
90
86
  a[1].nonblock = false
91
87
  b[0].read(b[0].nread)
92
88
  b[1].write(max)
93
- t = Thread.new do
94
- sleep 0.005
95
- [ a[0].read(max.size).size, b[0].read(max.size).size ]
96
- end
97
- assert_equal 5, buf.__broadcast_tee(blocked, [a[1], b[1]], 5)
98
- assert_equal [a[1]], blocked
99
- assert_equal [ max.size, max.size ], t.value
100
- b[0].close
101
- tmp = [a[1], b[1]]
102
-
103
- newerr = tmperr { assert_equal 5, buf.__broadcast_tee(blocked, tmp, 5) }
104
- assert_equal [a[1]], blocked
105
- assert_match(%r{dropping}, newerr.string)
106
- assert_equal [a[1]], tmp
107
89
  end
108
90
 
109
91
  def test_broadcast
@@ -115,8 +97,6 @@ class TestBuffer < Testcase
115
97
  assert_equal 5, buf.bytes_xfer
116
98
  assert_equal "HELLO", a[0].read(5)
117
99
  assert_equal "HELLO", b[0].read(5)
118
- assert_equal :wait_readable, buf.broadcast([a[1], b[1]])
119
- assert_equal 5, buf.bytes_xfer
120
100
 
121
101
  return unless b[1].respond_to?(:pipe_size)
122
102
 
@@ -0,0 +1,18 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require_relative 'helper'
4
+ require 'dtas/fadefx'
5
+
6
+ class TestFadeFX < Testcase
7
+ def test_fadefx
8
+ ffx = DTAS::FadeFX.new("fade=t1,t3.1;l4,t1")
9
+ assert_equal 't', ffx.out_prev.type
10
+ assert_equal 1, ffx.out_prev.len
11
+ assert_equal 't', ffx.in_main.type
12
+ assert_equal 3.1, ffx.in_main.len
13
+ assert_equal 'l', ffx.out_main.type
14
+ assert_equal 4, ffx.out_main.len
15
+ assert_equal 't', ffx.in_next.type
16
+ assert_equal 1, ffx.in_next.len
17
+ end
18
+ end
@@ -0,0 +1,50 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require './test/helper'
4
+ require 'dtas/trimfx'
5
+ require 'dtas/format'
6
+ require 'yaml'
7
+
8
+ class TestTrimFX < Testcase
9
+ def test_example
10
+ ex = YAML.load(File.read("examples/trimfx.sample.yml"))
11
+ effects = []
12
+ ex["effects"].each do |line|
13
+ words = Shellwords.split(line)
14
+ case words[0]
15
+ when "trim"
16
+ tfx = DTAS::TrimFX.new(words)
17
+ assert_equal 52.0, tfx.tbeg
18
+ assert_equal 1.0, tfx.tlen
19
+ effects << tfx
20
+ end
21
+ end
22
+ assert_equal 4, effects.size
23
+ end
24
+
25
+ def test_all
26
+ tfx = DTAS::TrimFX.new(%w(all))
27
+ assert_equal 0, tfx.tbeg
28
+ assert_nil tfx.tlen
29
+ assert_equal [], tfx.to_sox_arg(DTAS::Format.new)
30
+ end
31
+
32
+ def test_time
33
+ tfx = DTAS::TrimFX.new(%w(trim 2:30 3.1))
34
+ assert_equal 150, tfx.tbeg
35
+ assert_equal 3.1, tfx.tlen
36
+ end
37
+
38
+ def test_to_sox_arg
39
+ tfx = DTAS::TrimFX.new(%w(trim 1 0.5))
40
+ assert_equal %w(trim 44100s 22050s), tfx.to_sox_arg(DTAS::Format.new)
41
+
42
+ tfx = DTAS::TrimFX.new(%w(trim 1 sox vol -1dB))
43
+ assert_equal %w(trim 44100s), tfx.to_sox_arg(DTAS::Format.new)
44
+ end
45
+
46
+ def test_tfx_effects
47
+ tfx = DTAS::TrimFX.new(%w(trim 1 sox vol -1dB))
48
+ assert_equal %w(sox $SOXIN $SOXOUT $TRIMFX vol -1dB $FADEFX), tfx.cmd
49
+ end
50
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dtas
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dtas hackers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-30 00:00:00.000000000 Z
11
+ date: 2013-10-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |-
14
14
  Free Software command-line tools for audio playback, mastering, and
@@ -24,6 +24,7 @@ executables:
24
24
  - dtas-cueedit
25
25
  - dtas-enq
26
26
  - dtas-msinkctl
27
+ - dtas-partstats
27
28
  - dtas-player
28
29
  - dtas-sinkedit
29
30
  - dtas-sourceedit
@@ -65,6 +66,7 @@ files:
65
66
  - bin/dtas-cueedit
66
67
  - bin/dtas-enq
67
68
  - bin/dtas-msinkctl
69
+ - bin/dtas-partstats
68
70
  - bin/dtas-player
69
71
  - bin/dtas-sinkedit
70
72
  - bin/dtas-sourceedit
@@ -76,16 +78,21 @@ files:
76
78
  - dtas.gemspec
77
79
  - examples/README
78
80
  - examples/splitfx.sample.yml
81
+ - examples/trimfx.sample.yml
79
82
  - lib/dtas.rb
80
83
  - lib/dtas/buffer.rb
81
84
  - lib/dtas/buffer/read_write.rb
82
85
  - lib/dtas/buffer/splice.rb
83
86
  - lib/dtas/command.rb
84
87
  - lib/dtas/compat_onenine.rb
88
+ - lib/dtas/compat_rbx.rb
85
89
  - lib/dtas/cue_index.rb
86
90
  - lib/dtas/disclaimer.rb
87
91
  - lib/dtas/edit_client.rb
92
+ - lib/dtas/fadefx.rb
88
93
  - lib/dtas/format.rb
94
+ - lib/dtas/parse_time.rb
95
+ - lib/dtas/partstats.rb
89
96
  - lib/dtas/pipe.rb
90
97
  - lib/dtas/player.rb
91
98
  - lib/dtas/player/client_handler.rb
@@ -109,6 +116,7 @@ files:
109
116
  - lib/dtas/splitfx.rb
110
117
  - lib/dtas/state_file.rb
111
118
  - lib/dtas/tracklist.rb
119
+ - lib/dtas/trimfx.rb
112
120
  - lib/dtas/unix_accepted.rb
113
121
  - lib/dtas/unix_client.rb
114
122
  - lib/dtas/unix_server.rb
@@ -135,6 +143,7 @@ files:
135
143
  - test/player_integration.rb
136
144
  - test/test_buffer.rb
137
145
  - test/test_env.rb
146
+ - test/test_fadefx.rb
138
147
  - test/test_format.rb
139
148
  - test/test_format_change.rb
140
149
  - test/test_player.rb
@@ -150,6 +159,7 @@ files:
150
159
  - test/test_source_sox.rb
151
160
  - test/test_splitfx.rb
152
161
  - test/test_tracklist.rb
162
+ - test/test_trimfx.rb
153
163
  - test/test_unixserver.rb
154
164
  - test/test_util.rb
155
165
  homepage: http://dtas.80x24.org/