aboutsummaryrefslogtreecommitdiffstats
path: root/classes
diff options
context:
space:
mode:
Diffstat (limited to 'classes')
-rw-r--r--classes/Bowelyzer.sc267
-rw-r--r--classes/BowelyzerAnalyzer.sc209
-rw-r--r--classes/BowelyzerConfig.sc362
-rw-r--r--classes/BowelyzerGUI.sc520
-rw-r--r--classes/BowelyzerOSCHub.sc88
5 files changed, 1446 insertions, 0 deletions
diff --git a/classes/Bowelyzer.sc b/classes/Bowelyzer.sc
new file mode 100644
index 0000000..cafaf00
--- /dev/null
+++ b/classes/Bowelyzer.sc
@@ -0,0 +1,267 @@
+Bowelyzer{
+
+ var <gui, <server, <analyzer, <config, <hub;
+
+ *new{
+ arg configFile;
+ ^super.new.init(configFile);
+ }
+
+ init{
+ //initialize with configuration, if available (else use default)
+ arg configFile;
+ config = BowelyzerConfig.new(configFile);
+ server = Server.new(
+ \bowelyzer,
+ BowelyzerOSCHub.getNetAddr(
+ config.config.at(\synthServerAddress),
+ config.config.at(\synthServerPort)
+ )
+ );
+ Server.default = server;
+ server.waitForBoot({
+ hub = BowelyzerOSCHub.new(config.config);
+ analyzer = BowelyzerAnalyzer.new(config.config);
+ gui = BowelyzerGUI.new(config.config);
+ this.addGUIListeners;
+ this.addServerListeners;
+ },
+ 5,
+ {"scsynth failed to start!".postln});
+ }
+
+ addGUIListeners{
+ // listen for control changes
+ OSCdef.newMatching(
+ key: \controls,
+ func: {|msg, time, addr, recvPort|
+ postln("Received: "++msg);
+ // if the control exists, change it
+ if(msg[1].notNil && msg[2].notNil && msg[3].notNil,{
+ if(config.config.at(\controls).includesKey(msg[1]),{
+ if(config.config.at(\controls).at(msg[1]).includesKey(msg[2]),{
+ config.config.at(\controls).at(msg[1]).put(msg[2], config.getControlValue(msg[2], msg[3]));
+ });
+ });
+ });
+ },
+ 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 exists and the new name is not empty, change it
+ //FIXME: check whether new name exists and only setup then, else reset the textfield
+ if(update != "",{
+ //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);
+ Routine{
+ // rename the channel View
+ gui.channels.do({|channel|
+ if(channel.name.asSymbol == name.asSymbol, {
+ channel.name = update.asSymbol;
+ });
+ });
+ // OSC indicator: stop old task, remove it and create a new one with updated name
+ gui.indicators.at(name).stop;
+ gui.indicators.removeAt(name);
+ gui.addOSCIndicatorFadeOutTask(update);
+ }.play(AppClock);
+ // stop the listener for LevelIndicator by name and start a new one based upon the updated name
+ OSCdef(\levels_++name).free;
+ this.setupLevelListener(update, hub.synthServer);
+ // free the synth on the server and start a new one according to the config
+ analyzer.freeAnalysisSynth(name.asSymbol);
+ analyzer.synths.removeAt(name);
+ 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;
+ // rename the hub listener for the Synth
+ hub.freeSynthListener(name);
+ hub.addSynthListener(update);
+ hub.startSynthListener(update);
+ });
+ },
+ \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 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);
+ hub.setupNetAddressesFromConfig(config.config);
+ });
+ },
+ path: "/address",
+ srcID: hub.local
+ );
+
+ // 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);
+ hub.setupNetAddressesFromConfig(config.config);
+ });
+ },
+ path: "/port",
+ srcID: hub.local
+ );
+
+ // listen for port messages to change OSC ports
+ OSCdef.newMatching(
+ key: \save,
+ func: {|msg, time, addr, recvPort|
+ var path = msg[1];
+ postln("Received: "++msg);
+ //TODO: implement save (write to file) in BowelyzerConfig
+ },
+ path: "/save",
+ srcID: hub.local
+ );
+
+ // listen for port messages to change OSC ports
+ OSCdef.newMatching(
+ key: \load,
+ func: {|msg, time, addr, recvPort|
+ var path = msg[1];
+ postln("Received: "++msg);
+ //TODO: use BowelyzerConfig readFromConfigurationFile
+ //TODO: destroy current GUI elements
+ //TODO: load channels, etc.
+ },
+ path: "/load",
+ srcID: hub.local
+ );
+ }
+
+ // 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, hub.synthServer);
+ });
+ }
+
+ // 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];
+ //postln("Indicate for "++name);
+ if(config.config.at(\inputs).includesKey(name),{
+ if(gui.indicators.includesKey(name),{
+ gui.indicators.at(name).stop;
+ gui.indicators.at(name).reset;
+ gui.indicators.at(name).start(AppClock);
+ });
+ });
+ },
+ 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,
+ 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);
+ //postln("Receiving: "++msg);
+ {
+ gui.channels.do({|channel|
+ if(channel.name.asSymbol == name,{
+ channel.children.do({|channelChild|
+ if(channelChild.name.asSymbol == \meterAndControls,{
+ channelChild.children.do({|meterAndControls|
+ if(meterAndControls.name.asSymbol == \meterView, {
+ meterAndControls.children.do({|meterView|
+ if(meterView.isKindOf(LevelIndicator), {
+ //postln("Setting up LevelIndicator for "++name);
+ meterView.value = value;
+ meterView.peakLevel = peak;
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }.defer;
+ },
+ path: "/levels/"++name,
+ srcID: server
+ );
+ }
+}
diff --git a/classes/BowelyzerAnalyzer.sc b/classes/BowelyzerAnalyzer.sc
new file mode 100644
index 0000000..39728bd
--- /dev/null
+++ b/classes/BowelyzerAnalyzer.sc
@@ -0,0 +1,209 @@
+BowelyzerAnalyzer{
+
+ var <synths;
+ var <analyzerGroup;
+ var <monitoringGroup;
+
+ *new{
+ arg config;
+ ^super.new.init(config);
+ }
+
+ init{
+ arg config;
+ synths = Dictionary.new(config.at(\inputs).size);
+ this.setupGroups;
+ this.addSynthsFromConfig(config);
+ Routine{
+ 1.wait;
+ this.startSynthsFromConfig(config);
+ }.play;
+ }
+
+ // setup a synth group for the analyzers
+ setupGroups{
+ analyzerGroup = Group.new;
+ monitoringGroup = Group.after(analyzerGroup);
+ NodeWatcher.register(analyzerGroup);
+ NodeWatcher.register(monitoringGroup);
+ }
+
+ // add synths for the defined channels
+ addSynthsFromConfig{
+ arg config;
+ config.at(\inputs).keysValuesDo({|name, inputChannel|
+ ("Adding SynthDef \""++name++"\" on input "++inputChannel).postln;
+ this.addSynthWithName(name);
+ });
+ }
+
+ addSynthWithName{
+ arg name;
+ SynthDef(name, {
+ arg inputChannel,
+ sendReplyFreq,
+ amplitudeAttackTime,
+ amplitudeReleaseTime,
+ hfHainsworth,
+ hfFoote,
+ hfThreshold,
+ hfWaitTime,
+ pitchInitFreq,
+ pitchMinFreq,
+ pitchMaxFreq,
+ pitchExecFreq,
+ pitchMaxBinsPerOctave,
+ pitchMedian,
+ pitchAmpThreshold,
+ pitchPeakThreshold,
+ pitchDownSample
+ ;
+ var amplitude, input, detect, pitch, hasPitch;
+ //TODO: Add volume for SoundIn
+ input = SoundIn.ar(bus: inputChannel);
+ amplitude = Amplitude.kr(
+ in: input,
+ attackTime: amplitudeAttackTime,
+ releaseTime: amplitudeReleaseTime
+ );
+ detect = A2K.kr(
+ PV_HainsworthFoote.ar(
+ FFT(LocalBuf(2048), input),
+ proph: hfHainsworth,
+ propf: hfFoote,
+ threshold: hfThreshold,
+ waittime: hfWaitTime
+ )
+ );
+ # pitch, hasPitch = Pitch.kr(
+ in: input,
+ initFreq: pitchInitFreq,
+ minFreq: pitchMinFreq,
+ maxFreq: pitchMaxFreq,
+ execFreq: pitchExecFreq,
+ maxBinsPerOctave: pitchMaxBinsPerOctave,
+ median: pitchMedian,
+ ampThreshold: pitchAmpThreshold,
+ peakThreshold: pitchPeakThreshold,
+ downSample: pitchDownSample
+ );
+ // SendReply for OSCHub
+ SendReply.kr(
+ Impulse.kr(sendReplyFreq),
+ "/"++name,
+ [amplitude, pitch, hasPitch, detect]
+ );
+ // SendReply for LevelIndicator
+ SendReply.kr(
+ Impulse.kr(10),
+ "/levels/"++name,
+ [
+ Amplitude.kr(input),
+ K2A.ar(
+ Peak.ar(
+ input,
+ Delay1.kr(Impulse.kr(10))
+ ).lag(0, 3)
+ )
+ ]
+ );
+ }).add;
+ }
+
+ startSynthsFromConfig{
+ arg config;
+ config.at(\controls).keysValuesDo({|name, controls|
+ postln("Starting synth \""++name++"\".");
+ this.startSynthWithNameAndControls(name, controls, config.at(\inputs).at(name));
+ });
+ }
+
+ // add and start the Synth with name, controls and input channel
+ startSynthWithNameAndControls{
+ arg name, controls, input;
+ // if the input is set to active, start right away, else pause
+ if(controls.at(\active),{
+ synths.add(name -> Synth.new(
+ name,
+ [
+ inputChannel: input,
+ sendReplyFreq: controls.at(\sendReplyFreq),
+ amplitudeAttackTime: controls.at(\amplitudeAttackTime),
+ amplitudeReleaseTime: controls.at(\amplitudeReleaseTime),
+ hfHainsworth: controls.at(\hfhainsworth),
+ hfFoote: controls.at(\hfFoote),
+ hfThreshold: controls.at(\hfThreshold),
+ hfWaitTime: controls.at(\hfWaitTime),
+ pitchInitFreq: controls.at(\pitchInitFreq),
+ pitchMinFreq: controls.at(\pitchMinFreq),
+ pitchMaxFreq: controls.at(\pitchMaxFreq),
+ pitchExecFreq: controls.at(\pitchExecFreq),
+ pitchMaxBinsPerOctave: controls.at(\pitchMaxBinsPerOctave),
+ pitchMedian: controls.at(\pitchMedian),
+ pitchAmpThreshold: controls.at(\pitchAmpThreshold),
+ pitchPeakThreshold: controls.at(\pitchPeakThreshold),
+ pitchDownSample: controls.at(\pitchDownSample)
+ ],
+ analyzerGroup
+ ).register;
+ );
+ },{
+ synths.add(name -> Synth.newPaused(
+ name,
+ [
+ inputChannel: input,
+ sendReplyFreq: controls.at(\sendReplyFreq),
+ amplitudeAttackTime: controls.at(\amplitudeAttackTime),
+ amplitudeReleaseTime: controls.at(\amplitudeReleaseTime),
+ hfHainsworth: controls.at(\hfhainsworth),
+ hfFoote: controls.at(\hfFoote),
+ hfThreshold: controls.at(\hfThreshold),
+ hfWaitTime: controls.at(\hfWaitTime),
+ pitchInitFreq: controls.at(\pitchInitFreq),
+ pitchMinFreq: controls.at(\pitchMinFreq),
+ pitchMaxFreq: controls.at(\pitchMaxFreq),
+ pitchExecFreq: controls.at(\pitchExecFreq),
+ pitchMaxBinsPerOctave: controls.at(\pitchMaxBinsPerOctave),
+ pitchMedian: controls.at(\pitchMedian),
+ pitchAmpThreshold: controls.at(\pitchAmpThreshold),
+ pitchPeakThreshold: controls.at(\pitchPeakThreshold),
+ pitchDownSample: controls.at(\pitchDownSample)
+ ],
+ analyzerGroup
+ ).register;
+ );
+ });
+ }
+
+ // set a synth control by provoding its name, its control name and control value
+ setSynthControl{
+ arg name, controlName, controlValue;
+ synths.at(name).set(controlName, controlValue);
+ }
+
+ // start a synth by name
+ startAnalysisSynth{
+ arg name;
+ synths.at(name).run(true);
+ }
+
+ // stop a synth by name
+ stopAnalysisSynth{
+ arg name;
+ synths.at(name).run(false);
+ }
+
+ // free a synth by name
+ freeAnalysisSynth{
+ arg name;
+ synths.at(name).free;
+ }
+
+ startAllAnalysisSynths{
+ analyzerGroup.run(true);
+ }
+
+ stopAllAnalysisSynths{
+ analyzerGroup.run(false);
+ }
+}
diff --git a/classes/BowelyzerConfig.sc b/classes/BowelyzerConfig.sc
new file mode 100644
index 0000000..287aad4
--- /dev/null
+++ b/classes/BowelyzerConfig.sc
@@ -0,0 +1,362 @@
+BowelyzerConfig{
+
+ const <controlsWithSlider = #[
+ \amplitudeAttackTime,
+ \amplitudeReleaseTime,
+ \hfHainsworth,
+ \hfFoote,
+ \hfThreshold,
+ \hfWaitTime,
+ \pitchInitFreq,
+ \pitchMinFreq,
+ \pitchMaxFreq,
+ \pitchExecFreq,
+ \pitchMaxBinsPerOctave,
+ \pitchMedian,
+ \pitchAmpThreshold,
+ \pitchPeakThreshold,
+ \pitchDownSample,
+ \sendReplyFreq
+ ];
+ const <controlsWithRanger = #[
+ \test
+ ];
+ const <controlsWithKnob = #[
+ \test
+ ];
+
+ const <hasToBeString = #[
+ \forwardAddress,
+ \hubAddress,
+ \synthServerAddress
+ ];
+
+ const <hasToBeInteger = #[
+ \forwardPort,
+ \hubPort,
+ \synthServerPort,
+ \pitchMedian,
+ \pitchDownSample
+ ];
+ const <hasToBeDictionary = #[
+ \inputs,
+ \controls,
+ \left,
+ \right
+ ];
+ const <hasToBeFloat = #[
+ \amplitudeAttackTime,
+ \amplitudeReleaseTime,
+ \hfHainsworth,
+ \hfFoote,
+ \hfThreshold,
+ \hfWaitTime,
+ \pitchInitFreq,
+ \pitchMinFreq,
+ \pitchMaxFreq,
+ \pitchExecFreq,
+ \pitchMaxBinsPerOctave,
+ \pitchAmpThreshold,
+ \pitchPeakThreshold,
+ \sendReplyFreq
+ ];
+
+ const <hasToBeBool = #[
+ \active
+ ];
+
+ var <config,
+ <defaultConfig,
+ <defaultControls,
+ <changed = false;
+
+ *controlContainedIn{
+ arg control;
+ if(BowelyzerConfig.controlsWithSlider.includes(control),{
+ ^\slider;
+ });
+ if(BowelyzerConfig.controlsWithRanger.includes(control),{
+ ^\ranger;
+ });
+ if(BowelyzerConfig.controlsWithKnob.includes(control),{
+ ^\knob;
+ });
+ }
+
+ *new{
+ arg config;
+ ^super.new.init(config);
+ }
+
+ init{
+ arg configFile;
+ this.addControlSpecs;
+ this.createDefaultConfig;
+ config = Dictionary.new;
+ if(configFile.notNil,{
+ if(this.readConfigurationFile(configFile).not,{
+ error("Reading of configuration file failed. Using default.");
+ config = defaultConfig;
+ });
+ },{
+ ("No configuration file provided. Using default.").postln;
+ config = defaultConfig;
+ });
+ this.showConfig;
+ }
+
+ // add ControlSpecs to the global Dictionary
+ addControlSpecs{
+ ControlSpec.specs[\amplitudeAttackTime] = ControlSpec(0.001, 10, \lin, 0.001, 0.01, units: "s");
+ ControlSpec.specs[\amplitudeReleaseTime] = ControlSpec(0.001, 10, \lin, 0.001, 0.01, units: "s");
+ ControlSpec.specs[\hfHainsworth] = ControlSpec(0.0, 1.0, \lin, 0.001, 1.0, units: "");
+ ControlSpec.specs[\hfFoote] = ControlSpec(0.0, 1.0, \lin, 0.001, 0.0, units: "");
+ ControlSpec.specs[\hfThreshold] = ControlSpec(0.0, 1.0, \lin, 0.001, 1.0, units: "");
+ ControlSpec.specs[\hfWaitTime] = ControlSpec(0.0, 1.0, \lin, 0.001, 0.04, units: "s");
+ ControlSpec.specs[\pitchInitFreq] = ControlSpec(20.0, 20000.0, \exp, 0.01, 440.0, units: "Hz");
+ ControlSpec.specs[\pitchMinFreq] = ControlSpec(20.0, 20000.0, \exp, 0.01, 60.0, units: "Hz");
+ ControlSpec.specs[\pitchMaxFreq] = ControlSpec(20.0, 20000.0, \exp, 0.01, 4000.0, units: "Hz");
+ ControlSpec.specs[\pitchExecFreq] = ControlSpec(20.0, 20000.0, \exp, 0.01, 100.0, units: "Hz");
+ ControlSpec.specs[\pitchMaxBinsPerOctave] = ControlSpec(1, 64, \lin, 1, 16, units: "bins");
+ ControlSpec.specs[\pitchMedian] = ControlSpec(1, 64, \lin, 1, 1, units: "");
+ ControlSpec.specs[\pitchAmpThreshold] = ControlSpec(0.01, 1.0, \lin, 0.01, 0.01, units: "dB");
+ ControlSpec.specs[\pitchPeakThreshold] = ControlSpec(0.01, 1.0, \lin, 0.01, 0.5, units: "dB");
+ ControlSpec.specs[\pitchDownSample] = ControlSpec(1, 100, \lin, 1, 1, units: "samples");
+ ControlSpec.specs[\sendReplyFreq] = ControlSpec(1, 100, \exp, 0.1, 20, units: "Hz");
+ ControlSpec.specs[\input] = ControlSpec(0, Server.default.options.numInputBusChannels-1, \lin, 1, 0, units: "channels");
+ ControlSpec.specs[\port] = ControlSpec(1, 65535, \lin, 1, 0, units: "ports");
+ }
+
+ // create the default configuration
+ createDefaultConfig{
+ defaultControls = Dictionary.with(*[
+ \active -> true,
+ \amplitudeAttackTime -> 0.01,
+ \amplitudeReleaseTime -> 0.1,
+ \hfHainsworth -> 1.0,
+ \hfFoote -> 0.0,
+ \hfThreshold -> 0.3,
+ \hfWaitTime -> 0.04,
+ \pitchInitFreq -> 440,
+ \pitchMinFreq -> 60,
+ \pitchMaxFreq -> 4000,
+ \pitchExecFreq -> 100,
+ \pitchMaxBinsPerOctave -> 16,
+ \pitchMedian -> 1,
+ \pitchAmpThreshold -> 0.01,
+ \pitchPeakThreshold -> 0.5,
+ \pitchDownSample -> 1,
+ \sendReplyFreq ->20
+ ]);
+ defaultConfig = Dictionary.with(*[
+ \inputs -> Dictionary.with(*[
+ \left -> 0,
+ \right -> 1
+ ]),
+ //names -> [left, right],
+ \synthServerAddress -> "127.0.0.1",
+ \synthServerPort -> 57110,
+ \hubAddress -> "127.0.0.1",
+ \hubPort -> 57120,
+ \forwardAddress -> "127.0.0.1",
+ \forwardPort -> 57120,
+ \controls -> Dictionary.with(*[
+ \left -> defaultControls,
+ \right -> defaultControls
+ ])
+ ]);
+ }
+
+// read configuration from file
+ readConfigurationFile{
+ arg configFile;
+ var configFromFile, reader, path;
+ path = PathName.new(configFile.asString);
+ if(path.isFile,{
+ postln("Reading configuration file: "++configFile);
+ try{
+ configFromFile = (configFile.asString).parseYAMLFile;
+ configFromFile.keysValuesDo({|key, value|
+ switch(key.asSymbol,
+ \controls,{
+ // adding names dictionary
+ config.put(key.asSymbol, Dictionary.new(value.size));
+ // going through the names and their associated dictionaries
+ value.keysValuesDo({|name, dictionary|
+ if(dictionary.isKindOf(Dictionary),{
+ config.at(key.asSymbol).put(name.asSymbol, Dictionary.new(dictionary.size));
+ // going through each control dictionary with controls and their values
+ dictionary.keysValuesDo({|control, controlValue|
+ if(defaultControls.includesKey(control.asSymbol),{
+ config.at(key.asSymbol).at(name.asSymbol).put(control.asSymbol, this.getControlValue(control.asSymbol, controlValue));
+ });
+ });
+ },{
+ error("This element should be of type Dictionary: "++dictionary);
+ });
+ });
+ },
+ \inputs,{
+ config.put(key.asSymbol, Dictionary.new());
+ value.keysValuesDo({|name, channel|
+ config.at(\inputs).put(name.asSymbol, channel.asInteger);
+ });
+ },
+ {
+ if(defaultConfig.includesKey(key.asSymbol),{
+ config.put(key.asSymbol, this.getSettingValue(key.asSymbol, value));
+ },{
+ error("Key is not included in known configuration: "++key);
+ });
+ }
+ );
+ });
+ postln("Reading of configuration complete. Now validating...");
+ if(this.validateConfig,{
+ postln("Validation complete.");
+ ^true;
+ },{
+ postln("Validation failed.");
+ ^false;
+ });
+ }{
+ ("Failed parsing the file: "++configFile).error;
+ ("Make sure its JSON syntax is valid!").error;
+ ^false;
+ };
+ }, {
+ error("File not readable:"++configFile.asString);
+ ^false;
+ });
+ }
+
+ //TODO: add writeConfigurationFile
+
+ // return a control value as correct type and within its defined ControlSpec range
+ getControlValue{
+ arg control, value;
+ if(defaultControls.includesKey(control.asSymbol),{
+ if(hasToBeFloat.includes(control.asSymbol),{
+ ^control.asSymbol.asSpec.constrain(value.asFloat).asFloat;
+ });
+ if(hasToBeInteger.includes(control.asSymbol),{
+ ^control.asSymbol.asSpec.constrain(value.asInteger).asInteger;
+ });
+ if(hasToBeBool.includes(control.asSymbol),{
+ switch(value,
+ 0,{^false},
+ 1,{^true},
+ "true",{^true},
+ "false",{^false}
+ );
+ });
+ },{
+ ^nil;
+ });
+ }
+
+ //get a setting value as correct type
+ getSettingValue{
+ arg setting, value;
+ if(defaultConfig.includesKey(setting.asSymbol),{
+ if(hasToBeString.includes(setting.asSymbol),{
+ ^value.asString;
+ });
+ if(hasToBeInteger.includes(setting.asSymbol),{
+ ^value.asInteger;
+ });
+ },{
+ ^nil;
+ });
+ }
+
+ // validate/extend/fix the config and return true if it's useable
+ validateConfig{
+ var broken = false;
+ //fail if there are no inputs defined
+ if(config.includesKey(\inputs).not,{
+ ^false;
+ });
+ // if there are no controls defined at all, add at least a Dictionary for them
+ if(config.includesKey(\controls).not,{
+ config.put(\controls, Dictionary.new(config.at(\inputs).size));
+ });
+ // go through the defaultConfig/defaultControl collection and compare it to the provided config
+ defaultConfig.keysValuesDo({|key, value|
+ if(key != \controls && key != \inputs,{
+ // if a standard setting is not in the configuration, add it from the default
+ if(config.includesKey(key).not,{
+ config.put(key, value);
+ });
+ });
+ });
+ // go through the inputs and add controls, if there are none setup for them
+ config.at(\inputs).keysValuesDo({|name, channel|
+ // if there are no controls defined for the input, add them from default
+ if(config.at(\controls).includesKey(name).not,{
+ config.at(\controls).put(name, defaultControls);
+ });
+ });
+ // go through each control dictionary
+ config.at(\controls).keysValuesDo({|name, controlDictionary|
+ if(config.at(\inputs).includesKey(name),{
+ // go through the defaultControl Dictionary and add missing controls to controls from config
+ defaultControls.keysValuesDo({|defaultControl, defaultControlValue|
+ if(controlDictionary.includesKey(defaultControl).not,{
+ config.at(\controls).at(name).put(defaultControl, defaultControlValue);
+ });
+ });
+ },{
+ // remove controls for non-existing inputs
+ config.at(\controls).removeAt(name);
+ });
+ });
+ ^true;
+ }
+
+ //print the current config
+ showConfig{
+ postln("Configuration is: ");
+ config.keysValuesDo{|key, value|
+ switch(key,
+ \controls, {
+ postln(key++" ("++key.class++") -> ("++value.class++")");
+ value.keysValuesDo({|subKey, subValue|
+ postln(" "++subKey++" ("++subKey.class++") -> ("++subValue.class++")");
+ subValue.keysValuesDo({|subSubKey, subSubValue|
+ postln(" "++subSubKey++" ("++subSubKey.class++") -> "++subSubValue++" ("++subSubValue.class++")");
+ });
+ });
+ },
+ \inputs, {
+ postln(key++" ("++key.class++") -> ("++value.class++")");
+ value.keysValuesDo({|subKey, subValue|
+ postln(" "++subKey++" ("++subKey.class++") -> "++subValue++" ("++subValue.class++")");
+ });
+ },
+ {postln(key++" ("++key.class++") -> "++value++" ("++value.class++")");}
+ );
+ };
+ }
+
+ getSynthControl{
+ arg name, control;
+ if(config.at(\controls).includesKey(name) && config.at(\controls).at(name).includes(control),{
+ ^config.at(\controls).at(name).at(control);
+ },{
+ ^false;
+ });
+ }
+
+ // sets a control to a value for a synth
+ setSynthControl{
+ arg name, control;
+ if(config.at(\controls).includesKey(name) && config.at(\controls).at(name).includes(control[0]),{
+ config.at(\controls).at(name).put(control[0], control[1]);
+ ^true;
+ },{
+ ^false;
+ });
+ }
+
+}
diff --git a/classes/BowelyzerGUI.sc b/classes/BowelyzerGUI.sc
new file mode 100644
index 0000000..e26798e
--- /dev/null
+++ b/classes/BowelyzerGUI.sc
@@ -0,0 +1,520 @@
+BowelyzerGUI{
+
+ var mainView, <settingsView, <channelContainerView,
+ <channels,
+ <indicators,
+ //TODO: move to BowelyzerConfig
+ fadeOutTime = 0.3,
+ fadeOutSteps = 20,
+ minWidth=1280,
+ minHeight=720,
+ channelViewHeight = 700,
+ channelViewWidth = 300,
+ channelViewSize,
+ controlMeterContainerViewHeight = 700,
+ controlMeterContainerViewWidth = 300,
+ controlMeterContainerViewSize,
+ buttonHeight = 20,
+ buttonWidth = 48,
+ settingsSubViewMargins = #[2,2,2,2],
+ settingsSubViewSize,
+ settingsViewSize,
+ configViewSize,
+ meterViewWidth = 54,
+ meterViewHeight = 700,
+ headViewHeight = 24,
+ headViewWidth = 300,
+ controlsViewWidth = 246,
+ controlsViewHeight = 700
+ ;
+
+ *new{
+ arg config;
+ ^super.new.init(config)
+ }
+
+ init{
+ arg config;
+ postln("Launching GUI.");
+ settingsViewSize = 496@64;
+ settingsSubViewSize = 144@64;
+ configViewSize = 64@64;
+ controlMeterContainerViewSize = controlMeterContainerViewWidth@controlMeterContainerViewHeight;
+ channelViewSize = channelViewWidth@channelViewHeight;
+ channels = Set.new(config.at(\inputs).size);
+ indicators = Dictionary.new(config.at(\inputs).size);
+ this.setupMainView(config);
+ this.setupChannelViews(config);
+ }
+
+ //setup the main view, in which all other views reside
+ setupMainView{
+ arg config;
+ var channelScrollView, forwardView, hubView, synthServerView, configView, layout;
+ mainView = PageLayout.new("Bowelyzer");
+ mainView.asView.background = Color.fromHexString("#DBDBDB");
+ layout = mainView.asView.addFlowLayout(0@0, 0@0);
+ settingsView = View(mainView.asView, Rect(0, 0, settingsViewSize.x, settingsViewSize.y));
+ settingsView.asView.background = Color.fromHexString("#99EF99");
+ settingsView.layout = HLayout();
+ settingsView.layout.spacing = 0;
+ settingsView.layout.margins = [0,0,0,0];
+
+ //config
+ configView = View(settingsView.asView);
+ configView.asView.background = Color.fromHexString("#DDDDEF");
+ configView.layout = VLayout();
+ configView.layout.spacing = 0;
+ configView.layout.margins = settingsSubViewMargins;
+ configView.name = "config";
+ configView.maxSize_(configViewSize);
+ this.setupSaveButton(configView);
+ this.setupSaveAsButton(configView);
+ this.setupLoadButton(configView);
+
+ //synthServer
+ synthServerView = View(settingsView.asView);
+ synthServerView.asView.background = Color.fromHexString("#DDDDEF");
+ synthServerView.layout = VLayout();
+ synthServerView.layout.spacing = 0;
+ synthServerView.layout.margins = settingsSubViewMargins;
+ synthServerView.name = "synthServer";
+ synthServerView.maxSize_(settingsSubViewSize);
+ StaticText(synthServerView, Rect(0, 0, 140@20)).string_("synthServer").align_(\center);
+ this.setupEZText(synthServerView, "address", config.at(\synthServerAddress)).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(TextField),{item.align_(\right)});
+ });
+ this.setupEZNumber(synthServerView, "port", config.at(\synthServerPort)).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(NumberBox),{item.align_(\right)});
+ });
+
+ //hub
+ hubView = View(settingsView.asView);
+ hubView.asView.background = Color.fromHexString("#DDDDEF");
+ hubView.layout = VLayout();
+ hubView.layout.spacing = 0;
+ hubView.layout.margins = settingsSubViewMargins;
+ hubView.name = "hub";
+ hubView.maxSize_(settingsSubViewSize);
+ StaticText(hubView, Rect(0, 0, 140@20)).string_("hub").align_(\center);
+ this.setupEZText(hubView, "address", config.at(\hubAddress)).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(TextField),{item.align_(\right)});
+ });
+ this.setupEZNumber(hubView, "port", config.at(\hubPort)).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(NumberBox),{item.align_(\right)});
+ });
+
+ //forward
+ forwardView = View(settingsView.asView);
+ forwardView.asView.background = Color.fromHexString("#DDDDEF");
+ forwardView.layout = VLayout();
+ forwardView.layout.spacing = 0;
+ forwardView.layout.margins = settingsSubViewMargins;
+ forwardView.name = "forward";
+ forwardView.maxSize_(settingsSubViewSize);
+ StaticText(forwardView, Rect(0, 0, 140@20)).string_("forward").align_(\center);
+ this.setupEZText(forwardView, "address", config.at(\forwardAddress)).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(TextField),{item.align_(\right)});
+ });
+ this.setupEZNumber(forwardView, "port", config.at(\forwardPort)).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(NumberBox),{item.align_(\right)});
+ });
+
+ // go to next line in layout
+ layout.nextLine;
+ channelScrollView = ScrollView(mainView.asView, Rect(0,0,mainView.bounds.width, mainView.bounds.height-settingsView.bounds.height)).autohidesScrollers_(false).hasBorder_(true);
+ // container for channelViews
+ channelContainerView = View(parent: channelScrollView, bounds: Rect(0, 0, channelViewWidth*config.at(\inputs).size, channelViewHeight));
+ channelContainerView.asView.background = Color.fromHexString("#FEFEFE");
+ channelContainerView.layout = HLayout();
+ channelContainerView.layout.spacing = 0;
+ channelContainerView.layout.margins = [0,0,0,0];
+ channelContainerView.maxSize_((channelViewWidth*config.at(\inputs).size)@channelViewHeight);
+ }
+
+ //setup channel views
+ setupChannelViews{
+ arg config;
+ var channelView, headView, controlMeterContainerView, meterView, controlsView, controlsFromConfig, levelIndicator;
+ config.at(\inputs).keysValuesDo({|name, input|
+ //channelView = View(mainView.asView, Rect(0, 0, mainView.bounds.width/config.at(\inputs).size, mainView.bounds.height));
+ channelView = View(mainView.asView, Rect(0, 0, channelViewWidth, channelViewHeight));
+ channelView.asView.background = Color.fromHexString("#FEEFEF");
+ channelView.name = name;
+ channelView.layout = VLayout();
+ channelView.layout.spacing = 0;
+ channelView.layout.margins = [2,0,2,0];
+ channelView.maxSize_(300@700);
+ // adding name textfield of input
+ headView = View(channelView.asView, Rect(0, 0, headViewWidth, headViewHeight));
+ headView.asView.background = Color.fromHexString("#DDEFDD");
+ headView.name = \inputs;
+ headView.layout = HLayout();
+ headView.layout.spacing = 0;
+ headView.layout.margins = [2,0,2,0];
+ headView.maxHeight_(buttonHeight);
+ this.setupEZText(headView, "name", name.asString).children.do({|item|
+ if(item.isKindOf(StaticText),{item.align_(\left)});
+ if(item.isKindOf(TextField),{item.align_(\right)});
+ });
+ this.setupEZNumber(headView, "input", config.at(\inputs).at(name)).children.do({|item|
+ item.postln; if(item.isKindOf(StaticText),{item.align_(\left)});
+ item.postln; if(item.isKindOf(NumberBox),{item.align_(\right)});
+ });
+
+ controlMeterContainerView = View(channelView.asView, Rect(0, 0, controlMeterContainerViewWidth, controlMeterContainerViewHeight));
+ controlMeterContainerView.asView.background = Color.fromHexString("#DDDDEF");
+ controlMeterContainerView.name = \meterAndControls;
+ controlMeterContainerView.layout = HLayout();
+ controlMeterContainerView.layout.spacing = 0;
+ controlMeterContainerView.layout.margins = [0,0,0,0];
+
+ meterView = View(controlMeterContainerView.asView, Rect(0,0, meterViewWidth, meterViewHeight));
+ meterView.asView.background = Color.fromHexString("#EEEFEE");
+ meterView.name = \meterView;
+ meterView.layout = VLayout();
+ meterView.maxWidth = meterViewWidth;
+ meterView.layout.spacing = 0;
+ meterView.layout.margins = [2,0,2,0];
+
+ //setup a toggle button for each channel
+ this.setupPauseButton(meterView);
+
+ // setup a small View as indicator for incoming/outgoing OSC messages for each input
+ this.setupOSCIndicator(meterView);
+ this.addOSCIndicatorFadeOutTask(name);
+
+ // setup LevelIndicator for each input
+ this.setupLevelIndicator(meterView);
+
+ controlsView = View(controlMeterContainerView.asView, Rect(0,0, controlsViewWidth, controlsViewHeight));
+ controlsView.asView.background = Color.fromHexString("#EEEFEE");
+ controlsView.name = \controls;
+ controlsView.maxWidth = controlsViewWidth;
+ controlsView.layout = VLayout();
+ controlsView.layout.spacing = 0;
+ controlsView.layout.margins = [2,0,2,0];
+
+ //TODO: add ranger for amplitude and pitch threshold/region
+ //TODO: rearrange sliders/rangers/knobs in groups (maybe even tabs)
+ config.at(\controls).at(name).order.do({|control, i|
+ var unit;
+ var value = config.at(\controls).at(name).at(control), controlIs = BowelyzerConfig.controlContainedIn(control);
+ switch(
+ controlIs.asSymbol,
+ \knob, {this.setupEZKnob(controlsView, control, value, \controls, name).view.children.do({|item| if(item.isKindOf(NumberBox),{item.align_(\right)})})},
+ \slider, {this.setupEZSlider(controlsView, control, value).view.children.do({|item| if(item.isKindOf(NumberBox),{item.align_(\right)})})},
+ \ranger, {this.setupEZRanger(controlsView, control, value, \controls, name).view.children.do({|item| if(item.isKindOf(NumberBox),{item.align_(\right)})})}
+ );
+ });
+ // add channelView to the container and the global Dictionary for better access
+ channelContainerView.layout.add(channelView);
+ channels.add(channelView);
+ });
+ }
+
+ // setup a OSC indicator view (one on each channel)
+ setupOSCIndicator{
+ arg parent;
+ ^View(parent, Rect(0, 0, buttonWidth, buttonHeight))
+ .name_(\OSCIndicator)
+ .background_(Color.fromHexString("#EEEFEE"))
+ .maxSize_(buttonWidth@buttonHeight)
+ .visible_(true)
+ ;
+ }
+
+ // setup tasks for changing the OSC indicator color (on receiving a message)
+ addOSCIndicatorFadeOutTask{
+ arg name;
+ indicators.put(
+ name.asSymbol,
+ Task({
+ channels.do({|channel|
+ if(channel.name.asSymbol == name,{
+ channel.children.do({|channelChild|
+ if(channelChild.name.asSymbol == \meterAndControls,{
+ channelChild.children.do({|meterAndControls|
+ if(meterAndControls.name.asSymbol == \meterView, {
+ meterAndControls.children.do({|meterView|
+ if(meterView.isKindOf(View) && meterView.name.asSymbol == \OSCIndicator, {
+ meterView.background_(Color.fromHexString("#99FF99"));
+ fadeOutSteps.do({|item,i|
+ meterView.background_(meterView.background.blend(Color.fromHexString("#EEEFEE"), (fadeOutTime/fadeOutSteps)));
+ (fadeOutTime/fadeOutSteps).wait;
+ });
+ meterView.background_(Color.fromHexString("#EEEFEE"));
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ })
+ );
+ }
+
+ freeOSCIndicatorFadeOutTask{
+
+ }
+
+ setupLevelIndicator{
+ arg parent;
+ ^LevelIndicator(
+ parent
+ ).maxSize_(buttonWidth@600)
+ .style_(\led)
+ .drawsPeak_(true);
+ }
+
+ // create a load button, that launches a FileDialog on press
+ setupLoadButton{
+ arg parent;
+ ^Button(parent, Rect(0, 0, buttonWidth, buttonHeight))
+ .states_([
+ ["load", Color.black, Color.fromHexString("#99FF99")]
+ ])
+ .action_({
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/load";
+ FileDialog.new(
+ okFunc: {|path|
+ postln("Sending: ["++type++", "++path++"]");
+ address.sendMsg(type, path.asString);
+ },
+ cancelFunc: {},
+ fileMode: 1,
+ acceptMode: 0,
+ stripResult: true
+ );
+ })
+ .maxSize_(buttonWidth@buttonHeight)
+ ;
+ }
+
+ // create a save button, that launches a FileDialog on press
+ setupSaveButton{
+ arg parent;
+ ^Button(parent, Rect(0, 0, buttonWidth, buttonHeight))
+ .states_([
+ ["save", Color.black, Color.fromHexString("#99FF99")],
+ ])
+ .action_({
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/save";
+ postln("Sending: ["++type++"]");
+ address.sendMsg(type);
+ })
+ .maxSize_(buttonWidth@buttonHeight)
+ ;
+ }
+
+ // create a save button, that launches a FileDialog on press
+ setupSaveAsButton{
+ arg parent;
+ ^Button(parent, Rect(0, 0, buttonWidth, buttonHeight))
+ .states_([
+ ["save as", Color.black, Color.fromHexString("#99FF99")],
+ ])
+ .action_({
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/saveas";
+ FileDialog.new(
+ okFunc: {|path|
+ postln("Sending: ["++type++", "++path++"]");
+ address.sendMsg(type, path.asString);
+ },
+ cancelFunc: {},
+ fileMode: 1,
+ acceptMode: 1,
+ stripResult: true
+ );
+ })
+ .maxSize_(buttonWidth@buttonHeight)
+ ;
+ }
+
+ // setup a button to pause the Synth for a channel
+ setupPauseButton{
+ arg parent;
+ ^Button(parent, Rect(0, 0, buttonWidth, buttonHeight))
+ .states_([
+ ["on", Color.black, Color.fromHexString("#99FF99")],
+ ["off", Color.black, Color.fromHexString("#FF9999")]
+ ])
+ .action_({
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/toggle",
+ name = controlUnit.parent.parent.parent.name.asSymbol,
+ controlValue = controlUnit.value;
+ address.sendMsg(type, name, controlValue);
+ })
+ .maxSize_(buttonWidth@buttonHeight)
+ ;
+ }
+
+ // setup a StaticText and a TextField
+ setupEZText{
+ arg parent, control, value;
+ var bounds = 144@buttonHeight,
+ labelWidth= 48,
+ textWidth = 96;
+ // resize, depending on surrounding
+ if(control == "address",{
+ bounds = 140@buttonHeight;
+ labelWidth = 60;
+ textWidth = 80;
+ });
+ ^EZText(
+ parent: parent,
+ bounds: bounds,
+ label: control,
+ action: {
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/"++controlUnit.view.parent.name.asString,
+ name = controlUnit.view.parent.parent.name.asSymbol,
+ controlName = controlUnit.labelView.string.asSymbol,
+ controlValue = controlUnit.value;
+ if((type == "/synthServer") || (type == "/hub") || (type == "/forward"), {
+ type = (type++"Address").replace("/","").asSymbol;
+ name = "/"++controlName;
+ postln("Sending: "++"["++name++", "++type++", "++controlValue++"]");
+ address.sendMsg(name, type, controlValue);
+ },{
+ postln("Sending: "++"["++type++", "++name++", "++controlName++", "++controlValue++"]");
+ address.sendMsg(type, name, controlName, controlValue);
+ });
+ },
+ labelWidth: labelWidth,
+ textWidth: textWidth,
+ initVal:value.asString,
+ layout: \horz,
+ margin: nil
+ )
+ .view.maxHeight_(buttonHeight)
+ ;
+ }
+
+ // setup a StaticText and a NumberBox
+ setupEZNumber{
+ arg parent, control, value;
+ var bounds = 144@buttonHeight,
+ labelWidth = 48,
+ numberWidth = 96;
+ // resize, depending on surrounding
+ if(control == "port",{
+ bounds = 140@buttonHeight;
+ labelWidth = 70;
+ numberWidth = 70;
+ });
+ ^EZNumber(
+ parent: parent,
+ bounds: bounds,
+ label: control,
+ controlSpec: control.asSymbol,
+ action: {
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/"++controlUnit.view.parent.name.asString,
+ name = controlUnit.view.parent.parent.name.asSymbol,
+ controlName = controlUnit.labelView.string.asSymbol,
+ controlValue = controlUnit.value;
+ if((type == "/synthServer") || (type == "/hub") || (type == "/forward"), {
+ type = (type++"Port").replace("/", "").asSymbol;
+ name = "/"++controlName;
+ postln("Sending: "++"["++name++", "++type++", "++controlValue++"]");
+ address.sendMsg(name, type, controlValue);
+ },{
+ postln("Sending: "++"["++type++", "++name++", "++controlName++", "++controlValue++"]");
+ address.sendMsg(type, name, controlName, controlValue);
+ });
+ },
+ initVal:value,
+ labelWidth: labelWidth,
+ numberWidth: numberWidth,
+ layout: \horz,
+ margin: nil
+ )
+ .view.maxHeight_(buttonHeight)
+ ;
+ }
+
+ // setup a Slider, a StaticText and a NumberBox for a Synth setting
+ setupEZSlider{
+ arg parent, control, value;
+ ^EZSlider(
+ parent: parent,
+ bounds: 236@40,
+ label: control,
+ controlSpec: control.asSymbol,
+ action: {
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/"++controlUnit.view.parent.name.asString,
+ name = controlUnit.view.parent.parent.parent.name.asSymbol,
+ controlName = controlUnit.labelView.string.asSymbol,
+ controlValue = controlUnit.value;
+ address.sendMsg(type, name, controlName, controlValue);
+ },
+ numberWidth: 70,
+ layout: \line2,
+ margin: nil
+ );
+ }
+
+ setupEZRanger{
+ arg parent, control, value;
+ ^EZRanger(
+ parent: parent,
+ bounds: 300@16,
+ label: control,
+ controlSpec: control,
+ action: {
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/"++controlUnit.view.parent.name.asString,
+ name = controlUnit.view.parent.parent.parent.name.asSymbol,
+ controlName = controlUnit.labelView.string.asSymbol,
+ controlValue = controlUnit.value;
+ address.sendMsg(type, name, controlName, controlValue);
+ },
+ labelWidth: 120,
+ unitWidth:30
+ );
+ }
+
+ setupEZKnob{
+ arg parent, control, value, type, name;
+ ^EZKnob(
+ parent: parent,
+ bounds: 300@16,
+ label: control,
+ controlSpec: \freq,
+ action: {
+ arg controlUnit;
+ var address = NetAddr.new("127.0.0.1", NetAddr.langPort),
+ type = "/"++controlUnit.view.parent.name.asString,
+ name = controlUnit.view.parent.parent.parent.name.asSymbol,
+ controlName = controlUnit.labelView.string.asSymbol,
+ controlValue = controlUnit.value;
+ address.sendMsg(type, name, controlName, controlValue);
+ },
+ labelWidth: 120,
+ unitWidth:30
+ );
+ }
+}
+
diff --git a/classes/BowelyzerOSCHub.sc b/classes/BowelyzerOSCHub.sc
new file mode 100644
index 0000000..8e6bf52
--- /dev/null
+++ b/classes/BowelyzerOSCHub.sc
@@ -0,0 +1,88 @@
+BowelyzerOSCHub{
+ var <hub, <forward, <synthServer, <local;
+
+ *new{
+ arg config;
+ ^super.new.init(config);
+ }
+
+ // setup a NetAddr object and return it
+ *getNetAddr{
+ arg server, port;
+ ^NetAddr.new(server, port);
+ }
+
+ init{
+ arg config;
+ this.setupNetAddressesFromConfig(config);
+ this.setupSynthListenersFromConfig(config);
+ }
+
+ setupNetAddressesFromConfig{
+ arg config;
+ forward = BowelyzerOSCHub.getNetAddr(config.at(\forwardAddress), config.at(\forwardPort));
+ hub = BowelyzerOSCHub.getNetAddr(config.at(\hubAddress), config.at(\hubPort));
+ synthServer = BowelyzerOSCHub.getNetAddr(config.at(\synthServerAddress), config.at(\synthServerPort));
+ local = 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 "++synthServer.ip++":"++synthServer.port++".");
+ this.addSynthListener(name);
+ });
+ }
+
+ addSynthListener{
+ arg name;
+ OSCdef.newMatching(
+ key: name.asSymbol,
+ func: {|msg, time, addr, recvPort|
+ this.forwardToNetAddress(msg, time);
+ },
+ path: "/"++name.asString,
+ srcID: synthServer,
+ recvPort: hub.port
+ );
+ }
+
+ startSynthListener{
+ arg name;
+ OSCdef(name).enable;
+ }
+
+ stopSynthListener{
+ arg name;
+ OSCdef(name.asSymbol).disable;
+ }
+
+ freeSynthListener{
+ arg name;
+ OSCdef(name.asSymbol).free;
+ }
+
+ //forward a received OSC message to the globally specified forward address
+ forwardToNetAddress{
+ arg msg, time;
+ var name = msg[0],
+ amplitude = msg[3],
+ pitch = msg[4],
+ hasPitch = msg[5],
+ onsetDetect = msg[7];
+ if(amplitude != 0,{
+ local.sendMsg("/indicate", msg[0].asString.replace("/","").asSymbol);
+ if(forward.isLocal && (forward.port == NetAddr.langPort), {
+ postln(msg[0]++" (amplitude: "++amplitude++"; pitch: "++pitch++"; has pitch: "++hasPitch);
+ },{
+ forward.sendMsg(name, amplitude, pitch, hasPitch, onsetDetect);
+ });
+ });
+ }
+
+ //TODO: add functions to modify OSC listener behavior (what data to send from which channel)
+
+}
+