Loading Symbol

JS Synthesizer Part 8: Detuning Oscillators

Detune function

In part eight, we’ll be adding a detune feature to the synthesizer. This will enable the user to make the oscillators slightly out of tune with each other which can add a cool dimension to the sound.

This tutorial is part eight 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 Detune Control Elements

First we’re going to add the detune controls for each oscillator. They will be range sliders that go right under the waveform select. Here’s the markup for the first slider and label. The second one is the same, but with the values updated to 2. These go directly after the .input-group-prepend element of the oscillators. Notice that these aren’t actually input elements. They’ll be styled as input elements with CSS and their movement animated with JavaScript.

<p class="label">Detune</strong></p>
<div class="control">
    <div class="sliders">
      <div id="detune1" data-osc="osc1" class="dragmove slider" style=""></div>
    </div>
</div>

Next we need to add the Interact JavaScript library script to the bottom of our HTML template here. Once we get some more things build out, we’ll be adding the JavaScript to make this range slider work.

<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>

Add the Range Slider Styles

These styles will make the elements we added above look like a range slider. It still won’t function as one though. For that we’ll need the JavaScript.

/* Range Sliders */
.sliders {
padding: .5em
}
.slider {
    position: relative;
    width: 100%;
    height: 1em;
    margin: 0 auto;
    padding-left: 50%;
    background-color: #29e;
    border-radius: 0.5em;
    box-sizing: border-box;
    font-size: 1em;
    -ms-touch-action: none;
    touch-action: none;
}
/* the slider handle */
.slider:before {
    content: "";
    display: block;
    position: relative;
    top: -0.5em;
    width: 2em;
    height: 2em;
    margin-left: -1em;
    border: solid 0.25em #fff;
    border-radius: 1em;
    background-color: inherit;
    box-sizing: border-box;
    cursor: pointer;
}
/* display the value */
.slider:after {
    content: attr(data-value);
    position: absolute;
    top: -1.5em;
    width: 8em;
    line-height: 1em;
    margin-left: -1em;
    text-align: left;
    cursor: pointer;
}

Enable the Range Slider with JavaScript

Before we add events listeners to the detune sliders, were going to update and set default values on the synth object that contains our synth’s settings. We’re also going to update the Voice class to make use of this new detune variable the synth has to offer.

// Update synth settings object
osc1: {
    on: true,
    type: 'sawtooth',
    detune: 0 // In cents
},
osc2: {
    on: true,
    type: 'square',
    detune: 10 // In cents
}

Next we’ll create a new function in the Voice class that reads those new detune properties, and uses them to change the pitch of the oscillators.

detune(osc) {
    // console.log('detuning note ' + this.note + ' on dosc: ' + osc);
    if (osc == 'osc1') {
        this.oscillator1.detune.setValueAtTime(synth.osc1.detune, audioCtx.currentTime);
    } else if (osc == 'osc2') {
        this.oscillator2.detune.setValueAtTime(synth.osc2.detune, audioCtx.currentTime);
    }
}

Now we just need to call this new detune function from the constructor of the Voice class. Here’s the entire updated constructor. The only thing added are the two lines at the the end where detune is being called.

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;
    }

    console.log('Note: ' + kbNote);
    console.log('Frequency: ' + this.frequency);

    this.kbNote = kbNote;

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

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

    this.detune('osc1');
    this.detune('osc2');
}

Now we can add the event listener to make use of the new functionality we added to the synth. Notice we’re making use of the Interact JS library to detect the range slider movement.

// Detune Slider
interact('.slider') // target elements with the "slider" class
    .draggable({ // make the element fire drag events
        origin: 'self', // (0, 0) will be the element's top-left
        inertia: true, // start inertial movement if thrown
        modifiers: [
            interact.modifiers.restrict({
                restriction: 'self' // keep the drag coords within the element
            })
        ]
    })
    // Step 3
    .on('dragmove', function(event) { // call this listener on every dragmove
        const sliderWidth = $(event.currentTarget)[0].offsetWidth;
        var value = event.pageX / sliderWidth;
        if (value > 1) value = 1;
        var cents = value * 100 - 50;
        var osc = event.target.attributes.getNamedItem('data-osc').value;
        // Update synth data
        if (osc == 'osc1') {
            synth.osc1.detune = cents;
        } else if (osc == 'osc2') {
            synth.osc2.detune = cents;
        }
        // Detune all the active voices the same
        Object.keys(activeVoices).forEach(function(key, index) {
            activeVoices[key].detune(osc);
        });
        event.target.style.paddingLeft = (value * 100) + '%';
        // Update cents label
        event.target.setAttribute('data-value', cents.toFixed(2)+ ' cents');
    });
// Zero out detune on double click
$('.slider').dblclick(function() {
    $(this).attr('data-value', 0).css('padding-left', '50%');
    synth[$(this).attr('data-osc')].detune = 0;
});
// Init sliders
$('#detune1').attr('data-value', synth.osc1.detune+' cents').css('padding-left', '50%');
$('#detune2').attr('data-value', synth.osc1.detune+' cents').css('padding-left', '50%');

Conclusion

That’s it! If you’re not sure what it’s supposed to sound like, try setting the oscillators’ detune to the same value. That will completely eliminate the detuned sound. Then detune one oscillator 5 cents or more. You will here it now.

Not too difficult for the new sounds it delivers right? Now the synth has another dimension of possibilities.


Loading Symbol


Leave a Reply

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