Synth Keyboard ❤️
Last updated
Was this helpful?
Last updated
Was this helpful?
CSSTricks ⟩ ⭐️
維基 ⟩ (Octave)
MDN ⟩
EventTarget ⟩
BaseAudioContext ⟩
: number - ever-increasing timestamp in seconds
:
: GainNode
📁
AudioNode ⟩
📁 AudioScheduledSourceNode ⟩
📁 - a constant tone
📁 - control overall gain (volume) of the audio graph
: AudioParam
⟩
schedules a gradual change in the value of the
CSS ⟩
- controls whether user can select text.
八度(英語:Octave,亦稱為完全八度)是的一種,它的組成是由2個相同但來自不同所組成。兩音的距離為 12 個,而的比例是 2:1,換而言之,較高音的頻率為較低音的兩倍。
計算音頻的公式:
pitch =a⋅212n
a: 440 Hz ( pitch of A4)
n: number of notes above or below A4
z-index only works on positioned elements❗️
<!--
store the note value in a custom `note` attribute
so it’s easy to access in JavaScript
our keyboard covers just over an octave,
starting at C₃ and ending at F₄.
-->
<ul id="keyboard">
<li note="C" octave="3" class="white no-offset">A</li>
<li note="C#" octave="3" class="black">W</li>
<li note="D" octave="3" class="white">S</li>
<li note="D#" octave="3" class="black">E</li>
<li note="E" octave="3" class="white">D</li>
<li note="F" octave="3" class="white no-offset">F</li>
<li note="F#" octave="3" class="black">T</li>
<li note="G" octave="3" class="white">G</li>
<li note="G#" octave="3" class="black">Y</li>
<li note="A" octave="4" class="white">H</li>
<li note="A#" octave="4" class="black">U</li>
<li note="B" octave="4" class="white">J</li>
<li note="C2" octave="4" class="white no-offset">K</li>
<li note="C#2" octave="4" class="black">O</li>
<li note="D2" octave="4" class="white">L</li>
<li note="D#2" octave="4" class="black">P</li>
<li note="E2" octave="4" class="white">;</li>
<li note="F2" octave="4" class="white no-offset">'</li>
</ul>
/* css reset */
/* :root = html (with higher specificity) */
:root {
box-sizing: border-box;
}
/* "*" doesn't include pseudo-elements */
*, ::before, ::after {
/* box-sizing isn’t normally an inherited property, */
/* use `inherit` to force it. */
box-sizing: inherit;
}
/* css vars for colors */
:root {
--keyboard: hsl(300, 100%, 16%);
--keyboard-shadow: hsla(19, 50%, 66%, 0.2);
--keyboard-border: hsl(20, 91%, 5%);
--black-10: hsla(0, 0%, 0%, 0.1);
--black-20: hsla(0, 0%, 0%, 0.2);
--black-30: hsla(0, 0%, 0%, 0.3);
--black-50: hsla(0, 0%, 0%, 0.5);
--black-60: hsla(0, 0%, 0%, 0.6);
--white-20: hsla(0, 0%, 100%, 0.2);
--white-50: hsla(0, 0%, 100%, 0.5);
--white-80: hsla(0, 0%, 100%, 0.8);
}
/* keys */
.white, .black {
/* ⭐️⭐️⭐️ 有此設定 z-index 才有作用 ⭐️⭐️⭐️ */
position: relative;
/* ⭐️ 讓所有鍵靠在一起 */
float: left;
/* ⭐️ 所有鍵往左移(自己一半的寬度) */
margin: 0 0 0 -1rem;
/* ⭐️ list-style 自動消失❓ */
display: flex;
/* ⭐️ 英文字母放到下方中間 */
justify-content: center;
align-items: flex-end;
padding: .5rem 0;
/* ⭐️ user can't select text */
user-select: none;
cursor: pointer;
/* border: 1px solid black; */
}
#keyboard li:first-child {
border-radius: 5px 0 5px 5px;
}
#keyboard li:last-child {
border-radius: 0 5px 5px 5px;
}
.white {
height: 12.5rem;
width: 3.5rem;
z-index: 1;
border-left: 1px solid hsl(0, 0%, 73%);
border-bottom: 1px solid hsl(0, 0%, 73%);
border-radius: 0 0 5px 5px;
box-shadow:
-1px 0 0 var(--white-80) inset,
0 0 5px hsl(0, 0%, 80%) inset,
0 0 3px var(--black-20);
background: linear-gradient(
to bottom,
hsl(0, 0%, 93%) 0%,
white 100%
);
color: var(--black-30);
}
.black {
height: 8rem;
width: 2rem;
/* ⭐️ 黑鍵在白鍵上方 */
z-index: 2;
border: 1px solid black;
border-radius: 0 0 3px 3px;
box-shadow:
-1px -1px 2px var(--white-20) inset,
0 -5px 2px 3px var(--black-60) inset,
0 2px 4px var(--black-50);
background: linear-gradient(
45deg, hsl(0, 0%, 13%) 0%, hsl(0, 0%, 33%) 100%
);
color: var(--white-50);
}
/* ⭐️ A, F, K 鍵不需左移 */
.no-offset {
margin: 0;
}
/* pressed state */
.white.pressed {
border-top: 1px solid hsl(0, 0%, 47%);
border-left: 1px solid hsl(0, 0%, 60%);
border-bottom: 1px solid hsl(0, 0%, 60%);
box-shadow:
2px 0 3px var(--black-10) inset,
-5px 5px 20px var(--black-20) inset,
0 0 3px var(--black-20);
background: linear-gradient(
to bottom,
white 0%,
hsl(0, 0%, 91%) 100%
);
/* outline: none; */
}
.black.pressed {
box-shadow:
-1px -1px 2px var(--white-20) inset,
0 -2px 2px 3px var(--black-60) inset,
0 1px 2px var(--black-50);
background: linear-gradient(
to right,
hsl(0, 0%, 27%) 0%,
hsl(0, 0%, 13%) 100%
);
/* outline: none; */
}
/* keyboard */
#keyboard {
background-color: var(--keyboard);
box-shadow:
0 0 50px var(--black-50) inset,
0 1px var(--keyboard-shadow) inset,
0 5px 15px var(--black-50);
border: 1px solid var(--keyboard-border);
border-radius: 1rem;
height: 15.25rem;
width: 41rem;
margin: 0.5rem auto;
/* ⭐️ 將鍵盤移往下方偏右 */
padding: 3rem 0 0 1.25rem;
}
// ⭐️ audio context
const audioContext = new AudioContext();
// all note elements
const allNoteElems = document.querySelectorAll(`[note]`);
// ⭐️ keyboard keys (dictionary)
// keys[key] = {element: , note: , octave: }
let keys = {};
allNoteElems.forEach(elem => {
let key = elem.textContent;
keys[key] = {
element: elem,
note: elem.getAttribute('note'),
octave: +elem.getAttribute('octave')
};
});
// console.log(keys);
/*
pitch = 440 * 2^(n/12)
• 440 Hz: pitch value of A₄
• n: number of notes above or below that pitch
Musical Notation
----------------
• A# == Bb
*/
// ⭐️ usage: pitch("B#", 5)
function pitch(note="A", octave=4){
const notes = {
A: 0, B: 2, C: 3, D: 5, E:7, F: 8, G: 10
};
let n = notes[note[0]];
if(note.includes('#')) n += 1;
if(note.includes('b')) n -= 1;
if(note.includes('Ab')) n += 12; // "Ab" == 11
n += 12 * (octave - 4); // 1 octave = 12 notes
// pitch = 440 Hz * 2^(n/12)
return 440 * Math.pow(2, n/12);
}
// console.log(pitch("C#", 4));
/*
⭐️ pressedNotes
---------------
• notes that are playing at any given time.
• its unique key constraint can help prevent triggering
the same note multiple times in a single press.
*/
const pressedNotes = new Map();
// ⭐️
// user can only click one key at a time.
let clickedKey = "";
// ⭐️ usage: playKey('A')
const playKey = (key) => {
// no such key
if (!keys[key]) {
console.log(`no such key: ${key}`)
return;
}
// playback timestamp
const t = audioContext.currentTime;
// ⭐️ note's pitch
const freq = pitch(keys[key].note, keys[key].octave);
const GAIN = {
zero : 0.00001, // 為何不用 0 ?
max : 0.5,
sustained: 0.05,
};
// ⭐️ overall gain (volumn)
const noteGainNode = audioContext.createGain(); // ?
noteGainNode.connect(audioContext.destination); // ?
noteGainNode.gain.value = GAIN.zero; // 音量 ?
// ⭐️ attack:
// how quickly a sound goes from nothing to max volume
noteGainNode.gain.exponentialRampToValueAtTime(GAIN.max, t + 0.01);
// ⭐️ decay:
// time taken from peak volume to sustained volume
noteGainNode.gain.exponentialRampToValueAtTime(GAIN.sustained, t + 1);
// ⭐️ release:
// how long it takes to fade to nothing
noteGainNode.gain.exponentialRampToValueAtTime(GAIN.zero, t + 2);
// ⭐️ a constant tone
const osc = audioContext.createOscillator(); // ?
osc.connect(noteGainNode); // ?
osc.type = "triangle"; // ⭐️ timbre (音色) of the sound
osc.frequency.value = freq; // ⭐️ set pitch (音高)
// ⭐️ "pressed" state (改變外觀)
keys[key].element.classList.add("pressed");
// ⭐️
pressedNotes.set(key, osc);
pressedNotes.get(key).start();
};
// ⭐️ usage: stopKey('A')
const stopKey = (key) => {
// no such key
if (!keys[key]) {
console.log(`no such key: ${key}`)
return;
}
// ⭐️ remove "pressed" status
keys[key].element.classList.remove("pressed");
const osc = pressedNotes.get(key);
if (osc) {
setTimeout(() => osc.stop(), 2000);
pressedNotes.delete(key);
}
};
// "keydown" event
document.addEventListener("keydown", (e) => {
let key = e.key.toUpperCase();
if (pressedNotes.get(key)) return; // already playing?
playKey(key);
});
// "keyup" event
document.addEventListener("keyup", (e) => {
let key = e.key.toUpperCase();
stopKey(key);
});
// mouse events
for (const [key, { element }] of Object.entries(keys)) {
element.addEventListener("mousedown", () => {
playKey(key);
clickedKey = key;
});
}
document.addEventListener("mouseup", () => {
stopKey(clickedKey);
});
⟩ ,
The gain is a unitless value, changing with time, that is multiplied to each corresponding sample of all input channels. If modified, the new gain is instantly applied, causing unaesthetic 'clicks' in the resulting audio. To prevent this from happening, never change the value directly but use the exponential interpolation methods on the interface. 📘
💾