Tone.js icon indicating copy to clipboard operation
Tone.js copied to clipboard

Feature Request: audio rate sample-and-hold

Open samdafi opened this issue 5 years ago • 4 comments

A sample-and-hold locked to an audio rate signal (switching on 0/nonzero, but not necessarily locked to the transport) would be a very useful feature for LFOs.

I came up with this (which technically is double the rate of the LFO but easy enough to halve the signal coming in). There are two problems with my approach: there's still occasional noise at the switchover, and it's not technically legal, because Noise() abstracts the audio buffer playbackRate from a signal to a positive and its _source is private. I'm open to other suggestions or paradigms, thanks for the great library!

// gates that return opposite values (0, 1) on positive part of LFO
const one = new Tone.Signal(1);
const gate  = new Tone.GreaterThanZero();
const inverseGate = new Tone.Subtract();
one.connect(inverseGate);
gate.connect(inverseGate.subtrahend);

// connect LFO to gate
const osc = new Tone.Oscillator(1).start();
osc.connect(gate);

// Noise signal runs muted on positive and holds on negative, illegally connected
// to _source.playbackRate. Gain node because the incoming signal is in gain, not
// dB, so can't be connected to noisePos.volume.
const noisePos = new Tone.Noise().start();
const noisePosGain = new Tone.Gain();
noisePos.connect(noisePosGain);
gate.connect(noisePos._source.playbackRate);
inverseGate.connect(noisePosGain.gain);

// Opposite signal holds on positive and runs muted on negative, same connections.
const noiseNeg = new Tone.Noise().start();
const noiseNegGain = new Tone.Gain();
noiseNeg.connect(noiseNegGain);
inverseGate.connect(noiseNeg._source.playbackRate);
gate.connect(noiseNegGain.gain);

// Sum them
const output = new Tone.Add();
noisePosGain.connect(output);
noiseNegGain.connect(output.addend);

// Check it out
const meter = new Tone.DCMeter();
output.connect(meter);
setInterval(() => console.log(meter.getValue()), 10);

samdafi avatar Oct 19 '20 19:10 samdafi

It just occurred to me that a single impulse in an oscillator tied to the playback rate is a lot simpler 😬. Well, sometimes the brain goes crazy:

class SampleAndHold {
    private _frequency: number;
    private _pulse: Tone.PulseOscillator;
    private _noise: Tone.Noise;
    output : Tone.Gain;

    constructor(frequency? : number){      
        this._pulse = new Tone.PulseOscillator(0, -0.999).start();
        this._noise = new Tone.Noise().start();
        this.output = new Tone.Gain();

        //@ts-ignore -- private access complaint
        this._pulse.connect(this._noise._source.playbackRate);
        this._noise.connect(this.output);

        this.frequency = frequency || 1
    }
    set frequency(f : number) {
        this._pulse.width.value = Math.max(-1 + f/100, -0.9);
        this._pulse.frequency.value = f;
        this._frequency = f;
    }
}

samdafi avatar Oct 20 '20 07:10 samdafi

Thanks for the reference implementation. I think a Sample-And-Hold component would be really cool. The big issue blocking your implementation for the time being is that it still doesn't seem like it'd work on Safari since in Safari you can't connect a signal to playbackRate to control that AudioParam. See this issue for more context.

I think given that, the best alternative would be to implement it with either a Tone.Clock which could schedule the playback rate as 0 or 1 each time it ticks or implement it within an AudioWorklet.

tambien avatar Jan 31 '21 23:01 tambien

Ah, thanks for the reply, good to know! Lately I've been learning C++ audio development, but if I come back to the web side of things I may pick this back up.

samdafi avatar Feb 05 '21 18:02 samdafi

This is something I'm interested in too. The processing code for a latch is trivial. Here is a Max codebox example:

History x (0);

if (in2 != 0) {
	out1 = in1;
	x = in1;
} else {
	out1 = x;
}	

Would it be a good candidate for making an AudioWorklet and plugging it in that way?

jamesb93 avatar Mar 20 '21 13:03 jamesb93