Stopping a Web Audio oscillator at cycle completion (or zero-crossing)

When calling osc.stop(), a running oscillator is stopped immediately, which can result in a nasty click. This is because the oscillator is stopped right in the middle of a cycle.

Here’s a nice tutorial on how to get rid of the click, using automation curves for smooth gain fade-outs: http://alemangui.github.io/blog//2015/12/26/ramp-to-value.html

But there’s another way: When stopping the oscillator, we could just wait for cycle completion before actually stopping it.

Calculating the moment of next cycle completion

Let’s say, we have our sine oscillator running at a known frequency. startTime is also a known value:

var osc = context.createOscillator();
osc.type = "sine";
osc.frequency.value = freq;
osc.connect(context.destination);
osc.start(startTime);

Some time later, we want to stop it, though not immediately, but right at its next cycle completion:

osc.stop(timeAtNextCC);

But how to calculate timeAtNextCC (using context.currentTime and freq)?

First of all, let’s calculate the duration of one cycle:

var cycleDuration = 1 / freq;

Now, we can calculate how many cycles have already been completed since startTime:

var runningTime = context.currentTime - startTime;
var completedCycles = Math.floor(runningTime / cycleDuration);

From this, we know at what point in time the last cycle completion took place:

var timeOfLastCC = startTime + (cycleDuration * completedCycles);

Now, we can just add the duration of one cycle and we know when the next cycle will be completed:

var timeOfNextCC = timeOfLastCC + cycleDuration;

Now we can stop the oscillator using that value:

osc.stop(timeOfNextCC);

Complete code

var osc = context.createOscillator();
osc.type = "sine";
osc.frequency.value = freq;
osc.connect(context.destination);
osc.start(startTime);

var cycleDuration = 1 / freq;
var runningTime = context.currentTime - startTime;
var completedCycles = Math.floor(runningTime / cycleDuration);
var timeOfLastCC = startTime + (cycleDuration * completedCycles);
var timeOfNextCC = timeOfLastCC + cycleDuration;
osc.stop(timeOfNextCC);

Scheduled stopping

If we want to schedule the stop event right after calling osc.start(), we do not have to use context.currentTime, because we already know how long the oscillator will run (it is determined by us):

osc.start(startTime);

var stopTime = startTime + toneDuration;
var cycleDuration = 1 / freq;
var completedCycles = Math.floor(toneDuration / cycleDuration);
var timeOfLastCC = startTime + (cycleDuration * completedCycles);
var timeOfNextCC = timeOfLastCC + cycleDuration;

osc.stop(timeOfNextCC);

Stop at zero-crossing

If we want to stop at the next zero-crossing instead, we just have to do the maths with half-cycles instead. The code looks like this:

var halfCycleDuration = 0.5 / freq;
var runningTime = context.currentTime - startTime;
var completedHalfCycles = Math.floor(runningTime / halfCycleDuration);
var timeOfLastZC = startTime + (halfCycleDuration * completedHalfCycles);
var timeOfNextZC = timeOfLastZC + halfCycleDuration;

osc.stop(timeOfNextZC);

Be aware that this zero-crossing method only works with waveforms that actually cross zero after half a cycle. This is the case with the default waveforms like sine, triangle, square, but not necessarily with custom waveforms.

Leave a Reply

Your email address will not be published. Required fields are marked *