1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
# Copyright (C) 2013-2015 all contributors <dtas-all@nongnu.org>
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
require 'yaml'
require_relative '../dtas'
require_relative 'pipe'
require_relative 'process'
require_relative 'command'
require_relative 'format'
require_relative 'serialize'
# this is a sink (endpoint, audio enters but never leaves), used by -player
class DTAS::Sink # :nodoc:
attr_accessor :prio # any Integer
attr_accessor :active # boolean
attr_accessor :name
attr_accessor :pipe_size
attr_accessor :nonblock
include DTAS::Command
include DTAS::Process
include DTAS::Serialize
SINK_DEFAULTS = COMMAND_DEFAULTS.merge({
"name" => nil, # order matters, this is first
"command" => "exec play -q $SOXFMT -",
"prio" => 0,
"nonblock" => false,
"pipe_size" => nil,
"active" => false,
})
DEVFD_RE = %r{/dev/fd/([a-zA-Z]\w*)\b}
# order matters for Ruby 1.9+, this defines to_hsh serialization so we
# can make the state file human-friendly
SIVS = %w(name env command prio nonblock pipe_size active)
def initialize
command_init(SINK_DEFAULTS)
@sink = self
end
# allow things that look like audio device names ("hw:1,0" , "/dev/dsp")
# or variable names.
def valid_name?(s)
!!(s =~ %r{\A[\w:,/-]+\z})
end
def self.load(hash)
sink = new
return sink unless hash
(SIVS & hash.keys).each do |k|
sink.instance_variable_set("@#{k}", hash[k])
end
sink.valid_name?(sink.name) or raise ArgumentError, "invalid sink name"
sink
end
def parse(str)
inputs = {}
str.scan(DEVFD_RE) { |w| inputs[w[0]] = nil }
inputs
end
def on_death(status)
super
end
def sink_spawn(format, opts = {})
raise "BUG: #{self.inspect}#sink_spawn called twice" if @pid
rv = []
pclass = @nonblock ? DTAS::PipeNB : DTAS::Pipe
cmd = command_string
inputs = parse(cmd)
if inputs.empty?
# /dev/fd/* not specified in the command, assume one input for stdin
r, w = pclass.new
w.pipe_size = @pipe_size if @pipe_size
inputs[:in] = opts[:in] = r
w.sink = self
rv << w
else
# multiple inputs, fun!, we'll tee to them
inputs.each_key do |name|
r, w = pclass.new
w.pipe_size = @pipe_size if @pipe_size
inputs[name] = r
w.sink = self
rv << w
end
opts[:in] = DTAS.null
# map to real /dev/fd/* values and setup proper redirects
cmd = cmd.gsub(DEVFD_RE) do
read_fd = inputs[$1].fileno
opts[read_fd] = read_fd # do not close-on-exec
"/dev/fd/#{read_fd}"
end
end
@pid = dtas_spawn(format.to_env.merge!(@env), cmd, opts)
inputs.each_value(&:close)
rv
end
def to_hash
ivars_to_hash(SIVS)
end
def to_hsh
to_hash.delete_if { |k,v| v == SINK_DEFAULTS[k] }
end
end
|