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,463 @@
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
+ module DTAS::Player::ClientHandler # :nodoc:
5
+
6
+ # returns true on success, wait_ctl arg on error
7
+ def set_bool(io, kv, v)
8
+ case v
9
+ when "false" then yield(false)
10
+ when "true" then yield(true)
11
+ else
12
+ return io.emit("ERR #{kv} must be true or false")
13
+ end
14
+ true
15
+ end
16
+
17
+ def adjust_numeric(io, obj, k, v)
18
+ negate = !!v.sub!(/\A-/, '')
19
+ case v
20
+ when %r{\A\+?\d*\.\d+\z}
21
+ num = v.to_f
22
+ when %r{\A\+?\d+\z}
23
+ num = v.to_i
24
+ else
25
+ return io.emit("ERR #{k}=#{v} must be a float")
26
+ end
27
+ num = -num if negate
28
+
29
+ if k.sub!(/\+\z/, '') # increment existing
30
+ num += obj.__send__(k)
31
+ elsif k.sub!(/-\z/, '') # decrement existing
32
+ num = obj.__send__(k) - num
33
+ # else # normal assignment
34
+ end
35
+ obj.__send__("#{k}=", num)
36
+ true
37
+ end
38
+
39
+ # returns true on success, wait_ctl arg on error
40
+ def set_int(io, kv, v, null_ok)
41
+ case v
42
+ when %r{\A-?\d+\z}
43
+ yield(v.to_i)
44
+ when ""
45
+ null_ok or return io.emit("ERR #{kv} must be defined")
46
+ yield(nil)
47
+ else
48
+ return io.emit("ERR #{kv} must an integer")
49
+ end
50
+ true
51
+ end
52
+
53
+ # returns true on success, wait_ctl arg on error
54
+ def set_uint(io, kv, v, null_ok)
55
+ case v
56
+ when %r{\A\d+\z}
57
+ yield(v.to_i)
58
+ when %r{\A0x[0-9a-fA-F]+\z}i # hex
59
+ yield(v.to_i(16))
60
+ when ""
61
+ null_ok or return io.emit("ERR #{kv} must be defined")
62
+ yield(nil)
63
+ else
64
+ return io.emit("ERR #{kv} must an non-negative integer")
65
+ end
66
+ true
67
+ end
68
+
69
+ def __sink_activate(sink)
70
+ return if sink.pid
71
+ @targets.concat(sink.spawn(@format))
72
+ @targets.sort_by! { |t| t.sink.prio }
73
+ end
74
+
75
+ def drop_sink(sink)
76
+ @targets.delete_if do |t|
77
+ if t.sink == sink
78
+ drop_target(t)
79
+ true
80
+ else
81
+ false
82
+ end
83
+ end
84
+ end
85
+
86
+ # called to activate/deactivate a sink
87
+ def __sink_switch(sink)
88
+ if sink.active
89
+ if @current
90
+ # maybe it's still alive for now, but it's just being killed
91
+ # do not reactivate it until we've reaped it
92
+ if sink.pid
93
+ drop_sink(sink)
94
+ sink.respawn = true
95
+ else
96
+ __sink_activate(sink)
97
+ end
98
+ end
99
+ else
100
+ drop_sink(sink)
101
+ end
102
+ # if we change any sinks, make sure the event loop watches it for
103
+ # readability again, since we new sinks should be writable, and
104
+ # we've stopped waiting on killed sinks
105
+ @srv.wait_ctl(@sink_buf, :wait_readable)
106
+ end
107
+
108
+ # returns a wait_ctl arg
109
+ def sink_handler(io, msg)
110
+ name = msg[1]
111
+ case msg[0]
112
+ when "ls"
113
+ io.emit(Shellwords.join(@sinks.keys.sort))
114
+ when "rm"
115
+ sink = @sinks.delete(name) or return io.emit("ERR #{name} not found")
116
+ drop_sink(sink)
117
+ io.emit("OK")
118
+ when "ed"
119
+ sink = @sinks[name] || (new_sink = DTAS::Sink.new)
120
+
121
+ # allow things that look like audio device names ("hw:1,0" , "/dev/dsp")
122
+ # or variable names.
123
+ sink.valid_name?(name) or return io.emit("ERR sink name invalid")
124
+
125
+ sink.name = name
126
+ active_before = sink.active
127
+
128
+ # multiple changes may be made at once
129
+ msg[2..-1].each do |kv|
130
+ k, v = kv.split(/=/, 2)
131
+ case k
132
+ when %r{\Aenv\.([^=]+)\z}
133
+ sink.env[$1] = v
134
+ when %r{\Aenv#([^=]+)\z}
135
+ v == nil or return io.emit("ERR unset env has no value")
136
+ sink.env.delete($1)
137
+ when "prio"
138
+ rv = set_int(io, kv, v, false) { |i| sink.prio = i }
139
+ rv == true or return rv
140
+ @targets.sort_by! { |t| t.sink.prio } if sink.active
141
+ when "nonblock", "active"
142
+ rv = set_bool(io, kv, v) { |b| sink.__send__("#{k}=", b) }
143
+ rv == true or return rv
144
+ when "pipe_size"
145
+ rv = set_uint(io, kv, v, true) { |u| sink.__send__("#{k}=", u) }
146
+ rv == true or return rv
147
+ when "command" # nothing to validate, this could be "rm -rf /" :>
148
+ sink.command = v.empty? ? DTAS::Sink::SINK_DEFAULTS["command"] : v
149
+ end
150
+ end
151
+
152
+ @sinks[name] = new_sink if new_sink # no errors? it's a new sink!
153
+
154
+ # start or stop a sink if its active= flag changed. Additionally,
155
+ # account for a crashed-but-marked-active sink. The user may have
156
+ # fixed the command to not crash it.
157
+ if (active_before != sink.active) || (sink.active && !sink.pid)
158
+ __sink_switch(sink)
159
+ end
160
+ io.emit("OK")
161
+ when "cat"
162
+ sink = @sinks[name] or return io.emit("ERR #{name} not found")
163
+ io.emit(sink.to_hsh.to_yaml)
164
+ else
165
+ io.emit("ERR unknown sink op #{msg[0]}")
166
+ end
167
+ end
168
+
169
+ def bytes_decoded(src = @current)
170
+ bytes = src.dst.bytes_xfer - src.dst_zero_byte
171
+ bytes = bytes < 0 ? 0 : bytes # maybe negative in case of sink errors
172
+ end
173
+
174
+ def __seek_offset_adj(dir, offset)
175
+ if offset.sub!(/s\z/, '')
176
+ offset = offset.to_i
177
+ else # time
178
+ offset = @current.format.hhmmss_to_samples(offset)
179
+ end
180
+ n = __current_decoded_samples + (dir * offset)
181
+ n = 0 if n < 0
182
+ "#{n}s"
183
+ end
184
+
185
+ def __current_decoded_samples
186
+ initial = @current.offset_samples
187
+ decoded = @format.bytes_to_samples(bytes_decoded)
188
+ decoded = out_samples(decoded, @format, @current.format)
189
+ initial + decoded
190
+ end
191
+
192
+ def __current_requeue
193
+ return unless @current && @current.pid
194
+
195
+ # no need to requeue if we're already due to die
196
+ return if @current.requeued
197
+ @current.requeued = true
198
+
199
+ dst = @current.dst
200
+ # prepare to seek to the desired point based on the number of bytes which
201
+ # passed through dst buffer we want the offset for the @current file,
202
+ # which may have a different rate than our internal @format
203
+ if @current.respond_to?(:infile)
204
+ # this offset in the @current.format (not player @format)
205
+ @queue.unshift([ @current.infile, "#{__current_decoded_samples}s" ])
206
+ else
207
+ # DTAS::Source::Command (hash), just rerun it
208
+ @queue.unshift(@current.to_hsh)
209
+ end
210
+ # We also want to hard drop the buffer so we do not get repeated audio.
211
+ __buf_reset(dst)
212
+ end
213
+
214
+ def out_samples(in_samples, infmt, outfmt)
215
+ in_rate = infmt.rate
216
+ out_rate = outfmt.rate
217
+ return in_samples if in_rate == out_rate # easy!
218
+ (in_samples * out_rate / in_rate.to_f).round
219
+ end
220
+
221
+ # returns the number of samples we expect from the source
222
+ # this takes into account sample rate differences between the source
223
+ # and internal player format
224
+ def current_expect_samples(in_samples) # @current.samples
225
+ out_samples(in_samples, @current.format, @format)
226
+ end
227
+
228
+ def rg_handler(io, msg)
229
+ return io.emit(@rg.to_hsh.to_yaml) if msg.empty?
230
+ before = @rg.to_hsh
231
+ msg.each do |kv|
232
+ k, v = kv.split(/=/, 2)
233
+ case k
234
+ when "mode"
235
+ case v
236
+ when "off"
237
+ @rg.mode = nil
238
+ else
239
+ DTAS::RGState::RG_MODE.include?(v) or
240
+ return io.emit("ERR rg mode invalid")
241
+ @rg.mode = v
242
+ end
243
+ when "fallback_track"
244
+ rv = set_bool(io, kv, v) { |b| @rg.fallback_track = b }
245
+ rv == true or return rv
246
+ when %r{(?:gain_threshold|norm_threshold|
247
+ preamp|norm_level|fallback_gain)[+-]?\z}x
248
+ rv = adjust_numeric(io, @rg, k, v)
249
+ rv == true or return rv
250
+ end
251
+ end
252
+ after = @rg.to_hsh
253
+ __current_requeue if before != after
254
+ io.emit("OK")
255
+ end
256
+
257
+ def active_sinks
258
+ sinks = @targets.map { |t| t.sink }
259
+ sinks.uniq!
260
+ sinks
261
+ end
262
+
263
+ # show current info about what's playing
264
+ # returns non-blocking iterator retval
265
+ def current_handler(io, msg)
266
+ tmp = {}
267
+ if @current
268
+ tmp["current"] = s = @current.to_hsh
269
+ s["spawn_at"] = @current.spawn_at
270
+ s["pid"] = @current.pid
271
+
272
+ # this offset and samples in the player @format (not @current.format)
273
+ decoded = @format.bytes_to_samples(bytes_decoded)
274
+ if @current.respond_to?(:infile)
275
+ initial = tmp["current_initial"] = @current.offset_samples
276
+ initial = out_samples(initial, @current.format, @format)
277
+ tmp["current_expect"] = current_expect_samples(s["samples"])
278
+ s["format"] = @current.format.to_hash.delete_if { |_,v| v.nil? }
279
+ else
280
+ initial = 0
281
+ tmp["current_expect"] = nil
282
+ s["format"] = @format.to_hash.delete_if { |_,v| v.nil? }
283
+ end
284
+ tmp["current_offset"] = initial + decoded
285
+ end
286
+ tmp["current_inflight"] = @sink_buf.inflight
287
+ tmp["format"] = @format.to_hash.delete_if { |_,v| v.nil? }
288
+ tmp["paused"] = @paused
289
+ rg = @rg.to_hsh
290
+ tmp["rg"] = rg unless rg.empty?
291
+ if @targets[0]
292
+ sinks = active_sinks
293
+ tmp["sinks"] = sinks.map! do |sink|
294
+ h = sink.to_hsh
295
+ h["pid"] = sink.pid
296
+ h
297
+ end
298
+ end
299
+ io.emit(tmp.to_yaml)
300
+ end
301
+
302
+ def __buf_reset(buf)
303
+ @srv.wait_ctl(buf, :ignore)
304
+ buf.buf_reset
305
+ @srv.wait_ctl(buf, :wait_readable)
306
+ end
307
+
308
+ def skip_handler(io, msg)
309
+ __current_drop
310
+ echo("skip")
311
+ io.emit("OK")
312
+ end
313
+
314
+ def play_pause_handler(io, command)
315
+ prev = @paused
316
+ __send__("do_#{command}")
317
+ io.emit({
318
+ "paused" => {
319
+ "before" => prev,
320
+ "after" => @paused,
321
+ }
322
+ }.to_yaml)
323
+ end
324
+
325
+ def do_pause
326
+ return if @paused
327
+ echo("pause")
328
+ @paused = true
329
+ __current_requeue
330
+ end
331
+
332
+ def do_play
333
+ # no echo, next_source will echo on new track
334
+ @paused = false
335
+ return if @current && @current.pid
336
+ next_source(@queue.shift)
337
+ end
338
+
339
+ def do_play_pause
340
+ @paused ? do_play : do_pause
341
+ end
342
+
343
+ def do_seek(io, offset)
344
+ if @current && @current.pid
345
+ if @current.respond_to?(:infile)
346
+ begin
347
+ if offset.sub!(/\A\+/, '')
348
+ offset = __seek_offset_adj(1, offset)
349
+ elsif offset.sub!(/\A-/, '')
350
+ offset = __seek_offset_adj(-1, offset)
351
+ # else: pass to sox directly
352
+ end
353
+ rescue ArgumentError
354
+ return io.emit("ERR bad time format")
355
+ end
356
+ @queue.unshift([ @current.infile, offset ])
357
+ __buf_reset(@current.dst) # trigger EPIPE
358
+ else
359
+ return io.emit("ERR unseekable")
360
+ end
361
+ elsif @paused
362
+ case file = @queue[0]
363
+ when String
364
+ @queue[0] = [ file, offset ]
365
+ when Array
366
+ file[1] = offset
367
+ else
368
+ return io.emit("ERR unseekable")
369
+ end
370
+ # unpaused case... what do we do?
371
+ end
372
+ io.emit("OK")
373
+ end
374
+
375
+ def restart_pipeline
376
+ return if @paused
377
+ __current_requeue
378
+ @sinks.each_value { |sink| sink.respawn = sink.active }
379
+ @targets.each { |t| drop_target(t) }.clear
380
+ end
381
+
382
+ def format_handler(io, msg)
383
+ new_fmt = @format.dup
384
+ msg.each do |kv|
385
+ k, v = kv.split(/=/, 2)
386
+ case k
387
+ when "type"
388
+ new_fmt.valid_type?(v) or return io.emit("ERR invalid file type")
389
+ new_fmt.type = v
390
+ when "channels", "bits", "rate"
391
+ rv = set_uint(io, kv, v, false) { |u| new_fmt.__send__("#{k}=", u) }
392
+ rv == true or return rv
393
+ when "endian"
394
+ new_fmt.valid_endian?(v) or return io.emit("ERR invalid endian")
395
+ new_fmt.endian = v
396
+ end
397
+ end
398
+
399
+ if new_fmt != @format
400
+ restart_pipeline # calls __current_requeue
401
+
402
+ # we must assign this after __current_requeue since __current_requeue
403
+ # relies on the old @format for calculation
404
+ @format = new_fmt
405
+ end
406
+ io.emit("OK")
407
+ end
408
+
409
+ def env_handler(io, msg)
410
+ msg.each do |kv|
411
+ case kv
412
+ when %r{\A([^=]+)=(.*)\z}
413
+ ENV[$1] = $2
414
+ when %r{\A([^=]+)#}
415
+ ENV.delete($1)
416
+ else
417
+ return io.emit("ERR bad env")
418
+ end
419
+ end
420
+ io.emit("OK")
421
+ end
422
+
423
+ def source_handler(io, msg)
424
+ case msg.shift
425
+ when "cat"
426
+ io.emit({
427
+ "command" => @srccmd || DTAS::Source::SOURCE_DEFAULTS["command"],
428
+ "env" => @srcenv,
429
+ }.to_yaml)
430
+ when "ed"
431
+ before = [ @srccmd, @srcenv ].inspect
432
+ msg.each do |kv|
433
+ k, v = kv.split(/=/, 2)
434
+ case k
435
+ when "command"
436
+ @srccmd = v.empty? ? nil : v
437
+ when %r{\Aenv\.([^=]+)\z}
438
+ @srcenv[$1] = v
439
+ when %r{\Aenv#([^=]+)\z}
440
+ v == nil or return io.emit("ERR unset env has no value")
441
+ @srcenv.delete($1)
442
+ end
443
+ end
444
+ after = [ @srccmd, @srcenv ].inspect
445
+ __current_requeue if before != after
446
+ io.emit("OK")
447
+ else
448
+ io.emit("ERR unknown source op")
449
+ end
450
+ end
451
+
452
+ def chdir_handler(io, msg)
453
+ msg.size == 1 or return io.emit("ERR usage: cd DIRNAME")
454
+ begin
455
+ Dir.chdir(msg[0])
456
+ rescue => e
457
+ return io.emit("ERR chdir: #{e.message}")
458
+ end
459
+ # echo(%W(cd msg[0])) # should we broadcast this?
460
+ io.emit("OK")
461
+ end
462
+ end
463
+ # :startdoc: