Midi Fighter Twister Class

I just posted my Twister controller class as a quark. You can install via Quarks.install("https://github.com/scztt/Twister.quark")

It’s not very documented (just the Twister class itself), and isn’t really prepared for general public usage, so there may be issues especially when used outside of workflows that look like mine. However, I’ve used this for many performances and it’s basically my core control class, so it’s still quite stable and powerful. You may need to familiarize yourself with the Connection quark, since it makes heavy use of those patterns. I’m totally happy to help with configuration or answer questions about it if needed.

One additional caveat - you’ll need to configure your twister knobs to send relative updates rather than absolute values. The expectation is that the knobs are cc’s 0…15, but think this is the default anyway.

Thank you so much,
I’ve just bought it and will test it soon.

By installing your quark, I’ve got in the post window :
WARNING: MIDIWatcher not found
Is it necessary ?

Many thanks,

Christophe

It probably is neccesary. I may not have published that one, I’ll post it and fix the issue in the next day or so.

Twister is a fantastic and super flexible device, especially for the price :slight_smile: I’ve got two and use them all the time.

Okay - you should now be able to install https://github.com/scztt/MIDIWatcher.quark or update and re-install Twister to get things working.

Hello Scott,

I’m eventually testing the Twister class and I’ve got some questions, even though I still have to read the documentation about the Connection Quark.

  1. By evaluating the example of the Twister Class, I’ve got an error :

ERROR: Message ‘minval’ not understood.
RECEIVER:
Instance of Array { (0x120e85ba8, gc=00, fmt=01, flg=00, set=02)
indexed slots [2]
0 : Integer 100
1 : Integer 5000
}
ARGS:
PATH: /Users/xon/Documents/Projets/SC_LFL/Live4Life/_0 Live Q2.scd

PROTECTED CALL STACK:
Meta_MethodError:new 0x11ca99480
arg this = DoesNotUnderstandError
arg what = nil
arg receiver = [ 100, 5000 ]
Meta_DoesNotUnderstandError:new 0x11ca9b440
arg this = DoesNotUnderstandError
arg receiver = [ 100, 5000 ]
arg selector = minval
arg args = [ ]
Object:doesNotUnderstand 0x11d0dbc80
arg this = [ 100, 5000 ]
arg selector = minval
arg args = nil
ControlSpec:setFrom 0x1204761c0
arg this = a ControlSpec(0, 1, ‘linear’, 0.0, 0, “”)
arg otherSpec = [ 100, 5000 ]
AbstractControlValue:spec_ 0x11ecf7180
arg this = a BusControlValue
arg inSpec = [ 100, 5000 ]
a FunctionDef 0x113dccb38
sourceCode = “{
~amp.spec = \db.asSpec;
~freq.spec = \freq.asSpec;
~formant.spec = [100, 5000];
~bw.spec = [10, 200];
~speak = OnOffControlValue();
}”
a FunctionDef 0x11d35f440
sourceCode = “”
Function:prTry 0x11d62f5c0
arg this = a Function
var result = nil
var thread = a Routine
var next = nil
var wasInProtectedFunc = true

CALL STACK:
DoesNotUnderstandError:reportError
arg this =
< closed FunctionDef >
arg error =
Integer:forBy
arg this = 0
arg endval = 6
arg stepval = 2
arg function =
var i = 6
var j = 3
SequenceableCollection:pairsDo
arg this = [*8]
arg function =
Scheduler:seconds_
arg this =
arg newSeconds = 19.388749358
Meta_AppClock:tick
arg this =
var saveClock =
Process:tick
arg this =
^^ The preceding error dump is for ERROR: Message ‘minval’ not understood.
RECEIVER: [ 100, 5000 ]

probably due ro this :

~c.use {
~amp.spec = \db.asSpec;
~freq.spec = \freq.asSpec;
~formant.spec = [100, 5000];
~bw.spec = [10, 200];
~speak = OnOffControlValue();
};

  1. I do not succeed in changing the Led colors with this code :

~t = Twister(\default);
~t.rows(0).do(.ledColor(Color.rand));

  1. How do tou manage the 4 Banks ?

Many thanks for your help,

Christophe

There was a formatting issue with the second question :

By evaluating this :

~t = Twister(\default);
~t.rows(0).do(.ledColor(Color.rand));

The post window returns this :

Connecting TwisterDevice(-1862386936) to Twister(-825954363)
-> a Twister
-> [ a TwisterKnob, a TwisterKnob, a TwisterKnob, a TwisterKnob ]

But there is no update of Led colors on the Twister.

Many thanks,

Christophe

Hey @Xon
One thing I notice - if you’re JUST executing the code you specified - LED lights are ONLY shown if the knob is actually mapped to a CV. So you would first need to do e.g.

t.rows(0, 0).cv = NumericControlValue(spec:[0, 100]);
t.rows(0, 0).ledColor = Color.red;

If this does not work, a few things you should try for debugging:

  1. Make sure you have the right device connected - I believe the Connecting TwisterDevice(-1862386936) to Twister(-825954363) message codes should correspond to the device ID’s if you list MIDIOut sources.
  2. Try using an app like MIDI Monitor (on OSX), or MIDIFunc.trace to verify that you are receiving signals from your Twister, and that the device id’s and information match with the TwisterDevice.

You MIGHT need to have the knobs of the twister mapped a particular way using the MIDI Fighter config app. I can check in a few days when I have my equipment set up a little better to see what my local config is (I just moved, so I’m mostly a pile of boxes).

Hi Scott,

I think I 've got issues understanding the way connecting the Twister.

When I do :

MIDIClient.sources

I’ve got : MIDIEndPoint(“Midi Fighter Twister 1”, “Midi Fighter Twister”, -1404845322)
since I renamed The Midi Fighter Twister in the MIDI Monitor -> Midi Fighter Twister 1

When I do :

t = Twister(\default);

I’ve got : Connecting TwisterDevice(407605236) to Twister(1091711609),

But nothing seems connected, since I’ve got nothing with MIDIFunc.trace.
I’ve got to do :

MIDIIn.connectAll;

to get info with MIDIFunc.trace.

That is why, by evaluating this :

t.rows(0, 0).cv = NumericControlValue(spec:[0, 100]);

I’ve got this error :
ERROR: Message ‘default’ not understood.
RECEIVER:
Instance of Array { (0x10c725b78, gc=78, fmt=01, flg=00, set=02)
indexed slots [2]
0 : Integer 0
1 : Integer 100
}
ARGS:
PATH: /Users/xon/Documents/Projets/SC_LFL/Live4Life/_0 Live Q3.scd
CALL STACK:
DoesNotUnderstandError:reportError
arg this =
Nil:handleError
arg this = nil
arg error =
Thread:handleError
arg this =
arg error =
Object:throw
arg this =
Object:doesNotUnderstand
arg this = [*2]
arg selector = ‘default’
arg args = [*0]
AbstractControlValue:constrain
arg this =
arg notify = true
AbstractControlValue:init
arg this =
arg initialValue = nil
arg inSpec = [*2]
< closed FunctionDef > (no arguments or variables)
Interpreter:interpretPrintCmdLine
arg this =
var res = nil
var func =
var code = “t.rows(0, 0).cv = NumericCon…”
var doc = nil
var ideClass =
Process:interpretPrintCmdLine
arg this =
^^ The preceding error dump is for ERROR: Message ‘default’ not understood.
RECEIVER: [ 0, 100 ]

Many thanks for your help,

Christophe

Hello,

Today I’m trying to fight again with the MIDI Fighter Twister class.
I try to connect it to SC on Mac, with no success.
Has someone succeeded on Mac and can help me ?

The example of the Twister class posts an error for me, as previously posted.

If I do :

MIDIClient.sources;
// posts -> [ MIDIEndPoint(“Gestionnaire IAC”, “Bus 1”, -63353900), MIDIEndPoint(“Midi Fighter Twister 1”, “Midi Fighter Twister”, -1404845322) ]
MIDIClient.destinations;
// posts -> [ MIDIEndPoint(“Gestionnaire IAC”, “Bus 1”, 1886033969), MIDIEndPoint(“Midi Fighter Twister 1”, “Midi Fighter Twister”, -1777583588) ]

So I try :

TwisterDevice.registerDevice(\myTwister, “Midi Fighter Twister 1”, “Midi Fighter Twister”);
// posts -> TwisterDevice

t = Twister(\myTwister);
// posts something I do not understand
-> Connecting TwisterDevice(-737782931) to Twister(707069498)
-> a Twister

t.rows(0, 0).cv = NumericControlValue(spec:[0, 100]);
// posts an error :
ERROR: Message ‘default’ not understood. … RECEIVER: [ 0, 100 ]

Many thanks,

Christophe

First: Try spec: [0, 100].asSpec – probably the method ought to be changed to do that automatically, but the quark author may have been unaware of that loose convention.

Second: Please enclose your code snippets in triple-backticks.

```
code here
```

hjh

Many thanks, James!

I am doing something wrong by registering it… (as explained in my previous mail).

For example, the code below does nothing :

~t.rows(0).do(_.ledColor_(Color.rand)); 
// posts -> [ a TwisterKnob, a TwisterKnob, a TwisterKnob, a TwisterKnob ]

Second, I’ve got got a new error by evaluating the example and adding .asSpec to the 2 arrays of the example.

So below the example code and the error :

(
s.waitForBoot {
    ~t = Twister(\default);

    // Set up some control values we want to connect the twister to.
    // See help for Connection library or BusControlValue for more details.
    ~c = ControlValueEnvir(BusControlValue);
    ~c.use {
        ~amp.spec = \db.asSpec;
        ~freq.spec = \freq.asSpec;
        ~formant.spec = [100, 5000].asSpec;
        ~bw.spec = [10, 200].asSpec;
        ~speak = OnOffControlValue();
    };

    // Map our control values to knobs and buttons
    ~t.rows(0, 0).knobCV = ~c.amp;
    ~t.rows(0, 1).knobCV = ~c.freq;
    ~t.rows(0, 2).knobCV = ~c.formant;
    ~t.rows(0, 3).knobCV = ~c.bw;

    ~t.buttons[0].cv = ~c.speak;


    // Set some random colors
    ~t.rows(0).do(_.ledColor_(Color.rand));

    // Play a simple synth
    Ndef(\tone, {
        |freq = 100, amp = 0, formant = 100, bw = 100, t_trigger|
        var sig;
        sig = Formant.ar(freq, formant, bw, amp.dbamp);
        sig = sig + (WhiteNoise.ar(0.1) * Env.perc().kr(gate:t_trigger));
    }).play;

    // Map our control values to synth params.
    Ndef(\tone).map(
        \amp,         ~c.amp.asMap,
        \freq,         ~c.freq.asMap,
        \formant,     ~c.formant.asMap,
        \bw,         ~c.bw
    );

    // Map our button to something arbitrary
    ~c.speak.signal(\on).connectToUnique({
        "Pushing a button".postln;
        Ndef(\tone).set(\t_trigger, 1);
    });

    // Set some initial values.
    // Note that this should update the display on the Twister device if it is connected.
    ~c.setValues((
        freq: 370,
        amp: -10,
        formant: 1000,
        bw: 300
    ));
};
)

ERROR: Message ‘connectToUnique’ not understood.
RECEIVER:
Instance of UpdateDispatcherItem { (0x1693f06b8, gc=98, fmt=00, flg=00, set=02)
instance variables [3]
dependants : instance of IdentitySet (0x1693f0648, size=2, set=2)
object : instance of OnOffControlValue (0x129b85498, size=11, set=4)
key : Symbol ‘on’
}
ARGS:
Instance of Function { (0x1693f9ba8, gc=98, fmt=00, flg=00, set=02)
instance variables [2]
def : instance of FunctionDef - closed
context : Frame (0x123965658) of Interpreter:functionCompileContext
}
PATH: /Users/xon/Documents/Projets/SC_LFL/Live4Life/_Init Midi Twister.scd

Many thanks,
Christophe

And if it can help to help me to connect this controller :

MIDIFunc.trace

posts this as ID :

MIDI Message Received:
type: control
src: -1404845322
chan: 0
num: 12
val: 110

Christophe

I don’t know these classes at all.

The spec-related error is pretty common, so I made a reasonable guess about it.

That guess got you past that error into something much more specific to this class framework. I’ve got no idea about that.

Anyone else?

hjh

It looks like you have an out of date version of theConnection quark - both the .asSpec conversion and connectToUnique should be present in the current latest. You should be able to update from the quarks UI Or using something like Quark("Connection").update.

Many thanks, Scott!

By updating it specifically, the example works now without error (strange, because I am sure I had updated all the Quarks via the the Quarks window…).

Anyway, the controller still does not react…

Do you have an ideas of what I am doing wrong for registering it as written in my previous mails ?

Thanks

Christophe

By evaluating the example, it posts :

Connecting TwisterDevice(-425842367) to Twister(-1519646368)

The controller does not react. By evaluating it again :

Connecting TwisterDevice(-425842367) to Twister(1820904612)

What is weird is that :

MIDIFunc.trace;

gives apparently a different iD. But I do not know how to change it in your class…

MIDI Message Received:
type: control
src: -1404845322
chan: 0
num: 12
val: 28

The quarks system is not reliable about updating to specific versions, and I release versions of Connection semi-often, so it’s probably not your fault - just a glitch in the space-time fold.

Two things to check:

  1. The Twister class expects that the device is configured to send relative messages - you can set these in the MIDIFighter Utility by setting Encoder MIDI Type to Enc 3FH/41H. It looks from your trace log that it’s probably not configured this way (the values should be 63/65), so this is probably the issue.
  2. The pre-configured devices are \default and \secondary, which are linked to MIDI devices named Midi Fighter Twister 1 and Midi Fighter Twister 2. If your device is for some reason not showing up with these names, you can rename it in the MacOS MIDI/Audio application, or you can point to another name by doing:
    ~twister = Twister(TwisterDevice(\mydevice, MIDIClient.sources[0]))
    … with the correct source for your device. My device is the second item in the sources list, so this works immediately for me:
~t = Twister(TwisterDevice(\mine, MIDIClient.sources[1]));
~t.knobs[0].cv = NumericControlValue(spec:[0, 1]);
~t.knobs[0].ledColor = Color.red;

Finally, the device should be totally blacked out and unresponsive until you’ve set at least one CV (only knobs with CV’s attached will light up and behave).

Many thanks, Scott!
I can now make it work, your quark is so cool!

Just a few remarks and questions :

  1. Even though I had renamed the controller Midi Fighter Twister 1 in the MacOS MIDI/Audio application,
~t = Twister(\default);

does nothing.
But by specifying the source, like you indicate me, it works well :

~t = Twister(TwisterDevice(\mine, MIDIClient.sources[1]))

My sources are :

[ MIDIEndPoint(“Gestionnaire IAC”, “Bus 1”, -63353900), MIDIEndPoint(“Midi Fighter Twister 1”, “Midi Fighter Twister”, -1404845322) ]

  1. You seem not to use the 4 banks, or the super knob ?
    Would you recommend specific configurations in the Midi Fighter Utility concerning global settings, sensitivity, excluding Encoder Midi Type ?

  2. In the Twister example, you use ControlValueEnvir(BusControlValue), which seem not documented in the Connection Quark.
    I would like to know more about the types of ControlValue, e.g. the difference and advantages between a NumericalControlValue and a BusControlValue.
    I have now to dig into the Connection Quark and how to best use it for MVC.

  3. In the Twister example, .asMaps is missing in the Ndef \bw, ~c.bw .

Many thanks,
Christophe

Glad it’s working, and thanks for being so patient! This was pretty untested outside of my own usage.

The problem you’re seeing is one I see occasionally also - it seems to be possibly related to some kind of general MIDI initialization problem in SuperCollider, but I can’t reliably reproduce it? The best thing to do is just add a line for your TwisterDevice setup in your startup file - after you’ve run it once, you can refer to that device by name (e.g. \mine) when setting up Twister instances.

A TwisterDevice is a Singleton that corresponds to one actual physical device. You can have as many Twister instances as you’d like, and you can connect and disconnect them from devices as needed (only the connected Twister will update the controller, and receive control changes) - this is a replacement for the Twister bank behavior:

~bank1 = Twister(\mine);
~bank2 = Twister(\mine);
~bank3 = Twister(\mine); // this one is now connected, because Twister objects are connected when they are constructed

~bank1.connect(\mine); // connect ~bank1 - now these cv's will display and respond - the other banks will not

The super-knob doesn’t change the physical accuracy of the knob, only multiplies each tick so it increments/decrements the value faster. Since the Twister class is using relative cc’s, this has no effect. You can change the increment scale via e.g. Twister(\mine).knobs[0].knobScale = 0.1 (the default is 0.05).

ControlValueEnvir is a subclass of EnvironmentRedirect (which in turn behaves like an environment) where values are implicitly created when referred to. The class of the objects that are implicitly created is passed as an argument to the constructor, so ControlValueEnvir(BusControlValue) will create BusControlValues. If you’ve ever used ProxySpace, this behaves almost identical, but instead of creating NodeProxy's it creates a control value of the type you specify. There’s one specific use-case that makes this class very good:

~c = ~c ?? { ControlValueEnvir(BusControlValue) };
~c.use {
    ~freq.spec = [20, 20000];
    ~amp.spec = [-90, 0];
};
~t.knobs[0].cv = ~c[\freq];

In this case, ~freq and ~amp are implicitly created as BCV’s when they are referred to, and their specs are set. There’s a very good syntactic reason for this: you can change your specs or even add new CV’s and re-execute the above statement and the cv’s will be updated, but are not re-created - and will remain connected to any UI / controllers.

CVE has a few convenience methods to e.g. set all values from a Dictionary, ~c.setValues((freq:100, amp:-10)), convert them to a Pbind of the values (.asPbind), or turn them into an array of Synth arguments (~c.asSynthArgs).

BusControlValue is just a NumericControlValue that has an associated Bus that gets updated with the value. They can be used interchangeably, but BCV is more convenient and efficient if you’re going to map it to a synth parameter.

Note also that both Ndef(\foo).set(\freq, ~c.freq) and Ndef(\foo).set(\freq, ~c.freq.asMap) are valid. The first sets the value ONCE, the second actually maps the value to the Synth input.

Sorry, one last hidden detail. Twister buttons automatically have an OnOffControlValue set as their CV. So, usage looks like this:

~t = Twister(\mine);
~t.buttons[0].toggle = true;    // is the button a toggle button, or is down==on and up==falsee?
~t.buttons[0].cv.signal(\on).connectToUnique {
    "i'm on".postln;
};
~t.buttons[0].cv.signal(\off).connectToUnique {
    "i'm off".postln;
};
~t.buttons[0].cv.signal(\value).connectToUnique {
    |...args|
    "ive changed: %".format(args).postln;
};