diff options
Diffstat (limited to 'classes')
-rw-r--r-- | classes/Bowelyzer.sc | 267 | ||||
-rw-r--r-- | classes/BowelyzerAnalyzer.sc | 209 | ||||
-rw-r--r-- | classes/BowelyzerConfig.sc | 362 | ||||
-rw-r--r-- | classes/BowelyzerGUI.sc | 520 | ||||
-rw-r--r-- | classes/BowelyzerOSCHub.sc | 88 |
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) + +} + |