Javascript: Is it possible to set a seperate action for quick press and hold eg. setTimeout?

Post Reply New Topic
RELATED
PRODUCTS

Post

In Virtual DJ we had the ability to do an action if a button is held for a certain period of time or released before that time elapsed, like a built in shift button easily making buttons dual purpose.

I tried using clearTimeout & setTimeout but I get an error as it's not part of the available API. Is there any alternatives I can use for the same thing?

This is what I would do in the javascript web world.

Code: Select all

let timer = null;
const whileHeldDelay = 600;
const controls = initArray(0, 128).map((x, i) => ({
   lastPressed: 0,
   timer: null,
}));

function onMidi(status, note, velocity) {
   const now = Date.now;
   clearTimeout(controls[note].timer);
   if (isNoteOn(status)) {
      controls[note].lastPressed = now;
      controls[none].timer = setTimeout(() => {
         // do something when held 
         log('while held', controls[note]);
      }, whileHeldDelay);
   }
   if (isNoteOff(status)) {
      if (now - controls[note].lastPressed < whileHeldDelay) {
         // do something when quickly pressed
         log('quick press', controls[note]);
      }
   }
}
Cheers

Post

Use the scheduleTask methods of ControllerHost instead.

Post

Excellent! Thanks Moss! And thanks for the awesome youtube tutorials!

Post

Actually more question. Is there a way to cancel a task? or should I handle it in the callback with an if statement eg.

Code: Select all

let timer = null;
const whileHeldDelay = 600;
const controls = initArray(0, 128).map((x, i) => ({
    lastPressed: 0,
    shouldCancel: false,
}));

function onMidi(status, note, velocity) {
    const now = Date.now;
    controls[note].shouldCancel = true;
    if (isNoteOn(status)) {
        controls[note].lastPressed = now;
        controls[note].shouldCancel = false;
        host.scheduleTask(() => {
            if (controls[note].shouldCancel) {
            	// prevent action from occurring.
                return;
            }
            log('while held', controls[note]);
        }, whileHeldDelay);
    }
    if (isNoteOff(status)) {
        if (now - controls[note].lastPressed < whileHeldDelay) {
            // do something when quickly pressed
            log('quick press', controls[note]);
        }
    }
}
I'm refactoring so my code isn't running currently, I'll try it out once I'm done.

Post

You cannot cancel them. So, you need to check in your callback if the button is still pressed.

If you need more complex task handling you need to switch to Java.

Post

Java is scary, I know nothing about it haha! I'll see how far I get..

Actually, according to this post https://eighteyes.github.io/bitwig/utilities.html they are using setInterval as the abstracted name. is host.scheduleTask a single event, or a re-occurring one?

Post

synthet1c wrote: Sat Jan 07, 2023 10:43 pm Java is scary, I know nothing about it haha! I'll see how far I get..

Actually, according to this post https://eighteyes.github.io/bitwig/utilities.html they are using setInterval as the abstracted name. is host.scheduleTask a single event, or a re-occurring one?
It is a single event. You need to call it again if you need something like an interval. Also note that you get no guarantee that the callback will be called after exactly the given time.

Post

Cool, I can make set interval from setTimeout, although I can't imagine I would use setInterval. Thanks once again!

Post

So I was able to add the functionality for press, hold and release, but it's very inconsistent. Is `host.scheduleTask` reliable enough to trigger multiple events and handle them? The order doesn't matter as they are tokenised, a few milliseconds is fine, but would it be seconds, or not even firing at all.
  • Press - Quick press, triggers on release if less than 100ms has elapsed since pressing it
  • Hold - Trigger onNoteOne after holding the button for 100ms
  • Release - Trigger onNoteOff after releasing the button from the hold state
Press usually bugs out after 2 or 3 taps, but hold and release work fine.

I created a setTimeout & clearTimeout function which seems to store the latest subscription id's and cancel the events, but it seems the `host.scheduleTask`

Github timers.ts

Code: Select all

let timers: number[] = [];
let timerId = 0;
export function setTimeout(cb: () => void, delay: number) {
    const id = ++timerId;
    timers.push(id);
    host.scheduleTask(() => {
       // timers should always be called
        if (timers.indexOf(id) === -1) {
            // if the timer is removed no need to perform the action
            return;
        }
        // Only if the timer id is present in the timers array should the event be published
        cb();
    }, delay)
    return timerId // this token is used to cancel the subscription
}

export function clearTimeout(timerId: number) {
    const index = timers.indexOf(timerId);
    if (~index) {
       // if the timer id is in the timers array remove it
        timers.splice(index, 1);
    }
}
The subscriptions are fired in the Control class midi callback.
Github Control.ts

Code: Select all

static onMidi = (status: number, channel: number, note: number, velocity: number): void => {
        const key = `${channel}-${note}`;
        const control = controlState[key];
        const now = Date.now();
        if (control && control.usesPress) {
            if (isNoteOn(status)) {
                control.lastPress = now;
                clearTimeout(control.timeoutId);
                control.timeoutId = null;
                control.timeoutId = setTimeout(() => {
                    this.triggerSubscribers(control, ({ event }) => event.event === 'hold')(status, channel, note, velocity)
                }, 100);
            }
            else if (isNoteOff(status)) {
                clearTimeout(control.timeoutId);
                if (now - control.lastPress < 100) {
                    this.triggerSubscribers(control, ({ event }) => event.event === 'press')(status, channel, note, velocity)
                } else {
                    this.triggerSubscribers(control, ({ event }) => event.event === 'release')(status, channel, note, velocity)
                }
            }
        }
        // ...
    }
I made my code more webby adding jQuery like event listeners to publish events, but that should not change the underlying issue of the setTimeout firing inconstently.

Github Mixer.ts

Code: Select all

private testControlPressAndRelease = (control: Control, track: Track) => {
    let toggle = false;
    control.setState(LedButtonStates.AMBER);
    control.on('hold', (event) => {
        log('on:hold', event);
        control.setState(LedButtonStates.RED);
    });
    control.on('release', (event) => {
        log('on:release', event);
        control.setState(toggle ? LedButtonStates.GREEN : LedButtonStates.AMBER);
    });
    control.on('press', (event) => {
        log('on:press', event);
        toggle = !toggle;
        control.setState(toggle ? LedButtonStates.GREEN : LedButtonStates.AMBER);
    });
}

Post

So I made a setInterval function to test.

Code: Select all

let intervals: number[] = [];
let intervalId = 0;
export function setInterval<T>(cb: () => void, delay: number) {
    let token = ++intervalId;
    intervals.push(token);
    const _setInterval = (cb: () => void, delay: number) => {
        host.scheduleTask(() => {
            if (intervals.indexOf(token) === -1) {
                return;
            }
            cb();
             _setInterval(cb, delay);
        }, delay);
    }
    _setInterval(cb, delay);
    return token;
}

export function clearInterval(token: number) {
    const index = intervals.indexOf(token);
    if (index > -1) {
        intervals.splice(index, 1);
    }
}
The resolution is really bad. I get the following results for 1000ms, 200ms, 20ms, 10ms, 0ms.

Code: Select all

# 1000ms interval
--> elapsed: 3220  duration: 3220
--> elapsed: 6346  duration: 3126
--> elapsed: 9494  duration: 3148
--> elapsed: 12582  duration: 3088
--> elapsed: 15770  duration: 3188
--> elapsed: 18889  duration: 3119
--> elapsed: 22045  duration: 3156

# 200ms interval
--> elapsed: 526  duration: 526
--> elapsed: 1133  duration: 606
--> elapsed: 1711  duration: 578
--> elapsed: 2318  duration: 607
--> elapsed: 2963  duration: 645
--> elapsed: 3608  duration: 645
--> elapsed: 4281  duration: 673

# 20ms interval
--> elapsed: 38  duration: 38
--> elapsed: 110  duration: 72
--> elapsed: 188  duration: 78
--> elapsed: 261  duration: 73
--> elapsed: 305  duration: 44
--> elapsed: 378  duration: 73
--> elapsed: 455  duration: 77

# 10ms interval
--> elapsed: 38  duration: 38
--> elapsed: 59  duration: 20
--> elapsed: 85  duration: 26
--> elapsed: 106  duration: 21
--> elapsed: 154  duration: 48
--> elapsed: 204  duration: 49
--> elapsed: 255  duration: 51

# 0ms interval
--> elapsed: 16  duration: 17
--> elapsed: 44  duration: 27
--> elapsed: 66  duration: 22
--> elapsed: 94  duration: 28
--> elapsed: 116  duration: 22
--> elapsed: 144  duration: 28
--> elapsed: 168  duration: 24
I guess I can just divide the expected time by 3 to get around the right delay.

2 questions:
  • Would I need to rely on `host.scheduleTask` to blink a LED in time with the beat?
  • Is this something I can ask the bitwig devs to improve, or is it a limitation of the Javascript engine inside Java?
No wonder people think Javascript sucks haha!

Post

I use Java... I do feel the timing is better. not perfect but great enough for this type of things. You will like Java it's not much more difficult than .js and makes things easier in the long run.

I try to do all my led updates in flush(). This is done by storing a state of something. And then updating the controller in flush if that state has changed.

And then have something visual that needs to be beat synchronized I use the transport and derive from that. It seems to work here I display all sorts of transport data and midi notes on my iPad and it always feels like it's in sync.
----------------------------------------------------------------------
http://instagram.com/kirkwoodwest/
http://soundcloud.com/kirkwoodwest

Post

This is by design. Highest priority in Bitwig is to deliver audio without any dropouts no matter what the user does.
Delivering data / callbacks to the API has very low priority. It is not designed to do any kind of sync of devices or blinking LEDs.
As Kirkwood wrote, you can get close with some tricks. But it gets worse the higher the CPU demand is of the running project. The flush() method has a very high callback rate and also you can create a stable Java thread but you will still miss to have any kind of sync signal into the API from Bitwig. With some devices you can send them MIDI clock to make their LEDs blink in time.

Post

Thanks for the clarification guys! It seems I'm asking a little too much, I will reel in my expectations a little and work with what I have.

But I'll check the callback rate of flush and see if I can make an event loop out of it.

For blinking leds, the only thing I really want is a phrase counter (4 bars) so it doesn't really matter if it's out timing wise. It would have been cool to have dual purpose buttons, but definitely not a deal breaker.

I'll probably ignore your advice for now Kirkwood West, and finish my Javascript mapping, then port it over to Java once I understand the API better!

Post

synthet1c wrote: Mon Jan 09, 2023 7:33 pm I'll probably ignore your advice for now Kirkwood West, and finish my Javascript mapping, then port it over to Java once I understand the API better!
Thats what I did until i finished my first. But I do want to say you are already doing some advanced stuff so when you are ready it will be there.

Drive your leds entirely in the flush() based on the current transport time and you'll be good.
----------------------------------------------------------------------
http://instagram.com/kirkwoodwest/
http://soundcloud.com/kirkwoodwest

Post Reply

Return to “Controller Scripting”