Loading Symbol

JS Synthesizer Part 3: Creating a Multi-octave Keyboard

Virtual Synthesizer

This tutorial is part three in a series that will show you how to create a virtual synthesizer using JavaScript and the Web Audio API. In this part, we’ll cover how to create a multi-octave keyboard with JavaScript. 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.

This keyboard will be a functional two-octave keyboard. It will have another octave as well as a third C key. We will also add an octave select dropdown so the user can play any two octaves between C1 and C7.

The Multi-octave Keyboard HTML

We’ll start by replacing the old keyboard HTML with this new two-octave keyboard.

<div id="scalable-keys" class="keys">
    <ul>
        <li class="key white c" data-note="C"></li>
        <li class="key black cs" data-note="C#"></li>
        <li class="key white d" data-note="D"></li>
        <li class="key black ds" data-note="D#"></li>
        <li class="key white e" data-note="E"></li>
        <li class="key white f" data-note="F"></li>
        <li class="key black fs" data-note="F#"></li>
        <li class="key white g" data-note="G"></li>
        <li class="key black gs" data-note="G#"></li>
        <li class="key white a" data-note="A"></li>
        <li class="key black as" data-note="A#"></li>
        <li class="key white b" data-note="B"></li>
        <li class="key white c" data-note="2C"></li>
        <li class="key black cs" data-note="2C#"></li>
        <li class="key white d" data-note="2D"></li>
        <li class="key black ds" data-note="2D#"></li>
        <li class="key white e" data-note="2E"></li>
        <li class="key white f" data-note="2F"></li>
        <li class="key black fs" data-note="2F#"></li>
        <li class="key white g" data-note="2G"></li>
        <li class="key black gs" data-note="2G#"></li>
        <li class="key white a" data-note="2A"></li>
        <li class="key black as" data-note="2A#"></li>
        <li class="key white b" data-note="2B"></li>
        <li class="key white c" data-note="3C"></li>
    </ul>
</div>

We’re also going to add a dropdown box for the user to select which octave range they want to play in. This is how the two octave keyboard will allow the user to play in six octaves.

<div class="row">
    <div class="col-sm-6 col-lg-7">
        <p>Try playing on your keyboard.</p>
    </div>
    <div class="col-sm-6 col-lg-5 text-right">
        <div class="input-group mb-3">
          <div class="input-group-prepend">
            <label class="input-group-text" for="octaveSelect">Keyrange</label>
          </div>
          <select class="custom-select" id="octaveSelect">
            <option value="1">C1 - C3</option>
            <option value="2">C2 - C4</option>
            <option value="3" selected>C3 - C5</option>
            <option value="4">C4 - C6</option>
            <option value="5">C5 - C7</option>
          </select>
        </div>
    </div>
</div>

Multi-octave Keyboard Styles

We’re actually going to set a fixed height and width for the keyboard this time and use JavaScript to scale it based on the device window width.

.keys {
    ul {
        height: 192px;
        width: 758px;
        margin: 0 auto 1em;
        padding: 0 1.5em;
        position: relative;
        font-size: 12px;
        position: relative;
        left: 50%;
        top: 50%;
        transform: translate(-50%, 0);
        transform-origin: center center;
    }
    li {
        margin: 0;
        padding: 0;
        list-style: none;
        position: relative;
        float: left;
    }
    ul .white {
        height: 16em;
        width: 4em;
        z-index: 1;
        border-left: 1px solid #bbb;
        border-bottom: 1px solid #bbb;
        border-radius: 0 0 5px 5px;
        box-shadow: -1px 0 0 rgba(255, 255, 255, 0.8) inset, 0 0 5px #ccc inset, 0 0 3px rgba(0, 0, 0, 0.2);
        background: linear-gradient(to bottom, #eee 0%, #fff 100%)
    }
    ul .white.active {
        border-top: 1px solid #777;
        border-left: 1px solid #999;
        border-bottom: 1px solid #999;
        box-shadow: 2px 0 3px rgba(0, 0, 0, 0.1) inset, -5px 5px 20px rgba(0, 0, 0, 0.2) inset, 0 0 3px rgba(0, 0, 0, 0.2);
        background: linear-gradient(to bottom, #fff 0%, #e9e9e9 100%)
    }
    .black {
        height: 8em;
        width: 2em;
        margin: 0 0 0 -1em;
        z-index: 2;
        border: 1px solid #000;
        border-radius: 0 0 3px 3px;
        box-shadow: -1px -1px 2px rgba(255, 255, 255, 0.2) inset, 0 -5px 2px 3px rgba(0, 0, 0, 0.6) inset, 0 2px 4px rgba(0, 0, 0, 0.5);
        background: linear-gradient(45deg, #222 0%, #555 100%)
    }
    .black.active {
        box-shadow: -1px -1px 2px rgba(255, 255, 255, 0.2) inset, 0 -2px 2px 3px rgba(0, 0, 0, 0.6) inset, 0 1px 2px rgba(0, 0, 0, 0.5);
        background: linear-gradient(to right, #444 0%, #222 100%)
    }
    .a, .b, .g, .d, .e {
        margin: 0 0 0 -1em;
    }
    ul li:first-child {
        border-radius: 5px 0 5px 5px;
    }
    ul li:last-child {
        border-radius: 0 5px 5px 5px;
    }
}

JavaScript to Scale the Keyboard

We need a short script to scale the keyboard for any device width. This will ensure no keys are hanging out of the border of the synthesizer. The following code is dividing the width of the synthesizer border by the full width of the keyboard and scaling the keyboard down by that much.

// Scale Keyboard
var $keys = $("#keys");
var $container = $("#border"); // element we want to fill completely

function doResize(event) {
    // Don't scale it if the screen is big enough already
    if ($(window).width() > 992) return false;

    // Scale based on the '#border' element of the synthesizer
    // because this is the element we always want to fill 100%
    // with the keyboard
    var scale = $("#border").width() / $keys.outerWidth();

    $keys.css({
        transform: "translate(-50%, 0%) " + "scale(" + scale + ")"
    });
}

doResize(); // Scale keyboard initially

// Scale keyboard on screensize change
$(window).resize(function() {
    doResize();
});

Multi-octave Keyboard JavaScript

There are many ways to handle input and programmatically choose the right frequency to play. The way I’ve chosen for this synthesizer, is to base the entire keyboard on the low octave (C1 – B1). That is, we will multiply these base frequencies by multipliers to get the higher frequencies. Here is our base frequency map from the last part. We can leave it the same.

var hzs = {
    'C':  32.703195662574829,
    'C#':  34.647828872109012,
    'D':  36.708095989675945,
    'D#':  38.890872965260113,
    'E':  41.203444614108741,
    'F':  43.653528929125485,
    'F#':  46.249302838954299,
    'G':  48.999429497718661,
    'G#':  51.913087197493142,
    'A':  55.000000000000000,
    'A#':  58.270470189761239,
    'B':  61.735412657015513
};

Synthesizer Settings Object

var synth = {
    type: 'sawtooth',
    octave: 3,
}

The settings here are the defaults. The waveform ‘type’ property is used by the Voice constructor and the octave property will be used for two things:

  • The keyboard note (from the data-note attribute)
  • The note (also the data-note attribute but without the octave number in front)

We also need to update synth.octave by checking the octave currently selected in the dropdown. We calculate synth.octave using the current selected octave range and the key that was clicked on the keyboard.

$('.key').on('mousedown', function() {

  var kbNote = $(this).attr('data-note');
  synth.type = $('#waveType').val();

  // Reset octave to first showing onkeyboard (first octave on the left side)
  synth.octave = parseInt($('#octaveSelect').val());

  // Adjust octave
  if (kbNote.indexOf(2) != -1) {
    synth.octave++; // If they played in the second octave on the keyboard, add 1
  } else if (kbNote.indexOf(3) != -1) {
    synth.octave = synth.octave + 2; // If they played in the thrid octave on the keyboard, add 2
  }

  // Strip numbers. Right now it could look like this: 2F#
  var rawNote = kbNote.replace(/[0-9]/g, '');

  var voice = new Voice(rawNote, kbNote);

  activeVoices[rawNote+synth.octave] = voice;
  voice.start();

  $(this).addClass('active');
});

Update Mouseup EventListener

When a mouseup event happens, it doesn’t matter where, because we don’t want any notes continuing to play after the user lets up a mouse click. We’re going to create a function that stops all actives voices. Then we’re going to call that function anytime the mouseup event happens anywhere on the page.

$(document).on('mouseup', function(e) {
    stopAllKeys();
    return true;
});

function stopAllKeys() {
    // console.log("stopping all keys");
    const keys = Object.keys(activeVoices);

    for (const key of keys) {
        activeVoices[key].stop();
        delete activeVoices[key];
    }

    $('.key').removeClass('active');
    return true;
}

Update Voice Class to Calculate Note Frequency

Now we need to update the Voice class to calculate the correct frequency for the note played. Every note can be played an octave higher by multiplying it’s frequency by two. With that logic, we can check the octave and input note, then choose the correct multiplier to get the desired frequency. Take a look at the code below. See how the multiplier is doubling with each increasing octave?

class Voice {

    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.oscillator = audioCtx.createOscillator();
        this.oscillator.type = synth.type; // Get waveform type synth settings
        this.oscillator.frequency.setValueAtTime(this.frequency, audioCtx.currentTime); // value in hertz
    }

    start() {
        this.oscillator.connect(audioCtx.destination); // connect oscillator to output
        this.oscillator.start(); 
    }

    stop() {
        this.oscillator.stop();
    }
}

That’s it! Now our synthesizer has a multi-octave keyboard and a functional range. In the next post, we’ll receive input from the computer keyboard. Then it really becomes playable.


Loading Symbol


Leave a Reply

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