mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
Improve Audio Visualizer
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
|||||||
LunaSettings,
|
LunaSettings,
|
||||||
LunaNumberSetting,
|
LunaNumberSetting,
|
||||||
LunaSwitchSetting,
|
LunaSwitchSetting,
|
||||||
LunaTextSetting,
|
|
||||||
} from "@luna/ui";
|
} from "@luna/ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@@ -78,7 +77,6 @@ export const Settings = () => {
|
|||||||
setBarColor(color);
|
setBarColor(color);
|
||||||
setCustomInput(color);
|
setCustomInput(color);
|
||||||
settings.barColor = color;
|
settings.barColor = color;
|
||||||
(window as any).updateAudioVisualizer?.();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addCustomColor = () => {
|
const addCustomColor = () => {
|
||||||
@@ -125,7 +123,6 @@ export const Settings = () => {
|
|||||||
onChange={(_, checked) => {
|
onChange={(_, checked) => {
|
||||||
setBarRounding(checked);
|
setBarRounding(checked);
|
||||||
settings.barRounding = checked;
|
settings.barRounding = checked;
|
||||||
(window as any).updateAudioVisualizer?.();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -139,7 +136,6 @@ export const Settings = () => {
|
|||||||
onNumber={(value: number) => {
|
onNumber={(value: number) => {
|
||||||
setBarCount(value);
|
setBarCount(value);
|
||||||
settings.barCount = value;
|
settings.barCount = value;
|
||||||
(window as any).updateAudioVisualizer?.();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { LunaUnload, Tracer } from "@luna/core";
|
import { type LunaUnload, Tracer } from "@luna/core";
|
||||||
import { StyleTag, PlayState } from "@luna/lib";
|
import { StyleTag, PlayState, MediaItem, observe } from "@luna/lib";
|
||||||
import { settings, Settings } from "./Settings";
|
import { settings, Settings } from "./Settings";
|
||||||
|
|
||||||
// Import CSS styles for the visualizer
|
|
||||||
import visualizerStyles from "file://styles.css?minify";
|
import visualizerStyles from "file://styles.css?minify";
|
||||||
|
|
||||||
export const { trace } = Tracer("[Audio Visualizer]");
|
export const { trace } = Tracer("[Audio Visualizer]");
|
||||||
|
|
||||||
const log = (message: string) => console.log(`[Audio Visualizer] ${message}`);
|
|
||||||
export { Settings };
|
export { Settings };
|
||||||
|
|
||||||
|
const log = (message: string) => console.log(`[Audio Visualizer] ${message}`);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
enabled: true,
|
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 40,
|
height: 40,
|
||||||
get barCount() {
|
get barCount() {
|
||||||
@@ -27,22 +25,19 @@ const config = {
|
|||||||
smoothing: 0.8,
|
smoothing: 0.8,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clean up resources
|
|
||||||
export const unloads = new Set<LunaUnload>();
|
export const unloads = new Set<LunaUnload>();
|
||||||
|
|
||||||
// StyleTag for CSS
|
new StyleTag("AudioVisualizer", unloads, visualizerStyles);
|
||||||
const styleTag = new StyleTag("AudioVisualizer", unloads, visualizerStyles);
|
|
||||||
|
|
||||||
// Audio context and analyzer
|
|
||||||
let audioContext: AudioContext | null = null;
|
let audioContext: AudioContext | null = null;
|
||||||
let analyser: AnalyserNode | null = null;
|
let analyser: AnalyserNode | null = null;
|
||||||
let audioSource: MediaElementAudioSourceNode | null = null;
|
let audioSource: MediaStreamAudioSourceNode | null = null;
|
||||||
let dataArray: Uint8Array | null = null;
|
let dataArray: Uint8Array<ArrayBuffer> | null = null;
|
||||||
let animationId: number | null = null;
|
let animationId: number | null = null;
|
||||||
let currentAudioElement: HTMLAudioElement | null = null;
|
let isSourceConnected = false;
|
||||||
let isSourceConnected: boolean = false;
|
|
||||||
|
|
||||||
// Each placement gets its own container/canvas/context
|
|
||||||
interface VisualizerSlot {
|
interface VisualizerSlot {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
@@ -52,104 +47,48 @@ interface VisualizerSlot {
|
|||||||
const navSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
|
const navSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
|
||||||
const npSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
|
const npSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
|
||||||
|
|
||||||
// Find the audio element - this is a bit of a hack but it works
|
|
||||||
const findAudioElement = (): HTMLAudioElement | null => {
|
|
||||||
// Try main selectors first
|
|
||||||
const selectors = [
|
|
||||||
"audio",
|
|
||||||
"video",
|
|
||||||
"audio[data-test]",
|
|
||||||
'[data-test="audio-player"] audio',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const selector of selectors) {
|
const connectAudio = (): boolean => {
|
||||||
const element = document.querySelector(selector) as HTMLAudioElement;
|
const video = document.getElementById("video-one") as HTMLVideoElement | null;
|
||||||
if (
|
const capture = (video as unknown as { captureStream?: () => MediaStream })?.captureStream;
|
||||||
element &&
|
if (!video || typeof capture !== "function") return false;
|
||||||
(element.tagName === "AUDIO" || element.tagName === "VIDEO")
|
|
||||||
) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick scan for any audio elements
|
|
||||||
const audioElements = document.querySelectorAll("audio, video");
|
|
||||||
for (const element of audioElements) {
|
|
||||||
const audioEl = element as HTMLAudioElement;
|
|
||||||
if (audioEl.src || audioEl.currentSrc) {
|
|
||||||
return audioEl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize audio visualization
|
|
||||||
const initializeAudioVisualizer = async (): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
// Find the audio element
|
if (!audioContext) audioContext = new AudioContext();
|
||||||
const audioElement = findAudioElement();
|
|
||||||
if (!audioElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create audio context
|
|
||||||
if (!audioContext) {
|
|
||||||
audioContext = new AudioContext();
|
|
||||||
log("Created AudioContext");
|
|
||||||
}
|
|
||||||
|
|
||||||
// create analyser
|
|
||||||
if (!analyser) {
|
if (!analyser) {
|
||||||
analyser = audioContext.createAnalyser();
|
analyser = audioContext.createAnalyser();
|
||||||
analyser.fftSize = 512; // Fixed power of 2 that provides enough frequency bins
|
analyser.fftSize = 512;
|
||||||
analyser.smoothingTimeConstant = config.smoothing;
|
analyser.smoothingTimeConstant = config.smoothing;
|
||||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
dataArray = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
|
||||||
log("Created AnalyserNode");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// attempt audio connection if not already connected
|
audioSource?.disconnect();
|
||||||
if (!isSourceConnected && audioElement !== currentAudioElement) {
|
|
||||||
try {
|
const stream = capture.call(video);
|
||||||
// Create audio source - this might fail if already connected elsewhere
|
const audioTracks = stream.getAudioTracks();
|
||||||
audioSource = audioContext.createMediaElementSource(audioElement);
|
if (audioTracks.length === 0) {
|
||||||
|
log("No audio tracks in captured stream");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
audioSource = audioContext.createMediaStreamSource(stream);
|
||||||
audioSource.connect(analyser);
|
audioSource.connect(analyser);
|
||||||
// CRITICAL: connect back to destination for audio output (otherwise no sound)
|
|
||||||
analyser.connect(audioContext.destination);
|
|
||||||
|
|
||||||
currentAudioElement = audioElement;
|
|
||||||
isSourceConnected = true;
|
|
||||||
log("Connected to audio stream with output");
|
|
||||||
} catch (error) {
|
|
||||||
// Audio is connected elsewhere - that's fine, we just can't visualize
|
|
||||||
if (
|
|
||||||
error instanceof Error &&
|
|
||||||
error.message.includes("already connected")
|
|
||||||
) {
|
|
||||||
log("Audio already connected elsewhere - skipping visualization");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume context only if needed and don't wait for it
|
|
||||||
// (otherwise it will wait for the audio to start playing)
|
|
||||||
if (audioContext.state === "suspended") {
|
if (audioContext.state === "suspended") {
|
||||||
audioContext.resume().catch(() => {}); // Fire and forget
|
audioContext.resume().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
createVisualizerUI();
|
log("Connected via captureStream()");
|
||||||
|
return true;
|
||||||
// Start animation only if not already running
|
|
||||||
if (!animationId) {
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// log errors
|
log(`Audio connection failed: ${err}`);
|
||||||
console.error(err);
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Canvas things
|
||||||
|
|
||||||
const makeSlotElements = (): { container: HTMLDivElement; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null => {
|
const makeSlotElements = (): { container: HTMLDivElement; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.className = "audio-visualizer-container";
|
container.className = "audio-visualizer-container";
|
||||||
@@ -186,89 +125,43 @@ const clearSlot = (slot: VisualizerSlot): void => {
|
|||||||
slot.ctx = null;
|
slot.ctx = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureNavSlot = (): void => {
|
// UI Placement with Luna Observer
|
||||||
|
|
||||||
|
const attachNavSlot = (anchor: Element): void => {
|
||||||
if (navSlot.container?.isConnected) return;
|
if (navSlot.container?.isConnected) return;
|
||||||
clearSlot(navSlot);
|
clearSlot(navSlot);
|
||||||
|
|
||||||
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
|
const parent = anchor.parentElement;
|
||||||
if (!searchField) return;
|
if (!parent) return;
|
||||||
const searchContainer = searchField.parentElement;
|
|
||||||
if (!searchContainer?.parentElement) return;
|
|
||||||
|
|
||||||
const els = makeSlotElements();
|
const els = makeSlotElements();
|
||||||
if (!els) return;
|
if (!els) return;
|
||||||
els.container.style.marginRight = "12px";
|
els.container.style.marginRight = "12px";
|
||||||
Object.assign(navSlot, els);
|
Object.assign(navSlot, els);
|
||||||
searchContainer.parentElement.insertBefore(els.container, searchContainer);
|
parent.insertBefore(els.container, anchor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureNpSlot = (): void => {
|
const attachNpSlot = (anchor: Element): void => {
|
||||||
if (npSlot.container?.isConnected) return;
|
if (npSlot.container?.isConnected) return;
|
||||||
clearSlot(npSlot);
|
clearSlot(npSlot);
|
||||||
|
|
||||||
const artistInfo = document.querySelector('[data-test="artist-info"]');
|
const parent = anchor.parentElement;
|
||||||
if (!artistInfo) return;
|
if (!parent) return;
|
||||||
const leftContent = artistInfo.parentElement;
|
|
||||||
if (!leftContent) return;
|
|
||||||
|
|
||||||
const els = makeSlotElements();
|
const els = makeSlotElements();
|
||||||
if (!els) return;
|
if (!els) return;
|
||||||
els.container.style.marginLeft = "12px";
|
els.container.style.marginLeft = "12px";
|
||||||
Object.assign(npSlot, els);
|
Object.assign(npSlot, els);
|
||||||
leftContent.insertBefore(els.container, artistInfo.nextSibling);
|
parent.insertBefore(els.container, anchor.nextSibling);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createVisualizerUI = (): void => {
|
observe(unloads, '[data-test="search-popover-container"]', attachNavSlot);
|
||||||
if (!config.enabled) return;
|
observe(unloads, '[data-test="artist-info"]', attachNpSlot);
|
||||||
ensureNavSlot();
|
|
||||||
ensureNpSlot();
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeVisualizerUI = (): void => {
|
// Rendering things
|
||||||
clearSlot(navSlot);
|
|
||||||
clearSlot(npSlot);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Animation loop for rendering visualizer
|
|
||||||
const animate = (): void => {
|
|
||||||
// Re-attach slots that got disconnected from the DOM
|
|
||||||
createVisualizerUI();
|
|
||||||
|
|
||||||
const slots = [navSlot, npSlot].filter(s => s.ctx && s.canvas);
|
|
||||||
if (slots.length === 0) {
|
|
||||||
animationId = requestAnimationFrame(animate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasRealAudio = false;
|
|
||||||
if (analyser && dataArray) {
|
|
||||||
analyser.getByteFrequencyData(dataArray);
|
|
||||||
const avgVolume =
|
|
||||||
dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
|
|
||||||
hasRealAudio = avgVolume > 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const slot of slots) {
|
|
||||||
const ctx = slot.ctx!;
|
|
||||||
const cvs = slot.canvas!;
|
|
||||||
ctx.fillStyle = config.color;
|
|
||||||
ctx.strokeStyle = config.color;
|
|
||||||
ctx.clearRect(0, 0, cvs.width, cvs.height);
|
|
||||||
|
|
||||||
if (hasRealAudio && analyser && dataArray) {
|
|
||||||
drawBars(ctx, cvs);
|
|
||||||
} else {
|
|
||||||
drawScrollingWave(ctx, cvs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animationId = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Global wave animation state
|
|
||||||
let waveTime = 0;
|
let waveTime = 0;
|
||||||
|
|
||||||
// Helper function to draw rounded rectangles
|
|
||||||
const drawRoundedRect = (
|
const drawRoundedRect = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
@@ -283,8 +176,6 @@ const drawRoundedRect = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const drawScrollingWave = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
|
const drawScrollingWave = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
|
||||||
waveTime += 0.05 / [navSlot, npSlot].filter(s => s.ctx).length;
|
|
||||||
|
|
||||||
const barCount = config.barCount;
|
const barCount = config.barCount;
|
||||||
const barWidth = cvs.width / barCount;
|
const barWidth = cvs.width / barCount;
|
||||||
const maxHeight = cvs.height * 0.6;
|
const maxHeight = cvs.height * 0.6;
|
||||||
@@ -334,202 +225,84 @@ const drawBars = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw waveform visualization - NOT IMPLEMENTED YET
|
// Animation Loop
|
||||||
// const drawWaveform = (): void => {
|
|
||||||
// if (!canvasContext || !dataArray || !canvas) return;
|
|
||||||
|
|
||||||
// const centerY = canvas.height / 2;
|
const animate = (): void => {
|
||||||
// const amplitudeScale = canvas.height / 512;
|
const slots = [navSlot, npSlot].filter(s => s.ctx && s.canvas);
|
||||||
|
|
||||||
// canvasContext.strokeStyle = config.color;
|
if (slots.length > 0) {
|
||||||
// canvasContext.lineWidth = 2;
|
waveTime += 0.05;
|
||||||
// canvasContext.beginPath();
|
|
||||||
|
|
||||||
// for (let i = 0; i < config.barCount; i++) {
|
let hasRealAudio = false;
|
||||||
// const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
if (analyser && dataArray) {
|
||||||
// const amplitude = (dataArray[dataIndex] - 128) * config.sensitivity * amplitudeScale;
|
analyser.getByteFrequencyData(dataArray);
|
||||||
|
const avgVolume = dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
|
||||||
// const x = (i / config.barCount) * canvas.width;
|
hasRealAudio = avgVolume > 5;
|
||||||
// const y = centerY + amplitude;
|
|
||||||
|
|
||||||
// if (i === 0) {
|
|
||||||
// canvasContext.moveTo(x, y);
|
|
||||||
// } else {
|
|
||||||
// canvasContext.lineTo(x, y);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// canvasContext.stroke();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Draw circular visualization - NOT IMPLEMENTED YET
|
|
||||||
// const drawCircular = (): void => {
|
|
||||||
// if (!canvasContext || !dataArray || !canvas) return;
|
|
||||||
|
|
||||||
// const centerX = canvas.width / 2;
|
|
||||||
// const centerY = canvas.height / 2;
|
|
||||||
// const radius = Math.min(centerX, centerY) - 10;
|
|
||||||
|
|
||||||
// canvasContext.strokeStyle = config.color;
|
|
||||||
// canvasContext.lineWidth = 2;
|
|
||||||
|
|
||||||
// for (let i = 0; i < config.barCount; i++) {
|
|
||||||
// const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
|
||||||
// const amplitude = (dataArray[dataIndex] * config.sensitivity) / 255;
|
|
||||||
|
|
||||||
// const angle = (i / config.barCount) * Math.PI * 2;
|
|
||||||
// const startX = centerX + Math.cos(angle) * radius * 0.7;
|
|
||||||
// const startY = centerY + Math.sin(angle) * radius * 0.7;
|
|
||||||
// const endX = centerX + Math.cos(angle) * radius * (0.7 + amplitude * 0.3);
|
|
||||||
// const endY = centerY + Math.sin(angle) * radius * (0.7 + amplitude * 0.3);
|
|
||||||
|
|
||||||
// canvasContext.beginPath();
|
|
||||||
// canvasContext.moveTo(startX, startY);
|
|
||||||
// canvasContext.lineTo(endX, endY);
|
|
||||||
// canvasContext.stroke();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const updateAudioVisualizer = (): void => {
|
|
||||||
if (analyser) {
|
|
||||||
analyser.fftSize = 512;
|
|
||||||
analyser.smoothingTimeConstant = config.smoothing;
|
|
||||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const slot of [navSlot, npSlot]) {
|
for (const slot of slots) {
|
||||||
if (slot.canvas) {
|
const { ctx, canvas: cvs } = slot;
|
||||||
slot.canvas.width = config.width;
|
if (!ctx || !cvs) continue;
|
||||||
slot.canvas.height = config.height;
|
ctx.clearRect(0, 0, cvs.width, cvs.height);
|
||||||
slot.canvas.style.width = `${config.width}px`;
|
|
||||||
slot.canvas.style.height = `${config.height}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeVisualizerUI();
|
if (hasRealAudio) {
|
||||||
createVisualizerUI();
|
drawBars(ctx, cvs);
|
||||||
};
|
|
||||||
|
|
||||||
// Make updateAudioVisualizer available globally for settings
|
|
||||||
(window as any).updateAudioVisualizer = updateAudioVisualizer;
|
|
||||||
|
|
||||||
// Clean up function
|
|
||||||
const cleanupAudioVisualizer = (): void => {
|
|
||||||
// stop animation and hide UI - don't touch audio connections (otherwise it will reconnect)
|
|
||||||
if (animationId) {
|
|
||||||
cancelAnimationFrame(animationId);
|
|
||||||
animationId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeVisualizerUI();
|
|
||||||
|
|
||||||
// i was killing audio connections - But it was reconnecting and being a pain
|
|
||||||
// so i just left it alone - it works fine
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize when DOM is ready and track is playing
|
|
||||||
const observePlayState = (): void => {
|
|
||||||
let hasTriedInitialization = false;
|
|
||||||
let checkCount = 0;
|
|
||||||
|
|
||||||
const checkAndInitialize = () => {
|
|
||||||
checkCount++;
|
|
||||||
|
|
||||||
// Only try to initialize once when music starts playing
|
|
||||||
if (PlayState.playing && !hasTriedInitialization) {
|
|
||||||
hasTriedInitialization = true;
|
|
||||||
log("Initializing audio visualizer...");
|
|
||||||
|
|
||||||
// Initialize immediately - no delay (after audio starts playing ofc)
|
|
||||||
initializeAudioVisualizer().then(() => {
|
|
||||||
if (audioContext && analyser) {
|
|
||||||
log("Audio visualizer ready!");
|
|
||||||
} else {
|
} else {
|
||||||
hasTriedInitialization = false; // Allow retry if failed
|
drawScrollingWave(ctx, cvs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else if (!PlayState.playing && hasTriedInitialization) {
|
|
||||||
// Reset try flag when music stops so it can try again next time (otherwise it explode)
|
|
||||||
hasTriedInitialization = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep animation running regardless of play state
|
animationId = requestAnimationFrame(animate);
|
||||||
if (!animationId) {
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start with fast checking, then slow down
|
|
||||||
const fastInterval = setInterval(() => {
|
|
||||||
checkAndInitialize();
|
|
||||||
if (checkCount > 10) {
|
|
||||||
// After 10 quick checks, switch to slower
|
|
||||||
clearInterval(fastInterval);
|
|
||||||
const slowInterval = setInterval(checkAndInitialize, 2000);
|
|
||||||
unloads.add(() => clearInterval(slowInterval));
|
|
||||||
}
|
|
||||||
}, 200); // Check every 200ms initially
|
|
||||||
|
|
||||||
unloads.add(() => clearInterval(fastInterval));
|
|
||||||
|
|
||||||
// Immediate first check
|
|
||||||
checkAndInitialize();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the plugin
|
// Initialization (events)
|
||||||
const initialize = (): void => {
|
|
||||||
log("Audio Visualizer plugin initializing...");
|
|
||||||
|
|
||||||
// Start immediately - DOM should be ready by plugin load
|
PlayState.onState(unloads, (state) => {
|
||||||
setTimeout(() => {
|
if (state === "PLAYING" && !isSourceConnected) {
|
||||||
log("Starting visualizer...");
|
isSourceConnected = connectAudio();
|
||||||
// Create UI immediately so wave effect shows
|
}
|
||||||
createVisualizerUI();
|
});
|
||||||
// Start animation loop immediately
|
|
||||||
animate();
|
|
||||||
// Also observe play state for audio detection
|
|
||||||
observePlayState();
|
|
||||||
}, 100); // Minimal delay to ensure DOM is ready
|
|
||||||
};
|
|
||||||
|
|
||||||
// Complete cleanup function for plugin unload
|
MediaItem.onMediaTransition(unloads, () => {
|
||||||
const completeCleanup = (): void => {
|
isSourceConnected = false;
|
||||||
log("Complete cleanup - plugin unloading");
|
if (PlayState.playing) {
|
||||||
|
isSourceConnected = connectAudio();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialization (startup)
|
||||||
|
|
||||||
|
log("Initializing...");
|
||||||
|
|
||||||
|
if (PlayState.playing) {
|
||||||
|
isSourceConnected = connectAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
animationId = requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
|
||||||
|
unloads.add(() => {
|
||||||
|
log("Plugin unloading");
|
||||||
|
|
||||||
if (animationId) {
|
if (animationId) {
|
||||||
cancelAnimationFrame(animationId);
|
cancelAnimationFrame(animationId);
|
||||||
animationId = null;
|
animationId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVisualizerUI();
|
clearSlot(navSlot);
|
||||||
|
clearSlot(npSlot);
|
||||||
|
|
||||||
// Fully disconnect and reset everything
|
try { audioSource?.disconnect(); } catch {}
|
||||||
if (audioSource) {
|
|
||||||
try {
|
|
||||||
audioSource.disconnect();
|
|
||||||
log("Disconnected audio source completely");
|
|
||||||
} catch (e) {
|
|
||||||
log("Audio source already disconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close audio context completely on plugin unload
|
|
||||||
if (audioContext && audioContext.state !== "closed") {
|
if (audioContext && audioContext.state !== "closed") {
|
||||||
audioContext.close();
|
audioContext.close();
|
||||||
log("Closed AudioContext");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset all references
|
|
||||||
audioContext = null;
|
audioContext = null;
|
||||||
analyser = null;
|
analyser = null;
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
dataArray = null;
|
dataArray = null;
|
||||||
currentAudioElement = null;
|
|
||||||
isSourceConnected = false;
|
isSourceConnected = false;
|
||||||
};
|
});
|
||||||
|
|
||||||
// Register cleanup
|
|
||||||
unloads.add(completeCleanup);
|
|
||||||
|
|
||||||
// Start initialization
|
|
||||||
initialize();
|
|
||||||
|
|||||||
Reference in New Issue
Block a user