dtas 0.0.0 → 0.1.I

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Documentation/dtas-console.txt +15 -0
  4. data/Documentation/dtas-player_protocol.txt +5 -3
  5. data/Documentation/dtas-sourceedit.txt +14 -7
  6. data/GIT-VERSION-GEN +3 -3
  7. data/INSTALL +3 -3
  8. data/README +6 -5
  9. data/Rakefile +16 -7
  10. data/bin/dtas-console +48 -3
  11. data/bin/dtas-cueedit +1 -1
  12. data/bin/dtas-sinkedit +12 -28
  13. data/bin/dtas-sourceedit +15 -30
  14. data/lib/dtas.rb +1 -1
  15. data/lib/dtas/command.rb +0 -5
  16. data/lib/dtas/compat_onenine.rb +2 -2
  17. data/lib/dtas/disclaimer.rb +4 -3
  18. data/lib/dtas/edit_client.rb +48 -0
  19. data/lib/dtas/format.rb +2 -9
  20. data/lib/dtas/player.rb +64 -28
  21. data/lib/dtas/player/client_handler.rb +39 -20
  22. data/lib/dtas/process.rb +16 -15
  23. data/lib/dtas/replaygain.rb +19 -3
  24. data/lib/dtas/sink.rb +1 -2
  25. data/lib/dtas/source.rb +1 -141
  26. data/lib/dtas/source/av.rb +29 -0
  27. data/lib/dtas/source/av_ff_common.rb +127 -0
  28. data/lib/dtas/source/{command.rb → cmd.rb} +1 -1
  29. data/lib/dtas/source/ff.rb +30 -0
  30. data/lib/dtas/source/file.rb +94 -0
  31. data/lib/dtas/source/{mp3.rb → mp3gain.rb} +1 -1
  32. data/lib/dtas/source/sox.rb +114 -0
  33. data/lib/dtas/unix_client.rb +1 -9
  34. data/test/player_integration.rb +5 -17
  35. data/test/test_format.rb +10 -14
  36. data/test/test_format_change.rb +4 -8
  37. data/test/test_player_integration.rb +50 -62
  38. data/test/test_process.rb +33 -0
  39. data/test/test_rg_integration.rb +45 -35
  40. data/test/test_sink_pipe_size.rb +20 -0
  41. data/test/test_sink_tee_integration.rb +2 -4
  42. data/test/{test_source.rb → test_source_av.rb} +16 -16
  43. data/test/test_source_sox.rb +115 -0
  44. metadata +23 -12
  45. data/.rsync_doc +0 -3
@@ -4,4 +4,4 @@
4
4
  module DTAS # :nodoc:
5
5
  end
6
6
 
7
- require 'dtas/compat_onenine'
7
+ require_relative 'dtas/compat_onenine'
@@ -28,11 +28,6 @@ module DTAS::Command # :nodoc:
28
28
  end
29
29
  end
30
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
31
  def on_death(status)
37
32
  @pid = nil
38
33
  end
@@ -5,13 +5,13 @@
5
5
  # Make Ruby 1.9.3 look like Ruby 2.0.0 to us
6
6
  # This exists for Debian wheezy users using the stock Ruby 1.9.3 install.
7
7
  # We'll drop this interface when Debian wheezy (7.0) becomes unsupported.
8
- class String
8
+ class String # :nodoc:
9
9
  def b # :nodoc:
10
10
  dup.force_encoding(Encoding::BINARY)
11
11
  end
12
12
  end unless String.method_defined?(:b)
13
13
 
14
- def IO
14
+ def IO # :nodoc:
15
15
  def self.pipe # :nodoc:
16
16
  super.map! { |io| io.close_on_exec = true; io }
17
17
  end
@@ -2,17 +2,18 @@
2
2
  # :enddoc:
3
3
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
4
4
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
5
+ DTAS_PROGNAME = File.basename($0)
5
6
  DTAS_DISCLAIMER = <<EOF
6
7
  # WARNING!
7
8
  #
8
- # Ignorant or improper use of #$0 may lead to
9
+ # Ignorant or improper use of #{DTAS_PROGNAME} may lead to
9
10
  # data loss, hearing loss, and damage to audio equipment.
10
11
  #
11
12
  # Please read and understand the documentation of all commands you
12
13
  # attempt to configure.
13
14
  #
14
- # #$0 will never prevent you from doing stupid things.
15
+ # #{DTAS_PROGNAME} will never prevent you from doing stupid things.
15
16
  #
16
- # There is no warranty, the developers of #$0
17
+ # There is no warranty, the developers of #{DTAS_PROGNAME}
17
18
  # are not responsible for your actions.
18
19
  EOF
@@ -0,0 +1,48 @@
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 'tempfile'
5
+ require 'yaml'
6
+ require_relative 'unix_client'
7
+ require_relative 'disclaimer'
8
+
9
+ # common code between dtas-sourceedit and dtas-sinkedit
10
+ module DTAS::EditClient # :nodoc:
11
+ def editor
12
+ %w(VISUAL EDITOR).each do |key|
13
+ v = ENV[key] or next
14
+ v.empty? and next
15
+ return v
16
+ end
17
+ "vi"
18
+ end
19
+
20
+ def client_socket
21
+ DTAS::UNIXClient.new
22
+ rescue
23
+ e = "DTAS_PLAYER_SOCK=#{DTAS::UNIXClient.default_path}"
24
+ abort "dtas-player not running on #{e}"
25
+ end
26
+
27
+ def tmpyaml
28
+ tmp = Tempfile.new(%W(#{File.basename($0)} .yml))
29
+ tmp.sync = true
30
+ tmp.binmode
31
+ tmp
32
+ end
33
+
34
+ def update_cmd_env(cmd, orig, updated)
35
+ if env = updated["env"]
36
+ env.each do |k,v|
37
+ cmd << (v.nil? ? "env##{k}" : "env.#{k}=#{v}")
38
+ end
39
+ end
40
+
41
+ # remove deleted env
42
+ if orig_env = orig["env"]
43
+ env ||= {}
44
+ deleted_keys = orig_env.keys - env.keys
45
+ deleted_keys.each { |k| cmd << "env##{k}" }
46
+ end
47
+ end
48
+ end
@@ -64,7 +64,7 @@ class DTAS::Format # :nodoc:
64
64
  end
65
65
 
66
66
  def inspect
67
- "<#{self.class}(#{xs(to_sox_arg)})>"
67
+ "<#{self.class}(#{Shellwords.join(to_sox_arg)})>"
68
68
  end
69
69
 
70
70
  def to_hsh
@@ -75,13 +75,6 @@ class DTAS::Format # :nodoc:
75
75
  ivars_to_hash(SIVS)
76
76
  end
77
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
78
  # for the _decoded_ output
86
79
  def bits_per_sample
87
80
  return @bits if @bits
@@ -120,7 +113,7 @@ class DTAS::Format # :nodoc:
120
113
  end
121
114
 
122
115
  def valid_type?(type)
123
- !!(type =~ %r{\A[us](?:8|16|24|32)\z} || type =~ %r{\Af?:(32|64)})
116
+ !!(type =~ %r{\A[us](?:8|16|24|32)\z} || type =~ %r{\Af(?:32|64)\z})
124
117
  end
125
118
 
126
119
  def valid_endian?(endian)
@@ -5,7 +5,10 @@ require 'yaml'
5
5
  require 'shellwords'
6
6
  require_relative '../dtas'
7
7
  require_relative 'source'
8
- require_relative 'source/command'
8
+ require_relative 'source/sox'
9
+ require_relative 'source/av'
10
+ require_relative 'source/ff'
11
+ require_relative 'source/cmd'
9
12
  require_relative 'sink'
10
13
  require_relative 'unix_server'
11
14
  require_relative 'buffer'
@@ -24,11 +27,9 @@ class DTAS::Player # :nodoc:
24
27
  @state_file = nil
25
28
  @socket = nil
26
29
  @srv = nil
27
- @queue = [] # sources
30
+ @queue = [] # files for sources, or commands
28
31
  @paused = false
29
32
  @format = DTAS::Format.new
30
- @srccmd = nil
31
- @srcenv = {}
32
33
 
33
34
  @sinks = {} # { user-defined name => sink }
34
35
  @targets = [] # order matters
@@ -38,6 +39,16 @@ class DTAS::Player # :nodoc:
38
39
  @sink_buf = DTAS::Buffer.new
39
40
  @current = nil
40
41
  @watchers = {}
42
+ @source_map = {
43
+ "sox" => DTAS::Source::Sox.new,
44
+ "av" => DTAS::Source::Av.new,
45
+ "ff" => DTAS::Source::Ff.new,
46
+ }
47
+ source_map_reload
48
+ end
49
+
50
+ def source_map_reload
51
+ @sources = @source_map.values.sort_by { |src| src.tryorder }
41
52
  end
42
53
 
43
54
  def echo(msg)
@@ -57,13 +68,16 @@ class DTAS::Player # :nodoc:
57
68
  $stdout.write(msg << "\n")
58
69
  end
59
70
 
71
+ # used for state file
60
72
  def to_hsh
61
73
  rv = {}
62
74
  rv["socket"] = @socket
63
75
  rv["paused"] = @paused if @paused
64
- src = rv["source"] = {}
65
- src["command"] = @srccmd if @srccmd
66
- src["env"] = @srcenv if @srcenv.size > 0
76
+ src_map = rv["source"] = {}
77
+ @source_map.each do |name, src|
78
+ src_hsh = src.to_state_hash
79
+ src_map[name] = src_hsh unless src_hsh.empty?
80
+ end
67
81
 
68
82
  # Arrays
69
83
  rv["queue"] = @queue
@@ -106,8 +120,22 @@ class DTAS::Player # :nodoc:
106
120
  instance_variable_set("@#{k}", v)
107
121
  end
108
122
  if v = hash["source"]
109
- @srccmd = v["command"]
110
- e = v["env"] and @srcenv = e
123
+ # compatibility with 0.0.0, which was sox-only
124
+ # we'll drop this after 1.0.0, or when we support a source decoder
125
+ # named "command" or "env" :P
126
+ sox_cmd, sox_env = v["command"], v["env"]
127
+ if sox_cmd || sox_env
128
+ sox = @source_map["sox"]
129
+ sox.command = sox_cmd if sox_cmd
130
+ sox.env = sox_env if sox_env
131
+ end
132
+
133
+ # new style: name = "av" or "sox" or whatever else we may support
134
+ @source_map.each do |name, src|
135
+ src_hsh = v[name] or next
136
+ src.load!(src_hsh)
137
+ end
138
+ source_map_reload
111
139
  end
112
140
 
113
141
  if v = hash["format"]
@@ -233,19 +261,16 @@ class DTAS::Player # :nodoc:
233
261
  warn("#{sink.name} died unexpectedly: #{status.inspect}")
234
262
  deleted.each { |t| drop_target(t) }
235
263
  __current_drop unless @targets[0]
264
+ return # sink stays dead if it died unexpectedly
236
265
  end
237
266
 
238
267
  return unless sink.active
239
268
 
240
- if @queue[0] && !@paused
269
+ if (@current || @queue[0]) && !@paused
241
270
  # we get here if source/sinks are all killed in restart_pipeline
242
271
  __sink_activate(sink)
243
- next_source(@queue.shift)
244
- elsif sink.respawn
245
- __sink_activate(sink) if @current
272
+ next_source(@queue.shift) unless @current
246
273
  end
247
- ensure
248
- sink.respawn = false
249
274
  end
250
275
 
251
276
  # returns a wait_ctl arg for self
@@ -298,6 +323,21 @@ class DTAS::Player # :nodoc:
298
323
  end
299
324
  end
300
325
 
326
+ def try_file(*args)
327
+ @sources.each do |src|
328
+ rv = src.try(*args) and return rv
329
+ end
330
+
331
+ # keep going down the list until we find something
332
+ while source_spec = @queue.shift
333
+ @sources.each do |src|
334
+ rv = src.try(*source_spec) and return rv
335
+ end
336
+ end
337
+ echo "idle"
338
+ nil
339
+ end
340
+
301
341
  def next_source(source_spec)
302
342
  @current = nil
303
343
  if source_spec
@@ -306,24 +346,20 @@ class DTAS::Player # :nodoc:
306
346
 
307
347
  case source_spec
308
348
  when String
309
- @current = DTAS::Source.new(source_spec)
310
- echo(%W(file #{@current.infile}))
349
+ pending = try_file(source_spec) or return
350
+ echo(%W(file #{pending.infile}))
311
351
  when Array
312
- @current = DTAS::Source.new(*source_spec)
313
- echo(%W(file #{@current.infile} #{@current.offset_samples}s))
352
+ pending = try_file(*source_spec) or return
353
+ echo(%W(file #{pending.infile} #{pending.offset_samples}s))
314
354
  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?
355
+ pending = DTAS::Source::Cmd.new(source_spec["command"])
356
+ echo(%W(command #{pending.command_string}))
322
357
  end
323
358
 
324
359
  dst = @sink_buf
325
- @current.dst_assoc(dst)
326
- @current.spawn(@format, @rg, out: dst.wr, in: "/dev/null")
360
+ pending.dst_assoc(dst)
361
+ pending.spawn(@format, @rg, out: dst.wr, in: "/dev/null")
362
+ @current = pending
327
363
  @srv.wait_ctl(dst, :wait_readable)
328
364
  else
329
365
  stop_sinks if @sink_buf.inflight == 0
@@ -91,7 +91,9 @@ module DTAS::Player::ClientHandler # :nodoc:
91
91
  # do not reactivate it until we've reaped it
92
92
  if sink.pid
93
93
  drop_sink(sink)
94
- sink.respawn = true
94
+
95
+ # we must restart @current if there's a moment we're target-less:
96
+ __current_requeue unless @targets[0]
95
97
  else
96
98
  __sink_activate(sink)
97
99
  end
@@ -105,6 +107,10 @@ module DTAS::Player::ClientHandler # :nodoc:
105
107
  @srv.wait_ctl(@sink_buf, :wait_readable)
106
108
  end
107
109
 
110
+ def __sink_snapshot(sink)
111
+ [ sink.command, sink.env, sink.pipe_size ].inspect
112
+ end
113
+
108
114
  # returns a wait_ctl arg
109
115
  def sink_handler(io, msg)
110
116
  name = msg[1]
@@ -124,6 +130,7 @@ module DTAS::Player::ClientHandler # :nodoc:
124
130
 
125
131
  sink.name = name
126
132
  active_before = sink.active
133
+ before = __sink_snapshot(sink)
127
134
 
128
135
  # multiple changes may be made at once
129
136
  msg[2..-1].each do |kv|
@@ -142,7 +149,7 @@ module DTAS::Player::ClientHandler # :nodoc:
142
149
  rv = set_bool(io, kv, v) { |b| sink.__send__("#{k}=", b) }
143
150
  rv == true or return rv
144
151
  when "pipe_size"
145
- rv = set_uint(io, kv, v, true) { |u| sink.__send__("#{k}=", u) }
152
+ rv = set_uint(io, kv, v, false) { |u| sink.pipe_size = u }
146
153
  rv == true or return rv
147
154
  when "command" # nothing to validate, this could be "rm -rf /" :>
148
155
  sink.command = v.empty? ? DTAS::Sink::SINK_DEFAULTS["command"] : v
@@ -150,11 +157,13 @@ module DTAS::Player::ClientHandler # :nodoc:
150
157
  end
151
158
 
152
159
  @sinks[name] = new_sink if new_sink # no errors? it's a new sink!
160
+ after = __sink_snapshot(sink)
153
161
 
154
162
  # start or stop a sink if its active= flag changed. Additionally,
155
163
  # account for a crashed-but-marked-active sink. The user may have
156
164
  # fixed the command to not crash it.
157
- if (active_before != sink.active) || (sink.active && !sink.pid)
165
+ if (active_before != sink.active) ||
166
+ (sink.active && (!sink.pid || before != after))
158
167
  __sink_switch(sink)
159
168
  end
160
169
  io.emit("OK")
@@ -190,7 +199,7 @@ module DTAS::Player::ClientHandler # :nodoc:
190
199
  end
191
200
 
192
201
  def __current_requeue
193
- return unless @current && @current.pid
202
+ return unless @current
194
203
 
195
204
  # no need to requeue if we're already due to die
196
205
  return if @current.requeued
@@ -204,7 +213,7 @@ module DTAS::Player::ClientHandler # :nodoc:
204
213
  # this offset in the @current.format (not player @format)
205
214
  @queue.unshift([ @current.infile, "#{__current_decoded_samples}s" ])
206
215
  else
207
- # DTAS::Source::Command (hash), just rerun it
216
+ # DTAS::Source::Cmd (hash), just rerun it
208
217
  @queue.unshift(@current.to_hsh)
209
218
  end
210
219
  # We also want to hard drop the buffer so we do not get repeated audio.
@@ -299,7 +308,7 @@ module DTAS::Player::ClientHandler # :nodoc:
299
308
  io.emit(tmp.to_yaml)
300
309
  end
301
310
 
302
- def __buf_reset(buf)
311
+ def __buf_reset(buf) # buf is always @sink_buf for now
303
312
  @srv.wait_ctl(buf, :ignore)
304
313
  buf.buf_reset
305
314
  @srv.wait_ctl(buf, :wait_readable)
@@ -332,7 +341,7 @@ module DTAS::Player::ClientHandler # :nodoc:
332
341
  def do_play
333
342
  # no echo, next_source will echo on new track
334
343
  @paused = false
335
- return if @current && @current.pid
344
+ return if @current
336
345
  next_source(@queue.shift)
337
346
  end
338
347
 
@@ -341,7 +350,7 @@ module DTAS::Player::ClientHandler # :nodoc:
341
350
  end
342
351
 
343
352
  def do_seek(io, offset)
344
- if @current && @current.pid
353
+ if @current
345
354
  if @current.respond_to?(:infile)
346
355
  begin
347
356
  if offset.sub!(/\A\+/, '')
@@ -375,7 +384,6 @@ module DTAS::Player::ClientHandler # :nodoc:
375
384
  def restart_pipeline
376
385
  return if @paused
377
386
  __current_requeue
378
- @sinks.each_value { |sink| sink.respawn = sink.active }
379
387
  @targets.each { |t| drop_target(t) }.clear
380
388
  end
381
389
 
@@ -421,28 +429,39 @@ module DTAS::Player::ClientHandler # :nodoc:
421
429
  end
422
430
 
423
431
  def source_handler(io, msg)
424
- case msg.shift
432
+ map = @source_map
433
+ op = msg.shift
434
+ if op == "ls"
435
+ s = map.keys.sort { |a,b| map[a].tryorder <=> map[b].tryorder }
436
+ return io.emit(s.join(' '))
437
+ end
438
+
439
+ name = msg.shift
440
+ src = map[name] or return io.emit("ERR non-existent source name")
441
+ case op
425
442
  when "cat"
426
- io.emit({
427
- "command" => @srccmd || DTAS::Source::SOURCE_DEFAULTS["command"],
428
- "env" => @srcenv,
429
- }.to_yaml)
443
+ io.emit(src.to_source_cat.to_yaml)
430
444
  when "ed"
431
- before = [ @srccmd, @srcenv ].inspect
445
+ before = src.to_state_hash
446
+ sd = src.source_defaults
432
447
  msg.each do |kv|
433
448
  k, v = kv.split(/=/, 2)
434
449
  case k
435
450
  when "command"
436
- @srccmd = v.empty? ? nil : v
451
+ src.command = v.empty? ? sd[k] : v
437
452
  when %r{\Aenv\.([^=]+)\z}
438
- @srcenv[$1] = v
453
+ src.env[$1] = v
439
454
  when %r{\Aenv#([^=]+)\z}
440
455
  v == nil or return io.emit("ERR unset env has no value")
441
- @srcenv.delete($1)
456
+ src.env.delete($1)
457
+ when "tryorder"
458
+ rv = set_int(io, kv, v, true) { |i| src.tryorder = i || sd[k] }
459
+ rv == true or return rv
460
+ source_map_reload
442
461
  end
443
462
  end
444
- after = [ @srccmd, @srcenv ].inspect
445
- __current_requeue if before != after
463
+ after = src.to_state_hash
464
+ __current_requeue if before != after && @current.class == before.class
446
465
  io.emit("OK")
447
466
  else
448
467
  io.emit("ERR unknown source op")