dtas 0.0.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 +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
|