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