Table of Contents
HTML Structure
CSS Style
Music JavaScript
Play notes
Home Web Front-end CSS Tutorial How to Code a Playable Synth Keyboard

How to Code a Playable Synth Keyboard

Mar 21, 2025 am 10:19 AM

How to Code a Playable Synth Keyboard

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>
Copy after login
  • 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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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>
Copy after login

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!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1664
14
PHP Tutorial
1266
29
C# Tutorial
1239
24
Google Fonts   Variable Fonts Google Fonts Variable Fonts Apr 09, 2025 am 10:42 AM

I see Google Fonts rolled out a new design (Tweet). Compared to the last big redesign, this feels much more iterative. I can barely tell the difference

How to Create an Animated Countdown Timer With HTML, CSS and JavaScript How to Create an Animated Countdown Timer With HTML, CSS and JavaScript Apr 11, 2025 am 11:29 AM

Have you ever needed a countdown timer on a project? For something like that, it might be natural to reach for a plugin, but it’s actually a lot more

HTML Data Attributes Guide HTML Data Attributes Guide Apr 11, 2025 am 11:50 AM

Everything you ever wanted to know about data attributes in HTML, CSS, and JavaScript.

A Proof of Concept for Making Sass Faster A Proof of Concept for Making Sass Faster Apr 16, 2025 am 10:38 AM

At the start of a new project, Sass compilation happens in the blink of an eye. This feels great, especially when it’s paired with Browsersync, which reloads

How We Created a Static Site That Generates Tartan Patterns in SVG How We Created a Static Site That Generates Tartan Patterns in SVG Apr 09, 2025 am 11:29 AM

Tartan is a patterned cloth that’s typically associated with Scotland, particularly their fashionable kilts. On tartanify.com, we gathered over 5,000 tartan

How to Build Vue Components in a WordPress Theme How to Build Vue Components in a WordPress Theme Apr 11, 2025 am 11:03 AM

The inline-template directive allows us to build rich Vue components as a progressive enhancement over existing WordPress markup.

PHP is A-OK for Templating PHP is A-OK for Templating Apr 11, 2025 am 11:04 AM

PHP templating often gets a bad rap for facilitating subpar code — but that doesn&#039;t have to be the case. Let’s look at how PHP projects can enforce a basic

A Comparison of Static Form Providers A Comparison of Static Form Providers Apr 16, 2025 am 11:20 AM

Let’s attempt to coin a term here: "Static Form Provider." You bring your HTML

See all articles