Cabbage Logo
Back to Cabbage Site

Creating custom UIs with Cabbage 3

I’m already getting question about this, so here’s some more info on top of what’s in the docs. To use a JS framework like React or Svelte, you first need to export a basic plugin. This will create the plugin binary, and the self-contained folder where the web front-end will be served from. By default, it will export the files needed to run a native Cabbage interface. The file structure looks like this:


The only file you really need is the cabbage.js file which includes functions that allow you to pass events to your plugin from the web interface. If you want to receive data from your plugin check out the event handler in main.js. A ‘widgetUpdate’ event message will be sent to the UI whenever a widget is updated through a call to the cabbageSet opcode. So you can basically remove all of these files and replace them with your own. The index.html will be served inside the plugin window and hey presto, custom interface.

This is all somewhat theoretical as I’ve yet to try it myself. If anyone wants to give it a go (I’m looking at you @hdale94!) please do and let me know how it goes. It may need some tweaking to get it to work.

2 Likes

I can confirm this works. I just did the following:

  1. Exported a basic plugin.
  2. Using vite, I created a new template Svelte app
  3. I imported cabbage.js and added a call to Cabbage.sendParameterUpdate to the Counter.svelte component:
<script>
  import { Cabbage } from "../cabbage.js";
  let count = $state(0)
  let parameterIndex = 0;
  const increment = () => {
    count = count < 10 ? count + 1 : 0;
    const msg = { paramIdx: parameterIndex, channel: "freq", value: count/10 }
    Cabbage.sendParameterUpdate(null, msg);

  }
</script>

<button onclick={increment}>
  count is {count}
</button>
  1. Built the svelte app
  2. Copied the dist file over to my plugin resources folder, replacing any files that were there.
  3. Launched Reaper :slight_smile:

Note that I still had to declare a parameter in my .csd file:

<Cabbage>[
    {"type":"form","caption":"Button Example","size":{"width":380,"height":300},"pluginId":"def1", "enableDevTools":true},
    {"type":"rotarySlider","channel":"freq", "range":{"min":0, "max":10, "increment":1}}
]</Cabbage>

And I had to also leave the cabbage/wdgets folders as Cabbage needs to read the widget descriptors from disk. But you can easily add your own classes. So, if it works for Svelte, it should work with any JS frontend framework.

amazing! congrats on the alpha release. I’m very much looking forward to playing around with this. hopefully will find some time over the next couple weeks.

Btw the time you start playing with it I should have a few more of the teething issue sorted :wink:

1 Like

I’m trying to create custom widgets now!

Do you have examples of widgets using an audio buffer as an input? I’m trying to understand how to do it using the keyboard.js as a template but instead of using midi events I’ll use audio events.

Thanks!

Edit: Actually the gentable example might be a good starting point, i’ll try with this one

Yes, gentable. We’ll need to work on a more robust approach to sending samples. But the gentable has a samples property, which get populated with audio from a Csound function table. I will add an array version of cabbageSetValue so that you can grab audio samples more easily.

I will try to get that done later today if I can :+1:

Ok, new build underway that should provide more options for drawing things. I’ve added array variants of cabbageSet and updated the placeholder docs. My advice for you would be to clone the most recent gentable widget. rename both the class and the filename, but leave it in the same directory as gentable.js. Then hack the drawing routine as you see fit. You can send an array of samples to it using the cabbageSet opcode. Here’s me doing it with a small array.

The current gentable widget sucks at smaller tables. I’l fix this when I get a chance.

I was wondering if there’s any way to do live updates between cabbage and a React app during development?

Good question - right now I don’t think so. What I usually do is develop my interface using vscode’s live server, and then when ready test it with audio. The nice thing about this is I use vscode for the developing the interface too, so all my dev work happens in one place.

Alright, I’ll test more tomorrow.

One thing I noticed that would be useful in terms of development experience would be some JSDoc in the cabbage.js to describe the parameter types.

Like this:

/**
 * Function that combines name and age into one string
 * @param {string} name 
 * @param {number} age 
 * @returns string
 */
const CombineNameAndAge = (name, age) => {
    return `${name} (${age})`
}

will show up like this in the IDE:

Yes, good call. Just pushed some now :+1:

1 Like

The plugin resources folder, is this a custom folder you bundled in the form?

Are you referring to something like the bundle() identifier? No, the plugin resources gets bundled automatically whenever you export a plugin.

This is how I interpreted the original post, but I can’t find this folder on windows?

They get placed into the following locations:

  • MacOS /Library/CabbageAudio/PLUGIN_NAME/
  • Windows C:/ProgramData/CabbageAudio/PLUGIN_NAME/

Unless you have ‘Bundle resources’ enabled - which is a very new feature…

Alright, found it.

Do I remove the all the files except the .csd file, and then move the react build into this folder?

You might as well hold onto the Cabbage folder, but reallt cabbage.js is the only thing you need. Oh, and you might as well leave the widget files there too. This way you can define parameter in your csd file, for example rotarySlider .... Even though it won’t display, it will still create aparameter in the host that you can interact with via cabbage.js, and Csound.

Nice, seems like this works great!

If you control the channel-value outside of the UI (like automation), it will not update the slider in the UI. Is there any way to listen to external changes to the channel-value so we can sync this to the state?

add something like this:

window.addEventListener('message', async event => {
   console.log("Cabbage: onMsessage", event.data); // check input
   switch (message.command) {
        case 'widgetUpdate':
            //see note below

The message will contain a command, and either a data object, or value property. When it’s in this form:

{
   "command": "widgetUpdate",
   "channel": "channelName",
   "value": 0
}

It typically comes from a parameter change in the host. When its in this form:

{
   "command": "widgetUpdate",
   "channel": "channelName",
   "data": {...}
}

it’s usually a result of a called to cabbageSet. You can build your own widgets, and update them dynamically using cabbageSet in this way. It’s a pretty powerful system, albeit it incredibly simple. Just check the incoming data to make sure I have the format of it correct :wink: