Plugin System

I would absolutely love to ditch my electron code and simply drop a folder into a Ableset directory called: plugins. There is only so much that could be developed with client js without being able to send commands via ableton-js. This morning during our singing event, I was actually writing code in my head of how this could be possible. :smiley: There is zero pressure on this idea but I would sincerely appreciate the consideration:

First have the "Enable Plugins" option which would force the user to see a window warning them of the security risks of using plugins from unknown authors.

Have a menu link: "Show Plugins", which would direct them the plugins folder.

A plugin folder could be structured like this:

plugins
   my-plugin
      client
         ...resources
         index.js //react
      server
          index.js //express

Ableset could simply import client/index.js into itā€™s loading routine. For my case, I would be using index.js to create a React ā€œrootā€ div element named (for this example) <div id="my-plugin" /> and adding to the body tag. Then calling my React main entry file.

The server/index.js code could be a class with a default export. (Showing Typescript for clarity)

import { Ableton } from "ableton-js";
import express from "express";

export default class MyPluginServer {

    protected pluginAbleton : Ableton;
    protected port : number;

    constructor( pluginAbleton : Ableton, uniqueServerPort: number) {
        this.pluginAbleton = pluginAbleton;
        this.port = uniqueServerPort;

        
        const server = express();
        //...
        server.listen(uniqueServerPort, () => {
            console.log(`MyPluginServer listening on ${uniqueServerPort}`);
        });
    }
}

Ableset could then load all of the compiled plugins inside of the ā€œpluginsā€ folder with a script similar to this:

//...
//A separate Ableton class for all plugins to share
const pluginAbleton = new Ableton();
pluginFolders.forEach( async (folder) => {

    try {   

        const plugin = await import(`${folder}/server/index.js`);
        //find a unique port for the plugin server
        const pluginPort = await findPort();
        const pluginName = path.basename(folder);
        
        const pluginObj = new plugin( pluginAbleton, pluginPort );
        
        runningPlugins.push( { name: pluginName, port: pluginPort } );

    } catch (error) {
        console.error("Plugin error: ", error);
      }

});

From the above example, Ableset could then pass the plugin port information using an endpoint like: /plugins which would return:

{
    "plugins" : [
      {
          "name": "my-plugin",
          "port": 5000
      }
   ]
}

This is certainly barebones code but Iā€™d love to hear your thoughts about the possibilities of Ableset plugins. Itā€™s been mentioned by many users that Ableset is quickly becoming much more valuable than just a setlist manager. I could see an simple plugin being developed that would give users a mixer view to adjust volumes and mute instrument stems. With the addition of plugin functionality, Ableset could truly explode into a full ecosystem. Thanks for your time! Youā€™re a coding superhero! By the way, where is that link to make donations? You deserve much more money than youā€™re getting.

Thank you for your elaborate description! I think a plugin system would be great, and AbleSet already has a modular system for different features, though instead of using classes for the modules, it uses asynchronous functions.

Classes might be the more conventional approach though, so I could write an adapter that offers support for that.

Currently, each module is called with the following parameters:

{
  port: number;
  socketIo: socketIo.Server;
  ableton: Ableton;
  machineId: string;
  license?: License;
  abletonVersion: string;
  projectFile: Observable<ParsedProject | null>;
}

And it returns a promise with an Express router and an asynchronous stop function for cleanup when the server is stopped. So a simple module would look like this:

import { Router } from "express";

export const module = ({ router, socketIo }) => {
  const io = socketIo.of("/module");

  router.get("/", (req, res) => {
    res.json({ message: "I'm a module!" });
    io.emit("starting");
  });

  const stop = async () => {
    log.info("Stopping module...");
    io.emit("stopping");
  };

  return { stop };
};

If I were to make this a class-based design, it could look like this:

import { Router } from "express";

export class Module extends AbleSetPlugin {
  io = this.socketIo.of("/module");

  async start() {
    this.router.get("/", (req, res) => {
      res.json({ message: "I'm a module!" });
    });
  }

  async stop() {
    log.info("Stopping module...");
  };
};

Each moduleā€™s Express router is mounted on its own endpoint, so for example, it could listen to all requests to ā€œ/api/moduleā€. This also removes the need to use a separate port for each module.

If all plugins only make use of the packages that AbleSet already supplies, importing external scripts as modules should be fairly straightforward. Iā€™m not sure how it would work if they had their own node_modules folder, but there are enough plugin systems out there that I can take a look at to figure that out.

Regarding the client-side React integration, I think it would be nice to have some hooks that could be used to create a new page with its own menu item, for example. Your index.js file could look like this:

const MyPlugin = () => <div>Hello World!</div>
ableset.registerPage("My Plugin", MyPlugin);

It would be great if there was a way to reuse existing NPM packages instead of bundling them again with each plugin. This would also prevent version mismatches between different React packages leading to issues. One option for this would be to expose the shared packages as globals on the window object and tell the bundler to replace all imports of those packages with those globals.

Some packages I could imagine being helpful as globals:

  • react
  • react-dom
  • next
  • @tanstack/react-query

Your folder structure makes sense to me. In the long term, it could make sense to integrate NPM so that plugins can be published as packages and easily installed and kept up-to-date. I could also imagine offering a simple template project with all the required scaffolding and configuration files to get started building plugins.

Let me know what you think. This would certainly be a huge project, but it sounds very useful!

Oh my word! My whole body is tingling with pure joy that only a software developer would understand!! :clap: :100: :rocket: :fire: :smiley:

Your outlook on a plugin system is far more refined and I understand that this would be a big undertaking. I could see this paying off in the long run by bundling complicated features into Ableset as paid plugins. These could be offered on a dedicated Plugins page on Ableset.app. For example, Ableset is offered at one (affordable) price, but amazing features just keep coming. At some point, it would make sense to start offering extended functionality as Addon plugins for a price. This would keep the base price low while offering users plugins that fit their specific needs. Audio companies like Ableton, Waves, Slate, etc. offer plugin models like this.

I love the modular structure you proposed and the Express router having itā€™s own endpoint without using another port. To be honest, the reason I structured my server code using classes, was by following the design pattern of ableton-js. I actually learned Typescript by reading the code base of ableton-js.

Integrating NPM would be a awesome in terms of installation and updates. Iā€™ve notice that Github is now showing ā€œUse This Templateā€ feature on public packages. It would greatly cut down on your support requests if a template starter project would be available for developers.

As to client side code, it would be great to also have function to register callbacks when a page has changed. This would actually help me right now with my code. Iā€™ve tried to get a React Router to listen for url changes but to no avail. Iā€™ve actually hacked together some raw js code that notifies me when the Settings, Lyrics or Performance page changes.

Right now, Iā€™m going to find that donation page and make a $200 contribution. I believe in Ableset and I would never want to go onstage without it! I want this to be around for many more years. Thank you for all of the long hours you spend in creating software that sets the bar high!! Long live Ableset! :crown:

I canā€™t locate that donation link. Could you post it again?

Thank you for your support! Hereā€™s the link: Development Support - Checkout

Iā€™ll also add it to the forum sidebar for easier access :slight_smile:

Done! Thanks for the awesome support!!

1 Like

Thank you, I appreciate it! :heart:

1 Like

I hope this thread is the correct place to post this question.

In the plugin that Iā€™m developing (using Electron to run my Node server), Iā€™m needing to display accurate timing information in the client that stays in sync with the audio. Ablesetā€™s lyrics ā€œempty spaceā€ beat counter does a tremendous job of showing each count with near perfect accuracy. Is there something embedded within the exposed ableset client object that I can use to achieve this? If not, could you recommend a good resource that describes the coding logic?

Many thanks!

You can use AbleSetā€™s global socket for this:

ableset.getSocket("global").on("fineSongTime", (t) => {
  console.log("Fine time:", t)
});

Or, if you only need to be updated on each beat:

ableset.getSocket("global").on("songTime", (t) => {
  console.log("Beat time:", t)
});

I hope this helps!

Thanks a million Leo!

1 Like