├── .gitignore ├── .travis.yml ├── README.md ├── classes ├── JSON.sc ├── Library.sc ├── Network.sc ├── Runtime.sc └── SndFlo.sc ├── components ├── AudioOut.scd ├── Balance2.scd ├── DualSine.scd ├── LowPassFilter.scd ├── SawWave.scd └── SineWave.scd ├── config ├── jackd.service ├── sndflo.service └── xvfb.service ├── doc └── braindump.md ├── graphs └── sawsynth.json ├── package.json ├── sndflo-runtime.scd ├── sndflo.coffee ├── sndflo.js ├── sndflo.scd └── spec ├── runtime.coffee └── utils.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - sudo add-apt-repository -y ppa:supercollider/ppa 6 | - sudo apt-get update -qq 7 | - sudo apt-get -y install supercollider 8 | script: 9 | - sudo mkdir -p /usr/share/SuperCollider/Extensions || true 10 | - sudo ln -s `pwd` /usr/share/SuperCollider/Extensions/sndflo 11 | - jackd -d dummy & 12 | - sleep 3 13 | - export SNDFLO_TESTS_VERBOSE=1 14 | - npm test 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Data-flow programming for SuperCollider? 2 | ======================================== 3 | sndflo allows to program [SuperCollider](http://supercollider.sourceforge.net) using 4 | the [Flowhub](http://flowhub.io) visual data-flow IDE. 5 | 6 | Status 7 | ------ 8 | Proof-of-concept working for wiring up Synth's. 9 | 10 | 11 | Install 12 | -------- 13 | **Note: Only tested on GNU/Linux.** 14 | Should work fine on other platforms with minor adjustments in install. 15 | 16 | Prerequities 17 | 18 | * [SuperCollider](http://supercollider.sourceforge.net/downloads/) (version 3.5 or later) 19 | * [node.js](http://nodejs.org/download/) 20 | 21 | Install 22 | 23 | git clone https://github.com/jonnor/sndflo.git 24 | cd sndflo 25 | 26 | # Install as SuperCollider extension, ref http://doc.sccode.org/Guides/UsingExtensions.html 27 | # On Linux 28 | mkdir -p ~/.local/share/SuperCollider/Extensions || true 29 | ln -s `pwd` ~/.local/share/SuperCollider/Extensions/sndflo 30 | # On Mac OSX 31 | mkdir -p "$HOME/Library/Application Support/SuperCollider/Extensions" || true 32 | ln -s `pwd` "$HOME/Library/Application Support/SuperCollider/Extensions/sndflo" 33 | 34 | npm install -g node-gyp 35 | npm install 36 | 37 | Running 38 | --------- 39 | 40 | # For Mac OSX only, specify where sclang is 41 | export PATH=$PATH:/Applications/SuperCollider/SuperCollider.app/Contents/Resources/ 42 | 43 | node sndflo.js --verbose --user MY_FLOWHUB_UUID 44 | 45 | Will start up SuperCollider, loading the sndflo runtime and FBP protocol bridge. 46 | On success should output something like 47 | 48 | Listening at WebSocket port 3569 49 | OSC send/receive ports: 57120 57122 50 | Registered with Flowhub, should be accessible in UI 51 | 52 | Go to [http://app.flowhub.io](http://app.flowhub.io), refresh the runtime list. 53 | You should see our sndflo runtime listed, be able to create projects for 'sndflo' and connect. 54 | 55 | Note: on GNU/Linux sclang might fail with a segfault if X11 is not available. 56 | You can use `xvfb-run` to work around this. 57 | 58 | Vision 59 | --------- 60 | * Program audio pipelines visually, by Synth's wiring together with Busses 61 | * Program synths visually, by creating SynthDefs by combining UGens 62 | * Do not replace sclang, integrate with it 63 | * Allow to use SynthDefs created in sclang in visual pipelines 64 | * Allow to drive visually created Synth pipelines using sclang Events/Patterns 65 | * Seamless integration with other FBP 66 | * MicroFlo for communicating with microcontrollers (sensing/acting) 67 | * NoFlo for general-purpose use, and generating composition/scores 68 | * Combined audio processing with [noflo-webaudio](https://github.com/automata/noflo-webaudio) 69 | * Program scores visually, using Streams and Patterns 70 | 71 | Usecases 72 | ------- 73 | * Generative & algorithmic composed music 74 | * Reactive and interactive art installations 75 | * Audio effect pipelines, processing sound inputs 76 | * Audio and music analysis, feature extraction 77 | 78 | 79 | -------------------------------------------------------------------------------- /classes/JSON.sc: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/supercollider-quarks/quarks/edit/master/API/JSON.sc 2 | SndFloJSON { 3 | 4 | classvar key value all of its members 57 | 58 | // datetime 59 | // "2010-04-20T20:08:21.634121" 60 | // http://en.wikipedia.org/wiki/ISO_8601 61 | 62 | ("No JSON conversion for object" + obj).warn; 63 | ^SndFloJSON.stringify(obj.asCompileString) 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /classes/Library.sc: -------------------------------------------------------------------------------- 1 | // sndflo - Flowhub.io sound processing runtime based on SuperCollider 2 | // (c) 2014 Jon Nordby 3 | // sndflo may be freely distributed under the MIT license 4 | 5 | SndFloLibrary { 6 | // TODO: use a SynthDescLib? 7 | var on_component_changed; 12 | 13 | *new { arg server; 14 | ^super.new.init(server) 15 | } 16 | init { arg server; 17 | server = server; 18 | synthdefs = Dictionary.new; 19 | componentDir = "./components"; 20 | componentExtension = ".scd"; 21 | on_component_changed = { |name| }; // override 22 | 23 | this.registerDefaults(); 24 | } 25 | 26 | *silentIn { ^12; } 27 | *silentOut { ^13; } 28 | 29 | *componentDir { } 30 | 31 | registerSynthDef { arg id, def; 32 | synthdefs["synth/"++id] = def; 33 | def.send(server); 34 | } 35 | 36 | getSource { arg name; 37 | var tokens = name.split; 38 | var ret = nil; 39 | (tokens.size == 2).if({ 40 | var lib = tokens[0]; 41 | var component = tokens[1]; 42 | // XXX: right now lib is ignored, we only support "synth" 43 | var path = componentDir+/+component++componentExtension; 44 | var file = File.open(path, "r"); 45 | file.isOpen.if({ 46 | ret = file.readAllString; 47 | "GETSOURCE %\n".postf(path); 48 | }); 49 | }); 50 | ^ret; 51 | } 52 | 53 | setSource { arg name, code; 54 | var def = code.interpret; 55 | if(def.notNil && "synth/"++def.name == name, { 56 | this.registerSynthDef(def.name, def); 57 | "SETSOURCE %\n".postf(name); 58 | on_component_changed.value(name); 59 | }); 60 | } 61 | 62 | registerDefaults { 63 | var paths = PathName.new(componentDir).files; 64 | paths.do({ |pathobj| 65 | var path = pathobj.fullPath; 66 | path.endsWith(componentExtension).if({ 67 | var file = File.open(path, "r"); 68 | file.isOpen.if({ 69 | var content = file.readAllString; 70 | var def = content.interpret; 71 | this.registerSynthDef(def.name, def); 72 | file.close; 73 | }); 74 | }); 75 | }); 76 | } 77 | } -------------------------------------------------------------------------------- /classes/Network.sc: -------------------------------------------------------------------------------- 1 | // sndflo - Flowhub.io sound processing runtime based on SuperCollider 2 | // (c) 2014 Jon Nordby 3 | // sndflo may be freely distributed under the MIT license 4 | 5 | SndFloGraph : Object { 6 | var on_ports_changed; 12 | 13 | var Dictionary[ src -> .., tgt -> .. ] 24 | iips = Dictionary.new; // "port src" -> Object 25 | nextBusNumber = 20; // Avoid hardware busses. FIXME: unhardcode 26 | inports = Dictionary.new; 27 | outports = Dictionary.new; 28 | on_ports_changed = { |inports, outport| }; // override 29 | } 30 | 31 | addPort { arg direction, name, id, port; 32 | var ports; 33 | (direction == "in").if({ ports=inports }, { ports=outports }); 34 | ports[name] = Dictionary[ "port" -> port, "node" -> id]; 35 | "EXPORT %port: % => % %\n".postf(direction, name, port.toUpper, id); 36 | on_ports_changed.value(this.inports, this.outports); 37 | } 38 | removePort { arg direction, name; 39 | var ports; 40 | (direction == "in").if({ ports=inports }, { ports=outports }); 41 | ports[name] = nil; 42 | "UNEXPORT %port: %\n".postf(direction, name); 43 | on_ports_changed.value(this.inports, this.outports); 44 | } 45 | 46 | addNode { arg id, component; 47 | var d = library.synthdefs[component]; 48 | component.postln; 49 | "%(%)\n".postf(id, d.name); 50 | nodes[id] = Synth.newPaused(d.name); 51 | } 52 | removeNode { arg id; 53 | nodes[id] = nil; 54 | "DEL %()".postf(id); 55 | } 56 | 57 | addEdge { arg srcId, srcPort, tgtId, tgtPort; 58 | // TODO: recycle busses when edges using it is removed 59 | var busForEdge = nextBusNumber; 60 | nextBusNumber = nextBusNumber+1; 61 | "% % -> % %\n".postf(srcId, srcPort.toUpper, tgtPort.toUpper, tgtId); 62 | nodes[srcId].post; nodes[tgtId].postln; 63 | 64 | // Connect edge, by using same Bus number 65 | nodes[srcId].set(srcPort.asSymbol, busForEdge); 66 | nodes[tgtId].set(tgtPort.asSymbol, busForEdge); 67 | // Modify order-of-executioon so that target can hear source 68 | nodes[srcId].moveBefore(nodes[tgtId]); 69 | // TODO: walk backwards towards front and apply to all 70 | 71 | // Store state 72 | connections[busForEdge] = Dictionary[ 73 | "src" -> Dictionary [ "process" -> srcId, "port" -> srcPort ], 74 | "tgt" -> Dictionary [ "process" -> tgtId, "port" -> tgtPort ], 75 | ] 76 | } 77 | removeEdge { arg srcId, srcPort, tgtId, tgtPort; 78 | var busForEdge = nil; 79 | "DEL % % -> % %\n".postf(srcId, srcPort.toUpper, tgtPort.toUpper, tgtId); 80 | connections.keysValuesDo({ |k, v| 81 | var found = v["src"]["process"] == srcId && 82 | v["src"]["port"] == srcPort && 83 | v["tgt"]["process"] == tgtId && 84 | v["tgt"]["port"] == tgtPort; 85 | busForEdge = if(found, { ^k }, { ^nil }); 86 | }); 87 | "BUS: ".post; busForEdge.postln; 88 | 89 | connections[busForEdge] = nil; 90 | nodes[srcId].set(srcPort.asSymbol, SndFloLibrary.silentOut); 91 | nodes[tgtId].set(tgtPort.asSymbol, SndFloLibrary.silentIn); 92 | } 93 | 94 | addIIP { arg tgtId, tgtPort, data; 95 | "IIP: '%' -> % %\n".postf(data, tgtPort.toUpper, tgtId); 96 | // TODO: support other data than floats 97 | nodes[tgtId].set(tgtPort.asSymbol, data.asFloat); 98 | iips[tgtPort+tgtId] = data.asFloat; 99 | } 100 | removeIIP { arg tgtId, tgtPort; 101 | // sets back default value 102 | var tgtNode, component, definition, specs, defaultValue; 103 | tgtNode = nodes[tgtId]; 104 | if (tgtNode.notNil, { 105 | component = "synth/"++tgtNode.defName; 106 | definition = library.synthdefs[component]; 107 | specs = definition.metadata.specs; 108 | defaultValue = specs[tgtPort.asSymbol].default; 109 | tgtNode.set(tgtPort.asSymbol, defaultValue); 110 | iips[tgtPort+tgtId] = nil; 111 | 112 | "DEL IIP -> % %\n".postf(tgtPort.toUpper, tgtId); 113 | }); 114 | } 115 | } 116 | 117 | 118 | SndFloNetwork : Object { 119 | var ("synth/"++synth.defName); 174 | ]; 175 | root["processes"][name] = proc; 176 | }); 177 | 178 | root["connections"] = List.new(); 179 | this.graph.connections.keysValuesDo({ |bus,conn| 180 | root["connections"].add(conn); 181 | }); 182 | this.graph.iips.keysValuesDo({ |tgtStr, iip| 183 | var tokens = tgtStr.split($ ); 184 | tokens.postln; tokens[0].postln; 185 | root["connections"].add(Dictionary[ 186 | "tgt" -> Dictionary["port" -> tokens[0], "process" -> tokens[1]], 187 | "data" -> iip, 188 | ]); 189 | }); 190 | 191 | root["inports"] = Dictionary.new(); 192 | this.graph.inports.keysValuesDo({ |name, internal| 193 | root["inports"][name] = Dictionary[ 194 | "process" -> internal["node"], 195 | "port" -> internal["port"], 196 | ]; 197 | }); 198 | 199 | root["outports"] = Dictionary.new(); 200 | this.graph.outports.keysValuesDo({ |name, internal| 201 | root["outports"][name] = Dictionary[ 202 | "process" -> internal["node"], 203 | "port" -> internal["port"], 204 | ]; 205 | }); 206 | 207 | ^root; 208 | } 209 | 210 | toJSON { 211 | ^SndFloJSON.stringify(this.saveGraph()); 212 | } 213 | 214 | } -------------------------------------------------------------------------------- /classes/Runtime.sc: -------------------------------------------------------------------------------- 1 | // sndflo - Flowhub.io sound processing runtime based on SuperCollider 2 | // (c) 2014 Jon Nordby 3 | // sndflo may be freely distributed under the MIT license 4 | 5 | SndFloUiConnection : Object { 6 | var receiveOscFunc; 7 | var uiAddr; 8 | var <>on_message; 9 | 10 | *new { arg listenAddr; 11 | ^super.new.init(listenAddr) 12 | } 13 | init { arg listenAddr; 14 | uiAddr = NetAddr(listenAddr.ip, listenAddr.port+2); 15 | receiveOscFunc = OSCFunc.new({ |msg, time, addr, recvPort| 16 | "received: ".post; msg.postln; 17 | this.receiveOsc(msg,time,addr,recvPort); 18 | }, "/fbp/runtime/message"); 19 | on_message = { |a| }; // override 20 | } 21 | 22 | sendMessage { arg protocol, command, payload; 23 | var msg = Dictionary[ 24 | "protocol" -> protocol, 25 | "command" -> command, 26 | "payload" -> payload 27 | ]; 28 | var str = SndFloJSON.stringify(msg); 29 | "sending response: ".post; str.postln; 30 | uiAddr.sendMsg("/fbp/ui/message", str); 31 | } 32 | 33 | receiveOsc { |msg, time, addr, recvPort| 34 | var m = msg[1].asString.parseYAML; 35 | this.handleMessage(m["protocol"], m["command"], m["payload"]); 36 | } 37 | 38 | handleMessage { arg protocol, cmd, payload; 39 | "handleMessage: % %\n".postf(protocol, cmd); 40 | on_message.value(protocol, cmd, payload); 41 | } 42 | } 43 | 44 | 45 | SndFloRuntime : Object { 46 | var connection; 47 | var "default/main", // FIXME: unhardcode 79 | "inPorts" -> inports, 80 | "outPorts" -> outports, 81 | ]; 82 | network.graph.inports.keysValuesDo({ |key,value| 83 | var p = Dictionary[ 84 | "id" -> key, 85 | "type" -> "all", // TODO: implement 86 | "description" -> "", // TODO: implement 87 | "addressable" -> false, 88 | "required" -> false, 89 | ]; 90 | inports.add(p); 91 | }); 92 | network.graph.outports.keysValuesDo({ |key,value| 93 | outports.add(Dictionary[ 94 | "id" -> key, 95 | "type" -> "all", // TODO: implement 96 | "description" -> "", // TODO: implement 97 | "addressable" -> false, 98 | "required" -> false, 99 | ]); 100 | }); 101 | 102 | connection.sendMessage("runtime", "ports", payload); 103 | } 104 | 105 | sendComponent { arg name; 106 | var synthdef = library.synthdefs[name]; 107 | var inPorts = List.new; 108 | var outPorts = List.new; 109 | var info; 110 | synthdef.allControlNames.do({ |control| 111 | var type = "bus"; // TODO: separate out non-bus params 112 | var p = Dictionary[ 113 | "id" -> control.name, 114 | "type" -> type, 115 | "description" -> "", 116 | "addressable" -> false, // TODO: support multi-channel 117 | "required" -> false // TODO: should be true for input busses 118 | ]; 119 | // TODO: support multiple out-ports 120 | // FIXME: use something better than heuristics to determine out ports 121 | if (control.name.asString == "out", { 122 | outPorts.add(p); 123 | }, { 124 | inPorts.add(p); 125 | }); 126 | 127 | }); 128 | 129 | info = Dictionary[ 130 | "name" -> name, 131 | "description" -> synthdef.metadata.description.asString, 132 | "icon" -> "music", 133 | "subgraph" -> false, 134 | "inPorts" -> inPorts, 135 | "outPorts" -> outPorts 136 | ]; 137 | 138 | connection.sendMessage("component", "component", info); 139 | } 140 | 141 | handleMessage { arg protocol, cmd, payload; 142 | 143 | case 144 | { (protocol == "runtime" && cmd == "getruntime") } 145 | { 146 | var info = Dictionary[ 147 | "type" -> "sndflo", 148 | "version" -> "0.4", // protocol version 149 | "capabilities" -> ["protocol:component", 150 | "protocol:network", 151 | "protocol:graph", 152 | "protocol:runtime", 153 | "component:getsource", 154 | "component:setsource", 155 | ] 156 | ]; 157 | if(network.notNil, { 158 | info["graph"] = "default/main"; // FIXME: unhardcode 159 | }); 160 | connection.sendMessage("runtime", "runtime", info); 161 | this.sendPorts(nil); 162 | } 163 | { (protocol == "runtime" && cmd == "packet") } 164 | { 165 | if(payload["event"] == "data", { 166 | network.sendPacket(payload["port"], payload["payload"]); 167 | }); 168 | } 169 | { (protocol == "component" && cmd == "list") } 170 | { 171 | library.synthdefs.keysValuesDo({ |name,synthdef| 172 | this.sendComponent(name); 173 | }); 174 | } 175 | { (protocol == "component" && cmd == "getsource") } 176 | { 177 | var name = payload["name"]; 178 | var code = nil; 179 | (name == "default/main").if({ 180 | var response = Dictionary[ 181 | "library" -> "default", // https://github.com/noflo/noflo-ui/issues/411 182 | "name" -> "main", 183 | "language" -> "json", 184 | "code" -> this.network.toJSON(), 185 | ]; 186 | connection.sendMessage("component", "source", response); 187 | }, { 188 | var code = library.getSource(name); 189 | code.notNil.if({ 190 | var response = Dictionary[ 191 | "name" -> name, 192 | "language" -> "supercollider", 193 | "code" -> code, 194 | ]; 195 | connection.sendMessage("component", "source", response); 196 | }); 197 | }); 198 | 199 | } 200 | { (protocol == "component" && cmd == "source") } 201 | { 202 | var name = payload["name"]; 203 | library.setSource(name, payload["code"]); 204 | } 205 | { (protocol == "graph" && cmd == "clear") } 206 | { 207 | network = SndFloNetwork.new(library); 208 | network.graph.on_ports_changed = { 209 | this.sendPorts(); 210 | }; 211 | } 212 | { (protocol == "graph" && cmd == "addnode") } 213 | { 214 | network.graph.addNode(payload["id"], payload["component"]); 215 | } 216 | { (protocol == "graph" && cmd == "removenode") } 217 | { 218 | network.graph.removeNode(payload["id"]); 219 | } 220 | { (protocol == "graph" && cmd == "addinitial") } 221 | { 222 | network.graph.addIIP(payload["tgt"]["node"], payload["tgt"]["port"], 223 | payload["src"]["data"]); 224 | } 225 | { (protocol == "graph" && cmd == "removeinitial") } 226 | { 227 | network.graph.removeIIP(payload["tgt"]["node"], payload["tgt"]["port"]); 228 | } 229 | { (protocol == "graph" && cmd == "addedge") } 230 | { 231 | network.graph.addEdge(payload["src"]["node"], payload["src"]["port"], 232 | payload["tgt"]["node"], payload["tgt"]["port"]); 233 | } 234 | { (protocol == "graph" && cmd == "removeedge") } 235 | { 236 | network.graph.removeEdge(payload["src"]["node"], payload["src"]["port"], 237 | payload["tgt"]["node"], payload["tgt"]["port"]); 238 | } 239 | { (protocol == "graph" && cmd == "addinport") } 240 | { 241 | network.graph.addPort("in", payload["public"], payload["node"], payload["port"]); 242 | } 243 | { (protocol == "graph" && cmd == "removeinport") } 244 | { 245 | network.graph.removePort("in", payload["public"]); 246 | } 247 | { (protocol == "graph" && cmd == "addoutport") } 248 | { 249 | network.graph.addPort("out", payload["public"], payload["node"], payload["port"]); 250 | } 251 | { (protocol == "graph" && cmd == "removeoutport") } 252 | { 253 | network.graph.removePort("out", payload["public"]); 254 | } 255 | { (protocol == "network" && cmd == "start") } 256 | { 257 | // TODO: include timestamp 258 | network.start(true); 259 | connection.sendMessage("network", "started", Dictionary.new); 260 | } 261 | { (protocol == "network" && cmd == "stop") } 262 | { 263 | // TODO: include timestamp 264 | network.start(false); 265 | connection.sendMessage("network", "stopped", Dictionary.new); 266 | } 267 | { true /*default*/ } 268 | { 269 | "Unhandled message from UI: procotol=%, cmd=%\n".postf(protocol, cmd); 270 | }; 271 | 272 | } 273 | } -------------------------------------------------------------------------------- /classes/SndFlo.sc: -------------------------------------------------------------------------------- 1 | SndFlo { 2 | 3 | *outAudioBusSpec { 4 | ^ControlSpec(units: "Out2AudioBus", default: SndFloLibrary.silentOut); 5 | } 6 | *inAudioBusSpec { 7 | ^ControlSpec(units: "InAudioBus", default: SndFloLibrary.silentIn); 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /components/AudioOut.scd: -------------------------------------------------------------------------------- 1 | SynthDef("AudioOut", { 2 | arg in=SndFloLibrary.silentIn, out=0; 3 | Out.ar(out, In.ar(in)) 4 | }, 5 | metadata: ( 6 | description: "Play out on soundcard", 7 | specs: ( 8 | in: SndFlo.inAudioBusSpec, 9 | ) 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /components/Balance2.scd: -------------------------------------------------------------------------------- 1 | SynthDef("Balance2", { 2 | arg a=SndFloLibrary.silentIn, 3 | b=SndFloLibrary.silentIn, 4 | out=SndFloLibrary.silentOut, 5 | mix=0; 6 | var stereo = Balance2.ar(In.ar(a), In.ar(b), mix); 7 | var o = stereo[0] + stereo[1]; 8 | // var o = In.ar(a) + In.ar(b); 9 | Out.ar(out, o) 10 | }, 11 | metadata: ( 12 | description: "Mix two channels using Balance2", 13 | specs: ( 14 | a: SndFlo.inAudioBusSpec, 15 | b: SndFlo.inAudioBusSpec, 16 | out: SndFlo.outAudioBusSpec, 17 | mix: ControlSpec(-1, 1, default: 0), 18 | ) 19 | ) 20 | ) 21 | -------------------------------------------------------------------------------- /components/DualSine.scd: -------------------------------------------------------------------------------- 1 | SynthDef("DualSine", { 2 | arg out=SndFloLibrary.silentOut, 3 | freq=440, freq1=440, vol=1, vol1=1; 4 | var f = SinOsc.ar(freq, mul: vol); 5 | var f1 = SinOsc.ar(freq1, mul: vol1); 6 | Out.ar(out, f + f1, vol) 7 | }, 8 | metadata: ( 9 | description: "Mix two channels using Balance2", 10 | specs: ( 11 | a: SndFlo.inAudioBusSpec, 12 | b: SndFlo.inAudioBusSpec, 13 | out: SndFlo.outAudioBusSpec, 14 | mix: ControlSpec(-1, 1, default: 0), 15 | ) 16 | ) 17 | ) 18 | -------------------------------------------------------------------------------- /components/LowPassFilter.scd: -------------------------------------------------------------------------------- 1 | SynthDef("LowPassFilter", { 2 | arg in=SndFloLibrary.silentIn, out=SndFloLibrary.silentOut, freq=4400; 3 | Out.ar(out, BLowPass.ar(In.ar(in), freq)) 4 | }, 5 | metadata: ( 6 | description: "", 7 | specs: ( 8 | freq: ControlSpec(20, 20000, \exp, 0.1, 2200, "Hz") 9 | ) 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /components/SawWave.scd: -------------------------------------------------------------------------------- 1 | SynthDef("SawWave", { 2 | arg out=SndFloLibrary.silentOut, freq=440; 3 | Out.ar(out, Saw.ar(freq)) 4 | }, 5 | metadata: ( 6 | description: "", 7 | specs: ( 8 | freq: ControlSpec(20, 20000, \exp, 0.1, 220, "Hz"), 9 | out: SndFlo.outAudioBusSpec, 10 | ) 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /components/SineWave.scd: -------------------------------------------------------------------------------- 1 | SynthDef("SineWave", { 2 | arg out=SndFloLibrary.silentOut, freq=4400; 3 | 4 | Out.ar(out, SinOsc.ar(freq, 0, 0.5)) 5 | }, 6 | metadata: ( 7 | description: "my filter", 8 | specs: ( 9 | freq: ControlSpec(20, 20000, \exp, 0.1, 2200, "Hz"), 10 | out: SndFlo.outAudioBusSpec, 11 | ) 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /config/jackd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=jackd 3 | Wants=network.target 4 | Before=network.target 5 | 6 | [Service] 7 | Type=simple 8 | WorkingDirectory=/home/jon/sndflo 9 | User=jon 10 | Environment="DISPLAY=:99" 11 | ExecStart=/usr/bin/jackd -d alsa -d hw:1 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /config/sndflo.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=sndflo 3 | Wants=network.target 4 | Before=network.target 5 | Requires=jackd.service 6 | Before=jackd.service 7 | 8 | [Service] 9 | Type=simple 10 | WorkingDirectory=/home/jon/sndflo 11 | User=jon 12 | Environment="DISPLAY=:99" 13 | ExecStart=/usr/bin/node sndflo.js --verbose --user 3f3a8187-0931-4611-8963-239c0dff1931 --host 10.0.0.10 --id 3a7dab01-dfec-4580-9f4a-ab13fbb97a1a 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /config/xvfb.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=xvfb 3 | Wants=network.target 4 | Before=network.target 5 | 6 | [Service] 7 | Type=simple 8 | WorkingDirectory=/home/jon/sndflo 9 | User=jon 10 | ExecStart=/usr/bin/Xvfb :99 -screen 0 640x480x8 -nolisten tcp 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /doc/braindump.md: -------------------------------------------------------------------------------- 1 | 2 | The different architectural levels 3 | --------------------------- 4 | 5 | There are several, very related, pieces of SuperCollider that are 6 | interesting to attempt to make available to FBP and Flowhub. 7 | From the most fine-grained to the most high-level: 8 | 1. SynthDefs, composed of UGens 9 | 2. Synths, composed using Busses 10 | 3. Patterns, composed of Streams and Events 11 | 4. Inter-process audio pipelines, composed of multiple JACK clients 12 | 13 | 1) Can be implemented using the [Synth Definition File Format](http://doc.sccode.org/Reference/Synth-Definition-File-Format.html). 14 | 15 | 2) WIP, implemented in sclang as SuperCollider classes. 16 | Could also be implemented by sending OSC node control messages to scsynth directly. 17 | 18 | 3) Implementation strategy unknown 19 | 20 | Flow-based programming for algorithmic composition 21 | -------------------------------- 22 | 23 | Tools like the Event/Stream/Pattern system in SuperCollider make 24 | it possible to create complex algorithmic compositions. 25 | However, they are extremely hard to understand, largely because 26 | it is hard to see how each operation transforms its input on 27 | non-trivial data. 28 | 29 | Visualization of each step would be a major advantage. 30 | Even better would be if there were tools which would show whish inputs 31 | correspond to which outputs... 32 | Can one 'tag' each piece of input data and carry this metadata onwards 33 | until final destination, and be able to reconstruct a mapping between inputs<->outputs from this, and visualize it? 34 | 35 | The outputs are likely to be many-dimensonal, so advanced visualization 36 | techniques may be needed to illustrate the data... 37 | In SC, can the outputs be represented as a timed stream of 38 | scalar attributes with different synth/node targets? 39 | If time is explicitly modelled, how to handle casual realtime events? 40 | Perhaps just timestamp the in-data, play event at timestamp+delay comp. 41 | Will one need both absolute and relative timebases (offsets). 42 | 43 | There are many strong parallels between algorithmic audio composition 44 | and generative visual design. More traditional "sequencing" type composition 45 | has parallels to animation and keyframing. So maybe some of the concepts and 46 | practices used in noflo-canvas and similar can be reappropriated? 47 | And of course, for combined audiovisual works, having both audio and visuals be 48 | created/driven the same way is a killer feature! 49 | 50 | 51 | ## Communicating directly to scsynth (server) 52 | The SuperCollider frontend (sclang) communicates with the server using commands sent over OSC (Open Sound Control). 53 | This would make it possible to create synths and audio processing pipelines directly, without 54 | 55 | References 56 | 57 | * [Supercollider Tutorial - Node Messaging or Direct Server Commands](https://www.youtube.com/watch?v=ZZ1Lwq9hGg4). 58 | `s.queryAllNodes` can be used to show current node-tree on server. 59 | * [SuperCollider Server Command Reference](http://doc.sccode.org/Reference/Server-Command-Reference.html), describes 60 | all the messages and their OSC format. 61 | * [supercolliderjs](https://www.npmjs.com/package/supercolliderjs) JavaScript library for talking to scsynth server, 62 | and executing functions in the sclang interpreter. 63 | 64 | 65 | ## SuperCollider and MsgFlo 66 | 67 | To communicate with other systems, sndflo currently use the FBP runtime protocol and the 'remote subgraph' feature in NoFlo. 68 | This imposes a strict hierachicy where NoFlo (in browser or node.js) must be on the upper level. 69 | Especially problematic when one wants to have multiple consumers of the same data, like a virtual device display in a browser. 70 | 71 | Since then MsgFlo has been created, which is specifically for connecting together different systems, using standard message brokers. 72 | 73 | scsynth -> MsgFlo bridge (using MQTT). Creates a Participant, and exposes inports for certain node/synth properties. 74 | Probably needs ability load synthdefs and/or instantiate a set of synths. Maybe by executing a `.scd` file. 75 | Should connect to a running scsynth server. 76 | Primary usage probably as a commandline tool, with arguments/options configuring the particular use. 77 | Should be possible to have multiple using the same scsynth server. 78 | 79 | Could also allow a 'bundled' input, where one can send a JSON object with key/value pairs to set a bunch of properties at once. 80 | 81 | Typical uses would be to: 82 | 83 | * create interactive installations. Ex: trigger/influence sound from sensors 84 | * adjust processing parameters based on user input 85 | * create custom musical instruments 86 | 87 | Streaming to browser 88 | --------------------- 89 | For an integrated solution in Flowhub, we need to be able to send the live sound stream 90 | over to the browser with low latency. 91 | We could send audio frames over WebSocket and stuff it directly into a WebAudio element. 92 | This would enable processing the stream also on the clientside with webaudio. 93 | 94 | As SuperCollider does not have WS support, perhaps a WebSocket bridge for JACK (as a client) 95 | would be a way to go. This would also enable other applications than SuperCollider to us it. 96 | Could be implemented using glib,libsoup and libjack? 97 | Or could have a C++ module for node.js, and use its WebSocket support.. 98 | 99 | It is also useful in embedded interactive installations to be able to output both to 100 | From these perspectives, perhaps this runtime is more "audioflo" than specifically "scflo", 101 | and also include launching and wiring the various JACK clients needed? 102 | 103 | 104 | 105 | JACK and FBP 106 | -------------- 107 | [JACK](http://jackaudio.org/) is a sound-server which can connect audio and MIDI between 108 | different processes. It is used by default with SuperCollider on Linux. 109 | Ideally one would be able to wire together JACK clients (like SC) from Flowhub 110 | 111 | JACK bindings 112 | * https://github.com/metachronica/node-jack-connector 113 | * http://sourceforge.net/projects/py-jack/ 114 | 115 | Streaming 116 | * http://sourceforge.net/projects/jackrtp/ 117 | * http://gstreamer.freedesktop.org/documentation/rtp.html 118 | * http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good-plugins/html/gst-plugins-good-plugins-jackaudiosink.html 119 | 120 | Random 121 | --------- 122 | 123 | * https://trac.assembla.com/pkaudio/wiki/SuperCollider 124 | * https://pypi.python.org/pypi/SC/0.2 125 | 126 | 127 | Related 128 | ======== 129 | 130 | * https://github.com/mohayonao/CoffeeCollider 131 | * http://overtone.github.io 132 | 133 | -------------------------------------------------------------------------------- /graphs/sawsynth.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "name": "sawsynth", 4 | "environment": { 5 | "type": "sndflo" 6 | } 7 | }, 8 | "inports": { 9 | "freq": { 10 | "process": "f", 11 | "port": "freq", 12 | "metadata": { 13 | "x": 0, 14 | "y": -72, 15 | "width": 72, 16 | "height": 72 17 | } 18 | }, 19 | "freq1": { 20 | "process": "gen", 21 | "port": "freq", 22 | "metadata": { 23 | "x": -144, 24 | "y": -72, 25 | "width": 72, 26 | "height": 72 27 | } 28 | } 29 | }, 30 | "outports": {}, 31 | "groups": [], 32 | "processes": { 33 | "gen": { 34 | "component": "synth/SawWave", 35 | "metadata": { 36 | "x": -36, 37 | "y": 36, 38 | "width": 72, 39 | "height": 72, 40 | "label": "gen" 41 | } 42 | }, 43 | "f": { 44 | "component": "synth/LowPassFilter", 45 | "metadata": { 46 | "x": 108, 47 | "y": 36, 48 | "width": 72, 49 | "height": 72, 50 | "label": "f" 51 | } 52 | }, 53 | "out": { 54 | "component": "synth/AudioOut", 55 | "metadata": { 56 | "x": 252, 57 | "y": 36, 58 | "width": 72, 59 | "height": 72, 60 | "label": "out" 61 | } 62 | } 63 | }, 64 | "connections": [ 65 | { 66 | "src": { 67 | "process": "gen", 68 | "port": "out" 69 | }, 70 | "tgt": { 71 | "process": "f", 72 | "port": "in" 73 | } 74 | }, 75 | { 76 | "src": { 77 | "process": "f", 78 | "port": "out" 79 | }, 80 | "tgt": { 81 | "process": "out", 82 | "port": "in" 83 | } 84 | }, 85 | { 86 | "data": "440", 87 | "tgt": { 88 | "process": "gen", 89 | "port": "freq" 90 | } 91 | }, 92 | { 93 | "data": "2000", 94 | "tgt": { 95 | "process": "f", 96 | "port": "freq" 97 | } 98 | } 99 | ] 100 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sndflo", 3 | "version": "0.0.1", 4 | "description": "Flow-based programming runtime for SuperCollider", 5 | "keywords": [ 6 | "fbp", 7 | "noflo", 8 | "dataflow", 9 | "supercollider", 10 | "audio synthesis", 11 | "signal processing" 12 | ], 13 | "author": "Jon Nordby (http://www.jonnor.com)", 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/jonnor/sndflo.git" 17 | }, 18 | "bugs": "http://github.com/jonnor/sndflo/issues", 19 | "license": "MIT", 20 | "scripts": { 21 | "test": "mocha --reporter spec --compilers .coffee:coffee-script/register ./spec/*.coffee", 22 | "start": "node sndflo.js" 23 | }, 24 | "dependencies": { 25 | "coffee-script": "^1.7.1", 26 | "commander": "^2.1.0", 27 | "fbp": "~1.0.2", 28 | "flowhub-registry": "0.0.2", 29 | "node-uuid": "^1.4.1", 30 | "noflo": "~0.4.0", 31 | "osc-min": "~0.0.12", 32 | "pkginfo": "~0.3.0", 33 | "websocket": "^1.0.8" 34 | }, 35 | "devDependencies": { 36 | "mocha": "~1.13.0", 37 | "chai": "^1.9.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sndflo-runtime.scd: -------------------------------------------------------------------------------- 1 | var f = { 2 | "sndflo-runtime starting".postln; 3 | r = SndFloRuntime.new(s, NetAddr.localAddr); 4 | 5 | Task.new({ 6 | s.sync; // make sure library is loaded 7 | if (thisProcess.argv.size == 1, { 8 | // Load first arg as default graph 9 | r.loadDefaultGraphFile(thisProcess.argv[0]); 10 | }); 11 | "sndflo-runtime running on port %\n".postf(NetAddr.localAddr.port); 12 | }).play; 13 | }; 14 | 15 | Server.killAll(); 16 | Task.new({ 17 | 1.wait; 18 | //s.waitForBoot(f); 19 | s.boot; 20 | s.doWhenBooted(f); 21 | }).play; 22 | -------------------------------------------------------------------------------- /sndflo.coffee: -------------------------------------------------------------------------------- 1 | # sndflo - Flowhub.io sound processing runtime based on SuperCollider 2 | # (c) 2014 Jon Nordby 3 | # sndflo may be freely distributed under the MIT license 4 | 5 | # Adapter code between NoFlo WS/.JSON/.FBP protocol, 6 | # and the OSC protocol spoken by the SuperCollider runtime 7 | 8 | osc = require 'osc-min' 9 | udp = require 'dgram' 10 | http = require 'http' 11 | websocket = require 'websocket' 12 | EventEmitter = (require 'events').EventEmitter 13 | fs = require 'fs' 14 | child_process = require 'child_process' 15 | flowhub = require 'flowhub-registry' 16 | uuid = require 'node-uuid' 17 | querystring = require 'querystring' 18 | 19 | class WebSocketOscFbpAdapter extends EventEmitter 20 | 21 | constructor: () -> 22 | @httpServer = http.createServer() 23 | @wsServer = new websocket.server { httpServer: @httpServer } 24 | 25 | @wsServer.on 'request', (request) => 26 | subProtocol = if (request.requestedProtocols.indexOf("noflo") isnt -1) then "noflo" else null 27 | connection = request.accept subProtocol, request.origin 28 | connection.on 'message', (msg) => 29 | @handleWsMessage connection, msg 30 | 31 | @oscSockets = 32 | send: udp.createSocket "udp4" 33 | receive: udp.createSocket "udp4", @handleUdpMessage 34 | 35 | @wsConnection = null # FIXME: handle multiple 36 | 37 | start: (wsPort, oscPort, callback) -> 38 | @sendPort = oscPort 39 | @receivePort = @sendPort+2 40 | @wsPort = wsPort 41 | 42 | @oscSockets.receive.bind @receivePort 43 | @httpServer.listen @wsPort, callback 44 | 45 | stop: () -> 46 | # FIXME: reverse effects of start() 47 | 48 | handleUdpMessage: (msg, rinfo) => 49 | 50 | try 51 | data = osc.fromBuffer msg 52 | @handleOscMessage data 53 | catch err 54 | console.log "invalid OSC packet", err 55 | 56 | handleOscMessage: (data) -> 57 | respond = (m) => 58 | if not @wsConnection? 59 | throw new Error 'No WebSocket connection!' 60 | @wsConnection.send JSON.stringify m 61 | 62 | if data.address == '/fbp/ui/message' 63 | if data.args.length == 1 and data.args[0].type == 'string' 64 | # Note: We could just have sent the JSON encoded string on, but 65 | # right now decode and re-encode it to be able to detect errors earlier 66 | msg = null 67 | try 68 | msg = JSON.parse data.args[0].value 69 | catch err 70 | console.log 'Invalid JSON received on OSC:', data.args[0].value 71 | 72 | respond msg 73 | else 74 | console.log 'Unexpected OSC arguments: ', data.args 75 | 76 | else 77 | console.log 'Unexpected OSC address: ', data.address 78 | 79 | handleWsMessage: (connection, message) -> 80 | @wsConnection = connection 81 | 82 | if message.type == "utf8" 83 | msg = JSON.parse message.utf8Data 84 | path = "/fbp/runtime/message" 85 | args = [ JSON.stringify msg ] 86 | buf = osc.toBuffer { address: path, args: args } 87 | success = @oscSockets.send.send buf, 0, buf.length, @sendPort, "localhost" 88 | else 89 | console.log "Invalid WS message type", message.type 90 | 91 | class SuperColliderProcess 92 | constructor: (debug, verbose, graph) -> 93 | @process = null 94 | @started = false 95 | @debug = debug 96 | @errors = [] 97 | @verbose = verbose 98 | @graph = graph 99 | 100 | start: (desiredPort, callback) -> 101 | if @debug 102 | console.log 'Debug mode: setup runtime yourself!' 103 | return success 0 104 | 105 | exec = 'sclang' 106 | args = ['-u', desiredPort.toString(), 'sndflo-runtime.scd'] 107 | args.push @graph if @graph 108 | 109 | console.log exec, args.join ' ' if @verbose 110 | @process = child_process.spawn exec, args 111 | @process.on 'error', (err) -> 112 | throw err 113 | @process.on 'exit', (code, signal) -> 114 | if code != 0 115 | callback new Error 'Runtime exited with non-zero code: ' + code + ' :' +signal 116 | 117 | stderr = "" 118 | @process.stderr.on 'data', (d) => 119 | console.log d.toString() if @verbose 120 | output = d.toString() 121 | stderr += output 122 | lines = output.split '\n' 123 | for line in lines 124 | err = line.trim() 125 | @errors.push err if err 126 | 127 | stdout = "" 128 | @process.stdout.on 'data', (d) => 129 | console.log d.toString() if @verbose 130 | stdout += d.toString() 131 | failString = 'ERROR: server failed to start' 132 | readyExp = /sndflo-runtime running on port (\d+)/i 133 | readyMatch = stdout.match readyExp 134 | if readyMatch 135 | if not @started 136 | errors = @popErrors() 137 | port = readyMatch[1] 138 | @started = true 139 | callback null, port, process.pid 140 | if stdout.indexOf(failString) != -1 or stderr.indexOf(failString) != -1 141 | callback new Error 'Failed to start up', null, null 142 | 143 | stop: -> 144 | if @debug 145 | return 146 | @process.kill() 147 | 148 | popErrors: -> 149 | errors = @errors 150 | @errors = [] 151 | return errors 152 | 153 | # Handles both the SuperCollider setup and the OSC<->WebSocket bridging 154 | class Runtime extends EventEmitter 155 | constructor: (options) -> 156 | defaults = 157 | port: 3569 158 | oscPort: 57120 159 | verbose: false 160 | debug: false 161 | graph: null 162 | label: "unlabeled sndflo runtime" 163 | user: null 164 | id: null 165 | host: 'localhost' 166 | ping: 5*60 # seconds 167 | secret: 'not-secret' # FIXME: random 168 | @options = {} 169 | for k,v of defaults 170 | @options[k] = v 171 | for k,v of options 172 | @options[k] = v 173 | 174 | @adapter = new WebSocketOscFbpAdapter() 175 | @supercollider = new SuperColliderProcess @options.debug, @options.verbose, @options.graph 176 | 177 | @rt = null 178 | if @options.user 179 | @rt = new flowhub.Runtime 180 | label: @options.label 181 | id: @options.id 182 | user: @options.user 183 | secret: @options.secret 184 | protocol: 'websocket' 185 | type: 'sndflo' 186 | address: 'ws://' + @options.host + ':' + @options.port 187 | @registryPinger = null 188 | 189 | register: (callback) -> 190 | @rt.register (err, ok) => 191 | return callback err if err 192 | @rt.ping() 193 | if @options.ping > 0 194 | @registryPinger = setInterval () => 195 | @rt.ping() 196 | , @options.ping*1000 197 | return callback null 198 | 199 | start: (callback) -> 200 | @supercollider.start @options.oscPort, (err, port, pid) => 201 | return callback err, null if err 202 | internal = parseInt(port) 203 | console.log 'internal port', internal if @options.verbose 204 | @adapter.start @options.port, internal, (err) => 205 | return callback err, null if err 206 | if @rt 207 | @register (err) -> 208 | return console.log 'Failed to register Flowhub runtime: ' + err if err 209 | console.log 'Registered with Flowhub, should be accessible in UI' 210 | return callback null, internal 211 | 212 | stop: (callback) -> 213 | if @registryPinger 214 | clearInterval @registryPinger 215 | @registryPinger = null 216 | @supercollider.stop() 217 | @adapter.stop() 218 | 219 | liveModeUrl = (options) -> 220 | ide = options.ide or "http://app.flowhub.io" 221 | address = "ws://#{options.host}:#{options.port}" 222 | params = querystring.escape "protocol=websocket&address="+address 223 | return ide+'/#runtime/endpoint?'+params 224 | 225 | main = () -> 226 | program = require 'commander' 227 | program 228 | .option '-p, --port ', 'WebSocket port' 229 | .option '-i, --host ', 'WebSocket hostname' 230 | .option '-u, --user ', 'Flowhub user id to register for' 231 | .option '-r, --id ', 'Flowhub runtime id to use' 232 | .option '-l, --label