musikinformatik / SuperDirt

Tidal Audio Engine

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

cut groups break on simultaneous events

geikha opened this issue · comments

The following code acts as if there were no cut groups indicated at all:

d1 $ superimpose ((# speed 2) . (# cut 2))
        $ s "bev(6,8)" # cut 1

It should sound like

d1 $ s "bev(6,8)" # cut 1

d2 $ s "bev(6,8)" # speed 2 # cut 1

It seems it also doesn't work like this:

d1 $ stack[
    s "oregano2(6,8)" # cut 1,
    s "oregano2(6,8)" # speed 2 # cut 2 
]

So it seems the cut groups are broken?

Did you mean # cut 2 on the last line? Because the cut 2 in the first example will supersede the cut 1 for the superimposed pattern.

@bgold-cosmos sorry do you mean the last line of the last example or the last line of the first example?

either way, doesn't matter. what i've written it's what i meant :P

in the first example i want to have two different cut groups. one cutting the original and the other one cutting only the sped up samples. as for last example, each orbit has its own cut groups so it really doesn't matter which non-0 number i put in there.

Ah sorry, I forgot about d1 and d2 being different orbits by default.

Looking into this I think it may stem from #252 since I don't think there was a problem previous to that change, but I can't easily see why the new code shouldn't work. I'll try verifying some test cases and see if I can't figure out more clearly what's going on.

OK, still working on a fix, but it definitely appears to be an issue with the handling of simultaneous cut messages. Shifting things even slightly causes it to work:

d1
 $ stack [
  s "bev(6,8)" # cut 1 # shape 0.9,
  ((1/1024) ~>) $ s "cp*8" # cut 2
  ]

is fine, but change the 1/1024 to 0 and the cut gets obliterated

(edited to add): also to be clear, this is a regression - the issue doesn't happen in an older branch I have from 2021

Yes, at #252 we removed the cut group for each orbit, and maybe something else went wrong.

@bgold-cosmos If you switch on dump osc on the supercollider side, what messages do you see?

The key is somewhere either in DirtGateCutGroup:

DirtGateCutGroup {

	*ar { | releaseTime = 0.02, doneAction = 2 |
		// this is necessary because the message "==" tests for objects, not for signals
		var same = { |a, b| BinaryOpUGen('==', a, b) };
		var or = { |a, b| (a + b) > 0 };
		var and = { |... args| args.product };
		var sameSample = same.(\sample.ir(0), \gateSample.kr(0));
		var sameCut = same.(\cut.ir(0), \gateCut.kr(0));
		var free = and.(or.(\cutAllSamples.kr(0), sameSample), sameCut);
		^EnvGen.kr(Env.cutoff(releaseTime), (1 - free), doneAction:doneAction);
	}
}

and in DirtEvent:

if(~cut != 0) {
				server.sendMsg(\n_set,
					if(~cutAll.notNil) { orbit.dirt.group } { orbit.group },
					\gateSample, ~hash,
					\gateCut, ~cut.abs,
					\cutAllSamples, if(~cut > 0) { 1 } { 0 }
				)
			};

, and finally here:

sendGateSynth {
		server.sendMsg(\s_new,
			"dirt_gate" ++ ~numChannels,
			-1, // no id
			1, // add action: addToTail
			~synthGroup, // send to group
			*[
				in: orbit.synthBus.index, // read from synth bus, which is reused
				out: orbit.dryBus.index, // write to orbital dry bus
				amp: ~amp,
				gain: ~gain,
				overgain: ~overgain,
				sample: ~hash, // required for the cutgroup mechanism
				cut: ~cut.abs,
				sustain: ~sustain, // after sustain, free all synths and group
				fadeInTime: ~fadeInTime, // fade in
				fadeTime: ~fadeTime // fade out
			]
		)
	}

I'm trying to reproduce this on the sclang side.

All this seems to work fine:

SuperDirt.default = ~dirt;

// cut same cut group, correct
(
fork {
	(type:\dirt, orbit:0, s: \sax, cut: 1).play;
	0.2.wait;
	(type:\dirt, orbit:0, s: \cr, cut: 1).play;
}
)

// don't cut different cut group, correct
(
fork {
	(type:\dirt, orbit:0, s: \sax, cut: 1).play;
	0.1.wait;
	(type:\dirt, orbit:0, s: \cr, cut: 2).play;
}
)

// don't cut different cut group, simultaneous, correct
(
fork {
	(type:\dirt, orbit:0, s: \sax, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 2).play;
}
)

// don't cut different cut group, absolutely simultaneous, correct
(
s.bind {
	(type:\dirt, orbit:0, s: \sax, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 2).play;
}
)

// cut same cut group, absolutely simultaneous, correct
(
fork {
	(type:\dirt, orbit:0, s: \sax, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 1).play;
}
)

// cut same cut group, absolutely simultaneous, correct
(
s.bind {
	(type:\dirt, orbit:0, s: \sax, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 1).play;
}
)



@bgold-cosmos maybe you can find a minimal reproducer and then check in tidal what the actual events are that are being sent?

@telephon
The Tidal events look fine, its seems that when there are two bundles with the same timestamp that one n_set will override the other?

To see this on the sclang side, I think you need to have some cutting going on in each group. I notice an issue with this:

(
fork {
	(type:\dirt, orbit:0, s: \bev, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 2).play;
	0.3.wait;
	(type:\dirt, orbit:0, s: \bev, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 2).play;
}
)

You'll hear two overlapping "bev" samples playing, which should not be happening.

Minimal Tidal code to do something like this is

d1
 $ stack [
  s "bev*8" # cut 1,
  ((0/1024) ~>) $ s "cp*8" # cut 2
  ]

Looking at the OSC dump in supercollider it look like

[ "#bundle", 16676975298944028671, 
  [ 12, 1004, 1 ],
  [ 15, 1004, "resumed", 1 ],
  [ 12, 1003, 1 ],
  [ 15, 1003, "resumed", 1 ],
  [ 12, 1001, 1 ],
  [ 15, 1001, "resumed", 1 ],
  [ 12, 1000, 1 ],
  [ 15, 1000, "resumed", 1 ],
  [ "/n_set", 3, "gateSample", -1128873534, "gateCut", 1, "cutAllSamples", 1 ],
  [ "/g_new", 1098, 1, 3 ],
  [ "/s_new", "dirt_sample_1_2", -1, 1, 1098, "bufnum", 520, "sustain", 16.0552, "speed", 1, "freq", 261.626, "endSpeed", 1, "begin", 0, "end", 1, "pan", 0, "out", 34 ],
  [ "/s_new", "dirt_gate2", -1, 1, 1098, "in", 34, "out", 36, "amp", 0.4, "gain", 1, "overgain", 0, "sample", -1128873534, "cut", 1, "sustain", 16.0552, "fadeInTime", 0, "fadeTime", 0.001 ]
]
[ "#bundle", 16676975298944028671, 
  [ 12, 1004, 1 ],
  [ 15, 1004, "resumed", 1 ],
  [ 12, 1003, 1 ],
  [ 15, 1003, "resumed", 1 ],
  [ 12, 1001, 1 ],
  [ 15, 1001, "resumed", 1 ],
  [ 12, 1000, 1 ],
  [ 15, 1000, "resumed", 1 ],
  [ "/n_set", 3, "gateSample", -1175928170, "gateCut", 2, "cutAllSamples", 1 ],
  [ "/g_new", 1099, 1, 3 ],
  [ "/s_new", "dirt_sample_1_2", -1, 1, 1099, "bufnum", 715, "sustain", 0.445122, "speed", 1, "freq", 261.626, "endSpeed", 1, "begin", 0, "end", 1, "pan", 0, "out", 34 ],
  [ "/s_new", "dirt_gate2", -1, 1, 1099, "in", 34, "out", 36, "amp", 0.4, "gain", 1, "overgain", 0, "sample", -1175928170, "cut", 2, "sustain", 0.445122, "fadeInTime", 0, "fadeTime", 0.001 ]
]

The only difference I can see from this and the similar dump from a working situation is here the two bundles have the same stamp.

Here is a shortened one:

(
fork {
	(type:\dirt, orbit:0, s: \bev, cut: 1).play;
	0.3.wait;
	(type:\dirt, orbit:0, s: \bev, cut: 1).play;
	(type:\dirt, orbit:0, s: \cr, cut: 2).play;
}
)

I think I know now. The group gets set with cut 1 and cut 2 in one block of calculation and it overrides before anything could happen that would have freed the first one.

OK, will think of a solution, may need some reworking.

I think there is no other way than to go back to the more complicated separate cut groups. I wonder if we could use the occasion to use them for something else as well.

One difficulty is that currently we are able to cut across orbits. That was a feature of the current approach that we lose if we implement the cutGroups as server groups: every orbit must have its own group, but one group can't live in several groups.

It would be possible to keep track of every synth, but that causes a lot more messaging (for freeing them when they are done).

The proposed fix avoids the complicated separate groups but instead adds a bit of timing inaccuracy for the cut. We could make up for it though, but let's see if this solution is reasonably close.

@bgold-cosmos @ritchse let me know how it goes.

commented

@telephon

This fix delays each cut group by one control period right? The one set by ServerOptions.blockSize, which defaults to 64 samples.

Just making sure.

If that is the case it seems like that could be a good fix. I don't know how high other users might set their block size tho.

Will test eventually tho and I'll let you know

Yes, that is what happens. So each cut group will cut by a blockSize earlier. So for example, at 48kHz and 64 block size that will be a bit more than a millisecond for the second cut (64/48000), and 4 ms for the fourth (64/48000*3). It could be problematic when you have a lot of cut groups.

One problem I see is that it cuts early, so that it "eats up" the latency, it'll produce "late" errors when we have too many. 0.3 sec (default) will be eaten up with 225 cut groups.

A solution would then be to delay the cut – then we get a little overlap that grows with the cut number. Again, in our example setting, the 225th cut group would be cut 0.3 sec late.

It is a band aid, but it does keep things simple.

If people change block size, they usually lower it to get tighter feedback.

commented

When we talk about the amount of cut groups, these go for each orbit (or cutAll) right? The amount of cut groups in one orbit won't affect the latency of cut groups in another one. That being the case:

I think overlapping isn't as good of an idea in some use cases. For example, if you have quite a loud constant sound such as a sine wave, white noise or a bass that doesn't decay, the overlapping will be noticeable even in very small amounts as an unprecedented peak will appear in the sound. While an early cut, even if slightly noticeable, makes more sense to me. There have been countless times when, for example, writing sub-bass patterns on a piano roll, I've had to end a note, which is followed by another one of the same pitch, very so slightly early so that it becomes noticeable they are in fact different notes. In other words I think cutting early has more sense conceptually than overlapping even if it's just some milliseconds.

yes, you are right. Also for efficiency, it may be important not to overlap (it may cause spikes).

I'll add a safety net.

When we talk about the amount of cut groups, these go for each orbit (or cutAll) right? The amount of cut groups in one orbit won't affect the latency of cut groups in another one. That being the case:

The absolute total number is what counts, independent if you cut in your group or in all groups. So if you don't set cutAll, you can reuse the low cut numbers in your orbit. Only if you cut all, you may have to move to higher numbers if you don't want to cut another orbit.

What do you think, is that reasonable?

commented

I actually don't think we are being able to communicate this last idea properly. I don't blame either you or me of course, this is quite hard to put into words without it being confusing heh. I'll make up the following term to see if it helps:

Cut supergroup: a cut supergroup would be a cut of cut groups that run independently from each other on the tidal side. For example, each orbit has it's own cut supergroup, since if you use cut 1 on both d1 and d2, they don't affect each other, even if they are using the same index to indicate a cut group, these are different since they are on different cut supergroups. There is a special cut supergroup that can be used on tidal via cutAll which will allow you to cut notes between different orbits.

Does this make sense? I what I'm saying even right? lol. at least conceptually, i know implementation might be different. if this is the case, what i was trying to say is:

latency would only build up as you use more cut groups per each supergroup, right? so the third cut group of d1 would have the same latency as the third cut group in d2. they don't affect the latency of each other. and if you use cutAll then yes, they sum up because you're using the same cut supergroup

Does this make sense? I what I'm saying even right?

yes, that's conceptually correct! :)

latency would only build up as you use more cut groups per each supergroup, right? so the third cut group of d1 would have the same latency as the third cut group in d2. they don't affect the latency of each other.

yes, latency (or earliness) is a direct function of the actual cut number value.

and if you use cutAll then yes, they sum up because you're using the same cut supergroup

no, also then, they don't sum up. It is just that if you use cutAll and you want to separate them, you need high cut numbers (because you need lots of different ones), and then you geht a high "earliness" because of that.

commented

ok totally got it now!!! i think this is the way to go certainly 😄

Actually, not solved, sorry. Will need another solution …

(reason: there still may be cuts in the same block, if you cut very quickly, from another parallel stream)

@ritchse, @bgold-cosmos if you like, you can test this branch: https://github.com/musikinformatik/SuperDirt/tree/topic-flotsam

This should work reliably, it may have a little more cpu load in sclang for very dense cutting.

d1 $ s "bev(6,8)" # cut 1

d2 $ s "bev(6,8)" # speed 2 # cut 1

as for last example, each orbit has its own cut groups so it really doesn't matter which non-0 number i put in there.

Ah sorry, I forgot about d1 and d2 being different orbits by default.

I'm coming in very late to this issue, as far as my usage goes, I regularly use cut groups across orbits in both tidal and in estuary/minitidal across cells.

My understanding is that they are not orbit specific at all - instead functioning on a global level, and this is (imo) a good thing -

I feel a bit confused by these comments because it's something I've definitely leveraged in the past.

@telephon is the flotsam proposal changing (what I understand to be) this global functionality?

@cleary

It is definitely configurable.

If it should be global by default, we need to change L 185 in DirtEvent from

var cutAllOrbits = ~cutAll ? false;

to

var cutAllOrbits = ~cutAll ? true;

We need to make sure that in current main, this is really the default behaviour. I've been on the develop branch for too long …

@cleary

It is definitely configurable.

If it should be global by default, we need to change L 185 in DirtEvent from

var cutAllOrbits = ~cutAll ? false;

to

var cutAllOrbits = ~cutAll ? true;

We need to make sure that in current main, this is really the default behaviour. I've been on the develop branch for too long …

I'll give this a test tomorrow morning to confirm :)

@telephon - here's my test

d1 $ slow 2 $ s "moog" # cut 1

d2 $ s "~ sd" # cut 1   

I'd expect the short sd to cut the longer moog sample, but it doesn't (and I'm sure this behaviour existed in the past, but I can't easily verify it).

Is this an option we can raise up to the startup.scd option level?

damn i totally forgot to report back! but i've been using the PR version for some months already and it's been working perfectly