Bowelyzer{ var verbose; *new{ arg configFile, verbose = false; ^super.new.init(configFile, verbose); } init{ //initialize with configuration, if available (else use default) arg configFile, verbose; this.verbose = verbose; config = BowelyzerConfig.new(configFile); this.addServer; server.waitForBoot({ this.setupNetAddressesFromConfig(config.config); this.setupSynthListenersFromConfig(config.config); analyzer = BowelyzerAnalyzer.new(config.config); gui = BowelyzerGUI.new(config); this.addGUIListeners; this.addServerListeners; }, 5, {"scsynth failed to start!".postln}); } // add a scsynth server and make it the default addServer{ var addr = NetAddr.new(config.config.at(\synthServerAddress), config.config.at(\synthServerPort)); // if the NetAddr is local, boot locally, otherwise remote if(addr.isLocal,{ server = Server.new( name: \bowelyzer, addr: addr, options: Server.default.options ); },{ server = Server.remote( name: \bowelyzer, addr: addr, options: Server.default.options ); }); Server.default = server; } // add OSCdefs listening for GUI events addGUIListeners{ // listen for control changes OSCdef.newMatching( key: \controls, func: {|msg, time, addr, recvPort| var type = msg[0], name = msg[1], control = msg[2], value = msg[3], value2; postln("Received: "++msg); // if the control exists, change it if(name.notNil && control.notNil && value.notNil,{ if(config.config.at(\controls).includesKey(name),{ if(config.config.at(\controls).at(name).includesKey(control),{ if(BowelyzerConfig.hasToBeArray.includes(control), { if(msg[4].notNil, { value2 = msg[4]; config.config.at(\controls).at(name).put(control, config.getControlValue(control, [value, value2])); }); },{ config.config.at(\controls).at(name).put(control, config.getControlValue(control, value)); }); }); }); }); }, path: "/controls", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); // listen for input changes (rename and input channel) OSCdef.newMatching( key: \inputs, func: {|msg, time, addr, recvPort| var name = msg[1], type = msg[2], update = msg[3]; postln("Received: "++msg); if(name.notNil && type.notNil && update.notNil,{ if(config.config.at(\inputs).includesKey(name) && config.config.at(\controls).includesKey(name),{ switch(type, \name,{ // if the name is not empty and not used, change to it if((update.asString != "") && config.config.at(\inputs).includesKey(update).not, { //move the controls config.config.at(\controls).put(update.asSymbol, config.config.at(\controls).at(name)); config.config.at(\controls).removeAt(name.asSymbol); // rename the input config.config.at(\inputs).put(update.asSymbol, config.config.at(\inputs).at(name)); config.config.at(\inputs).removeAt(name); // rename the channelView gui.setChannelName(name, update); // free the old OSC indicator fadeout Task gui.freeOSCIndicatorFadeOutTask(name); // add a new OSC indicator fadeout Task using the new name gui.addOSCIndicatorFadeOutTask(update); // stop the listener for LevelIndicator by name and start a new one based upon the updated name OSCdef(("levels_"++name).asSymbol).free; this.setupLevelListener(update, synthServerAddr); //TODO: refactor into function // free the synth on the server and start a new one according to the config analyzer.freeAnalysisSynth(name.asSymbol); analyzer.synths.removeAt(name); //TODO: refactor into function analyzer.addSynthWithName(update); // start the Synth (possibly paused) Routine{ 1.wait; analyzer.startSynthWithNameAndControls(update.asSymbol, config.config.at(\controls).at(update), config.config.at(\inputs).at(update)); }.play; //TODO: refactor into function // rename the hub listener for the Synth this.freeSynthListener(name); this.addSynthListener(update); this.startSynthListener(update); },{ ("New name not valid: "++update).error; gui.setChannelText(name, name); }); }, \input,{ // change the input in configuration config.config.at(\inputs).put(name, update.asInteger); // change the input in the Synth analyzer.setSynthControl(name.asSymbol, \inputChannel, update.asInteger); } ); }); }); }, path: "/inputs", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); // listen for toggling messages to "mute" channel OSCdef.newMatching( key: \toggle, func: {|msg, time, addr, recvPort| var name = msg[1], toggle = msg[2]; postln("Received: "++msg); if(name.notNil && toggle.notNil && toggle.isInteger,{ if(config.config.at(\inputs).includesKey(name),{ switch(toggle, 0,{ config.config.at(\controls).at(name).put(\active, true); analyzer.startAnalysisSynth(name); }, 1,{ config.config.at(\controls).at(name).put(\active, false); analyzer.stopAnalysisSynth(name); } ); }); }); }, path: "/toggle", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); // listen for messages to toggle all channels on/off simultaneously OSCdef.newMatching( key: \toggle_all, func: {|msg, time, addr, recvPort| var type = msg[0], toggle = msg[1]; postln("Received: "++msg); switch(toggle, 0,{ config.config.at(\controls).pairsDo({|name, controls| controls.put(\active, true); analyzer.startAnalysisSynth(name); gui.setCurrentToggleState(name, true); }); }, 1,{ config.config.at(\controls).pairsDo({|name, controls| controls.put(\active, false); analyzer.stopAnalysisSynth(name); gui.setCurrentToggleState(name, false); }); } ); }, path: "/toggle_all", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); // listen for address messages to change OSC addresses OSCdef.newMatching( key: \address, func: {|msg, time, addr, recvPort| var type = msg[1], address = msg[2]; postln("Received: "++msg); if(type.notNil && config.config.includesKey(type),{ config.config.put(type.asSymbol, address.asString); this.setupNetAddressesFromConfig(config.config); }); }, path: "/address", srcID: localAddr ); // listen for port messages to change OSC ports OSCdef.newMatching( key: \port, func: {|msg, time, addr, recvPort| var type = msg[1], port = msg[2]; postln("Received: "++msg); if(type.notNil && config.config.includesKey(type),{ config.config.put(type.asSymbol, port.asInteger); this.setupNetAddressesFromConfig(config.config); }); }, path: "/port", srcID: localAddr ); // listen for message to save configuration to current file OSCdef.newMatching( key: \save, func: {|msg, time, addr, recvPort| var path = msg[1]; postln("Received: "++msg); if(config.configFilePath.isNil || config.configFilePath == "",{ gui.saveAsAction(nil); },{ config.writeConfigurationFile; }); }, path: "/save", srcID: localAddr ); // listen for message to save configuration to current file OSCdef.newMatching( key: \saveas, func: {|msg, time, addr, recvPort| var name = msg[0], fileName = msg[1], dict; postln("Received: "++msg); if(config.writeConfigurationFile(fileName), { if((gui.configFilePathView.items.size == 0), { gui.setCurrentConfigFilePath(fileName); },{ dict = Dictionary.with(*gui.configFilePathView.items); if(dict.includesKey(fileName.asSymbol).not,{ gui.setCurrentConfigFilePath(fileName); }); }); }); }, path: "/saveas", srcID: localAddr ); // listen for messages to load configuration from file OSCdef.newMatching( key: \load, func: {|msg, time, addr, recvPort| var name = msg[0], fileName = msg[1]; postln("Received: "++msg); // free all channels config.config.at(\inputs).order.do({ |name| this.freeChannel(name.asSymbol); }); // read the configuration file if(config.readConfigurationFile(fileName),{ this.setupNetAddressesFromConfig(config.config); this.setupSynthListenersFromConfig(config.config); analyzer.setupSynthsFromConfig(config.config); config.config.at(\inputs).keysDo({ |name| gui.setupChannelView(name, config.config); // setup a LevelListener for the new Synth this.setupLevelListener(name, synthServerAddr); // setup a OSC listener for the Synth by name this.addSynthListener(name); }); // reload the addresses and ports Views gui.removeSettingsView("synthServer"); gui.removeSettingsView("hub"); gui.removeSettingsView("forward"); gui.setupSynthServerView(config.config); gui.setupHubView(config.config); gui.setupForwardView(config.config); gui.setCurrentConfigFilePath(config.configFilePath); gui.setCurrentGlobalToggleState(true); },{ ("Configuration could not be read: "++fileName).error; }); }, path: "/load", srcID: localAddr ); // listen for messages to free (close) channels OSCdef.newMatching( key: \free, func: {|msg, time, addr, recvPort| var type = msg[0], name = msg[1]; postln("Received: "++msg); this.freeChannel(name); }, path: "/free", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); // listen for messages to add channels OSCdef.newMatching( key: \add, func: {|msg, time, addr, recvPort| var type = msg[0], name; postln("Received: "++msg); this.addChannel; }, path: "/add", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); } // add OSCdefs listening for messages coming from scsynth, to update the GUI addServerListeners{ this.setupIndicatorListener; // add and start a listener for each channel's LevelIndicator config.config.at(\inputs).pairsDo({|key,value| this.setupLevelListener(key, synthServerAddr); }); } // setup a listener for a level indicator by name and source (of synth server) setupIndicatorListener{ // listen for indicator messages OSCdef.newMatching( key: \indicate, func: {|msg, time, addr, recvPort| var name = msg[1]; if(config.config.at(\inputs).includesKey(name),{ gui.pingOSCIndicator(name); }); }, path: "/indicate", srcID: NetAddr.new("127.0.0.1", NetAddr.langPort) ); } // setup a listener for a level indicator by name and source (of synth server) setupLevelListener{ arg name, server; postln("Setting up LevelListener for: "++name); OSCdef.newMatching( key: ("levels_"++name).asSymbol, func: {|msg, time, addr, recvPort| var name = msg[0].asString.replace("/levels/", "").asSymbol, value = msg[3].ampdb.linlin(-40, 0, 0, 1), peak = msg[4].ampdb.linlin(-40, 0, 0, 1); gui.setLevel(name, value, peak); }, path: "/levels/"++name, srcID: server ); } // add a new channel by name addChannel{ arg extName; // add a new input to the config var name; if(extName.isNil,{ name = config.addInput; },{ name = config.addInput(extName); }); // initialize a new channelView with the name and controls gui.setupChannelView(name, config.config); // add and start a new Synth by the given name Routine{ analyzer.addSynthWithName(name); 1.wait; analyzer.startSynthWithNameAndControls(name, config.config.at(\controls).at(name), config.config.at(\inputs).at(name)); }.play; // setup a LevelListener for the new Synth this.setupLevelListener(name, synthServerAddr); // setup a OSC listener for the Synth by name this.addSynthListener(name); } // free (remove) a channel freeChannel{ arg name; ("Removing channel: "++name).postln; // free Synth analyzer.freeAnalysisSynth(name); // remove OSCdefs for the channel OSCdef.all.pairsDo({|key, value| if(key == name.asSymbol,{ ("Freeing OSCdef: "++name).postln; value.free; }); if(key == ("levels_"++name).asSymbol,{ ("Freeing OSCdef: levels_"++name).postln; value.free; }); }); // remove configurations for the channel config.config.at(\inputs).removeAt(name); config.config.at(\controls).removeAt(name); // remove fadeout task for OSC indicator gui.freeOSCIndicatorFadeOutTask(name); // remove GUI elements gui.removeChannelView(name, config.config.at(\inputs).size); } // setup the NetAddresses from configuration setupNetAddressesFromConfig{ arg config; forwardAddr = NetAddr.new(config.at(\forwardAddress), config.at(\forwardPort)); hubAddr = NetAddr.new(config.at(\hubAddress), config.at(\hubPort)); synthServerAddr = NetAddr.new(config.at(\synthServerAddress), config.at(\synthServerPort)); localAddr = NetAddr.new("127.0.0.1", NetAddr.langPort); } // setup OSCdef for SynthServerAddress:SynthServerPort setupSynthListenersFromConfig{ arg config; // listen for individual SendReply messages config.at(\inputs).keysValuesDo({|name, input| postln("Listening for messages called '/"++name++"' coming from scsynth at "++synthServerAddr.ip++":"++synthServerAddr.port++"."); this.addSynthListener(name); }); } // add a listener for a specific Synth (by name) addSynthListener{ arg name; OSCdef.newMatching( key: name.asSymbol, func: {|msg, time, addr, recvPort| var name = msg[0], sanitizedName = name.asString.replace("/","").asSymbol, amplitude = msg[3], pitch = msg[4], hasPitch = msg[5], onsetDetect = msg[6], range = config.config.at(\controls).at(sanitizedName).at(\freqRange), onlyForwardOnNewPitch = config.config.at(\controls).at(sanitizedName).at(\onlyForwardOnNewPitch); // if there is something connected if(amplitude != 0,{ // if the pitch is within freqRange if((pitch >= range[0]) && (pitch <= range[1]),{ if(onlyForwardOnNewPitch.not || (onlyForwardOnNewPitch && (hasPitch > 0)), { localAddr.sendMsg("/indicate", sanitizedName); if(this.verbose,{this.postLocally(name, amplitude, pitch, hasPitch, onsetDetect)}); try{ this.forwardToNetAddress(name, amplitude, pitch, hasPitch, onsetDetect); }{ |error| switch(error.species.name) { 'PrimitiveFailedError' }{ "".postln; "Your network is down!".postln; gui.pingForwardIndicator }{ "Unknown exception".postln; error.throw; } }; }); }); }); }, path: "/"++name.asString, srcID: synthServerAddr, recvPort: hubAddr.port ); } startSynthListener{ arg name; OSCdef(name).enable; } stopSynthListener{ arg name; OSCdef(name.asSymbol).disable; } freeSynthListener{ arg name; OSCdef(name.asSymbol).free; } //post a received OSC message locally postLocally{ arg name, amplitude, pitch, hasPitch, onsetDetect; postln("[ "++name++", "++amplitude++", "++pitch++", "++hasPitch++", "++onsetDetect++" ]"); } //forward a received OSC message to the globally specified forward address forwardToNetAddress{ arg name, amplitude, pitch, hasPitch, onsetDetect; forwardAddr.sendMsg(name, amplitude, pitch, hasPitch, onsetDetect); } }