With just a little knowledge of music theory, we can use ordinary HTML, CSS, and JavaScript (no libraries or audio samples required) to create a simple digital instrument. Let's put it into practice and explore a way to create a digital synthesizer that can be played and hosted on the internet.
Here is what we want to make:
We will use the AudioContext API to create our voice digitally without relying on samples. But first, let's deal with the appearance of the keyboard first.
HTML Structure
We will support a standard Western keyboard where each letter between A to; corresponds to a playable natural note (white key), while the lines above can be used for ascending and downing (black key). This means our keyboard covers a little more than one octave, starting at C₃ and ending at E₄. (For anyone who is not familiar with the score, the subscript numbers indicate the octave.)
One useful thing we can do is store the note value in a custom note property so that it can be easily accessed in our JavaScript. I will print letters on my computer keyboard to help our users understand what keys to press.
<code></code>
- A
- W
- S
- E
- D
- F
- T
- G
- Y
- H
- U
- J
- K
- O
- L
- P
- ;
CSS Style
We'll start with some boilerplate for our CSS:
<code>html { box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } body { margin: 0; }</code>
Let's specify the CSS variable for some of the colors we'll use. Feel free to change to any color you like!
<code>: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); }</code>
In particular, changing the --keyboard and --keyboard-border variables will greatly change the final result.
For style keys and keyboards – especially when pressed – I was inspired by this CodePen from zastrow. First, we specify the CSS for all key sharing:
<code>.white, .black { position: relative; float: left; display: flex; justify-content: center; align-items: flex-end; padding: 0.5rem 0; user-select: none; cursor: pointer; }</code>
Using specific rounded corners on the first and last keys helps make the design look more natural. Without rounded corners, the upper left and upper right corners of the key look a bit unnatural. This is a final design, minus any extra rounded corners on the first and last keys.
Let's add some CSS to improve this.
<code>#keyboard li:first-child { border-radius: 5px 0 5px 5px; } #keyboard li:last-child { border-radius: 0 5px 5px 5px; }</code>
The difference is subtle but effective:
Next, we apply styles that distinguish white and black keys. Note that the z-index for the white key is 1 and the z-index for the black key is 2:
<code>.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; margin: 0 0 0 -1rem; 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); }</code>
When we press the key, we will use JavaScript to add a "pressed" class to the relevant li element. Now we can test this by adding the class directly to our HTML element.
<code>.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; }</code>
Some white keys need to be moved left so that they are below the black keys. We assign these keys to the "offset" class in HTML so that we can keep CSS simple:
<code>.offset { margin: 0 0 0 -1rem; }</code>
If you've followed this CSS, you should have something like this:
Finally, we will style the keyboard itself:
<code>#keyboard { height: 15.25rem; width: 41rem; margin: 0.5rem auto; padding: 3rem 0 0 3rem; position: relative; border: 1px solid var(--keyboard-border); border-radius: 1rem; 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); }</code>
We now have a nice looking CSS keyboard, but it is not interactive and does not make any sound. To do this, we need JavaScript.
Music JavaScript
To create the synthesizer's sound effects, we don't want to rely on audio samples - that's cheating! Instead, we can use the AudioContext API of the network, which has tools that can help us convert digital waveforms into sound.
To create a new audio context we can use:
<code>const audioContext = new (window.AudioContext || window.webkitAudioContext)();</code>
Before using our audioContext, it will be helpful to select all the notes elements in HTML. We can easily query elements using this helper function:
<code>const getElementByNote = (note) => note && document.querySelector(`[note="${note}"]`);</code>
We can then store the element in an object where the key of the object is a key that the user presses the keyboard to play that note.
<code>const keys = { A: { element: getElementByNote("C"), note: "C", octaveOffset: 0 }, W: { element: getElementByNote("C#"), note: "C#", octaveOffset: 0 }, S: { element: getElementByNote("D"), note: "D", octaveOffset: 0 }, E: { element: getElementByNote("D#"), note: "D#", octaveOffset: 0 }, D: { element: getElementByNote("E"), note: "E", octaveOffset: 0 }, F: { element: getElementByNote("F"), note: "F", octaveOffset: 0 }, T: { element: getElementByNote("F#"), note: "F#", octaveOffset: 0 }, G: { element: getElementByNote("G"), note: "G", octaveOffset: 0 }, Y: { element: getElementByNote("G#"), note: "G#", octaveOffset: 0 }, H: { element: getElementByNote("A"), note: "A", octaveOffset: 1 }, U: { element: getElementByNote("A#"), note: "A#", octaveOffset: 1 }, J: { element: getElementByNote("B"), note: "B", octaveOffset: 1 }, K: { element: getElementByNote("C2"), note: "C", octaveOffset: 1 }, O: { element: getElementByNote("C#2"), note: "C#", octaveOffset: 1 }, L: { element: getElementByNote("D2"), note: "D", octaveOffset: 1 }, P: { element: getElementByNote("D#2"), note: "D#", octaveOffset: 1 }, semicolon: { element: getElementByNote("E2"), note: "E", octaveOffset: 1 } };</code>
I found it useful to specify the name of the notes here and octaveOffset, which is needed when calculating the pitch.
We need to provide a pitch in Hz. The equation used to determine the pitch is x * 2^(y / 12), where x is the Hz value of the selected note—usually A₄, with a pitch of 440Hz—and y is the number of notes above or below that pitch.
This gives us something like this in the code:
<code>const getHz = (note = "A", octave = 4) => { const A4 = 440; let N = 0; switch (note) { default: case "A": N = 0; break; case "A#": case "Bb": N = 1; break; case "B": N = 2; break; case "C": N = 3; break; case "C#": case "Db": N = 4; break; case "D": N = 5; break; case "D#": case "Eb": N = 6; break; case "E": N = 7; break; case "F": N = 8; break; case "F#": case "Gb": N = 9; break; case "G": N = 10; break; case "G#": case "Ab": N = 11; break; } N = 12 * (octave - 4); return A4 * Math.pow(2, N / 12); };</code>
Although we only use upshots in the rest of the code, I decided to include downs here as well so that this function can be easily reused in different contexts.
For anyone who is uncertain about the score, for example, notes A# and Bb describe exactly the same pitch. If we play in a specific tune, we might choose one over the other, but the difference is not important for our purposes.
Play notes
We're ready to start playing some notes!
First, we need some way to tell which notes are being played at any given time. Let's do this using Map, because its unique key constraints can help us prevent the same note from being triggered multiple times in a single press. Additionally, users can only click one key at a time, so we can store it as a string.
<code>const pressedNotes = new Map(); let clickedKey = "";</code>
We need two functions, one for the play key - we will fire when keydown or mousedown - the other for stopping play key - we will fire when keyup or mouseup.
Each key will play on its own oscillator and has its own gain node (for controlling volume) and its own waveform type (for determining the sound of the sound). I chose a Triangle waveform, but you can use any "sine", "triangle", "serrated", and "square wave" you prefer. The specification provides more information about these values.
<code>const playKey = (key) => { if (!keys[key]) { return; } const osc = audioContext.createOscillator(); const noteGainNode = audioContext.createGain(); noteGainNode.connect(audioContext.destination); noteGainNode.gain.value = 0.5; osc.connect(noteGainNode); osc.type = "triangle"; const freq = getHz(keys[key].note, (keys[key].octaveOffset || 0) 4); if (Number.isFinite(freq)) { osc.frequency.value = freq; } keys[key].element.classList.add("pressed"); pressedNotes.set(key, osc); pressedNotes.get(key).start(); };</code>
Our voice needs improvement. Currently, it has a slightly sharp microwave buzzer quality! But that's enough to start. We'll be back at the end to make some adjustments!
The stop key is an easier task. We need to have each note "continue" for a while (about two seconds) after the user lifts his finger and make the necessary visual changes.
<code>const stopKey = (key) => { if (!keys[key]) { return; } keys[key].element.classList.remove("pressed"); const osc = pressedNotes.get(key); if (osc) { setTimeout(() => { osc.stop(); }, 2000); pressedNotes.delete(key); } };</code>
All that's left is to add our event listener:
<code>document.addEventListener("keydown", (e) => { const eventKey = e.key.toUpperCase(); const key = eventKey === ";" ? "semicolon" : eventKey; if (!key || pressedNotes.get(key)) { return; } playKey(key); }); document.addEventListener("keyup", (e) => { const eventKey = e.key.toUpperCase(); const key = eventKey === ";" ? "semicolon" : eventKey; if (!key) { return; } stopKey(key); }); for (const [key, { element }] of Object.entries(keys)) { element.addEventListener("mousedown", () => { playKey(key); clickedKey = key; }); } document.addEventListener("mouseup", () => { stopKey(clickedKey); });</code>
Note that while most of our event listeners are added to the HTML document, we can use the keys object to add the click listener to the specific element we have already queryed. We also need to do some special treatment on our highest notes, making sure we convert the ";" key to "semicolon" of the spelling form we use in the keys object.
We can now play the keys on the synthesizer! There is only one problem. The sound is still harsh! We may want to lower the keyboard's octave by changing the expression we assign to the freq constant:
<code>const freq = getHz(keys[key].note, (keys[key].octaveOffset || 0) 3);</code>
You may also be able to hear the "click" sound at the beginning and end of the sound. We can solve this problem by fading in and fading out more gradually every sound.
In music production, we use the term attack to describe the time it takes for a sound to go from silent to maximum volume, and “release” to describe the time it takes for the sound to fade to silent after it stops playing. Another useful concept is attenuation , the time it takes for the sound to drop from peak volume to continuous volume. Thankfully, our noteGainNode has a gain property with a method called exponentialRampToValueAtTime which we can use to control attacks, releases, and attenuation. If we replace the previous playKey function with the following function, we will get better pick sounds:
<code>const playKey = (key) => { if (!keys[key]) { return; } const osc = audioContext.createOscillator(); const noteGainNode = audioContext.createGain(); noteGainNode.connect(audioContext.destination); const zeroGain = 0.00001; const maxGain = 0.5; const sustainedGain = 0.001; noteGainNode.gain.value = zeroGain; const setAttack = () => noteGainNode.gain.exponentialRampToValueAtTime( maxGain, audioContext.currentTime 0.01 ); const setDecay = () => noteGainNode.gain.exponentialRampToValueAtTime( sustainedGain, audioContext.currentTime 1 ); const setRelease = () => noteGainNode.gain.exponentialRampToValueAtTime( zeroGain, audioContext.currentTime 2 ); setAttack(); setDecay(); setRelease(); osc.connect(noteGainNode); osc.type = "triangle"; const freq = getHz(keys[key].note, (keys[key].octaveOffset || 0) - 1); if (Number.isFinite(freq)) { osc.frequency.value = freq; } keys[key].element.classList.add("pressed"); pressedNotes.set(key, osc); pressedNotes.get(key).start(); };</code>
At this point, we should have a working, network-ready synthesizer!
The numbers in our setAttack, setDecay, and setRelease functions seem a bit random, but they are actually just style choices. Try changing them and see what changes have occurred to the sound. You may end up getting the effect you prefer!
If you are interested in furthering the project, there are a number of ways you can improve it. Maybe it's a volume control, a way to switch between octaves, or a way to choose between waveforms? We can add reverb or low pass filters. Or maybe each sound can be composed of multiple oscillators?
For anyone interested in understanding how to implement the concept of music theory on the web, I recommend looking at the source code of the tonal npm package.
The above is the detailed content of How to Code a Playable Synth Keyboard. For more information, please follow other related articles on the PHP Chinese website!

There's been a run of tools, articles, and resources about color lately. Please allow me to close a few tabs by rounding them up here for your enjoyment.

Robin has covered this before, but I've heard some confusion about it in the past few weeks and saw another person take a stab at explaining it, and I wanted

I absolutely love the design of the Sandwich site. Among many beautiful features are these headlines with rainbow underlines that move as you scroll. It's not

Many popular resume designs are making the most of the available page space by laying sections out in a grid shape. Let’s use CSS Grid to create a layout that

Page reloads are a thing. Sometimes we refresh a page when we think it’s unresponsive, or believe that new content is available. Sometimes we’re just mad at

There is very little guidance on how to organize front-end applications in the world of React. (Just move files around until it “feels right,” lol). The truth

Most of the time you don’t really care about whether a user is actively engaged or temporarily inactive on your application. Inactive, meaning, perhaps they

Wufoo has always been great with integrations. They have integrations with specific apps, like Campaign Monitor, Mailchimp, and Typekit, but they also


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

AI Hentai Generator
Generate AI Hentai for free.

Hot Article

Hot Tools

mPDF
mPDF is a PHP library that can generate PDF files from UTF-8 encoded HTML. The original author, Ian Back, wrote mPDF to output PDF files "on the fly" from his website and handle different languages. It is slower than original scripts like HTML2FPDF and produces larger files when using Unicode fonts, but supports CSS styles etc. and has a lot of enhancements. Supports almost all languages, including RTL (Arabic and Hebrew) and CJK (Chinese, Japanese and Korean). Supports nested block-level elements (such as P, DIV),

WebStorm Mac version
Useful JavaScript development tools

VSCode Windows 64-bit Download
A free and powerful IDE editor launched by Microsoft

EditPlus Chinese cracked version
Small size, syntax highlighting, does not support code prompt function

MantisBT
Mantis is an easy-to-deploy web-based defect tracking tool designed to aid in product defect tracking. It requires PHP, MySQL and a web server. Check out our demo and hosting services.