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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Documentation/dtas-console.txt +15 -0
- data/Documentation/dtas-player_protocol.txt +5 -3
- data/Documentation/dtas-sourceedit.txt +14 -7
- data/GIT-VERSION-GEN +3 -3
- data/INSTALL +3 -3
- data/README +6 -5
- data/Rakefile +16 -7
- data/bin/dtas-console +48 -3
- data/bin/dtas-cueedit +1 -1
- data/bin/dtas-sinkedit +12 -28
- data/bin/dtas-sourceedit +15 -30
- data/lib/dtas.rb +1 -1
- data/lib/dtas/command.rb +0 -5
- data/lib/dtas/compat_onenine.rb +2 -2
- data/lib/dtas/disclaimer.rb +4 -3
- data/lib/dtas/edit_client.rb +48 -0
- data/lib/dtas/format.rb +2 -9
- data/lib/dtas/player.rb +64 -28
- data/lib/dtas/player/client_handler.rb +39 -20
- data/lib/dtas/process.rb +16 -15
- data/lib/dtas/replaygain.rb +19 -3
- data/lib/dtas/sink.rb +1 -2
- data/lib/dtas/source.rb +1 -141
- data/lib/dtas/source/av.rb +29 -0
- data/lib/dtas/source/av_ff_common.rb +127 -0
- data/lib/dtas/source/{command.rb → cmd.rb} +1 -1
- data/lib/dtas/source/ff.rb +30 -0
- data/lib/dtas/source/file.rb +94 -0
- data/lib/dtas/source/{mp3.rb → mp3gain.rb} +1 -1
- data/lib/dtas/source/sox.rb +114 -0
- data/lib/dtas/unix_client.rb +1 -9
- data/test/player_integration.rb +5 -17
- data/test/test_format.rb +10 -14
- data/test/test_format_change.rb +4 -8
- data/test/test_player_integration.rb +50 -62
- data/test/test_process.rb +33 -0
- data/test/test_rg_integration.rb +45 -35
- data/test/test_sink_pipe_size.rb +20 -0
- data/test/test_sink_tee_integration.rb +2 -4
- data/test/{test_source.rb → test_source_av.rb} +16 -16
- data/test/test_source_sox.rb +115 -0
- metadata +23 -12
- data/.rsync_doc +0 -3
data/lib/dtas.rb
CHANGED
data/lib/dtas/command.rb
CHANGED
data/lib/dtas/compat_onenine.rb
CHANGED
@@ -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
|
data/lib/dtas/disclaimer.rb
CHANGED
@@ -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
|
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
|
-
#
|
15
|
+
# #{DTAS_PROGNAME} will never prevent you from doing stupid things.
|
15
16
|
#
|
16
|
-
# There is no warranty, the developers of
|
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
|
data/lib/dtas/format.rb
CHANGED
@@ -64,7 +64,7 @@ class DTAS::Format # :nodoc:
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def inspect
|
67
|
-
"<#{self.class}(#{
|
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?:
|
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)
|
data/lib/dtas/player.rb
CHANGED
@@ -5,7 +5,10 @@ require 'yaml'
|
|
5
5
|
require 'shellwords'
|
6
6
|
require_relative '../dtas'
|
7
7
|
require_relative 'source'
|
8
|
-
require_relative 'source/
|
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
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
110
|
-
|
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
|
-
|
310
|
-
echo(%W(file #{
|
349
|
+
pending = try_file(source_spec) or return
|
350
|
+
echo(%W(file #{pending.infile}))
|
311
351
|
when Array
|
312
|
-
|
313
|
-
echo(%W(file #{
|
352
|
+
pending = try_file(*source_spec) or return
|
353
|
+
echo(%W(file #{pending.infile} #{pending.offset_samples}s))
|
314
354
|
else
|
315
|
-
|
316
|
-
echo(%W(command #{
|
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
|
-
|
326
|
-
|
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
|
-
|
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,
|
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) ||
|
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
|
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::
|
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
|
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
|
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
|
-
|
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 =
|
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
|
-
|
451
|
+
src.command = v.empty? ? sd[k] : v
|
437
452
|
when %r{\Aenv\.([^=]+)\z}
|
438
|
-
|
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
|
-
|
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 =
|
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")
|