Manta using the Snyderphonics Manta controller with SC3
to be used with the MantaCocoa application, which sends OSC data from the Manta controller.
A Manta object has OSCresponders for the different messages the Manta controller sends.
One can add and remove functions for each type of incoming event, as well as
for every individual pad, slider, noteOn, noteOff, velocity and centroid message coming in.
The Manta class keeps around the most recent values for every slider and pad.
Optionally, it can
normalize the incoming values for pads and sliders to 0.0 - 1.0,
mirror all the values on buses,
and create noteOn events based on incoming simple pad values.
(this functionality may move to the MantaCocoa app eventually).
Finally, one can switch between several setups of functions,
and add/remove globalActions that remain the same across different setups.
See also: http://www.snyderphonics.com
See also: MantaNormal for normalsing pad and slider values
Creation / Class Methods and Variables
*appPath
the file path to the MantaCocoa.app.
Default is "/Applications/manta*/MantaCocoa.app"
Manta.appPath;
// set it to some other path
// Manta.appPath_("/path/to/MantaCocoa.app");
*start
start the MantaCocoa.app
Manta.start;
*verbose
Turn posting of debugging info on and off
Manta.verbose_(true); // post
Manta.verbose_(false); // no posting
*killPrefs
deletes the preference file that the MantaCocoa app writes, which is sometimes broken.
Restarting MantaCocoa after killPrefs restores it to the default config, which works fine.
Manta.killPrefs;
*new (addr, addResps, makeBuses, server, usesTops)
Create a Manta object.
addr - The NetAddr of the Manta app to listen to.
Default value is nil, which listens to all sending ports.
addResps - a flag whether to add all OSCresponderNodes on creation.
Default is true.
makeBuses - a flag whether to make buses on the server.
server - which server to make the buses on, default is Server.default.
usesTops - a flag whether to use the 4 top buttons separately as topButtons, or as pads 49-52.
Default is true, which keeps them separate, false joins them to the main 48 pads.
g = Manta.new;
Manta.verbose_(true);
// touch all the surfaces now: it posts values for all the controllers
// cleanup
Manta.verbose_(false); // no posting
g.free;
*setups
The Manta class can keep multiple setups that can be switched.
See setup examples further down.
Example 1: control the volumes of 48 continuous sounds, using only buses.
upper slider controls vibrato.
g.free; g = Manta.new; Manta.verbose = true;
(
// this example uses unscaled controller data, so turn normed off:
g.normed = false;
Ndef(\manta, { |lag=0.003|
var freqs = (48 + (0..47)).midicps;
var sliderBus = g.buses[\slider].kr;
var padBus = g.buses[\pad].kr;
// scaling bus values here
var modCtl = (sliderBus[0] / 4000).squared * 0.02;
var volCtl = (sliderBus[1] - 20 / 4000).max(0); // top Slider
var amps = (padBus * 0.005 * AmpComp.ir(freqs)).squared.lag(lag);
var mod = SinOsc.kr({ rrand(5.0, 6.0) }!48) * modCtl;
var oscs = SinOsc.ar(freqs * (1+mod));
Splay.ar(oscs * amps); // * volCtl // add optional master vol by slider
});
Ndef(\manta).play;
)
(
// this uses normed pad and slider values, so turn normed back on:
g.normed = true;
Ndef(\manta, { |lag=0.003|
var freqs = (48 + (0..47)).midicps;
var sliderBus = g.buses[\slider].kr;
var padBus = g.buses[\pad].kr;
// buses are normed, so simpler scaling here
var modCtl = (sliderBus[0]).squared * 0.02;
var volCtl = sliderBus[1].squared; // top Slider
var amps = (padBus * AmpComp.ir(freqs)).squared.lag(lag);
var mod = SinOsc.kr({ rrand(5.0, 6.0) }!48) * modCtl;
var oscs = SinOsc.ar(freqs * (1+mod));
Splay.ar(oscs * amps) // * volCtl // add optional master vol by slider
});
Ndef(\manta).play;
)
g.free; Ndef(\manta).end;
Example 2 - start individual synths for each pad.
// make a little voicer by hand, and play synths with it:
// this is unreliable when you play lots of notes very quickly,
// as you can get hanging silent synths: while all the synths are properly
// removed from the dict, some may not have been released properly!
// this happens in all cases, with \vel, \noteOn, and \pad as synth starters.
// not sure where that happens, needs more precise testing.
// happens on both internal and localhost server,
// setting latency to nil makes no difference.
s.latency = nil;
g.free; g = Manta.new;
(
q = ();
q.synths = ();
g.addMulti(\pad, { |key, val|
var synth = q.synths[key];
if (val == 0) { // "end synth".postln;
q.synths.removeAt(key).release;
} {
if (synth.isNil) { // "make synth".postln;
q.synths.put(key, Synth(\default, [\freq, (key - 1 + 60).midicps, \amp, (val * 0.5).squared ]));
} {
// "update synth".postln; // e.g. uncomment to try aftertouch
q.synths.at(key).set(\amp, (val * 0.5).squared);
};
};
});
)
g.free;
Example 3 - use a NodeProxy as voicer.
With this latency value, using a nodeproxy as voicer seems to work well;
no hanging synths as in example 2!
s.latency = 0.05;
g.free; g = Manta.new;
(
Ndef(\mantaVox).clear.play;
Ndef(\mantaVox).prime(\default); // put your synthdef here
g.addMulti(\pad, { |key, val|
var pxSynth = Ndef(\mantaVox).objects[key];
if (val == 0) {
// "release: %\n".postf(key);
Ndef(\mantaVox).put(key, nil);
} {
if (pxSynth.isNil) {
// "starting: % \n".postf(key);
Ndef(\mantaVox).put(key, \default, 0,
[\freq, (key - 1 + 60).midicps, \amp, (val * 0.5).squared ]
);
} {
// " setting: % \n".postf(key);
pxSynth.set(\amp, (val * 0.5).squared);
};
};
});
)
g.free;
Instance Variables
// make 2 Mantas, to compare what usesTops does.
g = Manta.new; // with buses and 48 pads / 4 topButtons etc
h = Manta.new(makeBuses: false, usesTops: false); // no buses, all 52 pads are uniform.
responders - a dictionary of all the OSCresponderNodes that listen to the different control events
g.responders;
h.responders;
multi - a dictionary where the different actions for each kind of incoming event are.
g.multi;
h.multi;
indiv - a dictionary where all the registered functions per individual pad/button/slider are.
g.indiv; // with topButton, topNote, topVelocity etc
h.indiv; // no tops
actions - a dictionary where multi and indiv actions are stored, mainly for saving setups.
g.actions;
h.actions;
addr - the NetAddr the Manta object is listening to. Default value nil means it listens to all senders.
g.addr;
values - a dictionary where the recent values for all controls are kept.
g.values; // with topButton etc etc
h.values; // no top
buses - a dictionary of the optional buses that provide the recent values on the server.
g.buses // with top
h.buses // no buses
h.free;
Configuring how the Manta object responds to touches
OSCresponders can be added and removed by names:
addResps (keys)
Short prose description of method.
keys - Explanation of keys. Default value is nil, meaning all responders are added.
// turn on 2 responders by name
g.addResps([\sliderResp, \padResp]);
removeResps (keys)
// turn off 2 other responders
g.removeResps([\velocityResp, \centroidResp]);
g.addResps();
Adding actions for one type of events, e.g. all pad touches:
setMultiAction (key, func)
removeMultiAction (key)
set and remove the actions to happen when a pad, a topbutton, a slider,
a velocity, noteOn, noteOff comes in.
g.addMulti(\pad, { |key, val| "pad num: % val: %\n".postf(key, val) });
// try the pads now
g.addMulti(\topButton, { |key, val| " topButton num: % val: %\n".postf(key, val) });
// try the 4 topButtons now
g.removeMulti(\pad);
g.removeMulti(\topButton);
g.removeMulti([\pad, \topButton]); // remove several at the same time
g.addMulti(\slider, { |key, val| "slider - num: % val: %\n".postf(key, val) });
// try the sliders now
// test the noteOnLogic in the Manta Class
g.usesNoteOn = true;
// noteOn and velocity should be disabled in the Manta.app
g.addMulti(\noteOn, { |key, val| "noteOn - num: % val: %\n".postf(key, val) });
g.addMulti(\noteOff, { |key, val| "noteOff - num: % val: %\n".postf(key, val) });
// first touch on a pad should trigger noteOn, release creates a noteOff
// separate velocity message makes no sense really
// g.addMulti(\velocity, { |key, val| "velocity - num: % val: %\n".postf(key, val) });
g.removeMulti([\slider, \noteOn, \noteOff, \velocity]);
// test centroid calculation that is done in the Manta Class:
// centroid should be disabled in the Manta.app.
g.usesCentroid = true;
g.addMulti(\centroid, { |key, vals| "centroid - vals x, y, totalVal: %\n".postf(vals) });
// touch some pads now - centroid should be posted
g.removeMulti([\centroid]);
g.usesCentroid = false;
Adding/removing functions for individual pads, topButtons, sliders:
add (type, key, func)
add(type, key, func)
Add a function for one type and key:
type - the type of event; one of [\pad, \slider, \noteOn, \noteOff].
or if usesTops, also [\topButton, \topNoteOn, \topNoteOff].
key - the pad key from 1 - 52:
1 is bottom left, 48 is top right;
49-52 are the extra 4 small touchbuttons on the top right.
func - the function to run when that pad/button senses a value.
g.add(\pad, 1, { |val| "yo, pad 1: %\n".postf(val) });
g.add(\topButton, 49, { |val| "top 4 buttons, the top left button? : %\n".postf(val) });
remove(type, key)
Remove the function for type and key:
type - the type of event; one of [\pad, \slider, \noteOn, \noteOff].
or if usesTops, also [\topButton, \topNoteOn, \topNoteOff].
key - the key for e.g. the button whose function is to be removed
g.remove(\pad, 1);
g.remove(\topButton, 49);
g.add(\slider, 1, { |val| "manta slider1: %\n".postf(val) });
g.remove(\slider, 1);
g.add(\noteOn, 1, { |val| "manta noteOn 1: %\n".postf(val) });
g.remove(\noteOn, 1);
g.add(\noteOff, 1, { |val| "manta noteOff 1: %\n".postf(val) });
g.remove(\noteOff, 1);
Adding globalActions for event types and individual keys
globalActions are kept across changing setups, so they can
be used for switching between setups easily.
addMultiGlobal(key, func)
removeMultiGlobal(key)
g.free; g = Manta.new; g.usesNoteOn = true;
g.addMultiGlobal(\topNoteOn, { |key, val| "YOYO ... %\n".postf(key) });
g.addMultiGlobal(\topNoteOff, { |key, val| "... oyoy %\n".postf(key) });
g.removeMultiGlobal([\topNoteOn, \topNoteOff]);
addGlobal(type, key, func)
removeMultiGlobal(type, key)
g.addGlobal(\slider, 1, { |val| "slider 1 is at % now.\n".postf(val) });
g.addGlobal(\slider, 2, { |val| "and slider 2 ... at %.\n".postf(val) });
g.removeGlobal(\slider, [1, 2]);
// switch between setups - if they would exist
g.addGlobal(\topNoteOn, 49, { |val| g.setup(\otto.postln) });
g.addGlobal(\topNoteOn, 50, { |val| g.setup(\siegfried.postln) });
g.removeGlobal(\topNoteOn, 49);
g.removeGlobal(\topNoteOn, 50);
Adding functions to specific non-current setups
see examples for creating and editing setups below.
// multi functions:
addMultiToSetup( setupName, type, func)
removeMultiFromSetup(setupName, type)
setupName - the name of the setup to add to / remove from
type - the kind of event to add / remove
func - the func to evaluate for that type of event
// individual funcs for pads, sliders etc
addToSetup(setupName, type, key, func)
removeFromSetup(setupName, type, key)
setupName - the name of the setup to add to / remove from
type - the kind of event to add / remove
key - the individual pad/slider/top key for which to add/remove a function
func - the func to evaluate for that type of event
// internal methods that may be useful for testing:
respond (name, key, val)
name - the name of the message to respond to.
can be [\pad, \slider, \noteOn, \noteOff] etc.
key - the key to respond to; for the pads 1-48, or 1-52.
val - the value for that kind of event at the key given.
// respond to pad 12 being touched with a value of 50:
g.respond(\pad, 12, 50);
Accessing recent values and buses
values, buses
The Manta object remembers the last values for some controls,
and also writes them to buses, if buses were made on creation.
Note: both buses and values are global for this Manta instance,
so they can be problematic if you try to run recorded controllers for
one Manta setup, and play another live. For this usage, it is better to
make buses for each setup separately.
g.values // where the current values live
g.values[\pad] // current button touch values
g.values[\slider] // current slider values (latched in the Manta controller)
g.values[\velocity] // last velocity values for each pad (latched after noteOn)
g.values[\note] // whether pad is currently touched - 0 or 1
g.values[\centroid] // current centroid - x, y, overall /last weight
g.values[\topButton] // like pad, but for topButtons
g.values[\topNote] // like note
g.values[\topVelocity] // like velocity
g.values.keys;
g.buses // a dict with all the buses - same structure as values!
g.buses[\pad]; // 48 or 52 values, 0 - ca 200
g.buses[\slider]; // 2 values ca 12 - ca 4096
g.buses[\velocity]; // 48 or 52 values 0 - 127
g.buses[\note]; // 48 or 52 values 0 or 1
g.buses[\topButton]; // 4 values if there 0 - ca 200
g.buses[\topNote]; // 4 values if there 0 or 1
g.buses[\topVelocity]; // 4 values if there 0 - 127
Setting LEDs: -- g.canSetLEDs_ currently crashes the MantaCocoa app (0.9.71)
// a flag whether LEDs are controlled from SC3
// (if not, they light up when touched)
g.canSetLEDs; // false
g.canSetLEDs_(true); // make them settable
g.listenAddr; // listenAddr is made if needed
// setLEDs(val ... indices) val: 1 is on, 0 is off.
g.setLEDs(1, 1, 2, 3);
g.setLEDs(1, *(1, 3 .. 48));
g.setLEDs(0, *(1 .. 52));
g.setLEDs(1, *(1 .. 52).scramble.keep(13));
g.canSetLEDs_(false); // back to auto mode
Examples
Pads are in harmonic table layout (turn the Manta so that the sliders are at right top):
d d# e
b c c#
g g# a
e f f#
c c# d
a Bb b
f f# g
d eb e
Bb B C
G G# A
Eb E F
C C# D
As A Bb
F F# G
Db D Eb
Bb B C
[98, 95, 99, 96, 100, 97].collect { |x| x + (0, -7 .. -49) };
Manta.start;
g.free; g = Manta.new;
(
g.normed = true;
Ndef(\mantaHarm, { |lag=0.003|
// var freqs = (48 + (0..47)).midicps;
var freqs = ([98, 95, 99, 96, 100, 97].collect { |x| x + (0, -7 .. -49) }.flat).midicps;
var sliderBus = g.buses[\slider].kr;
var padBus = g.buses[\pad].kr;
// buses are normed, so simpler scaling here
var modCtl = (sliderBus[0]).squared * 0.02;
var volCtl = sliderBus[1].squared; // top Slider
var amps = (padBus * AmpComp.ir(freqs)).squared.lag(lag);
var mod = SinOsc.kr({ rrand(5.0, 6.0) }!48) * modCtl;
var oscs = SinOsc.ar(freqs * (1+mod));
Splay.ar(oscs * amps) // * volCtl // add optional master vol by slider
});
Ndef(\mantaHarm).play;
)
Ndef(\mantaHarm).end;
Ndef(\mantaHarm).scope;
// a 6x6 feedback matrix patch:
// the sliders set root pitch and interval spread of the 6 oscs,
// the first 6 pads in every row control phasemod depth of osc1 + osc2 .. + osc 6 -> osc1;
// the last two add pitch mod, 6 controls speed, 7 controls depth.
// the energy for each row is taken from the sum of all its controls;
// and when overall energy gets low, all pitches fall somewhat.
g.free; g = Manta.new;
(
Ndef(\mantaPM, { |lag=1, mod=0.5|
// top slider shifts the root pitch,
// lower slider the spread of the intervals
var sliders = g.buses.slider.kr;
var root = sliders[0].linlin(0, 1, 30, 60);
var spread = sliders[1] * 12;
// all the mod controls
var padBus = LagUD.kr(g.buses[\pad].kr, lag * 0.1, lag) * 2;
var padRows = padBus.clump(8);
// the rightmost 2 controls in every row is pitch mod speed and depth
var freqMods = padRows.collect { |row|
var modSpeed = row[6].linexp(0, 1.8, 0.3, 30);
var modDepth = (row[7] * 2.5).squared;
LFDNoise0.kr(modSpeed, modDepth);
};
var sumBus = padRows.collect(_.sum);
var freqs = ((0 .. 5) * spread + root + freqMods + LFNoise0.kr(0.3, 0.02)).midicps; // 6 notes
var phaseMods = LocalIn.ar(6);
var oscs = SinOsc.ar(freqs * (LagUD.kr(sumBus, lag * 0.4, lag * 4) * 0.5).min(2).pow(0.1), phaseMods);
var allMods = padRows.collect { |row| (row.keep(6) * oscs); };
var modOuts = allMods.collect(_.sum) * mod;
LocalOut.ar(modOuts);
Splay.ar(oscs * AmpComp.kr(freqs.max(50), exp: 0.25) * (LagUD.kr(sumBus, lag * 0.2, lag * 2) * 0.03).min(1)).softclip;
});
Ndef(\mantaPM).play;
)
Ndef(\mantaPM).end;
Storing setups:
/*
Manta.start; g.free; g = Manta.new;
*/
( // Setup1: some actions for upper slider, all pads, top 2 buttons
g.clearDicts;
g.addMulti(\pad, { |...args| "setup1 - Multi pad func: %\n".postf(args) });
g.add(\topButton, 49, { |... x| "setup1 - topButton 49: %\n".postf(x) });
g.add(\topButton, 50, { |... x| "setup1 - topButton 50: %\n".postf(x) });
g.add(\slider, 1, { |... x| "setup1 - slider 1: %\n".postf(x) });
)
// .. try what it does now, maybe add some more ...
// then store that setup:
g.store(\setup1);
( // build a different setup - lower slider, lower topButtons, different pad action
g.clearDicts;
g.addMulti(\pad, { |...args| "\t SETUP2 - Multi pad: %\n".postf(args) });
g.add(\slider, 2, { |... x| "\t SETUP2 - slider 2: %\n".postf(x) });
g.add(\topButton, 51, { |... x| "\t SETUP2 - topButton 51: %\n".postf(x) });
g.add(\topButton, 52, { |... x| "\t SETUP2 - topButton 51: %\n".postf(x) });
g.store(\SETUP2);
)
// .. try what that setup does ... then switch back to setup1:
g.setup(\setup1);
// the current setup is backed up:
g.setup(\backup);
( // use globalActions to switch between setups:
g.usesNoteOn = true;
g.addGlobal(\topNoteOn, 49, { "switch to phasemod.".postln; Ndef(\mantaPM).play; Ndef(\mantaHarm).end(2); });
g.addGlobal(\topNoteOn, 50, { "switch to mantaharm.".postln; Ndef(\mantaPM).end(2); Ndef(\mantaHarm).play; });
g.addGlobal(\topNoteOn, 51, { "switch to setup1.".postln; Ndef(\mantaPM).end(2); Ndef(\mantaHarm).end(2); g.setup(\setup1) });
g.addGlobal(\topNoteOn, 52, { "switch to SETUP2.".postln; Ndef(\mantaPM).end(2); Ndef(\mantaHarm).end(2); g.setup(\SETUP2) });
)
IMPORTANT NOTE:
If you use buses, and want to play back recorded Manta actions while playing new ones live,
the buses of the Manta object will get the newly incoming values.
So for this usage, make your own buses for the patch/setup.
[example to follow]
Manta.setups;
Manta.setups.keys;
Questions, discussion, notes, to do
* how/where to do flexible remapping of keyboard/pad layouts (accordion styles etc etc)?
* try using CtLoop to do flexible gesture recording
- maybe support several layers;
record one layer for one instrument, loop it,
add layer 2, etc