Merge branch 'main' of github.com:monochrome-music/monochrome
This commit is contained in:
commit
020176167e
5 changed files with 116 additions and 81 deletions
|
|
@ -545,8 +545,6 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
|||
if (player.isFallbackInProgress || canFallback) {
|
||||
return;
|
||||
}
|
||||
console.warn('Skipping to next track due to playback error');
|
||||
setTimeout(() => player.playNext(), 1000);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -949,9 +949,9 @@ export const visualizerSettings = {
|
|||
|
||||
getPreset() {
|
||||
try {
|
||||
return localStorage.getItem(this.PRESET_KEY) || 'butterchurn';
|
||||
return localStorage.getItem(this.PRESET_KEY) || 'kawarp';
|
||||
} catch {
|
||||
return 'butterchurn';
|
||||
return 'kawarp';
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
44
js/ui.js
44
js/ui.js
|
|
@ -36,6 +36,7 @@ import { syncManager } from './accounts/pocketbase.js';
|
|||
import { authManager } from './accounts/auth.js';
|
||||
import { partyManager } from './listening-party.js';
|
||||
import { Visualizer } from './visualizer.js';
|
||||
import { audioContextManager } from './audio-context.js';
|
||||
import { navigate } from './router.js';
|
||||
import { sidePanelManager } from './side-panel.js';
|
||||
import {
|
||||
|
|
@ -1318,7 +1319,7 @@ export class UIRenderer {
|
|||
|
||||
async showFullscreenCover(track, nextTrack, lyricsManager, activeElement) {
|
||||
if (!track) return;
|
||||
this.fullscreenVisualizerSuppressed = isMobileFullscreenViewport();
|
||||
this.fullscreenVisualizerSuppressed = false;
|
||||
if (window.location.hash !== '#fullscreen') {
|
||||
window.history.pushState({ fullscreen: true }, '', '#fullscreen');
|
||||
}
|
||||
|
|
@ -1600,7 +1601,17 @@ export class UIRenderer {
|
|||
}
|
||||
|
||||
async startFullscreenVisualizer(activeElement, overlay) {
|
||||
if (!activeElement) return;
|
||||
if (!activeElement || !overlay) return false;
|
||||
|
||||
if (audioContextManager.isReady()) {
|
||||
audioContextManager.changeSource(activeElement);
|
||||
await audioContextManager.resume();
|
||||
} else {
|
||||
audioContextManager.init(activeElement);
|
||||
if (audioContextManager.isReady()) {
|
||||
await audioContextManager.resume();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.visualizer) {
|
||||
const canvas = document.getElementById('visualizer-canvas');
|
||||
|
|
@ -1608,24 +1619,28 @@ export class UIRenderer {
|
|||
this.visualizer = new Visualizer(canvas, activeElement);
|
||||
await this.visualizer.initPresets();
|
||||
}
|
||||
} else {
|
||||
this.visualizer.audio = activeElement;
|
||||
}
|
||||
|
||||
if (this.visualizer) {
|
||||
await this.visualizer.start();
|
||||
overlay.classList.add('visualizer-active');
|
||||
const started = await this.visualizer.start();
|
||||
overlay.classList.toggle('visualizer-active', started);
|
||||
return started;
|
||||
}
|
||||
|
||||
overlay.classList.remove('visualizer-active');
|
||||
return false;
|
||||
}
|
||||
|
||||
async ensureVisualizerPermission(activeElement, overlay, { closeOnCancel = false } = {}) {
|
||||
if (localStorage.getItem('epilepsy-warning-dismissed') === 'true') {
|
||||
await this.startFullscreenVisualizer(activeElement, overlay);
|
||||
return true;
|
||||
return await this.startFullscreenVisualizer(activeElement, overlay);
|
||||
}
|
||||
|
||||
const modal = document.getElementById('epilepsy-warning-modal');
|
||||
if (!modal) {
|
||||
await this.startFullscreenVisualizer(activeElement, overlay);
|
||||
return true;
|
||||
return await this.startFullscreenVisualizer(activeElement, overlay);
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
|
|
@ -1637,8 +1652,7 @@ export class UIRenderer {
|
|||
acceptBtn.onclick = async () => {
|
||||
modal.classList.remove('active');
|
||||
localStorage.setItem('epilepsy-warning-dismissed', 'true');
|
||||
await this.startFullscreenVisualizer(activeElement, overlay);
|
||||
resolve(true);
|
||||
resolve(await this.startFullscreenVisualizer(activeElement, overlay));
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => {
|
||||
|
|
@ -1646,7 +1660,7 @@ export class UIRenderer {
|
|||
if (closeOnCancel) {
|
||||
this.closeFullscreenCover();
|
||||
}
|
||||
resolve(false);
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -1656,7 +1670,7 @@ export class UIRenderer {
|
|||
const visualizerBtn = document.getElementById('fs-visualizer-btn');
|
||||
const toggleBtn = document.getElementById('toggle-ui-btn');
|
||||
const isVideoTrack = this.player?.currentTrack?.type === 'video';
|
||||
const enabled = !isVideoTrack && !this.fullscreenVisualizerSuppressed && !isMobileFullscreenViewport();
|
||||
const enabled = !isVideoTrack && visualizerSettings.isEnabled() && !this.fullscreenVisualizerSuppressed;
|
||||
|
||||
if (!overlay) return;
|
||||
|
||||
|
|
@ -1681,8 +1695,10 @@ export class UIRenderer {
|
|||
}
|
||||
|
||||
const allowed = await this.ensureVisualizerPermission(activeElement, overlay, { closeOnCancel });
|
||||
if (!allowed) {
|
||||
this.fullscreenVisualizerSuppressed = true;
|
||||
if (allowed !== true) {
|
||||
if (allowed === null) {
|
||||
this.fullscreenVisualizerSuppressed = true;
|
||||
}
|
||||
overlay.classList.remove('visualizer-active');
|
||||
if (this.visualizer) {
|
||||
this.visualizer.stop();
|
||||
|
|
|
|||
130
js/visualizer.js
130
js/visualizer.js
|
|
@ -86,8 +86,8 @@ export class Visualizer {
|
|||
this.audioContext = audioContextManager.getAudioContext();
|
||||
this.analyser = audioContextManager.getAnalyser();
|
||||
|
||||
if (this.analyser) {
|
||||
this.bufferLength = this.analyser.frequencyBinCount;
|
||||
this.bufferLength = this.analyser?.frequencyBinCount || 512;
|
||||
if (!this.dataArray || this.dataArray.length !== this.bufferLength) {
|
||||
this.dataArray = new Uint8Array(this.bufferLength);
|
||||
}
|
||||
}
|
||||
|
|
@ -153,23 +153,24 @@ export class Visualizer {
|
|||
}
|
||||
|
||||
async start() {
|
||||
if (this.isActive) return;
|
||||
if (this.isActive) return true;
|
||||
|
||||
if (!this.ctx) {
|
||||
this.initContext();
|
||||
}
|
||||
if (!this.audioContext) {
|
||||
if (!this.audioContext && !this.analyser) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
if (!this.analyser) {
|
||||
return;
|
||||
const canRunWithoutAnalyser = !!this.activePreset?.managesOwnContext;
|
||||
if (!this.analyser && !canRunWithoutAnalyser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isActive = true;
|
||||
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
if (this.audioContext?.state === 'suspended') {
|
||||
await this.audioContext.resume();
|
||||
}
|
||||
|
||||
this.updateDimming();
|
||||
|
|
@ -182,12 +183,19 @@ export class Visualizer {
|
|||
// Initialize presets that need lazy init (Butterchurn, Kawarp)
|
||||
if (this.activePreset.lazyInit) {
|
||||
const sourceNode = audioContextManager.getSourceNode();
|
||||
this.activePreset.lazyInit(this.canvas, this.audioContext, sourceNode).then(() => {
|
||||
this.resize();
|
||||
});
|
||||
await this.activePreset.lazyInit(this.canvas, this.audioContext, sourceNode);
|
||||
this.resize();
|
||||
}
|
||||
|
||||
if (this.activePreset.managesOwnContext && this.activePreset.isInitialized === false) {
|
||||
this.isActive = false;
|
||||
this.canvas.style.display = 'none';
|
||||
window.removeEventListener('resize', this._resizeBound);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.animate();
|
||||
return true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
|
@ -223,57 +231,48 @@ export class Visualizer {
|
|||
if (!this.isActive) return;
|
||||
this.animationId = requestAnimationFrame(this.animate);
|
||||
|
||||
// ===== AUDIO ANALYSIS =====
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
|
||||
// Bass (dynamic bins based on sample rate)
|
||||
const volume = 10 * Math.max(this.audio.volume, 0.1);
|
||||
|
||||
// Robust bass detection: sum bins up to ~250Hz
|
||||
const binSize = this.audioContext.sampleRate / this.analyser.fftSize;
|
||||
const startBin = 1; // Skip DC offset
|
||||
// Calculate how many bins cover the bass range (up to 250Hz)
|
||||
let numBins = Math.floor(250 / binSize);
|
||||
if (numBins < 1) numBins = 1; // Ensure at least one bin is checked
|
||||
|
||||
let maxVal = 0;
|
||||
for (let i = 0; i < numBins && startBin + i < this.dataArray.length; i++) {
|
||||
const val = this.dataArray[startBin + i];
|
||||
if (val > maxVal) maxVal = val;
|
||||
}
|
||||
|
||||
// Normalize: (Max / 255) / Volume
|
||||
let bass = maxVal / 255 / volume;
|
||||
|
||||
const intensity = bass * bass * 10;
|
||||
const stats = this.stats;
|
||||
|
||||
stats.energyAverage = stats.energyAverage * 0.99 + intensity * 0.01;
|
||||
stats.upbeatSmoother = stats.upbeatSmoother * 0.92 + intensity * 0.08;
|
||||
if (this.analyser && this.dataArray && this.audioContext) {
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
|
||||
// ===== SENSITIVITY =====
|
||||
let sensitivity = visualizerSettings.getSensitivity();
|
||||
if (visualizerSettings.isSmartIntensityEnabled()) {
|
||||
if (stats.energyAverage > 0.4) {
|
||||
sensitivity = 0.7;
|
||||
} else if (stats.energyAverage > 0.2) {
|
||||
sensitivity = 0.1 + ((stats.energyAverage - 0.2) / 0.2) * 0.6;
|
||||
} else {
|
||||
sensitivity = 0.1;
|
||||
const volume = 10 * Math.max(this.audio.volume, 0.1);
|
||||
const binSize = this.audioContext.sampleRate / this.analyser.fftSize;
|
||||
const startBin = 1;
|
||||
let numBins = Math.floor(250 / binSize);
|
||||
if (numBins < 1) numBins = 1;
|
||||
|
||||
let maxVal = 0;
|
||||
for (let i = 0; i < numBins && startBin + i < this.dataArray.length; i++) {
|
||||
const val = this.dataArray[startBin + i];
|
||||
if (val > maxVal) maxVal = val;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== KICK DETECTION =====
|
||||
const now = performance.now();
|
||||
let threshold = stats.energyAverage < 0.3 ? 0.5 + (0.3 - stats.energyAverage) * 2 : 0.5;
|
||||
const bass = maxVal / 255 / volume;
|
||||
const intensity = bass * bass * 10;
|
||||
|
||||
// Lower threshold for more responsive kick
|
||||
if (intensity > threshold * 0.7) {
|
||||
if (intensity > stats.lastIntensity + 0.03 && now - stats.lastBeatTime > 50) {
|
||||
stats.kick = 1.0;
|
||||
stats.lastBeatTime = now;
|
||||
} else {
|
||||
if (stats.upbeatSmoother > 0.6 && stats.energyAverage > 0.4) {
|
||||
stats.energyAverage = stats.energyAverage * 0.99 + intensity * 0.01;
|
||||
stats.upbeatSmoother = stats.upbeatSmoother * 0.92 + intensity * 0.08;
|
||||
|
||||
let sensitivity = visualizerSettings.getSensitivity();
|
||||
if (visualizerSettings.isSmartIntensityEnabled()) {
|
||||
if (stats.energyAverage > 0.4) {
|
||||
sensitivity = 0.7;
|
||||
} else if (stats.energyAverage > 0.2) {
|
||||
sensitivity = 0.1 + ((stats.energyAverage - 0.2) / 0.2) * 0.6;
|
||||
} else {
|
||||
sensitivity = 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
const threshold = stats.energyAverage < 0.3 ? 0.5 + (0.3 - stats.energyAverage) * 2 : 0.5;
|
||||
|
||||
if (intensity > threshold * 0.7) {
|
||||
if (intensity > stats.lastIntensity + 0.03 && now - stats.lastBeatTime > 50) {
|
||||
stats.kick = 1.0;
|
||||
stats.lastBeatTime = now;
|
||||
} else if (stats.upbeatSmoother > 0.6 && stats.energyAverage > 0.4) {
|
||||
const upbeatLevel = (stats.upbeatSmoother - 0.6) / 0.4;
|
||||
if (stats.kick < upbeatLevel) {
|
||||
stats.kick = upbeatLevel;
|
||||
|
|
@ -283,14 +282,21 @@ export class Visualizer {
|
|||
} else {
|
||||
stats.kick *= 0.9;
|
||||
}
|
||||
} else {
|
||||
stats.kick *= 0.95;
|
||||
}
|
||||
} else {
|
||||
stats.kick *= 0.95;
|
||||
}
|
||||
|
||||
stats.lastIntensity = intensity;
|
||||
stats.intensity = intensity;
|
||||
stats.sensitivity = sensitivity;
|
||||
stats.lastIntensity = intensity;
|
||||
stats.intensity = intensity;
|
||||
stats.sensitivity = sensitivity;
|
||||
} else {
|
||||
stats.kick *= 0.92;
|
||||
stats.intensity *= 0.92;
|
||||
stats.energyAverage *= 0.98;
|
||||
stats.upbeatSmoother *= 0.95;
|
||||
stats.sensitivity = visualizerSettings.getSensitivity();
|
||||
this.dataArray?.fill(0);
|
||||
}
|
||||
|
||||
// ===== COLORS (CACHED) =====
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff';
|
||||
|
|
|
|||
17
styles.css
17
styles.css
|
|
@ -10656,10 +10656,15 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
|||
}
|
||||
|
||||
#fullscreen-cover-overlay .fullscreen-top-actions {
|
||||
display: none;
|
||||
display: flex;
|
||||
top: calc(0.85rem + env(safe-area-inset-top));
|
||||
left: auto;
|
||||
right: calc(3.85rem + env(safe-area-inset-right));
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
#fullscreen-cover-overlay .fullscreen-lyrics-toggle,
|
||||
#fullscreen-cover-overlay #close-fullscreen-cover-btn,
|
||||
#fullscreen-cover-overlay #toggle-ui-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -10668,6 +10673,16 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn {
|
||||
display: flex !important;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
#fullscreen-cover-overlay .fullscreen-main-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
Loading…
Reference in a new issue