diff options
Diffstat (limited to 'src/progs/supercollider')
-rw-r--r-- | src/progs/supercollider/Makefile.am | 2 | ||||
-rw-r--r-- | src/progs/supercollider/Om.sc | 746 | ||||
-rw-r--r-- | src/progs/supercollider/README | 11 | ||||
-rw-r--r-- | src/progs/supercollider/example.sc | 27 |
4 files changed, 786 insertions, 0 deletions
diff --git a/src/progs/supercollider/Makefile.am b/src/progs/supercollider/Makefile.am new file mode 100644 index 00000000..69661c73 --- /dev/null +++ b/src/progs/supercollider/Makefile.am @@ -0,0 +1,2 @@ +EXTRA_DIST = Om.sc example.sc + diff --git a/src/progs/supercollider/Om.sc b/src/progs/supercollider/Om.sc new file mode 100644 index 00000000..cb366d58 --- /dev/null +++ b/src/progs/supercollider/Om.sc @@ -0,0 +1,746 @@ +// TODO: +// * Keep track of established connections. +Om : Model { + classvar <>program = "om", <>patchLoader = "om_patch_loader"; + classvar <>oscURL, <nodeTypeMap, <>uiClass; + var <addr; + var <>loadIntoJack = true; + var allocator, requestResponders, requestHandlers, notificationResponders; + var creatingNode, newNodeEnd; + var <registered = false, <booting = false; + var <root; + var <plugins, pluginParentEvent; + var onNewPatch; + *initClass { + Class.initClassTree(Event); + Class.initClassTree(NetAddr); + Event.parentEvents.default[\omcmd] = \noteOn; + Event.parentEvents.default[\omEventFunctions] = ( + noteOn: #{ arg midinote=60, amp=0.1; + [midinote, asInteger((amp * 127).clip(0, 127)) ] }, + noteOff: #{ arg midinote=60; [ midinote ] }, + setPortValue: #{|portValue=0| [portValue] } + ); + Event.parentEvents.default.eventTypes[\om]=#{|server| + var freqs, lag, dur, sustain, strum, target, bndl, omcmd; + freqs = ~freq = ~freq.value + ~detune; + if (freqs.isKindOf(Symbol).not) { + ~amp = ~amp.value; + strum = ~strum; + lag = ~lag + server.latency; + sustain = ~sustain = ~sustain.value; + omcmd = ~omcmd; + target = ~target; + ~midinote = ~midinote.value; + ~portValue = ~portValue.value; + bndl = ~omEventFunctions[omcmd].valueEnvir.asCollection; + bndl = bndl.flop; + bndl.do {|msgArgs, i| + var latency; + latency = i * strum + lag; + if(latency == 0.0) { + target.performList(omcmd, msgArgs) + } { + thisThread.clock.sched(latency, { + target.performList(omcmd, msgArgs); + }) + }; + if(omcmd === \noteOn) { + thisThread.clock.sched(sustain + latency, { + target.noteOff(msgArgs[0]) + }) + } + } + } + }; + oscURL="osc.udp://"++NetAddr.localAddr.ip++$:++NetAddr.localAddr.port; + nodeTypeMap = IdentityDictionary[ + \Internal -> OmInternalNode, + \LADSPA -> OmLADSPANode, + \DSSI -> OmDSSINode + ]; + uiClass = OmEmacsUI + } + *new { | netaddr | + ^super.new.init(netaddr) + } + gui { ^uiClass.new(this) } + init { |netaddr| + addr = netaddr ? NetAddr("127.0.0.1", 16180); + onNewPatch = IdentityDictionary.new; + allocator = StackNumberAllocator(0,1024); + requestHandlers = IdentityDictionary.new; + requestResponders = [ + "response/ok" -> {|id| + requestHandlers.removeAt(id).value; allocator.free(id) }, + "response/error" -> {|id,text| + requestHandlers.removeAt(id); + allocator.free(id); + ("Om"+text).error } + ].collect({|a| + var func = a.value; + OSCresponder(addr, "/om/"++a.key, {|time,resp,msg| + func.value(*msg[1..]) + }) + }); + notificationResponders = [ + "new_patch" -> {|path,poly| + var func = onNewPatch.removeAt(path); + if (func.notNil) { + func.value(this.getPatch(path,false).prSetPoly(poly)) + } + }, + "metadata/update" -> {|path,key,value| + this.getObject(path).metadata.prSetMetadata(key, value) }, + "new_node" -> {|path,poly,type,lib,label| + var patchPath, nodeName, patch, node; + var lastSlash = path.asString.inject(nil,{|last,char,i| + if(char==$/,i,last) + }); + if (lastSlash.notNil) { + patchPath = path.asString.copyFromStart(lastSlash-1); + nodeName = path.asString.copyToEnd(lastSlash+1); + patch = this.getPatch(patchPath); + if (patch.notNil) { + if (patch.hasNode(nodeName).not) { + node = nodeTypeMap[type].new + (nodeName, patch, poly, label, lib); + creatingNode = node; + patch.nodes[nodeName.asSymbol] = node; + patch.changed(\newNode, node); + } { + if (patch.getNode(nodeName).class != nodeTypeMap[type]) { + ("Om sent an existng node with differing type"+path).warn + } + } + } { + ("Om tried to create node in non-existing patch"+patchPath).warn + } + } { + ("Invalid path in node creation"+path).warn + } + }, + "new_node_end" -> { + newNodeEnd.value(creatingNode); + newNodeEnd = nil; + creatingNode = nil }, + "new_port" -> {|path,type,dir,hint,def,min,max| + var basePath, portName, parent, port; + var lastSlash = path.asString.inject(nil,{|last,char,i| + if(char==$/,i,last) + }); + if (lastSlash.notNil) { + basePath = path.asString.copyFromStart(lastSlash-1); + portName = path.asString.copyToEnd(lastSlash+1); + parent = this.getNode(basePath) ? this.getPatch(basePath); + if (parent.notNil) { + if (parent.hasPort(portName).not) { + port = OmPort.new(portName, parent, type, dir, hint, def, min, max); + parent.ports[portName.asSymbol] = port; + parent.changed(\newPort, port) + } { + if (parent.getPort(portName).porttype != type) { + ("Om tried to create an already existing port with differing type" + +path).warn + } + } + } { + ("Om tried to create port on non-existing object"+basePath).warn + } + } { + ("Invalid path in port creation"+path).warn + } + }, + "control_change" -> {|path,value| + this.getPort(path).prSetValue(value) }, + "patch_enabled" -> {|path| this.getPatch(path).prSetEnabled(true) }, + "patch_disabled" -> {|path| this.getPatch(path).prSetEnabled(false) }, + "plugin" -> {|lib,label,name,type| + plugins.add(Event.new(4,nil,pluginParentEvent).putAll( + (type:type, lib:lib, label:label, name:name))) }, + "node_removal" -> {|path| + var node = this.getNode(path); + if (node.notNil) { + node.parent.nodes.removeAt(node.name.asSymbol).free + } { + ("Om attempting to remove non-existing node"+path).warn + } + }, + "port_removal" -> {|path| + var port = this.getPort(path); + if (port.notNil) { + port.parent.ports.removeAt(port.name.asSymbol).free + } { + ("Om attempting to remove non-existing port"+path).warn + } + }, + "patch_destruction" -> {|path| + var patch = this.getPatch(path); + if (patch.notNil) { + patch.parent.patches.removeAt(patch.name.asSymbol).free + } { + ("Om attempting to remove non-existing patch"+path).warn + } + }, + "program_add" -> {|path,bank,program,name| + var node = this.getNode(path); + if (node.respondsTo(\prProgramAdd)) { + node.prProgramAdd(bank,program,name) + } { + ("Om tried to add program info to"+node).warn + } + } + ].collect({|a| + var func = a.value; + OSCresponder(addr, "/om/"++a.key, {|time,resp,msg| + func.value(*msg[1..]) + }) + }); + pluginParentEvent = Event.new(2,nil,nil).putAll(( + engine:this, + new:{|self,path,poly=1,handler|self.engine.createNode(path?("/"++self.name),self.type,self.lib,self.label,poly,created:handler)} + )); + } + *waitForBoot {|func| ^this.new.waitForBoot(func) } + waitForBoot {|func| + var r, id = 727; + requestHandlers[id] = { + r.stop; + booting=false; + this.changed(\running, true); + func.value(this) + }; + if (booting.not) {this.boot}; + r = Routine.run { + 50.do { + 0.1.wait; + addr.sendMsg("/om/ping", id) + }; + requestHandlers.removeAt(id); + "Om engine boot failed".error; + } + } + getPatch {|path, mustExist=true| + var elements, currentPatch; + if (path.class == Array) { elements = path + } { elements = path.asString.split($/) }; + elements.do{|elem| + if (elem=="") { + currentPatch = root + } { + currentPatch = currentPatch.getPatch(elem,mustExist); + if (currentPatch.isNil) { ^nil } + } + }; + ^currentPatch; + } + getNode {|path| + var basePath, nodeName, patch; + if (path.class == Array) { basePath = path + } { basePath = path.asString.split($/) }; + nodeName = basePath.pop; + patch = this.getPatch(basePath,true); + if (patch.notNil) { + ^patch.getNode(nodeName) + }; + ^nil + } + getPort {|path| + var basePath, portName, node, patch; + basePath = path.asString.split($/); + portName = basePath.pop; + node = this.getNode(basePath.copy); + if (node.notNil) { ^node.getPort(portName) }; + patch = this.getPatch(basePath,true); + if (patch.notNil) { ^patch.getPort(portName) }; + ^nil + } + getObject {|path| + var patch,node,port; + patch = this.getPatch(path,true); + if (patch.notNil) { ^patch }; + node = this.getNode(path); + if (node.notNil) { ^node }; + port = this.getPort(path,true); + if (port.notNil) { ^port }; + ^nil + } + at {|path|^this.getObject(path.asString)} + *boot {|func| + ^Om.new.waitForBoot {|e| + e.activate { + e.register { + e.loadPlugins { + e.requestPlugins { + e.requestAllObjects { + func.value(e) + } + } + } + } + } + } + } + boot { + requestResponders.do({|resp| resp.add}); + booting = true; + if (addr.addr == 2130706433) { + if (loadIntoJack) { + ("jack_load"+"-i"+addr.port+"Om"+"om").unixCmd + } { + (program+"-p"+addr.port).unixCmd + } + } { + "You have to manually boot Om now".postln + } + } + loadPatch {|patchPath| (patchLoader + patchPath).unixCmd } + activate { | handler | + this.sendReq("engine/activate", { + root = OmPatch("",nil,this); + this.changed(\newPatch, root); + handler.value + }) + } + register { | handler | + this.sendReq("engine/register_client", { + registered=true; + notificationResponders.do({|resp| resp.add}); + this.changed(\registered, registered); + handler.value(this) + }) + } + unregister { | handler | + this.sendReq("engine/unregister_client", { + registered=false; + notificationResponders.do({|resp| resp.remove}); + this.changed(\registered, registered); + handler.value(this) + }) + } + registered_ {|flag| + if (flag and: registered.not) { + this.register + } { + if (flag.not and: registered) { + this.unregister + } + } + } + loadPlugins { | handler | this.sendReq("engine/load_plugins", handler) } + requestPlugins {|handler| + var startTime = Main.elapsedTime; + plugins = Set.new; + this.sendReq("request/plugins", { + ("Received info about"+plugins.size+"plugins in"+(Main.elapsedTime-startTime)+"seconds").postln; + this.changed(\plugins, plugins); + handler.value(this); + }) + } + requestAllObjects { |handler| + this.sendReq("request/all_objects", handler) + } + createPatch { | path, poly=1, handler | + onNewPatch[path.asSymbol] = handler; + this.sendReq("synth/create_patch", nil, path.asString, poly.asInteger) + } + createNode { | path, type='LADSPA', lib, label, poly=1, created, handler | + newNodeEnd = created; + this.sendReq("synth/create_node",handler,path,type,lib,label,poly) + } + createAudioInput { | path, handler | + this.createNode(path,"Internal","","audio_input",0,handler) + } + createAudioOutput {|path,handler| + this.createNode(path,"Internal","","audio_output",0,handler) + } + createMIDIInput {|path,handler| + this.createNode(path,"Internal","","midi_input",1,handler) + } + createMIDIOutput {|path,handler| + this.createNode(path,"Internal","","midi_output",1,handler) + } + createNoteIn {|path| this.createNode(path,"Internal","","note_in") } + connect {|fromPath,toPath,handler| + this.sendReq("synth/connect",handler,fromPath.asString,toPath.asString) + } + disconnect { | fromPath, toPath, handler | + this.sendReq("synth/disconnect",handler,fromPath.asString,toPath.asString) + } + disconnectAll { | path, handler | + this.sendReq("synth/disconnect_all",handler,path); + } + sendReq { | path, handler...args | + var id = allocator.alloc; + requestHandlers[id] = handler; + addr.sendMsg("/om/"++path, id, *args) + } + quit { + if (loadIntoJack) { + ("jack_unload"+"Om").unixCmd; + booting=false; + requestResponders.do(_.remove); + notificationResponders.do(_.remove); + this.changed(\running, false); + } { + this.sendReq("engine/quit", { + booting=false; + requestResponders.do(_.remove); + notificationResponders.do(_.remove); + this.changed(\running, false); + }) + } + } + ping {| n=1, func | + var id, result, start; + id = allocator.alloc; + result = 0; + requestHandlers[id] = { + var end; + end = Main.elapsedTime; + result=max((end-start).postln,result); + n=n-1; + if (n > 0) { + start = Main.elapsedTime; + addr.sendMsg("/om/ping", id) + } { + allocator.free(id); + func.value(result) + } + }; + start = Main.elapsedTime; + addr.sendMsg("/om/ping", id) + } + setPortValue {|path, val| this.getPort(path.asString).value=val } + jackConnect {|path, jackPort| + this.getPort(path).jackConnect(jackPort) + } + noteOn {|path, note, vel| + var patch,node; + patch = this.getPatch(path,true); + if (patch.notNil) { patch.noteOn(note,vel) }; + node = this.getNode(path); + if (node.notNil) { node.noteOn(note,vel) }; + } + noteOff {|path, note| + var patch,node; + patch = this.getPatch(path,true); + if (patch.notNil) { patch.noteOff(note) }; + node = this.getNode(path); + if (node.notNil) { node.noteOff(note) }; + } + matchPlugins{ | label, lib, name, type | + ^plugins.select{ |p| + label.matchItem(p.label) and: { + lib.matchItem(p.lib) and: { + name.matchItem(p.name) and: { + type.matchItem(p.type) + } + } + } + } + } + dssiMsg {|path,reqType="program" ...args| + addr.sendMsg("/dssi"++path++$/++reqType,*args) + } +} + +OmMetadata { + var object, dict; + *new {|obj|^super.new.metadataInit(obj)} + metadataInit {|obj| + dict=Dictionary.new; + object=obj + } + put {|key,val| + object.engine.sendReq("metadata/set", nil, + object.path, key.asString, val.asString) + } + at {|key|^dict.at(key.asSymbol)} + prSetMetadata {|key,val| + dict.put(key,val); + object.changed(\metadata, key, val) + } +} + +OmObject : Model { + var <name, <parent, <metadata; + *new {|name, parent| + ^super.new.initOmObject(name,parent); + } + initOmObject {|argName, argParent| + name = argName; + parent = argParent; + metadata=OmMetadata(this) + } + path { ^parent.notNil.if({ parent.path ++ $/ ++ name }, name).asString } + depth { ^parent.notNil.if({ parent.depth + 1 }, 0) } + engine { ^parent.engine } +} + +OmPort : OmObject { + var <porttype, <direction, <spec, <value, <connections; + *new {|name,parent,type,dir,hint,def,min,max| + ^super.new(name,parent).initPort(type,dir,hint,def,min,max) + } + initPort {|type,dir,hint,def,min,max| + porttype = type; + direction = dir; + spec = ControlSpec(min, max, + if (hint == 'LOGARITHMIC', 'exp', 'lin'), + if (hint == 'INTEGER', 1, 0), + def); + connections = IdentityDictionary.new; + } + jackConnect {|jackPort| + if (porttype.asSymbol != \AUDIO + || direction.asSymbol!=\OUTPUT) { + Error("Not a audio output port").throw + }; + ("jack_connect" + "Om:"++(this.path) + jackPort).unixCmd + } + value_ {|val| + if (porttype == \CONTROL and: {direction == \INPUT}) { + this.engine.sendReq("synth/set_port_value", nil, this.path, val) + } { + Error("Not a input control port"+this.path).throw + } + } + connectTo {|destPort| + if (this.direction!=destPort.direction) { + this.engine.connect(this,destPort) + } { + Error("Unable to connect ports with same direction").throw + } + } + + // Setters for OSC responders + prSetValue {|argValue| + if (value != argValue) { + value = argValue; + this.changed(\value, value) + } + } +} + +OmNode : OmObject { // Abstract class + var <ports, <polyphonic; + *new {|name,parent,poly| + ^super.new(name,parent).initNode(poly) + } + initNode {|argPoly| + polyphonic = argPoly; + ports = IdentityDictionary.new; + } + hasPort {|name| ^ports[name.asSymbol].notNil } + getPort {|name| ^ports[name.asSymbol] } + portArray {|type=\AUDIO,dir=\OUTPUT| + var result = Array.new(8); + ports.do {|port| + if (port.porttype==type and: {port.direction==dir}) { + result=result.add(port) + } + }; + ^result + } + audioOut { + ^this.portArray(\AUDIO, \OUTPUT) + } + audioIn { + ^this.portArray(\AUDIO, \INPUT) + } +} + +OmInternalNode : OmNode { + var <pluginlabel; + *new {|name,parent,poly,label| + ^super.new(name,parent,poly).initInternalNode(label) + } + initInternalNode {|label| + pluginlabel = label + } + noteOn { |note,vel| + if (pluginlabel == \note_in or: + {pluginlabel == \trigger_in and: {this.value == note}}) { + this.engine.sendReq("synth/note_on", nil, this.path, note, vel) + } { + Error("Not a trigger_in or note_in node").throw + } + } + noteOff {|note| + if (pluginlabel == \note_in or: + {pluginlabel == \trigger_in and: {this.value == note}}) { + this.engine.sendReq("synth/note_off", nil, this.path, note) + } { + Error("Not a trigger_in or note_in node").throw + } + } +} + +OmLADSPANode : OmNode { + var <pluginlabel, <libname; + *new {|name, parent, poly, label, lib| + ^super.new(name,parent,poly).initLADSPANode(label,lib) + } + initLADSPANode {|label,lib| + pluginlabel = label; + libname = lib + } +} + +OmDSSINode : OmLADSPANode { + var programs; + *new {|name,parent,poly,label,lib| + ^super.new(name,parent,poly,label,lib).initDSSI + } + initDSSI { + programs = Set.new; + } + program {|bank, prog| + this.engine.dssiMsg(this.path,"program",bank.asInteger,prog.asInteger) + } + prProgramAdd {|bank,program,name| + var info = (bank:bank, program:program, name:name); + if (programs.includes(info).not) { + programs.add(info); + this.changed(\programAdded, info) + } + } +} + +OmPatch : OmNode { + var <nodes, <patches, <poly, <enabled; + var om; + *new {|name,parent,engine| + ^super.new(name,parent).initPatch(engine); + } + initPatch {|argEngine| + nodes = IdentityDictionary.new; + patches = IdentityDictionary.new; + om = argEngine + } + hasNode {|name| + ^nodes[name.asSymbol].notNil + } + getNode {|name| + ^nodes[name.asSymbol] + } + hasPatch {|name| + ^patches[name.asSymbol].notNil + } + getPatch {|name,mustExist=false| + if (this.hasPatch(name).not) { + if (mustExist) { ^nil }; + patches[name.asSymbol] = OmPatch(name,this); + }; + ^patches[name.asSymbol] + } + engine { + if (om.notNil) { ^om } { ^parent.engine }; + } + connect {|fromPort, toPort| + this.engine.connect(this.path++$/++fromPort, this.path++$/++toPort) + } + dumpX { + (String.fill(this.depth,$ )++"*"+this.path).postln; + nodes.do{|node| + (String.fill(node.depth,$ )++"**"+node.path).postln; + node.ports.do{|port| + (String.fill(port.depth,$ )++"***"+port.path).postln }; + }; + ports.do{|port| (String.fill(port.depth,$ )++"***"+port.path).postln }; + patches.do(_.dump) + } + noteOn {|note,vel| + var targetNode; + this.nodes.do{|node| + if (node.class == OmInternalNode) { + node.pluginlabel.switch( + \trigger_in, { + if (node.ports['Note Number'].value == note) { + targetNode = node; + } + }, + \note_in, { + targetNode = node; + } + ) + } + }; + if (targetNode.notNil) { + targetNode.noteOn(note, vel) + } { + Error("Unable to find trigger_in for note "++note).throw + } + } + noteOff {|note| + var targetNode; + this.nodes.do{|node| + if (node.class == OmInternalNode) { + node.pluginlabel.switch( + \trigger_in, { + if (node.ports['Note Number'].value == note) { + targetNode = node; + } + }, + \note_in, { + targetNode = node; + } + ) + } + }; + if (targetNode.notNil) { + targetNode.noteOff(note) + } { + Error("Unable to find node for note_off "++note).throw + } + } + newPatch {|name, poly=1, handler| + this.engine.createPatch(this.path++$/++name, poly, handler) + } + newNode {|name, poly=1, type, lib, label, fullname, handler| + var candidates = this.engine.matchPlugins(label,lib,fullname,type); + if (candidates.size == 1) { + candidates.pop.new(this.path++"/"++name, poly, handler) + } { + if (candidates.size==0) { + Error("Plugin not found").throw + } { + Error("Plugin info not unique").throw + } + } + } + + // Setters for OSC responders + prSetPoly {|argPoly| poly = argPoly } + prSetEnabled {|flag| + if (flag != enabled) { + enabled = flag; + this.changed(\enabled, flag) + } + } +} + + +OmEmacsUI { + var engine, window, bootBtn; + *new {|engine| ^super.newCopyArgs(engine).init } + init { + window = EmacsBuffer("*Om -"+engine.addr.ip++$:++engine.addr.port); + bootBtn = EmacsButton(window, ["Boot","Quit"], {|value| + if (value==1) { + engine.boot + } { + engine.quit + } + }).value=engine.registered.binaryValue; + engine.addDependant(this); + window.front; + } + update {|who, what ... args| + Emacs.message(who.asString+what+args); + if (what == \newPatch or: {what == \newNode or: {what == \newPort}}) { + args[0].addDependant(this) + }; + } +} diff --git a/src/progs/supercollider/README b/src/progs/supercollider/README new file mode 100644 index 00000000..020da8aa --- /dev/null +++ b/src/progs/supercollider/README @@ -0,0 +1,11 @@ +Om bindings for SuperCollider +----------------------------- + +Installation: +Copy the Om.sc file into some directory in your SC class path and +reboot sclang. + +Usage: +See example.sc for inspiration. + + -- Mario Lang <mlang@delysid.org> diff --git a/src/progs/supercollider/example.sc b/src/progs/supercollider/example.sc new file mode 100644 index 00000000..6186e433 --- /dev/null +++ b/src/progs/supercollider/example.sc @@ -0,0 +1,27 @@ +// Boot Om (inside SC) and the scsynth server +( +o=Om.boot { + o.loadPatch("/home/mlang/src/om-synth/src/clients/patches/303.om"); + s.boot; +} +) +// Connect patch output to one SC input +o.jackConnect("/303/output", "SuperCollider:in_3"); +// Play this input on the default output bus (most simple postprocessor) +{AudioIn.ar(3).dup}.play; +// A simple 303 pattern +( +Tempo.bpm=170; +Ppar([ + Pbind(\type, \om, \target, o.getPatch("/303",true), + \dur, 1/1, \octave,2,\degree, Pseq([Pshuf([0,2,4,6],16)],2)), + Pbind(\type, \om, \target, o.getPatch("/303",true), + \dur, Prand([Pshuf([1/2,1/4,1/4],4),Pshuf([1/3,1/3,1/6,1/6],4)],inf), + \legato, Prand((0.5,0.55..0.9),inf), + \octave,3,\degree, Pseq([Pshuf([0,2,4,6],2)],40)), + Pbind(\type, \om, \omcmd, \setPortValue, + \target, o.getPort("/303/cutoff",true), + \dur, 1/10, \portValue, Pseq((0,0.005..1).mirror,2)), + Pbind(\type, \om, \omcmd, \setPortValue, + \target, o.getPort("/303/resonance",true), \portValue, 1)]).play(quant:4) +) |