remove pitch changing and allow more granular playback speed control, remember what setting tab youre on

This commit is contained in:
Eduard Prigoana 2026-02-10 19:57:18 +00:00
parent 63e9a71456
commit ea005c68ad
6 changed files with 113 additions and 182 deletions

View file

@ -3105,66 +3105,40 @@
<div class="setting-item">
<div class="info">
<span class="label">Playback Speed</span>
<span class="description">Adjust playback speed (0.5x - 2.0x)</span>
<span class="description">Adjust playback speed (0.01x - 100x)</span>
</div>
<div style="display: flex; align-items: center; gap: 12px">
<input
type="range"
id="playback-speed-slider"
min="0.5"
max="2.0"
step="0.1"
min="0.25"
max="4.0"
step="0.01"
value="1.0"
style="width: 150px"
/>
<span
id="playback-speed-value"
style="min-width: 50px; text-align: right; font-family: var(--font-family)"
>1.0x</span
>
</div>
</div>
<!-- Pitch Shift Control -->
<div class="setting-item">
<div class="info">
<span class="label">Pitch Shift</span>
<span class="description"
>Shift pitch up or down (-12 to +12 semitones). Note: Disable "Preserve
Pitch" to hear the effect</span
>
</div>
<div style="display: flex; align-items: center; gap: 12px">
<input
type="range"
id="pitch-shift-slider"
min="-12"
max="12"
step="1"
value="0"
style="width: 150px"
type="number"
id="playback-speed-input"
min="0.01"
max="100"
step="0.01"
value="1.0"
style="
width: 80px;
text-align: center;
font-family: var(--font-family);
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
"
/>
<span
id="pitch-shift-value"
style="min-width: 50px; text-align: right; font-family: var(--font-family)"
>0</span
>
<span style="font-family: var(--font-family)">x</span>
</div>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Preserve Pitch</span>
<span class="description"
>Keep original pitch when changing playback speed</span
>
</div>
<label class="toggle-switch">
<input type="checkbox" id="preserve-pitch-toggle" checked />
<span class="slider"></span>
</label>
</div>
<!-- 16-Band Equalizer -->
<div class="setting-item">
<div class="info">

View file

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

View file

@ -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) {

View file

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

View file

@ -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 =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
}
}
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 =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
}
}
},
};
export const listenBrainzSettings = {
ENABLED_KEY: 'listenbrainz-enabled',
TOKEN_KEY: 'listenbrainz-token',

View file

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