Loading Symbol

JS Synthesizer Part 7: Multiple Oscillators

Synth with Two Oscillators

In part seven, we’ll be adding an another oscillator to the synthesizer. We’ll end up with two, but you can use this logic to add as many oscillators as you want. Having multiple oscillators creates a lot of sonic potential for the instrument.

This tutorial is part seven in a series that will show you how to create a virtual synthesizer using JavaScript and the Web Audio API. Click here to clone or download the working code examples from the GitHub repository. Click here to view the finished product that uses the concepts and code discussed in this series. To get a better idea of the complete process, checkout the overview.

Add Second Oscillator Elements

First we’re going to replace the HTML that contained the single oscillator with new code that has two oscillator dropdowns. This will add the on/off toggle and waveforms select elements that allow the user to change the oscillator options. We’re going to default them to different waveforms, so that the synth’s initial sound is more interesting.

<div class="row">
	<div class="col-12 col-sm-6">
		<div class="section osc">
			<p class="label">Oscillator 1<input class="oscOn" data-osc="osc1" type="checkbox" checked data-toggle="toggle" data-size="xs"></p>
			<div class="input-group mb-3">
				<div class="input-group-prepend">
					<label class="input-group-text" for="waveType1">Waveform</label>
				</div>
				<select class="custom-select waveType" id="waveType1">
					<option value="sine">Sine</option>
					<option value="square">Square</option>
					<option value="triangle">Triangle</option>
					<option value="sawtooth" selected>Sawtooth</option>
				</select>
			</div>
		</div>
	</div>
	<div class="col-12 col-sm-6">
		<div class="section osc">
			<p class="label">Oscillator 2<input class="oscOn" data-osc="osc2" type="checkbox" data-toggle="toggle" data-size="xs" checked></p>
			<div class="input-group mb-3">
				<div class="input-group-prepend">
					<label class="input-group-text" for="waveType2">Waveform</label>
				</div>
				<select class="custom-select waveType" id="waveType2">
					<option value="sine">Sine</option>
					<option value="square" selected>Square</option>
					<option value="triangle">Triangle</option>
					<option value="sawtooth">Sawtooth</option>
				</select>
			</div>
		</div>
	</div>
</div>

Add Bootstrap Toggle Dependencies

This synthesizer uses Bootstrap Toggle to create the oscillator on/off buttons. We need to the add the CSS and JavaScript dependencies:

<link href="https://cdn.jsdelivr.net/gh/gitbrent/[email protected]/css/bootstrap4-toggle.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/gh/gitbrent/[email protected]/js/bootstrap4-toggle.min.js" crossorigin="anonymous"></script>

Update the Synthesizer JavaScript

Now we need to update the synthesizer JavaScript to use this second oscillator.

Add Second Oscillator to Synth Settings Object

Currently, the synth settings object looks like this:

// Synth settings object
var synth = {
    osc: {
        on: true,
        type: 'sawtooth',
    },
    octave: 3,
    // Values in Seconds
    attack: .005,
    hold: .15,
    decay: .1,
    sustain: 40, // Gain
    release: 1,
}

We need to add a second oscillator and on/off state for each oscillator. The new settings object looks like this:

// Synth settings object
var synth = {
    osc1: {
        on: true,
        type: 'sawtooth',
        detune: 0 // In cents
    },
    osc2: {
        on: true,
        type: 'square',
        detune: 10 // In cents
    },
    octave: 3,
    // Values in Seconds
    attack: .005,
    hold: .15,
    decay: .1,
    sustain: 40, // Gain
    release: 1,
}

Update the Voice Class

Now we need to update the constructor, start, and stop functions of the Voice class.

Constructor

First we’ll change the constructor. Currently, the oscillator is stored as a property of the Voice object – this.oscillator. We’re going to to do the same thing for each oscillator, e.g. this.oscillator1, this.oscillator2, this.oscillator3. Like I mentioned before, you can add as many oscillators as you’d like.

We’re also going to set the frequency of the oscillators in the start function instead of the constructor. This is just to group together all the logic that uses the now variable. The new constructor looks like this:

constructor(rawNote, kbNote) {
    this.frequency = 0;

    var octave = synth.octave;
    
    if (octave === 1) {
        this.frequency = hzs[rawNote];
    } else if (octave === 2) {
        this.frequency = hzs[rawNote] * 2;
    } else if (octave === 3) {
        this.frequency = hzs[rawNote] * 4;
    } else if (octave === 4) {
        this.frequency = hzs[rawNote] * 8;
    } else if (octave === 5) {
        this.frequency = hzs[rawNote] * 16;
    } else if (octave === 6) {
        this.frequency = hzs[rawNote] * 32;
    } else if (octave === 7) {
        this.frequency = hzs[rawNote] * 64;
    }

    this.kbNote = kbNote;

    this.oscillator1 = audioCtx.createOscillator();
    this.oscillator2 = audioCtx.createOscillator();

    this.oscillator1.type = synth.osc1.type;
    this.oscillator2.type = synth.osc2.type;
}

Start

In the start function, we set the frequency of each oscillator and connect it to the gain node. Lastly, we’ll call the start function of each oscillator. Here’s the fully updated start function.

start() {
    var now = audioCtx.currentTime;
    this.voiceGain = audioCtx.createGain();
    
    this.oscillator1.frequency.setValueAtTime(this.frequency, now); // value in hertz
    this.oscillator2.frequency.setValueAtTime(this.frequency, now); // value in hertz
    
    this.oscillator1.connect(this.voiceGain);
    this.oscillator2.connect(this.voiceGain);
    
    this.oscillator1.connect(this.voiceGain);
    this.oscillator2.connect(this.voiceGain);
    this.voiceGain.gain.setValueAtTime(0, now);

    // Attack
    this.voiceGain.gain.linearRampToValueAtTime(1, now + synth.attack);
    this.voiceGain.gain.setValueAtTime(1, now + synth.attack);

    // Hold
    this.voiceGain.gain.linearRampToValueAtTime(1, now + synth.attack + synth.hold);

    // Decay and sustain
    this.voiceGain.gain.linearRampToValueAtTime(synth.sustain, now + synth.attack + synth.hold + synth.decay);
    
    this.voiceGain.connect(pregainNode); // Connecting to output

    if (synth.osc1.on) this.oscillator1.start(0);
    if (synth.osc2.on) this.oscillator2.start(0);
}

Stop

In the stop function, we just need to update the function to call stop on all the oscillators:

stop() {
    this.voiceGain.gain.linearRampToValueAtTime(.01, audioCtx.currentTime + synth.release);
    if (synth.osc1.on) this.oscillator1.stop(audioCtx.currentTime + synth.release);
    if (synth.osc2.on) this.oscillator2.stop(audioCtx.currentTime + synth.release);
}

Detect On/Off Status of Oscillators

Now that there are two oscillators, we need to detect if one, both, or neither are on. To do that, we’ll add an event listener to the on/off toggle input elements, and then pass those values to the synth settings object. The names of the oscillators (‘osc1’ and ‘osc2’) are stored as the ‘data-osc’ attribute on the on/off toggle elements.

$('.oscOn').change(function() {
    // Set the osc on/off to true/false based on which toggle was clicked
    synth[$(this).attr('data-osc')].on = this.checked;
});

The last thing to do is update the waveform of the oscillators on the synth settings object when the user changes the waveform.

$('.waveType').change(function() {
    synth.osc1.type = $(this).val();
    synth.osc2.type = $(this).val();
});

Having more than one oscillator on your synth gives you much more potential in the variety of sounds you can create. Enjoy!


Loading Symbol


Leave a Reply

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