dtas 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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?
|