With a bit of data of music concept, we are able to use common HTML, CSS and JavaScript — with none libraries or audio samples — to create a easy digital instrument. Let’s put that into apply and discover one technique for making a digital synth that may be performed and hosted on the web.
Right here’s what we’re making:
We’ll use the AudioContext API to create our sounds digitally, with out resorting to samples. However first, let’s work on the keyboard’s look.
The HTML construction
We’re going to assist a normal western keyboard the place each letter between A and ; corresponds to a playable pure notice (the white keys), whereas the row above can be utilized for the sharps and flats (the black keys). This implies our keyboard covers simply over an octave, beginning at C₃ and ending at E₄. (For anybody unfamiliar with musical notation, the subscript numbers point out the octave.)
One helpful factor we are able to do is retailer the notice worth in a customized notice attribute so it’s simple to entry in our JavaScript. I’ll print the letters of the pc keyboard, to assist our customers perceive what to press.
<ul id=”keyboard”>
<li notice=”C” class=”white”>A</li>
<li notice=”C#” class=”black”>W</li>
<li notice=”D” class=”white offset”>S</li>
<li notice=”D#” class=”black”>E</li>
<li notice=”E” class=”white offset”>D</li>
<li notice=”F” class=”white”>F</li>
<li notice=”F#” class=”black”>T</li>
<li notice=”G” class=”white offset”>G</li>
<li notice=”G#” class=”black”>Y</li>
<li notice=”A” class=”white offset”>H</li>
<li notice=”A#” class=”black”>U</li>
<li notice=”B” class=”white offset”>J</li>
<li notice=”C2″ class=”white”>Okay</li>
<li notice=”C#2″ class=”black”>O</li>
<li notice=”D2″ class=”white offset”>L</li>
<li notice=”D#2″ class=”black”>P</li>
<li notice=”E2″ class=”white offset”>;</li>
</ul>
The CSS styling
We’ll start our CSS with some boilerplate:
html {
box-sizing: border-box;
}
*,
*:earlier than,
*:after {
box-sizing: inherit;
font-family: -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, Oxygen,
Ubuntu, Cantarell, “Open Sans”, “Helvetica Neue”, sans-serif;
}
physique {
margin: 0;
}
Let’s specify CSS variables for a number of the colours we’ll be utilizing. Be happy to vary them to no matter you like!
: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);
}
Particularly, altering the –keyboard and –keyboard-border variables will change the top end result dramatically.
For styling the keys and the keyboard — particularly within the pressed states — I owe a whole lot of my inspiration to this CodePen by zastrow. First, we specify the CSS shared by all of the keys:
.white,
.black {
place: relative;
float: left;
show: flex;
justify-content: heart;
align-items: flex-end;
padding: 0.5rem 0;
user-select: none;
cursor: pointer;
}
Utilizing a particular border radius on the primary and final key helps make the design look extra natural. With out rounding, the highest left and high proper corners of keys look a bit of unnatural. Right here’s a remaining design, minus any additional rounding on the primary and final keys.
Let’s add some CSS to enhance this.
#keyboard li:first-child {
border-radius: 5px 0 5px 5px;
}
#keyboard li:last-child {
border-radius: 0 5px 5px 5px;
}
The distinction is refined however efficient:
Subsequent, we apply the stylings that differentiate the white and black keys. Discover that the white keys have a z-index of 1 and the black keys have a z-index of two:
.white {
top: 12.5rem;
width: 3.5rem;
z-index: 1;
border-left: 1px stable hsl(0, 0%, 73%);
border-bottom: 1px stable 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 backside, hsl(0, 0%, 93%) 0%, white 100%);
colour: var(–black-30);
}
.black {
top: 8rem;
width: 2rem;
margin: 0 0 0 -1rem;
z-index: 2;
border: 1px stable 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%);
colour: var(–white-50);
}
When a key’s pressed, we’ll use JavaScript so as to add a category of “pressed” to the related li factor. For now, we are able to take a look at this by including the category on to our HTML parts.
.white.pressed {
border-top: 1px stable hsl(0, 0%, 47%);
border-left: 1px stable hsl(0, 0%, 60%);
border-bottom: 1px stable 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 backside, white 0%, hsl(0, 0%, 91%) 100%);
define: 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 proper,
hsl(0, 0%, 27%) 0%,
hsl(0, 0%, 13%) 100%
);
define: none;
}
Sure white keys have to be moved towards the left in order that they sit beneath the black keys. We give these a category of “offset” in our HTML, so we are able to hold the CSS easy:
.offset {
margin: 0 0 0 -1rem;
}
When you’ve adopted the CSS up up to now, you must have one thing like this:
Lastly, we’ll fashion the keyboard itself:
#keyboard {
top: 15.25rem;
width: 41rem;
margin: 0.5rem auto;
padding: 3rem 0 0 3rem;
place: relative;
border: 1px stable 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);
}
We now have a nice-looking CSS keyboard, however it’s not interactive and it doesn’t make any sounds. To do that, we’ll want JavaScript.
Musical JavaScript
To create the sounds for our synth, we don’t need to depend on audio samples — that’d be dishonest! As an alternative, we are able to use the net’s AudioContext API, which has instruments that may assist us flip digital waveforms into sounds.
To create a brand new audio context, we are able to use:
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
Earlier than utilizing our audioContext it will likely be useful to pick all our notice parts within the HTML. We will use this helper to simply question the weather:
const getElementByNote = (notice) =>
notice && doc.querySelector(`[note=”${note}”]`);
We will then retailer the weather in an object, the place the important thing of the article is the important thing {that a} person would press on the keyboard to play that notice.
const keys = {
A: { factor: getElementByNote(“C”), notice: “C”, octaveOffset: 0 },
W: { factor: getElementByNote(“C#”), notice: “C#”, octaveOffset: 0 },
S: { factor: getElementByNote(“D”), notice: “D”, octaveOffset: 0 },
E: { factor: getElementByNote(“D#”), notice: “D#”, octaveOffset: 0 },
D: { factor: getElementByNote(“E”), notice: “E”, octaveOffset: 0 },
F: { factor: getElementByNote(“F”), notice: “F”, octaveOffset: 0 },
T: { factor: getElementByNote(“F#”), notice: “F#”, octaveOffset: 0 },
G: { factor: getElementByNote(“G”), notice: “G”, octaveOffset: 0 },
Y: { factor: getElementByNote(“G#”), notice: “G#”, octaveOffset: 0 },
H: { factor: getElementByNote(“A”), notice: “A”, octaveOffset: 1 },
U: { factor: getElementByNote(“A#”), notice: “A#”, octaveOffset: 1 },
J: { factor: getElementByNote(“B”), notice: “B”, octaveOffset: 1 },
Okay: { factor: getElementByNote(“C2”), notice: “C”, octaveOffset: 1 },
O: { factor: getElementByNote(“C#2”), notice: “C#”, octaveOffset: 1 },
L: { factor: getElementByNote(“D2”), notice: “D”, octaveOffset: 1 },
P: { factor: getElementByNote(“D#2”), notice: “D#”, octaveOffset: 1 },
semicolon: { factor: getElementByNote(“E2”), notice: “E”, octaveOffset: 1 }
};
I discovered it helpful to specify the identify of the notice right here, in addition to an octaveOffset, which we’ll want when understanding the pitch.
We have to provide a pitch in Hz. The equation used to find out pitch is x * 2^(y / 12) the place x is the Hz worth of a selected notice — normally A₄, which has a pitch of 440Hz — and y is the variety of notes above or under that pitch.
That provides us one thing like this in code:
const getHz = (notice = “A”, octave = 4) => {
const A4 = 440;
let N = 0;
swap (notice) {
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);
};
Though we’re solely utilizing sharps in the remainder of our code, I made a decision to incorporate flats right here as properly, so this operate may simply be re-used in a distinct context.
For anybody who’s not sure about musical notation, the notes A# and Bb, for instance, describe the very same pitch. We would select one over one other if we’re taking part in in a specific key, however for our functions, the distinction doesn’t matter.
Enjoying notes
We’re prepared to begin taking part in some notes!
First, we’d like a way of telling which notes are taking part in at any given time. Let’s use a Map to do that, as its distinctive key constraint may help forestall us from triggering the identical notice a number of occasions in a single press. Plus, a person can solely click on one key at a time, so we are able to retailer that as a string.
const pressedNotes = new Map();
let clickedKey = “”;
We want two features, one to play a key — which we’ll set off on keydown or mousedown — and one other to cease taking part in the important thing — which we’ll set off on keyup or mouseup.
Every key will probably be performed by itself oscillator with its personal acquire node (used to regulate the quantity) and its personal waveform kind (used to find out the timbre of the sound). I’m choosing a “triangle” waveform, however you should use no matter you like of “sine”, “triangle”, “sawtooth” and “sq.”. The spec presents a bit of extra data on these values.
const playKey = (key) => {
if (!keys[key]) {
return;
}
const osc = audioContext.createOscillator();
const noteGainNode = audioContext.createGain();
noteGainNode.join(audioContext.vacation spot);
noteGainNode.acquire.worth = 0.5;
osc.join(noteGainNode);
osc.kind = “triangle”;
const freq = getHz(keys[key].notice, (keys[key].octaveOffset || 0) + 4);
if (Quantity.isFinite(freq)) {
osc.frequency.worth = freq;
}
keys[key].factor.classList.add(“pressed”);
pressedNotes.set(key, osc);
pressedNotes.get(key).begin();
};
Our sound may do with some refinement. In the mean time, is has a barely piercing, microwave-buzzer high quality to it! However this is sufficient to get began. We’ll come again and make some tweaks on the finish!
Stopping a key’s a less complicated process. We have to let every notice “ring out” for an period of time after the person lifts their finger (two seconds is about proper), in addition to make the required visible change.
const stopKey = (key) => {
if (!keys[key]) {
return;
}
keys[key].factor.classList.take away(“pressed”);
const osc = pressedNotes.get(key);
if (osc) {
setTimeout(() => {
osc.cease();
}, 2000);
pressedNotes.delete(key);
}
};
All that’s left is so as to add our occasion listeners:
doc.addEventListener(“keydown”, (e) => {
const eventKey = e.key.toUpperCase();
const key = eventKey === “;” ? “semicolon” : eventKey;
if (!key || pressedNotes.get(key)) {
return;
}
playKey(key);
});
doc.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)) {
factor.addEventListener(“mousedown”, () => {
playKey(key);
clickedKey = key;
});
}
doc.addEventListener(“mouseup”, () => {
stopKey(clickedKey);
});
Observe that, whereas most of our occasion listeners are added to the HTML doc, we are able to use our keys object so as to add click on listeners to the precise parts we have now already queried. We additionally want to offer some particular remedy to our highest notice, ensuring we convert the “;” key into the spelled-out “semicolon” utilized in our keys object.
We will now play the keys on our synth! There’s only one downside. The sound remains to be fairly shrill! We would need to knock down the octave of the keyboard by altering the expression that we assign to the freq fixed:
const freq = getHz(keys[key].notice, (keys[key].octaveOffset || 0) + 3);
You may also be capable of hear a “click on” at the start and finish of the sound. We will clear up this by shortly fading in and extra regularly fading out of every sound.
In music manufacturing, we use the time period assault to explain how shortly a sound goes from nothing to its most quantity, and “launch” to explain how lengthy it takes for a sound to fade to nothing as soon as it’s now not performed. One other helpful idea is decay, the the time taken for sound to go from its peak quantity to its sustained quantity. Fortunately, our noteGainNode has a acquire property with a way referred to as exponentialRampToValueAtTime, which we are able to use to regulate assault, launch and decay. If we substitute our earlier playKey operate with the next one, we’ll get a a lot nicer plucky sound:
const playKey = (key) => {
if (!keys[key]) {
return;
}
const osc = audioContext.createOscillator();
const noteGainNode = audioContext.createGain();
noteGainNode.join(audioContext.vacation spot);
const zeroGain = 0.00001;
const maxGain = 0.5;
const sustainedGain = 0.001;
noteGainNode.acquire.worth = zeroGain;
const setAttack = () =>
noteGainNode.acquire.exponentialRampToValueAtTime(
maxGain,
audioContext.currentTime + 0.01
);
const setDecay = () =>
noteGainNode.acquire.exponentialRampToValueAtTime(
sustainedGain,
audioContext.currentTime + 1
);
const setRelease = () =>
noteGainNode.acquire.exponentialRampToValueAtTime(
zeroGain,
audioContext.currentTime + 2
);
setAttack();
setDecay();
setRelease();
osc.join(noteGainNode);
osc.kind = “triangle”;
const freq = getHz(keys[key].notice, (keys[key].octaveOffset || 0) – 1);
if (Quantity.isFinite(freq)) {
osc.frequency.worth = freq;
}
keys[key].factor.classList.add(“pressed”);
pressedNotes.set(key, osc);
pressedNotes.get(key).begin();
};
We should always have a working, web-ready synth at this level!
The numbers inside our setAttack, setDecay and setRelease features could seem a bit random, however actually they’re simply stylistic decisions. Strive altering them and seeing what occurs to the sound. It’s possible you’ll find yourself with one thing you like!
When you’re taken with taking the challenge additional, there are many methods you would enhance it. Maybe a quantity management, a technique to swap between octaves, or a means to decide on between waveforms? We may add reverb or a low move filter. Or maybe every sound may very well be made up of a number of oscillators?
For anybody taken with understanding extra about the right way to implement music concept ideas on the net, I like to recommend wanting on the supply code of the tonal npm bundle.
The submit Code a Playable Synth Keyboard appeared first on CSS-Tips. You possibly can assist CSS-Tips by being an MVP Supporter.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!