Trigger Tidal code with MIDI

Then let me try to help you on the last distance! First, please make sure you are using the code from my GitHub issue.

The difference between cF and cI is that cF expects a float value from the OSC message and cI expects an integer value. If we assume that you send a value between 0-127 via a cc number, then cI would be the function of choice. If you want to send a value between 0-1 as float, than you would use the function cF.

And if I understand you right, then it looks like your MIDI controller sends note on and note off messages instead of cc values. I suggest that you send the note number as values via note on in Supercollider. A middle c has a value of 60, so I would probably calculate the offset and start from c. That's why I would set the starting value to 0 so that you can switch patterns from the middle c and above. And to avoid negative values, I use abs to always receive positive values, even if you trigger a lower note then the middle c.

The SuperCollider for this case would look like this:

(
var on;
var osc;

osc = NetAddr.new("127.0.0.1", 6010);

MIDIClient.init;
MIDIIn.connectAll;

on = MIDIFunc.noteOn({ |val, num, chan, src|
	(num-60).abs.postln;
	osc.sendMsg("/ctrl", "notes", (num-60).abs);
});
if (~stopMidiToOsc != nil, {
	~stopMidiToOsc.value;
});

~stopMidiToOsc = {
	on.free;
};
)

// Evaluate the line below to stop it.
~stopMidiToOsc.value;

Is that what you want to achieve?

Fear I may have misspoke in the above post. Was trying to explain that my controller's buttons are not hard clickable ie. when depressed they return back to their initial state. Here is the SC code I have been using (well one of them anyway):

(
	(
OSC
		MIDIClient.init;
		MIDIIn.connectAll;
		~tidalSocket = NetAddr("127.0.0.1", 6010);
		~notes=[];
		MIDIFunc.cc({|val, num, chan, src| ~tidalSocket.sendMsg('/ctrl', num, val/127.0);}, nil, nil, nil);
		MIDIFunc.noteOn({|veloc, num, chan, src| ~notes=~notes.add(num); ~notes=~notes.sort({|a,b| a>b}).reverse; ~tidalSocket.sendMsg('/ctrl', "notes", format("%", ~notes-60)); ~notes.postln;});
		MIDIFunc.noteOff({|veloc, num, chan, src| ~notes=~notes.takeThese({|x| x==num}); ~tidalSocket.sendMsg('/ctrl', "notes", format("%", ~notes-60)); ~notes.postln;});
	);
);

which prints an output of:
[ 36 ]
[ 36, 36 ]
[ 36, 36, 36 ]
[ ]
[ ]
[ ]
(for the button I am initially trying) in the post window. The 36s being pressed/on and the [ ]s being depressed/off. I have been successfully been using "cF 1 36" with capply to change patterns when pressed, returning to initial pattern when depressed

Using the SC code you provided:
24
is printed in the post window. Which seems like a step in the right direction as there is no output printed on release.

I have spent a little time substituting the 24 for the 36 in various capply cF and cIs to try to alter the 0 in:
d1 $ capply (cI 0 "test") pvector
but still no success.
Manually changing the 0 switches patterns in the defined list as desired.

Thank you for your patience!

Allright, I think I recognize your problem. For this reason I will describe this in more general terms.

First of all you should not send a list, but only a single value. In this way a Pattern remains active until you press another note, but you have to ignore the noteOff.

var on;

on = MIDIFunc.noteOn({ |val, num, chan, src|
	(num-60).abs.postln;
	osc.sendMsg("/ctrl", "notes", (num-60).abs);
});

Let's analyse the osc.sendMsg function:

  • "/ctrl" is an osc adress (keep this - tidal is listening to this address)
  • "notes" is the name of the control
  • (num-60).abs is the value which will be applied

And now the tidal side:

d1 $ capply (cI 0 "notes") pvector 

The function cI has two parameter:

  • 0 as the first parameter is the default value. That's why you could write something like d1 $ capply (cI 36 "notes) pvector and it would be totally fine.
  • "notes" is the name of the control we defined in SuperCollider. In this case the name is fixed.

But it is necessary that you use this capply version:

import qualified Data.Vector as V

capply condpat pvector =  
      condpat >>= \ i -> pvector V.! (mod i (V.length pvector))

Because the first two version in this thread worked in a different way.

Does that solve your problem?

@thgrund Some progress. Thanks. I do feel like a bit of an idiot not realising that the "test" in your prior example needed to be tied to a definition in SC... but it seems that wasn't the only issue with my configuration.

Here is the modified SC script:

(
var on, cc;
var osc;

osc = NetAddr.new("127.0.0.1", 6010);

MIDIClient.init;
MIDIIn.connectAll;

on = MIDIFunc.noteOn({ |val, num, chan, src|
	(num-60).abs.postln;
	osc.sendMsg("/ctrl", "notes", (num-60).abs);
});

cc = MIDIFunc.cc({ |val, num, chan, src|
	osc.sendMsg("/ctrl", num.asString, val/127);
});

if (~stopMidiToOsc != nil, {
	~stopMidiToOsc.value;
});

~stopMidiToOsc = {
	on.free;
	cc.free;
};
)

// Evaluate the line below to stop it.
~stopMidiToOsc.value;

and the Tidal side of things:

import qualified Data.Vector as V

capply condpat pvector =  
      condpat >>= \ i -> pvector V.! (mod i (V.length pvector))

pattern1 = s "bd*4" 
pattern2 = s "cp*4"
pattern3 = s "808*4"
pattern4 = s "hh*4"

let pvector = V.fromList [pattern1, pattern4, pattern3, pattern2]

d1 $ capply (cI 0 "notes") pvector

This allows switching between patterns, however every button on the controller actively changes the pattern (in this case horizontally across the controller's 4 channels/strips.
Changing cI 0 to any number (36 or otherwise) does not affect this behaviour.

Is there a way to specify the input numbers that affect pattern change? I am trying to have 4 vertical buttons on each channel/strip of the controller change the pattern for a given channel/strip. Channel/strip 1's inputs are:
24
28
32
36

If it is possible to get this working I imagine a follow up issue will be if it is possible to define multiple pvector lists? I just tried changing the name to pvector2 which didn't work. I assume, therefore that it is a fixed Haskell function (this is my first experience with Haskell so know next to nothing). I was however able to get to 2 lists/channels running by adding a capply2 and changing sounds in the pattern list, but obviously without the buttons being limited each pattern changes with a button press.

If this is getting to be all too much for you feel free to tap out at any time. I really appreciate the assistance you have given thus far. Learning Tidal is proving to be a lot of fun and if I wasn't doing so for a project involving live collaboration with others, where I need to be more responsive than typing allows (atm), I wouldn't be so concerned with controller mapping.

Yes it seems to be. Nevertheless I guess it could be useful to extend the functionality I provide so far and to implement an optional direct mapping of osc value to pattern key.

The simple (but technical) answer is: I use a vector (or array in other languages) with four elements. Each of the elements belongs to an index between 0 and 3. And to not trigger an index that does not exist (e.g. 26, 37, 12 etc.), I calculate modulo from the OSC value. So the math here with your example values is:

24%4 = 0
28%4 = 0
32%4 = 0
36%4 = 0

So in your case you always trigger the first index (0) of the vector.

I chose this approach because it is so beautifully simple and generic and requires no further configuration. If you use a MIDI-Keyboard with c as 0 than you would achieve this, if you trigger each key of the keyboard one by one:

0%4 = 0
1%4 = 1
2%4 = 2
3%4 = 3
4%4 = 0
5%4 = 1

I think you get the idea. So technically you need another data structure like a map, I guess. In this way you can explicity set key/value pairs. And it would look like this:

import qualified Data.Map as M

capply condpat pmap =  
      condpat >>= \ i -> M.findWithDefault 0 i pmap

pattern1 = s "bd*4" 
pattern2 = s "cp*4"
pattern3 = s "808*4"
pattern4 = s "hh*4"

pMap = M.fromList([ (24, pattern1), (28, pattern2), (32,pattern3), (36,pattern4) ])

d1 $ capply (cI 24 "notes") pMap

This works pretty well for me and I don't see a problem with it. For vectors:

let pvector1 = V.fromList [pattern2, pattern3]

let pvector2 = V.fromList [pattern1, pattern4]

d1 $ capply (cI 0 "notes") pvector1

d2 $ capply (cI 0 "notes") pvector2

And for maps:

pmap1 = M.fromList([ (24, pattern1), (28, pattern2) ])

pmap2 = M.fromList([ (24, pattern3), (28, pattern4) ])

d1 $ capply (cI 24 "notes") pmap1

d2 $ capply (cI 24 "notes") pmap2

So maybe the mapping solution solves all your problems?

All good. In the end I learn something and expand my own understanding of the benefits and problems of all these topics. :slight_smile:

2 Likes

@mrreason Is late here so no energy to test this out right now but from my cursory understanding of the above seems you have solved my problems. Will report back tomorrow. You, sir, are a god :slight_smile:

@mrreason Spoke too soon (see below). Problem now is that the push of any any other button (outside of the defined map) stops the playing pattern. So if I set up multiple maps over additional channels the changing of one stops the others from playing. I'm not sure how to overcome this as the code is obviously listening to the SC script which replaces the current value with the new input. I assume it may be possible to set multiple "on" listeners in SC (notes1, notes2, notes3, notes4) and limit the values they listen... for but this is beyond me.

"YES! Thank you so much! Works like a charm. Know where the rest of my day is going now... figuring out how best to configure this in function files, getting active pattern happening and re-configuring my existing files."

1 Like

Actually I have to think about it again. I have another potential solution that offers more flexibility in mapping, but also makes the configuration process more complex.

In any case, I really believe that the easiest way is via SuperCollider (as you mentioned above). You can't "memorize" what the last pattern was in Haskell because you have no side effects.

(
var on,osc;

var dict = Dictionary.new;

dict.put("notes1",  Set[60,61]);
dict.put("notes2", Set[59,58]);

osc = NetAddr.new("127.0.0.1", 6010);

MIDIClient.init;
MIDIIn.connectAll;

on = MIDIFunc.noteOn({ |val, num, chan, src|
	dict.keysDo { |key| if (dict.at(key).findMatch(num).notNil, {
		osc.sendMsg("/ctrl", key, num);
		[key,num].postln;
	}) };
});

if (~stopMidiToOsc != nil, {
	~stopMidiToOsc.value;
});

~stopMidiToOsc = {
	on.free;
};
)
// Evaluate the line below to stop it.
~stopMidiToOsc.value;

So now you have to do mapping on both sides, but you have more control.

And the tidal side:

capply condpat pmap defaultPattern =  
      condpat >>= \ i -> M.findWithDefault defaultPattern i pmap
      
pattern1 = s "bd*4" 
pattern2 = s "[~ cp]*2"
pattern3 = s "808(3,8)"
pattern4 = s "hh*8"
                   
pmap1 = M.fromList([ (60, pattern1), (61, pattern2) ])

pmap2 = M.fromList([ (59, pattern3), (58, pattern4) ])

d1 $ capply (cI 60 "notes1") pmap1 pattern1

d2 $ capply (cI 59 "notes2") pmap2 pattern3

I added a defaultPattern parameter to the TidalCycle function capply (just in case). But because I only send defined values from SuperCollider to TidalCycles at all, there should no gaps/silence any more.

2 Likes

@mrreason That seems to have done the trick. Was deep in the SC docs seeing if I couldn't figure it out for myself. So thanks.

Has done something weird with the number order but that is no issue as only needs to be configured once. Also the SC code above has one too many opening brackets... again no issue just for other peoples' reference.

I have corrected the thing with the brackets, thanks for the hint! But what do you mean with the weird order of the numbers? Sorry I don't get it :sweat_smile:

Setting: dict.put("notes", Set[29,33]);
gives a post of:
27
27
27
27
[ notes, 33 ]
31
31
31
31
[ notes2, 29 ]

So 33 is now connected to button 27 and 29 to 31....

Now that I think about it it may be because I haven't restarted all day and additional SC scripts may still be running...

That sounds very much like it. Restarting should do the trick. So maybe you should always use ~stopMidiToOsc.value; before you evaluate the code block, because this will free all functions from the server and you don't need to restart everything.

Yep realised I should have stopped OSC after posting :wink:

Happy to hear that you can work now and have fun with the coding!

1 Like

i've tried this, but got

Could not find module ‘Data.Vector’
Perhaps you meant Data.Functor (from base-4.10.1.0)

so, after searching on stackoverflow, i ran cabal install vector
and now every time i try to boot tidal i am greeted by

t> 
: error:
    Could not load module ‘System.IO’
    It is a member of the hidden package ‘base-4.18.0.0’.
    You can run ‘:set -package base’ to expose it.
    (Note: this unloads all the modules in the current scope.)
t> [GHC-88464] Variable not in scope: d1 Suggested fix: Perhaps use ‘dt’ (imported from Sound.Tidal.Context)

even after re-running the install script.
nuking everything haskell-related from my harddrive and re-installing fixed it.

who knows what breaking changes have been made in the last years that result in this...
in any case, it would be nice to add a warning to this particular post for us users from the relative future.