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,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: