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.
- checksums.yaml +4 -4
- data/Documentation/GNUmakefile +2 -0
- data/Documentation/dtas-player_protocol.txt +28 -1
- data/Documentation/dtas-splitfx.txt +167 -0
- data/Documentation/dtas-tl.txt +77 -0
- data/GIT-VERSION-GEN +2 -1
- data/GNUmakefile +1 -1
- data/README +2 -1
- data/Rakefile +7 -0
- data/bin/dtas-console +11 -1
- data/bin/dtas-splitfx +40 -0
- data/bin/dtas-tl +73 -0
- data/examples/README +3 -0
- data/examples/splitfx.sample.yml +19 -0
- data/lib/dtas/format.rb +11 -0
- data/lib/dtas/pipe.rb +0 -3
- data/lib/dtas/player.rb +38 -11
- data/lib/dtas/player/client_handler.rb +94 -7
- data/lib/dtas/process.rb +25 -3
- data/lib/dtas/sink.rb +0 -3
- data/lib/dtas/source/sox.rb +2 -1
- data/lib/dtas/splitfx.rb +342 -0
- data/lib/dtas/tracklist.rb +130 -0
- data/test/helper.rb +14 -1
- data/test/player_integration.rb +5 -3
- data/test/test_buffer.rb +4 -2
- data/test/test_env.rb +55 -0
- data/test/test_format.rb +1 -1
- data/test/test_format_change.rb +1 -1
- data/test/test_player.rb +1 -1
- data/test/test_player_client_handler.rb +1 -1
- data/test/test_player_integration.rb +3 -2
- data/test/test_process.rb +1 -1
- data/test/test_rg_integration.rb +4 -5
- data/test/test_rg_state.rb +1 -1
- data/test/test_sink.rb +1 -1
- data/test/test_sink_pipe_size.rb +1 -1
- data/test/test_sink_tee_integration.rb +1 -1
- data/test/test_source_av.rb +1 -1
- data/test/test_source_sox.rb +1 -1
- data/test/test_splitfx.rb +79 -0
- data/test/test_tracklist.rb +76 -0
- data/test/test_unixserver.rb +1 -1
- data/test/test_util.rb +1 -1
- 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,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
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
|
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
|
-
|
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 :
|
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(
|
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
|
-
|
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
|
-
|
375
|
+
wall(%W(file #{pending.infile}))
|
354
376
|
when Array
|
355
377
|
pending = try_file(*source_spec) or return
|
356
|
-
|
378
|
+
wall(%W(file #{pending.infile} #{pending.offset_samples}s))
|
357
379
|
else
|
358
380
|
pending = DTAS::Source::Cmd.new(source_spec["command"])
|
359
|
-
|
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
|
-
|
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 :
|
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
|
-
|
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
|
-
|
337
|
+
wall("pause")
|
338
338
|
@paused = true
|
339
339
|
__current_requeue
|
340
340
|
end
|
341
341
|
|
342
342
|
def do_play
|
343
|
-
# no
|
343
|
+
# no wall, next_source will wall on new track
|
344
344
|
@paused = false
|
345
345
|
return if @current
|
346
|
-
next_source(
|
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
|
-
|
367
|
-
|
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
|
-
#
|
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?
|