/
server.rb
181 lines (151 loc) · 6.11 KB
/
server.rb
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
require 'open3'
module Scruby
include OSC
TrueClass.send :include, OSC::OSCArgument
TrueClass.send(:define_method, :to_osc_type){ 1 }
FalseClass.send :include, OSC::OSCArgument
FalseClass.send(:define_method, :to_osc_type){ 0 }
Hash.send :include, OSC::OSCArgument
Hash.send :define_method, :to_osc_type do
self.to_a.collect{ |pair| pair.collect{ |a| OSC.coerce_argument a } }
end
Array.send(:include, OSC::OSCArgument)
Array.send( :define_method, :to_osc_type) do
Blob.new Message.new(*self).encode
end
class Server
attr_reader :host, :port, :path, :buffers, :control_buses, :audio_buses
# Initializes and registers a new Server instance and sets the host and port for it.
# The server is a Ruby representation of scsynth which can be a local binary or a remote
# server already running.
# Server class keeps an array with all the instantiated servers
#
# For more info
# $ man scsynth
#
# @param [Hash] opts the options to create a message with.
# @option opts [String] :path ('scsynt' if in PATH env variable otherwise '/Applications/SuperCollider/scsynth') scsynth binary path
# @option opts [String] :host ('localhost') SuperCollider Server address
# @option opts [Fixnum] :port (57111) TCP port
# @option opts [Fixnum] :control_bus_count (4096) Number of buses for routing control data, indices start at 0
# @option opts [Fixnum] :audio_bus_count (128) Number of audio Bus channels for hardware output and input and internal routing
# @option opts [Fixnum] :audio_output_count (8) Reserved buses for hardware output, indices start at 0
# @option opts [Fixnum] :audio_input_count (8) Reserved buses for hardware input, indices starting from the number of audio outputs
# @option opts [Fixnum] :buffer_count (1024) Number of available sample buffers
#
def initialize opts = {}
@path = opts.delete(:path) || (%x(which scsynth).empty? ? '/Applications/SuperCollider/scsynth' : 'scsynth')
@host = opts.delete(:host) || 'localhost'
@port = opts.delete(:port) || 57111
@audio_output_count = opts.delete(:audio_output_count) || 8
@audio_input_count = opts.delete(:audio_input_count) || 8
@buffer_count = opts.delete(:buffer_count) || 1024
@control_bus_count = opts.delete(:control_bus_count) || 4096
@audio_bus_count = opts.delete(:audio_bus_count) || 128
@buffers = []
@control_buses = []
@audio_buses = []
@client = Client.new port, host
# Bus.audio self, @audio_output_count # register hardware buses
# Bus.audio self, @audio_input_count
self.class.all << self
end
# Boots the local binary of the scsynth forking a process, it will rise a SCError if the scsynth
# binary is not found in path.
# The default path can be overriden using Server.scsynt_path=('path')
def boot
raise SCError.new("scsyth already running on port #{port}") if running?
ready, timeout = false, Time.now + 2
@thread = Thread.new do
Open3.popen3("#{@path} -u #{port}") do |stdin, stdout, stderr, thread|
stdout.each_line do |line|
puts line
ready = true if line.include? "server ready"
end
stderr.each_line { |line| puts "\e[31m#{line.chop}\e[0m" }
end
end
sleep 0.1 until Time.now > timeout || ready
raise SCError.new("could not boot scsynth") unless running?
send "/g_new", 1 # default group
self
end
def running?
@thread && @thread.alive?
end
def stop
send "/g_freeAll", 0
send "/clearSched"
send "/g_new", 1
end
alias :panic :stop
# Sends the /quit OSC signal to the scsynth
def quit
Server.all.delete self
send '/quit'
end
# Sends an OSC command or +Message+ to the scsyth server.
# E.g. +server.send('/dumpOSC', 1)+
def send message, *args
message = Message.new message, *args unless Message === message or Bundle === message
@client.send message
end
def send_bundle timestamp = nil, *messages
send Bundle.new( timestamp, *messages.map{ |message| Message.new *message } )
end
# Encodes and sends a SynthDef to the scsynth server
def send_synth_def synth_def
send Bundle.new( nil, Message.new('/d_recv', Blob.new(synth_def.encode), 0) )
end
# Allocates either buffer or bus indices, should be consecutive
def allocate kind, *elements
collection, max_size =
case kind
when :buffers
[buffers, @buffer_count]
when :control_buses
[control_buses, @control_bus_count]
when :audio_buses
[audio_buses, @audio_bus_count]
end
elements.flatten!
raise SCError.new("No more indices available -- free some #{kind} before allocating.") if collection.compact.size + elements.size > max_size
return collection.concat(elements) unless collection.index nil # just concat arrays if no nil item
indices = []
collection.each_with_index do |item, index|
break if indices.size >= elements.size
item.nil? ? indices.push(index) : indices.clear
end
case
when indices.size >= elements.size
collection[indices.first, elements.size] = elements
when collection.size + elements.size <= max_size
collection.concat elements
else
raise SCError.new("No block of #{elements.size} consecutive #{kind} indices is available.")
end
end
@@servers = []
class << self
# Returns an array with all the registered servers
def all
@@servers
end
# Clear the servers array
def clear
@@servers.clear
end
# Return a server corresponding to the specified index of the registered servers array
def [] index
@@servers[index]
end
# Set a server to the specified index of the registered servers array
def []= index
@@servers[index]
@@servers.uniq!
end
end
end
class SCError < StandardError
end
end