본문 바로가기

Web Audio API

Web Audio API - Simple synth keyboard source code

<!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

 

Pitch class - Wikipedia

From Wikipedia, the free encyclopedia Set of all pitches that are a whole number of octaves apart In music, a pitch class (p.c. or pc) is a set of all pitches that are a whole number of octaves apart; for example, the pitch class C consists of the Cs in al

en.wikipedia.org

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

 

Piano key frequencies - Wikipedia

From Wikipedia, the free encyclopedia This is a list of the fundamental frequencies in hertz (cycles per second) of the keys of a modern 88-key standard or 108-key extended piano in twelve-tone equal temperament, with the 49th key, the fifth A (called A4),

en.wikipedia.org

 

 

 

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