<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple synth keyboard</title>
<style>
.container {
/*overflow-x: scroll;*/
/*overflow-y: hidden;*/
/*width: 460px;*/
/*height: 110px;*/
/*white-space: nowrap;*/
/*margin: 10px;*/
}
.keyboard {
width: auto;
padding: 0;
margin: 0;
}
.key {
cursor: pointer;
font: 16px "Open Sans", "Lucida Grande", "Arial", sans-serif;
border: 1px solid black;
border-radius: 5px;
width: 20px;
height: 80px;
text-align: center;
box-shadow: 2px 2px darkgray;
display: inline-block;
position: relative;
margin-right: 3px;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.key div {
position: absolute;
bottom: 0;
text-align: center;
width: 100%;
pointer-events: none;
}
.key div sub {
font-size: 10px;
pointer-events: none;
}
.key:hover {
background-color: #eef;
}
.key:active,
.active {
background-color: #000;
color: #fff;
}
.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: 30px;
}
.left {
width: 50%;
position: absolute;
left: 0;
display: table-cell;
vertical-align: middle;
}
.left span,
.left input {
vertical-align: middle;
}
.right {
/*width: 50%;*/
width: 500px;
position: absolute;
/*right: 0;*/
/*display: table-cell;*/
vertical-align: middle;
margin-top: 20px;
}
.right span {
vertical-align: middle;
}
.right input {
vertical-align: baseline;
}
</style>
</head>
<body onload="init()">
<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>
<option value="1.0" label="100%"></option>
</datalist>
</div>
</div>
<div class="right">
<span>Current waveform: </span>
<select name="waveform">
<option value="sine">Sine</option>
<option value="square" selected>Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle">Triangle</option>
<option value="custom">Custom</option>
</select>
</div>
<script>
const freqList = []
// SPN : scientific pitch notation
const spnList = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
const audioContext = new AudioContext()
const oscList = []
let mainGainNode = null
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
let synthKeys
const keyCodes = [
'Space',
'ShiftLeft', 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM', 'Comma',
'Period', 'Slash', 'ShiftRight',
'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon',
'Quote', 'Enter',
'Tab', 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
'BracketLeft', 'BracketRight',
'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8',
'Digit9', 'Digit0', 'Minus', 'Equal', 'Backspace',
'Escape'
]
function init() {
for (let i=1; i<89; i++) { // 88 key
freqList.push(calculateKeyFrequency(i))
}
setup()
addKeyEvent()
}
// standard piano with the 49th key tuned to A4 at 440 Hz
function calculateKeyFrequency(pianoKey) {
const num = (pianoKey - 49) / 12
return Math.pow(2, num) * 440
}
function createNoteTable() {
const noteFreq = []
for (let i=0; i<9; i++) { // 0 ~ 8 octave (Middle C : 4)
noteFreq[i] = []
}
noteFreq[0]['A'] = freqList[0]
noteFreq[0]['A#'] = freqList[1]
noteFreq[0]['B'] = freqList[2]
let index = 3
for (let i=1; i<8; i++) {
for (const spn of spnList) {
noteFreq[i][spn] = freqList[index]
index++
}
}
noteFreq[8]['C'] = freqList[index] // 87
return noteFreq
}
function setup() {
noteFreq = createNoteTable()
volumeControl.addEventListener('change', changeVolume, false)
mainGainNode = audioContext.createGain()
mainGainNode.connect(audioContext.destination)
mainGainNode.gain.value = volumeControl.value
// Create the keys; skip any that are sharp or flat; for
// our purposes we don't need them. Each octave is inserted
// into a <div> of class "octave".
noteFreq.forEach((keys, idx) => {
const keyList = Object.entries(keys)
const octaveElem = document.createElement('div')
octaveElem.className = 'octave'
keyList.forEach((key) => {
if (key[0].length === 1) { // no sharp or flat
octaveElem.appendChild(createKey(key[0], idx, key[1]))
}
})
keyboard.appendChild(octaveElem)
})
document.querySelector("div[data-note='B'][data-octave='5']").scrollIntoView(false)
sineTerms = new Float32Array([0, 0, 1, 0, 1])
cosineTerms = new Float32Array(sineTerms.length)
customWaveform = audioContext.createPeriodicWave(cosineTerms, sineTerms)
for (let i=0; i<9; i++) oscList[i] = {}
}
function createKey(note, octave, freq) {
const keyElement = document.createElement('div')
const labelElement = document.createElement('div')
keyElement.className = 'key'
keyElement.dataset['octave'] = octave
keyElement.dataset['note'] = note
keyElement.dataset['frequency'] = freq
labelElement.appendChild(document.createTextNode(note))
labelElement.appendChild(document.createElement('sub')).textContent = octave
keyElement.appendChild(labelElement)
keyElement.addEventListener('mousedown', notePressed, false)
keyElement.addEventListener('mouseup', noteReleased, false)
keyElement.addEventListener('mouseover', notePressed, false)
keyElement.addEventListener('mouseleave', noteReleased, false)
return keyElement
}
function playTone(freq) {
const osc = audioContext.createOscillator()
osc.connect(mainGainNode)
const type = wavePicker.options[wavePicker.selectedIndex].value
if (type === 'custom') {
osc.setPeriodicWave(customWaveform)
} else {
osc.type = type
}
osc.frequency.value = freq
osc.start()
return osc
}
function notePressed(e) {
if (e.buttons & 1) {
const dataset = e.target.dataset
if (!dataset['pressed'] && dataset['octave']) {
const octave = Number(dataset['octave'])
oscList[octave][dataset['note']] = playTone(dataset['frequency'])
dataset['pressed'] = 'yes'
}
}
}
function noteReleased(e) {
const dataset = e.target.dataset
if (dataset && dataset['pressed']) {
const octave = Number(dataset["octave"])
if (oscList[octave] && oscList[octave][dataset['note']]) {
oscList[octave][dataset['note']].stop()
delete oscList[octave][dataset['note']]
delete dataset['pressed']
}
}
}
function changeVolume(e) {
mainGainNode.gain.value = volumeControl.value
}
function addKeyEvent() {
synthKeys = document.querySelectorAll('.key')
addEventListener('keydown', keyNote)
addEventListener('keyup', keyNote)
}
function keyNote(e) {
const elKey = synthKeys[keyCodes.indexOf(e.code)]
if (elKey) {
if (e.type === 'keydown') {
elKey.tabIndex = -1
elKey.focus()
elKey.classList.add('active')
notePressed({buttons: 1, target: elKey})
} else {
elKey.classList.remove('active')
noteReleased({buttons: 1, target: elKey})
}
e.preventDefault()
}
}
</script>
</body>
</html>
const freqList = []
// SPN : scientific pitch notation
const spnList = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
https://en.wikipedia.org/wiki/Pitch_class
spnList index -> pitch class
// standard piano with the 49th key tuned to A4 at 440 Hz
function calculateKeyFrequency(pianoKey) {
const num = (pianoKey - 49) / 12
return Math.pow(2, num) * 440
}
https://en.wikipedia.org/wiki/Piano_key_frequencies
function init() {
for (let i=1; i<89; i++) { // 88 key
freqList.push(calculateKeyFrequency(i))
}
setup()
addKeyEvent()
}
function createNoteTable() {
const noteFreq = []
for (let i=0; i<9; i++) { // 0 ~ 8 octave (Middle C : 4)
noteFreq[i] = []
}
noteFreq[0]['A'] = freqList[0]
noteFreq[0]['A#'] = freqList[1]
noteFreq[0]['B'] = freqList[2]
let index = 3
for (let i=1; i<8; i++) {
for (const spn of spnList) {
noteFreq[i][spn] = freqList[index]
index++
}
}
noteFreq[8]['C'] = freqList[index] // 87
return noteFreq
}
noteFreq 관련된 내용이 생략되어서 추가
Refercences : https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Simple_synth
'Web Audio API' 카테고리의 다른 글
Web Audio API - Simple synth keyboard (0) | 2024.08.23 |
---|