Getting started with the Function Sequencer

The Function Sequencer is a very powerful MIDI device. Depending on the preset you load (or the script that you write) it can be a MIDI sequencer, MIDI effect, filter, generative music maker…

Each preset is powered by a small javascript program that defines the automation parameters, the UI, and what MIDI events are generated or modified.

You can see the code that powers each preset by pressing the “Code” button. Edit that code, press “Run”, and your new script is now running. This code window is also multiplayer - invite other programmers and you can edit that file together! Or invite other music-makers, and have them play the device while you live-code new features on the fly.

Usage

When you pick Function Sequencer from the device list, you are prompted to pick a preset. If you press ‘cancel’ you will have loaded a simple 16-step pitch and step sequencer.

The UI for each preset is different as the script writer can control what knobs, sliders, and buttons are presented. The parameters defined by the script are automate-able, so you can wire up a step modulator or randomizer (or more modulators in the future).

That’s about it! Press the buttons, turn the knobs.

Writing a Sequencer

Let’s write a very simple sequencer, and then slowly add features to learn features of the plugin.

First, lets set up a small session suitable for writing our script. Load up a Function Sequencer, a MIDI Debugger, and something that makes noise like a Synth-101. Wire the Function Sequencer MIDI output to the MIDI Debugger and the Synth-101, and assign the Synth-101 to Track Output.

1) From scratch

Press the ‘Code’ button, and delete everything. We’re starting from scratch.

Here is the code for your first sequencer:

/**
 * @implements {FunctionSequencer}
 */
class MyFirstSequencer {
}

return new MyFirstSequencer()

Hit “Run”. Congrats, you just wrote a MIDI sequencer! But it doesn’t do anything.

Before we make it do anything, lets take a look at what we’ve got here. We’re defining a class called MyFirstSequencer and at the end of the script we return an instance of the class.

The comment above the class is in a format called JSDoc, and it’s there to instruct the code editor that our class implements FunctionSequencer. By adding that comment, code complete should appear when you start typing function names:

As you’ll see in the next steps, the plugin will call into your class when events happen (we receive MIDI, the transport starts or stops, or a ‘tick’ event which we’ll discuss next).

As well, your plugin can call out to various subsystems to make things happen - register parameters for automation, register a UI design, emit MIDI events, and more.

2) Make some noise

We’re going to implement a method in our class called onTick. onTick is called 96 times every beat when the transport is running. That’s perfect for emitting MIDI events on a schedule.

Replace your script with this code:

/**
 * @implements {FunctionSequencer}
 */
class MyFirstSequencer {
    onTick(ticks) {
        if (ticks % 24 == 0) {
            api.emitNote(0, 40, 100, 0.1)
        }
    }
}

return new MyFirstSequencer()

Press Run (or press Ctrl-S) and then start running the transport. Tada! You hear notes.

So, what did we just do? Our sequencer class implemented the onTick method. It receives one parameter - ticks - which is the number of time ticks that have occurred since transport started. Our sequencer is very simple. Once every 16th note (when ticks % 24 equals zero), we call api.emitNote to emit a MIDI note.

3) ‘api’ Class Reference

The ‘api’ object is your script’s interface to the rest of the world. Your script calls methods on api to interact with other plugins - emitting MIDI notes, registering automatable parameters, and more.

If you start typing api. you should see the list of methods your script can call. Here is the complete definition of the methods you can call, and what parameters they accept:

    /**
     * emits a MIDI Note on message followed by a MIDI Note off message delayed by the duration
     * @param channel {number} the MIDI channel minus one, from 0-15. So to emit on channel 1, send a 0.
     * @param note {number} the MIDI note number, from 0-127
     * @param velocity {number} MIDI note on velocity, from 0-127
     * @param duration {number} the midi note duration, in seconds.
     * @param startTime {number} optionally set the starting time of the note, in relation to api.getCurrentTime()
     * */
    emitNote(channel: number, note: number, velocity: number, duration: number, startTime?: number): void;

    /**
     * Emit a regular, non-sysex MIDI message up to 3 bytes in length.
     * @param bytes {number[]} a 1 to 3 array of bytes, the raw MIDI message.
     * @param eventTime {number} the time to emit the event, relative to api.getCurrentTime()
     * */
    emitMidiEvent(bytes: number[], eventTime: number): void;

    /**
     * returns the current time
     * @returns {number} the current audioContext time, in seconds
     */
    getCurrentTime(): number;

    /**
     * returns the duration, in seconds, for the input number of ticks
     * @param ticks {number} the number of ticks to convert to seconds
     */
    getTickDuration(ticks: number): number;

    /**
     * Set (or unset) a list of named MIDI notes.  Used to inform earlier MIDI processors what MIDI notes are valid.
     * @param noteList {NoteDefinition[]} a list of midi notes this processor accepts.  Set to undefined to clear the custom note list.
     */
    setCustomNoteList(noteList?: NoteDefinition[]): void;

    /**
     * Register the complete list of plugin parameters.  These parameters can be mapped to UI controls and are exposed to the host for automation.
     * @param parameters {ParameterDefinition[]} the list of parameters to register for the plugin.
     */
    registerParameters(parameters: ParameterDefinition[]): void;

    /**
     * Stores an additional variable into the patch.  This gets sent to other collaborators and will be restored after refreshing the page.
     * Be warned: this is an expensive operation as the value change is sent to the server and all other users.  Only use this function
     * to hold state that is not in a registered parameter (which are automatically synced to the server).
     * Calling setState() will result in your onStateChange() callback running on all plugin instances including locally.
     * @param name {string} the variable name
     * @param value {any} the value to store
     */
    setState(name: string, value: any): void;

    /**
     * Returns the stored value for a variable name that was previously stored with setState.
     * @param name {string} the variable name to return
     * @returns {any} the previously stored value, or undefined if nothing is stored.
     */
    getState(name: string): any;

    /**
     * Returns the values for all parameters that were registered by registerParameters.
     * @returns {Record<string, number>} a map of parameter names to parameter values
     */
    getParams(): Record<string, number>;
}

4) Emitting notes

So, back to our sequencer, you can see that our call to api.emitNote(0, 40, 100, 0.1) will emit note 40 on channel 0, at velocity 100, and it will last for 0.1 seconds. The plugin takes care of scheduling the corresponding MIDI note off event 0.1 seconds later so the script does not need to manage sending the MIDI off event.

Emitting the same note every 1/16th isn’t very interesting. As well, we should calculate our note time to be a certain number of ticks, which depends on tempo. Thankfully api can help with that.

/**
 * @implements {FunctionSequencer}
 */
class MyFirstSequencer {
    onTick(ticks) {
        if (ticks % 24 == 0) {
            const duration = api.getTickDuration(20)
            api.emitNote(0, 40 + Math.floor(Math.random() * 20), 100, duration)
        }
    }
}

return new MyFirstSequencer()

Two new things to note in this iteration of our sequencer:

  1. we call api.getTickDuration() to get the duration, in seconds, of 20 ticks. Since we’re playing a new note every 24 ticks, that gives us a 4 tick break between each note. If we wanted to ‘tie’ our notes together, we could play a note longer than 24 ticks so that the next note would start before the previous note ends, but if you do this for all notes don’t be surprised if some sequencers stop making any sound as there will always be a held note.

  2. Instead of emitting a static note, we are picking a random note value with
    40 + Math.floor(Math.random() * 20). This is an example of us using part of the Javascript API built into browsers. Math.random returns a random value between 0 and 1. We multiply that by 20, and then turn it back into an integer with Math.floor.

Cool, now our sequencer plays different notes. Let’s add some controls!

5) Parameters

Register a parameter and it is exposed to the host - it can be automated, randomized, saved and reloaded in a preset. As well, we bind UI controls directly to parameters to speed up plugin development and reduce the amount of code necessary to build UI.

Parameters have names, and a parameter configuration. The configuration informs the host the type of the parameter, and it’s valid range.

Let’s make our root note a parameter.

/**
 * @implements {FunctionSequencer}
 */
class MyFirstSequencer {
    init() {
        api.registerParameters([
            {
                id: "root",
                config: {
                    type: "int",
                    defaultValue: 40,
                    minValue: 20,
                    maxValue: 80
                }
            }
        ])
    }

    onTick(ticks) {
        if (ticks % 24 == 0) {
            const params = api.getParams()

            const duration = api.getTickDuration(20)
            api.emitNote(0, params.root + Math.floor(Math.random() * 20), 100, duration)
        }
    }
}

return new MyFirstSequencer()

Many new things!

  1. We added another method to our class, called init(). init() is called immediately after the plugin is loaded. It’s a good place to register parameters and a UI.

  2. In onTick, we obtain the values for all of our registered parameters by calling api.getParams(). This returns all of the parameter values. You can access the parameter value either with the style params.name or params["name"].

  3. Lastly, if you switch to the GUI panel you will see that a UI has been generated for you. This is a very simple UI, it renders a knob or selector for every parameter that has been registered.

6) UI

The generated UI is ok, but as our sequencers get more complex we’ll want to control the layout. Thankfully that’s possible as well.

Similar to how we registered our parameters, we can register a custom UI.

UI’s are made up of columns and rows of elements.

The goal of this UI interface is to give script writers a lot of control and power with what their UI looks like, without them writing a lot of code that is typically a source of a lot of bugs.

Let’s look at our UI code separately for a second:

api.registerUI(ui.Col("mainColumn", [
    ui.Label("hello", {label: "hello world"}),
    ui.Knob("root", {width: 200, height: 200})
]))

This creates the following UI:

Our UI is one main column, with a label and a knob. The first parameter to each UI element is it’s identifier, which has to be unique. For knobs, sliders, toggles, and selects, the ID should match the ID of a registered parameter. This automatically connects the UI to control the parameter value.

7) Next Steps

Congrats, that’s the basics of Function Sequencer! There are other concepts we’ll cover in an intermediate tutorial at a later date. From here, try reading the code of the presets. Feel free to post on here or our discord if you’re in trouble!