dtas 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/