Skeuomorphic CSS
The heart of skeuomorphic design is tricking the eye into seeing materials and depth. The boombox uses layered gradients, box-shadows, and rounded borders to recreate surfaces and hardware.
Boombox Panel Texture
The gradient and box-shadow supply subtle reflections and a sense of physical edges, giving the whole case a plasticky/aluminium look.
.boombox {
background-color: hsl(0, 0%, 90%);
background-image: linear-gradient(
180deg,
rgb(179, 179, 179) 0%,
rgb(230, 230, 230) 50%,
rgb(179, 179, 179) 100%
);
box-shadow:
inset 0 0 0 1px rgba(0, 0, 0, 0.2),
0 6px 7px rgba(0, 0, 0, 0.3);
border-radius: 1rem;
}
VU metre and needle
Rounded corners and multiple shadows made the semi-circle and a sort of backlight on the visualizer.
.vu-meter {
background: white;
border-radius: 150rem 150rem 0 0;
box-shadow: inset -10px -10px 50px rgba(22, 22, 22, 0.3),
inset 10px 20px 30px -16px rgba(255,255,255,0.5);
clip-path: polygon(0 15%, 100% 15%, 100% 90%, 0 90%);
}
.needle {
border-right: 120px solid rgba(0,0,0,0.8);
border-radius: 15px;
transform: rotate(20deg);
transition: transform 0.05s;
opacity: 0.8;
}
Using and abusing the Web Audio API
To make this work, we had to make use of the Web Audio API , to be able to listen and manipulate the audio output.
Animating the VU metre needles
We split the audio output values in left and right channels (for stereo). The Root Mean Square (RMS) is calculated from audio that's playing. This calculation is important to measure the average loudness of an audio signal, giving a better idea of perceived loudness rather than peak levels.
setupAudioContext() {
this.splitterNode = this.audioContext.createChannelSplitter(2);
this.analyserNodeLeft = this.audioContext.createAnalyser();
this.analyserNodeRight = this.audioContext.createAnalyser();
this.gain.connect(this.splitterNode);
this.splitterNode.connect(this.analyserNodeLeft, 0);
this.splitterNode.connect(this.analyserNodeRight, 1);
this.analyserNodeLeft.fftSize = 256;
this.dataArrayLeft = new Float32Array(this.analyserNodeLeft.frequencyBinCount);
// ... same for right channel
}
The values are then mapped to degrees, making each VU meter needle exactly like an analog gauge.
updateMeters() {
requestAnimationFrame(() => this.updateMeters());
this.analyserNodeLeft.getFloatTimeDomainData(this.dataArrayLeft);
const volumeLeft = this.getRMS(this.dataArrayLeft);
const dbLeft = 20 * Math.log10(Math.max(volumeLeft, 0.00001));
const angleLeft = Math.max(0, Math.min(100, dbLeft + 100)) + 20;
// ... right side values
this.leftNeedle.style.transform = `rotate(${angleLeft}deg)`;
}
Building the Equalizer
We have to split the input audio into different channels/filters before it's outputted again. Then we're able to adjust the gains in those sections, and mimic an equalizer. The five frequency bands were chosen to cover the essential ranges of human hearing and musical content:
- 60Hz (Low Shelf): Sub-bass and bass fundamentals
- 170Hz (Peaking): Upper bass range
- 350Hz (Peaking): Low midrange
- 1000Hz (Peaking): Core midrange
- 3500Hz (High Shelf): Upper midrange to treble
The key to making the EQ work then is connecting each filter in sequence, creating a signal chain where audio flows through all five bands.
this.frequencies = [60, 170, 350, 1000, 3500];
this.filterTypes = ["lowshelf", "peaking", "peaking", "peaking", "highshelf"];
this.filters = this.frequencies.map((freq, index) => {
const filter = this.audioContext.createBiquadFilter();
filter.type = this.filterTypes[index];
filter.frequency.value = freq;
filter.gain.value = 0;
filter.Q.value = 1;
return filter;
});
// Connect filters in a series
this.gain.connect(this.filters[0]);
for (let i = 0; i < this.filters.length - 1; i++) {
this.filters[i].connect(this.filters[i+1]);
}
this.filters[this.filters.length-1].connect(this.audioContext.destination);
Genre-Based Preset Values
The preset values are based on established equalizer curves used in HiFi apparel and apps.
this.presets = {
none: [0, 0, 0, 0, 0],
pop: [5, -7, 6, 8, 9],
dance: [9, 6, -3, 4, 7],
rock: [8, 5, -5, 7, 6],
jazz: [4, -2, -7, 2, 5],
classic: [5, 4, 3, -6, -8]
};Et voilá! You're ready to listen to Chopin the way it was meant to be?