feat: extract audio controller into ES module, fix restart bug
Made-with: Cursor
This commit is contained in:
139
js/audio-controller.js
Normal file
139
js/audio-controller.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Theme audio for the room code lobby: reload, volume, loop, and fade-out.
|
||||||
|
* Implements the overlay component contract: init, activate, deactivate, update, getStatus.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} AudioControllerInputs
|
||||||
|
* @property {HTMLInputElement} enabled
|
||||||
|
* @property {HTMLInputElement} volume
|
||||||
|
* @property {HTMLInputElement} soundUrl
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AudioController {
|
||||||
|
/** @type {HTMLAudioElement | null} */
|
||||||
|
#audio = null;
|
||||||
|
|
||||||
|
/** @type {AudioControllerInputs | null} */
|
||||||
|
#inputs = null;
|
||||||
|
|
||||||
|
/** @type {boolean} */
|
||||||
|
#active = false;
|
||||||
|
|
||||||
|
/** @type {ReturnType<typeof setInterval> | null} */
|
||||||
|
#fadeInterval = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLAudioElement} audioElement
|
||||||
|
* @param {AudioControllerInputs} inputs
|
||||||
|
*/
|
||||||
|
init(audioElement, inputs) {
|
||||||
|
this.#audio = audioElement;
|
||||||
|
this.#inputs = inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [_ctx]
|
||||||
|
*/
|
||||||
|
activate(_ctx) {
|
||||||
|
this.deactivate();
|
||||||
|
|
||||||
|
this.#active = true;
|
||||||
|
|
||||||
|
const inputs = this.#inputs;
|
||||||
|
const audio = this.#audio;
|
||||||
|
if (!inputs || !audio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputs.enabled.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.src = inputs.soundUrl.value;
|
||||||
|
audio.load();
|
||||||
|
audio.volume = parseFloat(inputs.volume.value);
|
||||||
|
audio.loop = true;
|
||||||
|
audio.currentTime = 0;
|
||||||
|
audio.play().catch((error) => {
|
||||||
|
console.log('Audio playback failed:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.#active = false;
|
||||||
|
|
||||||
|
if (this.#fadeInterval != null) {
|
||||||
|
clearInterval(this.#fadeInterval);
|
||||||
|
this.#fadeInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audio = this.#audio;
|
||||||
|
const inputs = this.#inputs;
|
||||||
|
if (!audio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.pause();
|
||||||
|
audio.currentTime = 0;
|
||||||
|
|
||||||
|
if (inputs) {
|
||||||
|
audio.volume = parseFloat(inputs.volume.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [_ctx]
|
||||||
|
*/
|
||||||
|
update(_ctx) {
|
||||||
|
// No in-state audio updates.
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
const audio = this.#audio;
|
||||||
|
const inputs = this.#inputs;
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: this.#active,
|
||||||
|
enabled: inputs?.enabled.checked ?? false,
|
||||||
|
playing: Boolean(audio && !audio.paused),
|
||||||
|
volume: audio?.volume ?? 0,
|
||||||
|
src: audio?.currentSrc ?? audio?.src ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gradually reduce volume to zero, then pause. No-op if audio is already paused.
|
||||||
|
* @param {number} durationMs
|
||||||
|
*/
|
||||||
|
startFadeOut(durationMs) {
|
||||||
|
const audio = this.#audio;
|
||||||
|
if (!audio || audio.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#fadeInterval != null) {
|
||||||
|
clearInterval(this.#fadeInterval);
|
||||||
|
this.#fadeInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startVolume = audio.volume;
|
||||||
|
const stepCount = 10;
|
||||||
|
const intervalMs = Math.max(1, durationMs / stepCount);
|
||||||
|
let step = 0;
|
||||||
|
|
||||||
|
this.#fadeInterval = setInterval(() => {
|
||||||
|
step += 1;
|
||||||
|
audio.volume = Math.max(0, startVolume * (1 - step / stepCount));
|
||||||
|
|
||||||
|
if (step >= stepCount) {
|
||||||
|
audio.volume = 0;
|
||||||
|
audio.pause();
|
||||||
|
if (this.#fadeInterval != null) {
|
||||||
|
clearInterval(this.#fadeInterval);
|
||||||
|
this.#fadeInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user