Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controlling parameters between events via parameter buses #190

Open
jarmitage opened this issue Apr 6, 2020 · 128 comments
Open

Controlling parameters between events via parameter buses #190

jarmitage opened this issue Apr 6, 2020 · 128 comments

Comments

@jarmitage
Copy link

jarmitage commented Apr 6, 2020

E.g.:

d1 $ pan "0 1" $ s "bev" 
  # panmode "smooth" 1

When # pan "1" is sent, # panmode sets interpolation to smooth over 1 cycle duration.

@telephon
Copy link
Contributor

telephon commented Apr 6, 2020

good idea.

@telephon
Copy link
Contributor

telephon commented Jun 8, 2020

thinking again about this: what would smooth mean here?

@jarmitage
Copy link
Author

jarmitage commented Jun 8, 2020

One way this could work is that the panning runs one event or one cycle behind so that it knows where to interpolate to. Thoughts?

And, maybe smooth isn't a useful descriptor and linear ease etc would be better.

@telephon
Copy link
Contributor

telephon commented Jun 8, 2020

ah I see, so you want a pan that moves within each event. It would be possible to add a panFrom parameter.

In principle, this would be a possible desire to have for every existing parameter, so I'm not sure if it would be better to introduce some architecture for such a thing.

@jarmitage
Copy link
Author

ah I see, so you want a pan that moves within each event. It would be possible to add a panFrom parameter.

Yes, and that's a better suggestion.

In principle, this would be a possible desire to have for every existing parameter, so I'm not sure if it would be better to introduce some architecture for such a thing.

I completely agree, and I think this would overcome one of the main limitations of the Tidal-SuperDirt relationship cc @yaxu. Should we create a separate issue for that?

@yaxu
Copy link
Collaborator

yaxu commented Jun 8, 2020

We could rename this one. What would we call it. "Parameter envelopes?"

A simple case would be a list of values, equally spaced over the duration of an event (one at start one at end) that supercollider moves between linearly.

We already use : as a kind of list separator in the mini-notation (although this needs work to make the elements patternable).

The interface for that simple case could then be d1 $ sound "sax" # speed "0.5:1:<0.25 0.5>"

@jarmitage
Copy link
Author

I was thinking slightly more generally "parameter interpolation".

It's going to give parameters more independence from the main pattern structure, so there should be a clear way to use the regular approach still.

A notation for this would be great, but maybe we also want a way of doing this with an explicit parameter as well? It's always great having multiple ways of doing things, and sometimes the pattern notation can get quite dense.

What would # speed "0.5:1:<0.25 0.5>" do exactly?

@jarmitage jarmitage changed the title "panmode" parameter for different panning methods Controlling parameters between events via interpolation & envelopes Jun 8, 2020
@yaxu
Copy link
Collaborator

yaxu commented Jun 8, 2020

Yes I was just trying to start with the simplest case of linear interpolation,

# speed "0.5:1:<0.25 0.5> would make events during the first cycle have a speed starting at 0.5, going up linearly to 1, then down to 0.25. The second cycle it would go down to 0.5 instead. This would probably happen over the duration of the sound.

A helper function could turn 'monophonic' patterns into this kind of envelope e.g. # pan (slide "0 1") would be the same as pan "0:1 1:0"

We should probably start with finding out how this kind of thing is usually done with supercollider, then looking for a minimal and expressive Tidal UI to support that.

@yaxu
Copy link
Collaborator

yaxu commented Jun 8, 2020

By the way I just checked and dirt used to have a pan_to parameter a couple of rewrites ago, back in 2011 :) http://slab.org/datadirt/datadirt_1.0.0.tar.gz

@jarmitage
Copy link
Author

jarmitage commented Jun 8, 2020

I see, although I'm not sure : would be very scalable for this use case:

  • Writing/reading long patterns with many : would be hard
  • Trouble combining : with [], , and {} (e.g. "[0.5:1]:0.25 0.5")
  • : is already used by grp to mean separate lists for separate parameters, whereas this proposal suggests separating events within a single parameter pattern

All that being said, I'm afraid I don't have any better ideas off the top of my head.

Until we know a bit more how it's looking on SuperDirt side it might be easier to prototype with additional explicit parameters, and then come back to the UI/notation side later.

And that is a great archeological find - no new ideas under the sun!

@yaxu
Copy link
Collaborator

yaxu commented Jun 8, 2020

My thinking is that both are lists, so should maybe make use of the same syntax, with wider context giving meaning to what the list is of. Additional parameters will likely be needed too, for giving the type of envelope

@jarmitage
Copy link
Author

jarmitage commented Jun 8, 2020

Ok, so in this case the "lists" would be lists of patterns that describe interpolation points for the same parameter, with the first list being the "from" pattern, and subsequent lists being "to" patterns?

Wouldn't your example though need to be "0.5:~ 1:~ ~ <0.25 0.5>"? I'm struggling to grasp the temporal relations a bit here.

@yaxu
Copy link
Collaborator

yaxu commented Jun 8, 2020

tidalcycles/Tidal#397 related..

@telephon
Copy link
Contributor

telephon commented Jun 9, 2020

isn't interpolation just a special case? What you want, in a way, is to be able to change parameters on a running synth, which does the intrpolation internally, e.g. by lagging the parameter.

@telephon
Copy link
Contributor

telephon commented Jun 9, 2020

Let's assume we have a synth event called "set", defined like this (we'll need a more general solution):

~dirt.soundLibrary.addSynth(\set, (play: { ~group.set(\freq, ~freq.value) }))

Then in tidal, we could make a synth that has a long legato, and while it runs, set it:

sound "supergong set set set" # freq "200 700 100 500" # legato 4

(I realise that it would be nice to have a legato that is relative to the cycle length)

The synth supergong has no interpolation in its values, but this could work. Still need to test.

Would this lead to a solution in your direction?

@yaxu
Copy link
Collaborator

yaxu commented Jun 9, 2020

That would only work for monophonic synths, unless we made progress on #133.

My suggestion is to send a parameter as an array at trigger time, that sets an envelope on that parameter.

@yaxu
Copy link
Collaborator

yaxu commented Jun 9, 2020

I had a look at the OSCv1.1 spec and couldn't see support for lists

@telephon
Copy link
Contributor

telephon commented Jun 9, 2020

That would only work for monophonic synths, unless we made progress on #133.

My suggestion is to send a parameter as an array at trigger time, that sets an envelope on that parameter.

The suggestion above is independent of all this. You can set the orbit's group, and this will set the parameters of all synths in that orbit.

So (I thinkt at least) independently of this:
http://opensoundcontrol.org/spec-1_0 down below:

[ | Indicates the beginning of an array. The tags following are for data in the Array until a close brace tag is reached.

etc.

@yaxu
Copy link
Collaborator

yaxu commented Jun 9, 2020

I see. You might still want different sounds in the same orbit with different frequencies.

I couldn't get the example to work, I get "Instrument isn't found" for "set"

@telephon
Copy link
Contributor

I think it should be:

~dirt.soundLibrary.addSynth(\set, (play: { ~synthGroup.set(\freq, ~freq.value) }))

@yaxu
Copy link
Collaborator

yaxu commented Jun 10, 2020

Hm I'm still getting the same:

-> a DirtSoundLibrary
module 'sound': instrument not found: set
module 'sound': instrument not found: set
module 'sound': instrument not found: set

If I run the addSynth line again I get

replacing 'set' (1)
-> a DirtSoundLibrary

So it seems it's definitely adding it.. Strange.

@telephon
Copy link
Contributor

telephon commented Jun 10, 2020

yes, I had the same. I need to investigate why this happens. Also I need to refactor the DirtEvent a little, need to first get the group id so that the synths can grab it.

This will be a good first step. If we want to separate the synths, we can think how to do this. There is still the possibility to allocate groups and put synths in groups. Alll this adds complexity though.

@telephon
Copy link
Contributor

telephon commented Jun 10, 2020

Here is a first example that actually works:

(
~dirt.soundLibrary.addSynth(\set, 
	(play: { |dirtEvent|
		var group = dirtEvent.orbit.group.asGroup;
		group.set(\freq, ~freq.value) 
	})
);

SynthDef(\supertest, {|out, freq=440, sustain=1, pan |
	var sound = RLPF.ar(Pulse.ar(freq.lag(1)), (freq * 3).lag(2), 0.05);
	Out.ar(out, DirtPan.ar(sound, ~dirt.numChannels, pan))
}).add
);

Then you can write:

sound "supertest set set set" # freq "200 700 100 500" # legato 4

@telephon
Copy link
Contributor

telephon commented Jun 10, 2020

What we could do is add control routing busses to superdirt (we already have audio routing busses):

(
~dirt.addModule(\get, { |orbit|
	var bus = orbit.dirt.controlBusses.wrapAt(~fromBus);
	orbit.server.sendMsg("/n_map", ~setParam, bus.index);
}, { ~fromBus.notNil })
)


(
~dirt.addModule(\set, { |orbit|
	var bus = orbit.dirt.controlBusses.wrapAt(~setBus);
	bus.set(~toValue);
}, { ~setBus.notNil })
)

And then we could write in tidal, after having defined the right parameter functions:

sound "supertest" # setParam "freq" # fromBus "7" # setBus "7" # toValue "403 508 201 703"

Both parts, the setParam and the setBus could happen in completely different orbits.

Currently, we can do this already with audio signal routing, but not with setting controls.

@telephon
Copy link
Contributor

telephon commented Jun 11, 2020

P.S. I've added controlBusses now, so the above code should almost work.
In tidal:

  let fromBus = pI "fromBus"
      setBus = pI "setBus"
      toValue = pF "toValue"
      setParam = pS "setParam"

@telephon
Copy link
Contributor

telephon commented Jun 11, 2020

OK, here we go:

  let fromBus = pI "fromBus"
      setBus = pI "setBus"
      toValue = pF "toValue"
      setParam = pS "setParam"
      fadeTime = pF "fadeTime"
  
d1 $ stack [
      sound "supertest" # sustain 4 # fadeTime 1 # setParam "freq" # fromBus "7" ,
      sound "supersilent*4" # setBus "7" # toValue "403 508 201 703"
      ]

SuperDirt:

(
SynthDef(\supertest, {|out, freq=440, sustain=1, pan |
	var sound;
	sound = RLPF.ar(Pulse.ar(freq.lag(1)), (freq * 3).lag(2), 0.05);
	Out.ar(out, DirtPan.ar(sound, ~dirt.numChannels, pan))
}).add;
SynthDef(\supersilent, {|out, freq=440, sustain=1, pan |
	var sound;
	sound = Silent.ar(~dirt.numChannels);
	Out.ar(out, DirtPan.ar(sound, ~dirt.numChannels, pan))
}).add;

~dirt.addModule(\get, { |event|
	var bus = event.orbit.dirt.controlBusses.wrapAt(~fromBus);
	event.orbit.server.sendMsg("/n_map", ~synthGroup, ~setParam, bus.index);
}, { ~fromBus.notNil });

~dirt.addModule(\set, { |event|
	var bus = event.orbit.dirt.controlBusses.wrapAt(~setBus);
	bus.set(~toValue);
}, { ~setBus.notNil })
)

Does this go in the right direction?

@yaxu
Copy link
Collaborator

yaxu commented Jun 20, 2020

Instead of sending

/play2 "sound" "supersaw" "setParam" "freq" "fromBus" "7"

... could tidal instead send the bus value with the name prefixed e.g. by '_'?

/play2 "sound" "supersaw" "_freq" 7

Then there could be multiple things set in one message

/play2 "sound" "supersaw" "_freq" 7 "_squiz" 8

Instead of using '/play2' for setting a bus value, tidal could use a different OSC path, e.g.

/set 7 0.4

@telephon
Copy link
Contributor

telephon commented Jun 20, 2020

This would work, but would incur a bit of calculation cost on all messages (we need to check every argument name for the prefix, but there is a primitive that is quite efficient).

If this is ok, the nicest way would be:

// getting a bus value from a bus named "theFreq"
"_freq" "theFreq"
// and setting the bus value
"theFreq_" 567

But maybe numbers are just fine as well.

Assuming that we do /set 7 0.4 (which would work much better!).

A very direct way would be to send:

/play2 "sound" "supersaw" "freq" "c7"

We don't have to change anything on the superdirt side for that really. (Note that this won't work for all parameters, e.g. n doesn't work).

But: Then we need to guarantee that superdirt has the relevant bus numbers. That probably means that tidal needs to know the busses that superdirt has, and also how many. Superdirt could send them to tidal.

@telephon
Copy link
Contributor

telephon commented Jun 20, 2020

The possibility of having synths playing out into audio busses which can then be mapped to arguments is already implemented, but this works differently (there, you mostly don't want to have the same pattern write and read).

@yaxu
Copy link
Collaborator

yaxu commented Oct 10, 2020

maybe something like this or below: #190 (comment)

Did you mean a different link?

@telephon
Copy link
Contributor

If you wanted, you could also ask for each bus separately from dirt via OSC. Then there is no need for writing an allocator.

@yaxu
Copy link
Collaborator

yaxu commented Oct 11, 2020

Thanks! I guess a network ping pong each time a bus is used would slow things down though?
I am super nervous about using strings as keys though. I've realised I have an irrational fear of string comparison. I guess it's not a super efficient thing to do, and haskell's default string representation is known to be a bit slow. Maybe it's worth switching Tidal to a more efficient string type https://mmhaskell.com/blog/2017/5/15/untangling-haskells-strings
That blog post suggests that it's only string manipulation that's inefficient with basic strings, and I don't think we do much of that..

@telephon
Copy link
Contributor

telephon commented Oct 11, 2020

I guess a network ping pong each time a bus is used would slow things down though?

You'd need it only the first time you define a pattern that uses the bus. But maybe yes.

I am super nervous about using strings as keys though.

I also think that strings are not necessary. Numbers are fine, if there is an implicit number allocator.

@telephon
Copy link
Contributor

I am super nervous about using strings as keys though. I've realised I have an irrational fear of string comparison. I guess it's not a super efficient thing to do, and haskell's default string representation is known to be a bit slow.

Not sure if we need it at all, but you could use Symbol, couldn't you? Usually, symbols have a unique identity, so they are very fast in checking for that.

@yaxu
Copy link
Collaborator

yaxu commented Jan 8, 2021

I got a bit bogged down in the technicalities of this, so keen to get a simple version released..

@telephon by Symbol, do you mean this:
https://hackage.haskell.org/package/symbol

Or is this something on the SuperCollider side?

Anyway to reiterate, a simple start would be to support something like this to allocate a bus (denoted by the suffix _):

d1 $ sound "sax" # vowel_ "a e i o"

That reaches the scheduler as the identifier d1vowel or similar, which will retrieve or allocate a bus number, and will send it but only if it is different from the previous value that was sent.

I think that's enough for now. It might be nice to be able to pattern the bus ids, so that one parameter could switch between receiving values from different busses, but that mgiht be taking it too far..

@telephon
Copy link
Contributor

telephon commented Jan 9, 2021

@telephon by Symbol, do you mean this:
https://hackage.haskell.org/package/symbol

Or is this something on the SuperCollider side?

I think it is better if tidal can maintain the binding between names and bus indices, unless that's really too much work. Otherwise, superdirt can also do this, as long as tidal makes sure the timing is correct and names are marked as freed after use somehow.

That reaches the scheduler as the identifier d1vowel or similar, which will retrieve or allocate a bus number, and will send it but only if it is different from the previous value that was sent.

So this is now for writing the bus signal, right? Then yes, since the synth lives across the messages it will keep the mapping. But this is just a matter of optimisation.

For the synth that reads the bus signal, you'll have to send a "c12" like parameter in each message.

I hope I understood you correctly.

@yaxu
Copy link
Collaborator

yaxu commented Jan 10, 2021

Yes just a matter of optimisation, but currently without this tidal will generate a lot of spurious identical bus messages at 20hz. Mainly I was just reiterating things after returning to this after a break.

Though this turns out to be difficult:

d1 $ sound "sax" # vowel_ "a e i o"

Because of the book keeping needed to support multiple versions of the same parameter e.g.:

d1 $ stack [sound "sax" # vowel_ "a e i o", sound "kick" # vowel "a o i", sound "clap" # vowel_ "e a u"]

I think they have to have unique identifiers, so I might go back to using 'dial'. I find this fiddly though..

d1 $ stack [sound "sax" # (dial 3 $ vowel "a e i o"), sound "kick" # vowel "a o i", sound "clap" # (dial 4 $ vowel "e a u")]

So perhaps something like this is better

d1 $ stack [sound "sax" # vowelbus 3 "a e i o", sound "kick" # vowel "a o i", sound "clap" # vowelbus 4 "e a u"]

That doubles the size of Params.hs.. Or I think we could use template haskell to generate the declarations and make it much smaller than it is.

@telephon
Copy link
Contributor

The explicit version (dial) is a good intermediate step, maybe. To go further, it seems that you need a way to identify the start and end of a particular subpattern?

Then the ids for the dials could be hidden under a hood. And they could be overridden as well to make cross pattern communication possible.

@yaxu
Copy link
Collaborator

yaxu commented Jan 23, 2021

Ok I'm playing around with this for the first time, e.g.:

d1 $ sound "sax"
  # resonance 0.2
  # cutoffbus 3 (fast 4 $ segment 32 $ range 0 3000 saw)
  # ampbus 5 "[1.5 0]*8"

This sounds nice but there's seems to be a strange lag. If I comment out the ampbus line for example, it takes a couple of seconds for it to fade to 1, and then if I uncomment it, it similar amount of time to fully take effect..

@telephon
Copy link
Contributor

Maybe let's reconsider what happens on the OSC level? What are the messages that are sent with # ampbus 5 "[1.5 0]*8"?

@yaxu
Copy link
Collaborator

yaxu commented Jan 23, 2021

Ok this simpler pattern has the same kind of behaviour:

once $ sound "sax"
  # resonance 0.2
  # cutoffbus 3 "2000 1000 500"
  # ampbus 20 "[1.5 0]*4"

Interestingly it sends 20 events, one per unique effect onset times two as there's two effects to send. So it doesn't actually send the 20Hz on top as I'd expected.. Hmm!!

The /dirt/play looks like this:
image

Then the first /c_set looks like:
image

(I haven't implemented sending multiple parameter sets in one message yet)

The rest of the /c_sets look fine..

@yaxu
Copy link
Collaborator

yaxu commented Jan 23, 2021

A strange thing is that if I play this:

d1 $ sound "sax"
  # resonance 0.2
  # cutoffbus 3 "2000 1000 500"
  # pan 1

Then change the pan to 0, I hear the sound panning from one side to the other, over a couple of seconds. So there's a weird lag on all the effects if I use the bus controls on one of them.

@telephon
Copy link
Contributor

I would have said that amp isn't supported yet (because we have the formula ~amp = pow(~gain.value.min(2) + ~overgain.value, 4) * ~amp.value in DirtEvent, which, in the case of ~amp = "c10" will ignore gain and overgain.

But what you describe doesn't quite fit into this image.

@telephon
Copy link
Contributor

telephon commented Jan 23, 2021

For debugging, you could use a synth and a special parameter:

SynthDef(\debug, { |testvalue|
testvalue.poll(Impulse.kr(3));
}).add
d1 $ sound "debug" # testvaluebus "2000 1000 500"

@yaxu
Copy link
Collaborator

yaxu commented Jan 23, 2021

I'm afraid I have been rather stupid. 'sax' is a long sound, and I forgot to add a 'cut' or 'legato' parameter. The amp cutting the sound for everything in sync was confusing me, so I didn't realise that different instances of the sound were overlapping.

@yaxu
Copy link
Collaborator

yaxu commented Jan 23, 2021

Playing with ampbus and it seems to work fine. It's just a little bit surprising that amp 1 is louder than no amp parameter

@yaxu
Copy link
Collaborator

yaxu commented Jan 23, 2021

panbus works fine too, but as expected is in the -1 to 1 range. I could put a hack into the scheduler to fix this.

@telephon
Copy link
Contributor

It's just a little bit surprising that amp 1 is louder than no amp parameter

#220

@yaxu
Copy link
Collaborator

yaxu commented Feb 2, 2021

Just reading through the discussion to see what was lost in this simple implementation.. Maybe not too much, people have to handle allocating bus numbers by hand but that's OK I think.

Currently if Tidal doesn't manage to handshake straight away (i.e. if superdirt is started after tidal) then it just sends the bus ids that the user specifies, without using a mapping sent from SuperDirt. I think this works as long as superdirt is the only thing running in supercollider? The bus allocations I see are always contiguous from 0 in any case.

However it would be best to have a successful handshake in the usual cases:

  • Superdirt starts before Tidal
  • Tidal restarts while superdirt is running
  • Tidal starts before superdirt
  • Superdirt restarts while tidal is running

The first two already work in Tidal 1.7. The third I've got working locally, with a simple loop waiting for a handshake.

The fourth case would be solved by this:

> /dirt/play "s" "bd"
< /dirt/hello
> /dirt/hello/reply tidal
> /dirt/handshake
< /dirt/handshake reply <data burst>

i.e. a restarted superdirt receives a /dirt message from a port it hasn't shaken hands with, and sends a /dirt/hello. Then tidal knows it has to handshake. (actually the /dirt/hello/reply seems redundant here, the next message could just be the handshake initiation)

Looking at the source and behaviour though the /dirt/hello seems to be implemented to go in the other direction?

@yaxu
Copy link
Collaborator

yaxu commented Oct 7, 2022

@telephon is accelerate intended to work with busses? I'm guessing not for the same reason as speed
tidalcycles/Tidal#956

@telephon
Copy link
Contributor

telephon commented Oct 16, 2022

No, accelerate doesn't work with busses. It would be complicated to implement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants