class AudioVisualiser extends HTMLElement {
static observedAttributes = ['src', 'gain'];
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
`;
this.waveformImg = this.shadowRoot.querySelector('img.waveform');
this.audio = this.shadowRoot.querySelector('audio');
this.animFrame = null;
this.audioCtx = null;
this.gainNode = null;
this.hideTimer = null;
this.isHovering = false;
}
connectedCallback() {
this.applySrc();
this.applyGain();
this.bindAudioEvents();
this.bindHoverEvents();
}
attributeChangedCallback(name, _oldVal, _newVal) {
if (!this.isConnected) {
return;
}
if (name === 'src') {
this.applySrc();
}
if (name === 'gain') {
this.applyGain();
}
}
applySrc() {
const src = this.getAttribute('src');
if (!src) {
throw new Error('No src attribute provided');
}
this.audio.src = `/sounds/${src}`;
this.waveformImg.src = `/audio-waveform-image?fileName=${encodeURIComponent(src)}`;
}
applyGain() {
if (this.gainNode) {
this.gainNode.gain.value = parseFloat(this.getAttribute('gain')) || 1;
}
}
initAudioGraph() {
if (this.audioCtx) {
return;
}
this.audioCtx = new AudioContext();
const source = this.audioCtx.createMediaElementSource(this.audio);
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value = parseFloat(this.getAttribute('gain')) || 1;
source.connect(this.gainNode);
this.gainNode.connect(this.audioCtx.destination);
}
setProgress(fraction) {
this.style.setProperty('--progress', Math.round(fraction * 1000) / 10 + '%');
}
bindAudioEvents() {
const tick = () => {
if (this.audio.duration) {
this.setProgress(this.audio.currentTime / this.audio.duration);
}
this.animFrame = requestAnimationFrame(tick);
};
this.audio.addEventListener('play', () => {
this.initAudioGraph();
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
cancelAnimationFrame(this.animFrame);
tick();
this.scheduleHide(2000);
});
this.audio.addEventListener('pause', () => {
cancelAnimationFrame(this.animFrame);
this.showControls();
});
this.audio.addEventListener('ended', () => {
cancelAnimationFrame(this.animFrame);
this.setProgress(0);
this.showControls();
});
this.audio.addEventListener('seeked', () => {
if (this.audio.duration) {
this.setProgress(this.audio.currentTime / this.audio.duration);
}
});
}
showControls() {
clearTimeout(this.hideTimer);
this.audio.classList.remove('hidden');
}
scheduleHide(delay = 2000) {
clearTimeout(this.hideTimer);
if (this.audio.paused) {
return;
}
this.hideTimer = setTimeout(
() => {
if (!this.isHovering) {
this.audio.classList.add('hidden');
}
},
delay
);
}
bindHoverEvents() {
this.addEventListener('mouseenter', () => {
this.isHovering = true;
this.showControls();
});
this.addEventListener('mouseleave', () => {
this.isHovering = false;
this.scheduleHide(1000);
});
this.addEventListener('touchstart', () => {
this.showControls();
this.scheduleHide(3000);
}, {passive: true});
}
}
customElements.define('x-audio', AudioVisualiser);