2025-08-21 21:30:53 -07:00
|
|
|
// adapted from: https://codepen.io/uixamp / https://arisetyo.github.io
|
|
|
|
|
|
|
|
function human_readable_time(duration) {
|
|
|
|
return new Date(duration * 1000).toISOString().slice(duration > 3600 ? 11 : 14, 19);
|
|
|
|
}
|
|
|
|
|
2025-08-21 23:24:34 -07:00
|
|
|
const DISPLAY_WINDOW = 1;
|
|
|
|
const BAR_COUNT = 48;
|
|
|
|
const BAR_GAP = 2;
|
|
|
|
const BAR_CAP_HEIGHT = 8;
|
2025-08-21 21:30:53 -07:00
|
|
|
const PROGRESS_TICKS = 1000;
|
|
|
|
const MS_BETWEEN_PROGRESS_UPDATES = 200;
|
|
|
|
|
|
|
|
new MutationObserver((mutations) => {
|
|
|
|
const uninitialized_audio_elements = document.querySelectorAll(
|
|
|
|
".audio-container:not([data-audio-player])",
|
|
|
|
);
|
|
|
|
for (const player of uninitialized_audio_elements) {
|
|
|
|
const CURRENT_TIMES = player.querySelectorAll(
|
|
|
|
".audio-controls-container .time-container .current",
|
|
|
|
);
|
|
|
|
const DURATIONS = player.querySelectorAll(
|
|
|
|
".audio-controls-container .time-container .duration",
|
|
|
|
);
|
|
|
|
|
|
|
|
let progress_being_changed_by_user = false;
|
|
|
|
|
|
|
|
const AUDIO = player.querySelector("audio");
|
|
|
|
AUDIO.controls = false;
|
|
|
|
AUDIO.addEventListener("ended", () => {
|
|
|
|
delete player.dataset.playing;
|
|
|
|
});
|
|
|
|
AUDIO.addEventListener("loadedmetadata", () => {
|
|
|
|
for (const duration of DURATIONS) {
|
|
|
|
duration.innerHTML = human_readable_time(AUDIO.duration);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
AUDIO.addEventListener("timeupdate", () => {
|
|
|
|
if (!progress_being_changed_by_user) {
|
|
|
|
PROGRESS.value = parseInt((AUDIO.currentTime / AUDIO.duration) * PROGRESS_TICKS);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const current of CURRENT_TIMES) {
|
|
|
|
current.innerHTML = human_readable_time(AUDIO.currentTime);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
AUDIO.addEventListener("volumechange", () => {
|
|
|
|
if (!volume_being_changed_by_user) {
|
|
|
|
VOLUME.value = parseInt(AUDIO.volume * 100);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const volume_display of VOLUME_DISPLAYS) {
|
|
|
|
volume_display.innerHTML = parseInt(AUDIO.volume * 100);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
player.addEventListener("keydown", (event) => {
|
|
|
|
const key = event.which ?? event.keyCode;
|
|
|
|
switch (key) {
|
|
|
|
case 32: // space
|
|
|
|
event.preventDefault();
|
|
|
|
if (player.dataset.playing) {
|
|
|
|
AUDIO.pause();
|
|
|
|
delete player.dataset.playing;
|
|
|
|
} else {
|
|
|
|
AUDIO.play();
|
|
|
|
player.dataset.playing = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 37:
|
|
|
|
event.preventDefault();
|
|
|
|
AUDIO.currentTime -= 5;
|
|
|
|
break;
|
|
|
|
case 39:
|
|
|
|
event.preventDefault();
|
|
|
|
AUDIO.currentTime += 5;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const VOLUME = player.querySelector('.audio-controls-container input[name="volume"]');
|
|
|
|
const VOLUME_DISPLAYS = player.querySelectorAll(
|
|
|
|
".audio-controls-container .volume-display",
|
|
|
|
);
|
|
|
|
|
|
|
|
let volume_being_changed_by_user = false;
|
|
|
|
|
|
|
|
VOLUME.addEventListener("pointerdown", (event) => {
|
|
|
|
volume_being_changed_by_user = true;
|
|
|
|
});
|
|
|
|
VOLUME.addEventListener("pointerup", () => {
|
|
|
|
volume_being_changed_by_user = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
function on_volume_changed() {
|
|
|
|
if (volume_being_changed_by_user) {
|
|
|
|
AUDIO.volume = Math.min(1.0, Math.max(VOLUME.value / 100, 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const volume_display of VOLUME_DISPLAYS) {
|
|
|
|
volume_display.innerHTML = parseInt(AUDIO.volume * 100);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VOLUME.addEventListener("change", on_volume_changed);
|
|
|
|
VOLUME.addEventListener("input", on_volume_changed);
|
|
|
|
|
|
|
|
VOLUME.value = Math.min(100, Math.max(AUDIO.volume * 100, 0));
|
|
|
|
|
|
|
|
const PROGRESS = player.querySelector('.audio-controls-container input[name="progress"]');
|
|
|
|
PROGRESS.min = 0;
|
|
|
|
PROGRESS.max = PROGRESS_TICKS;
|
|
|
|
|
|
|
|
PROGRESS.addEventListener("pointerdown", (event) => {
|
|
|
|
progress_being_changed_by_user = true;
|
|
|
|
});
|
|
|
|
PROGRESS.addEventListener("pointerup", () => {
|
|
|
|
progress_being_changed_by_user = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
function on_progress_changed() {
|
|
|
|
if (progress_being_changed_by_user) {
|
|
|
|
AUDIO.currentTime = (PROGRESS.value / PROGRESS_TICKS) * AUDIO.duration;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
PROGRESS.addEventListener("change", on_progress_changed);
|
|
|
|
PROGRESS.addEventListener("input", on_progress_changed);
|
|
|
|
|
|
|
|
let CANVAS;
|
|
|
|
let ACTX;
|
|
|
|
|
|
|
|
let ANALYSER;
|
|
|
|
let DATA;
|
|
|
|
let SOURCE;
|
|
|
|
|
|
|
|
let CTX;
|
|
|
|
|
|
|
|
let VIZ_RGB;
|
|
|
|
|
|
|
|
function init() {
|
|
|
|
CANVAS = CANVAS ?? player.querySelector("canvas");
|
2025-08-21 23:24:34 -07:00
|
|
|
CANVAS.height = 255;
|
|
|
|
CANVAS.width = BAR_COUNT * (16 + BAR_GAP);
|
|
|
|
|
2025-08-21 21:30:53 -07:00
|
|
|
VIZ_RGB = window
|
|
|
|
.getComputedStyle(CANVAS)
|
|
|
|
.color.slice(4, -1)
|
|
|
|
.split(",")
|
|
|
|
.map((v) => parseInt(v));
|
|
|
|
|
|
|
|
ACTX = ACTX ?? new AudioContext();
|
|
|
|
|
|
|
|
if (!ANALYSER) {
|
|
|
|
ANALYSER = ACTX.createAnalyser();
|
|
|
|
}
|
|
|
|
|
|
|
|
DATA = DATA ?? new Uint8Array(ANALYSER.frequencyBinCount);
|
|
|
|
|
|
|
|
if (!SOURCE) {
|
|
|
|
SOURCE = ACTX.createMediaElementSource(AUDIO);
|
|
|
|
SOURCE.connect(ANALYSER);
|
|
|
|
SOURCE.connect(ACTX.destination);
|
|
|
|
}
|
|
|
|
|
|
|
|
CTX = CTX ?? CANVAS.getContext("2d");
|
|
|
|
|
|
|
|
for (const current of CURRENT_TIMES) {
|
|
|
|
current.innerHTML = human_readable_time(AUDIO.currentTime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-08-21 23:24:34 -07:00
|
|
|
let previous_max_values;
|
2025-08-21 21:30:53 -07:00
|
|
|
function draw() {
|
2025-08-21 23:24:34 -07:00
|
|
|
previous_max_values =
|
|
|
|
previous_max_values ?? new Array(ANALYSER.frequencyBinCount).fill(0);
|
|
|
|
|
2025-08-21 21:30:53 -07:00
|
|
|
if (!player.dataset.playing) {
|
2025-08-21 23:24:34 -07:00
|
|
|
if (previous_max_values.all((v) => v === 0)) return;
|
2025-08-21 21:30:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
ANALYSER.getByteFrequencyData(DATA);
|
|
|
|
|
2025-08-21 23:24:34 -07:00
|
|
|
const gradient = CTX.createLinearGradient(0, 0, 0, CANVAS.height);
|
|
|
|
gradient.addColorStop(
|
|
|
|
0.4,
|
|
|
|
`rgba( ${parseInt(VIZ_RGB[0] / 4)}, ${parseInt(VIZ_RGB[1] / 4)}, ${parseInt(VIZ_RGB[2] / 4)}, 0)`,
|
|
|
|
);
|
|
|
|
gradient.addColorStop(1, `rgba( ${VIZ_RGB[0]}, ${VIZ_RGB[1]}, ${VIZ_RGB[2]}, 0.25)`);
|
|
|
|
|
|
|
|
const bar_width = parseInt(CANVAS.width / BAR_COUNT);
|
|
|
|
const stride = parseInt((DATA.length * DISPLAY_WINDOW) / BAR_COUNT);
|
|
|
|
|
2025-08-21 21:30:53 -07:00
|
|
|
CTX.clearRect(0, 0, CANVAS.width, CANVAS.height);
|
|
|
|
|
2025-08-21 23:24:34 -07:00
|
|
|
for (let i = 0; i < BAR_COUNT; ++i) {
|
|
|
|
const value = DATA[i * stride];
|
|
|
|
|
|
|
|
const cap_value = value > previous_max_values[i] ? value : previous_max_values[i];
|
|
|
|
const cap_color_value = 0.33 + cap_value / 255;
|
|
|
|
CTX.fillStyle = `rgba(${VIZ_RGB[0]}, ${VIZ_RGB[1]}, ${VIZ_RGB[2]}, 0.8)`;
|
|
|
|
CTX.fillRect(
|
|
|
|
i * (bar_width + BAR_GAP),
|
|
|
|
CANVAS.height - cap_value,
|
|
|
|
bar_width,
|
|
|
|
BAR_CAP_HEIGHT,
|
|
|
|
);
|
|
|
|
|
|
|
|
previous_max_values[i] =
|
|
|
|
value > previous_max_values[i]
|
|
|
|
? value
|
|
|
|
: previous_max_values[i] - BAR_CAP_HEIGHT / 2;
|
|
|
|
|
|
|
|
CTX.fillStyle = gradient;
|
|
|
|
CTX.fillRect(
|
|
|
|
i * (bar_width + BAR_GAP),
|
|
|
|
CANVAS.height - value + BAR_CAP_HEIGHT + BAR_CAP_HEIGHT,
|
|
|
|
bar_width,
|
|
|
|
value,
|
|
|
|
);
|
2025-08-21 21:30:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
requestAnimationFrame(draw);
|
|
|
|
}
|
|
|
|
|
|
|
|
player.querySelector(".audio-control.skip-back")?.addEventListener("click", (event) => {
|
|
|
|
init();
|
|
|
|
AUDIO.currentTime = 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
player
|
|
|
|
.querySelector(".audio-control.play-pause-toggle")
|
|
|
|
.addEventListener("click", (event) => {
|
|
|
|
init();
|
|
|
|
|
|
|
|
const is_playing = player.dataset.playing;
|
|
|
|
if (is_playing) {
|
|
|
|
AUDIO.pause();
|
|
|
|
delete player.dataset.playing;
|
|
|
|
// CTX.clearRect(0, 0, canvasEl.width, canvasEl.height);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AUDIO.play();
|
|
|
|
player.dataset.playing = true;
|
|
|
|
draw();
|
|
|
|
});
|
|
|
|
|
|
|
|
player.querySelector(".audio-control.skip-forward")?.addEventListener("click", (event) => {
|
|
|
|
init();
|
|
|
|
|
|
|
|
AUDIO.currentTime = AUDIO.duration;
|
|
|
|
AUDIO.pause();
|
|
|
|
delete player.dataset.playing;
|
|
|
|
});
|
|
|
|
|
|
|
|
player.dataset.audioPlayer = true;
|
|
|
|
}
|
|
|
|
}).observe(document, {
|
|
|
|
subtree: true,
|
|
|
|
childList: true,
|
|
|
|
});
|