Digital Piano with Web Audio API (2) : Setting the time variation of amplitude

The time variation of the sound amplitude after pressing a key on the digital piano on this page has been changed.

Although it is different from the sound of a piano, it seems a little closer to the sound of the instrument.

Volume:
Current waveform:

The JavaScript function playTone, which outputs sound, was changed as follows. The amplitude (gain) of the sound is maximized 0.05 seconds after the key is pressed. It becomes smaller and the sound disappears after 1 second. The linearRampToValueAtTime function is used so that the amplitude varies linearly with time.

function playTone(freq) {
    const osc = audioContext.createOscillator();
    const volume = volumeControl.value;

    const gainNode = audioContext.createGain();
    gainNode.connect(audioContext.destination);
    gainNode.gain.setValueAtTime(0, audioContext.currentTime);
    gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.05);
    gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1.00);

    osc.connect(gainNode);

    const type = wavePicker.options[wavePicker.selectedIndex].value;

    if (type == "custom") {
        osc.setPeriodicWave(customWaveform);
    } else {
        osc.type = type;
    }

    osc.frequency.value = freq;
    osc.start();

    osc.stop(audioContext.currentTime + 1.00);

    return osc;
}

The css and HTML code is the same as this page.

Except for the function playTone, some of the JavaScript code has also been changed. The entire JavaScript code is as follows.

<script>
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
let oscList = [];

const keyboard = document.querySelector(".keyboard");
const wavePicker = document.querySelector("select[name='waveform']");
const volumeControl = document.querySelector("input[name='volume']");

let noteFreq = null;
let customWaveform = null;
let sineTerms = null;
let cosineTerms = null;

const note_names =
[
    ["ラ", "", "A", ""],
    ["ラ#", "シ$\\flat$", "A#", "B$\\flat$"],
    ["シ", "","B", ""],
    ["ド", "","C", ""],
    ["ド#", "レ$\\flat$","C#", "D$\\flat$"],
    ["レ", "","D", ""],
    ["レ#", "ミ$\\flat$","D#", "E$\\flat$"],
    ["ミ", "","E", ""],
    ["ファ", "","F", ""],
    ["ファ#", "ソ$\\flat$","F#", "G$\\flat$"],
    ["ソ", "", "G", ""],
    ["ソ#", "ラ$\\flat$", "G#", "A$\\flat$"]
];

setup();

// -------------------------------------------------------
// functions
// -------------------------------------------------------

function createNoteTable() {

    let noteFreq = [];
    for (let octave = 0; octave < 9; octave++) {
        noteFreq[octave] = [];
    }

    for (let n = 0; n < 88; n++) {

        const frequency = getAudioFrequency(n);

        let octave = parseInt(n/12);
        if (n % 12 >= 3) {
            octave++;
        }

        const note_name_sharp_english = note_names[n % 12][2];
        noteFreq[octave][note_name_sharp_english] = frequency;
    }

    return noteFreq;
}

function getAudioFrequency(n) {
    return 27.5 * ( Math.pow( Math.pow(2, 1/12), n) );
}

function setup() {
    noteFreq = createNoteTable();

    noteFreq.forEach(function(keys, idx) {
        const keyList = Object.entries(keys);
        const octaveElem = document.createElement("div");
        octaveElem.className = "octave";

        keyList.forEach(function(key) {
            const key_name = key[0];
            if (key_name === 'A' || key_name === 'B' || key_name === 'C' || key_name === 'D' ||
                key_name === 'E' || key_name === 'F' || key_name === 'G') {
                octaveElem.appendChild(createKey(key_name, idx, key[1], 'white-key'));
            } else {
                octaveElem.appendChild(createKey(key_name, idx, key[1], 'black-key'));
            }
        });

        keyboard.appendChild(octaveElem);
    });

    document.querySelector("div[data-note='F'][data-octave='5']").scrollIntoView(false);

    sineTerms = new Float32Array([0, 0, 1, 0, 1]);
    cosineTerms = new Float32Array(sineTerms.length);
    customWaveform = audioContext.createPeriodicWave(cosineTerms, sineTerms);

    for (i = 0; i < 9; i++) {
        oscList[i] = {};
    }
}

function createKey(note, octave, freq, keyColor) {
    const keyElement = document.createElement("div");
    const labelElement = document.createElement("div");

    if (keyColor === 'black-key') {
        keyElement.className = "key black-key";
    } else {
        keyElement.className = "key";
    }
    keyElement.dataset["octave"] = octave;
    keyElement.dataset["note"] = note;
    keyElement.dataset["frequency"] = freq;

    labelElement.innerHTML = note + "<sub>" + octave + "</sub>";
    keyElement.appendChild(labelElement);

    keyElement.addEventListener("mousedown", notePressed, false);
    keyElement.addEventListener("touchstart", notePressed, false);

    return keyElement;
}

function playTone(freq) {
    const osc = audioContext.createOscillator();
    const volume = volumeControl.value;

    const gainNode = audioContext.createGain();
    gainNode.connect(audioContext.destination);
    gainNode.gain.setValueAtTime(0, audioContext.currentTime);
    gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.05);
    gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1.00);

    osc.connect(gainNode);

    const type = wavePicker.options[wavePicker.selectedIndex].value;

    if (type == "custom") {
        osc.setPeriodicWave(customWaveform);
    } else {
        osc.type = type;
    }

    osc.frequency.value = freq;
    osc.start();

    osc.stop(audioContext.currentTime + 1.00);

    return osc;
}

function notePressed(event) {
    const dataset = event.target.dataset;
    const octave = +dataset["octave"];

    oscList[octave][dataset["note"]] = playTone(dataset["frequency"]);
}
</script>

Leave a Reply