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

should we zeropad [*~] with mc signals of different length? #2009

Closed
porres opened this issue Jun 21, 2023 · 15 comments
Closed

should we zeropad [*~] with mc signals of different length? #2009

porres opened this issue Jun 21, 2023 · 15 comments

Comments

@porres
Copy link
Contributor

porres commented Jun 21, 2023

How is [*~] and other objects supposed to work when you have two multichannel inputs of different channel length?

This is how it works now. Say you have 5 channels on one side (A) and 3 on the other (B). The first 3 channels of each A & B are combined but then for the channel 4 and 5 of A are multiplied to channels 1 and 2 of B.

Screen Shot 2023-06-21 at 13 33 09

Is this intentional? I'd expect that the remaining channels 4 and 5 of A would combine with 0 values for the B side has no 4 and 5 channels.

I know that if you have a single channel on one side it makes sense that the single value is multiplied with each channel of the other side, but this would be a special case and maybe the way things were coded to make this work gave us this bug as a side effect.

Or is it an intended feature? I think that we could discuss different ways and choose how to work with this, but having this as the only option seems weird.

One can always create a multichannel signal of the exact same length and create the exact desired list, be it with repeated values or not.

So, my intuition would ask to just pad values to 0 when you have one side with more than one channel and a signal with even more channels on the other side.

ping @millerpuckette

@Spacechild1
Copy link
Contributor

Spacechild1 commented Jun 21, 2023

Yes, this behavior is intentional.

There is no obvious way how to apply a binary operator to a 5-channel and 3-channel signal. Wrap around, zero-padding, ignoring excess channels, etc. are all equally valid. The nice thing about wrap around is that it doesn't require an exception for single-channel signals.

Side note: some objects have a different behavior. rifft~, for example, just prints an error if the two inlets have different channel counts. (Of course, externals can also choose their own strategies for dealing with channel count mismatches.)

One can always create a multichannel signal of the exact same length and create the exact desired list, be it with repeated values or not.

That's exactly the point. If the user wants a specific behavior, they can implement it. (This will become easier with additional snake~ methods.)

I don't see a need to change the current behavior, but maybe it is not documented sufficiently? ;-)

@porres
Copy link
Contributor Author

porres commented Jun 21, 2023

maybe it is not documented sufficiently?

nope, and before documenting it I wanted to make things sure.

I still think it is counterintuitive, but I guess it's just arbitrary and a byproduct on how to deal with single channels as I pointed.

The thing is that someone was already seeing this as a special feature and hoping for yet more features on how to deal with this based on the way supercollider works, giving it more special commands and arguments on how the wrapping and distribution would work, giving it different breakpoints with interleaving and whatot and, as I see it, this is not "pd-like" or something to expect and yeah, one should always and just build the channel list as desired outside the scope of binary operators.

So I wouldn't like people abusing this, treating it as "feature" and then suggesting "hey, can I also this, and that?"... cause arbitrary as it is, I don't see it as something particularly useful and could make sense in the same scenario as SC as part of a full set of ideas.

zero-padding seems a nice candidate for a single arbitrary decision but yeah, the code would need to be a bit less lazy, haha

@porres porres changed the title [*~] with mc signals of different length, bug or feature? should we zeropad [*~] with mc signals of different length? Jun 21, 2023
@Spacechild1
Copy link
Contributor

giving it more special commands and arguments on how the wrapping and distribution would work

I think all these things can be done with (new) snake~ methods (or externals)! The point is that it should not be the responsibility of the binops themselves. We should really consider the current behavior as a fallback. (But we still need to document it!)

zero-padding seems a nice candidate for a single arbitrary decision but yeah, the code would need to be a bit less lazy, haha

Zero-padding might make sense for +~ and -~, but would it be the appropriate choice for *~ and /~? ;-)

One nice thing about wrapping is that it works equally "well" for all binops.

porres added a commit to porres/pure-data that referenced this issue Jun 22, 2023
@porres
Copy link
Contributor Author

porres commented Jun 22, 2023

I documented the way it is but will keep this open so maybe miller says something.

I gave the example of [*~] so I expected it to be a good candidate for zero padding and don't see an issue in any other case really

@Spacechild1
Copy link
Contributor

Spacechild1 commented Jun 22, 2023

I documented the way it is

Thanks!

I gave the example of [*~] so I expected it to be a good candidate for zero padding

Actually, I know realized that it is not entirely clear what you mean by "zero-padding" in the first place. Are you talking about the inputs - or the result? There is a big difference!

Also, I forgot about another strategy: only use the smaller number of channels and discard the rest.

But again, there is no "natural" way of dealing with mismatching channel counts in binops. It is the responsibility of the user to fill up missing channels (or discard excess channels) in a way that is most appropriate for the task at hand.

Wrap-around is a good default because it is easy to implement and it is the only one that does not require special treatment of single-channel inputs.

@abreubacelar
Copy link
Contributor

abreubacelar commented Jun 22, 2023

Hello! i am the one porres was talking about that thinks this is a "special feature". For me this isn't special, is just normal and a good choice for default, i wouldn't like to waste signal processing creating many empty signals, so i don't think that zero pad makes sense either, but having a choice to create a shorter signal would make more sense than zero pad.

what i said about supercollider, is about the possibility of having adverbs that change the way the channels interact with each other in the binop operation, currently on sc there are 5 ways (1 one them doesn't make sense in pd, but maybe the other 4 could be interesting, but this is all, just interesting). For reference, in supercollider documentation: https://doc.sccode.org/Reference/Adverbs.html

about the "pd way" of doing, i guess i can imagine adding a symbol argument to the binops that could act like the symbol argument for [inlet~] and [outlet~], The situation there is about upsampling and downsampling, and we can choose [inlet~ pad] [inlet~ hold] and [inlet~ lin] to choose how we want the operation on the signal to be performed, and i think binops could do something similar:

so, taking the examples from supercollider (excluding "table" adverb, that makes no sense for pd):

since the current default is wrap, i think we could have
[+~ ] (default) or [+~ wrap] could do: [1, 2, 3, 4, 5] + [10, 100, 1000] = [11, 102, 1003, 14, 105]

the next could be short, which just takes the smallest size of the two signals and discard the rest:
[+~ short] could do: [1, 2, 3, 4, 5] + [10, 100, 1000] = [11, 102, 1003]

as an alternative to wrap, there is also folding:
[+~ fold] could do: [1, 2, 3, 4, 5] + [10, 100, 1000] = [11, 102, 1003, 104, 15]

and an interesting one, that makes more channels, is the cross one:
[+~ cross] could do: [1, 2, 3, 4, 5] + [10, 100, 1000] = [11, 101, 1001, 12, 102, 1002, 13, 103, 1003, 14, 104, 1004, 15, 105, 1005],
and if we swap the two signals,
[10, 100, 1000] + [1, 2, 3, 4, 5] = [11, 12, 13, 14, 15, 101, 102, 103, 104, 105, 1001, 1002, 1003, 1004, 1005]

I guess that the short and the fold could be achieved by creating other snake~ methods too, the cross is a bit more complicated

so, for the short case, considering a signal A bigger in size than a signal B, and if i can constrain the signal A by the size of signal B, which gives me Ac, Ac + B is the same as short(A + B), in pd:

A            B
|            |
[snake~ shrink] (or some better verb)
|        /
|      /
[+~ ]    the wrap won't happen here, since the two signals were constrained by the smallest one in size

for folding, we could split the second signal to remove the last element, than merge with the original one, and then do the binop operation:

EDIT: this is wrong, i would need to split the B signal to remove the tail, and reverse the signal without its tail and then merge the original B with the reversed signal without tail, but i don't have time now to fix the ascii art ...

A  B
|   |
|   [snake~ split -1] (a negative index could split on the last element)
|   |           |
|   |           [snake~ reverse]
|   |           |
|   [snake~ merge]
|   |
|   |
[+~ ] (the default wrap here will give the fold behavior 

the cross one could be done using the default [snake~ out], but i would have to manually connect the number of the signals i want to cross

A   B
|   |
|   [snake~ out 3]
|   |       |     \
|   |       |      \
[*~ ]   [*~ ]   [*~ ] (the left inlets, not shown, would receive all the same A signal)
|       |       |
[snake~ merge]  |
|              /
|             /
[snake~ merge]   (and finally this would contain the cross version)

So, i think that having the possibility of choosing the method directly on the binops is the cleaner solution, but i also understand that extending snake~ methods would make the users have more control on how they want to manage their signals. But in both situations, i think that having the wrap method the default is a very good choice since it can derive the other possibilities and makes sense for the 1 signal case

@umlaeute
Copy link
Contributor

imho, overloading the binops with different strategies: that way lies madness.

since the built-in binops all share the same codebase, it's probably easy enough to extend them to support such a thing as an argument to specify the signal extension strategy.
but of course there are much more binops out there than those built-in...which would lead to inconsistencies and/or duplicate code all over the place.

i'm therefore with @Spacechild1 to

  • either only allow consistent channel-sets (1:m, n:1, n:n)
  • OR pick a somewhat sane default and defer all other cases to specialized [snake~] objects.

the first option is the only well-defined one, albeit it's a bit harsh (and i think @Spacechild1 did not actually propose that).
for the 2nd one, we need a "somewhat sane" default, and I agree with @Spacechild1 that "wrapping the channels" is not so bad (and has advantages on the implementation side)

@Spacechild1
Copy link
Contributor

Spacechild1 commented Jun 22, 2023

I think @abreubacelar has raised interesting use cases and I am also familiar with sclang adverbs. However, all of these can - and should! - be implemented as snake~ methods; it is just a matter of creating the appopriate multi-channel input signals.

To reiterate what @umlaeute has written:

With snake~ methods, every 'adverb' only needs to be implemented once and then it can be used with any multi-channel object.

Otherwise M objects would need to implement N adverbs - in a consistent matter at that!

I think it is pretty obvious which one is the better strategy.

@porres
Copy link
Contributor Author

porres commented Jun 22, 2023

i wouldn't like to waste signal processing creating many empty signals, so i don't think that zero pad makes sense either, but having a choice to create a shorter signal would make more sense than zero pad.

I guess my point is that zero padding seems like a neutral thing, not something people expect as a useful and desired behaviour for implementing stuff, well, actually, I'd expect it as the natural behaviour but I mean not as a desired and useful feature. My point is that there is no sensible desired feature and I'm no t excited about people treating this as a desired feature, it'd be more like "abusing" it, just like people usually abuse other entries and back doors in Pd (like using a numeric float into an object and treat it as an abbreviation of [float]).

Seems like a sane prediction that people would need to give these objects a list of channels of the same length and that the operations would work "one by one". I'd go as far as zeroing out the result not the input for missing channels and it might be a way of telling people they probably missed or made a mistake. It'd also force them to be careful and deal with the channel matching.

Maybe shortening the number of output channels could do the same and might even be 'better', I agree...

but I guess the point here is to not worry about what to do and just have this as a somewhat obscure feature for the sake of code simplicity. I have documented it, but I'm not giving explicit examples showing how great it is encouraging people to use it.

all of these can - and should! - be implemented as snake~ methods

+1 (but I guess I was already clear about it)

I agree with @Spacechild1 that "wrapping the channels" is not so bad (and has advantages on the implementation side)

I can give it in since it's already implemented like this and because of the simplicity implementation advantage that works as desired for single channels

@Spacechild1
Copy link
Contributor

Spacechild1 commented Jun 22, 2023

Just to demonstrate the cross adverb:

  |         |
[snake~ cross]
  |         |
[*~          ]

If the inputs for [snake~ cross] are l1 l2 l3 l4 and r1 r2 r3, the outputs would be l1 l1 l1 l2 l2 l2 l3 l3 l3 l4 l4 l4 and r1 r2 r3 r1 r2 r3 r1 r2 r3. You can then feed these two multi-channel signals into any multi-channel binop.

Just to show that this also works with more "advanced" adverbs.

(Of course, cross doesn't have to be a snake~ method, it could also be an external.)

@jlsiegel07
Copy link

I think something along the lines of "identity padding" would make more sense than zero-padding, when considering alternatives to wrap. As in, pad with whatever values correspond with no change for a given operation. So for addition and subtraction, it would pad with zeroes, while for multiplication and division it would pad with ones -- and so on, for any other operation that this makes sense for.

Also this would be computationally simple because it amounts to passing any excess channels directly through with no change.

@Spacechild1
Copy link
Contributor

Spacechild1 commented Jun 23, 2023

As in, pad with whatever values correspond with no change for a given operation. So for addition and subtraction, it would pad with zeroes, while for multiplication and division it would pad with ones -- and so on, for any other operation that this makes sense for.

You wouldn't have to pad the shorter input, but rather copy the larger input. But yes, this would also be a valid strategy. However, it requires a special case for single-channel inputs.

Again, any of these strategies are equally valid, it really depends on the use case. There is no point in trying to find the "best" behavior because it simply does not exist.

@porres
Copy link
Contributor Author

porres commented Jun 23, 2023

this would be computationally simple because it amounts to passing any excess channels directly through with no change.

I'd rather they'd all become 0 instead.

Again, any of these strategies are equally valid, it really depends on the use case. There is no point in trying to find the "best" behavior because it simply does not exist.

yup, it's pretty clear people have different expectations and ideas, so I'm settling to whatever we have for the sake of "whatever" and laziness to deal with expectations and possibilities. Closing by giving this topic up...

@porres porres closed this as completed Jun 23, 2023
@Spacechild1
Copy link
Contributor

Spacechild1 commented Jun 23, 2023

At least this thread has brought a few more interesting possible snake~ methods :)

@porres
Copy link
Contributor Author

porres commented Jun 23, 2023

I have a spread object in ELSE and I should add mc support to it... it takes whatever input channels and spreads over another number of channels, like getting 8 channels and spreading it over the stereo field. Would be a pretty cool [snake~] method tough

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

5 participants