Event dispatch with lookups and lambdas

Post Reply
poetix
Posts: 54
Joined: Mon Nov 28, 2022 3:26 pm

Event dispatch with lookups and lambdas

Post by poetix »

The more things I have in a UI, the more of a pain I find it dispatching events from the Notify method to update smoothed knob values here, start processing a newly-connected input there, etc.

So, I wrote this thing:

Code: Select all

public class NotificationReceiver {

    private final Map<Object, DoubleConsumer> valueObservers = new IdentityHashMap<>();
    private final Map<Object, BooleanConsumer> statusObservers = new IdentityHashMap<>();

    public NotificationReceiver register(Object component, DoubleConsumer valueObserver) {
        valueObservers.put(component, valueObserver);
        return this;
    }

    public NotificationReceiver register(Object component, BooleanConsumer valueObserver) {
        statusObservers.put(component, valueObserver);
        return this;
    }

    private boolean newDoubleValue(Object component, double newValue) {
        var observer = valueObservers.get(component);
        if (observer != null) {
            observer.accept(newValue);
            return true;
        } else {
            return false;
        }
    }

    public boolean knobValueChanged(Object component, double newValue) {
        return newDoubleValue(component, newValue);
    }

    public boolean jackConnected(Object component) {
        var observer = statusObservers.get(component);
        if (observer != null) {
            observer.accept(true);
            return true;
        } else {
            return false;
        }
    }

    public boolean jackDisconnected(Object component) {
        var observer = statusObservers.get(component);
        if (observer != null) {
            observer.accept(false);
            return true;
        } else {
            return false;
        }
    }

}
What does it do? Well, it simplifies your handlers in Notify to this:

Code: Select all

         case Knob_Changed: return receiver.knobValueChanged(component, doubleValue);
         case Jack_Connected: return receiver.jackConnected(component);
         case Jack_Disconnected: receiver.jackDisconnected(component);
         // etc
and the wiring of events starts looking like this:

Code: Select all

receiver.register(frequencyKnob, myController::setFrequencyValue)
        .register(fmAmountKnob, myController::setFmAmountValue);
as opposed to the old style:

Code: Select all

    case Knob_Changed: {
         if (component == frequencyKnob) {
            myController.setFrequencyValue(doubleValue);
            return true;
         }
         if (component == fmAmountKnob) {
            myController.setFmAmountValue(doubleValue);
            return true;
         }
    }
    break;
    // etc
There's a slight penalty in terms of hash lookup and method dispatch, but none of this is happening in the main ProcessSample loop, so I doubt it's worth caring about very much.

A bit further downstream of this, you can start defining components representing common collections of UI controls and their interactions, and writing custom registration methods to do all the wiring for them. Here's an example:

Code: Select all

CvModulatableKnob oddEvenBalance = new CvModulatableKnob(
         0.0, 1.0,
         receiver.registerInput(oddEvenBalanceCv, oddEvenBalanceCv::GetValue),
         receiver.registerSmoothedKnob(oddEvenBalanceKnob, oddEvenBalanceKnob.GetValue()),
         receiver.registerSmoothedKnob(oddEvenBalanceMod, 0.0));
A CvModulatableKnob is actually three things: a big knob which controls a value, an audio input which supplies a modulating signal, and a little knob which controls how much the modulating signal modifies the value. It's a common pattern in many modules.

To make it efficient, we just want to be providing the value of the big knob if the audio input isn't connected. Once the audio input is connected, we want to start pulling values from it, and multiplying them together with the value of the little knob and the value of the big knob to get the actual value of the control - that's a lot more work for the CPU, so we only switch it on when we see that the input's been wired up. We also smooth the two knobs' values, although again there's no point in applying the smoothing to the little knob's value unless the input is connected.

To make all this work, we need to handle four events - knob value changes for the big and little knobs, and input connection and disconnection for the audio input. Now imagine you have several of these. The Notify method can get quite big if we have to write out dispatch for all the events we care about in longhand. By passing them through the notification receiver instead, we can simplify things quite a bit. The CvModulatableKnob doesn't have to know anything about the event handling and value smoothing - it just accepts DoubleSuppliers as inputs, and pulls values from them as it sees fit. The flag that controls whether the input is connected is hooked up like this:

Code: Select all

    public CvModulatableKnob(double lowerBound,
                             double upperBound,
                             DisconnectableInput cvValue,
                             DoubleSupplier knobValue,
                             DoubleSupplier modulationAmount) {
        this.bottom = lowerBound;
        this.top = upperBound;
        this.cvValue = cvValue;
        this.knobValue = knobValue;
        this.modulationAmount = modulationAmount;
        this.outputValue = knobValue;

        cvValue.onConnectionStatusChanged(this::setCvIsConnected);
    }
- we ask the DisconnectableInput object to tell us when the notification receiver has told it that its connection status has changed.

The other thing to notice here is that CvModulatableKnob doesn't know anything at all about VoltageKnob and VoltageAudioJack components, which is just as well because my IDE doesn't know anything about them either. By binding VoltageAudioJack::GetValue as a method reference, I can pass the ability to read a value from an audio jack into code, and a code editing environment, that doesn't have access to the Voltage Modular core libraries. That code is then effectively decoupled from the UI - all it knows is that it has a DoubleSupplier that will give it a double value when it asks for one, and that DoubleSupplier may very well (as here) be supplying a smoothed value rather than the literal value of a UI knob.
London, UK
Developer, Vulpus Labs
Musician, w/trem
UrbanCyborg
Posts: 585
Joined: Mon Nov 15, 2021 9:23 pm

Re: Event dispatch with lookups and lambdas

Post by UrbanCyborg »

Thanks for the clarification. You're clearly more of a Java shark than I; I could manage this sort of thing in C++, but I tend to need to keep things simpler in Java, and I don't have a number of your stated requirements, in any event. Still, very interesting.

Reid
Cyberwerks Heavy Industries -- viewforum.php?f=76
Post Reply

Return to “Module Designer”