Adjust frequency, hold note, better synth
This commit is contained in:
		
							
								
								
									
										35
									
								
								index.css
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								index.css
									
									
									
									
									
								
							| @@ -1,17 +1,19 @@ | |||||||
| .whiteKey { | .whiteKey { | ||||||
|     color: white; |     color: white; | ||||||
|  |     background-color: white; | ||||||
|     border: solid 1px; |     border: solid 1px; | ||||||
|     border-color: grey; |     border-color: grey; | ||||||
|  |  | ||||||
|     height: 67pt; |  | ||||||
| /* |  | ||||||
|     max-height: 60pt; |  | ||||||
|     max-width: 20pt; |  | ||||||
|     */ |  | ||||||
|     position: relative; |     position: relative; | ||||||
|     left: 7.5pt; |  | ||||||
|     width: 20pt; |  | ||||||
|     display: inline-block; |     display: inline-block; | ||||||
|  |     height: 67pt; | ||||||
|  |     width: 20pt; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .blackKeyOuter { | ||||||
|  |     position: relative; | ||||||
|  |     left: -7.5pt; | ||||||
|  |     z-index: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| .blackKey { | .blackKey { | ||||||
| @@ -19,12 +21,19 @@ | |||||||
|     background-color: black; |     background-color: black; | ||||||
|  |  | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     z-index: 1; |     display: inline-block; | ||||||
|     height: 40pt; |     height: 40pt; | ||||||
|     width: 15pt; |     width: 15pt; | ||||||
|     /* | } | ||||||
|     max-height: 40pt; |  | ||||||
|     max-width: 15pt; | .playing { | ||||||
|     */ |     background-color: lime; | ||||||
|     display: inline-block; | } | ||||||
|  |  | ||||||
|  | .held { | ||||||
|  |     background-color: gold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #keyboard { | ||||||
|  |     white-space: nowrap; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								index.html
									
									
									
									
									
								
							| @@ -11,8 +11,65 @@ | |||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <main class="container"> |     <main class="container"> | ||||||
|         <div id="keyboard"> |       <article> | ||||||
|         </div> |         <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> |     </main> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
							
								
								
									
										173
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								index.js
									
									
									
									
									
								
							| @@ -1,80 +1,142 @@ | |||||||
| const kNumKeys = 49; | const kNumKeys = 49; | ||||||
|  | const kMaxKeys = 88; | ||||||
| const kFirstKey = 'C2'; | const kFirstKey = 'C2'; | ||||||
| const kNotes = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B']; | const kNotes = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'G#', 'A', 'Bb', 'B']; | ||||||
| const kA0Frequency = 27.5; | const kA0Frequency = 27.5; | ||||||
| const kSemiToneMultiplier = Math.pow(2, 1/12); | const kSemiToneMultiplier = Math.pow(2, 1/12); | ||||||
|  |  | ||||||
| const audioCtx = new AudioContext(); | const audioCtx = new AudioContext(); | ||||||
|  | const frequencies = []; | ||||||
|  | const holds = []; | ||||||
|  |  | ||||||
| function playNote(frequency, duration) { | let globalVolume = 1.0; | ||||||
|     let currentGain = 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 bassBoost = f => { | ||||||
|         const t = f / 20000; |         const t = Math.pow(f / 20000, 1/10); | ||||||
|         const a = 4.0; |         const a = 1.0; | ||||||
|         const b = 0.2; |         const b = 0.05; | ||||||
|  |  | ||||||
|         return b * t + a * (1 - t); |         return b * t + a * (1 - t); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     const oscillators = []; | ||||||
|  |  | ||||||
|     while (frequency < 20000) { |     while (frequency < 20000) { | ||||||
|         const oscillator = new OscillatorNode(audioCtx); |         const oscillator = new OscillatorNode(audioCtx, {frequency}); | ||||||
|         const gain = new GainNode(audioCtx); |         const gain = new GainNode(audioCtx); | ||||||
|  |  | ||||||
|         oscillator.type = 'sine'; |         const g = bassBoost(frequency) * currentGain; | ||||||
|         oscillator.frequency.value = frequency; // value in hertz |  | ||||||
|         oscillator.connect(gain); |  | ||||||
|         gain.gain.setValueAtTime(0, audioCtx.currentTime); |         gain.gain.setValueAtTime(0, audioCtx.currentTime); | ||||||
|         gain.gain.exponentialRampToValueAtTime(bassBoost(frequency) * currentGain, audioCtx.currentTime + 0.010); |         gain.gain.linearRampToValueAtTime(g, audioCtx.currentTime + 0.010); | ||||||
|         gain.gain.linearRampToValueAtTime(1e-10, audioCtx.currentTime + 0.500); |         gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration); | ||||||
|         gain.connect(audioCtx.destination); |  | ||||||
|  |         oscillator.connect(gain).connect(audioCtx.destination); | ||||||
|         oscillator.start(); |         oscillator.start(); | ||||||
|  |  | ||||||
|         setTimeout(() => {oscillator.stop();}, duration); |         oscillators.push(oscillator); | ||||||
|  |  | ||||||
|  |        // break;  // only one frequency for now | ||||||
|  |  | ||||||
|         frequency *= 2; |         frequency *= 2; | ||||||
|         currentGain *= 0.3; |         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)/); |     let [note, octave, ] = key.split(/(\d)/); | ||||||
|     octave = parseInt(octave); |     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) { | function onKeyClick(event) { | ||||||
|     const note = event.srcElement.id; |     const keySpan = event.srcElement; | ||||||
|     const freq = noteFrequency(note); |     const key = keySpan.id; | ||||||
|     playNote(freq, 500); |     playNote(noteFrequency(key), 0.5); | ||||||
|  |     updateOptions(key); | ||||||
|  |  | ||||||
|  |     keySpan.classList.add('playing'); | ||||||
|  |     setTimeout(() => { keySpan.classList.remove('playing'); }, 500) | ||||||
| } | } | ||||||
|  |  | ||||||
| function makeKey(note) { | function makeKey(note) { | ||||||
|     const key = document.createElement('span'); |     const key = document.createElement('span'); | ||||||
|     if (note.includes('#') || note.includes('b')) { |  | ||||||
|         key.classList.add('blackKey'); |  | ||||||
|     } else { |  | ||||||
|         key.classList.add('whiteKey'); |  | ||||||
|     } |  | ||||||
|     key.id = note; |     key.id = note; | ||||||
|     key.addEventListener('click', onKeyClick); |     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; |     return key; | ||||||
| } | } | ||||||
|  |  | ||||||
| function setupKeyboard() { | function setupKeyboard(firstKey=kFirstKey, numKeys=kNumKeys) { | ||||||
|     const keyboard = document.getElementById('keyboard'); |     const keyboard = document.getElementById('keyboard'); | ||||||
|     keyboard.innerHTML = ''; |     keyboard.innerHTML = ''; | ||||||
|  |  | ||||||
|     let curIndex = kNotes.indexOf(kFirstKey.charAt(0)); |     let curIndex = kNotes.indexOf(firstKey.charAt(0)); | ||||||
|     let octave = parseInt(kFirstKey.charAt(1)); |     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 note = kNotes[curIndex] + octave; | ||||||
|         const key = makeKey(note) |         const key = makeKey(note) | ||||||
|         keyboard.appendChild(key); |         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 => { | window.addEventListener('load', e => { | ||||||
|     setupKeyboard(); |     setupKeyboard('C2', 49); | ||||||
|  |     tuneEqual(); | ||||||
|  |     setupListeners(); | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user