Cabbage Logo
Back to Cabbage Site

Bézier Curves for Gentables

Hi,

What do you think about having an edit option for drawing gentables with Bézier Curves? Would that be to difficult to implement? Maybe gentable could use Bézier curves all the time but, for instance, when you hold down the shift key they could show the third point in each segment for quadratic curves? Interesting and good-looking envelopes could be designed with that kind of tool, don’t you think? Free-hand mouse drawing could also be a thing, maybe.

Just a thought… Cheers

1 Like

I looked into this before. I thought the best thing to do would be to use something similar to how SVGs implement these curves:
https://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands
So each segment is described by it’s lineto points and controls points. This is also the way JUCE implements these curves. Nice. But…

How do we save these curves so they can be edited again? So far I’ve made a point of avoiding the need for Cabbage to save any aux data in order to generate gentable data. For example, Cabbage can create each point in a gen5/gen7 table by reading the table’s fstatement. In order for this to work with bezier curves we would need to also implement a bezier curve gen routine. It just so happens that I starting writing one before, but stopped due to other distractions. I think we would need this in order to keep consistency in how Cabbage works with tables.

It might be Ok to simply create a nice curve and then save all it’s contents to a gen02 for example. But wouldn’t be frustrating to open that curve again later and not have access to all its control points?

Oh, I see. And how about passing a second gen02 as a “ctrlTable” parameter or some other name in that fashion. The only thing to care about by the user would be that the size of this table should be at least equal to the size of the table containing the lineto points.

The code should check then if a ctrlTable was passed. If it was, draw quadratic beziers. If it wasn’t, do the same as gentables do now. That way we could assure that gentables still work with previously written code.

How does active(1) change Csound f-tables? I’ll see if I can find it in the code. But gentables aren’t saved in the .snaps, right? I mean it is still up to the user to dump table data to a file from inside Csound, or am I missing something?

EDIT: I understand how you deal with tables now. The idea above could still be applied. Gentable’s main table should be a gen02 updated by Cabbage with the newly generated Bézier segments; ctrlTable should be a gen02 with coordinates for P1, Ctrl1, P2, etc. This should only apply whenever ctrlTable is being passed, of course.

How would Csound know a table is a ctrlTable? All it knows is that it is a gen02 table?

Cabbage looks at the table arguments on startup.
https://github.com/rorywalsh/cabbage/blob/master/Source/Plugin/CabbagePluginProcessor.cpp#L1546
Yeah, it’s up to the user to save table if they wish. Csound will read a table on startup, and based on the gen rountine used, it will generate editable points.

Again, how would we know which table is the control table? I think it would be easier to generate a new gen routine that creates curves based on bezier line segments. It really shouldn’t be that tricky. We could then read them as we do any other supported gen routine?

Of course the better solution would be to have a new gen routine. But to answer your questions: Csound doesn’t need to know anything about ctrlTables because Cabbage is the one generating the curves.

I thought it like this. You pass a big gen02 table to gentable as usual and also another gen02 table with P1, Ctrl1, P2, Ctrl2, etc. Cabbage detects that an auxiliary table has been passed and then computes the bezier curves using the aux. table data. After that it’s just replacing the main table (hfgens and InputMessage if I’m not mistaken) with the data computed by Cabbage.

When the user edits the gentable, Cabbage would have to also update the ctrlTable. If working with Cabbage only the user could save only the ctrTable. If instead you wanted to reuse the Bezier table in pure Csound, you would have to save the main table.

Again, much easier with a new gen routine. But I have no idea how to make one. In the meantime I’ll see if I can use a javascript library or pd to generate a string and send it through OSC to Csound with the data for a gen02 table.

How would Cabbage detect it, or even know that a particular GEN02 auxiliary table needs to be used in conjunction with another table. Are you proposing we parse the Csound text file? I’m not a big fan of parsing the Csound files ourselves. I much rather leave that up to Csound.

It’s not that tricky. You can read about it here. And I found the following to be a good starting point:

I’d be happy to help. If I remember correctly, I had some issues writing sample data that corresponded to the curves I was creating, but it was years ago when I tried this last. I will try to dig out the code I had, but I fear it is long gone.

This would be with the same method gentables know their tables now. For Bézier curves you’d have to pass tableNumber and also ctrlTableNumber or something like that. Then in C++ check if the argument has been passed or not.

That’s great! So you can have your own routines as opcodes? If I have some free time this week I’ll see if I can study how to extend Csound. Although I’d prefer to continue studying Cabbage’s source code as I’m just starting to feel comfortable with JUCE, as to be able to help you fix things. Maybe I may even ask you a few “Why did you do that” kind of questions regarding the code if you don’t mind.

So we would use one of the table slots to mark the table as a ctrlTable? Sorry, I’m still having difficulty understanding this! Ha! Maybe you could present the syntax of how the tables would look?

By all means do! I’m more than happy to share the development workload! In fact, it might be time we started looking at the new parameter system in JUCE. They have moved away from using the setParameter() methods, which Cabbage still uses. I have started to wonder if we shouldn’t follow their examples. If anything it would modernise the JUCE code-base.

Btw, I found some old code I was using for the bezier function tables. If I get a chance I will wrap it up in a function table and send it on. I’m pretty sure it doesn’t work, but it might be a good start. In my implementation the syntax went like this(btw, I was creating a named gen routine):

fn 0 size “quadbezier” y1, cx1, cy1, x2, y2 [cx2, cx2, dur2, y3, etc…]

But I’m now wondering if we shouldn’t just consider reading an .svg file? Something like this perhaps:

fn 0 size “quadbezier” “envelope1.svg”

Using an .svg file would make it more generic and portable. But the first option would be more Csound-esque.

No, probably I’m missing something really obvious here that you’re thinking ahead and that’s why we are not understanding each other. What I’m saying would look like this:

In the .csd:

gentable bounds( 10, 30, 225, 120), tablenumber(1), ctrltable(2), tablecolour(“silver”), identchannel(“table”), zoom(-1), active(1)

and then in the score:

f 1 0 4096 -2  0 ; the main table
f 2 0    6 -2 82 39 393 472 486 430 ;P1(x y), Ctrl1(x y), P2(x y)

In Cabbage’ source code we would have to check with -I’m guessing in mixed pseudo-code- something like:

if(cAttr.getIntArrayProp("ctrlTable").size()!=0)
    goBezierMode;
else
    JustDoWhatYouAreAlreadyDoingWithTables;

goBezierMode means Cabbage calculates the Bézier segment and replace f1 with the computed values. So InputMessage would have a really long string passed to that contains the newly generated y-values. The function provided by wikipedia doesn’t produce values at every consecutive index (using the values in f2 the first two x-values are 82 and 88), so we should find a way of interpolating between those or else changing the general strategy.

So after that you’d have an f1 with a Bezier segement like the one I attach to this post. If the user moves a lineTo point or a ctrlPoint in editMode we would have to replace f2 (which is the one the user should save) and then also f1 with the newly generated values. In the end what you’d see in Cabbage is an illusion -just like you do now- because Cabbage is constantly replacing the tables when active(1) has been set. The only difference is that Cabbage would replace f1 even with active(0) if you pass a table number as an argument to ctrlTable.

I hope that was clear and I didn’t messed up what I said!

I’dont know much about JUCE and svg editing, but we would have to create a parser for svgs in Csound to do this, shouldn’t we?

Here’s the result for that segment in gnuplot:

Now I understand your proposal. I guess it should work. Feel free to explore it further. I hit some problems trying to calculate all points along the x-axis when I was creating my gen routine, but there has to be a way.

We would. JUCE has one, but we went with native Csound code we would have to write our own simple parser. THe best thing about getting Csound to generate the waveform is that we don’t have to worry about painting it. We simply paint the points. Doing it in Cabbage means we have to create a new table routine for calculating points. But it should be possible.

Right, so, it seems that it’s possible to derive the control points for quadratic bezier curves. I tried a random example and indeed it worked. All that is needed is to rearrange the original equation. This means the best way to go would be to write a new gen routine for Csound, which I think should be pretty straightforward because of the excellent resources available online.

The only thing left would be how to mark the lineTo points when rebuilding the table in Cabbage in a way that saves the intentionally created handles (by Cabbage). I imagined three ways of doing it:

The first one is to have a parameter in the gen routine that when set to 1, marks the lineTo points inside the table as negative y-values. We could then check for that in Cabbage. To use the f-table inside Csound you’d just have to abs() the fetched values. Is this too hacky for a solution? Of course, you won’t be able to use curves with negative values if we go this route.

Another way would be to construct tables like this:

value 3 13 20 100   150   180
index 0  1  2   3... 13... 20 

index(0) always marks where the first lineTo point is and also how much offset do you need to read the real table. In this example indexes(0,1,2) contain the indexes of the lineTo points(and maybe to simplify things later, it could also contain where the control points are). From index(3) onwards the real table begins.

The third way would be just to save the Cabbage handles inside .snaps files.

Which one do you think it’s best? Oh, and one final thing for the Cabbage implementation: it would be nice if when you are dragging the control points while editing, the handle turns a different color when you are right in the middles of the two lineTo points. That’d mean you are drawing a perfect straight line (haven’t done the math but I think that’s how it works). We could have curves and straight lines with the same routine then, like gen16 does in Csound.

Agreed. What syntax would the table have? If it’s like above, i.e,

fn 0 size “quadbezier” y1, cx1, cy1, x2, y2 [cx2, cx2, dur2, y3, etc…]; (x0 is not given as it’s always 0)

Then I can use that data to draw the curves exactly as they should be. csoundGetTableArgs() will give us all the info we need to reconstruct the curves and add the handles. I’d have to create a new drawing routine, but it would be pretty simple, since this is how JUCE handles drawing of these curves too.

With regards to saving the envelopes, I’d suggest we simply use Csound to save them? I have a feeling you may be over complicating this, but then again, you’ve given it much more thought that I have!

That would be the syntax (except dur2 which I hope it’s a typo), indeed. I’ve found of another way to save the handles based on the size of the f-tables. It is the simplest way really. The only problem may be that a really short segment would have the same resolution that a really long one. But at least gnuplot plots them well.

Basically the idea is that every X amount of samples in the table there is a handle. Right now I’m plotting with 100 values per segment. So the handles should be at 1, 100, 200, etc. I don’t know if that’s good enough or not for JUCE. This will have to be a symbolic constant defined in Csound and also in Cabbage.

I’ve solved the rounding problem for the indexes with the quadratic equation. But it only works if every P given (including the control ones) are less than the ones after and greater than the ones before. (… Px1 < Cx1 < Px2 …). This is actually reasonable because bezier curves don’t always behave like functions of the form y=f(x). This will have it’s implications for Cabbage and limiting mouse dragging of the points further on. But otherwise it wouldn’t be possible to use f-tables for storing these curves anyway as there could be several Images for the same index.

This is no longer needed. All we’ll need is to have a shared symbolic constant between Csound and Cabbage. Right now I have a semi-functional sketch of the gen routine written in C++. I’ll keep trying to make it work and then post it here.

Sounds good. Once we have the GEN routine we can see how it looks in Cabbage. Note that for this envelope, Cabbage will not be drawing all the points in the table like it usually does. I’ll simply be passing the control data to JUCE and let it do the drawing. In this way we need not worry about the resolution of the envelop or the number of points. Those concerns are reserved for the gen routine.

I’m an idiot. I didn’t pay attention to what you said about csoundGetTableArgs(). Does this function give you the original arguments passed to the gen routine??? It should be much easier to do then.

I can give you a table with the data for the + signs represented in this graph below. Does JUCE have something to show the handles for control points (not plotted in the graph)?

It sure does. I asked Victor to add this API function back in 6.03 for this very reason. It makes drawing and managing tables very easy. JUCE doesn’t have a set way for displaying handles but I can add them myself. I recall writing a simple bezier curve generator before at some point. My memory is a little foggy, but I don’t think it was that tricky.

And can you access that function from inside a .csd? Or even better, does ftsave saves the original arguments passed to the gen routine? Otherwise we’ll need to write a new opcode that saves not only the table contents, but also the output from csoundGetTableArgs() in the same file.

I think the best way to deal with this is to use a file out opcode to write the gen arguments to a text file. Then use a file in opcode to read them and instantiate the gentable. I was disappointed to see that ftsave saves the entire contents of the table rather than the arguments used to create it. In fairness, the creation of a new ftsave opcode to save the arguments would be trivial.

readfi and fprints?

I made some progress today. It isn’t user-safe yet, but if you don’t mess up the arguments the brand new “quadbezier” gen routine seems to produce the correct output. I’m not used to writing in C, so I probably messed up somewhere.
Here’s a screen-shot of gnuplot and Csound/ascii, both taking the same coordinates for a segment.

Actually, I was thinking of new dedicated opcodes that write a tables creation arguments to disk, and can open them later. It shouldn’t take more than a few minutes to write, but would help when it comes to working with tables in Cabbage.

Ha, you’re making much faster progress on this than I did the last time I tried! You don’t have to write in C. It’s possible to write opcodes and gen routines in c++ too.