dtas 0.3.0 → 0.4.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 (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?