Using VSTPlugin with Tidal

Thanks for the updates @mrreason!

I'm trying to get up and running on an M1 machine and having some struggles, don't know if you've had similar issues; Building SuperCollider (and plugins) on Mac M1 - #25 by jarm - Development - scsynth

Edit: Build issues solved: Testing on Apple Silicon (Mac Mini M1) (#137) · Issues · Pure Data libraries / vstplugin · GitLab

1 Like

When I add the example TidalVST.scd to my Boot.hs like so:

s.waitForBoot {
	~dirt = SuperDirt(2, s);
	~dirt.start(57120, [0, 2, 4, 6, 8, 10, 12, 14]);

I get this syntax error on boot (referring to TidalVST/TidalVST.scd at main · thgrund/TidalVST · GitHub):

ERROR: syntax error, unexpected '(', expecting $end
  in interpreted text
  line 20 char 1:

  var diversions = ();
ERROR: Command line parse failed

Another thing I spotted comparing TidalVST.scd to MrReasonsSetup.scd is:

var serverMessage = { |synth|
	[\out, ~out, \sustain, ~sustain] { |each|
			-1, // no id
			1, // add action: addToTail\synthGroup), // send to group
			*each.asOSCArgArray // append all other args

Shouldn't that \VST be replaced with synth, even for this demo example?

Thank you @superpeachman!

I believe one of the significant unique points of using Tidal is that it is easy to trigger a massive number of parameters simultaneously with a simple code.

I agree! I feel like I'm still at the beginning of TidalVST, even if you can change parameters and busses.
But you can already experiment with it well.

I know you didn't ask a question, but I wanted to take the chance to say that\dexed).gui in SuperCollider helps to start with the parameter mapping. Found this out myself only recently

You can map the parameter 0 from the VST plugin with varg0 = pF "varg0" for example.

I think we can dive deeper into legendary synthesizers by combining them with Tidal VST.

I would find that extremely exciting :slight_smile:


I'm glad to hear that! I have not yet switched to M1, so thanks for the tip. I haven't encountered this problem yet, but I'm glad to hear that it's been fixed.

Another thing I spotted comparing TidalVST.scd to MrReasonsSetup.scd is:

For me it looks like that in MrReasonsSetup everything is fine but I updated it for TidalVST.scd - thanks!

I get this syntax error on boot

You need to remove the brackets in the scd file and store the synthdef in the global synthdef store (or you separate TidalVST.scd and the Synthdef in two files and load them).
At least it worked for me.

1 Like

Not sure if this is helpful at this point but this is my current setup for playing VST with tidal :slight_smile:


not sure if i missed this somewhere in the thread, but would setting this up and doing this make it possible to use effect vsts in tidal?

Unfortunately this isn't implemented right now. I had this on my to do list. It should be possible in theory and I should know everything to realize it. But I am quite unsure when I will be able to do it.

It hasn't been a big priority for me because I primarily use reverb and delay effects as effect busses in Ableton and adjust the dry/wet value via my custom functions "reverb" and "delay".


Thanks for letting me know. Great work anyways...

Hi - I'm trying to find a solution for vst effects. As @mrreason points out, TidalVST currently works for vst instruments only, where the vst plugin becomes the "sound" in a Tidal pattern
h1 $ n "0" # sound "vst" etc. I don't have a working solution (yet) and unfortunately, my SuperCollider programming ability is currently at a beginner's level. So I'm seeking help!

My solution approach is based on "adding-effects.scd" which is found in the SuperDirt/hacks folder or in the SuperDirt GitHub hacks .

  • Tidal: use sound source as sample or synth registered in SuperDirt
  • BootTidal.hs: add vstFXName for parameter mapping
  • Tidal pattern: d1 $ s "bd hh bd sn" #vstFXName "FabFilter Pro-Q 3.vst3" #varg20 0.5 #varg30 500
  • SuperCollider: add a SuperDirt module which responds to the OSC event from Tidal (using the model of effects like squiz, hpf, waveloss, etc)
  • SuperCollider: add a SynthDef to instantiate VSTPlugin and call the VSTPluginController with the vstFXName value. Load the specific vst fx plugin, and launch the editor.

Below is code I'm working on. ~dirt.addModule loads fine, but SynthDef errors. I'm looking for some help - either to fix the SC code, OR - to point me/us in a different direction.

I also tried a solution where I have 2 SynthDefs - one called from ~dirt.addModule to Instantiate VSTPlugin, and the 2nd to handle the VSTPluginController.

~dirt.addModule('vstFXPlugin', { |dirtEvent|
	dirtEvent.sendSynth('dirt_vstFXPlugin' ++ ~dirt.numChannels,
			vstFXName: ~vstFXName,
			out: ~out
}, { ~vstFXName.notNil });

// Won't load - ERROR: Message 'defName' not understood.
SynthDef("dirt_vstFXPlugin" ++ ~dirt.numChannels, { |out, vstFXName|
	var sig =, ~dirt.numChannels), ~dirt.numChannels);
	var fx =, [\bus, out]);, editor: true, verbose: true);
	fx.editor;, sig)
}, [\ir]).add;

Hey, VSTPluginController does not belong in a SynthDef. The purpose of a SynthDef is to define a graph of unit generators, but VSTPluginController needs an actual instance of a Synth on the Server.

Thanks @Spacechild1 for the input. I can easily remove the VSTPluginController code and have the SynthDef just handle the VSTPlugin. However, the problem with having VSTPluginController in an isolated code block is that it doesn't meet the requirement to load a plugin dynamically from Tidal. In a live-coding environment, the user should be able to define a new plugin name as a parameter and value, and not have to leave Tidal and jump into SC code to get it loaded. For the built-in effects in SuperDirt, we add, change values, remove these at will during a livecode session/performance with just a few key strokes in Tidal. I'm looking for the same results here.

I haven't used TidalCycles, so I don't know where the VSTPluginController method calls should go; all I can say is that the UGen graph function is certainly the wrong place.

dynamically loading and unloading plugins from tidal in the same way superdirt effects get applied is not something i would recommend under any circumstances. there's an enormous overhead to instantiating a VST plugin instance compared to instantiating a predefined synth in SC. simply put, it takes a while for a plugin to load, let alone load any kind of preset/state, and in the context of music even a few dozens of milliseconds are simply too long. the more sensible option would be to predefine a signal path via SynthDefs that contain "slots" for VST plugins in the form of ugens, and then load up a few plugins we like.

regarding sending MIDI and changing parameters from tidalcycles, you can check out this startup file - it's a stripped down version of what i'm currently using to control VST plugins with tidal. it's also possible to layer the sounds up with superdirt, but it's not a full superdirt implementation of VST plugins, so you can't apply djf or squiz to your vst instruments in this way. however, other people like @mrreason have done work on how to achieve that, so it's definetly possible!

s.reboot { // server options are only updated on reboot
	// configure the sound server: here you could add hardware specific options
	// see
	s.options.numBuffers = 1024 * 256; // increase this if you need to load more samples
	s.options.memSize = 8192 * 32; // increase this if you get "alloc failed" messages
	s.options.numWireBufs = 64; // increase this if you get "exceeded number of interconnect buffers" messages
	s.options.maxNodes = 1024*32; // increase this if you are getting drop outs and the message "too many nodes"
	s.options.numOutputBusChannels = 2; // set this to your hardware output channel size, if necessary
	s.options.numInputBusChannels = 10; // set this to your hardware output channel size, if necessary
    s.options.maxLogins = 8; // optional, required to enable feedforward VU meter
	// boot the server and start SuperDirt
	s.waitForBoot {
		~dirt = SuperDirt(2, s); // two output channels, increase if you want to pan across more channels
        ~dirt.doNotReadYet = true; // load samples on demand, memory saving
		//~dirt.loadSoundFiles;   // load samples (path containing a wildcard can be passed in)
		// for example: ~dirt.loadSoundFiles("/Users/myUserName/Dirt/samples/*");

		// s.sync; // optionally: wait for samples to be read
		~dirt.start(57120, 0 ! 12);   // start listening on port 57120, create two busses each sending audio to channel 0

        // optional, enable feedforward vu meter

		// optional, needed for convenient access from sclang:
			~d1 = ~dirt.orbits[0]; ~d2 = ~dirt.orbits[1]; ~d3 = ~dirt.orbits[2];
			~d4 = ~dirt.orbits[3]; ~d5 = ~dirt.orbits[4]; ~d6 = ~dirt.orbits[5];
			~d7 = ~dirt.orbits[6]; ~d8 = ~dirt.orbits[7]; ~d9 = ~dirt.orbits[8];
			~d10 = ~dirt.orbits[9]; ~d11 = ~dirt.orbits[10]; ~d12 = ~dirt.orbits[11];

		  // midi

			~midithroughport0 = MIDIOut(0);
            ~midithroughport0.latency = 0.0;
            ~dirt.soundLibrary.addMIDI(\midi0, ~midithroughport0);

			~midithroughport1 = MIDIOut(1);
            ~midithroughport1.latency = 0.0;
            ~dirt.soundLibrary.addMIDI(\midi1, ~midithroughport1);

//,['/home/ribon/.wine/drive_c/Program Files/Steinberg/VstPlugins']);
//,['/home/ribon/.wine/drive_c/Program Files/Native Instruments/VSTPlugins 64 bit/']);;

		~reverbBus =,2);
		~masterBus =,2);

		//OPTIONAL: redirect superdirt orbits, would require instantiating buses, adding an to the "voice" synthdef and passing the bus names as arguments. 
		//~dirt.orbits[0].outBus = ~kickBus;
		//~dirt.orbits[1].outBus = ~snareBus;
		//~dirt.orbits[2].outBus = ~hatBus;
		//~dirt.orbits[3].outBus = ~percBus;
		//~dirt.orbits[4].outBus = ~bassBus;


				var sound;
				sound =,2);
				sound =,2,id:\masterchain);
				sound = sound.tanh; // TANH ON THE MASTER YEEEEAAAAAHHH,sound);

				var sound;
				sound =,2);
				sound =,2,id:\reverb);,sound);


			SynthDef("voice",{ | out, level = 1, sendAmount = 0 |
				var sound, chain;
				sound =;
				sound = sound +,2,id:\generator);
				sound =,2,id:\channelstrip);,sound * level);, sound * sendAmount);


		//Now comes the ugly part - in order to stop SuperDirt from flooding the post window with "sound x not found" messages,
		//we need to define "dummy" synthdefs with names matching all instances of the "voice" synthDef we want to use
		//alternatively, we could reate a separate synthDef for each VST plugin instance we want to use 
		//also note that any synthdefs and sample folders loaded by superdirt sharing the same name will get played together
		//my solution is to not load any default superdirt samples, alternatively you can use different names for your VSTs
			//this synthdef basically does nothing except immediately freeing itself. CPU impact should be almost zero. 
                        SynthDef("kick",{ | out |
                                var u;
                                u =;
                        SynthDef("snare",{ | out |
                                var u;
                                u =;
                        SynthDef("hat",{ | out |
                                var u;
                                u =;
                        SynthDef("perc",{ | out |
                                var u;
                                u =;
                        SynthDef("bass",{ | out |
                                var u;
                                u =;
                        SynthDef("keys",{ | out |
                                var u;
                                u =;

			~synths = Dictionary [
				\masterBus -> Synth("MasterBus"),
				\kick -> Synth("voice",[out:0]),
				\snare -> Synth("voice",[out:0]),
				\hat -> Synth("voice",[out:0]),
				\perc -> Synth("voice",[out:0]),
				\bass -> Synth("voice",[out:0]),
				\keys -> Synth("voice",[out:0]),
				\reverb -> Synth("ReverbBus"),
			~instruments = Dictionary [
				\kick -> VSTPluginController.collect(\kick)),
				\snare -> VSTPluginController.collect(\snare)),
				\hat -> VSTPluginController.collect(\hat)),
				\perc -> VSTPluginController.collect(\perc)),
				\bass -> VSTPluginController.collect(\bass)),
        			\keys -> VSTPluginController.collect(\keys)),
				\masterBus -> VSTPluginController.collect(\masterBus)),
        			\reverb -> VSTPluginController.collect(\reverb)),

			~oscfunc = OSCFunc({ |msg, time, tidalAddr|
				var dict = (), timeDelta; // Create dictionary
				if (\n).isNil, {dict[\n] = 0});

				timeDelta = time - Main.elapsedTime;

				//it took me a while of playing around to find this value, it might be different for you.
				timeDelta = timeDelta - 0.3; //server latency + dict[\shift];

				/// adding a midi play function
				if ( ~instruments[dict[\s]].isNil == false ,{
					thisThread.clock.sched(timeDelta, {(\type: \vst_midi,\vst: ~instruments[dict[\s]].generator,
						\midicmd: \noteOn,
						\chan: 0,
						\amp: dict[\amp],
						\midinote: dict[\n] + 60,
						\dur: dict[\delta],
					//param automation - needs stacks in tidal for multiple params since its 1 per "voice"/msg/event bundle
					if (\param).notNil == true &&\paramval).notNil == true &&\paramtarget).notNil == true ,{
						thisThread.clock.sched(timeDelta, {~instruments[dict[\paramtarget]].generator.set(dict[\param].asInteger,dict[\paramval]);


			}, '/dirt/play', NetAddr(""), recvPort: 57120 /*3337*/);




			~instruments[\kick]"Vital.vst3", editor: true, verbose: false, multiThreading: true);
			~instruments[\snare]"Vital.vst3", editor: true, verbose: false, multiThreading: true);
			~instruments[\hat]"Vital.vst3", editor: true, verbose: false, multiThreading: true);
			~instruments[\perc]"Vital.vst3", editor: true, verbose: false, multiThreading: true);
			~instruments[\bass]"Vital.vst3", editor: true, verbose: false, multiThreading: true);
			~instruments[\keys]"Vital.vst3", editor: true, verbose: false, multiThreading: true);
			~instruments[\reverb]"VCV Rack 2.vst3", editor: true, verbose: false, multiThreading: true);

			//loading presets - but this will crash unless you have already saved presets with these names!!
			"PRESETS LOADED!!!".postln;


	s.latency = 0.333; // increase this if you get "late" messages

for my personal use, i have defined a "voice" as being 2 VST plugins in series, with the second processing the output of the first before passing it on to 2 outputs, one of which goes into a separate reverb Synth i also defined, and finally a "master" track that can process everything together.

after creating these synth definitions, i then instantiate a couple of named instances of these synths. some of them contain multiple VST plugin slots, others just one.
in any case, i can use VSTPluginController.collect to create an object that allows me to access all instances that live within a single synth, and address these separately by their ID.
after that is done, the script then loads the actual plugins and a few preset files i've previously saved. note that the second in each "voice" synth actually doesn't load a plugin yet - this is no problem, as will simply pass any audio through if there's no plugin loaded.

at any point after the server started and the script finished, i can use SClang to easily load and unload plugins from any of the Synths, or load different presets. However, i am leaving the audio routing and all spawned Synth instances in place.

it would be possible to do the loading, unloading, bypassing and preset management via Tidal, but personally i don't think that these are actions that need to be patterned.

personally, i'm currently wrapping supercollider and tidal in an enclosing nodeJS program that can send messages to both separately, allowing me to do things like trigger tidal code snippets or load/unload VST plugins via MIDI/OSC controls. in this way i can leave tidal to do what it does best, which is generating patterns of control data, and don't need to worry about making it do extra things.

1 Like

@Robin_Hase Thank you for your detailed response and code for the startup file. This is really helpful!

I appreciate you pointing out the fatal flaw in my approach - I also did come to realize that loading vsts dynamically from Tidal was a bad idea, and that pre-loading them was the way to go. Being able to send patternable parameters to a pre-loaded and configured VST I think is useful. @mrreason has a solution to map the parameters in his TidalVST.

I also like your use of VSTPlugin / VSTPluginController. I would be interested to know what @Spacechild1 has to say about your approach.

I will try your startup script - and will post back results or questions I may have.

There is a bigger challenge here - to define and document a working solution for VST use in Tidal - both instruments and effects.


as i said, i've made a few mistakes when trimming down the startup script to that example so i doubt it will work right now, but i'm working to have them fixed later today :slight_smile:

ok, updated the startup file (sry for the delay, currently recovering from a broken shoulder so sometimes sitting at a desk & typing isn't really possible)

i'm not entirely happy with the "dummy" synthdefs i've had to create, but they do the trick of stopping superdirt from complaining about sounds not being found. maybe someone has a better idea on how to tackle that?


Thanks Robin. Given your broken shoulder - I'm happy you have been able to respond at all! Hope you recover soon.

So - I was unable to get this solution working - but no need to respond right away. All of this can wait until you are well.

  1. MIDI error:
    I had to comment out the MIDI section. I got this error:
    ^^ ERROR: Message 'uid' not understood.

  2. No sound output from VSTs (sound from ~dirt samples is fine).
    Any VST I load does not produce sound when called from Tidal. I have a VST synth (Dexed) that I loaded in the "keys" Voice. When I send a pattern: d1 $ note "a3 b4 ds5" # sound "keys" I can see the GUI keyboard playing notes - but no sound. Tells me that Tidal -> synth is working, Synth -> out is not. I've tried looking at how your Reverb and Master bus outputs work, but I'm not versed enough in sclang to know what to do other than hack away.

  3. Preset load fails\keys).generator.loadPreset("keys");
    I commented out these lines - I assume that shouldn't matter for now.

Any insight you can provide (when you are well :wink:) is appreciated.

1 Like

the lines about loading presets are commented out in the updated post. but they serve to illustrate that you can have supercollider load default presets on startup if you want it to. first you would have to use the writePreset method to save a preset with the corresponding name though. i highly recommend you to spend some time going through the excellent VSTPlugin documentation in the supercollider IDE, without using tidalcycles. that way you can get a feel for how the ugen, the vstcontroller and their various methods interact.

the MIDIOut lines aren't necessary, i'll remove them from the example as well - thx for pointing this out!

finally, i think the reason for the sound not reaching the speakers was that i initialized the Synth instances in the wrong order - i think the main/master channel needs to be instantiated first so that the other synths can write to it properly. the updated example should work, if not please let me know.

to troubleshoot, you could try having a given voice synth send directly to output 0 instead of the ~masterBus and see if you get sound. you can also just skip bus processing altogether and simply have each plugin write to output 0 - again, i've left all that in as an example to show how you could do things like having everything run through your favorite VST compressor before hitting the speakers.

Thanks! I'm getting sound now, but I also had to make one other change. In the voice SynthDef, you have revSendAmount set to 0. As I understand it, without increasing this beyond 0, no sound goes out ~reverbBus. So I added two "keys" instances to test the different settings:

\keys -> Synth("voice",[out:0]), // should go directly to speakers
\keys2 -> Synth("voice",[out:~masterBus, revSendAmount:0.5]), // should go thru main/master

Both sound as expected - keys2 has reverb, keys does not!

What is still missing for me - routing non-VST sound via VST effects. Basically, play normal ~dirt samples and route audio to VST reverb, eq, whatever. I think this is what you started to scope in your optional section - to redirect ~dirt.orbits.

And yes -- the VSTPlugin documentation is excellent. This SC extension is really good - all of what we are doing here builds on it. I did build out a simple VST effects solution just in SC some time ago. I didn't get into preset management yet - and I will follow your suggestion to go deeper just in SC. Thanks.

Thanks for sticking with this.

Hey everyone!

The way i'm doing VSTPluginFx currently is through an Ndef and AudioBuses.

This allows me to only have 1 global instance of an FX VST Plugin as opposed to a more traditional tidal setup where we have 1 instance per FX.


~dirt.addModule('verb', { |dirtEvent|
    dirtEvent.sendSynth('verb' ++ ~dirt.numChannels,
            verb: ~verb,
            out: ~out
}, { ~verb.notNil });

if(~verbBus != nil, {});
~verbBus =, 2);

SynthDef("verb" ++ ~dirt.numChannels, {
	arg out, verb, sustain;
	var sig;
    sig =, ~dirt.numChannels);, sig*verb);

if(~verbOut != nil, {});
~verbOut =, 2);

Ndef(\verb, \vst -> {
	arg vdry, vwet, vpred, vinputlow, vinputhigh, vsize, vdiff, vdecay,
	vreverbhigh, vreverblow, vmodrate, vmodshape, vmoddepth;
	var verb =, ~dirt.numChannels),
		params: [
			1, 0, //Dry
			2, 1, //Wet
			3, vpred, //Pred
			4, 0.5, //InputLow
			5, 0.5, //InputHigh
			6, vsize, //Size
			7, vdiff, //Diffusion
			8, vdecay, //Decay
			9, 0.5, //ReverbHigh
			10, 0.5, //ReverbLow
			11, vmodrate, //ModRate
			12, vmodshape, //ModShape
			13, vmoddepth, //ModDepth
	], id: \verb).tanh * 0.5;, verb);

r {
	~verb = VSTPluginNodeProxyController(Ndef(\verb),id:\verb).open("VCV Rack 2.vst3");




    ~dirt.orbits[0].outBus = Ndef(\d1).bus;
    ~dirt.orbits[1].outBus = Ndef(\d2).bus;
    ~dirt.orbits[2].outBus = Ndef(\d3).bus;
    ~dirt.orbits[3].outBus = Ndef(\d4).bus;
    ~dirt.orbits[4].outBus = Ndef(\d5).bus;
    ~dirt.orbits[5].outBus = Ndef(\d6).bus;

Ndef(\Master, \vst -> {
	arg damp1=1, damp2=1, damp3=1, damp4=1, damp5=1, damp6=1;
	var dry, wet, sig;

            // Receive Dry Channels
	dry = [\d1)*damp1,\d2)*damp2,\d3)*damp3,\d4)*damp4,\d5)*damp5,\d6)*damp6];

            // Send DryOutputs to Channels 5-17{arg i;*2)+4, dry[i]);});
            // Receive Wet Output and Send to Channel [1,2]
	wet =, ~dirt.numChannels) ;, wet);

            // Master Out
	sig = dry[0] + dry[1] + dry[2] + dry[3] + dry[4] + dry[5];
	sig = sig + wet;
	sig =*3);