こちらのページの電子ピアノの鍵盤の配置を変えました。
Volume:
Current waveform:
主な変更点は HTML を生成する下記の JavaScript で白鍵の子要素として黒鍵を用意し、css で黒鍵の子要素の position に absolute を指定した点です。黒鍵の位置を指定する css で親要素の白鍵の位置に対する黒鍵の相対的な位置を top と left で指定しています。黒鍵が白鍵の前面に表示されるように z-index: 1; を指定しています。また、黒鍵を押したときに親要素の白鍵ではなく黒鍵の音が鳴るように pointer-events: auto; を指定しています。
1. JavaScript の主な変更点 (鍵盤のHTMLを生成するコードを変更)
noteFreq.forEach(function(keys, idx) {
const keyList = Object.entries(keys);
const octaveElem = document.createElement("div");
octaveElem.className = "octave";
let whiteKey = null;
let blackKey = null;
keyList.forEach(function(key) {
const key_name = key[0];
if (key_name === 'ド' || key_name === 'レ' || key_name === 'ミ' || key_name === 'ファ' ||
key_name === 'ソ' || key_name === 'ラ' || key_name === 'シ') {
if (whiteKey != null) {
octaveElem.appendChild(whiteKey);
}
whiteKey = createKey(key_name, idx, key[1], 'white-key');
} else {
blackKey = createKey(key_name, idx, key[1], 'black-key');
whiteKey.appendChild(blackKey);
}
});
octaveElem.appendChild(whiteKey);
keyboard.appendChild(octaveElem);
});
2. cssの主な変更点
.key.black-key {
position: absolute;
background-color: #000;
color: #fff;
height: 80px;
top: 0px;
left: 20px;
z-index: 1;
pointer-events: auto;
vertical-align: top;
}
3. 修正後のコード全体
3.1. css
<style type="text/css">
.container {
overflow-x: scroll;
overflow-y: hidden;
width: 100%;
height: 180px;
white-space: nowrap;
margin: 10px;
}
.keyboard {
width: auto;
padding: 0;
margin: 0;
}
.key {
position: relative;
cursor: pointer;
font: 12px "Open Sans", "Lucida Grande", "Arial", sans-serif;
border: 1px solid black;
border-radius: 5px;
width: 40px;
height: 120px;
text-align: center;
box-shadow: 2px 2px darkgray;
display: inline-block;
margin-right: 3px;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.key.black-key {
position: absolute;
background-color: #000;
color: #fff;
height: 80px;
top: 0px;
left: 20px;
z-index: 1;
pointer-events: auto;
vertical-align: top;
}
.key div {
position: absolute;
bottom: 0;
text-align: center;
width: 100%;
pointer-events: none;
}
.key div sub {
font-size: 10px;
pointer-events: none;
}
.octave {
display: inline-block;
padding: 0 6px 0 0;
}
.settingsBar {
padding-top: 8px;
font: 14px "Open Sans", "Lucida Grande", "Arial", sans-serif;
position: relative;
vertical-align: middle;
width: 100%;
height: 60px;
}
.left {
width: 50%;
position: absolute;
left: 0;
display: table-cell;
vertical-align: middle;
}
.left span, .left input {
vertical-align: middle;
}
.right {
width: 50%;
position: absolute;
right: 0;
display: table-cell;
vertical-align: middle;
}
.right span {
vertical-align: middle;
}
.right input {
vertical-align: baseline;
}
</style>
3.2. HTML
<div class="container">
<div class="keyboard"></div>
</div>
<div class="settingsBar">
<div class="left">
<span>Volume: </span>
<input type="range" min="0.0" max="1.0" step="0.01" value="0.5" list="volumes" name="volume">
<datalist id="volumes">
<option value="0.0" label="Mute">
<option value="1.0" label="100%">
</datalist>
</div>
<div class="right">
<span>Current waveform: </span>
<select name="waveform">
<option value="sine">Sine</option>
<option value="square">Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle" selected>Triangle</option>
<option value="custom">Custom</option>
</select>
</div>
</div>
3.3. 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";
let whiteKey = null;
let blackKey = null;
keyList.forEach(function(key) {
const key_name = key[0];
if (key_name === 'ド' || key_name === 'レ' || key_name === 'ミ' || key_name === 'ファ' ||
key_name === 'ソ' || key_name === 'ラ' || key_name === 'シ') {
if (whiteKey != null) {
octaveElem.appendChild(whiteKey);
}
whiteKey = createKey(key_name, idx, key[1], 'white-key');
} else {
blackKey = createKey(key_name, idx, key[1], 'black-key');
whiteKey.appendChild(blackKey);
}
});
octaveElem.appendChild(whiteKey);
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>