Merge branch 'main' of github.com:monochrome-music/monochrome

This commit is contained in:
Samidy 2026-04-06 22:49:18 +03:00
commit 020176167e
5 changed files with 116 additions and 81 deletions

View file

@ -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);
}
});

View file

@ -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';
}
},

View file

@ -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();

View file

@ -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';

View file

@ -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%;