Adjust frequency, hold note, better synth
This commit is contained in:
		
							
								
								
									
										35
									
								
								index.css
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								index.css
									
									
									
									
									
								
							@@ -1,17 +1,19 @@
 | 
			
		||||
.whiteKey {
 | 
			
		||||
    color: white;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    border: solid 1px;
 | 
			
		||||
    border-color: grey;
 | 
			
		||||
 | 
			
		||||
    height: 67pt;
 | 
			
		||||
/*
 | 
			
		||||
    max-height: 60pt;
 | 
			
		||||
    max-width: 20pt;
 | 
			
		||||
    */
 | 
			
		||||
    position: relative;
 | 
			
		||||
    left: 7.5pt;
 | 
			
		||||
    width: 20pt;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    height: 67pt;
 | 
			
		||||
    width: 20pt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blackKeyOuter {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    left: -7.5pt;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blackKey {
 | 
			
		||||
@@ -19,12 +21,19 @@
 | 
			
		||||
    background-color: black;
 | 
			
		||||
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    height: 40pt;
 | 
			
		||||
    width: 15pt;
 | 
			
		||||
    /*
 | 
			
		||||
    max-height: 40pt;
 | 
			
		||||
    max-width: 15pt;
 | 
			
		||||
    */
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.playing {
 | 
			
		||||
    background-color: lime;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.held {
 | 
			
		||||
    background-color: gold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#keyboard {
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								index.html
									
									
									
									
									
								
							@@ -11,8 +11,65 @@
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <main class="container">
 | 
			
		||||
        <div id="keyboard">
 | 
			
		||||
        </div>
 | 
			
		||||
      <article>
 | 
			
		||||
        <div id="keyboard" class="overflow-auto"></div>
 | 
			
		||||
        <label>
 | 
			
		||||
          Volume
 | 
			
		||||
          <input type="range" min="0.0" max="2.0" value="1.0" step="0.01" id="range">
 | 
			
		||||
        </label>
 | 
			
		||||
      </article>
 | 
			
		||||
      <section id="options">
 | 
			
		||||
        <form>
 | 
			
		||||
          <article>
 | 
			
		||||
            <h3 id="current-note">-</h3>
 | 
			
		||||
            <label>
 | 
			
		||||
              <input type="checkbox" role="switch" id="hold">
 | 
			
		||||
              Hold
 | 
			
		||||
            </label>
 | 
			
		||||
          </article>
 | 
			
		||||
          <article>
 | 
			
		||||
            <label>
 | 
			
		||||
              <small>Frequency</small>
 | 
			
		||||
              <input type="number" id="current-frequency" step="0.01">
 | 
			
		||||
            </label>
 | 
			
		||||
          </article>
 | 
			
		||||
          <article>
 | 
			
		||||
            <fieldset>
 | 
			
		||||
              <legend><small>Tuning</small></legend>
 | 
			
		||||
              <label>
 | 
			
		||||
                <input type="radio" name="tuning">
 | 
			
		||||
                Equal temperament
 | 
			
		||||
              </label>
 | 
			
		||||
              <label>
 | 
			
		||||
                <input type="radio" name="tuning">
 | 
			
		||||
                Perfect 5th
 | 
			
		||||
              </label>
 | 
			
		||||
              <label>
 | 
			
		||||
                <input type="radio" name="tuning">
 | 
			
		||||
                Perfect 4th
 | 
			
		||||
              </label>
 | 
			
		||||
              <label>
 | 
			
		||||
                <input type="radio" name="tuning">
 | 
			
		||||
                Perfect major 3rd
 | 
			
		||||
              </label>
 | 
			
		||||
              <label>
 | 
			
		||||
                <input type="radio" name="tuning">
 | 
			
		||||
                Perfect minor 3rd
 | 
			
		||||
              </label>
 | 
			
		||||
              <div id="tune-from" style="display: none">
 | 
			
		||||
                <label>
 | 
			
		||||
                  From:
 | 
			
		||||
                  <select name="tuning-from">
 | 
			
		||||
                    <option selected disabled value></option>
 | 
			
		||||
                    <option>C2</option>
 | 
			
		||||
                  </select>
 | 
			
		||||
                </label>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button>Tune</button>
 | 
			
		||||
            </fieldset>
 | 
			
		||||
          </article>
 | 
			
		||||
        </form>
 | 
			
		||||
      </section>
 | 
			
		||||
    </main>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										173
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								index.js
									
									
									
									
									
								
							@@ -1,80 +1,142 @@
 | 
			
		||||
const kNumKeys = 49;
 | 
			
		||||
const kMaxKeys = 88;
 | 
			
		||||
const kFirstKey = 'C2';
 | 
			
		||||
const kNotes = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B'];
 | 
			
		||||
const kA0Frequency = 27.5;
 | 
			
		||||
const kSemiToneMultiplier = Math.pow(2, 1/12);
 | 
			
		||||
 | 
			
		||||
const audioCtx = new AudioContext();
 | 
			
		||||
const frequencies = [];
 | 
			
		||||
const holds = [];
 | 
			
		||||
 | 
			
		||||
function playNote(frequency, duration) {
 | 
			
		||||
    let currentGain = 1.0;
 | 
			
		||||
let globalVolume = 1.0;
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * TODO:
 | 
			
		||||
 * - smooth frequency adjustments
 | 
			
		||||
 * - auto octave update
 | 
			
		||||
 * - implement perfect 5/4/3M/3m
 | 
			
		||||
 * - keyboard playing (qwerty)
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function playNote(frequency, duration=86400) {
 | 
			
		||||
    let currentGain = globalVolume;
 | 
			
		||||
 | 
			
		||||
    const bassBoost = f => {
 | 
			
		||||
        const t = f / 20000;
 | 
			
		||||
        const a = 4.0;
 | 
			
		||||
        const b = 0.2;
 | 
			
		||||
        const t = Math.pow(f / 20000, 1/10);
 | 
			
		||||
        const a = 1.0;
 | 
			
		||||
        const b = 0.05;
 | 
			
		||||
 | 
			
		||||
        return b * t + a * (1 - t);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const oscillators = [];
 | 
			
		||||
 | 
			
		||||
    while (frequency < 20000) {
 | 
			
		||||
        const oscillator = new OscillatorNode(audioCtx);
 | 
			
		||||
        const oscillator = new OscillatorNode(audioCtx, {frequency});
 | 
			
		||||
        const gain = new GainNode(audioCtx);
 | 
			
		||||
 | 
			
		||||
        oscillator.type = 'sine';
 | 
			
		||||
        oscillator.frequency.value = frequency; // value in hertz
 | 
			
		||||
        oscillator.connect(gain);
 | 
			
		||||
        const g = bassBoost(frequency) * currentGain;
 | 
			
		||||
        gain.gain.setValueAtTime(0, audioCtx.currentTime);
 | 
			
		||||
        gain.gain.exponentialRampToValueAtTime(bassBoost(frequency) * currentGain, audioCtx.currentTime + 0.010);
 | 
			
		||||
        gain.gain.linearRampToValueAtTime(1e-10, audioCtx.currentTime + 0.500);
 | 
			
		||||
        gain.connect(audioCtx.destination);
 | 
			
		||||
        gain.gain.linearRampToValueAtTime(g, audioCtx.currentTime + 0.010);
 | 
			
		||||
        gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration);
 | 
			
		||||
 | 
			
		||||
        oscillator.connect(gain).connect(audioCtx.destination);
 | 
			
		||||
        oscillator.start();
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {oscillator.stop();}, duration);
 | 
			
		||||
        oscillators.push(oscillator);
 | 
			
		||||
 | 
			
		||||
       // break;  // only one frequency for now
 | 
			
		||||
 | 
			
		||||
        frequency *= 2;
 | 
			
		||||
        currentGain *= 0.3;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const stop = () => {
 | 
			
		||||
        for (let oscillator of oscillators) {
 | 
			
		||||
            oscillator.stop();
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    setTimeout(stop, duration*1000);
 | 
			
		||||
 | 
			
		||||
    return stop;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function noteFrequency(key) {
 | 
			
		||||
function keyIndex(key) {
 | 
			
		||||
    let [note, octave, ] = key.split(/(\d)/);
 | 
			
		||||
    octave = parseInt(octave);
 | 
			
		||||
 | 
			
		||||
    const interval = kNotes.indexOf(note) - kNotes.indexOf('A') + octave * 12;
 | 
			
		||||
    return kNotes.indexOf(note) - kNotes.indexOf('A') + octave * 12;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    let freq = kA0Frequency * Math.pow(kSemiToneMultiplier, interval);
 | 
			
		||||
function noteFrequency(key) {
 | 
			
		||||
    return frequencies[keyIndex(key)];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    return freq;
 | 
			
		||||
function setFrequency(key, frequency) {
 | 
			
		||||
    frequencies[keyIndex(key)] = frequency;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function tuneEqual() {
 | 
			
		||||
    const startFrequency = kA0Frequency;
 | 
			
		||||
    let frequency = startFrequency;
 | 
			
		||||
    for (let i = 0; i < kMaxKeys; i++) {
 | 
			
		||||
        frequencies[i] = frequency;
 | 
			
		||||
        frequency *= kSemiToneMultiplier;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateOptions(key) {
 | 
			
		||||
    const noteSpan = document.getElementById("current-note");
 | 
			
		||||
    const frequencySpan = document.getElementById("current-frequency");
 | 
			
		||||
    noteSpan.innerHTML = key;
 | 
			
		||||
    frequencySpan.value = noteFrequency(key).toFixed(2);
 | 
			
		||||
    const index = keyIndex(key);
 | 
			
		||||
    const hold = document.getElementById("hold");
 | 
			
		||||
    if (holds[index] !== undefined) {
 | 
			
		||||
        hold.checked = true;
 | 
			
		||||
    } else {
 | 
			
		||||
        hold.checked = false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onKeyClick(event) {
 | 
			
		||||
    const note = event.srcElement.id;
 | 
			
		||||
    const freq = noteFrequency(note);
 | 
			
		||||
    playNote(freq, 500);
 | 
			
		||||
    const keySpan = event.srcElement;
 | 
			
		||||
    const key = keySpan.id;
 | 
			
		||||
    playNote(noteFrequency(key), 0.5);
 | 
			
		||||
    updateOptions(key);
 | 
			
		||||
 | 
			
		||||
    keySpan.classList.add('playing');
 | 
			
		||||
    setTimeout(() => { keySpan.classList.remove('playing'); }, 500)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeKey(note) {
 | 
			
		||||
    const key = document.createElement('span');
 | 
			
		||||
    if (note.includes('#') || note.includes('b')) {
 | 
			
		||||
        key.classList.add('blackKey');
 | 
			
		||||
    } else {
 | 
			
		||||
        key.classList.add('whiteKey');
 | 
			
		||||
    }
 | 
			
		||||
    key.id = note;
 | 
			
		||||
    key.addEventListener('click', onKeyClick);
 | 
			
		||||
 | 
			
		||||
    if (note.includes('#') || note.includes('b')) {
 | 
			
		||||
        key.classList.add('blackKey');
 | 
			
		||||
        const container = document.createElement('span');
 | 
			
		||||
        container.classList.add('blackKeyOuter');
 | 
			
		||||
        container.appendChild(key);
 | 
			
		||||
 | 
			
		||||
        return container;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    key.classList.add('whiteKey');
 | 
			
		||||
    return key;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupKeyboard() {
 | 
			
		||||
function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) {
 | 
			
		||||
    const keyboard = document.getElementById('keyboard');
 | 
			
		||||
    keyboard.innerHTML = '';
 | 
			
		||||
 | 
			
		||||
    let curIndex = kNotes.indexOf(kFirstKey.charAt(0));
 | 
			
		||||
    let octave = parseInt(kFirstKey.charAt(1));
 | 
			
		||||
    let curIndex = kNotes.indexOf(firstKey.charAt(0));
 | 
			
		||||
    let octave = parseInt(firstKey.charAt(1));
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < kNumKeys; i++) {
 | 
			
		||||
    for (let i = 0; i < numKeys; i++) {
 | 
			
		||||
        const note = kNotes[curIndex] + octave;
 | 
			
		||||
        const key = makeKey(note)
 | 
			
		||||
        keyboard.appendChild(key);
 | 
			
		||||
@@ -87,6 +149,57 @@ function setupKeyboard() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addHold(key) {
 | 
			
		||||
    holds[keyIndex(key)] = playNote(noteFrequency(key));
 | 
			
		||||
    document.getElementById(key).classList.add('held');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeHold(key) {
 | 
			
		||||
    const index = keyIndex(key);
 | 
			
		||||
    if (holds[index] !== undefined) {
 | 
			
		||||
        holds[index]();
 | 
			
		||||
    }
 | 
			
		||||
    delete holds[index];
 | 
			
		||||
    document.getElementById(key).classList.remove('held');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function refreshHolds() {
 | 
			
		||||
    for (let i = 0; i < holds.length; i++) {
 | 
			
		||||
        if (holds[i] !== undefined) {
 | 
			
		||||
            holds[i]();
 | 
			
		||||
            holds[i] = playNote(frequencies[i]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupListeners() {
 | 
			
		||||
    const freq = document.getElementById("current-frequency");
 | 
			
		||||
    freq.onchange = e => {
 | 
			
		||||
        const key = document.getElementById("current-note").innerHTML;
 | 
			
		||||
        setFrequency(key, parseFloat(freq.value));
 | 
			
		||||
        refreshHolds();
 | 
			
		||||
    };
 | 
			
		||||
    freq.oninput = freq.onchange;
 | 
			
		||||
 | 
			
		||||
    const hold = document.getElementById("hold");
 | 
			
		||||
    hold.onchange = e => {
 | 
			
		||||
        const key = document.getElementById("current-note").innerHTML;
 | 
			
		||||
        if (hold.checked) {
 | 
			
		||||
            addHold(key);
 | 
			
		||||
        } else {
 | 
			
		||||
            removeHold(key);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    const range = document.getElementById("range");
 | 
			
		||||
    range.onchange = e => {
 | 
			
		||||
        const val = parseFloat(range.value);
 | 
			
		||||
        globalVolume = val;
 | 
			
		||||
        refreshHolds();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener('load', e => {
 | 
			
		||||
    setupKeyboard();
 | 
			
		||||
    setupKeyboard('C2', 49);
 | 
			
		||||
    tuneEqual();
 | 
			
		||||
    setupListeners();
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user