helgoboss / helgobox

Helgobox: ReaLearn & Playtime
https://www.helgoboss.org/projects/helgobox
GNU General Public License v3.0
210 stars 20 forks source link

Add "Don't squeeze" option to discrete targets #184

Open helgoboss opened 3 years ago

helgoboss commented 3 years ago

At the moment, discrete targets with a variable about of discrete values (such as "Selected track" and "FX parameter") are most useful with relative encoders or prev/next buttons. With (absolute) faders and knobs they are only useful for birds-eye view navigation, not for precise control. Because the target range is not constant, precise control is impossible and every change in the number of discrete values (e.g. tracks) will let things fall apart.

We could offer an option to make the number of discrete values irrelevant and set a fixed number of relevant values instead.

Another similar thing: "Seek" target currently always relates to the complete time range in question. Great for many use cases. But if you want the seek increment/decrement for to be the same no matter the size of the time range, you need another solution. Either an action, but that would need #129. Or a new feature, maybe related to the "rounding" step size.

vonglan commented 3 years ago

sounds similar to the "zoom factor" request #204

helgoboss commented 3 years ago

@vonglan It's something else.

helgoboss commented 3 years ago

This would need a new kind of target control value in addition to Absolute (%) and Relative (+/- n): Absolute (n)

helgoboss commented 3 years ago

This should be a new absolute mode "Discrete" or "No scale". It takes absolute discrete control values and passes them on to the target without scaling/squeezing. When adding #315 to the mix, we would then have a quite symmetrical looking set of control value kinds:

Pseudo code:

enum ControlValue {
    /// Already existing: Absolute value that represents a percentage (e.g. fader position on the scale from lowest to 
    /// highest, knob position on the scale from closed to fully opened, key press on the scale from not pressed to
    /// pressed with full velocity, key release).
    AbsoluteContinuous(UnitValue),
    /// New for #184: Absolute value that is capable of retaining the original discrete value, e.g. the played note 
    /// number, without immediately converting it into a UnitValue and thereby losing that information - which is
    /// important for the new "Discrete" mode.
    AbsoluteDiscrete(DiscreteValue),
    /// New for #315: Relative increment that is very specific in that it already expresses a certain step size.
    RelativeContinuous(UnitIncrement),
    /// Already existing: Relative increment (e.g. encoder movement)
    RelativeDiscrete(DiscreteIncrement),
}

/// Already existing: Represents a percentage in the form of a value of the unit interval (0.0 to 1.0).
struct UnitValue {
    // ...
}

/// Already existing: Represents an increment within the positive or negative unit interval (-1.0 to 1.0).
struct UnitIncrement {
    // ...
}

struct DiscreteValue{
    /// Concrete discrete value.
    actual: u32,
    /// Maximum value: Good to know in order to be able to instantly convert to a UnitValue whenever we
    /// want to go absolute-continuous.
    max: u32
}

/// Already existing: Represents a discrete increment, either positive or negative. Example: +5
struct DiscreteIncrement {
    // ...
}
helgoboss commented 3 years ago

On second thought:

On third thought:

vonglan commented 3 years ago

Regarding scaling: the scaling based on (source_min ... source_max) and (target_min ... target_max) is logically only possible for absolute sources. But for relative sources, a simple zoom factor (#204 ) would be possible and useful IMHO.

I must say that I do not yet understand the difference between discrete and continuous sources, because technically, I am sure all sources are discrete, as they are digital. To me it seems it is just a ReaLearn convention that for "discrete" sources the smallest increment is encoded as 0.01 or 1%, and then a discrete target can translate that back to "smallest increment" with regard to the target. So I think the difference between the two is just that the discrete increment says to the target "by the way, my smallest delta is xxx, in case you want to use that information".

Could a normal 7-bit potentiometer also be represented as discrete, with the smallest Increment then being 1/127?

helgoboss commented 3 years ago

Regarding scaling: the scaling based on (source_min ... source_max) and (target_min ... target_max) is logically only possible for absolute sources. But for relative sources, a simple zoom factor (#204 ) would be possible and useful IMHO.

I know, I know, I get the message ;)

I must say that I do not yet understand the difference between discrete and continuous sources, because technically, I am sure all sources are discrete, as they are digital.

Yes, it's all binary in computers but it still makes sense to distinguish between numbers that are countable/discrete (integers) and numbers that are (or at least are perceived as) continuous (floating point numbers). When I say "discrete control value", I mean integers. MIDI only has integers, it doesn't have floating point numbers. In many cases this is just a protocol limitation, e.g. I guess 14-bit CCs are just the MIDI way of controlling something continuous, whereas floating point numbers would be suited better for this purpose.

But there are cases in which the integers in MIDI make much sense, e.g. keys on a piano! There are no other keys between C4 an C#4 ... that means the set of keys is discrete. Up until now, what ReaLearn does is taking the incoming discrete value, e.g. 63 (of 128 possible values) and immediately converts it to the normalized floating point value 0.49606299... by doing 63 / 127. Because it's easy to process normalized values. It also applies scaling from source min/max to target min/max and thereby squeezing or stretching the value range. That's all great and desired - as long as you have a continuous target, such as volume or pan. But as soon as you have a discrete target - such as "Project: Navigate within tracks" (the set of tracks is discrete) - scaling is usually not desired. If you press key 5 on the MIDI keyboard, you probably want to navigate to track 5 in REAPER ... but this is exactly the scenario which is hard to achieve now, especially because the number of tracks can change.

To me it seems it is just a ReaLearn convention that for "discrete" sources the smallest increment is encoded as 0.01 or 1%, and then a discrete target can translate that back to "smallest increment" with regard to the target. So I think the difference between the two is just that the discrete increment says to the target "by the way, my smallest delta is xxx, in case you want to use that information".

For relative control this doesn't apply. This ticket is about improvement of absolute control via discrete sources only.

Could a normal 7-bit potentiometer also be represented as discrete, with the smallest Increment then being 1/127?

It's the goal of this ticket to improve ReaLearn's processing chain so that the 1 stays a 1 from start (incoming source message) till end (a possibly discrete target). Without any scaling/squeezing/streching) applied on the way (huge difference) and without converting things to floating point numbers (which excludes possible numerical rounding errors happening on the way).

helgoboss commented 3 years ago
  • In which stages do we need to decide whether to use scaling or not? Identify them and decide if it's more feasible to let the target decide about "discrete vs. continuous" or the tuning section.

These are the current operations. The scaling operations among them should be replaced with non-scaling operations (addition/clamping) in discrete mode:

  1. Apply source interval (if source range restricted)
    • [ ] Problem: In controller mappings, when the target is virtual (and the source emits discrete value and the source range is restricted), we can't decide yet whether to use scaling or not because we don't know if the target desires continuous or discrete control. We whould offer a choice in the tuning section.
  2. Apply control transformation
    • It's okay to make this a destructive operation which always converts a discrete value into a continuous value.
    • Therefore, a discrete mode with control transformation filled doesn't make sense. Scaling should be applied as usual.
  3. Apply reverse
    • Okay, not a scaling operation.
  4. Apply target interval
    • Target min/max is only available if the target is known, so we know if the target desires continuous or discrete control.
  5. Apply rounding
    • Ignore if value is discrete.

I guess the problem mentioned in point 1 and the fact that it's not just about "retaining" the discreteness of a value but also about applying completely different operations (addition/clamping instead of multiplication) indicates that this should be a property of the tuning section rather than a property of the target.

In any case, it's important that we make already existing controller presets retain incoming discrete values! Without breaking existing behavior of course.

Therefore I think the way to go is this:

  1. Make both absolute modes "Normal" and the new "Discrete" retain the discreteness of incoming control values as far as possible.
    • "As far as possible" means that it stays discrete until meeting a "destructive" operation (an operation which needs to turn the discrete value into a continuous one).
    • Destructive operations in mode "Normal":
      • Source interval scaling (only if source range restricted)
      • Control transformation (only if filled with a valid formula)
      • Target interval scaling (only if target range restricted)
    • Destructive operations in mode "Discrete":
      • Control transformation (only if filled with a valid formula)
  2. Make absolute mode "Normal" use scaling, but only if necessary.
  3. Make absolute mode "Discrete" use addition/clamping instead of scaling.

Consequences:

vonglan commented 3 years ago

My two cents:

  • Destructive operations in mode "Discrete":

    • Control transformation (only if filled with a valid formula)

I think it would be simpler for the user, if the transformation field would always be inactive and hidden for "discrete". If anyone wants to use the formula, then they can't use "discrete".

helgoboss commented 3 years ago

I changed my mind about that in the meantime:

Then it should be simple: Discrete means discrete all the way (as long as both source and target are discrete).

vonglan commented 3 years ago

Sounds good.

helgoboss commented 3 weeks ago

Note to self, if I unlock this feature at some point: "Scaled absolute control" vs. "Direct absolute control" is a better distinction. It shouldn't be about continuous and discrete.

Scaled Absolute Control: Source and target values are proportionally scaled to each other, with the target's value range mapped to the source’s percentage.

Direct Absolute Control: Source value directly represents the target value without scaling, with each source value corresponding to a discrete target point.