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) {
|
if (player.isFallbackInProgress || canFallback) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.warn('Skipping to next track due to playback error');
|
|
||||||
setTimeout(() => player.playNext(), 1000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
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 { 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();
|
||||||
|
|
|
||||||
130
js/visualizer.js
130
js/visualizer.js
|
|
@ -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';
|
||||||
|
|
|
||||||
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 {
|
#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%;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue