remove pitch changing and allow more granular playback speed control, remember what setting tab youre on
This commit is contained in:
parent
63e9a71456
commit
ea005c68ad
6 changed files with 113 additions and 182 deletions
68
index.html
68
index.html
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
43
js/player.js
43
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue