Writing a script action to overcome Bitwig's project "Save" behaviour.

Post Reply New Topic
RELATED
PRODUCTS

Post

It turns out that BW only retrieves the current state of a plugin from the plugin during a project Save when it has detected that there has been a change in the plugin state.
In the case of a network connected plugin (in the case of Vienna Ensemble Pro server hosted plugin) changing a parameter in such a remote plugin GUI, there is no automatic update of the plugin data in the DAW, as there would be in a normal locally hosted plugin instance.
This is where BW is different from other hosts (I've tested). Hosts such as Reaper and Ableton will retrieve the state of plugins regardless. This means that plugins hosted via VEP always have their current state saved with the DAW project. Not so with BW. Generally changes made in remote plugin will get lost after a saved project is reloaded.

I exchanged a thread of emails with BW support back in 2020 over this issue and they confirmed this situation is as I describe and that VEP is a unique case that they were unwilling to support. To them, the prospect of transferring every parameter regardless of whether it has changed was too much.

However, I discovered a workaround: it turns out that if a parameter on the VEP instance plugin (the VEP plugin hosted by the DAW that communicates Audio and control data with the remote server), that if such a parameter is touched, then all the data of that plugin is queried by BW and saved with the project file.

So I am coding a special "Save with data" action that will traverse the project and change and restore a single parameter of each VEP plugin instance before invoking "Save".

My first question would be: is there an API callback that is triggered as the result of a Save command?

Post

dungle wrote: Mon Jan 20, 2025 3:58 am It turns out that BW only retrieves the current state of a plugin from the plugin during a project Save when it has detected that there has been a change in the plugin state.
In the case of a network connected plugin (in the case of Vienna Ensemble Pro server hosted plugin) changing a parameter in such a remote plugin GUI, there is no automatic update of the plugin data in the DAW, as there would be in a normal locally hosted plugin instance.
This is where BW is different from other hosts (I've tested). Hosts such as Reaper and Ableton will retrieve the state of plugins regardless. This means that plugins hosted via VEP always have their current state saved with the DAW project. Not so with BW. Generally changes made in remote plugin will get lost after a saved project is reloaded.

I exchanged a thread of emails with BW support back in 2020 over this issue and they confirmed this situation is as I describe and that VEP is a unique case that they were unwilling to support. To them, the prospect of transferring every parameter regardless of whether it has changed was too much.

However, I discovered a workaround: it turns out that if a parameter on the VEP instance plugin (the VEP plugin hosted by the DAW that communicates Audio and control data with the remote server), that if such a parameter is touched, then all the data of that plugin is queried by BW and saved with the project file.

So I am coding a special "Save with data" action that will traverse the project and change and restore a single parameter of each VEP plugin instance before invoking "Save".

My first question would be: is there an API callback that is triggered as the result of a Save command?
I don't think there is a callback on save...
your best bet would be to make a signal setting in the document or some midi based event. then trigger both your parameter saving technique and a bitwig save moments later
----------------------------------------------------------------------
/CTRL → http://slashctrl.io
Music & mixes → http://soundcloud.com/kirkwoodwest

Post

I'm triggering my script via midi, which then does as you suggest, and nudge and return the first parameter of every VEP plugin instance, and then execute a regular Save. It works perfectly well. Just as well too, because I spent a lot of time getting it going. I won't continue describing the intricacies but if anyone wants to use VEP with BW let me know and I can describe what I did.

Post

That’s dope! U put in the time and got the results. Well done!
----------------------------------------------------------------------
/CTRL → http://slashctrl.io
Music & mixes → http://soundcloud.com/kirkwoodwest

Post

Hello, I'm interested in your solution, as I have problems with VE Pro parameters in Bitwig, too. And, in the same way, they have pushed the problem away from them. Do you have to map parameters in VE Pro? How do you access them from Bitwig?

Seems like another problem that they are the only ones to meet... I give up with their pathetic (paid!) support.

Post

The quickest and by far simplest and easiest solution is to:
- use a spare knob on a midi controller set it to some obscure CC like 0x7D and if you can, limit its range to 0x62 to 0x64.
- on each VEP track plugin (BW native device panel), right click on "Param 1" and select "Map to Controller or Key ..." .Now wiggle the knob.
Once you done this on every instance in the project, simply wiggle the knob, then hit "Save".
This causes every instance data to be included in the save. Remember to wiggle and save after changing any parameter on a remote plugin sometime before you quit the session.
The global midi mapping generally persists. You have to remember to do the midi learn every time you make a new VEP instance in a project.
There are programmatic ways of doing this that avoid having to remember to set each instance, but developing java extensions for bitwig is a relatively huge learning curve and time investment.

Post

dungle wrote: Mon Jan 20, 2025 3:58 am ... if a parameter on the VEP instance plugin (the VEP plugin hosted by the DAW that communicates Audio and control data with the remote server), that if such a parameter is touched ...
For me to understand, this means that you have to expose this parameter to DAW through the automation panel in VEP? https://cdn2.vsl.co.at/manuals/vienna_e ... odD96.webp
Because on my side if I do that, Bitwig becomes unusable since they introduced their lazy implementation of VST undo feature. Or does it mean that I have just to wiggle a param on VEP VST device in BW not matter if its linked to something in VEP?

Post

AI analysis of Bitwig API:
The Bitwig API provides access to project-related information and actions via the com.bitwig.extension.controller.api.Project interface. However, there are no direct save or load project functions exposed in the API for extensions.

Project-related functions available:
getProject() (returns the current project object)
projectName() (gets the project name)
nextProject(), previousProject() (navigate between projects)
isModified() (checks if the project has unsaved changes)
Scene and track group management: createScene(), createSceneFromPlayingLauncherClips(), getRootTrackGroup(), etc.
No API methods for saving or loading the project file directly are exposed. Project save/load is managed by Bitwig Studio itself, not by the extension API.

If you need to detect when a project is modified or get its name, you can use:

isModified() (returns a BooleanValue)
projectName() (returns a StringValue)

I have generated a little DisplayFusion script that sends a random MIDI CC and CTRL+S or CTRL+SHIT+S to Bitwig.
bws.png
Tools:
- https://www.displayfusion.com/ (not sure that you can add custom functions in Free version, but this app is so useful that I think $34 is worth it). See launchers in DF functions dialog (picture is just an example of launcher in title bar).
- https://github.com/gbevin/SendMIDI
- https://www.tobias-erichsen.de/software/loopmidi.html or any other virtual MIDI cable


Function flow

Code: Select all

User triggers DisplayFusion function
        v
DisplayFusionFunction.Run(windowHandle)
        v
Get all visible windows
        v
Autoselect first window whose title starts with "Bitwig Studio"
        v
Create ContextMenuStrip with "Save" and "Save as..." items
        v
Show menu at mouse position
        v
User clicks "Save" or "Save as..."
        v
-------------------------------
| For selected menu item:      |
|-----------------------------|
| 1. Generate random CC value |
| 2. Build sendmidi.exe args  |
| 3. Start sendmidi.exe       |
| 4. Wait for process exit    |
| 5. Wait timeoutMs           |
| 6. Focus Bitwig window      |-----> Bitwig window activated
| 7. Wait 100ms               |
| 8. Send Ctrl+S or Ctrl+Shift+S |-----> Bitwig receives key combo (Save/Save As)
-------------------------------
        v
Menu closes, function ends

Function code:
Set in --- CONFIGURATION --- section for user settings

Code: Select all

using System;
using System.Drawing;
using System.Diagnostics;
using System.Windows.Forms;
using System.Threading;

// The 'windowHandle' parameter will contain the window handle for the:
//   - Active window when run by hotkey
//   - Trigger target when run by a Trigger rule
//   - TitleBar Button owner when run by a TitleBar Button
//   - Jump List owner when run from a Taskbar Jump List
//   - Currently focused window if none of these match
public static class DisplayFusionFunction
{
    public static void Run(IntPtr windowHandle)
    {
    // --- CONFIGURATION ---
    string sendmidiPath = @"...\\sendmidi.exe"; // Path to sendmidi.exe
    string midiDevice = "loopMIDI Port"; // MIDI output device name
    int midiChannel = 1;                  // MIDI channel (1-16)
    int ccNumber = 20;                    // CC number (0-127)
    int ccMin = 0;                        // Minimum CC value
    int ccMax = 127;                      // Maximum CC value
    int timeoutMs = 1000;                 // Wait time after MIDI send (ms)
    Random ccRnd = new Random();           // Random generator for CC value

        // --- WINDOW SELECTION ---
        // Get all visible and minimized windows
        IntPtr[] windowHandles = BFS.Window.GetVisibleAndMinimizedWindowHandles();
        IntPtr bitwigHandle = IntPtr.Zero;
        for (int i = 0; i < windowHandles.Length; i++)
        {
            string title = BFS.Window.GetText(windowHandles[i]);
            // Autoselect first window whose title starts with "Bitwig Studio"
            if (!string.IsNullOrEmpty(title) && title.StartsWith("Bitwig Studio"))
            {
                bitwigHandle = windowHandles[i];
                break;
            }
        }
        if (bitwigHandle == IntPtr.Zero)
        {
            MessageBox.Show("No Bitwig Studio window found.");
            return;
        }

        // --- MENU CREATION ---
        using (ContextMenuStrip menu = new ContextMenuStrip())
        {
            // Remove check and image margins for cleaner look
            menu.ShowCheckMargin = false;
            menu.ShowImageMargin = false;

            // Add "Save" menu item
            var saveItem = new ToolStripMenuItem("Save");
            saveItem.Click += (s, e) =>
            {
                // Generate random CC value for MIDI message
                int ccValue = ccRnd.Next(ccMin, ccMax + 1);
                var args = $"dev \"{midiDevice}\" ch {midiChannel} cc {ccNumber} {ccValue}";
                try
                {
                    // Send MIDI CC message using sendmidi.exe
                    using (var proc = Process.Start(new ProcessStartInfo {
                        FileName = sendmidiPath,
                        Arguments = args,
                        UseShellExecute = false,
                        CreateNoWindow = true
                    }))
                    {
                        if (proc != null)
                        {
                            bool finished = proc.WaitForExit(timeoutMs);
                            if (!finished)
                                MessageBox.Show("sendmidi timed out after " + (timeoutMs/1000) + " seconds.");
                        }
                    }
                }
                catch (Exception ex) { MessageBox.Show("sendmidi failed: " + ex.Message); }
                // Wait for MIDI to be processed
                Thread.Sleep(timeoutMs);
                // Focus Bitwig window and send Ctrl+S
                BFS.Window.Focus(bitwigHandle);
                Thread.Sleep(100); // Ensure focus
                SendCtrlS();
            };
            menu.Items.Add(saveItem);

            // Add "Save as..." menu item
            var saveAsItem = new ToolStripMenuItem("Save as...");
            saveAsItem.Click += (s, e) =>
            {
                int ccValue = ccRnd.Next(ccMin, ccMax + 1);
                var args = $"dev \"{midiDevice}\" ch {midiChannel} cc {ccNumber} {ccValue}";
                try
                {
                    using (var proc = Process.Start(new ProcessStartInfo {
                        FileName = sendmidiPath,
                        Arguments = args,
                        UseShellExecute = false,
                        CreateNoWindow = true
                    }))
                    {
                        if (proc != null)
                        {
                            bool finished = proc.WaitForExit(timeoutMs);
                            if (!finished)
                                MessageBox.Show("sendmidi timed out after " + (timeoutMs/1000) + " seconds.");
                        }
                    }
                }
                catch (Exception ex) { MessageBox.Show("sendmidi failed: " + ex.Message); }
                Thread.Sleep(timeoutMs);
                BFS.Window.Focus(bitwigHandle);
                Thread.Sleep(100);
                SendCtrlShiftS();
            };
            menu.Items.Add(saveAsItem);

            // Show menu at mouse position
            Point pt = new Point(Cursor.Position.X, Cursor.Position.Y);
            menu.Show(pt);
            BFS.Window.Focus(menu.Handle);
            // Wait for menu to close
            while (menu.Visible) Application.DoEvents();
            // Refocus Bitwig window after menu closes
            BFS.Window.Focus(bitwigHandle);
        }
    }

    private static void FocusBitwig(IntPtr hWnd)
    {
    if (hWnd == IntPtr.Zero) return;
    try { BFS.Window.Focus(hWnd); } catch { }
    }

    private static void SendCtrlS()
    {
        SendKeys.SendWait("^s"); // Ctrl+S
    }

    private static void SendCtrlShiftS()
    {
        SendKeys.SendWait("^+s"); // Ctrl+Shift+S
    }

    private static class NativeMethods
    {
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        public static extern bool SetForegroundWindow(IntPtr hWnd);
    }
}

dungle wrote: Wed Sep 24, 2025 3:50 am There are programmatic ways of doing this that avoid having to remember to set each instance, but developing java extensions for bitwig is a relatively huge learning curve and time investment.
With AI things have considerably changed. I was able to do very serious and advanced work without much diving into API specs. :) Start from an exiting Github project.
You do not have the required permissions to view the files attached to this post.

Post

monolithx wrote: Wed Sep 24, 2025 2:51 pm
For me to understand, this means that you have to expose this parameter to DAW through the automation panel in VEP?
No, don't map anything in VEP. Like I tried to say you only need to nudge a parameter: those exposed by default in the BW native device panel for VEP plugin. I always do the first one "Param 1". You can midi learn many of these (ie one for each VEP instance) in your project to the same knob.

AI is a real game changer for any kind of coding these days, but there are other things such as setting up the environment, and generally learning to understand what you're dealing with that are real "time sinks" that need to be balanced with how you want to be spending your time IMO.

The BW API (java) does allow for the triggering of menu actions such as "Save".

Post Reply

Return to “Controller Scripting”