Skip to content

pendragon-andyh/WebAudio-PulseOscillator

Repository files navigation

Create a Pulse Oscillator using the Web Audio API

Many classic analogue synthesiser lead-synth and string-ensemble sounds were based upon a modulated pulse waveform. This project demonstrates a set of techniques for recreating a Pulse Oscillator using the Web Audio API.

The new oscillator is demonstrated in the following examples:

The pulse wave is similar to a normal square waveform: plot_square

... but the "duty-cycle" or "mark-space ratio" is asymmetrical: plot_square_duty_wide

This creates a very distinctive sound – especially if mark-space ratio is modulated (which is what we’re going to do here).

Implementing the basics

Creating sawtooth oscillator node

The OscillatorNode provided by the Web Audio API can create sine, square, triangle and sawtooth waveforms.
Let start with a normal sawtooth wave:

    const audioContext = new AudioContext();
    const sawtooth = new OscillatorNode(audioContext, { type: "sawtooth", frequency: 110 });

    // connect to the destination    
    sawtooth.connect(audioContext.destination);

    // start the oscillator
    sawtooth.start(audioContext.currentTime);
    sawtooth.stop(audioContext.currentTime + 2);

This creates a waveform that rises from -1 to +1 – with an average value of 0 plot_sawtooth

Transforming sawtooth into square wave

Now lets add a WaveShaper node to transform the sawtooth into a square wave:

    const audioContext = new AudioContext();

    // create new curve that will transform values [0:127] to -1 and [128:255] to +1
    const squareCurve = new Float32Array(256); 
    squareCurve.fill(-1, 0, 128);
    squareCurve.fill(1, 128, 256);

    const sawtooth = new OscillatorNode(audioContext, { type: "sawtooth", frequency: 110 });
    const squareShaper = new WaveShaperNode(audioContext, { curve: squareCurve });

    // connect everything and the destination
    sawtooth.connect(squareShaper);
    squareShaper.connect(audioContext.destination);
    
    // start the oscillator
    sawtooth.start(audioContext.currentTime);
    sawtooth.stop(audioContext.currentTime + 2);

Half of the sawtooth wave was below 0 and so it was translates to -1 and other half was above 0 and so it was translates to +1. plot_sawtooth_squared

To get a pulse wave however, sawtooth wave need an offset so that its duty-cycle get longer.

Creating offset node

Adding constant signal of amplitude x will offset any wave by this value.

There are 3 ways of creating a constant offset value using the Web Audio API:

  • create an AudioBufferSource where the AudioBuffer only contains the value that we want
  • create another WaveShaper node that shapes all of its input values to the desired constant value
  • create an ConstantSourceNode

In this case, it is more convenient to use the WaveShaper node:

    const audioContext = new AudioContext();
    const offset = 0.5;

    // creating curve with constant amplitude of value   
    const constantCurve = (value) => {
        const curve = new Float32Array(2);
        curve[0] = value;
        curve[1] = value;
        
        return curve; 
    }

    const sawtooth = new OscillatorNode(audioContext, { type: "sawtooth", frequency: 110 });
    const constantShaper = new WaveShaperNode(audioContext, { curve: constantCurve(offset) });

    // mixing sawtooth signal with constant on the destination thus offseting sawtooth
    sawtooth.connect(constantShaper);
    sawtooth.connect(audioContext.destination);
    constantShaper.connect(audioContext.destination);

    // start the oscillator
    sawtooth.start(audioContext.currentTime);
    sawtooth.stop(audioContext.currentTime + 2);

Here is pulse wave offset by 0.5. Now its covering values from -0.5 to +1.5 plot_sawtooth_lifted

By doing so sawtooth wave changed its amplitude value ratio from 50/50 negative/positive values to 25/75: plot_sawtooth_lifted_filled

Transforming offset signal to square wave

Just like previously signal need to be transform by WaveShaper node:

    const audioContext = new AudioContext();
    const offset = 0.5; 

    // create new curve that will transform values [0:127] to -1 and [128:255] to +1
    const squareCurve = new Float32Array(256); 
    squareCurve.fill(-1, 0, 128);
    squareCurve.fill(1, 128, 256);

    // creating curve with constant amplitude of value   
    const constantCurve = (value) => {
        const curve = new Float32Array(2);
        curve[0] = value;
        curve[1] = value;
        
        return curve; 
    }

    const sawtooth = new OscillatorNode(audioContext, { type: "sawtooth", frequency: 110 });
    const squareShaper = new WaveShaperNode(audioContext, { curve: squareCurve });
    const constantShaper = new WaveShaperNode(audioContext, { curve: constantCurve(offset) });
    
    // mixing sawtooth signal with constant, transforming into square, connecting to the destination
    sawtooth.connect(constantShaper); 
    constantShaper.connect(squareShaper);
    sawtooth.connect(squareShaper);
    squareShaper.connect(audioContext.destination);

    // start the oscillator
    sawtooth.start(audioContext.currentTime);
    sawtooth.stop(audioContext.currentTime + 2);

The squareShaper will transform this into an output where a ¼ of the output values are -1, and the remaining ¾ are +1. plot_sawtooth_squared_lifted

This is cool, but the resulting sound is a bit static. It would be better to modulate pulse's width with AudioParam.

Adding modulation of the pulse width

The Web Audio API doesn’t support creation of AudioParam object directly so we're going to be devious again – and borrow an AudioParam from the GainNode.

The following code adds a new createPulseOscillator function to the AudioContext - and exposes a width parameter that can be modulated:

let audioContext = new (window.AudioContext ||
    window.webkitAudioContext ||
    function () {
        throw "Your browser does not support Web Audio API";
    })();

// create new curve that will flatten values [0:127] to -1 and [128:255] to 1
const squareCurve = new Float32Array(256);
squareCurve.fill(-1, 0, 128);
squareCurve.fill(1, 128, 256);

// constant signal on level 1
const constantCurve = new Float32Array(2);
constantCurve[0] = 1;
constantCurve[1] = 1;

// add a new factory method to the AudioContext object.
audioContext.createPulseOscillator = () => {
    // use a normal oscillator as the basis of pulse oscillator.
    const oscillator = new OscillatorNode(audioContext, { type: "sawtooth" });
    // shape the output into a pulse wave.
    const squareShaper = new WaveShaperNode(audioContext, { curve: squareCurve });
    // pass a constant value of 1 into the widthParameter – so the "width" setting
    // is duplicated to its output.
    const constantShaper = new WaveShaperNode(audioContext, { curve: constantCurve });
    // use a GainNode as our new "width" audio parameter.
    const widthParameter = new GainNode(audioContext, { gain: 0 });

    // add parameter to oscillator node as the new attribute
    oscillator.width = widthParameter.gain;

    // connect everything
    oscillator.connect(constantShaper);
    constantShaper.connect(widthParameter);
    widthParameter.connect(squareShaper);

    // override the oscillator's "connect" and "disconnect" method so that the
    // new node's output actually comes from the squareShaper.
    oscillator.connect = () => {
        squareShaper.connect.apply(squareShaper, arguments);
    };
    oscillator.disconnect = () => {
        squareShaper.disconnect.apply(squareShaper, arguments);
    };

    return oscillator;
};

Constant value of +1 is passed into the widthParameter. This means that whatever we do to its "gain" parameter will be reflected onto the node’s output. We attach the "gain" parameter to the oscillator node so that it becomes part of the oscillator’s interface.

Have a play - then feel free to incorporate these techniques in your own code.

Useful links

I found the following links useful for constructing this project:

License

Copyright (c) 2014 Andy Harman and Pendragon Software Limited.

Released under the MIT License.

About

Create a Pulse Oscillator using the Web Audio API

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published