Web Audio APIで電子ピアノ(2):振幅の時間変化の設定

こちらのページの電子ピアノで鍵盤を押した後の音の振幅の時間変化を変更しました。

ピアノの音とは異なりますが、少し楽器に近い音に近付いたような気がします。

Volume:
Current waveform:

音を出力する JavaScript の関数 playTone を下記のように変更しました。鍵盤を押してから 0.05 秒後に音の振幅(ゲイン)が最大になり、その後小さくなって1秒後に音が消えるようにしました。振幅の時間変化は linearRampToValueAtTime 関数を使用し、時間を横軸、振幅を縦軸に取ったグラフで振幅が直線的に時間変化するようにしています。

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

cssとHTMLのコードはこちらのページと同様です。

関数 playTone 以外のコードも一部書き換えました。書き換え後の JavaScript のコード全体は以下になります。

<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_japanese = note_names[n % 12][0];
        noteFreq[octave][note_name_sharp_japanese] = 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 === 'ド' || key_name === 'レ' || key_name === 'ミ' || key_name === 'ファ' ||
                key_name === 'ソ' || key_name === 'ラ' || key_name === 'シ') {
                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='ファ'][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>

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA