MIDI Synth Notes Stuck

I’m having some issues with code I made to emulate a Hammond for use with MIDI. After awhile, a note invariably seems to get stuck and something goes wrong with an EnvGen. I have added a rather poor emulation of the Hammond key clicks and when a note gets stuck, it seems to be holding the gate on the click envelope open, along with the other envelopes.

(
// (c) M. Josiah Sytsma 2022

s = Server.local;

s.newBusAllocators;

MIDIClient.init;
MIDIIn.connectAll;

s.waitForBoot{

	var pedBuf = Buffer.alloc(s, 1024, 1, {arg buf; buf.sine1Msg(
		(1..11).collect{
			arg n;
			if (n.odd)
			{n.reciprocal}
			{0}
		}
	)
});

	var organOut = Bus.audio(s,1);
	var vibOut = Bus.audio(s,1);

	var defs = (
		SynthDef(\organ, {
			arg out, freqs=#[0,0,0,0,0,0,0,0,0], amps=#[0,0,0,0,0,0,0,0,0], amp=0.05, drawbarAmp=1, clickDur=0.01, clickAmp=0.03, gate=1, clickDec=1e-3, clickHold=0.003;
			var sig, env, click;
			env = Env.cutoff(clickDec).ar(0,gate);
			click = WhiteNoise.ar(Hasher.kr(freqs).range(0.0,1.0))*amps;
			click = OnePole.ar(click.sum, 0.8);
			click = click*Env([1,1,0,1,1],[clickHold, clickDec, clickDec, clickHold], -4,2).ar(2,gate);
			click = click*clickAmp;
			sig = SinOsc.ar(freqs, 0, amps);
			sig = sig.sum;
			sig = sig*env*amp;
			sig = [sig,click].sum;
			Out.ar(out, sig);
		}).add;

		SynthDef(\perc, {
			arg vol = 0.025, out, freq=261.63, dec, amp=1, gate=1;
			var sig, env;
			sig = SinOsc.ar(freq);
			env = Env.perc(0.01, dec, 1, -4).ar(2, gate);
			sig = sig*env*amp*vol/2;
			Out.ar(out, sig);
		}).add;

		SynthDef(\pedal, {
			arg out, freq=261.63, amp=0.03, drawbarAmp=1, clickDur=0.01, clickAmp=0.02, gate=1, clickDec=1e-3, clickHold=0.003, buf;
			var sig, env, click;
			env = Env.cutoff(clickHold+clickDec/2).ar(2,gate);
			click = OnePole.ar(WhiteNoise.ar((Hasher.kr(freq).range(0,1/9))),0.9);
			click = click*Env([1,1,0,1,1],[clickHold, clickDec, clickDec, clickHold], -4,2).ar(0,gate);
			click = click*clickAmp;
			sig = Osc.ar(buf, freq);
			sig = sig*env*amp;
			sig = [sig,click].sum;
			Out.ar(out, sig);
		}).add;

		SynthDef(\vibrato, {
			arg in, out, on=0, amp=1;
			var dry, wet, sig, c1, c2, c3, v1, v2, v3, wet2, wet3, scanner;
			amp = Lag.kr(amp, 0.2);
			dry = In.ar(in, 1);
			wet = (1..9).collect{arg i; DelayN.ar(dry, 0.01, i/9000)};
			scanner = LFTri.ar(7,0).range(0,wet.size+1);
			wet = SelectX.ar(scanner, [dry]++wet);
			c3 = dry.blend(wet, MouseX.kr);
			v3 = wet;
			sig = SelectX.ar(on.varlag(0.1), [dry*(5/3), c3*5/3]);
			sig = sig*amp;
			Out.ar(out, sig!2);
		}).add;
	);

	var sync = s.sync;

	var
	shadow = Color.grey(0.2, 0.5),
	ivory = Color.new255(200,190,180),
	brown = Color.fromHexString("654321"),
	ebony = Color.grey(0.3),
	light = Color.white.alpha_(0.2);
	var win = Window("Hermond B5", Rect(300, 300, 680, 600), false, true, s, false).onClose_{pedBuf.free; CmdPeriod.run;}
	.front
	.background_(Color.grey(0.3));

	var organNotes = Array.newClear(128);
	var percNotes;



	var switches = Dictionary.newFrom([
		\vol, 0,
		\vib, 0,
		\perc, 0,
		\percVol, 0,
		\dec, 0,
		\harm, 0
	]);

	var vib = Synth(\vibrato, [\in, organOut, \out, 0, \amp, [-3.dbamp,1.0][switches[\vol]]], s, \addToTail);

	var switch = {
		arg point, string, ctrl, bottomString, topString;
		var rect = Rect(0, 0, 60, 120);
		var bottomShade = Rect(0, rect.height*(5/6), rect.width, rect.height*(1/6));
		var topShade = Rect(0, 0, rect.width, rect.height*(1/6));
		var textBox;
		var shade;
		var val = switches[ctrl];
		var selectorRect = [bottomShade.moveBy(0,bottomShade.height*1.25), topShade.moveBy(0,(topShade.height*1.25).neg)][val];
		shade = [topShade,bottomShade][val];
		Pen.use{
			Pen.translate(point.x, point.y);
			Pen.addRoundedRect(rect,4,4).width_(5).color_(Color.black).stroke;
			Pen.color_(ivory);
			Pen.addRoundedRect(rect,4,4);
			Pen.use{
				Pen.clip;
				Pen.fillRect(rect);
			};

			textBox=Rect(0,rect.height*(6-val)/12,rect.width,20);

			Pen.addRect(shade).fillAxialGradient(shade.center.x @ shade.top, shade.center.x @ shade.bottom, [Color.grey(0.1,0.3), Color.white.alpha_(0.2)][val], [Color.white.alpha_(0.2), Color.grey(0.1,0.3)][val]);

			Pen.stringCenteredIn(string, textBox, Font("Futura", 12, false, false, false), Color.black);

			Pen.stringCenteredIn(bottomString, bottomShade.moveBy(0,bottomShade.height*1.25), Font("Futura", 12, false, false, false), [Color.white, Color.grey(0.6)][val]);

			Pen.stringCenteredIn(topString, topShade.moveBy(0,(topShade.height*1.25).neg), Font("Futura", 12, false, false, false), [Color.grey(0.6), Color.white][val]);

			Pen.addRect(Rect.aboutPoint(selectorRect.center, selectorRect.width*2, selectorRect.height*2)).fillRadialGradient(selectorRect.center, selectorRect.center, 1, selectorRect.width/2, Color.white.alpha_(0.1), Color.clear);
		};
	};


	/////////////////////////////////////the switches

	var volx = 40;
	var vibx = 115;
	var percx = 350;
	var percVolx = 425;
	var decx = 500;
	var harmx = 575;

	var drawbarYs = Array.newClear(9);

	var switchY = 40;

	var switchGUI = UserView(win, win.view.bounds)
	.drawFunc_{
		switch.(volx @ switchY, "VOLUME", \vol, "SOFT", "NORMAL");
		switch.(vibx @ switchY, "VIBRATO", \vib, "OFF", "ON");
		switch.(percx @ switchY, "PERC", \perc, "OFF", "ON");
		switch.(percVolx @ switchY, "PERC VOL", \percVol, "SOFT", "NORMAL");
		switch.(decx @ switchY, "DECAY", \dec, "SLOW", "FAST");
		switch.(harmx @ switchY, "HARM", \harm, "SECOND", "THIRD");
	}
	.mouseDownAction_{
		arg view, x, y, modif, button, clickCount;

		case
		{Rect(volx,switchY,60,120).containsPoint(x@y)}{switches.add(\vol->(switches[\vol]+1).mod(2)); switchGUI.refresh}

		{Rect(vibx,switchY,60,120).containsPoint(x@y)}{switches.add(\vib->(switches[\vib]+1).mod(2)); switchGUI.refresh}

		{Rect(percx,switchY,60,120).containsPoint(x@y)}{switches.add(\perc->(switches[\perc]+1).mod(2)); switchGUI.refresh}

		{Rect(percVolx,switchY,60,120).containsPoint(x@y)}{switches.add(\percVol->(switches[\percVol]+1).mod(2)); switchGUI.refresh}

		{Rect(decx,switchY,60,120).containsPoint(x@y)}{switches.add(\dec->(switches[\dec]+1).mod(2)); switchGUI.refresh}

		{Rect(harmx,switchY,60,120).containsPoint(x@y)}{switches.add(\harm->(switches[\harm]+1).mod(2)); switchGUI.refresh};

		vib.set(\on, switches[\vib]);
		vib.set(\amp, [0.5,1][switches[\vol]]);
		if (switches[\perc]==0 && drawbarYs[8].isNil.not){amps[8]=drawbarYs[8].linlin(0,300,0,1)};


	};

	/////////////////////////////////////vibrato knob
	var vkRect = Rect.aboutPoint(265 @ 100, 50, 50);

	var vkRot = 0;

	var vibKnob = UserView(win, vkRect)
	.mouseMoveAction_{
		arg view, x, y;
	}
	.drawFunc_{
		arg view;
		var squares = 20;
		var center = vkRect.radiusH@vkRect.radiusV;
		Pen.use{
			Pen.color_(Color.grey(0.1));

			//body
			Pen.addOval(Rect.aboutPoint(center, 48, 48));

			//decor
			squares.do{
				arg i;
				var rad = 30;
				var angle = 2pi/squares * i.neg,
				x = rad*sin(angle) + vkRect.radiusH,
				y = rad*cos(angle) + vkRect.radiusV;
				Pen.use{
					Pen.rotate(angle, vkRect.radiusH, vkRect.radiusV);
					Pen.addRoundedRect(Rect.aboutPoint(center, vkRect.radiusH, 4), 2, 2)
					.fill;
				};
			};

			//gradient

			//label
			Pen.stringCenteredIn(
				"VIBRATO\n"++"-&-\n"++"CHORUS",
				Rect.aboutPoint(center.x@(center.y+3), 50, 50),
				Font("Futura", 12),
				Color.white;
			);

			//line
			// Pen.color_(Color.white).width_(3).capStyle_(1).line(vkRect.radiusH @ 26, vkRect.radiusH @ 10).stroke;
			Pen.color_(Color.white).fillOval(Rect.aboutPoint(center.x @ 10, 3, 3))
		};
		Pen.addOval(Rect.aboutPoint(center, 45, 45))
		.fillRadialGradient(center-20, center-10, 1, 30, Color.grey(0.9,0.4), Color.clear);
	};


	/////////////////////////////////////the drawbars
	var amps = (0!9);
	var drawbarRatios = [-12,0,7,12,19,24,28,31,36];
	var drawbarMin = 0;
	var drawbarMax = 300;
	var drawbarView = View(win, Rect(0, 200, win.view.bounds.width, win.view.bounds.height-200))
	.background_(Color.grey(0.1));

	var teethArray = (7.collect{arg i; 2.pow(i+1)!12}++(192!7)).flat;

	var ratioArray = ((([85,71,67,105,103,84,74,98,96,88,67,108]/[104,82,73,108,100,77,64,80,74,64,46,70])!7)++([84,74,98,96,88,67,108]/[77,64,80,74,64,46,70])).flat;

	var drawbar = {
		arg point, color, ind;
		var val;
		var rect = Rect(0, 300, 40, 60);
		UserView(drawbarView, Rect(point.x, 0, 40, 600))
		.animate_(false)
		.drawFunc_{
			var numberRect = Rect(rect.left+5, rect.top, rect.width-10, -600);
			var nums;
			Pen.color_(color).fillRect(rect);

			//bottom shadow
			Pen.moveTo(rect.leftBottom)
			.lineTo(rect.rightBottom)
			.lineTo(rect.rightBottom - 8)
			.lineTo(rect.left + 8 @ (rect.bottom-8))
			.lineTo(rect.leftBottom)
			.color_(Color.grey(0.1, 0.2))
			.fill;

			//right shadow
			Pen.moveTo(rect.rightBottom)
			.lineTo(rect.rightBottom - 8)
			.lineTo(rect.right - 8 @ (rect.top+8))
			.lineTo(rect.rightTop)
			.lineTo(rect.rightBottom)
			.fillAxialGradient(rect.rightBottom - 4, rect.right - 4 @ (rect.top+4), shadow, shadow.alpha_(0.3));

			//grabby knobby
			Pen.moveTo(rect.left + 8 @ (rect.bottom-8))
			.lineTo(rect.left + 8 @ (rect.top+(rect.height*3/5)))
			.lineTo(rect.right - 8 @ (rect.top+(rect.height*3/5)))
			.lineTo(rect.right - 8 @ (rect.bottom-8))
			.fillAxialGradient(rect.left @ (rect.top+(rect.height*4/5)), rect.right @ (rect.top+(rect.height*4/5)), light.alpha_(0.1), Color.grey(0.2,0.2));


			//number line drawer thing
			Pen.color_(Color.white).fillRect(numberRect);
			Pen.color_(Color.black).fillRect(numberRect.insetAll(5,0,5,0));

			//numbers
			nums = 8.collect{arg i; Rect(numberRect.left, rect.top-((i+1)*drawbarMax/8), numberRect.width, 300/8)};
			nums.do{arg r, i; Pen.stringCenteredIn((i+1).asString, r, Font("Futura", 20), Color.white)};

			drawbarYs = drawbarYs.put(ind, rect.top);

			val = rect.top.linlin(drawbarMin,drawbarMax,0,1);
			amps.put(ind, val);
		}
		.mouseMoveAction_{
			arg view, x, y;
			var lim;
			view.animate_(true);
			lim = y.clip(drawbarMin,drawbarMax);
			rect = Rect(0, lim, 40, 60);
			val = lim.linlin(drawbarMin,drawbarMax,0,1);
			if (ind == 8 && switches[\perc] == 1){val=0};
			amps.put(ind, val);
			organNotes.collect{arg item, i; if (item.isPlaying){organNotes[i].set(\amps, amps/9)}};
			// amps.postln;
		}
		.mouseUpAction_{
			arg view;
			view.animate_(false);
		};
	};

	var drawbarPoints =
	9.collect{
		arg i;
		var offset=40, spacing = 50, y = 500, point;
		point = offset+(i*spacing) @ y;
	};

	var drawbars = [brown, brown, ivory, ivory, ebony, ivory, ebony, ebony, ivory].collect{arg color, i; drawbar.(drawbarPoints[i], color, i)};

	var freqsArray = (24..114).collect{
		arg num;
	var freqs, teeth;
		freqs = num+drawbarRatios; //an array of midinums
		freqs = freqs.collect{arg inval; 	//array of wheel numbers with foldback
			w = inval-24;
			while {w>90}{w=w-12};
			while {w<0}{w=w+12};
			w};

		freqs = freqs.collect{arg inval;
			ratioArray[inval] * teethArray[inval] * 20};
	};

	s.sync;

	MIDIdef.noteOn(\onManuals, {
		arg val, num, src, chan;
		var freqs = freqsArray[num-24], perc;
		perc = freqs[[2,3]];

		// [freqs, amps/9].flop.collect{arg in; ("Num: "++num++" Freq: "++in[0].round(0.01)++" Drawbar: "++in[1]).postln};

		if (switches[\perc]==1)
		{
			amps.put(8, 0);
			if (organNotes.any{arg item; item.isPlaying}.not)
			{percNotes = Synth(\perc, [
				\freq, perc[switches[\harm]],
				\dec, [0.5,0.125][switches[\dec]],
				\out, organOut,
				\amp, [-3.dbamp,0.dbamp][switches[\percVol]]
			]).register;
			};
		};


	organNotes.put(num, Synth(\organ, [\freqs, freqs, \amps, amps/9, \out, organOut]).register);

	},
	(24..114)
	);

	MIDIdef.noteOn(\onPeds, {
		arg val, num, src, chan;
		var freq;
		freq = ratioArray[num] * teethArray[num] * 20;
		organNotes.put(num, Synth(\pedal, [\freq, freq, \out, organOut, \buf, pedBuf]));
	}, (0..23));

	MIDIdef.noteOff(\oof, {
		arg val, num, src, chan;
		organNotes[num].set(\gate, 0);
		if (percNotes.isPlaying){percNotes.set(\gate, -1)};
	});
};
)

Still happening after updating the code to not run such a heavy MIDI On function

At least, for the key clicks, I’d suggest to simplify. A very short-lived perc envelope does not need a gate, and it doesn’t need early release, and there’s not much point in register-ing either. You could just play the Synth, let the perc envelope release itself, and doneAction cleans it up. Avoid unnecessary logic.

If there are other envelopes being held open: are those synths making sound? If not, a very common problem with external note triggers + high density is: if the note-off follows extremely quickly after the note-on, then the Synth.new /s_new message and the close-gate /n_set message may arrive in the server at the same time – leaving a new synth, but with a closed gate.

If the stuck notes are playing sound, though, then the issue is something else.

When there are stuck notes, could you dump the node tree with controls (Server menu)? I don’t have a clear, specific idea what “something goes wrong with an EnvGen” or “along with the other envelopes” mean.

hjh

Thanks James. When I opened the node tree when that was happening, it showed organ synth still playing on the server with no keys being pressed. The problem is definitely somewhere in that def. What’s happening more specifically is that I get the white noise form the click being held, along with the sines. So either the envelope is looping at the beginning or it’s just not doing anything at all.

What’s going on with the logic in the MIDI On/Off is that when the percussion switch is on on a B3, the ninth drawbar becomes the percussion sound. It uses either the second or third harmonic and has a percussive envelope that can only be triggered after all keys have been released. I’ve registered perc synth so only one can be played at a time and only after all keys have been released. Is there a more efficient way to handle this part?

The key clicks, which are separate from the percussion, happen both at the attack and release of a note on the organ, so the idea was to have an envelope that doesn’t free the synth until the click at the release of the note is done. So maybe this should be redesigned.

The perc synth doesn’t seem to be having any issues and the issue is less frequent now that the frequency calculation isn’t being performed in the note on function but I’ll see if changing my input device setup makes a difference so I can dump the node tree. It seemed more frequent with an external MIDI controller as opposed to MIDI Touchbar.

EDIT: Node tree dump with controls:

NODE TREE Group 0
   1 group
      12144 organ
        out: 4 freqs: 155.55555725098 311.11111450195 466.08694458008 622.22222900391 932.17388916016 1244.4444580078 1568 1864.3477783203 2488.8889160156 amps: 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0 amp: 0.050000000745058 drawbarAmp: 1 clickDur: 0.0099999997764826 clickAmp: 0.029999999329448 gate: 0 clickDec: 0.0010000000474975 clickHold: 0.003000000026077
      12143 organ
        out: 4 freqs: 116.52173614502 233.04347229004 349.09091186523 466.08694458008 698.18182373047 932.17388916016 1174.7945556641 1396.3636474609 1864.3477783203 amps: 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0 amp: 0.050000000745058 drawbarAmp: 1 clickDur: 0.0099999997764826 clickAmp: 0.029999999329448 gate: 0 clickDec: 0.0010000000474975 clickHold: 0.003000000026077
      12142 organ
        out: 4 freqs: 87.272727966309 174.54545593262 261.53845214844 349.09091186523 523.07690429688 698.18182373047 880 1046.1538085938 1396.3636474609 amps: 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0 amp: 0.050000000745058 drawbarAmp: 1 clickDur: 0.0099999997764826 clickAmp: 0.029999999329448 gate: 0 clickDec: 0.0010000000474975 clickHold: 0.003000000026077
      11783 vibrato
        in: 4 out: 0 on: 0 amp: 1
   11782 safeClip_2
     limit: 1

Update: changing the organ synthdef to free self on click gate’s release did not fix it.

SynthDef(\organ, {
			arg out, freqs=#[0,0,0,0,0,0,0,0,0], amps=#[0,0,0,0,0,0,0,0,0], amp=0.05, clickDur=0.01, clickAmp=0.03, gate=1, clickDec=1e-3, clickHold=0.003;
			var sig, env, click;
			env = Env.cutoff(clickDec).ar;
			click = WhiteNoise.ar(Hasher.kr(freqs).range(0.0,1.0))*amps;
			click = OnePole.ar(click.sum, 0.8);
			click = click*Env([1,1,0,1,1],[clickHold, clickDec, clickDec, clickHold], -4,2).ar(2,gate);
			click = click*clickAmp;
			sig = SinOsc.ar(freqs, 0, amps);
			sig = sig.sum;
			sig = sig*env*amp;
			sig = [sig,click].sum;
			Out.ar(out, sig);
		}).add;
NODE TREE Group 0
   1 group
      1932 organ
        out: 4 freqs: 233.04347229004 466.08694458008 698.18182373047 932.17388916016 1396.3636474609 1864.3477783203 2349.5891113281 2792.7272949219 3728.6955566406 amps: 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0.11111111193895 0 amp: 0.050000000745058 clickDur: 0.0099999997764826 clickAmp: 0.029999999329448 gate: 0 clickDec: 0.0010000000474975 clickHold: 0.003000000026077
      1211 vibrato
        in: 4 out: 0 on: 1 amp: 1 sel: 1
   1210 safeClip_2
     limit: 1


**notice the faint filtered white noise in the audio file

I see a couple of things going on here. One of them (I’m pretty sure) is the issue I already mentioned about gates being closed before they can open. The other issue, which I think is confusing the first, is that your envelopes are a bit strange.

To illustrate the gate issue:

s.boot;

s.dumpOSC(1);

(
s.makeBundle(0.2, {
	x = Synth(\default);
});

s.makeBundle(0.20001, {
	x.set(\gate, 0);  // '.release'
});
)

// dumpOSC output:
[ "#bundle", 16620540075947396843, 
  [ 9, "default", 1000, 0, 1, "gate", 1, "freq", 220 ]
]
[ "#bundle", 16620540075947439792, 
  [ 15, 1000, "gate", 0 ]
]

s.queryAllNodes(true);
NODE TREE Group 0
   1 group
      1000 default
        out: 0 freq: 220 amp: 0.10000000149012 pan: 0 gate: 0

I’m including the dumpOSC output to show that both the note-on and note-off messages are in fact being sent… but, the node didn’t stop. It’s just hanging out there – gate is 0 but, no release.

The reason is that the time between note on and off is too short. The server is evaluating both messages in the same control block. The gate is set to 0 before any audio is calculated – so, as far as the synth knows, there was never any signal to open the gate. If the envelope didn’t start, then it can’t finish, and if it doesn’t finish, then doneAction can’t do anything. (Note that it is a valid use case to start a synth with the gate closed, and open it later, as in modular gear.)

In your case, there’s external note control (which is one of the ways this can happen – because the controller determines the timing of incoming messages, and it may not care about leaving enough time between on and off), and your node dumps do show gate: 0. So I’m fairly sure this is the problem.

If you’re certain that a new synth should always open the envelope immediately, you could handle it by doing gate = gate + Impulse.kr(0) before any envelopes. The Impulse guarantees that the envelope generators’ gate inputs will have at least one control block of a positive input.

Now, those envelopes…

There is no sound in nature with an instantaneous attack or release. Even in percussion, it takes time for the noise burst from the impact to spread through the body of the instrument. This is very fast, but the time required is not zero.

From this, I derive a rule of thumb: Every sound needs an envelope with a nonzero attack, and nonzero release. Every sound. A 1 ms attack and release is enough – just, not zero.

Env.cutoff and Env([1, …], …) have a zero-length attack. In this case, for the noise bursts, you may not be able to distinguish the discontinuity click from the noise itself, so you might “think” it’s OK – but formally, it’s not. (For the sines, there’s a bit of folklore floating around that “it’s OK to start or stop at a zero crossing” but I strongly disagree.)

The side effect here is, because the noise envelope begins with non-silence, then the envelope’s result when gate begins at 0 is also non-silent! This is why you’re hearing the noise leaking through. If you began your envelopes with a 0 level, you would still have stuck notes, but they would be silent.

So I’d suggest:

...

	gate = gate + Impulse.kr(0);  // prevent stuck nodes
	// "attack-sustain-release" rather than `cutoff`
	env = Env.asr(clickDec, 1, clickDec).ar(0, gate);

...

// or 'clickDec' instead of 0.001 maybe
// adding a 0 at the start also requires +1 for the releaseNode
	click = click * Env([0, 1, 1, 0, 1, 1, 0], [0.001, clickHold, clickDec, clickDec, clickHold, 0.001], -4, 3).ar(2, gate);

hjh

This seems to have worked. I see what you’re saying about instantaneous attack. I liked the extra percussive-ness of the 0 attack, but you’re right it really isn’t practical. Thanks James!