diff --git a/index.html b/index.html index 26f0d7f..8c02e38 100644 --- a/index.html +++ b/index.html @@ -3105,66 +3105,40 @@
Playback Speed - Adjust playback speed (0.5x - 2.0x) + Adjust playback speed (0.01x - 100x)
- 1.0x -
-
- - -
-
- Pitch Shift - Shift pitch up or down (-12 to +12 semitones). Note: Disable "Preserve - Pitch" to hear the effect -
-
- 0 + x
-
-
- Preserve Pitch - Keep original pitch when changing playback speed -
- -
-
diff --git a/js/audio-context.js b/js/audio-context.js index ef30140..aff161e 100644 --- a/js/audio-context.js +++ b/js/audio-context.js @@ -95,16 +95,6 @@ class AudioContextManager { this._loadSettings(); } - /** - * Calculate playback rate for pitch shift - * @param {number} semitones - Pitch shift in semitones (-12 to +12) - * @returns {number} - Playback rate multiplier - */ - getPitchRate(semitones) { - // Convert semitones to playback rate: rate = 2^(semitones/12) - return Math.pow(2, semitones / 12); - } - /** * Register a callback to be called when audio graph is reconnected * @param {Function} callback - Function to call when graph changes diff --git a/js/player.js b/js/player.js index 643a805..19eec19 100644 --- a/js/player.js +++ b/js/player.js @@ -117,54 +117,17 @@ export class Player { applyAudioEffects() { const speed = audioEffectsSettings.getSpeed(); - const pitchShift = audioEffectsSettings.getPitch(); - const preservePitch = audioEffectsSettings.getPreservePitch(); - - // Calculate pitch rate: 2^(semitones/12) - const pitchRate = Math.pow(2, pitchShift / 12); - - // When preservePitch is enabled, playbackRate only affects speed - // When disabled, playbackRate affects both speed and pitch - // To shift pitch without changing speed (when preservePitch is off), - // we need to compensate the speed - if (preservePitch) { - // Pitch is preserved, playbackRate controls speed only - if (this.audio.playbackRate !== speed) { - this.audio.playbackRate = speed; - } - } else { - // playbackRate affects both speed and pitch - // Combine speed and pitch: finalRate = speed * pitchRate - const finalRate = speed * pitchRate; - if (this.audio.playbackRate !== finalRate) { - this.audio.playbackRate = finalRate; - } - } - - // Apply pitch preservation setting - if (this.audio.preservesPitch !== preservePitch) { - this.audio.preservesPitch = preservePitch; + if (this.audio.playbackRate !== speed) { + this.audio.playbackRate = speed; } } setPlaybackSpeed(speed) { - const validSpeed = Math.max(0.5, Math.min(2.0, parseFloat(speed) || 1.0)); + const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0)); audioEffectsSettings.setSpeed(validSpeed); this.applyAudioEffects(); } - setPitchShift(semitones) { - const validPitch = Math.max(-12, Math.min(12, parseInt(semitones, 10) || 0)); - audioEffectsSettings.setPitch(validPitch); - // For now, pitch shift is informational only - // Full implementation would require Web Audio API pitch shifting - } - - setPreservePitch(enabled) { - audioEffectsSettings.setPreservePitch(enabled); - this.applyAudioEffects(); - } - loadQueueState() { const savedState = queueManager.getQueue(); if (savedState) { diff --git a/js/settings.js b/js/settings.js index 39bf616..c623866 100644 --- a/js/settings.js +++ b/js/settings.js @@ -27,6 +27,7 @@ import { monoAudioSettings, exponentialVolumeSettings, audioEffectsSettings, + settingsUiState, pwaUpdateSettings, } from './storage.js'; import { audioContextManager, EQ_PRESETS } from './audio-context.js'; @@ -37,6 +38,16 @@ import { syncManager } from './accounts/pocketbase.js'; import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js'; export function initializeSettings(scrobbler, player, api, ui) { + // Restore last active settings tab + const savedTab = settingsUiState.getActiveTab(); + const settingsTab = document.querySelector(`.settings-tab[data-tab="${savedTab}"]`); + if (settingsTab) { + document.querySelectorAll('.settings-tab').forEach((t) => t.classList.remove('active')); + document.querySelectorAll('.settings-tab-content').forEach((c) => c.classList.remove('active')); + settingsTab.classList.add('active'); + document.getElementById(`settings-tab-${savedTab}`)?.classList.add('active'); + } + // Initialize account system UI & Settings authManager.updateUI(authManager.user); @@ -789,40 +800,37 @@ export function initializeSettings(scrobbler, player, api, ui) { } // ======================================== - // Audio Effects (Playback Speed & Pitch) + // Audio Effects (Playback Speed) // ======================================== const playbackSpeedSlider = document.getElementById('playback-speed-slider'); - const playbackSpeedValue = document.getElementById('playback-speed-value'); - if (playbackSpeedSlider && playbackSpeedValue) { - playbackSpeedSlider.value = audioEffectsSettings.getSpeed(); - playbackSpeedValue.textContent = playbackSpeedSlider.value + 'x'; + const playbackSpeedInput = document.getElementById('playback-speed-input'); + if (playbackSpeedSlider && playbackSpeedInput) { + const currentSpeed = audioEffectsSettings.getSpeed(); + // Clamp slider to its range (0.25-4), but show actual value in input + playbackSpeedSlider.value = Math.max(0.25, Math.min(4.0, currentSpeed)); + playbackSpeedInput.value = currentSpeed; + // Slider only controls 0.25-4 range playbackSpeedSlider.addEventListener('input', (e) => { - const speed = e.target.value; - playbackSpeedValue.textContent = speed + 'x'; + const speed = parseFloat(e.target.value) || 1.0; + playbackSpeedInput.value = speed; player.setPlaybackSpeed(speed); }); - } - const pitchShiftSlider = document.getElementById('pitch-shift-slider'); - const pitchShiftValue = document.getElementById('pitch-shift-value'); - if (pitchShiftSlider && pitchShiftValue) { - pitchShiftSlider.value = audioEffectsSettings.getPitch(); - pitchShiftValue.textContent = (pitchShiftSlider.value > 0 ? '+' : '') + pitchShiftSlider.value; + // Input allows full 0.01-100 range + const handleInputChange = () => { + const speed = parseFloat(playbackSpeedInput.value) || 1.0; + const validSpeed = Math.max(0.01, Math.min(100, speed)); + playbackSpeedInput.value = validSpeed; + // Only update slider if value is within slider range + if (validSpeed >= 0.25 && validSpeed <= 4.0) { + playbackSpeedSlider.value = validSpeed; + } + player.setPlaybackSpeed(validSpeed); + }; - pitchShiftSlider.addEventListener('input', (e) => { - const pitch = e.target.value; - pitchShiftValue.textContent = (pitch > 0 ? '+' : '') + pitch; - player.setPitchShift(pitch); - }); - } - - const preservePitchToggle = document.getElementById('preserve-pitch-toggle'); - if (preservePitchToggle) { - preservePitchToggle.checked = audioEffectsSettings.getPreservePitch(); - preservePitchToggle.addEventListener('change', (e) => { - player.setPreservePitch(e.target.checked); - }); + playbackSpeedInput.addEventListener('change', handleInputChange); + playbackSpeedInput.addEventListener('blur', handleInputChange); } // ======================================== @@ -2034,7 +2042,7 @@ function filterSettings(query) { const allTabs = settingsPage.querySelectorAll('.settings-tab'); if (!query) { - // Reset: show active tab only + // Reset: show saved active tab allTabContents.forEach((content) => { content.classList.remove('active'); }); @@ -2042,12 +2050,17 @@ function filterSettings(query) { tab.classList.remove('active'); }); - // Restore first tab as active - const firstTab = allTabs[0]; - const firstContent = allTabContents[0]; - if (firstTab && firstContent) { - firstTab.classList.add('active'); - firstContent.classList.add('active'); + // Restore saved tab as active + const savedTabName = settingsUiState.getActiveTab(); + const savedTab = document.querySelector(`.settings-tab[data-tab="${savedTabName}"]`); + const savedContent = document.getElementById(`settings-tab-${savedTabName}`); + if (savedTab && savedContent) { + savedTab.classList.add('active'); + savedContent.classList.add('active'); + } else if (allTabs[0] && allTabContents[0]) { + // Fallback to first tab if saved tab not found + allTabs[0].classList.add('active'); + allTabContents[0].classList.add('active'); } // Show all settings groups and items diff --git a/js/storage.js b/js/storage.js index 9953e16..e554188 100644 --- a/js/storage.js +++ b/js/storage.js @@ -897,78 +897,36 @@ export const exponentialVolumeSettings = { export const audioEffectsSettings = { SPEED_KEY: 'audio-effects-speed', - PITCH_KEY: 'audio-effects-pitch', - PRESERVE_PITCH_KEY: 'audio-effects-preserve-pitch', - // Playback speed (0.5 to 2.0, default 1.0) + // Playback speed (0.01 to 100, default 1.0) getSpeed() { try { const val = parseFloat(localStorage.getItem(this.SPEED_KEY)); - return isNaN(val) ? 1.0 : Math.max(0.5, Math.min(2.0, val)); + return isNaN(val) ? 1.0 : Math.max(0.01, Math.min(100, val)); } catch { return 1.0; } }, setSpeed(speed) { - const validSpeed = Math.max(0.5, Math.min(2.0, parseFloat(speed) || 1.0)); + const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0)); localStorage.setItem(this.SPEED_KEY, validSpeed.toString()); }, - - // Pitch shift (-12 to +12 semitones, default 0) - getPitch() { - try { - const val = parseInt(localStorage.getItem(this.PITCH_KEY), 10); - return isNaN(val) ? 0 : Math.max(-12, Math.min(12, val)); - } catch { - return 0; - } - }, - - setPitch(pitch) { - const validPitch = Math.max(-12, Math.min(12, parseInt(pitch, 10) || 0)); - localStorage.setItem(this.PITCH_KEY, validPitch.toString()); - }, - - // Preserve pitch when changing speed (default true) - getPreservePitch() { - try { - return localStorage.getItem(this.PRESERVE_PITCH_KEY) !== 'false'; - } catch { - return true; - } - }, - - setPreservePitch(enabled) { - localStorage.setItem(this.PRESERVE_PITCH_KEY, enabled ? 'true' : 'false'); - }, }; -export const sidebarSettings = { - STORAGE_KEY: 'monochrome-sidebar-collapsed', +export const settingsUiState = { + ACTIVE_TAB_KEY: 'settings-active-tab', - isCollapsed() { + getActiveTab() { try { - return localStorage.getItem(this.STORAGE_KEY) === 'true'; + return localStorage.getItem(this.ACTIVE_TAB_KEY) || 'appearance'; } catch { - return false; + return 'appearance'; } }, - setCollapsed(collapsed) { - localStorage.setItem(this.STORAGE_KEY, collapsed ? 'true' : 'false'); - }, - - restoreState() { - const isCollapsed = this.isCollapsed(); - if (isCollapsed) { - document.body.classList.add('sidebar-collapsed'); - const toggleBtn = document.getElementById('sidebar-toggle'); - if (toggleBtn) { - toggleBtn.innerHTML = - ''; - } - } + setActiveTab(tab) { + localStorage.setItem(this.ACTIVE_TAB_KEY, tab); }, }; @@ -1002,6 +960,34 @@ export const queueManager = { }, }; +export const sidebarSettings = { + STORAGE_KEY: 'monochrome-sidebar-collapsed', + + isCollapsed() { + try { + return localStorage.getItem(this.STORAGE_KEY) === 'true'; + } catch { + return false; + } + }, + + setCollapsed(collapsed) { + localStorage.setItem(this.STORAGE_KEY, collapsed ? 'true' : 'false'); + }, + + restoreState() { + const isCollapsed = this.isCollapsed(); + if (isCollapsed) { + document.body.classList.add('sidebar-collapsed'); + const toggleBtn = document.getElementById('sidebar-toggle'); + if (toggleBtn) { + toggleBtn.innerHTML = + ''; + } + } + }, +}; + export const listenBrainzSettings = { ENABLED_KEY: 'listenbrainz-enabled', TOKEN_KEY: 'listenbrainz-token', diff --git a/js/ui-interactions.js b/js/ui-interactions.js index 3abaa3d..3fea1f5 100644 --- a/js/ui-interactions.js +++ b/js/ui-interactions.js @@ -465,6 +465,11 @@ export function initializeUIInteractions(player, api, ui) { const contentId = `settings-tab-${tab.dataset.tab}`; document.getElementById(contentId)?.classList.add('active'); + + // Save active tab + import('./storage.js').then(({ settingsUiState }) => { + settingsUiState.setActiveTab(tab.dataset.tab); + }); }); });