dtas 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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