pitch and speed in settings, back to ko-fi
This commit is contained in:
parent
af1c0fc1ee
commit
f81973af88
6 changed files with 227 additions and 13 deletions
70
index.html
70
index.html
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
60
js/player.js
60
js/player.js
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -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
13
todo.md
|
|
@ -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
|
||||
Loading…
Reference in a new issue