Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Z alternative #241

Closed
unthingable opened this issue Sep 25, 2020 · 10 comments
Closed

Z alternative #241

unthingable opened this issue Sep 25, 2020 · 10 comments

Comments

@unthingable
Copy link
Contributor

unthingable commented Sep 25, 2020

I think I'm here to advocate for a breaking change.

There are three conceptual ways to implement Zs "rate" parameter:

  1. step interval: how many steps it will advance per frame
  2. actual rate: how quickly the steps will advance (step size undefined and we can't do sub-frame timing, if we assume steps size of 1 we can't go any faster than unit increment per frame)
  3. duration: how quickly (in frames) it takes to get to the target value

Currently it's (1) but I believe (3) would make more sense.

  • It's more useful in performance context (at least to me, I'm more interested in when it will reach the target rather than what size the individual steps will be)
  • It combines functionality of both (1) and (2) and offers more possibilities than either of them (if I'm not mistaken, it covers all possible values of (1) and half those of (2) — with current Z any nZm where n>m is equivalent to nZn)

Assume we're starting with:

.Z.
.1.
case current Z new Z
1 step in 1 frame 1Z1 1Z1
z steps in 1 frame zZz 1Zz
z steps in z frames 1Zz zZz
f steps in 8 frames 2Zf 8Zf
f steps in 7 frames nope 7Zf
5 steps in z frames nope zZ5

In other words, it can go as fast as current Z, as slow as current Z, and slower than current Z can. It always takes the same number of frames regardless of the target value and allows more non-integer relationships between duration and target.

Pretty sure it's possible to implement duration-based Z with the current step-based Z but only for a subset of possible cases, because of the aforementioned limitations.

@neauoire if you would consider accepting a PR, I'm happy to attempt one. As described above It's less trivial to implement then a step-based Z because it needs to store additional state, either on the grid or elsewhere.

@unthingable
Copy link
Contributor Author

unthingable commented Sep 26, 2020

A crude initial version for consideration, using an additional input as placeholder for the current sequence (state has to go somewhere):

......
.zzZ2.
...2..
......

Left inputs:

  • duration (in frames)
  • current frame (runs from 0 to duration in unit increments, the operator updates it but it's also overwriteable! Can be used to shift to a different part in the sequence while it's running)

Right input:

  • target (same as current Z)

Output:

  • value

In use:

The operator is either stable (target == value) or running, same as with current Z. When target input changes:

  • If operator is stable, it will start lerping from value to target in equal size increments and keeping track of the current frame in the second input. This will take exactly duration frames unless you mess with the frame counter (or duration itself). Can even drive it with C (it will always win because of order of evaluation), lots of interesting possibilities.
  • If operator is already running, it will retarget mid cycle without affecting the remaining duration and lerp towards the new target at a new rate. Modulating the target makes the output behave nonlinearly.

I was unenthused about the additional input at first, but it came out cooler than expected.

library.z = function OperatorZ (orca, x, y, passive) {
  Operator.call(this, orca, x, y, 'z', passive)

  this.name = 'lerp'
  this.info = 'Transitions operand to target'

  this.ports.duration = { x: -2, y: 0, default: '8' }
  this.ports.stepped = { x: -1, y: 0, default: '0' }
  this.ports.target = { x: 1, y: 0 }
  this.ports.output = { x: 0, y: 1, sensitive: true, reader: true, output: true }

  this.operation = function (force = false) {
    const duration = this.listen(this.ports.duration, true)
    const target = this.listen(this.ports.target, true)
    const val = this.listen(this.ports.output, true)
    var stepped = this.listen(this.ports.stepped, true)
    if (stepped >= duration) {
      stepped = duration
      this.output(stepped, this.ports.stepped)
    }
    if (val !== target) {
      if (stepped === duration) { stepped = 0 }  // previous iteration done, ok to restart
      const steps = duration - stepped
      const remaining = target - val
      const mod = Math.round(remaining / steps)
      this.output(orca.keyOf(val + mod), this.ports.output)
      this.output(orca.keyOf(stepped + 1), this.ports.stepped)
    }
  }
}

@unthingable
Copy link
Contributor Author

unthingable commented Sep 26, 2020

Flipping positions of duration and frame counter arguments will allow this construct:

......
..9Y9.
..J.J.
.49Z9.
...9..
......

Original Z behavior.

@neauoire
Copy link
Member

This would break almost every set I've ever written since I use the current Z implementation to handle cross fading. I've copy this implementation over my fork to mess around with it for a few days :) Will keep you posted, can you show me how you use it to write music? I'd love to see it used in practice.

@unthingable
Copy link
Contributor Author

unthingable commented Sep 27, 2020

It sure would! Hence the breaking change designator :)

I do think the flipped arguments make more sense, I'm using this version to mess around: https://github.com/unthingable/Orca/tree/z.

I'll try to use it in a composition and show you. Base use case is crossfades, same as original Z, except I know exactly how long the fade will take and can time it musically when I'm triggering crossfades manually, while using different target values each time. What's more, I can do synchronized multi-parameter crossfades.

For example, let's say I have a complex melody consisting of 3 concurrent parts, each playing at default velocity. I can crossfade their velocities to different targets and have them get there at the same time, together.

.......................
..d.O.a.O.7.O....#0fb#.
.ggZ0fgZfggZb....#z0z#.
.1V0.2Vf.3Vb.....#0z0#.
.......................

Here I can drag the commented block up and down, variables 1, 2 and 3 will crossfade synchronously. Profit, I think.

@neauoire
Copy link
Member

Sorry if took me a while to get back to you, I've been trying all sorts of different Z iterations. Someone suggested something that I kindda like, and I wanted to run it by you and know what you thought. It's basically the opposite of the current rate, where instead of being a rate at which it will change toward the target, it's the modulo of the frame at which it will change, so for example:

  • 4Za, every 4 frames it will animate by 1 toward a.
  • 8Zm, every 8 frames it will animate by 1 toward m.

@unthingable
Copy link
Contributor Author

No worries at all! Yes, it is the opposite, more or less exactly the complement of original Z (covering a mostly disjoint set of cases)

Another table, let's call this "this", my suggestion "duration based" and assume Z is fading from a to b (variables, not values):

Z variant runs from a to b at fastest largest transition slowest smallest transition
original no slower than 1 step per frame 1 frame, z steps (from 0 to z) |a - b| frames
this no faster than 1 step per frame |a - b| frames z*z frames from 0 to z by 1 every z frames
duration constant time 1 frame,z steps z frames, 1 step

I think each is interesting and they cover different sets of cases, with some overlap:

covers original this duration
original exactly one (1 frame 1 step) half
this exactly one (1 frame 1 step) all
duration all some

In other words, "this" and original each cover cases unattainable by the other (except one), "duration" covers all of original cases and some of "this". "This" covers cases unattainable by either of the two variants (really long fades). To me, the duration variant still seems the most practical, it's more expressive and makes it easy to synchronize fades to musical grid.

Well, with #248 we can have them all :) Thought not at once.

@neauoire
Copy link
Member

neauoire commented Oct 31, 2020

The problem I have with the current Z is that transitioning to another value is done very fast, and in most cases I use a rate of 1 because I want to do a slow transition, even 1 is a bit fast. So the invert where it only animates on frame % rate == 0 would probably answer a lot more usecases, especially if you want to slowly transition.

I don't want to add an extra port to Z..

I love your markdown tables btw, always help me understand what you mean.

@unthingable
Copy link
Contributor Author

A duration-based Z without an extra port would be ideal (to me), I just couldn't find a simple way to do it without storing additional state somewhere, either in a port or somewhere internally (in fact I suspect it's not even possible).

@neauoire
Copy link
Member

You can simply do this if you want the feature in the meantime:

.D..
....
.z9.

@neauoire
Copy link
Member

After using the new Z design for a while, I've reverted it to the classic version since this behavior can easily be recreated with a D operator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants