How to go about making a knob that springs back, like a physical pitch wheel?

So, I’m very new to module development (and C++/audio dev in general), only made a little toy module so far. Kinda out of my depth, but I’m surprised how fast I’m getting things to work, so I’m trying something a bit more ambitious for my second module: a pitchbend helper that abstracts out the math of it (and will eventually have a bigger version with scale-aware quantization features). Most of the task seems manageable, except for this:

I want my module to provide a knob that springs back in the center position when operated with the mouse.

For now, I’m just resetting the value back to 0 when there’s no change in input for a while.
That’s not a good enough solution, since you often want to hold the pitchbend knob at a specific value for a long while.

Bear in mind that:

  • If the user is using real hardware, they’re expected to patch a MIDI-CV’s PW output to my module’s input, so there’s no software springing back action on CV input - the hardware takes care of it. I’m only concerned with springing back the software knob.
  • The user might intercept the knob while it’s springing back in position, which adds a bit of complexity to the problem.
  • The user might use MIDI-MAP or the like to control the knob, which doesn’t make sense with my intended usage pattern… but modular is all about disregarding the intended usage pattern, innit? Still, I’m not sure what would be the best behavior to default to if they do this.

How should I go about detecting whether the user is currently interacting with the knob? Holding it in the current position counts as an interaction.

This is more of an open-ended question to get suggestions how to do things the right way, since it seems like the kind of thing where a naïve solution could cause issues I didn’t envision.

Previous discussion that might have useful relevant bits: Controlling a knob automatically from another knob

I’d probably look into calling Knob::reset() on the end of drag.

I’ve tested this calling reset() alone which works nicely, though it will make the cursor disappear but only when “Lock cursor while dragging” is enabled (not sure if bug). So the way I got around this was making the calls to cursor also. You have to call the cursor to get it back.

@Vortico is there a more appropriate way to do this? or is this okay?

 struct resetKnobToZero: MyKnob {
	void onDragEnd(const event::DragEnd& e) override {
		Knob::reset();
		APP->window->cursorLock();
		APP->window->cursorUnlock();
	}
};
1 Like

Say this 10 times: “When overriding a method, call the superclass’s method.”

2 Likes

Thanks. The thing I was wondering about the most is whether intercepting it at the onDragStart and onDragEnd level is the best way to do this. Any more elegant/abstract solution that wouldn’t break MIDI input?

E.g., if I delete the ReMOVE here, the knob will remain in place, since it didn’t parse the automation it was sent as a mouse input.

On Windows, I’m not experiencing any mouse weirdness with this, in either mode. I’m using params[PB_PARAM].setValue rather than Knob::reset to reset the value so I can smooth over the change later.

Maybe something like this (untested)

void step() override {
	WhateverSuperclass::step();
	// This is NULL if not attached to a Module
	if (!paramQuantity)
		return;
	// Don't spring back if user is holding the ParamWidget
	if (APP->event->getDraggedWidget() == this)
		return;
	float value = paramQuantity->getSmoothValue();
	float newValue = value + (0.5f - value) * lambda;
	// Don't set smooth value if value didn't change
	if (value == newValue)
		return;
	paramQuantity->setSmoothValue(newValue);
}
1 Like

Thanks! The general idea seems to work for me, I can probably figure out the rest from here :smiley:

Looks like I’m gonna have to add a little pause before I start processing, haha, didn’t realize I should expect MIDI automation to have a lower sampling rate:

Any Mapping will do this anyway regardless.

If it had paramHandle you could reset to default, which-ever suits.

Yeah, not to reset things is definitely the correct default behavior! A simulated spring-loaded knob is very much an edge case, it’s not clear how an user would prefer the module to behave in some advanced use cases.

A software implementation of a pitch wheel just resets it to 0 same as the hardware. Not much difference between letting go of the PW in hardware or software in fact the same message is sent from the brain to a hand.

If you could control a hardware PW with telekinesis the spring would kick in if the connection was lost. :grin:

This module is not released ?

Nope, sorry!

It was just a little test thing I never actually expanded on, but probably will eventually, once I think up a few more pieces to the puzzle (such as configuring the spring, a XY pad that springs back, making it a bit scale-aware, etc).

It only exists as a neglected list of TODOs and naïve beginner code hardcoding all the values that’d make it useful, for now.