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) { if (player.isFallbackInProgress || canFallback) {
return; 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() { getPreset() {
try { try {
return localStorage.getItem(this.PRESET_KEY) || 'butterchurn'; return localStorage.getItem(this.PRESET_KEY) || 'kawarp';
} catch { } catch {
return 'butterchurn'; return 'kawarp';
} }
}, },

View file

@ -36,6 +36,7 @@ import { syncManager } from './accounts/pocketbase.js';
import { authManager } from './accounts/auth.js'; import { authManager } from './accounts/auth.js';
import { partyManager } from './listening-party.js'; import { partyManager } from './listening-party.js';
import { Visualizer } from './visualizer.js'; import { Visualizer } from './visualizer.js';
import { audioContextManager } from './audio-context.js';
import { navigate } from './router.js'; import { navigate } from './router.js';
import { sidePanelManager } from './side-panel.js'; import { sidePanelManager } from './side-panel.js';
import { import {
@ -1318,7 +1319,7 @@ export class UIRenderer {
async showFullscreenCover(track, nextTrack, lyricsManager, activeElement) { async showFullscreenCover(track, nextTrack, lyricsManager, activeElement) {
if (!track) return; if (!track) return;
this.fullscreenVisualizerSuppressed = isMobileFullscreenViewport(); this.fullscreenVisualizerSuppressed = false;
if (window.location.hash !== '#fullscreen') { if (window.location.hash !== '#fullscreen') {
window.history.pushState({ fullscreen: true }, '', '#fullscreen'); window.history.pushState({ fullscreen: true }, '', '#fullscreen');
} }
@ -1600,7 +1601,17 @@ export class UIRenderer {
} }
async startFullscreenVisualizer(activeElement, overlay) { 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) { if (!this.visualizer) {
const canvas = document.getElementById('visualizer-canvas'); const canvas = document.getElementById('visualizer-canvas');
@ -1608,24 +1619,28 @@ export class UIRenderer {
this.visualizer = new Visualizer(canvas, activeElement); this.visualizer = new Visualizer(canvas, activeElement);
await this.visualizer.initPresets(); await this.visualizer.initPresets();
} }
} else {
this.visualizer.audio = activeElement;
} }
if (this.visualizer) { if (this.visualizer) {
await this.visualizer.start(); const started = await this.visualizer.start();
overlay.classList.add('visualizer-active'); overlay.classList.toggle('visualizer-active', started);
return started;
} }
overlay.classList.remove('visualizer-active');
return false;
} }
async ensureVisualizerPermission(activeElement, overlay, { closeOnCancel = false } = {}) { async ensureVisualizerPermission(activeElement, overlay, { closeOnCancel = false } = {}) {
if (localStorage.getItem('epilepsy-warning-dismissed') === 'true') { if (localStorage.getItem('epilepsy-warning-dismissed') === 'true') {
await this.startFullscreenVisualizer(activeElement, overlay); return await this.startFullscreenVisualizer(activeElement, overlay);
return true;
} }
const modal = document.getElementById('epilepsy-warning-modal'); const modal = document.getElementById('epilepsy-warning-modal');
if (!modal) { if (!modal) {
await this.startFullscreenVisualizer(activeElement, overlay); return await this.startFullscreenVisualizer(activeElement, overlay);
return true;
} }
return await new Promise((resolve) => { return await new Promise((resolve) => {
@ -1637,8 +1652,7 @@ export class UIRenderer {
acceptBtn.onclick = async () => { acceptBtn.onclick = async () => {
modal.classList.remove('active'); modal.classList.remove('active');
localStorage.setItem('epilepsy-warning-dismissed', 'true'); localStorage.setItem('epilepsy-warning-dismissed', 'true');
await this.startFullscreenVisualizer(activeElement, overlay); resolve(await this.startFullscreenVisualizer(activeElement, overlay));
resolve(true);
}; };
cancelBtn.onclick = () => { cancelBtn.onclick = () => {
@ -1646,7 +1660,7 @@ export class UIRenderer {
if (closeOnCancel) { if (closeOnCancel) {
this.closeFullscreenCover(); this.closeFullscreenCover();
} }
resolve(false); resolve(null);
}; };
}); });
} }
@ -1656,7 +1670,7 @@ export class UIRenderer {
const visualizerBtn = document.getElementById('fs-visualizer-btn'); const visualizerBtn = document.getElementById('fs-visualizer-btn');
const toggleBtn = document.getElementById('toggle-ui-btn'); const toggleBtn = document.getElementById('toggle-ui-btn');
const isVideoTrack = this.player?.currentTrack?.type === 'video'; const isVideoTrack = this.player?.currentTrack?.type === 'video';
const enabled = !isVideoTrack && !this.fullscreenVisualizerSuppressed && !isMobileFullscreenViewport(); const enabled = !isVideoTrack && visualizerSettings.isEnabled() && !this.fullscreenVisualizerSuppressed;
if (!overlay) return; if (!overlay) return;
@ -1681,8 +1695,10 @@ export class UIRenderer {
} }
const allowed = await this.ensureVisualizerPermission(activeElement, overlay, { closeOnCancel }); const allowed = await this.ensureVisualizerPermission(activeElement, overlay, { closeOnCancel });
if (!allowed) { if (allowed !== true) {
this.fullscreenVisualizerSuppressed = true; if (allowed === null) {
this.fullscreenVisualizerSuppressed = true;
}
overlay.classList.remove('visualizer-active'); overlay.classList.remove('visualizer-active');
if (this.visualizer) { if (this.visualizer) {
this.visualizer.stop(); this.visualizer.stop();

View file

@ -86,8 +86,8 @@ export class Visualizer {
this.audioContext = audioContextManager.getAudioContext(); this.audioContext = audioContextManager.getAudioContext();
this.analyser = audioContextManager.getAnalyser(); this.analyser = audioContextManager.getAnalyser();
if (this.analyser) { this.bufferLength = this.analyser?.frequencyBinCount || 512;
this.bufferLength = this.analyser.frequencyBinCount; if (!this.dataArray || this.dataArray.length !== this.bufferLength) {
this.dataArray = new Uint8Array(this.bufferLength); this.dataArray = new Uint8Array(this.bufferLength);
} }
} }
@ -153,23 +153,24 @@ export class Visualizer {
} }
async start() { async start() {
if (this.isActive) return; if (this.isActive) return true;
if (!this.ctx) { if (!this.ctx) {
this.initContext(); this.initContext();
} }
if (!this.audioContext) { if (!this.audioContext && !this.analyser) {
await this.init(); await this.init();
} }
if (!this.analyser) { const canRunWithoutAnalyser = !!this.activePreset?.managesOwnContext;
return; if (!this.analyser && !canRunWithoutAnalyser) {
return false;
} }
this.isActive = true; this.isActive = true;
if (this.audioContext.state === 'suspended') { if (this.audioContext?.state === 'suspended') {
this.audioContext.resume(); await this.audioContext.resume();
} }
this.updateDimming(); this.updateDimming();
@ -182,12 +183,19 @@ export class Visualizer {
// Initialize presets that need lazy init (Butterchurn, Kawarp) // Initialize presets that need lazy init (Butterchurn, Kawarp)
if (this.activePreset.lazyInit) { if (this.activePreset.lazyInit) {
const sourceNode = audioContextManager.getSourceNode(); const sourceNode = audioContextManager.getSourceNode();
this.activePreset.lazyInit(this.canvas, this.audioContext, sourceNode).then(() => { await this.activePreset.lazyInit(this.canvas, this.audioContext, sourceNode);
this.resize(); 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(); this.animate();
return true;
} }
stop() { stop() {
@ -223,57 +231,48 @@ export class Visualizer {
if (!this.isActive) return; if (!this.isActive) return;
this.animationId = requestAnimationFrame(this.animate); 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; const stats = this.stats;
stats.energyAverage = stats.energyAverage * 0.99 + intensity * 0.01; if (this.analyser && this.dataArray && this.audioContext) {
stats.upbeatSmoother = stats.upbeatSmoother * 0.92 + intensity * 0.08; this.analyser.getByteFrequencyData(this.dataArray);
// ===== SENSITIVITY ===== const volume = 10 * Math.max(this.audio.volume, 0.1);
let sensitivity = visualizerSettings.getSensitivity(); const binSize = this.audioContext.sampleRate / this.analyser.fftSize;
if (visualizerSettings.isSmartIntensityEnabled()) { const startBin = 1;
if (stats.energyAverage > 0.4) { let numBins = Math.floor(250 / binSize);
sensitivity = 0.7; if (numBins < 1) numBins = 1;
} else if (stats.energyAverage > 0.2) {
sensitivity = 0.1 + ((stats.energyAverage - 0.2) / 0.2) * 0.6; let maxVal = 0;
} else { for (let i = 0; i < numBins && startBin + i < this.dataArray.length; i++) {
sensitivity = 0.1; const val = this.dataArray[startBin + i];
if (val > maxVal) maxVal = val;
} }
}
// ===== KICK DETECTION ===== const bass = maxVal / 255 / volume;
const now = performance.now(); const intensity = bass * bass * 10;
let threshold = stats.energyAverage < 0.3 ? 0.5 + (0.3 - stats.energyAverage) * 2 : 0.5;
// Lower threshold for more responsive kick stats.energyAverage = stats.energyAverage * 0.99 + intensity * 0.01;
if (intensity > threshold * 0.7) { stats.upbeatSmoother = stats.upbeatSmoother * 0.92 + intensity * 0.08;
if (intensity > stats.lastIntensity + 0.03 && now - stats.lastBeatTime > 50) {
stats.kick = 1.0; let sensitivity = visualizerSettings.getSensitivity();
stats.lastBeatTime = now; if (visualizerSettings.isSmartIntensityEnabled()) {
} else { if (stats.energyAverage > 0.4) {
if (stats.upbeatSmoother > 0.6 && 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; const upbeatLevel = (stats.upbeatSmoother - 0.6) / 0.4;
if (stats.kick < upbeatLevel) { if (stats.kick < upbeatLevel) {
stats.kick = upbeatLevel; stats.kick = upbeatLevel;
@ -283,14 +282,21 @@ export class Visualizer {
} else { } else {
stats.kick *= 0.9; stats.kick *= 0.9;
} }
} else {
stats.kick *= 0.95;
} }
} else {
stats.kick *= 0.95;
}
stats.lastIntensity = intensity; stats.lastIntensity = intensity;
stats.intensity = intensity; stats.intensity = intensity;
stats.sensitivity = sensitivity; 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) ===== // ===== COLORS (CACHED) =====
const color = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff'; 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 { #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 .fullscreen-lyrics-toggle,
#fullscreen-cover-overlay #close-fullscreen-cover-btn,
#fullscreen-cover-overlay #toggle-ui-btn { #fullscreen-cover-overlay #toggle-ui-btn {
display: none !important; display: none !important;
} }
@ -10668,6 +10673,16 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
display: flex; 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 { #fullscreen-cover-overlay .fullscreen-main-view {
width: 100%; width: 100%;
height: 100%; height: 100%;