Ugh that’s pretty nasty that doneAction with gate is unreliable :confused:

I cant recall I’ve had these issues. Sounds like OSC messages are not synced up. I almost always wrap synth creation and set() calls in s.bind clauses. Does that help in your case?

(s.bind incurs the default Server latency as defined in s.latency. I lower the default and usually keep it at 0.05)

My reasoning: s.bind creates a bundle with time stamps. I’m not sure non-bundled Synth.new and set() calls are time stamped.

2 Likes

really is this necessary: (from bind doc)

(
s.bind {
    a = { |freq=100| SinOsc.ar(freq, LFTri.ar(freq)) * 0.2 }.play;
    s.sync;
    a.set(\freq, 400);
}
)

this seems ludicrous if its required…

I guess I could put the OSCdef callback code in a bind, but having the users have to litter around s.sync if they call set, seems pretty horrible.

e.g. what I’m thinking is:

OSCdef( \OrganelleKeys,
	{
		arg msg, time, addr, recvPort;
                s.bind({
                    if ( msg[1]>0,
                       {~notes.key_hit(msg[1],msg[2]);},
                       {~aux.key_hit(msg[1],msg[2]);}
                     );
                     s.sync;
                }
               );
	},
	"/key",
	recvPort:4000
);

what Im concerned about though, is this will only sync, between osc callbacks, which will probably solve this particular issue, but not if in the callback the user has to do sync between multiple calls to set… and what is this going to do to performance.

also there is something still a bit odd here… in my particular case, the synth creation, and set of gate (to zero) , are coming form different OSC messages, i.e. multiple invocations of the OSDdef function, surely SC, would sync between them?
Im guessing… possibly the issues is, in the case of ‘mashing the keyboard’, perhaps two messages turn up in the same udp packet, so perhaps the OSCdef is done multiple times with no sync in between… might explain why its uncommon but still happens.
(I will note though, in this case… these messages (/key) are not in an OSC bundle, though I dont think thats relevant)

I quite like the trig solution, as it could be slow, to just recover ‘lost’ synths, but my fear is… if this is a sync issue, is this the only time this occurs, or can it occur with things other than gate.

I’ll have to play with it… but feels ‘wrong’, whilst as a programmer I’m ok with technicalities like this (thread sync is bread n butter for me) , its not really for musicians/patch makers…

Does anyone know of a doc which explain when sync is necessary, and when its not?
(bind/makebundle say how to do it , rather than why… as far as i could see)

BTW: sorry if this is all off-topic, Im new to SC, so dont really know where the ‘support channels’ are , so came here, as you guys are very knowledgeable, thanks for your input.

I was unclear and just referred to documentation without any context, sorry.

I propose to wrap your Synth.new and set() messages in s.bind { } clauses (or equivalent) and see if the issue remains.

I’m not suggesting to use s.sync. I believe the time stamps implicitly added in bundles created using s.bind may solve your problem.

OSC events of Synth.new() and set() invocations are AFAIK not guaranteed to arrive in the order they are made in SClang to the scsynth server. This might cause issues with messages without timestamps as events without timestamps are performed directly on the scsynth.

Time stamped ones makes the events timing deterministic (as long as server latency is not exceeded), since (again, AFAIK) the scsynth server will store and schedule the events at a given time point and not perform them directly upon arrival.

I cannot recreate the issues using @zebra 's posted code above so this is a guess. :slight_smile:

More on this:
http://doc.sccode.org/Guides/ServerTiming.html

1 Like

The official mailing list is the best place to ask, I suggest posting your question there since it does seem a bit weird this isn’t working. Some of the developers who know more may be able to chime in.

Though IMO here is totally fine, but there’s definitely a larger audience there.

1 Like

sorry if i was unclear,the posted code above is working for me - that’s the workaround that doesn’t rely on self-freeing synths.

here is a version that doesn’t work - occasionally it leaves some synths un-freed (though silent)

s = Server.local;

s.waitForBoot { Routine {

	var interval, offset;

	SynthDef.new(\gated_sine, { arg out=0, hz=110, amp=0.5, gate=1, pan=0;
		var ampenv, snd, done;
		ampenv =  EnvGen.ar(Env.asr(), gate, doneAction:2);
		snd = ampenv * SinOsc.ar(hz) * amp;
		Out.ar(out, Pan2.ar(snd, pan));
	}).send(s);

	s.sync;

	/// make a bunch of notes and sustain them for random times
	// each is on a different thread
	interval = Array.fill(1 + 4.rand, { 2 + 8.rand });
	interval.postln;
	offset = 30 + 20.rand;
	200.do {
		Routine {
			var syn;
			0.4.rand.wait;
			syn = Synth.new(\gated_sine, [
				\hz, (offset + (10.rand * interval.choose)).midicps,
				\amp, 0.02, \pan, 1.0.rand2
			]);
			1.0.rand.wait;
			syn.set(\gate, 0);
		}.play;
	}

}.play; }

/*
s.queryAllNodes
*/

i actually don’t think this has anything to do with the timing of client->server OSC. the failure state is the self-freeing, which should happen long after the gate argument is set - and it is being set since we can’t hear anything.

the failure conditions seem to be:

  • multiple self-freeing synths are finishing at the same time. (i assume)
  • they are using gate arguments at all - this is the really weird part to me.

so yeah, seems like a race condition all right, but one that is somehow internal to the server process (?)

i think this is definitely a good candidate for a bug report or query to sc-dev.


both AFAIKs tally with my understanding as well. in particular, non-timestamped messages will be executed at the start of the very next audio processing block after they were received.

(in practice, i’ve never actually had a problem with calling Synth.new(); Synth.set() - my assumption is that all queued, unstamped synth creation commands are executed before all queued, unstamped set commands, on each block? dunno.)

you do need to sync after server commands that complete asynchronously, and this includes SynthDef compilation { }.play is a convenience method that creates a SynthDef on the fly, creates a Synth using it, and returns the Synth object - which isn’t synchronous, so the Synth doesn’t really exist on the server yet! (and yeah, this is definitely one of those ugly misleading default cases that @jah mentions far above.)

so {}.play is really only useful (IMHO) for simple fire-and-forget cases, and its almost always better to manage SynthDefs explicitly.

1 Like

I’m pretty sure that sclang doesn’t communicate with scsynth via OSC if they are running on the same machine, there’s a shared memory interface in use instead.

alas, shm interface is only used for control busses and scope buffers, AFAICT

sc-dev thread
sclang interface
c++ header

ok some interesting, and good (?) news…

so based on @jah 's suggestion i changed it too…

OSCdef( \OrganelleKeys,
	{
		arg msg, time, addr, recvPort;
                s.makeBundle(0.003, {
                    if ( msg[1]>0,
                       {~notes.key_hit(msg[1],msg[2]);},
                       {~aux.key_hit(msg[1],msg[2]);}
                     );
                });
	},
	"/key",
	recvPort:4000
);

and this fixes it…

I decided to use makeBundle, as then I dont have to set the servers global latency time…
this also meant I was comfortable using much lower than the suggested 0.05, which was unusable for latency for playing a keyboard (50ms), but 3ms is kind of acceptable.

I did initially try 1-2ms, I found this would still leave a few synths would still be left over (after mashing the keyboard, for 400+ notes) … probably in ‘real’ use this would be ok.

I dont think this explains why this is happening really…
in my particular example, the gate is off, and the env is marked as done, so why is the doneAction not being triggered (and as @zebra noted, freeWhenDone also does not trigger).
something (server?) appears not to realise that the ‘done’ of the envelope requires that the parent object (synth) should be freed… Im assuming this is the responsibility of the server/rather than client.
(or do doneactions get processed on the client end?)

Ive a nasty suspicion, that perhaps this makeBundle might not be fixing it, but rather introduces enough of a time delay to make it work… such is the nature of thread race conditions, slight timing changes often ‘fix’ the issue :wink:

Interesting. The packets sent between the IDE process and scsynth don’t look like regular OSC messages…

Edit: nevermind, I was getting confused because for some reason there’s an extra 4 bytes at the start of the message (0x0 0x0 0x0 0x0f) before the OSC type tag. But also there doesn’t seem to be an address either.

Edit2: Ah, I see the protocol is a modified version of OSC defined here: http://doc.sccode.org/Reference/Server-Command-Reference.html They use command numbers instead of path strings.

2 Likes

Ive not read these bugs thoroughly, but I wonder if one or both might be related :


yeah it’s all on the server; i’d blame this on some bug in the way NodeEnd events are handled. it’s a bit convoluted and i don’t fully grok it. but AFAICT between a UGen calling NodeEnd (e.g. here) and a Node actually being freed (e.g. here) is all on the same thread?

i know the “gate” argument is treated specially in some way for the purpose of some “features,” and at this point i can only guess that something about gate-argument processing is interfering with node deletion (node ID is getting zeroed prematurely, i dunno)

those issues you link do seem related…
… and here’s another one

i think that’s true. it ensures that two gate-close messages are never sent close enough together for the corresponding synths to hit their “done” states at the same time? (if they have same release curve)

i have a new test in mind…

… ok, this is just so weird: if i take the script above, and don’t randomize the timings at all, then there are no stuck synths. not what i expected!

like, it breaks if one node is freeing itself at the same time as a different node gets gate=0 ??

anyway, i’m going to stick with the method of using SendTrig(Done.kr()) and explicitly freeing nodes from the client, since it seems most reliable right now, and the server messaging code looks like murky waters…

1 Like

Interesting. There appears to be open scsynth issues on gate messages not working as expected when handled within the same “block”.

I’ve not experienced hanging sounds due to the above but have had sync issues causing not-so-great-sounding audio modulations. These have always been resolved by making sure OSC messages sent from sclang at the same (logical) time are bundled and, most importantly, time stamped. BTW I’ve never felt the need to communicate back node-lifecycle information from scsynth to sclang. I’ve tried to keep synth creation and automation a one-way sclang -> scsynth thing and tried to work around issues within sclang.

The Client-Server, Scheduling, Server timing and Physical vs Logical time concepts in SuperCollider are tricky. Bundling stuff with timestamps has certain trade offs. As @TheTechnobear noted the added delay, if large, can be problematic for live triggering. The flip side is that sample accurate timing (more exact than, for instance, Pd) can be attained for certain use cases. OffsetOut will sample accurately trigger sounds within a “block buffer”. OffsetOut + timestamps makes a lot of difference in tight drum sequencing.

@TheTechnobear Your suspicion seems correct. As long as timestamping things prevents this I dont worry to much about it.

One thing you might wanna try out to get around sloppy live triggering due to server delay would be to send note-ons without timestamp and not offs with timestamps possibly after a really small, fixed delay:

if (noteOn) {
	synth = Synth.new(\whatever);
} {
	fork {
		0.05.wait; // lower might work
		s.bind { // or s.makeBundle()
			synth.release; // a variant of set(\gate, 0)
		};
	}
}

… or something (untested code). Reasoning: onsets are more important to trigger as fast as possible compared to note offs. You still might run into issues with this, though, since some messages do not have timestamps.

Also if you decide to use non-timestamped messaging, check if block size (s.options.hardwareBufferSize) can be lowered since the reported issues seems to relate to it.

@zebra I sure hope these issues are not due to one node affecting other nodes but rather isolated to a single node. Nodes affecting other nodes would be severe!

hope these issues are not due to one node affecting other nodes

i would love to see a demonstration of this bug without >1 nodes (or indeed, many nodes), that would clarify things

i want to make sure the issue (my issue) is clear though:

  • no problem at all setting gate values, getting EnvGen to honor the gate value and become silent. i’ve never needed any special tricks for this (even bundling.)

  • once in a while, a synth will go silent all right, but not free itself as its supposed to from doneAction or &c.

  • i haven’t got this to happen with a single node[*], or even (weirdly) with a large number of nodes on different threads that are controlled in tandem. i can only get it to happen with a large number of nodes that are controlled at kinda random times.

  • the reproducing cases are definitely (i think) not due to missed gate msgs, nor (therefore?) to client-side race conditions when sending OSC.

  • its not even a problem in many circumstances, like typical performance patches that are run for an hour or two. (maybe 1/5K synths will silently hang.) but for an embedded supercollider app with arbitrarily long uptime, it’s unacceptable to have zombie synths even if they are silent, b/c the server will eventually stop working.

some of the issues reported in links above are definitely not about this, but about clear edge cases. like starting a synth with one gate value and setting to different gate val in same audio block. however, it’s all maybe a bit mixed up so seems good to at least glance at any issue even tangentially related.

[*] admittedly haven’t tried hard enough to reproduce with only 1 node at a time - seems like it’d take awhile!

(apologies for being so tedious about this)

1 Like

I want to build a batteries-included ‘DX7 orchestra’ on top of @cannc’s work(https://github.com/everythingwillbetakenaway/DX7-Supercollider) to bundle some prime soft-synth preset sounds with a grid-controlled looping midi sequencer I’ve been working on. Thinking along the same lines as tidal/super-dirt.

I know nearly nothing about sc & the code for that thing looks scary! Here’s what I’m planning, and despite lack of existing sc skills I think it should be a very manageable amount of work:

  • audition sounds & curate a GM drumkit from the best of internet percussion presets
  • bundle some drum sounds to trigger from a single midi channel in some kind of GM-drum-like layout
  • curate 14 other midi instrument tones to put on remaining 14 channels
  • audition more presets, rinse, repeat
  • add UI to select between a couple of different ‘orchestras’
  • there should be a stereo mixer onboard, jack-stereo-out for live & also individual jack outputs for certain instruments/instrument-groups to record into ardour.

Before I start fudging through learning enough sc to pull this off, can anyone say if I will get there without getting stuck into the internals of DX7-supercollider? sc should behave more or less like a general midi module when running the ‘DX7 orchestra’ program. Not looking for real-time param tweaks on midi CCs (yet!)

My other option is to start sifting through zynaddsubfx banks & hydrogen sample-kits but that feels like a bit of a cop-out somehow (sampling gives me the heebie-jeebies anyway)… Equally I don’t want this to get too open-ended - looking for a quick win so I can stay focussed on the controller itself. Good sounds ‘in the box’ would help a lot!

2 Likes

i had a similar ambition - wanting to package that work a little better for inclusion in plug-and-play instruments. not wanting to take a lot of time or dig into the guts too much.

here’s my fork - i wrapped the script in a class, which works basically fine - but the linked line is the next thing that really needs to change; currently it loads a data file from disk and steps through it line by line - on every note - this data really needs to be a (byte array) classvar in a separate class, or something.

[ https://github.com/catfact/DX7-Supercollider/blob/master/DX7Clone.sc#L785 ]

2 Likes

haha so basically needs a bit of tlc but the sounds are so cool. Just sat there auditioning percussive elements for like half an hour using pd-kria as a trigger… rather magical! Better stop this in case it’s actually physically accessing disk on those notes & grinding my ssd to death.

3 Likes

This is interesting, I took a closer look: the delays of the affected zombie synths kind of points in the direction of the issue of setting gate to a different gate value within the same block:

(
s.waitForBoot { Routine {

	var interval, offset;

	SynthDef.new(\gated_sine, { arg out=0, hz=110, amp=0.5, gate=1, pan=0;
		var ampenv, snd, done;
		ampenv =  EnvGen.ar(Env.asr(), gate, doneAction:2);
		snd = ampenv * SinOsc.ar(hz) * amp;
		Out.ar(out, Pan2.ar(snd, pan));
	}).send(s);

	s.sync;

	/// make a bunch of notes and sustain them for random times
	// each is on a different thread
	interval = Array.fill(1 + 4.rand, { 2 + 8.rand });
	interval.postln;
	offset = 30 + 20.rand;
	200.do {
		Routine {
			var syn, delay;
			0.4.rand.wait;
			syn = Synth.new(\gated_sine, [
				\hz, (offset + (10.rand * interval.choose)).midicps,
				\amp, 0.02, \pan, 1.0.rand2
			]);
			delay = 1.0.rand;	
			delay.wait;
			syn.set(\gate, 0);
			~info = ~info.add( (syn: syn, delay: delay) );
		}.play;
	}

}.play; }
)

s.queryAllNodes;

// when a zombie synth appears, check below
~infoSortedByDelay = ~info.sortBy(\delay);
~synthSmallestDelay=~infoSortedByDelay[0];
~synthSmallestDelay[\delay] < (s.options.blockSize/44100); // true
~synthSmallestDelay[\syn].get(\gate, { |gate| ("gate is" + gate).debug }); // it's possible to check gate, a non-zombie synth would not reply with an answer

~synthSmallestDelay[\syn].set(\gate, 1); // this will trigger the synth for the first time
~synthSmallestDelay[\syn].set(\gate, 0); // this will release synth and invoke doneAction
1 Like

that was pretty much the conclusion i came too (with a very similar test to yours) for me, a wait of around 0.01-0.02 will result in the synth not freeing, above and its fine… and indeed you can retrig it with a gate=1, and then gate 0 will free it.
the only thing that surprised me, was my osc messages from another process were getting in so quickly (note-on then note-off) , but on reflection, its probably just they happen to be delivered in the same packet, so they would be very close together.

1 Like

oh wow - i could have sworn i put a minimum lower bound on that delay time but i guess i didn’t. ugh. so dumb

you’re absolutely right - can’t get the zombies if there is a delay of more than the blocksize between synth creation and setting gate = 0. (e.g. delay = (s.options.blockSize * 1.5)/ s.sampleRate;)

so this is the same bug - or rather, known limitation.

good news is there’s a simple, known server-side workaround - it’s weird-looking but ensures that gate=1 on synth creation, before the \set messages are processed:

	gate = gate + Impulse.kr(0);
2 Likes