aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bowelyzer.sc30
-rw-r--r--BowelyzerAnalyzer.sc155
-rw-r--r--BowelyzerConfig.sc229
-rw-r--r--BowelyzerOSCHub.sc47
-rw-r--r--bowelyzer.scd12
-rw-r--r--darmstadt.json54
6 files changed, 527 insertions, 0 deletions
diff --git a/Bowelyzer.sc b/Bowelyzer.sc
new file mode 100644
index 0000000..3e2be34
--- /dev/null
+++ b/Bowelyzer.sc
@@ -0,0 +1,30 @@
+Bowelyzer{
+
+ var gui, <>server, <>analyzer, <>config, <>hub;
+
+ *new{
+ arg config;
+ ^super.new.init(config);
+ }
+
+ init{
+ //initialize with configuration, if available (else use default)
+ arg config;
+ this.config = BowelyzerConfig.new(config);
+ server = Server.new(\bowelyzer, BowelyzerOSCHub.getNetAddr(this.config.config.at("synthServerAddress"), this.config.config.at("synthServerPort")));
+ Server.default = server;
+ server.waitForBoot({
+ analyzer = BowelyzerAnalyzer.new(this.config.config);
+ hub = BowelyzerOSCHub.new(this.config.config);
+ },
+ 5,
+ {"scsynth failed to start!".postln});
+ //meter = ServerMeter.new(server, this.config.config.at(\channels));
+ //server.meter(this.config.config.at(\channels));
+ //server.makeGUI;
+ //server.scope;
+ }
+
+
+
+} \ No newline at end of file
diff --git a/BowelyzerAnalyzer.sc b/BowelyzerAnalyzer.sc
new file mode 100644
index 0000000..f6dda05
--- /dev/null
+++ b/BowelyzerAnalyzer.sc
@@ -0,0 +1,155 @@
+BowelyzerAnalyzer{
+
+ var <>config;
+ var <synths;
+ var <controls;
+ var <analyzerGroup;
+ var <monitoringGroup;
+
+ *new{
+ arg config;
+ ^super.new.init(config);
+ }
+
+ init{
+ arg config;
+ this.config = config;
+ synths = Dictionary.new(config.at("names").size);
+ controls = Dictionary.new(config.at("names").size);
+ config.at("names").do({ |item, i|
+ controls.putPairs([item, Dictionary.new(7)]);
+ });
+ this.setupGroups;
+ this.addAnalysisSynthsFromConfig(this.config);
+ }
+
+ // 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
+ addAnalysisSynthsFromConfig{
+ arg config;
+ config.at("names").size.do({|i|
+ var name = config.at("names")[i],
+ controls = config.at("controls").at(name);
+ ("Adding SynthDef '"++name++"' on input "++(i+config.at("channelOffset")[i])).postln;
+ synths.add(name -> SynthDef(name, {
+ arg 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: i+config.at("channelOffset")[i]);
+ 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.kr(
+ Impulse.kr(sendReplyFreq),
+ '/'++config.at("names")[i].asSymbol,
+ [amplitude, pitch, hasPitch, detect]
+ );
+ }).play(
+ target: analyzerGroup,
+ args: //arguments to the Synth. Order matters!
+ [
+ controls.at("sendReplyFreq"),
+ controls.at("amplitudeAttackTime"),
+ controls.at("amplitudeReleaseTime"),
+ controls.at("hfHainsworth"),
+ controls.at("hfFoote"),
+ controls.at("hfThreshold"),
+ controls.at("hfWaitTime"),
+ controls.at("pitchInitFreq"),
+ controls.at("pitchMinFreq"),
+ controls.at("pitchMaxFreq"),
+ controls.at("pitchExecFreq"),
+ controls.at("pitchMaxBinsPerOctave"),
+ controls.at("pitchMedian"),
+ controls.at("pitchAmpThreshold"),
+ controls.at("pitchPeakThreshold"),
+ controls.at("pitchDownSample")
+ ]
+ )
+ );
+ });
+ }
+
+ setAnalysisSynthControl{
+ arg name, control;
+ controls.at(name).put(control[0], control[1]);
+ synths.at(name).set(control[0], control[1]);
+ }
+//TODO: get all this from BowelyzerConfig
+ getAnalysisSynthControl{
+ arg name, control;
+ synths.at(name).get(
+ control,
+ {
+ arg value;
+ controls.at(name).put(control, value);
+ }
+ );
+ //TODO: wait for the control to be set
+ ^controls.at(name.asSymbol).at(control.asSymbol);
+ }
+
+ startAnalysisSynth{
+ arg name;
+ synths.at(name).run(true);
+ }
+
+ stopAnalysisSynth{
+ arg name;
+ synths.at(name).run(false);
+ }
+
+ startAllAnalysisSynths{
+ analyzerGroup.run(true);
+ }
+
+ stopAllAnalysisSynths{
+ analyzerGroup.run(false);
+ }
+}
diff --git a/BowelyzerConfig.sc b/BowelyzerConfig.sc
new file mode 100644
index 0000000..9686c17
--- /dev/null
+++ b/BowelyzerConfig.sc
@@ -0,0 +1,229 @@
+BowelyzerConfig{
+
+ var <config,
+ <defaultConfig,
+ <defaultControls,
+ <changed = false,
+ <hasToBeString,
+ <hasToBeInteger,
+ <hasToBeArray,
+ <hasToBeDictionary,
+ <hasToBeFloat;
+
+
+ *new{
+ arg config;
+ ^super.new.init(config);
+ }
+
+ init{
+ arg configFile;
+ hasToBeString = [
+ \forwardAddress,
+ \hubAddress,
+ \synthServerAddress
+ ];
+ hasToBeInteger = [
+ \forwardPort,
+ \hubPort,
+ \synthServerPort,
+ \pitchMedian,
+ \pitchDownSample
+ ];
+ hasToBeArray = [
+ \names,
+ \channelOffset
+ ];
+ hasToBeDictionary = [
+ \controls,
+ \left,
+ \right
+ ];
+ hasToBeFloat = [
+ \amplitudeAttackTime,
+ \amplitudeReleaseTime,
+ \hfHainsworth,
+ \hfFoote,
+ \hfThreshold,
+ \hfWaitTime,
+ \pitchInitFreq,
+ \pitchMinFreq,
+ \pitchMaxFreq,
+ \pitchExecFreq,
+ \pitchMaxBinsPerOctave,
+ \pitchAmpThreshold,
+ \pitchPeakThreshold,
+ \sendReplyFreq
+ ];
+ config = this.createDefaultConfig;
+ if(configFile.notNil,{
+ this.readConfigurationFile(configFile)
+ },{
+ ("No configuration file provided. Using default.").postln;
+ });
+ this.showConfig;
+ }
+
+// create the default configuration
+ createDefaultConfig{
+ defaultControls = Dictionary.with(*[
+ "amplitudeAttackTime" -> 0.01,
+ "amplitudejReleaseTime" -> 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(*[
+ "channelOffset" -> [0, 0],
+ "names" -> ["left", "right"],
+ "synthServerAddress" -> "127.0.0.1",
+ "synthServerPort"->57110,
+ "hubAddress" -> "127.0.0.1",
+ "hubPort" -> 57120,
+ "forwardAddress" -> "127.0.0.1",
+ "forwardPort" -> 9999,
+ "controls" -> Dictionary.with(*[
+ "left" -> defaultControls,
+ "right" -> defaultControls
+ ])
+ ]);
+ ^defaultConfig;
+ }
+
+// 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|
+ if(config.includesKey(key),{
+ config.put(key,value);
+ });
+ });
+ this.validateConfig;
+ }{
+ ("Failed parsing the file: "++configFile).error;
+ ("Make sure its JSON syntax is valid!").error;
+ };
+ }, {
+ error("File not readable:"++configFile.asString);
+ });
+ this.validateConfig;
+ }
+
+ //check common mistakes in configuration and try fixing them
+ validateConfig{
+ var broken = false;
+ //check for correct types
+ config.keysValuesDo({|key, value|
+ if(key.isKindOf(String).not,{
+ error("Key is no string: "++key);
+ broken = true;
+ });
+ // check if the strings are associated with the correct keys
+ if(hasToBeString.includes(key.asSymbol) && value.isKindOf(String).not, {
+ config.put(key, value.asString);
+ });
+ // check if the integers are associated with the correct keys
+ if(hasToBeInteger.includes(key.asSymbol) && value.isKindOf(Integer).not, {
+ config.put(key, value.asInteger);
+ });
+ // check if the floats are associated with the correct keys
+ if(hasToBeFloat.includes(key.asSymbol) && value.isKindOf(Float).not, {
+ config.put(key, value.asFloat);
+ });
+ // check if the dictionaries are associated with the correct keys
+ if(hasToBeDictionary.includes(key.asSymbol) && value.isKindOf(Dictionary).not, {
+ error("Value ("++value++") of key ("++key++") should be of type Dictionary.");
+ broken = true;
+ });
+ //check if arrays are associated with the correct keys
+ if(hasToBeArray.includes(key.asSymbol),{
+ if(value.isArray.not,{
+ error("Value ("++value++") of key ("++key++") should be Array.");
+ broken = true;
+ },{
+ //setting correct types in Arrays
+ if((key == "channelOffset") && (value.size >= 1) && value[0].isKindOf(Integer).not,{
+ for(0, value.size-1, {|i|
+ config.at(key)[i] = value[i].asInteger;
+ });
+ });
+ if((key == "names") && (value.size >= 1) && value[0].isKindOf(String).not,{
+ for(0, value.size-1, {|i|
+ config.at(key)[i] = value[i].asString;
+ });
+ });
+ });
+ });
+ // check if controls dictionaries are setup correctly
+ if(key == "controls",{
+ value.keysValuesDo({|name, controls|
+ if(hasToBeDictionary.includes(name.asSymbol) && controls.isKindOf(Dictionary).not, {
+ error("Value ("++controls++") of key ("++name++") should be of type Dictionary.");
+ broken = true;
+ },{
+ config.at("controls").at(name).keysValuesDo({|controlKey, controlValue|
+ // check if the integers are associated with the correct keys
+ if(hasToBeInteger.includes(controlKey.asSymbol) && controlValue.isKindOf(Integer).not, {
+ config.at(key).at(name).put(controlKey, controlValue.asInteger);
+ });
+ // check if the floats are associated with the correct keys
+ if(hasToBeFloat.includes(controlKey.asSymbol) && controlValue.isKindOf(Float).not, {
+ config.at(key).at(name).put(controlKey, controlValue.asFloat);
+ });
+ });
+ });
+ });
+ });
+ });
+ // if not completely broken, fix stuff
+ if(broken,{
+ error("There were errors. Reverting to default config.");
+ config = this.createDefaultConfig;
+ },{
+ postln("Fixing stuff");
+ // zero-pad channel offsets
+ if(config.at("names").size > config.at("channelOffset").size,{
+ var difference = config.at("names").size - config.at("channelOffset").size;
+ config.at("channelOffset").asArray = config.at("channelOffset")++Array.fill(difference, {arg i; i*0});
+ });
+ //TODO: add defaultControls for channels that have none
+ //TODO: fill non-existing keys with default values (in case config file is incomplete)
+ });
+ }
+
+ //print the current config
+ showConfig{
+ postln("Configuration is:");
+ config.keysValuesDo{|key, value|
+ if(key == "controls",{
+ postln(key++" ("++key.class++") ->");
+ value.keysValuesDo{|key, value|
+ postln( key++" ("++key.class++"):");
+ value.keysValuesDo{|key, value|
+ postln(" "++key++" ("++key.class++") -> "++value++" ("++value.class++")");
+ }
+ }
+ },{
+ postln(key++" ("++key.class++") -> "++value++" ("++value.class++")");
+ });
+ };
+ }
+
+}
diff --git a/BowelyzerOSCHub.sc b/BowelyzerOSCHub.sc
new file mode 100644
index 0000000..028de55
--- /dev/null
+++ b/BowelyzerOSCHub.sc
@@ -0,0 +1,47 @@
+BowelyzerOSCHub{
+ var <>config, <hub, <forward, <synthServer, <synthServerListener;
+
+ *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.config = 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"));
+ this.addAnalysisListener;
+ }
+
+
+// setup a new listener for SynthServerAddress:SynthServerPort
+ addAnalysisListener{
+ // listen for individual SendReply messages
+ config.at("names").do({|name|
+ postln("Listening for messages called '/"++name++"' coming from scsynth.");
+ //TODO: use OSCdef here to be able to use key and free
+ synthServerListener = OSCFunc.newMatching(
+ {|msg, time, addr, recvPort|
+ postln(time.asString++": "++msg[0]++" (amplitude: "++msg[3]++"; pitch: "++msg[4]++"; has pitch: "++msg[5]++"; note on: "++msg[6]++")");
+ if(msg[6] != 0.0,{
+ postln(msg[0]++" (amplitude: "++msg[3]++"; pitch: "++msg[4]++"; has pitch: "++msg[5]);
+ });
+ },
+ '/'++name.asSymbol,
+ synthServer,
+ hub.port
+ );
+ });
+ }
+
+ //TODO: add functions to set arguments for analyzer synths generically
+
+} \ No newline at end of file
diff --git a/bowelyzer.scd b/bowelyzer.scd
new file mode 100644
index 0000000..7b9c3df
--- /dev/null
+++ b/bowelyzer.scd
@@ -0,0 +1,12 @@
+// this is bowelyzer, a simple audio analysis to OSC hub
+
+
+// init bowelyzer with a configuration file (in JSON format)
+~bowelyzer = Bowelyzer.new("/my/path/to/configuration.json");
+
+// or without for the default values (stereo)
+~bowelyzer = Bowelyzer.new();
+
+// display the configuration (will be shown upon first init, too)
+~bowelyzer.config.showConfig;
+
diff --git a/darmstadt.json b/darmstadt.json
new file mode 100644
index 0000000..e9b64bf
--- /dev/null
+++ b/darmstadt.json
@@ -0,0 +1,54 @@
+{
+"names": [
+ "left",
+ "right"
+],
+"channelOffset": [
+ 0,
+ 0
+],
+"synthServerAddress": "127.0.0.1",
+"synthServerPort": 57110,
+"hubAddress": "127.0.0.1",
+"hubPort": 57120,
+"forwardAddress": "127.0.0.1",
+"forwardPort": 9999,
+"controls":{
+ "left": {
+ "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.0
+ },
+ "right": {
+ "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.0
+ }
+ }
+} \ No newline at end of file