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

A new buffer player #2600

Open
nhthn opened this issue Dec 29, 2016 · 38 comments
Open

A new buffer player #2600

nhthn opened this issue Dec 29, 2016 · 38 comments
Assignees
Milestone

Comments

@nhthn
Copy link
Contributor

@nhthn nhthn commented Dec 29, 2016

To summarize the discussion at #2596, PlayBuf is missing two coveted features: the ability to set the end position of a loop, and crossfading. BufRd can improve on this, but it has insurmountable precision issues with large buffers. It wouldn't be acceptable to add new arguments to PlayBuf because it's too drastic of an API change for one of SuperCollider's most popular UGens. Promoting LoopBuf from sc3-plugins to core is a possible solution, but it doesn't support crossfading.

The best solution is to introduce a new UGen that overcomes these issues. I suggest that it be called Play or Player.

I have another idea to throw into the mix: can we consider getting rid of * BufRateScale.kr(bufnum)?

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 29, 2016

Thank you for the summary, it makes all sense to me. I argued before against making UGens more complex than necessary, and I have been able to implement a cross-fading buffer playback before by combining UGens, but it was indeed very involved, and so I think adding the cross-fade capability here would make sense. Also the multiplication with BufRateScale.

In my opinion, the doneAction argument could be dropped. Nowadays the UGens set a done flag that can be easily fed into Done. Or is it a problem because the player might have multi-channel output?

Another question would be frame precision again. We want to ensure that start and stop positions even for large buffers are detected precisely. I think this is no problem with up to around 2^24 frames given as a 32-bit float?

Name-wise, I think Play and Player are too generic, it would be good if the buffer or sample aspect of the UGen was preserved in the name? That's why LoopBuf was good, but if there is already a third-party UGen with that name, we shouldn't take it. Is PlayBuf2 too silly? Or, if we think of the newly added cross-fading, perhaps XPlayBuf or something along this line?

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 29, 2016

Thinking more broadly about it, would there be value in supporting multi-channel buffers? Currently, one would have to read each channel into a separate buffer using b_readChannel. I'm on the fence here, whether there is benefit in adding a numChannels argument.

@telephon
Copy link
Member

@telephon telephon commented Dec 29, 2016

Would it make sense to just add interpolation to LoopBuf?

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 29, 2016

LoopBuf would also need an isLooping argument.

@telephon
Copy link
Member

@telephon telephon commented Dec 29, 2016

Wouldn't be a problem to add this. The gate parameter may need a discussion:

positive gate starts playback from startPos. negative gate plays rest of sample from current position

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

First pass at an argument order:

numChannels, bufnum = 0, rate = 1, startPos = 0, startLoop = 0, endLoop = -1, isLooping = 0, fadeTime = 1e-3

If endLoop is -1, then the loop ends at the last frame. If isLooping is 0 and the end of the buffer is reached, the UGen sets the done flag.

How does the crossfade affect timing? If my loop is from 0 seconds to 2 seconds and my crossfade lasts 0.1 seconds, is the period of the resulting audio 2 seconds or 1.9 seconds? If the former, is it acceptable for the crossfade to be cut off if the entire buffer is less than 2.1 seconds long?

startPos, startLoop, and endLoop are actually floats, so they still suffer from precision issues for long buffers. How do we deal with this?

Would it be wise to have an argument controlling the type of interpolation, like LoopBuf?

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

@snappizz I'd say we absolutely want the loop to be 2 seconds in your example. I imagine the "crossfade" to really be making sure there isn't a big discontinuity between the last and the first sample.

Multiplying by the equivalent of Env([0,1,0],[fadeTime / 2, sampleLength]) is what I envisioned.

@telephon
Copy link
Member

@telephon telephon commented Dec 30, 2016

startPos, startLoop, and endLoop are actually floats, so they still suffer from precision issues for long buffers. How do we deal with this?

As you certainly want to be able to modulate them, I suppose there is no other way than to accept a loss of precision there.

Maybe it is better to specify loopStart and loopLength, so that at least you can play precisely a certain short length at a time point further down in the buffer?

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

If isLooping is 0 and the end of the buffer is reached, the UGen sets the done flag.

Hmmm, but then you disallow a one-shot playback with given start and stop time. Wouldn't it be better to specify: If isLooping is 0 and the endLoop position of the buffer is reached, the UGen sets the done flag. Then if you keep the default of endLoop = -1, you can still easily play to the end of the buffer.

Maybe it is better to specify loopStart and loopLength, so that at least you can play precisely a certain short length at a time point further down in the buffer?

I don't think you win any precision with this. Remember that for roughly the 2^24 range, you can give precise frame numbers encoded in a 32-bit float. Also this is easier to understand in terms of looping: You have precise start and stop points, but the loop-length would be in fact absdif(endLoop, startLoop) - fadeTime*BufSampleRate).

That last calculation shows that we might think carefully about whether we want to give positions and durations in seconds or sample frames, and furthermore, how these interact with the playback speed. PlayBuf has chosen to require the offset in sample frames, BufRd likewise uses frame position. So I would recommend to specify all values in frames, and refer to times before applying playback speed. So without cross-fade, the actually heard loop duration in seconds will be (loopEnd - loopStart) / (SampleRate.ir * rate). This is assuming we apply the BufRateScale mechanism (if not, one would replace SampleRate by BufSampleRate in this formula).

I also would like to suggest that loopEnd < loopStart is acceptable and plays back the buffer in reverse.

So from the above, I would suggest that fadeTime is actually fadeLen and given in sample frames instead of seconds, simply because it will then be consistent with the other args, and you can precisely calculate the actual loop duration in frames without having to forward and backward between frames and seconds. WDYT?

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

Further thoughts on the cross-fade... There are actually quite a few more parameters that determine a cross-fade than just the length:

  • do we fade at the beginning or the end of the buffer / buffer span? I think intuitively we assumed that the fade will happen at the end, so that when loopEnd is reached, the buffer frame at loopEnd is heard with zero amplitude and the buffer frame at loopStart is heard at unity amplitude. One could also fade in the beginning, but this would require an additional parameter, so perhaps it is not worth it.
  • as we all know, specifying a fade envelope is a tricky business and depends on the scenario and material. In many cases we want equal-power (sqrt), but in certain cases where phases are aligned we want equal-energy (linear). Furthermore, a fade could be "fast" or "slow", or easy-in-easy-out, etc. Would it make sense to give the user more precise control by using the well-known curvature codes from Env? The disadvantage would be that we need two more parameters (a floor value for exponential curvature), or we simply hard-code the floor value used in exp fade.

I'm still on the fence about fadeTime vs fadeLen. Contrary to my previous comment, somehow my intuition says the user most likely wants to give the time in seconds, and we can introduce the burden to divide by sample-rate if the user has fade-time in frames? But then, I would assume the time is given as "user time", that is what is actually heard and not affected by playback rate. That is, fadeLen = fadeTime * rate * BufSampleRate / SampleRate.


Here's another attempt:

numChannels, bufnum = 0, rate = 1, startPos = 0, startLoop = 0, endPos = -1, endLoop = -1,
  isLooping = 0, fadeTime = 1e-3, fadeCurve = \lin
  • numChannels: the number of channels of the buffer provided
  • bufnum: the index of the buffer to play
  • rate: the playback rate. At rate = 1 (default), the buffer is played back at original speed, automatically applying a BufRateScale translation between buffer sample rate and server sample rate, if they differ. At rate = 0.5, the buffer is played back at half speed, at rate = 2.0, the buffer is played back at double speed. This can be modulated.
  • startPos: sample frame position (inclusive) in the buffer where playback starts.
  • startLoop: sample frame position (inclusive) in the buffer where playback resumes in loop. This can be modulated.
  • endPos: sample frame position (exclusive) in the buffer where where playback ends. When < 0 (default), the length of the buffer is used instead. This can be modulated.
  • endLoop: sample frame position (exclusive) in the buffer where where looping rewinds. When < 0 (default), the length of the buffer is used instead. This can be modulated.
  • isLooping: a flag that indicates, when > 0, that the playback should loop. This can be modulated.
  • fadeTime: a cross-fade duration in seconds. This is the actual duration of the cross-fade and independent of the playback rate. The cross-fade happens at the end of the loop span, i.e. fadeTime seconds before endLoop is reached, the previous playback begins to fade out, and a new playback starting at startLoop begins to fade in. When endLoop is reached, the previous playback has completely faded out, and the new playback has completely faded in. A value of zero indicates no cross-fade. This can be modulated.
  • fadeCurve: the type of curvature used for the cross-fade. This is the same as the curve parameter used in an Env. The cross-fade can be visualized as Env([[0, 1], [1, 0]], [fadeTime, fadeTime], fadeCurve).plot, e.g. Env([[0, 1], [1, 0]], [1, 1], \lin).plot for a linear (equal energy) cross-fade or Env([[0, 1], [1, 0]], [1, 1], \welch).plot for a square-root (equal power) cross-fade. Note that for exponential fades, where fadeCurve = \exp, a floor value of -60.dbamp is used instead of zero. This can be modulated.

(should we really give a default for bufnum?)

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

So exponential, Env([[-60.dbamp, 1], [1, -60.dbamp]], [1, 1], \exp).plot, hardly makes sense. Of course one could give two fade-curve parameters, one for fade-in, the other for fade-out. That way one could use the numeric "power" curves very flexibly, but it adds perhaps unnecessary complexity where in almost all cases you will be fine with \lin and \welch.

As you may have noted, I have added an endPos parameter in the last example. It was only now that I understood why @snappizz suggested that

If isLooping is 0 and the end of the buffer is reached, the UGen sets the done flag.

This is to allow a "release" phase. With an explicit endPos this then becomes

If isLooping is 0 and the endPos in the buffer is reached, the UGen sets the done flag.

We should still contemplate whether there is a problem doing

x = XFadePlayBuf.ar(numChannels: 2, ...);
FreeSelfWhenDone.kr(x);

i.e. when we actually get two instances of FreeSelfWhenDone because of multi-channel-expansion. Personally, I think this is no problem, just a bit inelegant. One could still write

x = XFadePlayBuf.ar(numChannels: 2, ...);
FreeSelfWhenDone.kr(x[0]);

But that's quite ugly, too.

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

I've started a wiki page here to compile the current state of the proposal: https://github.com/supercollider/supercollider/wiki/PlayBuf-2017

I'd definitely advise that we use stringent unit tests when developing this UGen!

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

@snappizz thanks; although I think for discussion it's better to stay here in "issues", the wiki is better for compiling the "snapshots" of the discussion.

I just wanted to explode the solution space even more with a slightly different proposal - shrink the functionality of the UGen by delegating the buffer playback again to another existing UGen such as BufRd. That is, define a new UGen XFadeLoopIndex (or whatever good name we can find) that has four outputs: [frame-1, level-1, frame-2, level-2]. Then the user can choose a very simple or a very complex actual cross-fade using the levels, and the buffer playback is just throwing the frame positions at two BufRd instances. And before someone protests that this makes life difficult for the avg user - we can then add a pseudo-UGen on top that encapsulates the three functions (frame/level generation, buffer reading, x-fading).

I sometimes wish this had been done for other UGens such as Limiter or Compander as well, so that you can actually get hold of the level balancing signal which is now hidden in a black box.

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

This hypothetical UGen XFadeLoopIndex would have the following arguments:

rate = 1, startPos = 0, startLoop = 0, endPos, endLoop, isLooping = 0, fadeTime = 1e-3

Obviously we cannot use a shortcut of endPos/endLoop = -1 unless we also specify the buffer here. So alternatively:

bufnum, rate = 1, startPos = 0, startLoop = 0, endPos = -1, endLoop = -1, isLooping = 0,
  fadeTime = 1e-3

Usage example:

var fr1, lvl1, fr2, lvl2, rd1, rd2, sig;
#fr1, lvl1, fr2, lvl2 = XFadeLoopIndex.ar(bufnum, startLoop = 44100, endLoop = 88200);
rd1 = BufRd.ar(numChannels, bufnum, fr1);
rd2 = BufRd.ar(numChannels, bufnum, fr2);
sig = (rd1 * lvl1.sqrt) + (rd2 * lvl2.sqrt)
@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

But then we'll encounter the same issues as BufRd -- floats will lose precision for large buffers.

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

I do agree that we should aim for transparency and grant the user access to the read pointer. One idea is to introduce a new UGen, PlayBufXPos, that outputs only the playback heads and amplitudes, but it's more for querying the state of PlayBufX rather than feeding into the actual playback UGen.

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

Ah I forgot about the float noise again. How would PlayBufXPos be different from XFadeLoopIndex in that respect? Or does it output both signal and position?

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

I just edited to clarify -- what I mean is that it runs in parallel with PlayBufX but it doesn't actually control any buffer playback.

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

I also would like to suggest that loopEnd < loopStart is acceptable and plays back the buffer in reverse.

But what happens when your playback rate is positive and your loopEnd < loopStart?

Or, if your loopEnd < loopStart and your playback rate is negative, does it play forward?

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

Maybe it is better to specify loopStart and loopLength, so that at least you can play precisely a certain short length at a time point further down in the buffer?

I don't think you win any precision with this. Remember that for roughly the 2^24 range, you can give precise frame numbers encoded in a 32-bit float. Also this is easier to understand in terms of looping: You have precise start and stop points, but the loop-length would be in fact absdif(endLoop, startLoop) - fadeTime*BufSampleRate).

@Sciss I don't understand what you mean here. I think the proposal is we specify a start time in either seconds or # of frames, and specify duration in number of frames.

The problem with specifying an end time instead of a duration is this:
Imagine you want to loop 2 different 1-second samples simultaneously. One of the samples has a starting point that's many minutes into a long Buffer. The other is at the beginning of a buffer. Because of float encoding, your sample lengths (computed from endSample) will be different and the loops will slowly go out of phase.

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

@vivid-synth

But what happens when your playback rate is positive and your loopEnd < loopStart?

Or, if your loopEnd < loopStart and your playback rate is negative, does it play forward?

Good questions. I think we could treat the entire buffer as a circle with the end joined at the beginning. This gives us the intuition necessary to tackle those cases.

Say endLoop < startLoop = startPos, rate > 0, and isLooping = 1 (so endPos has no effect). Then the pointer starts at startLoop, wraps around the end of the buffer and back to the beginning, reaches endLoop, and then jumps "forward" to startLoop.

Say endLoop > startLoop = startPos. rate < 0, and isLooping = 1. Then the pointer starts at startLoop, moves backwards and wraps around the beginning of the buffer and back to the beginning, reaches endLoop, and then jumps "backward" to startLoop.

This also addresses my earlier question of "what happens if the loop region is 0 to 2 seconds and the crossfade is 0.1 seconds, but the buffer is 2.05 seconds long?" On the second half of the crossfade, you play 0 to 0.05.

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

Wait, no. I got the second case wrong. That should be

Say endLoop > startLoop = startPos. rate < 0, and isLooping = 1. Then the pointer starts at startLoop, tries to go backwards but ends up jumping to endLoop, and then plays backwards back to startLoop.

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

So from the above, I would suggest that fadeTime is actually fadeLen and given in sample frames instead of seconds

@Sciss I could go either way. FadeTime in other UGens is in seconds.

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

@vivid-synth thanks for the clarification with the two 1-seconds loops; seems reasonable to prefer duration then. I'm still wondering, if you have such long buffers, can't you split your material into several buffers, each of which is in the "safe" range for float positions?

Sorry about the confusion with endLoop < startLoop. Of course they should always be startLoop < endLoop. just the implementation should support running from endPos instead of startPos when rate is negative at init-time, and then during run-time, it should adapt to the signum of rate and wrap at startLoop or endLoop as appropriate.

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

do we fade at the beginning or the end of the buffer / buffer span?

I'd want to fade both. If the first frame is (-)1, I as a user don't want it to start with a clipping sound. If this is controversial, we could have an optional startFade argument that defaults to e.g. -1, meaning "take the value of fade"

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

"isLooping" should probably just be "loop", for consistency with PlayBuf

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

Is there any potential for problems with crossfade (e.g. with exponents) if the duration is changed to a larger or smaller value in the middle of fading out?

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

I'd definitely advise that we use stringent unit tests when developing this UGen!

One option would be to use this as the first test case of sc3-plugins -> SuperCollider. I.e. it's considered unstable at first, so we put it in sc3-plugins, then when we're more confident we can migrate it over?

@Sciss
Copy link
Member

@Sciss Sciss commented Dec 30, 2016

I would latch any fade related argument during the actual fade. It doesn't make sense to change the fade-duration during an ongoing fade.

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Dec 30, 2016

@vivid-synth Sure, I was thinking that as well. I think our schedule would be to implement in plugins by 3.9 with a warning that the API is in flux, and then aim to make it stable enough for migration for 3.10. We can keep discussion here, however.

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

@Sciss what I meant was, if we change the sample playback duration in the middle of a fade. Although the question of fade duration is also interesting.

@telephon
Copy link
Member

@telephon telephon commented Dec 30, 2016

Although you want to continuously vary rate, but that's probably the only one.

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Dec 30, 2016

One downside of the idea of putting it in sc3-plugins is we won't have access to the various existing playbuf c++ macros.

In addition to tests, may I suggest we document our code? It can be very hard to read UGen code that has no explanatory comments at all.

@vivid-synth vivid-synth modified the milestones: 3.9, future Dec 30, 2016
@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Jan 2, 2017

It seems that we've worked out most of the details here, and the next step is to draft up an implementation.

@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Jan 2, 2017

I'll volunteer to help work on this, maybe others who would be willing to help also can comment/ 👍 here?

@nhthn
Copy link
Contributor Author

@nhthn nhthn commented Jan 2, 2017

I won't chastise you if you want to get started on this, but I consider it pretty low-priority. Considering some of the major open projects in 3.9 (unit tests, ctor bugs in sc3-plugins, qtwebengine), I don't think it's the best thing to work on now.

@vivid-synth vivid-synth modified the milestones: future, 3.9 Feb 25, 2017
@vivid-synth
Copy link
Member

@vivid-synth vivid-synth commented Feb 25, 2017

Moving this out of 3.9

@elgiano
Copy link
Contributor

@elgiano elgiano commented Jul 6, 2020

I'm writing an implementation here: https://github.com/elgiano/xplaybuf

I was heavily inspired by this discussion and the work with Eric Sluyter on super-bufrd. However, my implementation doesn't involve passing double precision position values across UGens.

Features:

  • have all time-related parameters passed in seconds
  • startPos and loopDur instead of endPos
  • rate is adjusted by bufratescale
  • fade to silence on loop (or buf) boundaries and crossfade on triggered cue
  • can loop through the buffer's end (a loop can be across loop boundaries)
  • no doneAction

If anyone is interested, I appreciate discussion, comments, issues, and contributions :)

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

Successfully merging a pull request may close this issue.

None yet
5 participants