Skip to content
Permalink
scd-engine-pro…
Go to file
 
 
Cannot retrieve contributors at this time
345 lines (297 sloc) 8.32 KB
NornsProto {
// address of remote client
classvar remoteAddr;
// port for sending OSC to matron
classvar txPort = 8888;
var commandPrefix = "/command";
var <>traceCommands=false;
var name;
var >onFree;
var oscfuncs;
var <commands;
var <polls;
var complete = 0;
var <server;
var <context;
*initClass {
remoteAddr = NetAddr("127.0.0.1", txPort);
}
*spawn { arg name, func;
^this.new(name, func);
}
*new { arg name, func;
^super.new.init(name, func);
}
init { arg argName, func;
name = argName;
commands = Array.new;
polls = Array.new;
this.prBootServer {
context = this.prSpawnContext;
func.value(this);
this.prInitOscRx;
complete = 1;
CmdPeriod.doOnce {
this.free;
};
};
}
free {
"engine %: about to free...".format(name).inform;
oscfuncs.do { arg oscfunc;
oscfunc.free;
};
commands.do { arg command;
command[\oscfunc].free;
};
polls.do { arg poll;
this.prStopPoll(poll);
};
fork {
onFree.value;
"engine %: freed".format(name).inform;
};
}
/* engine API */
addCommand { arg commandName, format, func;
commandName = commandName.asSymbol;
postln([ "engine % adding command".format(name), commandName, format, func ]);
if (this.prCommandExists(commandName).not) {
commands = commands.add(
(
name: commandName,
format: format,
oscfunc: OSCFunc(
{ arg msg, time, addr, rxport;
if (traceCommands) {
postln(["rx command", msg, time, addr, rxport]);
};
func.value(msg); // TODO: consider changing to method call with "exploded" arguments...
// func.value(*msg[1..]); // ... like so
},
(commandPrefix++"/"++commandName).asSymbol, // TODO: rename to cmd to optimize UDP transmission size
recvPort: NetAddr.langPort
)
)
);
} {
"in engine % command % not added, already exists".format(name, commandName).error;
};
}
// create and define a new poll.
// function argument should return value or data
// type should be \value or \data
addPoll { arg pollName, func, periodic=true, type=\value, period=0.25;
pollName = pollName.asSymbol;
postln([ "engine % adding poll".format(name), pollName, if (periodic, "periodic", "aperiodic"), func ]);
if (this.prPollExists(pollName).not) {
var index = polls.size;
var poll = (
name: pollName, // Symbol; label for outside world
index: index, // Integer; another label, for ordering/efficiency
period: period, // Number; callback interval
type: type, // Symbol; either \value or \data
function: func, // Function; what produces the value/data
// TODO callback: callback, // Function; full (private) callback function
periodic: periodic, // Boolean; is this a scheduled/repeating poll or not
isRunning: false, // Boolean; running state
oscPath: if( type == \value, '/poll/value', '/poll/data'), // Symbol, full destination path
trace: false // Boolean; whether to dump values to sclang Post Window
);
if (periodic) {
poll[\task] = Task { // Task; timing thread
inf.do {
var val;
poll[\val] = poll[\function].value;
this.prPollSendValue(poll);
poll[\period].wait;
}
};
};
polls = polls.add(poll);
^poll
} {
"in engine % poll % not added, already exists".format(name, pollName).error;
};
}
// update and send the poll's value
// non-periodic polls should call this
pollUpdate { arg pollName, val;
if (this.prPollExists(pollName)) {
var poll = this.prLookupPollByName(pollName);
poll[\val] = val;
if (poll[\isRunning]) {
this.prPollSendValue(poll);
};
} {
"in % engine, no poll named: %".format(name, pollName).error;
};
}
/* engine debugging facilities */
cmd { |commandName ... args|
if (this.prCommandExists(commandName)) {
var msg = [commandPrefix++"/"++commandName] ++ args;
NetAddr.localAddr.sendMsg(*msg);
// TODO: lookup command and invoke function instead
} {
"in % engine, no command named: %".format(name, commandName).error;
}
}
polli { |selector, index ... args|
this.poll(selector, polls[index][\name], *args);
}
poll { |selector, pollName ... args|
if (this.prPollExists(pollName)) {
var pollIndex = polls.detectIndex { |poll| poll[\name] == pollName };
var msg = case
{ selector == 'start' } {
['/poll/start', pollIndex]
}
{ selector == 'stop' } {
['/poll/stop', pollIndex]
}
{ selector == 'time' } {
['/poll/time', pollIndex, args.first]
}
{ selector == 'value' } {
['/poll/value', pollIndex]
};
NetAddr.localAddr.sendMsg(*msg);
// TODO: lookup command and invoke function instead
} {
"in % engine, no poll named: %".format(name, pollName).error;
}
}
tracePoll { arg pollName, toggle=true;
if (this.prPollExists(pollName)) {
var poll = this.prLookupPollByName(pollName);
poll[\trace] = toggle;
} {
"in % engine, no poll named: %".format(name, pollName).error;
};
}
/* private helper methods */
prBootServer { |completeFunc|
Server.scsynth;
server = Server.local;
server.options.memSize = 2**16;
server.latency = 0.05;
server.waitForBoot {
completeFunc.value;
};
}
prSpawnContext {
var ig, xg, og;
ig = Group.new(server);
xg = Group.after(ig);
og = Group.after(xg);
^(
server: server,
// input, process, output groups
// FIXME: not good naming, use an Event to match engine style
ig: ig,
xg: xg,
og: og,
// input, output busses
in_b: Server.default.options.numOutputBusChannels,
out_b: 0
);
}
prInitOscRx {
oscfuncs = [
/// send a `/engine/ready` response if engine is done starting up,
/// otherwise send nothing
// @function /ready
OSCFunc.new({
arg msg, time, addr, recvPort;
if (complete==1) {
remoteAddr.sendMsg('/engine/ready');
}
}, '/ready', recvPort: NetAddr.langPort),
// @function /poll/start
// @param poll index (integer)
OSCFunc.new({
arg msg, time, addr, recvPort;
this.prStartPoll(polls[msg[1]]);
}, '/poll/start', recvPort: NetAddr.langPort),
// @function /poll/stop
// @param poll index (integer)
OSCFunc.new({
arg msg, time, addr, recvPort;
this.prStopPoll(polls[msg[1]]);
}, '/poll/stop', recvPort: NetAddr.langPort),
/// set the period of a poll
// @function /poll/time
// @param poll index (integer)
// @param poll period(float)
OSCFunc.new({
arg msg, time, addr, recvPort;
this.prSetPollTime(polls[msg[1]], msg[2]);
}, '/poll/time', recvPort: NetAddr.langPort),
/// request a poll's value
// @function /poll/value
// @param poll index (integer)
OSCFunc.new({
arg msg, time, addr, recvPort;
this.prRequestPollValue(polls[msg[1]]);
}, '/poll/value', recvPort: NetAddr.langPort),
/// shutdown engine gracefully disposing all resources
// @function /quit
OSCFunc.new({
arg msg, time, addr, recvPort;
// TODO: wait for engine.free, then:
0.exit;
}, '/quit', recvPort: NetAddr.langPort),
];
}
prCommandExists { arg commandName;
^commands.any { |command| command[\name] == commandName }
}
prLookupCommandByName { arg commandName;
^commands.detect { |command| command[\name] == commandName }
}
prPollExists { arg pollName;
^polls.any { |poll| poll[\name] == pollName }
}
prLookupPollByName { arg pollName;
^polls.detect { |poll| poll[\name] == pollName }
}
prStartPoll { arg poll;
postln("in % engine, starting poll # % (%)".format(name, poll[\index], poll[\name]));
if(poll[\isRunning].not) {
poll[\isRunning] = true;
if (poll[\periodic]) {
poll[\task].play;
};
};
}
prStopPoll { arg poll;
if(poll[\isRunning]) {
poll[\isRunning] = false;
if(poll[\periodic]) {
poll[\task].stop;
};
};
}
prSetPollTime { arg poll, time;
if (poll[\periodic].not) {
"in % engine, setPollTime: poll # % (%) is aperiodic".format(name, poll[\index], poll[\name]).warn;
};
poll[\period] = time;
}
// triggered by remote client.
// sends the last value, and refresh it using the value-function if there is one
prRequestPollValue { arg poll;
if(poll[\function].notNil) {
poll[\val] = poll[\function].value
};
this.prPollSendValue(poll);
}
// send the most recent value
prPollSendValue { arg poll;
remoteAddr.sendMsg(poll[\oscPath], poll[\index], poll[\val]);
if (poll[\trace]) {
"in % engine, poll # % (%) sent value %".format(name, poll[\index], poll[\name], poll[\val]).inform;
};
}
}