Cabbage Logo
Back to Cabbage Site

Velocity curve widget?

Hi, if you try to make a synth that generates different sounds based on touch velocity (like a piano) you will soon notice that you need some way to change the velocity curve to adapt it to the sensibility of your midi keyboard. In many midi keyboards you have some way to change the “hardness” of the touch response (i.e. the velocity curve), in other keyboards you have only some standard presets like light, normal, hard, etc. and there are some cheap keyboards that doesn’t have this ability at all. Anyway, it’s always nice to be able to change precisely the velocity directly in the GUI of your instrument.

I would like something like in the following tool:

Where you can add some points in the graph and move them as you like, to generate your personal velocity curve.

For now, in my experimental instrument, the velocity curve is hard-coded with a global array such this:

 giVelCurve[]  fillarray  0, 0,   0.2, 0.05,   0.5, 0.2,   1, 1

where each pair means: (original_velocity => modded_velocity), a bit like in the above graph of that tool.
Then I have a little macro that does a simple linear interpolation to convert from the original velocity (that you give to the macro as a parameter) to the modded one, based on the above array (note that it’s not limited to 4 points… The array could have all the pairs you want and the macro would calculate the correct interpolation).

Now, if only there was some widget in Cabbage that allowed to “draw” some points in a XY graph and return these points as an array of pairs (x,y), we could have a very cool velocity curve changer…

Something like this work for you? Of course, you’ll need to save the points yourself, but that shouldn’t be tricky. Check out the writing/reading opcodes for saving table data to disk.

velCurve.csd (694 Bytes)

Hey, that looks very very nice! :astonished:
It could be perfect for my needs… I will try it and let you know! :wink:

I just tried to make a simple Velocity Curve changer and it works well if I use GEN07 (that creates linear segments) but I don’t understand how to use GEN05: how to change the concavity type? And how to change the “power” of the curve? Anyway, the GEN07 version is good enough for my needs so I think I will stick with that one. Now I will try to save the curve so that the next time you run the Csound program it will load the last one modified.

You can’t do that with GEN05. I think it’s GEN16…yup. GEN16 should do the trick. But note that editing tables in Cabbage is only supported with GEN02, GEN05 and GEN07. But you can still make this work by superimposing one table on top of another. It’s a bit of a hack, but should work Ok.

How it is the state of this?

To be honest, I’m not sure. Have you tried it? @gsenna contributed this, but it was just before a huge rewrite of the entire system. Therefore I’m not exactly sure it made it to Cabbage 2. I’d need to look through the source. It was a nice idea.

[edit] Didn’t make it through. Here is the old source(below). I’m not sure how tricky it might be to integrate. I will try it if I get a chance, but might be a while. Right now I’m wrestling with some really strange behaviour on certain Windows machines.

Bezier curves editable with the mouse would be a very nice feature for Cabbage! In addition to a more precise velocity curve, you could shape a waveform as you want!

Of course it’s not an urgent matter, but I hope you will try to make it work when you have some time. :wink:

I’m having trouble to save and load the table. I use ftsavek to save the table to a file, and it works, but then each time I try to load it with ftloadk Cabbage crashes.

Here is some code:

<Cabbage>  bounds(0, 0, 0, 0)
form caption("Other crashes...") size(600, 300), colour(255, 255, 255), pluginID("def1")
keyboard bounds(10, 192, 581, 95) value(30) whitenotecolour(255, 255, 255, 255) 
nslider bounds(484, 14, 105, 27) range(1, 100, 32, 1, 1) velocity(50) text("Max Polyphony") channel("maxPoly") increment(1) value(32) textcolour(144, 144, 144, 255)
label bounds(474, 48, 83, 12) text("Active notes:") colour(243, 3, 3, 0) align("right") fontcolour(144, 144, 144, 255)
label bounds(562, 48, 26, 12) text("0") colour(202, 238, 90, 0) fontcolour(34, 34, 34, 255) identchannel("activeNotes") align("right")
gentable bounds(12, 66, 147, 99) tablenumber(1111.00000000000000000000) amprange(0.00000000000000000000, 1.00000000000000000000, 1111.00000000000000000000, 0.0100) tablegridcolour(236, 236, 236, 255) tablebackgroundcolour(245, 245, 245, 255) active(1) tablecolour:0(111, 111, 111, 147)
label bounds(12, 168, 147, 13) text("Velocity Curve") fontcolour(43, 43, 43, 255) 
combobox bounds(10, 40, 102, 20) channel("velPreset") text("Preset 1", "Preset 2", "Preset 3", "Preset 4")
button bounds(116, 40, 44, 20) text("SAVE", "SAVE") colour:1(0, 255, 0, 255) colour:0(0, 255, 0, 255) channel("velBtn")
-n -d -+rtmidi=NULL -M0 -m0d --midi-key-cps=4 --midi-velocity-amp=5
; Initialize the global variables. 
sr  = 44100
ksmps = 256
nchnls  = 2
0dbfs   = 1

giVelCurveRes   =           1024
giFT_velCurve   ftgen       1111, 0, giVelCurveRes, -7,              \
                                  0, giVelCurveRes, 1
#define VELCURVE(vel) #
    $vel    tab_i   floor(giVelCurveRes * $vel), giFT_velCurve

opcode corda, a, ii
    iFreq, iVel xin
                setksmps 8
    aNoise      rand    iVel
    iDelayLdur  =       1 / iFreq
    aImpulse    =       aNoise * linseg:a(1, iDelayLdur, 1, 0, 0)
    aOut        delayr  1 / iFreq
    aOut        +=      aImpulse
    aOut        =       (aOut + delay1(aOut)) * 0.5
                delayw  aOut
                xout    aOut

instr 2
    iFreq	    =	    p4
	iVel	    =       p5
    aOut        corda   iFreq, iVel
                out    aOut

        massign     0, 1

;instrument will be triggered by keyboard widget

instr 1
    iFreq	    =	        p4
	iAmp	    =           p5
    iNumKey     =           notnum() - 20
    if (iNumKey < 1 || iNumKey > 88) goto skipnote
	aOut        subinstr    2, iFreq, iAmp
    aOut        =           aOut * madsr(0.01, 0, 1, 5)
			    outs        aOut, aOut

instr Mntr
    kMaxPoly    chnget  "maxPoly"
    kVelPreset  chnget  "velPreset"
    kVelBtn     chnget  "velBtn"
    kActivNotes active  1
	if (kActivNotes > kMaxPoly) then
	    turnoff2    1, 1, 0
    if (changed(kActivNotes) == 1) then
        chnsetks  sprintfk("text(\"%d\")", kActivNotes), "activeNotes"
    if (changed(kVelBtn) == 1) then
        printks "save: velPreset%d.txt\n", 0, kVelPreset
        ftsavek sprintfk("velPreset%d.txt", kVelPreset), 1, 1, giFT_velCurve
    elseif (changed2(kVelPreset) == 1) then
        printks "load: velPreset%d.txt\n", 0, kVelPreset
        ftloadk sprintfk("velPreset%d.txt", kVelPreset), 1, 1, giFT_velCurve

;causes Csound to run for about 7000 years...
f 0 z
i "Mntr" 0 z

The problem with using ftsave is that it doesn’t save any meta data about the table such as the GEN routine used to create it, or the number or parameters, etc. Therefore, recalling a table in this way won’t give you any editable points, as these are based on the GEN parameters used to create the table.

I think the best approach here would be to use a GEN02 with a relatively small umber of points. And then save those points to a text file. Let me think about it, maybe there is a better way.

I get a crash here also when I don’t have tables previously saved. When I do have a list of tables previously saved the debugger brings me to the Csound source: csoundPerformKsmps(csound);. Clearly something is not right there.

Something like this should work, but it still causes a crash. I don’t think Csound is freeing up the files its writing? In this one I save the contents of a table to a text file. Then when it’s recalled, I parse the text file. Only Csound seems to have a pretty short limit for strings? So it only ever returns 114 elements instead of 1024. :angry:

Anyhow, after that I use copyatftab to send the data to the table, call update on the table and hey presto. Only if I do it a second time I get a crash and the debugger takes me to Csound. I will need to build a debug version of Csound to figure out what’s happening. I don’t have one here at the moment.

simple.csd (4.9 KB)

I’m a little confused… If the function tables are generated only at init-time by ftgen (based on a GEN routine and other i-rate params), I don’t understand where gentable takes the info on the original points used by the GEN routine… And what happens when you add some new points in the displayed gentable widget? It directly alters the array content or it does an hidden ‘reinit’ to make ftgen regenerate again the table with new parameters?

Anyway, yes, Csound has a limit on the length of the strings, but you can tell Csound to increase that limit with the following flag:

    (min: 10, max: 10000) Maximum length of string variables + 1; defaults to 256 allowing a length of 255 characters. The length of string constants is not limited by this parameter.

So, the max string length you can achieve, with the above flag, is 9999 characters. I don’t know why there is such a limit, but it’s not a thing I would expect from a programming language. I know that Csound is not a general purpose language, but yet…

When you create a gentable widget in Cabbage, it looks at the GEN arguments passed to the corresponding function table and adds the editable breakpoints.

After editing the table, Cabbage updates it via a call to Csound::InputMessage(). It happens here:

Exactly. Why on earth do it this way you might ask? Simply because I didn’t want to create a proprietary table system just for Cabbage. I’ve always tried to ensure that Cabbage .csds can run in other hosts with minimal tweaking. Whether or not that was the correct choice remains to be seen. So often I’ve been tempted to just write custom opcodes for use with Cabbage. It would make life so much easier, on so many levels. But then people would be writing in a kind of hybrid fork of Csound, and I don’t think that would be good; for Cabbage or Csound.

Great. :angry: I’ve never found the implementation of strings to be exactly user friendly in Csound. No great surprise really considering they weren’t ever part of the original language structure. Anyhow, I tried updating that instrument to read line by line, but wouldn’t you know it, now strtod gives me a ridiculous ‘invalid format’ error. Maybe you will have more luck with it?

instr 1000
    iIndex = 0
    gihand fiopen "preset1.txt", 0
    while iIndex<giVelCurveRes do
        fprints "preset1.txt", "%f\n", tab_i(iIndex, giFT_velCurve)
    ficlose gihand

instr 1001
    iLinNum = 0
    iCnt = 0
    iArr[] init ftlen(giFT_velCurve)
    gihand fiopen "preset1.txt", 1
    while iLinNum != -1 do
        Sline, iLinNum readfi "preset1.txt"
        iArr[iCnt] = strtod(Sline)
    copya2ftab iArr, giFT_velCurve
    chnset "tablenumber(1111)", "table1"
    ficlose gihand

Btw, I’m just wondering if @iainmccurdy hasn’t done this before with some of his instruments, i.e., save table data and recall it later. I must take another look through his magnus opus.

I found the problem in the ‘simple.csd’ file you attached before.

Looking at the source code of the Csound “readfi” opcode, I found just another “hard-coded” limit (independent from the flag to set the max string length) :

#ifndef MAXLINE
#define MAXLINE 1024

The “readfi” function read a line at a time but until MAXLINE. So, if your line is bigger than 1024 characters, readfi splits the line in many lines of max 1024 chars and that was the reason why StrToArr returned only 114 elements… The first line read from readfi contained only the first 114 numbers…

Not exactly intuitive :joy: I was also thinking something like that might be at fault. Any luck with the strtod error in the last two instruments I posted?

I’ve just made a version that begins to work…

works.csd (4.9 KB)

1 Like

Uggghhh, macros! :joy: But yes, it’s working better, but now we have the problem of the editable points not updating? Are you seeing that too?

HahahaI :smiley: I use macros because I’m too much used to FOR syntax-style in high level programming languages. Usually I never use the “while” statement in C/C++/PHP/Python or whatever language, so when I use a language that doesn’t have a FOR statement I feel like I miss something…

With “while” you have to do:

i1 = 0
while (i1 <= iSize) do
   i1 += 1

with my macro it’s just:

$FOR_INDEX('i1' FROM '0' TO 'iSize')

or in a more C-like and flexible way:

$FOR(i1 = 0 ' i1 <= iSize)
$NEXT(i1 += 1)

I think the ‘FOR_INDEX’ version is more readable, compact and the thing I like very much is that you cannot forget to increment the index inside the loop, because the macro does it for you! I have made another version that is very convenient when you have to iterate on each element of an array:

$FOR_EACH('iElement' IN 'iSomeArray')
   ... do something with iElement ...

Anyway, sorry for the digression. :blush:

Returning to our problem, yes, I see that the points are not updating… The problem is: after we change the curve in Cabbage, from Csound we don’t have a way to get the new points locations… If we could save those points too, then we could resend them to the Cabbage widget. So we would need:

  1. a way to receive in Csound those new control points made in the widget, so that we could save them;
  2. a way to resend those points to the widget and tell the widget to refresh itself.

In this way we should not have to save ALL the points of the generated table, but only the control points!


Yes, those were the problems I tried to solve. There’s GENquadbezier for creating a quadratic bezier curve and getftargs for extracting the coordinates from the table updated by Cabbage, so one could save/load those in/from a text file.

What happened was it was just bad timing because I worked on that feature just before Rory transitioned to Cabbage 2.

P.S.: the string length restriction still in some of the opcodes is really absurd. I think it was just because there was no mechanism in place on those to dynamically allocate the space.

Very nice! So, if Cabbage ‘reinit’ the function table each time the user change the curve in the widget, with the getftargs we can save the updated control points!
Now the only problem remains how to send new control points to the gentable widget to make refresh itself…