Conditional branching with state

Started by paolo, Jan 23, 2023, 05:41 PM

paolo

Hello!

These days I'm working on introducing conditional branching in Ciaramella. Implementation is currently in debugging phase and I am now focusing on writing a scientific paper about if for SMC23.
Stefano already wrote a summary post about it https://www.orastron.com/blog/conditionals-ciaramella

As an example, here's a "decimator" (if I understand correctly):

y = downsampler(x) {
y, s = if (delay1(s)) {
y = x
s = 0
} else {
y = delay1(y)
s = 1
}
@s = 1
@y = 0
}

and here's a dumb synth:

y = synth_saw(enable, fr) {
y = if (enable) {
phaseInc = fr / fs
phase = frac(delay1(phase) + phaseInc)
@phase = 0
y = phase * 2 - 1
} else {
y = 0
}
}

Point is that branches can have their inner and private variables and state, like phase e.g..
As far as I know and see, this feature is rare to find in a block-oriented audio programming language. For example, Faust only has a "Select" operator, same goes for Max and others.

I was wondering if anyone from this fresh community has any experience with Reaktor and knows if something similar is supported. I know that there is an "if" operator, but can branches own state? What if I insert delays in the branches and create a loop from the output of the IF to its input? Does it "schedule" and compile/run it?

Thank you in advance!

stefano

Personal website: https://dangelo.audio/

sletz

Faust select2 primitive semantic is precised here.

paolo

Quote from: sletz on Jan 23, 2023, 06:11 PMFaust select2 primitive semantic is precised here.
Thank you for the links. So it confirms that all the branches are always semantically computed and you cannot have an inner branch state, since it will be updated in any case.

sletz

I guess what you are doing in Ciaramella will be achievable in Faust with the ondemand new primitive see: https://www.youtube.com/watch?v=LVnSSmu5g9g&t=354s

stefano

From what I understand the approach would be practically similar even though the theory behind it is quite different.

Essentially we can associate a set of boolean conditions to an operation, and that operation is only executed when all such conditions are true or when no conditions are associated to it. Having a boolean value controlling the execution and updating of values gives the same result.
Personal website: https://dangelo.audio/

david_a

#6
I'm not sure specifically how the Reaktor core compiler works internally but conditional objects work in tandem with 'routers', routing the flow of events.

A conditional 'compare' module in Reaktor core is always used with a router module. Divergent streams from this are then combined downstream with a 'merge' module, like this:



Note the green 'boolean' wire that is only used with these conditional modules.

OBC (Object Bus Connections ) connected to memory are used for persistent variables (I think the idea for OBCs were they could be used for lots of things but currently they are only used for memory and arrays.)

This is a simple 'Latch' macro that uses a 'memory write' module and a 'memory read' module connected together using their OBC ports (coloured brown):



Events from the lower input port will trigger a read from the 'memory read' module it's connected to, the same memory (variable) is written to by the upper input port. Only events from the lower input port will trigger events sent to the output of this macro.

The order of the OBC connections dictates the order of reads and writes, so you could reverse the order of the connection like this to to get a 'Latch[-1]':



equivalent to 'delay1' like this -->



So for you would use these memory read and write modules for private variables and state, with the OBC wires defining the scope.

You can name any wire in Core, turning it into a 'QuickBus', the scope of the these QuickBuses is restricted to just the current macro, not parent or sub macros. You can extend the scope to the parent by changing the 'Solid' setting of the macro (you can also create a 'Scooped Bus' which is like a QuickBus but the scope extends to all sub macros). Here's the latch macro with QuickBuses -->



I can make a couple of screenshots based on your examples if that would be useful to see how they could be implemented in Reaktor Core?

stefano

Thanks a lot @david_a, very clear explaination.

I only wonder if you could have a router in one macro and and the corresponding merge in another, and also if you can actually have zero-delay feedbacks around macros that contain routers/merges (we managed to get the equivalent thing working in Ciaramella).
Personal website: https://dangelo.audio/

david_a

Quote from: stefano on Jan 24, 2023, 03:04 PMI only wonder if you could have a router in one macro and and the corresponding merge in another

Yes you can just have output ports for each branch and merge them back at any point in the structure, if there are multiple audio-rate streams arriving simultaneously at a single merge module then the stream at the lowest input port of the merge module takes precedence.

You can also do stuff like route sample clock signals into macros to enable / disable processing, like this:




vs



Quote from: stefano on Jan 24, 2023, 03:04 PMand also if you can actually have zero-delay feedbacks around macros that contain routers/merges (we managed to get the equivalent thing working in Ciaramella).

There's a ZDF library, created by Vadim Zavalishin included in the factory library that does something like this:



He demonstrates the library here:

https://blog.native-instruments.com/the-art-of-dsp-in-reaktor-with-vadim-zavalishin/

Vadim originally created Reaktor core, so would be a great person to ask about how the Core compiler works, I might be able to send him some questions maybe, or invite him to the discussion! :)

stefano

Firstly, thank you so much for taking the time to explain us all that.

Quote from: david_a on Jan 24, 2023, 04:29 PM
QuoteI only wonder if you could have a router in one macro and and the corresponding merge in another

Yes you can just have output ports for each branch and merge them back at any point in the structure, if there are multiple audio-rate streams arriving simultaneously at a single merge module then the stream at the lowest input port of the merge module takes precedence.

I'm not sure we're talking about the same thing. I meant something like I have a macro A whose outputs go into a macro B, and there is one/multiple routers in macro A but no merges, and one or multiple merges in macro B but no routers.

Quote from: david_a on Jan 24, 2023, 04:29 PMHe demonstrates the library here:

https://blog.native-instruments.com/the-art-of-dsp-in-reaktor-with-vadim-zavalishin/

Vadim originally created Reaktor core, so would be a great person to ask about how the Core compiler works, I might be able to send him some questions maybe, or invite him to the discussion!

Ok, I'll try to get him here. There are many interesting details involved in solving these sort of problems at a language/compiler level that can't be easily guessed by just using software.
Personal website: https://dangelo.audio/

david_a

Great! :) Yes what you described is possible.

stefano

I had a quick chat with Vadim Zavalishin and it looks like we won't be getting much insight on how ReaktorCore works internally. Understandably, it's part of a commercial project and he is just not allowed to reveal details. :(
Personal website: https://dangelo.audio/

paolo

#12
Quote from: david_a on Jan 25, 2023, 12:06 PMGreat! :) Yes what you described is possible.

Well, thanks for all the info and examples you provided.

Anyway, we just implemented conditionals in Ciaramella (some bug is still out there ofc) and submitted a paper describing the theory behind it. News as soon as it will be accepted (crossed fingers).

stefano

Ok, we're really late (sorry), but here's the paper we were talking about with all detail (presented at SMC 2023 in Stockholm last month).

The Zampogna compiler already has them implemented and working, and you can also use them in the web IDE.
Personal website: https://dangelo.audio/