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