cmajor-lang/cmajor

Cannot use `value` in an event handler?

Closed this issue · 4 comments

Issue

Given the following minimal patch:

processor ValueInEvent  [[ main ]]
{
    input event std::midi::Message midiIn;
    input value std::timeline::Tempo tempoIn;
    output stream float out;

    event midiIn(std::midi::Message midi) 
    {
        console <- tempoIn;
    }
}

I get the following error:

error: Streams cannot be used in event callback functions

It seems like this error is meant for using streams in an event instead of values, and maybe values ought to be allowed?

Workaround

If not, I can treat values as events instead. Then cache them to access them from other events. Which works but seems like needless boilerplate:

processor Untitled  [[ main ]]
{
    input event std::midi::Message midiIn;
    input event std::timeline::Tempo tempoIn;
    output stream float out;

    float bpm;

    event tempoIn(std::timeline::Tempo tempo) 
    {
        bpm = tempo.bpm;
    }

    event midiIn(std::midi::Message midi) 
    {
        console <- bpm;
    }
}

Environment

  • Cmajor Tools: v1.0.2397
  • macOS v14.2.1 (23C71)
  • Apple Silicon M1

That's a good question, I'm surprised it didn't work. Not sure whether there's a fundamental reason why we can't do this, but if not we should be able to relax that rule

I agree, it looks like it should work, but i've had a think through the implications, and there are issues...

If you were to submit a value change and an event at the same frame, the event handler would not see the value change, in some technical implementation specific cases - if it's a top level node for example. So, although we could relax this rule I think it would introduce some weird behaviour. Now we could consider this to then be a broken implementation of the language, and want to address this, but another way of looking at it would be to say why should code be able to read values in an event handler, when it can't read other event, or streams?

The error message is confusing, i'll get that updated!

The updated error message has been merged in

@cesaref wrote:

why should code be able to read values in an event handler, when it can't read other event, or streams?

To me, the answer to this question is obvious. Events and streams are sample-accurate. Values are, by definition, not. If I'm wrong about this, you can safely ignore the rest of this post ;-)

It makes intuitive sense to me that sample-accurate endpoints cannot access one another without having a fight over what “now” is. But "laggy" endpoints like value do not have that limitation — they’re just the “best effort” representaiton of whatever their most recent update was whenever it arrived.

And if it arrives late? ¯\(ツ)/¯ Doesn’t matter. If it did matter, it would be sample-accurate! And this is, I think, value’s super power. It’s less demanding of sample-accuracy and in exchange can have fewer constraints around it. But it feels like those constraints are being applied anyway.

So, although we could relax this rule I think it would introduce some weird behaviour.

I don’t know about weird. Maybe unexpected? But importantly: no different from the behavior of the boilerplate we would have to write out, regardless?

Assume a synth on a touchscreen with no velocity. We might include an “expression” control to manually set the velocity of a struck note. The note on would obviously be an event. And I think we agree the natural endpoint for the velocity expression would be a value.

We might want a processor in our chain that takes the note event, expression value, and combines them into a single note-with-velocity to pass on to our synth engine:

processor Merge
{
    input event std::notes::NoteOn noteIn;
    input value float expression [[ name: "exp", min: 0.0, max: 1.0 ]];
    output event std::notes::NoteOn noteOut;

    event noteIn (std::notes::NoteOn n)
    {
        n.velocity = expression; // Error!
        noteOut <- n;
    }
}

This won’t work because expression is a value. So we’d have to write something very similar to:

processor Merge
{
    input event std::notes::NoteOn noteIn;
    input value float expression [[ name: "exp", min: 0.0, max: 1.0 ]];
    output event std::notes::NoteOn noteOut;

    float exp_state;

    event noteIn (std::notes::NoteOn n)
    {
        n.velocity = exp_state;
        noteOut <- n;
    }

    void main()
    {
        loop {
            exp_state = expression;
            advance();
        }
    }
}

Wouldn’t this yield the same situation where the event might not “see” the latest value of expression depending on implementation details?

If so, I think you could simplify users’ mental model, reduce boilerplate, and perhaps remove redundancies around whatever register is holding the value if you relaxed the “no values in events” rule. And you wouldn’t actually lose anything with regard to fuzzy non-sample-accurate -> sample-accurate behavior because you’ll have that either way ;-)