dtas 0.0.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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +9 -0
  4. data/.rsync_doc +3 -0
  5. data/COPYING +674 -0
  6. data/Documentation/.gitignore +3 -0
  7. data/Documentation/GNUmakefile +46 -0
  8. data/Documentation/dtas-console.txt +42 -0
  9. data/Documentation/dtas-ctl.txt +64 -0
  10. data/Documentation/dtas-cueedit.txt +24 -0
  11. data/Documentation/dtas-enq.txt +29 -0
  12. data/Documentation/dtas-msinkctl.txt +45 -0
  13. data/Documentation/dtas-player.txt +110 -0
  14. data/Documentation/dtas-player_effects.txt +45 -0
  15. data/Documentation/dtas-player_protocol.txt +181 -0
  16. data/Documentation/dtas-sinkedit.txt +41 -0
  17. data/Documentation/dtas-sourceedit.txt +33 -0
  18. data/Documentation/dtas-xdelay.txt +57 -0
  19. data/Documentation/troubleshooting.txt +13 -0
  20. data/GIT-VERSION-GEN +30 -0
  21. data/GNUmakefile +9 -0
  22. data/HACKING +12 -0
  23. data/INSTALL +53 -0
  24. data/README +103 -0
  25. data/Rakefile +97 -0
  26. data/TODO +4 -0
  27. data/bin/dtas-console +160 -0
  28. data/bin/dtas-ctl +10 -0
  29. data/bin/dtas-cueedit +78 -0
  30. data/bin/dtas-enq +13 -0
  31. data/bin/dtas-msinkctl +51 -0
  32. data/bin/dtas-player +34 -0
  33. data/bin/dtas-sinkedit +58 -0
  34. data/bin/dtas-sourceedit +48 -0
  35. data/bin/dtas-xdelay +85 -0
  36. data/dtas-linux.gemspec +18 -0
  37. data/dtas-mpris.gemspec +16 -0
  38. data/examples/dtas_state.yml +18 -0
  39. data/lib/dtas.rb +7 -0
  40. data/lib/dtas/buffer.rb +90 -0
  41. data/lib/dtas/buffer/read_write.rb +102 -0
  42. data/lib/dtas/buffer/splice.rb +142 -0
  43. data/lib/dtas/command.rb +43 -0
  44. data/lib/dtas/compat_onenine.rb +18 -0
  45. data/lib/dtas/disclaimer.rb +18 -0
  46. data/lib/dtas/format.rb +151 -0
  47. data/lib/dtas/pipe.rb +39 -0
  48. data/lib/dtas/player.rb +393 -0
  49. data/lib/dtas/player/client_handler.rb +463 -0
  50. data/lib/dtas/process.rb +87 -0
  51. data/lib/dtas/replaygain.rb +41 -0
  52. data/lib/dtas/rg_state.rb +99 -0
  53. data/lib/dtas/serialize.rb +9 -0
  54. data/lib/dtas/sigevent.rb +10 -0
  55. data/lib/dtas/sigevent/efd.rb +20 -0
  56. data/lib/dtas/sigevent/pipe.rb +28 -0
  57. data/lib/dtas/sink.rb +121 -0
  58. data/lib/dtas/source.rb +147 -0
  59. data/lib/dtas/source/command.rb +40 -0
  60. data/lib/dtas/source/common.rb +14 -0
  61. data/lib/dtas/source/mp3.rb +37 -0
  62. data/lib/dtas/state_file.rb +33 -0
  63. data/lib/dtas/unix_accepted.rb +76 -0
  64. data/lib/dtas/unix_client.rb +51 -0
  65. data/lib/dtas/unix_server.rb +110 -0
  66. data/lib/dtas/util.rb +15 -0
  67. data/lib/dtas/writable_iter.rb +22 -0
  68. data/perl/dtas-graph +129 -0
  69. data/pkg.mk +26 -0
  70. data/setup.rb +1586 -0
  71. data/test/covshow.rb +30 -0
  72. data/test/helper.rb +76 -0
  73. data/test/player_integration.rb +121 -0
  74. data/test/test_buffer.rb +216 -0
  75. data/test/test_format.rb +61 -0
  76. data/test/test_format_change.rb +49 -0
  77. data/test/test_player.rb +47 -0
  78. data/test/test_player_client_handler.rb +86 -0
  79. data/test/test_player_integration.rb +220 -0
  80. data/test/test_rg_integration.rb +117 -0
  81. data/test/test_rg_state.rb +32 -0
  82. data/test/test_sink.rb +32 -0
  83. data/test/test_sink_tee_integration.rb +34 -0
  84. data/test/test_source.rb +102 -0
  85. data/test/test_unixserver.rb +66 -0
  86. data/test/test_util.rb +15 -0
  87. metadata +208 -0
@@ -0,0 +1,43 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # common code for wrapping SoX/ecasound/... commands
5
+ require_relative 'serialize'
6
+ require 'shellwords'
7
+
8
+ module DTAS::Command # :nodoc:
9
+ include DTAS::Serialize
10
+ attr_reader :pid
11
+ attr_reader :to_io
12
+ attr_accessor :command
13
+ attr_accessor :env
14
+ attr_accessor :spawn_at
15
+
16
+ COMMAND_DEFAULTS = {
17
+ "env" => {},
18
+ "command" => nil,
19
+ }
20
+
21
+ def command_init(defaults = {})
22
+ @pid = nil
23
+ @to_io = nil
24
+ @spawn_at = nil
25
+ COMMAND_DEFAULTS.merge(defaults).each do |k,v|
26
+ v = v.dup if Hash === v || Array === v
27
+ instance_variable_set("@#{k}", v)
28
+ end
29
+ end
30
+
31
+ def kill(sig = :TERM)
32
+ # always kill the pgroup since we run subcommands in their own shell
33
+ Process.kill(sig, -@pid)
34
+ end
35
+
36
+ def on_death(status)
37
+ @pid = nil
38
+ end
39
+
40
+ def command_string
41
+ @command
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+
5
+ # Make Ruby 1.9.3 look like Ruby 2.0.0 to us
6
+ # This exists for Debian wheezy users using the stock Ruby 1.9.3 install.
7
+ # We'll drop this interface when Debian wheezy (7.0) becomes unsupported.
8
+ class String
9
+ def b # :nodoc:
10
+ dup.force_encoding(Encoding::BINARY)
11
+ end
12
+ end unless String.method_defined?(:b)
13
+
14
+ def IO
15
+ def self.pipe # :nodoc:
16
+ super.map! { |io| io.close_on_exec = true; io }
17
+ end
18
+ end if RUBY_VERSION.to_f <= 1.9
@@ -0,0 +1,18 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
4
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
5
+ DTAS_DISCLAIMER = <<EOF
6
+ # WARNING!
7
+ #
8
+ # Ignorant or improper use of #$0 may lead to
9
+ # data loss, hearing loss, and damage to audio equipment.
10
+ #
11
+ # Please read and understand the documentation of all commands you
12
+ # attempt to configure.
13
+ #
14
+ # #$0 will never prevent you from doing stupid things.
15
+ #
16
+ # There is no warranty, the developers of #$0
17
+ # are not responsible for your actions.
18
+ EOF
@@ -0,0 +1,151 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # class represents an audio format (type/bits/channels/sample rate/...)
5
+ require_relative '../dtas'
6
+ require_relative 'process'
7
+ require_relative 'serialize'
8
+
9
+ class DTAS::Format # :nodoc:
10
+ include DTAS::Process
11
+ include DTAS::Serialize
12
+ NATIVE_ENDIAN = [1].pack("l") == [1].pack("l>") ? "big" : "little"
13
+
14
+ attr_accessor :type # s32, f32, f64 ... any point in others?
15
+ attr_accessor :channels # 1..666
16
+ attr_accessor :rate # 44100, 48000, 88200, 96000, 176400, 192000 ...
17
+ attr_accessor :bits # only set for playback on 16-bit DACs
18
+ attr_accessor :endian
19
+
20
+ FORMAT_DEFAULTS = {
21
+ "type" => "s32",
22
+ "channels" => 2,
23
+ "rate" => 44100,
24
+ "bits" => nil, # default: implied from type
25
+ "endian" => nil, # unspecified
26
+ }
27
+ SIVS = FORMAT_DEFAULTS.keys
28
+
29
+ def self.load(hash)
30
+ fmt = new
31
+ return fmt unless hash
32
+ (SIVS & hash.keys).each do |k|
33
+ fmt.instance_variable_set("@#{k}", hash[k])
34
+ end
35
+ fmt
36
+ end
37
+
38
+ def initialize
39
+ FORMAT_DEFAULTS.each do |k,v|
40
+ instance_variable_set("@#{k}", v)
41
+ end
42
+ end
43
+
44
+ def to_sox_arg
45
+ rv = %W(-t#@type -c#@channels -r#@rate)
46
+ rv.concat(%W(-b#@bits)) if @bits # needed for play(1) to 16-bit DACs
47
+ rv
48
+ end
49
+
50
+ # returns 'be' or 'le' depending on endianess
51
+ def endian2
52
+ case e = @endian || NATIVE_ENDIAN
53
+ when "big"
54
+ "be"
55
+ when "little"
56
+ "le"
57
+ else
58
+ raise"unsupported endian=#{e}"
59
+ end
60
+ end
61
+
62
+ def to_eca_arg
63
+ %W(-f #{@type}_#{endian2},#@channels,#@rate)
64
+ end
65
+
66
+ def inspect
67
+ "<#{self.class}(#{xs(to_sox_arg)})>"
68
+ end
69
+
70
+ def to_hsh
71
+ to_hash.delete_if { |k,v| v == FORMAT_DEFAULTS[k] }
72
+ end
73
+
74
+ def to_hash
75
+ ivars_to_hash(SIVS)
76
+ end
77
+
78
+ def from_file(path)
79
+ @channels = qx(%W(soxi -c #{path})).to_i
80
+ @type = qx(%W(soxi -t #{path})).strip
81
+ @rate = qx(%W(soxi -r #{path})).to_i
82
+ # we don't need to care for bits, do we?
83
+ end
84
+
85
+ # for the _decoded_ output
86
+ def bits_per_sample
87
+ return @bits if @bits
88
+ /\A[fst](8|16|24|32|64)\z/ =~ @type or
89
+ raise TypeError, "invalid type=#@type (must be s32/f32/f64)"
90
+ $1.to_i
91
+ end
92
+
93
+ def bytes_per_sample
94
+ bits_per_sample / 8
95
+ end
96
+
97
+ def to_env
98
+ rv = {
99
+ "SOX_FILETYPE" => @type,
100
+ "CHANNELS" => @channels.to_s,
101
+ "RATE" => @rate.to_s,
102
+ "ENDIAN" => @endian || NATIVE_ENDIAN,
103
+ "SOXFMT" => to_sox_arg.join(' '),
104
+ "ECAFMT" => to_eca_arg.join(' '),
105
+ "ENDIAN2" => endian2,
106
+ }
107
+ begin # don't set these if we can't get them, SOX_FILETYPE may be enough
108
+ rv["BITS_PER_SAMPLE"] = bits_per_sample.to_s
109
+ rescue TypeError
110
+ end
111
+ rv
112
+ end
113
+
114
+ def bytes_to_samples(bytes)
115
+ bytes / bytes_per_sample / @channels
116
+ end
117
+
118
+ def bytes_to_time(bytes)
119
+ Time.at(bytes_to_samples(bytes) / @rate.to_f)
120
+ end
121
+
122
+ def valid_type?(type)
123
+ !!(type =~ %r{\A[us](?:8|16|24|32)\z} || type =~ %r{\Af?:(32|64)})
124
+ end
125
+
126
+ def valid_endian?(endian)
127
+ !!(endian =~ %r{\A(?:big|little|swap)\z})
128
+ end
129
+
130
+ # HH:MM:SS.frac (don't bother with more complex times, too much code)
131
+ # part of me wants to drop this feature from playq, feels like bloat...
132
+ def hhmmss_to_samples(hhmmss)
133
+ time = hhmmss.dup
134
+ rv = 0
135
+ if time.sub!(/\.(\d+)\z/, "")
136
+ # convert fractional second to sample count:
137
+ rv = ("0.#$1".to_f * @rate).to_i
138
+ end
139
+
140
+ # deal with HH:MM:SS
141
+ t = time.split(/:/)
142
+ raise ArgumentError, "Bad time format: #{hhmmss}" if t.size > 3
143
+
144
+ mult = 1
145
+ while part = t.pop
146
+ rv += part.to_i * mult * @rate
147
+ mult *= 60
148
+ end
149
+ rv
150
+ end
151
+ end
data/lib/dtas/pipe.rb ADDED
@@ -0,0 +1,39 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ begin
5
+ require 'io/splice'
6
+ rescue LoadError
7
+ end
8
+ require_relative '../dtas'
9
+ require_relative 'writable_iter'
10
+
11
+ class DTAS::Pipe < IO # :nodoc:
12
+ include DTAS::WritableIter
13
+ attr_accessor :sink
14
+
15
+ def self.new
16
+ _, w = rv = pipe
17
+ w.writable_iter_init
18
+ rv
19
+ end
20
+
21
+ # create no-op methods for non-Linux
22
+ unless method_defined?(:pipe_size=)
23
+ def pipe_size=(_)
24
+ end
25
+
26
+ def pipe_size
27
+ end
28
+ end
29
+ end
30
+
31
+ # for non-blocking sinks, this avoids extra fcntl(..., F_GETFL) syscalls
32
+ # We don't need fcntl at all for splice/tee in Linux
33
+ # For non-Linux, we write_nonblock/read_nonblock already call fcntl()
34
+ # behind our backs, so there's no need to repeat it.
35
+ class DTAS::PipeNB < DTAS::Pipe # :nodoc:
36
+ def nonblock?
37
+ true
38
+ end
39
+ end
@@ -0,0 +1,393 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'yaml'
5
+ require 'shellwords'
6
+ require_relative '../dtas'
7
+ require_relative 'source'
8
+ require_relative 'source/command'
9
+ require_relative 'sink'
10
+ require_relative 'unix_server'
11
+ require_relative 'buffer'
12
+ require_relative 'sigevent'
13
+ require_relative 'rg_state'
14
+ require_relative 'state_file'
15
+
16
+ class DTAS::Player # :nodoc:
17
+ require_relative 'player/client_handler'
18
+ include DTAS::Player::ClientHandler
19
+ attr_accessor :state_file
20
+ attr_accessor :socket
21
+ attr_reader :sinks
22
+
23
+ def initialize
24
+ @state_file = nil
25
+ @socket = nil
26
+ @srv = nil
27
+ @queue = [] # sources
28
+ @paused = false
29
+ @format = DTAS::Format.new
30
+ @srccmd = nil
31
+ @srcenv = {}
32
+
33
+ @sinks = {} # { user-defined name => sink }
34
+ @targets = [] # order matters
35
+ @rg = DTAS::RGState.new
36
+
37
+ # sits in between shared effects (if any) and sinks
38
+ @sink_buf = DTAS::Buffer.new
39
+ @current = nil
40
+ @watchers = {}
41
+ end
42
+
43
+ def echo(msg)
44
+ msg = Shellwords.join(msg) if Array === msg
45
+ @watchers.delete_if do |io, _|
46
+ if io.closed?
47
+ true
48
+ else
49
+ case io.emit(msg)
50
+ when :wait_readable, :wait_writable
51
+ false
52
+ else
53
+ true
54
+ end
55
+ end
56
+ end
57
+ $stdout.write(msg << "\n")
58
+ end
59
+
60
+ def to_hsh
61
+ rv = {}
62
+ rv["socket"] = @socket
63
+ rv["paused"] = @paused if @paused
64
+ src = rv["source"] = {}
65
+ src["command"] = @srccmd if @srccmd
66
+ src["env"] = @srcenv if @srcenv.size > 0
67
+
68
+ # Arrays
69
+ rv["queue"] = @queue
70
+
71
+ %w(rg sink_buf format).each do |k|
72
+ rv[k] = instance_variable_get("@#{k}").to_hsh
73
+ end
74
+
75
+ # no empty hashes or arrays
76
+ rv.delete_if do |k,v|
77
+ case v
78
+ when Hash, Array
79
+ v.empty?
80
+ else
81
+ false
82
+ end
83
+ end
84
+
85
+ unless @sinks.empty?
86
+ sinks = rv["sinks"] = []
87
+ # sort sinks by name for human viewability
88
+ @sinks.keys.sort.each do |name|
89
+ sinks << @sinks[name].to_hsh
90
+ end
91
+ end
92
+
93
+ rv
94
+ end
95
+
96
+ def self.load(hash)
97
+ rv = new
98
+ rv.instance_eval do
99
+ @rg = DTAS::RGState.load(hash["rg"])
100
+ if v = hash["sink_buf"]
101
+ v = v["buffer_size"]
102
+ @sink_buf.buffer_size = v
103
+ end
104
+ %w(socket queue paused).each do |k|
105
+ v = hash[k] or next
106
+ instance_variable_set("@#{k}", v)
107
+ end
108
+ if v = hash["source"]
109
+ @srccmd = v["command"]
110
+ e = v["env"] and @srcenv = e
111
+ end
112
+
113
+ if v = hash["format"]
114
+ @format = DTAS::Format.load(v)
115
+ end
116
+
117
+ if sinks = hash["sinks"]
118
+ sinks.each do |sink_hsh|
119
+ sink = DTAS::Sink.load(sink_hsh)
120
+ @sinks[sink.name] = sink
121
+ end
122
+ end
123
+ end
124
+ rv
125
+ end
126
+
127
+ def enq_handler(io, msg)
128
+ # check @queue[0] in case we have no sinks
129
+ if @current || @queue[0] || @paused
130
+ @queue << msg
131
+ else
132
+ next_source(msg)
133
+ end
134
+ io.emit("OK")
135
+ end
136
+
137
+ def do_enq_head(io, msg)
138
+ # check @queue[0] in case we have no sinks
139
+ if @current || @queue[0] || @paused
140
+ @queue.unshift(msg)
141
+ else
142
+ next_source(msg)
143
+ end
144
+ io.emit("OK")
145
+ end
146
+
147
+ # yielded from readable_iter
148
+ def client_iter(io, msg)
149
+ msg = Shellwords.split(msg)
150
+ command = msg.shift
151
+ case command
152
+ when "enq"
153
+ enq_handler(io, msg[0])
154
+ when "enq-head"
155
+ do_enq_head(io, msg)
156
+ when "enq-cmd"
157
+ enq_handler(io, { "command" => msg[0]})
158
+ when "pause", "play", "play_pause"
159
+ play_pause_handler(io, command)
160
+ when "seek"
161
+ do_seek(io, msg[0])
162
+ when "clear"
163
+ @queue.clear
164
+ echo("clear")
165
+ io.emit("OK")
166
+ when "rg"
167
+ rg_handler(io, msg)
168
+ when "skip"
169
+ skip_handler(io, msg)
170
+ when "sink"
171
+ sink_handler(io, msg)
172
+ when "current"
173
+ current_handler(io, msg)
174
+ when "watch"
175
+ @watchers[io] = true
176
+ io.emit("OK")
177
+ when "format"
178
+ format_handler(io, msg)
179
+ when "env"
180
+ env_handler(io, msg)
181
+ when "restart"
182
+ restart_pipeline
183
+ io.emit("OK")
184
+ when "source"
185
+ source_handler(io, msg)
186
+ when "cd"
187
+ chdir_handler(io, msg)
188
+ when "pwd"
189
+ io.emit(Dir.pwd)
190
+ end
191
+ end
192
+
193
+ def event_loop_iter
194
+ @srv.run_once do |io, msg| # readability handler, request/response
195
+ case io
196
+ when @sink_buf
197
+ sink_iter
198
+ when DTAS::UNIXAccepted
199
+ client_iter(io, msg)
200
+ when DTAS::Sigevent # signal received
201
+ reap_iter
202
+ else
203
+ raise "BUG: unknown event: #{io.class} #{io.inspect} #{msg.inspect}"
204
+ end
205
+ end
206
+ end
207
+
208
+ def reap_iter
209
+ DTAS::Process.reaper do |status, obj|
210
+ warn [ :reap, obj, status ].inspect if $DEBUG
211
+ obj.on_death(status) if obj.respond_to?(:on_death)
212
+ case obj
213
+ when @current
214
+ next_source(@paused ? nil : @queue.shift)
215
+ when DTAS::Sink # on unexpected sink death
216
+ sink_death(obj, status)
217
+ end
218
+ end
219
+ :wait_readable
220
+ end
221
+
222
+ def sink_death(sink, status)
223
+ deleted = []
224
+ @targets.delete_if do |t|
225
+ if t.sink == sink
226
+ deleted << t
227
+ else
228
+ false
229
+ end
230
+ end
231
+
232
+ if deleted[0]
233
+ warn("#{sink.name} died unexpectedly: #{status.inspect}")
234
+ deleted.each { |t| drop_target(t) }
235
+ __current_drop unless @targets[0]
236
+ end
237
+
238
+ return unless sink.active
239
+
240
+ if @queue[0] && !@paused
241
+ # we get here if source/sinks are all killed in restart_pipeline
242
+ __sink_activate(sink)
243
+ next_source(@queue.shift)
244
+ elsif sink.respawn
245
+ __sink_activate(sink) if @current
246
+ end
247
+ ensure
248
+ sink.respawn = false
249
+ end
250
+
251
+ # returns a wait_ctl arg for self
252
+ def broadcast_iter(buf, targets)
253
+ case rv = buf.broadcast(targets)
254
+ when Array # array of blocked sinks
255
+ # have sinks wake up the this buffer when they're writable
256
+ trade_ctl = proc { @srv.wait_ctl(buf, :wait_readable) }
257
+ rv.each do |dst|
258
+ dst.on_writable = trade_ctl
259
+ @srv.wait_ctl(dst, :wait_writable)
260
+ end
261
+
262
+ # this @sink_buf hibernates until trade_ctl is called
263
+ # via DTAS::Sink#writable_iter
264
+ :ignore
265
+ else # :wait_readable or nil
266
+ rv
267
+ end
268
+ end
269
+
270
+ def bind
271
+ @srv = DTAS::UNIXServer.new(@socket)
272
+ end
273
+
274
+ # only used on new installations where no sink exists
275
+ def create_default_sink
276
+ return unless @sinks.empty?
277
+ s = DTAS::Sink.new
278
+ s.name = "default"
279
+ s.active = true
280
+ @sinks[s.name] = s
281
+ end
282
+
283
+ # called when the player is leaving idle state
284
+ def spawn_sinks(source_spec)
285
+ return true if @targets[0]
286
+ @sinks.each_value do |sink|
287
+ sink.active or next
288
+ next if sink.pid
289
+ @targets.concat(sink.spawn(@format))
290
+ end
291
+ if @targets[0]
292
+ @targets.sort_by! { |t| t.sink.prio }
293
+ true
294
+ else
295
+ # fail, no active sink
296
+ @queue.unshift(source_spec)
297
+ false
298
+ end
299
+ end
300
+
301
+ def next_source(source_spec)
302
+ @current = nil
303
+ if source_spec
304
+ # restart sinks iff we were idle
305
+ spawn_sinks(source_spec) or return
306
+
307
+ case source_spec
308
+ when String
309
+ @current = DTAS::Source.new(source_spec)
310
+ echo(%W(file #{@current.infile}))
311
+ when Array
312
+ @current = DTAS::Source.new(*source_spec)
313
+ echo(%W(file #{@current.infile} #{@current.offset_samples}s))
314
+ else
315
+ @current = DTAS::Source::Command.new(source_spec["command"])
316
+ echo(%W(command #{@current.command_string}))
317
+ end
318
+
319
+ if DTAS::Source === @current
320
+ @current.command = @srccmd if @srccmd
321
+ @current.env = @srcenv.dup unless @srcenv.empty?
322
+ end
323
+
324
+ dst = @sink_buf
325
+ @current.dst_assoc(dst)
326
+ @current.spawn(@format, @rg, out: dst.wr, in: "/dev/null")
327
+ @srv.wait_ctl(dst, :wait_readable)
328
+ else
329
+ stop_sinks if @sink_buf.inflight == 0
330
+ echo "idle"
331
+ end
332
+ end
333
+
334
+ def drop_target(target)
335
+ @srv.wait_ctl(target, :delete)
336
+ target.close
337
+ end
338
+
339
+ def stop_sinks
340
+ @targets.each { |t| drop_target(t) }.clear
341
+ end
342
+
343
+ # only call on unrecoverable errors (or "skip")
344
+ def __current_drop(src = @current)
345
+ __buf_reset(src.dst) if src && src.pid
346
+ end
347
+
348
+ # pull data from sink_buf into @targets, source feeds into sink_buf
349
+ def sink_iter
350
+ wait_iter = broadcast_iter(@sink_buf, @targets)
351
+ __current_drop if nil == wait_iter # sink error, stop source
352
+ return wait_iter if @current
353
+
354
+ # no source left to feed sink_buf, drain the remaining data
355
+ sink_bytes = @sink_buf.inflight
356
+ if sink_bytes > 0
357
+ return wait_iter if @targets[0] # play what is leftover
358
+
359
+ # discard the buffer if no sinks
360
+ @sink_buf.discard(sink_bytes)
361
+ end
362
+
363
+ # nothing left inflight, stop the sinks until we have a source
364
+ stop_sinks
365
+ :ignore
366
+ end
367
+
368
+ # the main loop
369
+ def run
370
+ sev = DTAS::Sigevent.new
371
+ @srv.wait_ctl(sev, :wait_readable)
372
+ old_chld = trap(:CHLD) { sev.signal }
373
+ create_default_sink
374
+ next_source(@paused ? nil : @queue.shift)
375
+ begin
376
+ event_loop_iter
377
+ rescue => e # just in case...
378
+ warn "E: #{e.message} (#{e.class})"
379
+ e.backtrace.each { |l| warn l }
380
+ end while true
381
+ ensure
382
+ __current_requeue
383
+ trap(:CHLD, old_chld)
384
+ sev.close if sev
385
+ # for state file
386
+ end
387
+
388
+ def close
389
+ @srv = @srv.close if @srv
390
+ @sink_buf.close!
391
+ @state_file.dump(self, true) if @state_file
392
+ end
393
+ end