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>
|
</label>
|
||||||
</div>
|
</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 -->
|
<!-- 16-Band Equalizer -->
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
|
@ -3655,7 +3719,7 @@
|
||||||
flex-wrap: wrap;
|
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>
|
<button id="donate-btn" class="btn-secondary">Donate to Monochrome</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3930,7 +3994,9 @@
|
||||||
If Monochrome has been useful to you and you're able to, consider making a donation. <br />
|
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 :)
|
It helps pay for the domain, and you get to support us :)
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,16 @@ class AudioContextManager {
|
||||||
this._loadSettings();
|
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
|
* Register a callback to be called when audio graph is reconnected
|
||||||
* @param {Function} callback - Function to call when graph changes
|
* @param {Function} callback - Function to call when graph changes
|
||||||
|
|
|
||||||
60
js/player.js
60
js/player.js
|
|
@ -9,7 +9,13 @@ import {
|
||||||
getTrackYearDisplay,
|
getTrackYearDisplay,
|
||||||
createQualityBadgeHTML,
|
createQualityBadgeHTML,
|
||||||
} from './utils.js';
|
} 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';
|
import { audioContextManager } from './audio-context.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
|
|
@ -109,6 +115,56 @@ export class Player {
|
||||||
this.audio.volume = Math.max(0, Math.min(1, effectiveVolume));
|
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() {
|
loadQueueState() {
|
||||||
const savedState = queueManager.getQueue();
|
const savedState = queueManager.getQueue();
|
||||||
if (savedState) {
|
if (savedState) {
|
||||||
|
|
@ -387,6 +443,7 @@ export class Player {
|
||||||
|
|
||||||
this.currentRgValues = null;
|
this.currentRgValues = null;
|
||||||
this.applyReplayGain();
|
this.applyReplayGain();
|
||||||
|
this.applyAudioEffects();
|
||||||
|
|
||||||
this.audio.src = streamUrl;
|
this.audio.src = streamUrl;
|
||||||
|
|
||||||
|
|
@ -425,6 +482,7 @@ export class Player {
|
||||||
streamUrl = URL.createObjectURL(track.file);
|
streamUrl = URL.createObjectURL(track.file);
|
||||||
this.currentRgValues = null; // No replaygain for local files yet
|
this.currentRgValues = null; // No replaygain for local files yet
|
||||||
this.applyReplayGain();
|
this.applyReplayGain();
|
||||||
|
this.applyAudioEffects();
|
||||||
|
|
||||||
this.audio.src = streamUrl;
|
this.audio.src = streamUrl;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
fontSettings,
|
fontSettings,
|
||||||
monoAudioSettings,
|
monoAudioSettings,
|
||||||
exponentialVolumeSettings,
|
exponentialVolumeSettings,
|
||||||
|
audioEffectsSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
||||||
import { getButterchurnPresets } from './visualizers/butterchurn.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
|
// 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 = {
|
export const sidebarSettings = {
|
||||||
STORAGE_KEY: 'monochrome-sidebar-collapsed',
|
STORAGE_KEY: 'monochrome-sidebar-collapsed',
|
||||||
|
|
||||||
|
|
|
||||||
11
todo.md
11
todo.md
|
|
@ -3,17 +3,10 @@
|
||||||
Sorted by ease of implementation (easiest to hardest):
|
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)
|
- [ ] 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
|
- [ ] 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
|
[ ] SoundCloud support: Integrate SoundCloud through SoundCloak
|
||||||
[ ] Qobuz support: Integrate Qobuz through Qobuz-DL
|
[ ] Qobuz support: Integrate Qobuz through Qobuz-DL
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- [ ] Next track and casting buttons overlap on some resolutions
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue