dtas 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Documentation/GNUmakefile +2 -0
  3. data/Documentation/dtas-player_protocol.txt +28 -1
  4. data/Documentation/dtas-splitfx.txt +167 -0
  5. data/Documentation/dtas-tl.txt +77 -0
  6. data/GIT-VERSION-GEN +2 -1
  7. data/GNUmakefile +1 -1
  8. data/README +2 -1
  9. data/Rakefile +7 -0
  10. data/bin/dtas-console +11 -1
  11. data/bin/dtas-splitfx +40 -0
  12. data/bin/dtas-tl +73 -0
  13. data/examples/README +3 -0
  14. data/examples/splitfx.sample.yml +19 -0
  15. data/lib/dtas/format.rb +11 -0
  16. data/lib/dtas/pipe.rb +0 -3
  17. data/lib/dtas/player.rb +38 -11
  18. data/lib/dtas/player/client_handler.rb +94 -7
  19. data/lib/dtas/process.rb +25 -3
  20. data/lib/dtas/sink.rb +0 -3
  21. data/lib/dtas/source/sox.rb +2 -1
  22. data/lib/dtas/splitfx.rb +342 -0
  23. data/lib/dtas/tracklist.rb +130 -0
  24. data/test/helper.rb +14 -1
  25. data/test/player_integration.rb +5 -3
  26. data/test/test_buffer.rb +4 -2
  27. data/test/test_env.rb +55 -0
  28. data/test/test_format.rb +1 -1
  29. data/test/test_format_change.rb +1 -1
  30. data/test/test_player.rb +1 -1
  31. data/test/test_player_client_handler.rb +1 -1
  32. data/test/test_player_integration.rb +3 -2
  33. data/test/test_process.rb +1 -1
  34. data/test/test_rg_integration.rb +4 -5
  35. data/test/test_rg_state.rb +1 -1
  36. data/test/test_sink.rb +1 -1
  37. data/test/test_sink_pipe_size.rb +1 -1
  38. data/test/test_sink_tee_integration.rb +1 -1
  39. data/test/test_source_av.rb +1 -1
  40. data/test/test_source_sox.rb +1 -1
  41. data/test/test_splitfx.rb +79 -0
  42. data/test/test_tracklist.rb +76 -0
  43. data/test/test_unixserver.rb +1 -1
  44. data/test/test_util.rb +1 -1
  45. metadata +23 -3
data/bin/dtas-tl ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ #
5
+ # WARNING: totally unstable API, use dtas-ctl for scripting (but the protocol
6
+ # itself is also unstable, but better than this one probably).
7
+ require 'dtas/unix_client'
8
+ require 'yaml'
9
+ require 'shellwords'
10
+
11
+ def get_track_ids(c)
12
+ track_ids = c.req("tl tracks")
13
+ # we could get more, but SEQPACKET limits size...
14
+ track_ids = track_ids.split(/ /)
15
+ track_ids.shift
16
+ track_ids
17
+ end
18
+
19
+ c = DTAS::UNIXClient.new
20
+ case cmd = ARGV[0]
21
+ when "cat"
22
+ get_track_ids(c).each do |track_id|
23
+ res = c.req("tl get #{track_id}")
24
+ res.sub!(/\A1 /, '')
25
+ puts res
26
+ end
27
+ when "clear"
28
+ get_track_ids(c).each do |track_id|
29
+ puts "#{track_id} " << c.req("tl remove #{track_id}")
30
+ end
31
+ when "addhead"
32
+ ARGV.shift
33
+ ARGV.reverse.each do |path|
34
+ path = File.expand_path(path.b)
35
+ res = c.req(%W(tl add #{path}))
36
+ puts "#{path} #{res}"
37
+ end
38
+ when "addtail"
39
+ ARGV.shift
40
+ track_ids = get_track_ids(c)
41
+ last_id = track_ids.pop
42
+ ARGV.each do |path|
43
+ path = File.expand_path(path.b)
44
+ req = %W(tl add #{path})
45
+ req << last_id.to_s if last_id
46
+ res = c.req(req)
47
+ puts "#{path} #{res}"
48
+ last_id = res if res =~ /\A\d+\z/
49
+ end
50
+ when "reto"
51
+ fixed = ARGV.delete("-F")
52
+ ignorecase = ARGV.delete("-i")
53
+ re = ARGV[1]
54
+ time = ARGV[2]
55
+ re = Regexp.quote(re) if fixed
56
+ re = ignorecase ? %r{#{re}}i : %r{#{re}}
57
+ get_track_ids(c).each do |track_id|
58
+ res = c.req("tl get #{track_id}")
59
+ res.sub!(/\A1 /, '')
60
+ if re =~ res
61
+ req = %W(tl goto #{track_id})
62
+ req << time if time
63
+ res = c.req(req)
64
+ puts res
65
+ exit(res == "OK")
66
+ end
67
+ end
68
+ warn "#{re.inspect} not found"
69
+ exit 1
70
+ else
71
+ # act like dtas-ctl for now...
72
+ puts c.req([ "tl", *ARGV ])
73
+ end
data/examples/README ADDED
@@ -0,0 +1,3 @@
1
+ All files in this example directory (including this one) are CC0:
2
+ To the extent possible under law, Eric Wong has waived all copyright and
3
+ related or neighboring rights to these examples.
@@ -0,0 +1,19 @@
1
+ # To the extent possible under law, Eric Wong has waived all copyright and
2
+ # related or neighboring rights to this example.
3
+ # Note: be sure to update test/test_splitfx.rb if you change this,
4
+ # test_splitfx.rb relies on this.
5
+ ---
6
+ infile: foo.flac
7
+ env:
8
+ PATH: $PATH
9
+ SOX_OPTS: $SOX_OPTS -R
10
+ comments:
11
+ ARTIST: John Smith
12
+ ALBUM: Hello World
13
+ YEAR: 2013
14
+ track_start: 1 # 0 for pregap/intro tracks
15
+ cdda_align: true
16
+ tracks:
17
+ - t 0:04 "track one"
18
+ - t 0:10 "track two"
19
+ - stop 24
data/lib/dtas/format.rb CHANGED
@@ -59,6 +59,16 @@ class DTAS::Format # :nodoc:
59
59
  end
60
60
  end
61
61
 
62
+ # returns 1 or 0 depending on endianess
63
+ def endian_opusenc
64
+ case e = @endian || NATIVE_ENDIAN
65
+ when "big" then "1"
66
+ when "little" then "0"
67
+ else
68
+ raise"unsupported endian=#{e}"
69
+ end
70
+ end
71
+
62
72
  def to_eca_arg
63
73
  %W(-f #{@type}_#{endian2},#@channels,#@rate)
64
74
  end
@@ -92,6 +102,7 @@ class DTAS::Format # :nodoc:
92
102
  "SOXFMT" => to_sox_arg.join(' '),
93
103
  "ECAFMT" => to_eca_arg.join(' '),
94
104
  "ENDIAN2" => endian2,
105
+ "ENDIAN_OPUSENC" => endian_opusenc,
95
106
  }
96
107
  begin # don't set these if we can't get them, SOX_FILETYPE may be enough
97
108
  rv["BITS_PER_SAMPLE"] = bits_per_sample.to_s
data/lib/dtas/pipe.rb CHANGED
@@ -21,9 +21,6 @@ class DTAS::Pipe < IO # :nodoc:
21
21
  unless method_defined?(:pipe_size=)
22
22
  def pipe_size=(_)
23
23
  end
24
-
25
- def pipe_size
26
- end
27
24
  end
28
25
  end
29
26
 
data/lib/dtas/player.rb CHANGED
@@ -15,6 +15,7 @@ require_relative 'buffer'
15
15
  require_relative 'sigevent'
16
16
  require_relative 'rg_state'
17
17
  require_relative 'state_file'
18
+ require_relative 'tracklist'
18
19
 
19
20
  class DTAS::Player # :nodoc:
20
21
  require_relative 'player/client_handler'
@@ -25,6 +26,7 @@ class DTAS::Player # :nodoc:
25
26
  attr_reader :sinks
26
27
 
27
28
  def initialize
29
+ @tl = DTAS::Tracklist.new
28
30
  @state_file = nil
29
31
  @socket = nil
30
32
  @srv = nil
@@ -52,7 +54,7 @@ class DTAS::Player # :nodoc:
52
54
  @sources = @source_map.values.sort_by { |src| src.tryorder }
53
55
  end
54
56
 
55
- def echo(msg)
57
+ def wall(msg)
56
58
  msg = xs(Array(msg))
57
59
  @watchers.delete_if do |io, _|
58
60
  if io.closed?
@@ -87,6 +89,8 @@ class DTAS::Player # :nodoc:
87
89
  rv[k] = instance_variable_get("@#{k}").to_hsh
88
90
  end
89
91
 
92
+ rv["tracklist"] = @tl.to_hsh
93
+
90
94
  # no empty hashes or arrays
91
95
  rv.delete_if do |k,v|
92
96
  case v
@@ -111,6 +115,9 @@ class DTAS::Player # :nodoc:
111
115
  def self.load(hash)
112
116
  rv = new
113
117
  rv.instance_eval do
118
+ if v = hash["tracklist"]
119
+ @tl = DTAS::Tracklist.load(v)
120
+ end
114
121
  @rg = DTAS::RGState.load(hash["rg"])
115
122
  if v = hash["sink_buf"]
116
123
  v = v["buffer_size"]
@@ -190,7 +197,7 @@ class DTAS::Player # :nodoc:
190
197
  do_seek(io, msg[0])
191
198
  when "clear"
192
199
  @queue.clear
193
- echo("clear")
200
+ wall("clear")
194
201
  io.emit("OK")
195
202
  when "rg"
196
203
  rg_handler(io, msg)
@@ -218,6 +225,8 @@ class DTAS::Player # :nodoc:
218
225
  chdir_handler(io, msg)
219
226
  when "pwd"
220
227
  io.emit(Dir.pwd)
228
+ when "tl"
229
+ tl_handler(io, msg)
221
230
  end
222
231
  end
223
232
 
@@ -242,7 +251,7 @@ class DTAS::Player # :nodoc:
242
251
  obj.on_death(status) if obj.respond_to?(:on_death)
243
252
  case obj
244
253
  when @current
245
- next_source(@paused ? nil : @queue.shift)
254
+ next_source(@paused ? nil : _next)
246
255
  when DTAS::Sink # on unexpected sink death
247
256
  sink_death(obj, status)
248
257
  end
@@ -250,6 +259,10 @@ class DTAS::Player # :nodoc:
250
259
  :wait_readable
251
260
  end
252
261
 
262
+ def _next
263
+ @queue.shift || @tl.advance_track
264
+ end
265
+
253
266
  def sink_death(sink, status)
254
267
  deleted = []
255
268
  @targets.delete_if do |t|
@@ -272,7 +285,7 @@ class DTAS::Player # :nodoc:
272
285
  if (@current || @queue[0]) && !@paused
273
286
  # we get here if source/sinks are all killed in restart_pipeline
274
287
  __sink_activate(sink)
275
- next_source(@queue.shift) unless @current
288
+ next_source(_next) unless @current
276
289
  end
277
290
  end
278
291
 
@@ -337,7 +350,16 @@ class DTAS::Player # :nodoc:
337
350
  rv = src.try(*source_spec) and return rv
338
351
  end
339
352
  end
340
- echo "idle"
353
+
354
+ # don't get stuck in an infinite loop if @tl.repeat==true and we can't
355
+ # decode anything (FS errors, sox uninstalled, etc...)
356
+ while path_off = @tl.advance_track(false)
357
+ @sources.each do |src|
358
+ rv = src.try(*path_off) and return rv
359
+ end
360
+ end
361
+
362
+ player_idle
341
363
  nil
342
364
  end
343
365
 
@@ -350,13 +372,13 @@ class DTAS::Player # :nodoc:
350
372
  case source_spec
351
373
  when String
352
374
  pending = try_file(source_spec) or return
353
- echo(%W(file #{pending.infile}))
375
+ wall(%W(file #{pending.infile}))
354
376
  when Array
355
377
  pending = try_file(*source_spec) or return
356
- echo(%W(file #{pending.infile} #{pending.offset_samples}s))
378
+ wall(%W(file #{pending.infile} #{pending.offset_samples}s))
357
379
  else
358
380
  pending = DTAS::Source::Cmd.new(source_spec["command"])
359
- echo(%W(command #{pending.command_string}))
381
+ wall(%W(command #{pending.command_string}))
360
382
  end
361
383
 
362
384
  dst = @sink_buf
@@ -365,11 +387,16 @@ class DTAS::Player # :nodoc:
365
387
  @current = pending
366
388
  @srv.wait_ctl(dst, :wait_readable)
367
389
  else
368
- stop_sinks if @sink_buf.inflight == 0
369
- echo "idle"
390
+ player_idle
370
391
  end
371
392
  end
372
393
 
394
+ def player_idle
395
+ stop_sinks if @sink_buf.inflight == 0
396
+ @tl.reset unless @paused
397
+ wall("idle")
398
+ end
399
+
373
400
  def drop_target(target)
374
401
  @srv.wait_ctl(target, :delete)
375
402
  target.close
@@ -410,7 +437,7 @@ class DTAS::Player # :nodoc:
410
437
  @srv.wait_ctl(sev, :wait_readable)
411
438
  old_chld = trap(:CHLD) { sev.signal }
412
439
  create_default_sink
413
- next_source(@paused ? nil : @queue.shift)
440
+ next_source(@paused ? nil : _next)
414
441
  begin
415
442
  event_loop_iter
416
443
  rescue => e # just in case...
@@ -317,7 +317,7 @@ module DTAS::Player::ClientHandler # :nodoc:
317
317
 
318
318
  def skip_handler(io, msg)
319
319
  __current_drop
320
- echo("skip")
320
+ wall("skip")
321
321
  io.emit("OK")
322
322
  end
323
323
 
@@ -334,16 +334,16 @@ module DTAS::Player::ClientHandler # :nodoc:
334
334
 
335
335
  def do_pause
336
336
  return if @paused
337
- echo("pause")
337
+ wall("pause")
338
338
  @paused = true
339
339
  __current_requeue
340
340
  end
341
341
 
342
342
  def do_play
343
- # no echo, next_source will echo on new track
343
+ # no wall, next_source will wall on new track
344
344
  @paused = false
345
345
  return if @current
346
- next_source(@queue.shift)
346
+ next_source(_next)
347
347
  end
348
348
 
349
349
  def do_play_pause
@@ -363,8 +363,13 @@ module DTAS::Player::ClientHandler # :nodoc:
363
363
  rescue ArgumentError
364
364
  return io.emit("ERR bad time format")
365
365
  end
366
- @queue.unshift([ @current.infile, offset ])
367
- __buf_reset(@current.dst) # trigger EPIPE
366
+ if @current.requeued
367
+ @queue[0][1] = offset
368
+ else
369
+ @queue.unshift([ @current.infile, offset ])
370
+ @current.requeued = true
371
+ __buf_reset(@current.dst) # trigger EPIPE
372
+ end
368
373
  else
369
374
  return io.emit("ERR unseekable")
370
375
  end
@@ -484,7 +489,7 @@ module DTAS::Player::ClientHandler # :nodoc:
484
489
  rescue => e
485
490
  return io.emit("ERR chdir: #{e.message}")
486
491
  end
487
- # echo(%W(cd msg[0])) # should we broadcast this?
492
+ # wall(%W(cd msg[0])) # should we broadcast this?
488
493
  io.emit("OK")
489
494
  end
490
495
 
@@ -508,5 +513,87 @@ module DTAS::Player::ClientHandler # :nodoc:
508
513
  end
509
514
  io.emit("OK")
510
515
  end
516
+
517
+ def _tl_skip
518
+ @queue.clear
519
+ __current_drop
520
+ end
521
+
522
+ def tl_handler(io, msg)
523
+ case msg.shift
524
+ when "add"
525
+ path = msg.shift
526
+ after_track_id = msg.shift
527
+ after_track_id = after_track_id.to_i if after_track_id
528
+ case set_as_current = msg.shift
529
+ when "true" then set_as_current = true
530
+ when "false", nil then set_as_current = false
531
+ else
532
+ return io.emit("ERR tl add PATH [after_track_id] [true|false]")
533
+ end
534
+ begin
535
+ track_id = @tl.add_track(path, after_track_id, set_as_current)
536
+ rescue ArgumentError => e
537
+ return io.emit("ERR #{e.message}")
538
+ end
539
+ _tl_skip if set_as_current
540
+
541
+ # start playing if we're the only track
542
+ if @tl.size == 1 && !(@current || @queue[0] || @paused)
543
+ next_source(_next)
544
+ end
545
+ io.emit("#{track_id}")
546
+ when "repeat"
547
+ case msg.shift
548
+ when "true" then @tl.repeat = true
549
+ when "false" then @tl.repeat = false
550
+ when "1" then @tl.repeat = 1
551
+ when nil
552
+ return io.emit("repeat #{@tl.repeat.to_s}")
553
+ end
554
+ io.emit("OK")
555
+ when "remove"
556
+ track_id = msg.shift or return io.emit("ERR track_id not specified")
557
+ track_id = track_id.to_i
558
+ cur = @tl.cur_track
559
+
560
+ # skip if we're removing the currently playing track
561
+ if cur.object_id == track_id && @current &&
562
+ @current.respond_to?(:infile) && @current.infile == cur
563
+ _tl_skip
564
+ end
565
+
566
+ io.emit(@tl.remove_track(track_id) ? "OK" : "MISSING")
567
+ when "get"
568
+ res = @tl.get_tracks(msg.map! { |i| i.to_i })
569
+ res.map! { |tid, file| "#{tid}=#{file ? Shellwords.escape(file) : ''}" }
570
+ io.emit("#{res.size} #{res.join(' ')}")
571
+ when "tracks"
572
+ tracks = @tl.tracks
573
+ io.emit("#{tracks.size} " << tracks.map! { |i| i.to_s }.join(' '))
574
+ when "goto"
575
+ track_id = msg.shift or return io.emit("ERR track_id not specified")
576
+ offset = msg.shift # may be nil
577
+ if @tl.go_to(track_id.to_i, offset)
578
+ _tl_skip
579
+ io.emit("OK")
580
+ else
581
+ io.emit("MISSING")
582
+ end
583
+ when "current"
584
+ path = @tl.cur_track
585
+ io.emit(path ? path : "NONE")
586
+ when "current-id"
587
+ path = @tl.cur_track
588
+ io.emit(path ? path.object_id.to_s : "NONE")
589
+ when "next"
590
+ _tl_skip
591
+ io.emit("OK")
592
+ when "previous"
593
+ @tl.previous!
594
+ _tl_skip
595
+ io.emit("OK")
596
+ end
597
+ end
511
598
  end
512
599
  # :startdoc:
data/lib/dtas/process.rb CHANGED
@@ -19,12 +19,33 @@ module DTAS::Process # :nodoc:
19
19
  end while true
20
20
  end
21
21
 
22
+ # expand common shell constructs based on environment variables
23
+ # this is order-dependent, but Ruby 1.9+ hashes are already order-dependent
24
+ def env_expand(env, opts)
25
+ env = env.dup
26
+ if false == opts.delete(:expand)
27
+ env.each do |key, val|
28
+ Numeric === val and env[key] = val.to_s
29
+ end
30
+ else
31
+ env.each do |key, val|
32
+ case val
33
+ when Numeric # stringify numeric values to simplify users' lives
34
+ env[key] = val.to_s
35
+ when /[\`\$]/ # perform variable/command expansion
36
+ tmp = env.dup
37
+ tmp.delete(key)
38
+ val = qx(tmp, "echo #{val}", expand: false)
39
+ env[key] = val.chomp
40
+ end
41
+ end
42
+ end
43
+ end
44
+
22
45
  # for long-running processes (sox/play/ecasound filters)
23
46
  def dtas_spawn(env, cmd, opts)
24
47
  opts = { close_others: true, pgroup: true }.merge!(opts)
25
-
26
- # stringify env, integer values are easier to type unquoted as strings
27
- env.each { |k,v| env[k] = v.to_s }
48
+ env = env_expand(env, opts)
28
49
 
29
50
  pid = begin
30
51
  Process.spawn(env, cmd, opts)
@@ -53,6 +74,7 @@ module DTAS::Process # :nodoc:
53
74
  re.binmode
54
75
  opts[:err] = we
55
76
  end
77
+ env = env_expand(env, opts)
56
78
  pid = begin
57
79
  Process.spawn(env, *cmd, opts)
58
80
  rescue Errno::EINTR # Ruby bug?