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

Support for control bus changes #762

Closed
yaxu opened this issue Jan 19, 2021 · 18 comments
Closed

Support for control bus changes #762

yaxu opened this issue Jan 19, 2021 · 18 comments

Comments

@yaxu
Copy link
Member

yaxu commented Jan 19, 2021

I've been a bit stalled on implementing the tidal side of this: musikinformatik/SuperDirt#190

We need a way to label values that are not sent with the trigger message, but with later control messages direct to scsynth. The value needs to be associated with a bus identifier (probably an integer), which gets sent in place of the value in the trigger message, so that superdirt reads from that bus, and tidal later sends control changes to that bus.

I've tried a couple of approaches.

One is to add a vbus field to every Value constructor (VS, VF, VB etc). This kind of worked and is how things are in the main branch. One disadvantage is that it changes the interface for describing OSC 'targets', adding an extra, generally unused parameter to every control.. But we could add some helper functions with an improved interface.

https://github.com/tidalcycles/Tidal/blob/main/src/Sound/Tidal/Pattern.hs#L279

Then I tried removing vbus and instead making a new VBus constructor for the Value datatype:

00d5a2f#diff-c7cfc09d6380f2dbbc46f6f2d9cba755924358f0429a782c7c746ff7c409007fR293

That seemed to be a bit tidier, but I realised a bit late that this makes manipulating and combining patterns more complicated, e.g. adding together controlpatterns.

Then the current thing I'm trying is adding a Control datatype, composed of the Value and Meta types:

data Meta = Meta {mBus :: Maybe Int}
  deriving (Eq)

data Control a = Control {cMeta :: Meta, cValue :: a}

type ControlMap = Map.Map String (Control Value)

type ControlPattern = Pattern ControlMap

I guess I was thinking this would be more extensible, if we needed to add more metadata. But implementing this is a bit of a pain, with the extra stage of misdirection.. Maybe a case for 'arrows' but I've not managed to get my head around those.

Thinking about it though we already have metadata stored at the event level - the 'context', where source code positions are kept. So perhaps it would make more sense for the bus id to be stored there..? I think that would be better as it means that values could be associated with bus ids either before or after they get associated with control names.

It seems I'm mainly rubberducking here but thoughts very welcome!

@yaxu
Copy link
Member Author

yaxu commented Jan 19, 2021

Actually using the current 'context' wouldn't work, because that would be applied to the whole value, which for controlpatterns is the whole dictionary. Bah.

@yaxu
Copy link
Member Author

yaxu commented Jan 19, 2021

I think some of the confusion is trying to represent two things at once:

d1 $ sound "sax" # pan (bus 2 "0 0.5 1")

That is both setting the pan parameter to bus 2, and sending a pattern to that bus. So maybe it's clearer like this:

d1 $ sound "sax" # panbus 2 # busF 2 "0 0.5 1"

Then if pan_ n pat = panbus n # busF n pat, this would work:

d1 $ sound "sax" # pan_ 2 "0 0.5 1"

Or maybe just leave pan as it is, and use another parameter to 'send' it places:

send :: String -> Pattern Int -> ControlPattern
send name busid = fmap (makeControlMap ('_':name) . VI)

d1 $ sound "sax" # pan "0 0.5 1" # send "pan" 2

Then the scheduler could just look for _pan, and if it exists redirect it via a bus.

Then receive could be implemented somehow too:

d3 $ sound "sax" # receive 2 "pan"

It's a bit annoying having parameter names as barewords for function names and as quoted strings as parameters though..

@yaxu
Copy link
Member Author

yaxu commented Jan 19, 2021

Again though there could be shorthand:

pan_ busid pat = pan pat # send "pan" busid
d1 $ sound "sax" # pan_ "0 0.5 1"

@yaxu
Copy link
Member Author

yaxu commented Jan 20, 2021

This is nice because it doesn't require any changes to the underlying types.. But this means that it's "stringly typed", in particular that the type of a bus value comes from its name in a controlpattern hash being prefixed by an _.

@yaxu
Copy link
Member Author

yaxu commented Jan 20, 2021

Ah actually it's much simpler than all this! Just set the parameter to the string value of c<busid> from the start:

panbus :: Pattern Int -> Pattern Double -> ControlPattern
panbus busid pat = pF "^pan" pat  # pS "pan" (fmap (('c':) . show) n)

Then all the scheduler has to do is redirect parameters starting with ^ to scsynth.

Then to only receive from (and not send to) that bus it'd simply be:

panrecv :: Pattern Int -> ControlPattern
panrecv n = pS "pan" (fmap (('c':) . show) n)

@yaxu
Copy link
Member Author

yaxu commented Jan 21, 2021

Well that would break e.g. |+ pan 0.1 but not if it was more like:

panbus :: Pattern Int -> Pattern Double -> ControlPattern
panbus busid pat = pF "pan" pat  # pI "^pan" busid

and then the scheduler swapped things around.

@telephon
Copy link
Contributor

telephon commented Jan 21, 2021

Well that would break e.g. |+ pan 0.1

This sounds like something that almost logically occurs with this kind of bus mapping. You normally have numbers, which support all kind of math, then you allow the same system to use bus mappings to pass signals. But then the backend would theoretically have to replicate all the math on numbers on the bus system.

Of course superdirt/sc-server does support math on signals, but to implement something that would make this work is almost like designing an intermediate language that sends and maintains synthessis graphs on the engine.

In sclang, these operations of bus symbols are usually just ignored.

@yaxu
Copy link
Member Author

yaxu commented Jan 21, 2021

I think it's no problem to do all the maths on the tidal side.

@telephon
Copy link
Contributor

I think it's no problem to do all the maths on the tidal side.

I meant continuous audio signals (the values on a control bus). For example, you read some bus value written by some pattern from some other pattern. As long as you just use that value for sending the OSC message, that's fine. But if you want to multiply it with some value first, it gets tricky. That can't be done in tidal, and doing it in superdirt is complicated because of the potential complexity of calculations.

@yaxu yaxu changed the title Metavalues Support for effect bus changes Jan 22, 2021
@yaxu yaxu changed the title Support for effect bus changes Support for control bus changes Jan 22, 2021
@yaxu
Copy link
Member Author

yaxu commented Jan 22, 2021

Hm maybe we are talking about different things. I was thinking about this sort of thing:

d1 $ every 3 (+ pan "0.25 0.125") $ sound "sax" # panbus 3 "0.125 0.25 0.75"

This is working now - I just had to make sure all the pan patterns had the same name, and the bus id (actually also a pattern) is kept separately and dealt with by the scheduler while constructing OSC messages to sclang (/dirt/play) and scsynth (/c_set).

A remaining problem is that querying a tidal pattern can cut events into parts. As tidal queries at a rate of 20Hz that's the minimum number of /c_set messages sent per second. I'll implement something so that it only sends a value to a bus if it's changed from the previous value, but feel that's a workaround and this should really be fixed in the representation.

E.g.

"0.5"

The event represents one 0.5 event per cycle.

"0.5" |+ "0.25 0.125"

The above represents one event but in two parts, one of which has value 0.75 and the other 0.625.

The problem is that these look the same:

queryArc ("0.625" :: Pattern Double) (Arc 0.5 1)
queryArc ("0.5" |+ "0.25 0.125" :: Pattern Double) (Arc 0.5 1)

The caller can't tell that the latter represents a changed value. They both return 0-(½>1)|0.625, i.e. the second half of an event with value 0.625.

This is one of those problems where I really don't know whether there is a very simple solution or whether it's actually impossible to solve.

@telephon
Copy link
Contributor

Hm maybe we are talking about different things.

yes, we were.

A remaining problem is that querying a tidal pattern can cut events into parts. As tidal queries at a rate of 20Hz that's the minimum number of /c_set messages sent per second.

On the sc-synth side, this kind of load is not a problem, I think. So one could first try and then optimise afterwards?

@yaxu
Copy link
Member Author

yaxu commented Jan 22, 2021

On the sc-synth side, this kind of load is not a problem, I think. So one could first try and then optimise afterwards?

Ok!

@yaxu
Copy link
Member Author

yaxu commented Jan 22, 2021

This means that if someone did:

d1 $ sound "sax" # panbus 3 sine

The sine would be sampled at 20Hz. They could increase that by being explicit

d1 $ sound "sax" # panbus 3 (segment 100 sine)

That would be sent at e.g. 100Hz if the cps was set at 1. Well it would be sent more often than that unless the underlying 20Hz framerate was perfectly in phase with the 100Hz segmentation.. Still with 100 new values per second, but some repeated. This all seems fine.

@telephon
Copy link
Contributor

It would be good to aggregate messages from several parallel running patterns. The OSC spec of sc-synth supports this, you can send to many busses at once: "/c_set, bus_index_1, value_1, bus_index_2, value_2, …". Then the load would be constant.

@yaxu
Copy link
Member Author

yaxu commented Jan 22, 2021

Ok, this shouldn't be very difficult to implement. In the end there is only really one running pattern, they all get combined into one for scheduling.

@yaxu
Copy link
Member Author

yaxu commented Jan 23, 2021

This is a lot of fun! It does get in the way of patterning things though

For example this doesn't really work:

d1 $ jux (rev . (|+ note 7)) $ sound "sax" # cutoffbus 1 "1000 2000 3000" # legato 1 # resonance 0.2

Because the cutoff control goes via a single bus, the two layers are in conflict and it sounds like the cutoff isn't reversed.

There is a workaround:

d1 $ jux (rev . (|+ note 7) . (|+ cutoffbus 1 0)) $ sound "sax" # cutoffbus 1 "1000 2000 3000" # legato 1 # resonance 0.2

Adding a cutoffbus of 1 with a value of 0 adds 1 to the bus id, and nothing to the cutoff value, so the cutoff goes via a different bus and you can hear it descending properly. A bit strange but works!

@telephon
Copy link
Contributor

ah yes, that's why I thought the busses would need to get allocated implicitly – but of course that is complictaed.

@yaxu
Copy link
Member Author

yaxu commented Jan 23, 2021

Yes definitely complicated. This'll do for now!

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

2 participants