dtas 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +9 -0
- data/.rsync_doc +3 -0
- data/COPYING +674 -0
- data/Documentation/.gitignore +3 -0
- data/Documentation/GNUmakefile +46 -0
- data/Documentation/dtas-console.txt +42 -0
- data/Documentation/dtas-ctl.txt +64 -0
- data/Documentation/dtas-cueedit.txt +24 -0
- data/Documentation/dtas-enq.txt +29 -0
- data/Documentation/dtas-msinkctl.txt +45 -0
- data/Documentation/dtas-player.txt +110 -0
- data/Documentation/dtas-player_effects.txt +45 -0
- data/Documentation/dtas-player_protocol.txt +181 -0
- data/Documentation/dtas-sinkedit.txt +41 -0
- data/Documentation/dtas-sourceedit.txt +33 -0
- data/Documentation/dtas-xdelay.txt +57 -0
- data/Documentation/troubleshooting.txt +13 -0
- data/GIT-VERSION-GEN +30 -0
- data/GNUmakefile +9 -0
- data/HACKING +12 -0
- data/INSTALL +53 -0
- data/README +103 -0
- data/Rakefile +97 -0
- data/TODO +4 -0
- data/bin/dtas-console +160 -0
- data/bin/dtas-ctl +10 -0
- data/bin/dtas-cueedit +78 -0
- data/bin/dtas-enq +13 -0
- data/bin/dtas-msinkctl +51 -0
- data/bin/dtas-player +34 -0
- data/bin/dtas-sinkedit +58 -0
- data/bin/dtas-sourceedit +48 -0
- data/bin/dtas-xdelay +85 -0
- data/dtas-linux.gemspec +18 -0
- data/dtas-mpris.gemspec +16 -0
- data/examples/dtas_state.yml +18 -0
- data/lib/dtas.rb +7 -0
- data/lib/dtas/buffer.rb +90 -0
- data/lib/dtas/buffer/read_write.rb +102 -0
- data/lib/dtas/buffer/splice.rb +142 -0
- data/lib/dtas/command.rb +43 -0
- data/lib/dtas/compat_onenine.rb +18 -0
- data/lib/dtas/disclaimer.rb +18 -0
- data/lib/dtas/format.rb +151 -0
- data/lib/dtas/pipe.rb +39 -0
- data/lib/dtas/player.rb +393 -0
- data/lib/dtas/player/client_handler.rb +463 -0
- data/lib/dtas/process.rb +87 -0
- data/lib/dtas/replaygain.rb +41 -0
- data/lib/dtas/rg_state.rb +99 -0
- data/lib/dtas/serialize.rb +9 -0
- data/lib/dtas/sigevent.rb +10 -0
- data/lib/dtas/sigevent/efd.rb +20 -0
- data/lib/dtas/sigevent/pipe.rb +28 -0
- data/lib/dtas/sink.rb +121 -0
- data/lib/dtas/source.rb +147 -0
- data/lib/dtas/source/command.rb +40 -0
- data/lib/dtas/source/common.rb +14 -0
- data/lib/dtas/source/mp3.rb +37 -0
- data/lib/dtas/state_file.rb +33 -0
- data/lib/dtas/unix_accepted.rb +76 -0
- data/lib/dtas/unix_client.rb +51 -0
- data/lib/dtas/unix_server.rb +110 -0
- data/lib/dtas/util.rb +15 -0
- data/lib/dtas/writable_iter.rb +22 -0
- data/perl/dtas-graph +129 -0
- data/pkg.mk +26 -0
- data/setup.rb +1586 -0
- data/test/covshow.rb +30 -0
- data/test/helper.rb +76 -0
- data/test/player_integration.rb +121 -0
- data/test/test_buffer.rb +216 -0
- data/test/test_format.rb +61 -0
- data/test/test_format_change.rb +49 -0
- data/test/test_player.rb +47 -0
- data/test/test_player_client_handler.rb +86 -0
- data/test/test_player_integration.rb +220 -0
- data/test/test_rg_integration.rb +117 -0
- data/test/test_rg_state.rb +32 -0
- data/test/test_sink.rb +32 -0
- data/test/test_sink_tee_integration.rb +34 -0
- data/test/test_source.rb +102 -0
- data/test/test_unixserver.rb +66 -0
- data/test/test_util.rb +15 -0
- metadata +208 -0
data/lib/dtas/command.rb
ADDED
@@ -0,0 +1,43 @@
|
|
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
|
+
# common code for wrapping SoX/ecasound/... commands
|
5
|
+
require_relative 'serialize'
|
6
|
+
require 'shellwords'
|
7
|
+
|
8
|
+
module DTAS::Command # :nodoc:
|
9
|
+
include DTAS::Serialize
|
10
|
+
attr_reader :pid
|
11
|
+
attr_reader :to_io
|
12
|
+
attr_accessor :command
|
13
|
+
attr_accessor :env
|
14
|
+
attr_accessor :spawn_at
|
15
|
+
|
16
|
+
COMMAND_DEFAULTS = {
|
17
|
+
"env" => {},
|
18
|
+
"command" => nil,
|
19
|
+
}
|
20
|
+
|
21
|
+
def command_init(defaults = {})
|
22
|
+
@pid = nil
|
23
|
+
@to_io = nil
|
24
|
+
@spawn_at = nil
|
25
|
+
COMMAND_DEFAULTS.merge(defaults).each do |k,v|
|
26
|
+
v = v.dup if Hash === v || Array === v
|
27
|
+
instance_variable_set("@#{k}", v)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def kill(sig = :TERM)
|
32
|
+
# always kill the pgroup since we run subcommands in their own shell
|
33
|
+
Process.kill(sig, -@pid)
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_death(status)
|
37
|
+
@pid = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def command_string
|
41
|
+
@command
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
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
|
+
|
5
|
+
# Make Ruby 1.9.3 look like Ruby 2.0.0 to us
|
6
|
+
# This exists for Debian wheezy users using the stock Ruby 1.9.3 install.
|
7
|
+
# We'll drop this interface when Debian wheezy (7.0) becomes unsupported.
|
8
|
+
class String
|
9
|
+
def b # :nodoc:
|
10
|
+
dup.force_encoding(Encoding::BINARY)
|
11
|
+
end
|
12
|
+
end unless String.method_defined?(:b)
|
13
|
+
|
14
|
+
def IO
|
15
|
+
def self.pipe # :nodoc:
|
16
|
+
super.map! { |io| io.close_on_exec = true; io }
|
17
|
+
end
|
18
|
+
end if RUBY_VERSION.to_f <= 1.9
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
5
|
+
DTAS_DISCLAIMER = <<EOF
|
6
|
+
# WARNING!
|
7
|
+
#
|
8
|
+
# Ignorant or improper use of #$0 may lead to
|
9
|
+
# data loss, hearing loss, and damage to audio equipment.
|
10
|
+
#
|
11
|
+
# Please read and understand the documentation of all commands you
|
12
|
+
# attempt to configure.
|
13
|
+
#
|
14
|
+
# #$0 will never prevent you from doing stupid things.
|
15
|
+
#
|
16
|
+
# There is no warranty, the developers of #$0
|
17
|
+
# are not responsible for your actions.
|
18
|
+
EOF
|
data/lib/dtas/format.rb
ADDED
@@ -0,0 +1,151 @@
|
|
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
|
+
# class represents an audio format (type/bits/channels/sample rate/...)
|
5
|
+
require_relative '../dtas'
|
6
|
+
require_relative 'process'
|
7
|
+
require_relative 'serialize'
|
8
|
+
|
9
|
+
class DTAS::Format # :nodoc:
|
10
|
+
include DTAS::Process
|
11
|
+
include DTAS::Serialize
|
12
|
+
NATIVE_ENDIAN = [1].pack("l") == [1].pack("l>") ? "big" : "little"
|
13
|
+
|
14
|
+
attr_accessor :type # s32, f32, f64 ... any point in others?
|
15
|
+
attr_accessor :channels # 1..666
|
16
|
+
attr_accessor :rate # 44100, 48000, 88200, 96000, 176400, 192000 ...
|
17
|
+
attr_accessor :bits # only set for playback on 16-bit DACs
|
18
|
+
attr_accessor :endian
|
19
|
+
|
20
|
+
FORMAT_DEFAULTS = {
|
21
|
+
"type" => "s32",
|
22
|
+
"channels" => 2,
|
23
|
+
"rate" => 44100,
|
24
|
+
"bits" => nil, # default: implied from type
|
25
|
+
"endian" => nil, # unspecified
|
26
|
+
}
|
27
|
+
SIVS = FORMAT_DEFAULTS.keys
|
28
|
+
|
29
|
+
def self.load(hash)
|
30
|
+
fmt = new
|
31
|
+
return fmt unless hash
|
32
|
+
(SIVS & hash.keys).each do |k|
|
33
|
+
fmt.instance_variable_set("@#{k}", hash[k])
|
34
|
+
end
|
35
|
+
fmt
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
FORMAT_DEFAULTS.each do |k,v|
|
40
|
+
instance_variable_set("@#{k}", v)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_sox_arg
|
45
|
+
rv = %W(-t#@type -c#@channels -r#@rate)
|
46
|
+
rv.concat(%W(-b#@bits)) if @bits # needed for play(1) to 16-bit DACs
|
47
|
+
rv
|
48
|
+
end
|
49
|
+
|
50
|
+
# returns 'be' or 'le' depending on endianess
|
51
|
+
def endian2
|
52
|
+
case e = @endian || NATIVE_ENDIAN
|
53
|
+
when "big"
|
54
|
+
"be"
|
55
|
+
when "little"
|
56
|
+
"le"
|
57
|
+
else
|
58
|
+
raise"unsupported endian=#{e}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_eca_arg
|
63
|
+
%W(-f #{@type}_#{endian2},#@channels,#@rate)
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
"<#{self.class}(#{xs(to_sox_arg)})>"
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_hsh
|
71
|
+
to_hash.delete_if { |k,v| v == FORMAT_DEFAULTS[k] }
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_hash
|
75
|
+
ivars_to_hash(SIVS)
|
76
|
+
end
|
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
|
+
# for the _decoded_ output
|
86
|
+
def bits_per_sample
|
87
|
+
return @bits if @bits
|
88
|
+
/\A[fst](8|16|24|32|64)\z/ =~ @type or
|
89
|
+
raise TypeError, "invalid type=#@type (must be s32/f32/f64)"
|
90
|
+
$1.to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
def bytes_per_sample
|
94
|
+
bits_per_sample / 8
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_env
|
98
|
+
rv = {
|
99
|
+
"SOX_FILETYPE" => @type,
|
100
|
+
"CHANNELS" => @channels.to_s,
|
101
|
+
"RATE" => @rate.to_s,
|
102
|
+
"ENDIAN" => @endian || NATIVE_ENDIAN,
|
103
|
+
"SOXFMT" => to_sox_arg.join(' '),
|
104
|
+
"ECAFMT" => to_eca_arg.join(' '),
|
105
|
+
"ENDIAN2" => endian2,
|
106
|
+
}
|
107
|
+
begin # don't set these if we can't get them, SOX_FILETYPE may be enough
|
108
|
+
rv["BITS_PER_SAMPLE"] = bits_per_sample.to_s
|
109
|
+
rescue TypeError
|
110
|
+
end
|
111
|
+
rv
|
112
|
+
end
|
113
|
+
|
114
|
+
def bytes_to_samples(bytes)
|
115
|
+
bytes / bytes_per_sample / @channels
|
116
|
+
end
|
117
|
+
|
118
|
+
def bytes_to_time(bytes)
|
119
|
+
Time.at(bytes_to_samples(bytes) / @rate.to_f)
|
120
|
+
end
|
121
|
+
|
122
|
+
def valid_type?(type)
|
123
|
+
!!(type =~ %r{\A[us](?:8|16|24|32)\z} || type =~ %r{\Af?:(32|64)})
|
124
|
+
end
|
125
|
+
|
126
|
+
def valid_endian?(endian)
|
127
|
+
!!(endian =~ %r{\A(?:big|little|swap)\z})
|
128
|
+
end
|
129
|
+
|
130
|
+
# HH:MM:SS.frac (don't bother with more complex times, too much code)
|
131
|
+
# part of me wants to drop this feature from playq, feels like bloat...
|
132
|
+
def hhmmss_to_samples(hhmmss)
|
133
|
+
time = hhmmss.dup
|
134
|
+
rv = 0
|
135
|
+
if time.sub!(/\.(\d+)\z/, "")
|
136
|
+
# convert fractional second to sample count:
|
137
|
+
rv = ("0.#$1".to_f * @rate).to_i
|
138
|
+
end
|
139
|
+
|
140
|
+
# deal with HH:MM:SS
|
141
|
+
t = time.split(/:/)
|
142
|
+
raise ArgumentError, "Bad time format: #{hhmmss}" if t.size > 3
|
143
|
+
|
144
|
+
mult = 1
|
145
|
+
while part = t.pop
|
146
|
+
rv += part.to_i * mult * @rate
|
147
|
+
mult *= 60
|
148
|
+
end
|
149
|
+
rv
|
150
|
+
end
|
151
|
+
end
|
data/lib/dtas/pipe.rb
ADDED
@@ -0,0 +1,39 @@
|
|
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
|
+
begin
|
5
|
+
require 'io/splice'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
require_relative '../dtas'
|
9
|
+
require_relative 'writable_iter'
|
10
|
+
|
11
|
+
class DTAS::Pipe < IO # :nodoc:
|
12
|
+
include DTAS::WritableIter
|
13
|
+
attr_accessor :sink
|
14
|
+
|
15
|
+
def self.new
|
16
|
+
_, w = rv = pipe
|
17
|
+
w.writable_iter_init
|
18
|
+
rv
|
19
|
+
end
|
20
|
+
|
21
|
+
# create no-op methods for non-Linux
|
22
|
+
unless method_defined?(:pipe_size=)
|
23
|
+
def pipe_size=(_)
|
24
|
+
end
|
25
|
+
|
26
|
+
def pipe_size
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# for non-blocking sinks, this avoids extra fcntl(..., F_GETFL) syscalls
|
32
|
+
# We don't need fcntl at all for splice/tee in Linux
|
33
|
+
# For non-Linux, we write_nonblock/read_nonblock already call fcntl()
|
34
|
+
# behind our backs, so there's no need to repeat it.
|
35
|
+
class DTAS::PipeNB < DTAS::Pipe # :nodoc:
|
36
|
+
def nonblock?
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
data/lib/dtas/player.rb
ADDED
@@ -0,0 +1,393 @@
|
|
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 'yaml'
|
5
|
+
require 'shellwords'
|
6
|
+
require_relative '../dtas'
|
7
|
+
require_relative 'source'
|
8
|
+
require_relative 'source/command'
|
9
|
+
require_relative 'sink'
|
10
|
+
require_relative 'unix_server'
|
11
|
+
require_relative 'buffer'
|
12
|
+
require_relative 'sigevent'
|
13
|
+
require_relative 'rg_state'
|
14
|
+
require_relative 'state_file'
|
15
|
+
|
16
|
+
class DTAS::Player # :nodoc:
|
17
|
+
require_relative 'player/client_handler'
|
18
|
+
include DTAS::Player::ClientHandler
|
19
|
+
attr_accessor :state_file
|
20
|
+
attr_accessor :socket
|
21
|
+
attr_reader :sinks
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@state_file = nil
|
25
|
+
@socket = nil
|
26
|
+
@srv = nil
|
27
|
+
@queue = [] # sources
|
28
|
+
@paused = false
|
29
|
+
@format = DTAS::Format.new
|
30
|
+
@srccmd = nil
|
31
|
+
@srcenv = {}
|
32
|
+
|
33
|
+
@sinks = {} # { user-defined name => sink }
|
34
|
+
@targets = [] # order matters
|
35
|
+
@rg = DTAS::RGState.new
|
36
|
+
|
37
|
+
# sits in between shared effects (if any) and sinks
|
38
|
+
@sink_buf = DTAS::Buffer.new
|
39
|
+
@current = nil
|
40
|
+
@watchers = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def echo(msg)
|
44
|
+
msg = Shellwords.join(msg) if Array === msg
|
45
|
+
@watchers.delete_if do |io, _|
|
46
|
+
if io.closed?
|
47
|
+
true
|
48
|
+
else
|
49
|
+
case io.emit(msg)
|
50
|
+
when :wait_readable, :wait_writable
|
51
|
+
false
|
52
|
+
else
|
53
|
+
true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
$stdout.write(msg << "\n")
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_hsh
|
61
|
+
rv = {}
|
62
|
+
rv["socket"] = @socket
|
63
|
+
rv["paused"] = @paused if @paused
|
64
|
+
src = rv["source"] = {}
|
65
|
+
src["command"] = @srccmd if @srccmd
|
66
|
+
src["env"] = @srcenv if @srcenv.size > 0
|
67
|
+
|
68
|
+
# Arrays
|
69
|
+
rv["queue"] = @queue
|
70
|
+
|
71
|
+
%w(rg sink_buf format).each do |k|
|
72
|
+
rv[k] = instance_variable_get("@#{k}").to_hsh
|
73
|
+
end
|
74
|
+
|
75
|
+
# no empty hashes or arrays
|
76
|
+
rv.delete_if do |k,v|
|
77
|
+
case v
|
78
|
+
when Hash, Array
|
79
|
+
v.empty?
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
unless @sinks.empty?
|
86
|
+
sinks = rv["sinks"] = []
|
87
|
+
# sort sinks by name for human viewability
|
88
|
+
@sinks.keys.sort.each do |name|
|
89
|
+
sinks << @sinks[name].to_hsh
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
rv
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.load(hash)
|
97
|
+
rv = new
|
98
|
+
rv.instance_eval do
|
99
|
+
@rg = DTAS::RGState.load(hash["rg"])
|
100
|
+
if v = hash["sink_buf"]
|
101
|
+
v = v["buffer_size"]
|
102
|
+
@sink_buf.buffer_size = v
|
103
|
+
end
|
104
|
+
%w(socket queue paused).each do |k|
|
105
|
+
v = hash[k] or next
|
106
|
+
instance_variable_set("@#{k}", v)
|
107
|
+
end
|
108
|
+
if v = hash["source"]
|
109
|
+
@srccmd = v["command"]
|
110
|
+
e = v["env"] and @srcenv = e
|
111
|
+
end
|
112
|
+
|
113
|
+
if v = hash["format"]
|
114
|
+
@format = DTAS::Format.load(v)
|
115
|
+
end
|
116
|
+
|
117
|
+
if sinks = hash["sinks"]
|
118
|
+
sinks.each do |sink_hsh|
|
119
|
+
sink = DTAS::Sink.load(sink_hsh)
|
120
|
+
@sinks[sink.name] = sink
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
rv
|
125
|
+
end
|
126
|
+
|
127
|
+
def enq_handler(io, msg)
|
128
|
+
# check @queue[0] in case we have no sinks
|
129
|
+
if @current || @queue[0] || @paused
|
130
|
+
@queue << msg
|
131
|
+
else
|
132
|
+
next_source(msg)
|
133
|
+
end
|
134
|
+
io.emit("OK")
|
135
|
+
end
|
136
|
+
|
137
|
+
def do_enq_head(io, msg)
|
138
|
+
# check @queue[0] in case we have no sinks
|
139
|
+
if @current || @queue[0] || @paused
|
140
|
+
@queue.unshift(msg)
|
141
|
+
else
|
142
|
+
next_source(msg)
|
143
|
+
end
|
144
|
+
io.emit("OK")
|
145
|
+
end
|
146
|
+
|
147
|
+
# yielded from readable_iter
|
148
|
+
def client_iter(io, msg)
|
149
|
+
msg = Shellwords.split(msg)
|
150
|
+
command = msg.shift
|
151
|
+
case command
|
152
|
+
when "enq"
|
153
|
+
enq_handler(io, msg[0])
|
154
|
+
when "enq-head"
|
155
|
+
do_enq_head(io, msg)
|
156
|
+
when "enq-cmd"
|
157
|
+
enq_handler(io, { "command" => msg[0]})
|
158
|
+
when "pause", "play", "play_pause"
|
159
|
+
play_pause_handler(io, command)
|
160
|
+
when "seek"
|
161
|
+
do_seek(io, msg[0])
|
162
|
+
when "clear"
|
163
|
+
@queue.clear
|
164
|
+
echo("clear")
|
165
|
+
io.emit("OK")
|
166
|
+
when "rg"
|
167
|
+
rg_handler(io, msg)
|
168
|
+
when "skip"
|
169
|
+
skip_handler(io, msg)
|
170
|
+
when "sink"
|
171
|
+
sink_handler(io, msg)
|
172
|
+
when "current"
|
173
|
+
current_handler(io, msg)
|
174
|
+
when "watch"
|
175
|
+
@watchers[io] = true
|
176
|
+
io.emit("OK")
|
177
|
+
when "format"
|
178
|
+
format_handler(io, msg)
|
179
|
+
when "env"
|
180
|
+
env_handler(io, msg)
|
181
|
+
when "restart"
|
182
|
+
restart_pipeline
|
183
|
+
io.emit("OK")
|
184
|
+
when "source"
|
185
|
+
source_handler(io, msg)
|
186
|
+
when "cd"
|
187
|
+
chdir_handler(io, msg)
|
188
|
+
when "pwd"
|
189
|
+
io.emit(Dir.pwd)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def event_loop_iter
|
194
|
+
@srv.run_once do |io, msg| # readability handler, request/response
|
195
|
+
case io
|
196
|
+
when @sink_buf
|
197
|
+
sink_iter
|
198
|
+
when DTAS::UNIXAccepted
|
199
|
+
client_iter(io, msg)
|
200
|
+
when DTAS::Sigevent # signal received
|
201
|
+
reap_iter
|
202
|
+
else
|
203
|
+
raise "BUG: unknown event: #{io.class} #{io.inspect} #{msg.inspect}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def reap_iter
|
209
|
+
DTAS::Process.reaper do |status, obj|
|
210
|
+
warn [ :reap, obj, status ].inspect if $DEBUG
|
211
|
+
obj.on_death(status) if obj.respond_to?(:on_death)
|
212
|
+
case obj
|
213
|
+
when @current
|
214
|
+
next_source(@paused ? nil : @queue.shift)
|
215
|
+
when DTAS::Sink # on unexpected sink death
|
216
|
+
sink_death(obj, status)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
:wait_readable
|
220
|
+
end
|
221
|
+
|
222
|
+
def sink_death(sink, status)
|
223
|
+
deleted = []
|
224
|
+
@targets.delete_if do |t|
|
225
|
+
if t.sink == sink
|
226
|
+
deleted << t
|
227
|
+
else
|
228
|
+
false
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
if deleted[0]
|
233
|
+
warn("#{sink.name} died unexpectedly: #{status.inspect}")
|
234
|
+
deleted.each { |t| drop_target(t) }
|
235
|
+
__current_drop unless @targets[0]
|
236
|
+
end
|
237
|
+
|
238
|
+
return unless sink.active
|
239
|
+
|
240
|
+
if @queue[0] && !@paused
|
241
|
+
# we get here if source/sinks are all killed in restart_pipeline
|
242
|
+
__sink_activate(sink)
|
243
|
+
next_source(@queue.shift)
|
244
|
+
elsif sink.respawn
|
245
|
+
__sink_activate(sink) if @current
|
246
|
+
end
|
247
|
+
ensure
|
248
|
+
sink.respawn = false
|
249
|
+
end
|
250
|
+
|
251
|
+
# returns a wait_ctl arg for self
|
252
|
+
def broadcast_iter(buf, targets)
|
253
|
+
case rv = buf.broadcast(targets)
|
254
|
+
when Array # array of blocked sinks
|
255
|
+
# have sinks wake up the this buffer when they're writable
|
256
|
+
trade_ctl = proc { @srv.wait_ctl(buf, :wait_readable) }
|
257
|
+
rv.each do |dst|
|
258
|
+
dst.on_writable = trade_ctl
|
259
|
+
@srv.wait_ctl(dst, :wait_writable)
|
260
|
+
end
|
261
|
+
|
262
|
+
# this @sink_buf hibernates until trade_ctl is called
|
263
|
+
# via DTAS::Sink#writable_iter
|
264
|
+
:ignore
|
265
|
+
else # :wait_readable or nil
|
266
|
+
rv
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def bind
|
271
|
+
@srv = DTAS::UNIXServer.new(@socket)
|
272
|
+
end
|
273
|
+
|
274
|
+
# only used on new installations where no sink exists
|
275
|
+
def create_default_sink
|
276
|
+
return unless @sinks.empty?
|
277
|
+
s = DTAS::Sink.new
|
278
|
+
s.name = "default"
|
279
|
+
s.active = true
|
280
|
+
@sinks[s.name] = s
|
281
|
+
end
|
282
|
+
|
283
|
+
# called when the player is leaving idle state
|
284
|
+
def spawn_sinks(source_spec)
|
285
|
+
return true if @targets[0]
|
286
|
+
@sinks.each_value do |sink|
|
287
|
+
sink.active or next
|
288
|
+
next if sink.pid
|
289
|
+
@targets.concat(sink.spawn(@format))
|
290
|
+
end
|
291
|
+
if @targets[0]
|
292
|
+
@targets.sort_by! { |t| t.sink.prio }
|
293
|
+
true
|
294
|
+
else
|
295
|
+
# fail, no active sink
|
296
|
+
@queue.unshift(source_spec)
|
297
|
+
false
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def next_source(source_spec)
|
302
|
+
@current = nil
|
303
|
+
if source_spec
|
304
|
+
# restart sinks iff we were idle
|
305
|
+
spawn_sinks(source_spec) or return
|
306
|
+
|
307
|
+
case source_spec
|
308
|
+
when String
|
309
|
+
@current = DTAS::Source.new(source_spec)
|
310
|
+
echo(%W(file #{@current.infile}))
|
311
|
+
when Array
|
312
|
+
@current = DTAS::Source.new(*source_spec)
|
313
|
+
echo(%W(file #{@current.infile} #{@current.offset_samples}s))
|
314
|
+
else
|
315
|
+
@current = DTAS::Source::Command.new(source_spec["command"])
|
316
|
+
echo(%W(command #{@current.command_string}))
|
317
|
+
end
|
318
|
+
|
319
|
+
if DTAS::Source === @current
|
320
|
+
@current.command = @srccmd if @srccmd
|
321
|
+
@current.env = @srcenv.dup unless @srcenv.empty?
|
322
|
+
end
|
323
|
+
|
324
|
+
dst = @sink_buf
|
325
|
+
@current.dst_assoc(dst)
|
326
|
+
@current.spawn(@format, @rg, out: dst.wr, in: "/dev/null")
|
327
|
+
@srv.wait_ctl(dst, :wait_readable)
|
328
|
+
else
|
329
|
+
stop_sinks if @sink_buf.inflight == 0
|
330
|
+
echo "idle"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def drop_target(target)
|
335
|
+
@srv.wait_ctl(target, :delete)
|
336
|
+
target.close
|
337
|
+
end
|
338
|
+
|
339
|
+
def stop_sinks
|
340
|
+
@targets.each { |t| drop_target(t) }.clear
|
341
|
+
end
|
342
|
+
|
343
|
+
# only call on unrecoverable errors (or "skip")
|
344
|
+
def __current_drop(src = @current)
|
345
|
+
__buf_reset(src.dst) if src && src.pid
|
346
|
+
end
|
347
|
+
|
348
|
+
# pull data from sink_buf into @targets, source feeds into sink_buf
|
349
|
+
def sink_iter
|
350
|
+
wait_iter = broadcast_iter(@sink_buf, @targets)
|
351
|
+
__current_drop if nil == wait_iter # sink error, stop source
|
352
|
+
return wait_iter if @current
|
353
|
+
|
354
|
+
# no source left to feed sink_buf, drain the remaining data
|
355
|
+
sink_bytes = @sink_buf.inflight
|
356
|
+
if sink_bytes > 0
|
357
|
+
return wait_iter if @targets[0] # play what is leftover
|
358
|
+
|
359
|
+
# discard the buffer if no sinks
|
360
|
+
@sink_buf.discard(sink_bytes)
|
361
|
+
end
|
362
|
+
|
363
|
+
# nothing left inflight, stop the sinks until we have a source
|
364
|
+
stop_sinks
|
365
|
+
:ignore
|
366
|
+
end
|
367
|
+
|
368
|
+
# the main loop
|
369
|
+
def run
|
370
|
+
sev = DTAS::Sigevent.new
|
371
|
+
@srv.wait_ctl(sev, :wait_readable)
|
372
|
+
old_chld = trap(:CHLD) { sev.signal }
|
373
|
+
create_default_sink
|
374
|
+
next_source(@paused ? nil : @queue.shift)
|
375
|
+
begin
|
376
|
+
event_loop_iter
|
377
|
+
rescue => e # just in case...
|
378
|
+
warn "E: #{e.message} (#{e.class})"
|
379
|
+
e.backtrace.each { |l| warn l }
|
380
|
+
end while true
|
381
|
+
ensure
|
382
|
+
__current_requeue
|
383
|
+
trap(:CHLD, old_chld)
|
384
|
+
sev.close if sev
|
385
|
+
# for state file
|
386
|
+
end
|
387
|
+
|
388
|
+
def close
|
389
|
+
@srv = @srv.close if @srv
|
390
|
+
@sink_buf.close!
|
391
|
+
@state_file.dump(self, true) if @state_file
|
392
|
+
end
|
393
|
+
end
|