diff options
author | David Runge <dave@sleepmap.de> | 2016-05-29 13:38:07 +0200 |
---|---|---|
committer | David Runge <dave@sleepmap.de> | 2016-05-29 13:38:07 +0200 |
commit | 74bb3e3290d8f4ea82b2caf79f4b96bb7991faad (patch) | |
tree | 2da93c9e1923893ec752108733397204821e273d | |
parent | 33d8b5bff1feb3c35fc049e29fcaca985d50520b (diff) | |
download | bowelyzer-74bb3e3290d8f4ea82b2caf79f4b96bb7991faad.tar.gz bowelyzer-74bb3e3290d8f4ea82b2caf79f4b96bb7991faad.tar.bz2 bowelyzer-74bb3e3290d8f4ea82b2caf79f4b96bb7991faad.tar.xz bowelyzer-74bb3e3290d8f4ea82b2caf79f4b96bb7991faad.zip |
First commit of bowelyzer code. Includes Bowelyzer.sc, Bowelyzer{Analyzer,Config,OSCHub}.sc, bowelyzer.scd (for help) and darmstadt.json for a standard configuration in JSON.
-rw-r--r-- | Bowelyzer.sc | 30 | ||||
-rw-r--r-- | BowelyzerAnalyzer.sc | 155 | ||||
-rw-r--r-- | BowelyzerConfig.sc | 229 | ||||
-rw-r--r-- | BowelyzerOSCHub.sc | 47 | ||||
-rw-r--r-- | bowelyzer.scd | 12 | ||||
-rw-r--r-- | darmstadt.json | 54 |
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 |