Back and forth playback of a sample --> avoiding singularity

Hi guys,
let’s say I want a sample to be played normally up to a certain point in time, then I want the same sample to be played in reverse from that very point in time towards the beginning. But I want to have the freedom to stop the reverse playback before it reaches the beginning of the audio file.

This is a way for me to experiment with BufRd using Env as the phase.
So, let’s say I have a piano sample (taken from the Sonatina Synphonic Orchestra)

s.boot;
~mySample = Buffer.readChannel(s,"/my/Sonatina Symphonic Orchestra/Samples/Grand Piano/piano-p-c4.wav", channels:0);

I would like to process this way:

  • play the sample normally for 2 third of the “playback time”;
  • play back it for the remaining 1 third of the “playback time”;

I have a function to calculate these values for me:

(
~func_calculate_levels_and_times = {
	|timeRatio, playback_time, buf|
	var timeA = timeRatio * playback_time;
	var timeB = playback_time - timeA;

	var framesA = buf.numFrames * timeA / buf.duration;
	var framesB = framesA-(buf.numFrames * timeB / buf.duration);

	[[0, framesA, framesB], [timeA, timeB]];
}
)

~func_calculate_levels_and_times.value( 2/3, 4, ~mySample);

So, I’ll use this envelope:

Env.new( [ 0, 117600.0, 58800.0 ], [ 2.6666666666667, 1.3333333333333 ], \lin).plot;

Now I can build my SynthDef:

(
SynthDef(\bufplay_back_and_forth, {
	|out=0, buf, pan=0.0, amp=1.0, framesA=44100, framesB=44100,timeA=1, timeB=1 |
	var env = Env.new( [ 0, framesA, framesB ], [ timeA, timeB ], \lin );
	var sig = BufRd.ar(1, buf, EnvGen.ar( env, 1, doneAction:2), loop:0, interpolation:2);
	sig = sig * amp;
	Out.ar(out, Pan2.ar(sig, pan));
}).add;
)

and eventually play it!

Synth(\bufplay_back_and_forth, [\buf, ~mySample, \framesA, 117600.0, \framesB, 58800.0, \timeA, 2.6666666666667, \timeB, 1.3333333333333 ]);

You can try yourself, changing the playback time or the ratio if you want but, you will hear a kind of glitch in the signal, right at the point where the envelope reaches its highest point (the point where the playback direction reverses).

This, in my case, is undesirable behaviour. I would not like to have any spurious elements during playback.


I realised that this glitch is obviously caused by a ‘singularity’ in the resulting waveform: a point at which the oscillation pattern undergoes an abrupt change of direction.

See here a recording I’ve made of the resulting sound. I have marked the “singularity”.

Reasoning for a moment, I realise that, in order to avoid this glitch, the point of reverse reproduction should take place at a belly point, or in any case at a point where the specularity due to the reversal of the direction of reproduction does not cause any audible artefacts.

I am therefore wondering if there are any ugen who can suggest to me what might be the best time to make this reversal of the direction of reproduction.

Alternatively, I ask you what you think might be the best method to achieve the desired result.

Thank you very much for your support
na

1 Like

I don’t know if there exists some ugen, or even some DSP solution to this… but the naive option would be to calculate the derivative and when it is zero (or close to) only then reverse the phase.

You could probably do this quite nicely with Phasor (or Phaser I forget), Demand with Dser([-1,1]) for forward or backwards, Delay1 and Latch. I don’t have time now, but might do tomorrow.

2 Likes

Slope.ar can find the derivative.

Another approach might be to use 2 separate BufRd.ar’s, each with their own envelope, one for forward playback and one for backwards.
Then you could play them one after another with a bit of overlap time and crossfade them so you would get continuous sound with no glitch.
Of course, you still might be able to hear a change in quality over the crossfade, depending on how different the sounds are.

1 Like

Yes, and thank you so much @jordan ,
I’m super curious about the implementation you have in mind.

Thank you @TXMod ,
I was also thinking about the same “double BufRd w/ crossfade” approach.
I think I will also try this way.

So I just made a Ugen plugin that does this… I thinks it pretty cool, but I’m going to make a new thread!

1 Like

Thank you so much @jordan
I will try it immediately :slight_smile:

Ok guys,
after suggestions by @jordan and @TXMod I’ve tried my best to implement something using what is already directly available within the language.

First, I’ve made some experiment and I came up with the following code snippet.

The basic steps of the algorithm are as follows:

  1. find the peak points (derivative = 0) of the starting signal;
  2. generate a trigger at each of these instants (peak triggers);
  3. among these, safeguard only those peak triggers (saved peak triggers) that are directly subsequent to control triggers coming from the external user;
  4. use these saved peak triggers to alternately select numerical values 1 and -1 (which can then be used with a PlayBuf Ugen to change the playback direction of the audio sample);
({
    var a, b, c, d, e, f, g, scale;
	// quadratic noise - the signal to be back-and-forth played back
    a = LFNoise2.ar(2000);
	// first derivative produces line segments
    b = Slope.ar(a);
	// "peak triggers" - peak related impulse train
	// note that I'm using both the original and the inverse waveform
	// in order to get both the peaks above and below the 0 reference.
	c = Trig1.ar(b,0.0) + Trig1.ar(b*(-1.0), 0.0);
	// "control trigger" simulation (simulating two external triggers here
	// one after 3ms and the other after 6ms from the beginning of file)
	d = TDelay.ar(Impulse.ar(0), 0.003) + TDelay.ar(Impulse.ar(0), 0.006);
	// A way to allow only the very first "peak trigger"
	// to pass immediately after the external trigger
	e = SetResetFF.ar(d, c);
	f = Delay1.ar(e)*c;
	// Using a demand Ugen in order to sequentially output 1 and -1
	// to be later used with a PlayBuffer to change the playback direction.
	// Changes between 1 and -1 are caused by the "saved peak triggers".
	// Note: I'm using an initial Impulse just to have a "1" output from the beginning
	g = Demand.ar(Impulse.ar(0)+f, 0, Dser([1,-1], inf));

	scale = 0.0002; // needed to scale back to +/- 1.0
    [a, b*scale, c, d, e, f, g]
}.plot;
)

Here’s the commented graph

Eventually I have placed it inside a SynthDef.

This caused me some headaches trying to figure out how to solve the inherent dependency on a feedback loop and, consequently, how to solve the latency between the analysed signal and the output signal to the listener (I’m not even sure if I solved it in the best way, let me know what you think).

(
SynthDef(\bufplay_back_and_forth, {
	|out=0, buf, pan=0.0, amp=1.0, rate=1.0, t_trig=0  |
	var a, b,c,d,e,f,g, sig;

	// The signal to be back-and-forth played back
	a = PlayBuf.ar(1, buf, rate* LocalIn.ar(1), 1, loop:1);

    b = Slope.ar(a);
	c = Trig1.ar(b,0.0) + Trig1.ar(b*(-1.0), 0.0);
	// external control triggers
	d = T2A.ar(t_trig);
	e = SetResetFF.ar(d, c);
	f = Delay1.ar(e)*c;
	g = Demand.ar(Impulse.ar(0)+f, 0, Dser([1,-1], inf)).poll;

	LocalOut.ar(g);

	a = a * amp;

	// Uncomment the line below if you want to listen to the final sound effect
	Out.ar(out, Pan2.ar(a, pan));

	// Ucomment the line below if you want to record a synchronized version
	// of played back sample and the "saved peak triggers"
	//Out.ar(out, [a, DelayC.ar(f, 0.25, ControlRate.ir.reciprocal)]);
}).add;
)

// load a sample
~mySample = Buffer.readChannel(s,"/home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Grand Piano/piano-p-c4.wav", channels:0);

// instantiate the synth
x = Synth(\bufplay_back_and_forth, [\buf, ~mySample ]);
// when you are ready, feel free to send the synth some trigger to reverse the playback direction
x.set(\t_trig,1);

// when you are done, free the synth
x.free;

Now a few comments on the final result: audibly, although the result is much improved from my first attempt (you don’t hear the discontinuities as much anymore) there is still something that doesn’t quite add up.

Now, we can also record with reaper the audio output of the synth where on the left we have the sample (a) and on the right the saved peak triggers (f) (making the necessary corrections to align the two outputs a and f)

Examining the waveform we see how the pulses of f actually correspond to the instants in which the sample playback changes direction (pay attention to the waveform symmetry)

overall_Istantanea_2023-01-03_23-19-41

some of these points seems to work quite well

but other still maintain their unwanted “singularity” characteristic

Do you have any suggestions on how can I improve this mechanism?
Thank you so much for your patience and support

Hi,

playing a buffer back and forth is a synthesis method I’ve used quite often. I’m refering to it as buffer modulation or buffer scratching.

Concerning the second derivative idea, miSCellaneous_lib contains the drate ugen Dwalk which was thought for that. You can use it in combination with the waveset classes ZeroXBufRd / ZeroXBufWr, see Ex. 10 of ZeroXBufRd’s help file: ‘Smooth concatenation of adjacent segments restricted by turning points resp. local minima or maxima’

I think it’s closely related to what you described in your recent post.

That’s the technical side so far, but I rather thought about a synthesis with many zig-zags of that kind. If you only want few “smooth turnarounds” I suppose that a better strategy would be crossfading the two playbacks. Because even a smooth turning point doesn’t exclude an audible hickup (depending a lot on the characteristics of the source).

The crossfade issue comes up quite often, you could either try a dedicated solution yourself, e.g. with SelectX and two BufRds / PlayBufs or check Wouter Snoei’s PlayBufCF in wslib. You could also search the forum for PlayBuf and crossfade or so, I think there must be some threads.

Hope that helps, best

Daniel

1 Like

I could not get a sc version to work either. I don’t think its possible as you really need sample rate accuracy.

Did you get the server plugin to work, Releases · JordanHendersonMusic/smoothreversal · GitHub ?

Here is some screenshots in the style you posted - with the mark placed at the reversal point:


Although this one looks like a peak, if you zoom in its actually flat.



Thank you @jordan I confess that I had done some preliminary testing (I had opened an issue on your repo a few days ago, thank you for your reply there) but not with due care.

I was able to test it again and make a few remarks, which I reproduce below:

Firstly, but this could be a misunderstanding on my part as to how SmootReversal works, it seems to me that, as soon as it is instantiated, a synth that makes use of the Ugen, starts playing the sample from the end, already in ‘reverse’ mode (with an artifact at the beginning of the reproduction).

Secondly, although, as you say, the transition points are smooth, I still perceive discontinuities in the listening (marked in red in the image above).

Here is the code for my test Synth.

(
SynthDef(\smoothR, {
	|out=0, buf, pan, amp=1.0, t_trig=1|
	var trigger = K2A.ar(t_trig);
	var sig = SmoothReversal.ar(buf, BufRateScale.kr(buf), trigger, threshold: -15.dbamp);
	sig = sig * amp;
	Out.ar(out, Pan2.ar(sig, pan));
}).add;
)

I then use this set instruction to launch the one-sample trigger.

~mySample = Buffer.readChannel(s,"/home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Grand Piano/piano-p-c4.wav", channels:0);

x = Synth(\smoothR, [\buf, ~mySample]);
x.set(\t_trig, 1);

A couple of pictures of the “singularities”.

I am becoming increasingly convinced that the approach of looking for the zero of the first derivative is not the best way to get what I need. As @dkmayer says, an improvement could be achieved by ensuring that the first derivative is 0, but with the additional restriction that this point should also be located at a local maximum or minimum.

Does it make any sense to you?
Am I doing something wrong in using your Ugen jordan?
Thank you so much

I think that’s because your ugen starts the the trigger at 1?

Does reducing the threshold help at all try -40.dbamp? I’ll be honest, I wrote it mostly to learn how to deal with buffers in the backend, and spent more time trying to make a nice interface for that, as opposed to writing tests. And whilst the changes are audible, the weren’t as obvious as the audio you have. I’ll write some more tests to check - but tomorrow.

1 Like

I messed around tonight and came up with this, works pretty well on a guitar signal. The crossfading between the two signal could be improved.

b = Buffer.read(s, ...load a stereo buffer...);

(
SynthDef(\samp, {
	var buf = \buf.kr();
	var rate = \rate.kr(1);
	var trigForw = Trig.kr(rate > 0);
	var trigBack = Trig.kr(rate < 0);
	var tracker = Phasor.ar(0, rate, 0, BufFrames.kr(buf));
	var phaseForw = Phasor.ar(trigForw, 1, 0, BufFrames.kr(buf), tracker);
	var phaseBack = Phasor.ar(trigBack, -1, 0, BufFrames.kr(buf), tracker);
	
	var back = BufRd.ar(2, buf, phaseBack);
	var forw = BufRd.ar(2, buf, phaseForw);
	
	Out.ar(\bus.kr(0), XFade2.ar(back, forw, rate.lag(\lt.kr(0.2))))
}).add
)

(
x = Synth(\samp, [buf:b]);
i = 1; // 1 = normal, -1 = reverse
)

// changes between normal and reverse
(
i = i * -1;
x.set(\mix, 1, \rate, i);
)
1 Like

Okay, I’ve figured out the problem. First I have updated it a few times, I don’t know if you are using the newest. Again, it doesn’t matter whether you use it or not, this was mostly a chance to learn how to use buffers in the C++ interface.

First another example that works,

(
s.waitForBoot {
    ~buffer = ~buffer ?? {Buffer.read(s, "/home/jordan/Audio/AlanEvansTrio_ImComingHome_Full/AE3_ImComingHome-Full Session/VOX - M80-SH.wav")};
    s.sync;

    try {x.free}{};

    x = {
		var t =  Dust.ar(MouseX.kr(0, 10)) > 0.01;
		SmoothReversal.ar(~buffer, 1, t, -45.dbamp)
    }.play;
}
)

I think this demonstrates the issue with using the derivative like this, the voice is pretty good, but the drums sound bizzare if you catch it in the middle of a hit.

The problem is this line

K2A.ar(\trig.tr);

produces this…
230107
…rather than a single sample impulse.
The Ugen will switch direction when ever the trigger is above 0.5. This lets you reverse at sample rate. So the clicks you are getting is it getting stuck over a few samples.

I actually don’t know how to fix this in sc. Seems like the probably should be fixed in sc rather than in the C++.

There could be another source of clicks though, the buffer plays back with cubic interpolation, but the derivative is only checked linearly, but I imagine this would be quite a small difference (at least while the playback speed is >=1 ).

Thank you so much @jordan for the time you spent on this.

I see, listening to your audio there is still something strange but the overall effect is very impressive (and also very interesting creatively speaking when especially a voice is processed).

You are right about “that line”, this was actually my fault. I converted the trigger to audio rate incorrectly (I should not have used K2A but T2A). Now I have a correct impulse trigger with a single sample duration.

This is the fixed code

(
SynthDef(\smoothR, {
	|out=0, buf, pan, amp=1.0, t_trig=1|
	var trigger = T2A.ar(t_trig);
	var sig = SmoothReversal.ar(buf, BufRateScale.kr(buf)*(-1), trigger, threshold: -40.dbamp);
	sig = sig * amp;
	Out.ar(out, [sig, trigger]);
}).add;
)

~mySample = Buffer.readChannel(s,"/home/nicola/Musica/samples/Sonatina Symphonic Orchestra/Samples/Grand Piano/piano-p-c4.wav", channels:0);


x = Synth(\smoothR, [\buf, ~mySample]);
x.set(\t_trig, 1);
x.free;

I made some more test decreasing the threshold as you said and also applying a workaround for the samples starting directly in reverse (which, I now, is actually caused by the fact that a trigger is immediately fired just after the synth instantiation, I think there would be more elegant methods to get the same result).

There are still these “glitches” in the sound sample from this very last test I made.

Maybe I should try another way :frowning:

I still wanted to thank you for this super stimulating discussion and also congratulate you on your plugin. I confess that I have never developed anything on the backend side and would be really curious about it.

You could just fake it with grains…

(s.waitForBoot {
	~b = ~b ?? {Buffer.readChannel(...)};
	s.sync;
	try {x.free}{};
	x = {
		var dir = Demand.kr(\trig.tr(1), 0, Dseq([1, -1], inf));
		var phase = Phasor.ar(0, BufRateScale.kr(~b) * dir, BufFrames.kr(~b) / 2, BufFrames.kr(~b));
		var freq = 32;
		GrainBuf.ar(
			1, 
			trigger: Impulse.ar(freq), 
			dur: freq.reciprocal * 4, 
			sndbuf: ~b, 
			rate: dir, 
			pos: phase / BufFrames.kr(~b) + WhiteNoise.ar(0.00001), 
			interp: 4
		)

	}.play;
})

x.set(\trig, 1);

Also, is there any change you could send me the audio file you are using in a message? I still can’t reproduce the error your getting.

1 Like