pitch and speed in settings, back to ko-fi

This commit is contained in:
Eduard Prigoana 2026-02-09 14:04:40 +00:00
parent af1c0fc1ee
commit f81973af88
6 changed files with 227 additions and 13 deletions

View file

@ -3082,6 +3082,70 @@
</label>
</div>
<!-- Playback Speed Control -->
<div class="setting-item">
<div class="info">
<span class="label">Playback Speed</span>
<span class="description">Adjust playback speed (0.5x - 2.0x)</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"
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"
/>
<span
id="pitch-shift-value"
style="min-width: 50px; text-align: right; font-family: var(--font-family)"
>0</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">
@ -3655,7 +3719,7 @@
flex-wrap: wrap;
"
>
<a href="https://pally.gg/p/monochrome">
<a href="https://ko-fi.com/monochromemusic">
<button id="donate-btn" class="btn-secondary">Donate to Monochrome</button>
</a>
</div>
@ -3930,7 +3994,9 @@
If Monochrome has been useful to you and you're able to, consider making a donation. <br />
It helps pay for the domain, and you get to support us :)
</p>
<button id="donate-btn-page" class="btn-secondary">Donate to Monochrome</button>
<a href="https://ko-fi.com/monochromemusic">
<button id="donate-btn-page" class="btn-secondary">Donate to Monochrome</button>
</a>
</div>
</div>
</main>

View file

@ -95,6 +95,16 @@ 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

@ -9,7 +9,13 @@ import {
getTrackYearDisplay,
createQualityBadgeHTML,
} from './utils.js';
import { queueManager, replayGainSettings, trackDateSettings, exponentialVolumeSettings } from './storage.js';
import {
queueManager,
replayGainSettings,
trackDateSettings,
exponentialVolumeSettings,
audioEffectsSettings,
} from './storage.js';
import { audioContextManager } from './audio-context.js';
export class Player {
@ -109,6 +115,56 @@ export class Player {
this.audio.volume = Math.max(0, Math.min(1, effectiveVolume));
}
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;
}
}
setPlaybackSpeed(speed) {
const validSpeed = Math.max(0.5, Math.min(2.0, 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) {
@ -387,6 +443,7 @@ export class Player {
this.currentRgValues = null;
this.applyReplayGain();
this.applyAudioEffects();
this.audio.src = streamUrl;
@ -425,6 +482,7 @@ export class Player {
streamUrl = URL.createObjectURL(track.file);
this.currentRgValues = null; // No replaygain for local files yet
this.applyReplayGain();
this.applyAudioEffects();
this.audio.src = streamUrl;

View file

@ -26,6 +26,7 @@ import {
fontSettings,
monoAudioSettings,
exponentialVolumeSettings,
audioEffectsSettings,
} from './storage.js';
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
import { getButterchurnPresets } from './visualizers/butterchurn.js';
@ -786,6 +787,43 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
// ========================================
// Audio Effects (Playback Speed & Pitch)
// ========================================
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';
playbackSpeedSlider.addEventListener('input', (e) => {
const speed = e.target.value;
playbackSpeedValue.textContent = speed + 'x';
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;
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);
});
}
// ========================================
// 16-Band Equalizer Settings
// ========================================

View file

@ -895,6 +895,55 @@ 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)
getSpeed() {
try {
const val = parseFloat(localStorage.getItem(this.SPEED_KEY));
return isNaN(val) ? 1.0 : Math.max(0.5, Math.min(2.0, val));
} catch {
return 1.0;
}
},
setSpeed(speed) {
const validSpeed = Math.max(0.5, Math.min(2.0, 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',

13
todo.md
View file

@ -3,17 +3,10 @@
Sorted by ease of implementation (easiest to hardest):
- [ ] Update notifications: Add ability to show the update popup in settings, with an option to automatically update (enabled by default)
- [ ] Volume curve option: Add setting to switch between exponential and linear volume scaling
- [ ] Custom fonts: Allow users to change fonts in settings or add custom fonts from Google Fonts or direct links
- [ ] Audio effects: Add ability to change pitch and playback speed, plus effects like reverb, delay, and bitcrushing
- [ ] Audio effects: Add ability to change pitch and playback speed,
plus effects like reverb, delay, and bitcrushing
- [ ] Customizable EQ: Allow users to change the number of EQ bands and their range (-30 to 30), with a drag-to-adjust interface similar to FL Studio's velocity editor
[ ] SoundCloud support: Integrate SoundCloud through SoundCloak
[ ] Qobuz support: Integrate Qobuz through Qobuz-DL
---
## Bug Fixes
- [ ] Next track and casting buttons overlap on some resolutions
[ ] Qobuz support: Integrate Qobuz through Qobuz-DL