Merge branch 'main' of github.com:monochrome-music/monochrome
This commit is contained in:
commit
46c565e437
11 changed files with 15646 additions and 93 deletions
22
bun.lock
22
bun.lock
|
|
@ -5,27 +5,27 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "monochrome",
|
"name": "monochrome",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.1",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
"@neutralinojs/lib": "^6.5.0",
|
"@neutralinojs/lib": "^6.5.0",
|
||||||
"butterchurn": "^2.6.7",
|
"butterchurn": "^2.6.7",
|
||||||
"butterchurn-presets": "^2.4.7",
|
"butterchurn-presets": "^2.4.7",
|
||||||
"cookie-session": "^2.1.0",
|
"cookie-session": "^2.1.1",
|
||||||
"dashjs": "^5.1.1",
|
"dashjs": "^5.1.1",
|
||||||
"jose": "^6.0.11",
|
"jose": "^6.1.3",
|
||||||
"pocketbase": "^0.26.5",
|
"pocketbase": "^0.26.8",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@neutralinojs/neu": "^11.7.0",
|
"@neutralinojs/neu": "^11.7.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.3.0",
|
||||||
"htmlhint": "^1.8.0",
|
"htmlhint": "^1.9.1",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.8.1",
|
||||||
"stylelint": "^16.26.1",
|
"stylelint": "^16.26.1",
|
||||||
"stylelint-config-standard": "^39.0.1",
|
"stylelint-config-standard": "^39.0.1",
|
||||||
"stylelint-config-standard-scss": "^16.0.0",
|
"stylelint-config-standard-scss": "^16.0.0",
|
||||||
"vite": "^7.3.0",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-neutralino": "^1.0.3",
|
"vite-plugin-neutralino": "^1.0.3",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
},
|
},
|
||||||
|
|
@ -722,7 +722,7 @@
|
||||||
|
|
||||||
"global-prefix": ["global-prefix@3.0.0", "", { "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" } }, "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg=="],
|
"global-prefix": ["global-prefix@3.0.0", "", { "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" } }, "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg=="],
|
||||||
|
|
||||||
"globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
|
"globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
|
||||||
|
|
||||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -4500,6 +4500,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Preserve Pitch Control -->
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Preserve Pitch</span>
|
||||||
|
<span class="description">Keep original pitch when changing speed</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="preserve-pitch-toggle" />
|
||||||
|
<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">
|
||||||
|
|
|
||||||
13
js/api.js
13
js/api.js
|
|
@ -1254,7 +1254,18 @@ export class LosslessAPI {
|
||||||
message: 'Adding metadata...',
|
message: 'Adding metadata...',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
blob = await addMetadataToAudio(blob, track, this, quality);
|
|
||||||
|
const enrichedTrack = { ...track };
|
||||||
|
if (lookup.info) {
|
||||||
|
enrichedTrack.replayGain = {
|
||||||
|
trackReplayGain: lookup.info.trackReplayGain,
|
||||||
|
trackPeakAmplitude: lookup.info.trackPeakAmplitude,
|
||||||
|
albumReplayGain: lookup.info.albumReplayGain,
|
||||||
|
albumPeakAmplitude: lookup.info.albumPeakAmplitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
blob = await addMetadataToAudio(blob, enrichedTrack, this, quality);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect actual format and fix filename extension if needed
|
// Detect actual format and fix filename extension if needed
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,15 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lookup.info) {
|
||||||
|
enrichedTrack.replayGain = {
|
||||||
|
trackReplayGain: lookup.info.trackReplayGain,
|
||||||
|
trackPeakAmplitude: lookup.info.trackPeakAmplitude,
|
||||||
|
albumReplayGain: lookup.info.albumReplayGain,
|
||||||
|
albumPeakAmplitude: lookup.info.albumPeakAmplitude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Handle DASH streams (blob URLs)
|
// Handle DASH streams (blob URLs)
|
||||||
let blob;
|
let blob;
|
||||||
if (streamUrl.startsWith('blob:')) {
|
if (streamUrl.startsWith('blob:')) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { getCoverBlob } from './utils.js';
|
import { getCoverBlob, getTrackTitle } from './utils.js';
|
||||||
|
|
||||||
async function writeID3v2Tag(mp3Blob, metadata, coverBlob = null) {
|
async function writeID3v2Tag(mp3Blob, metadata, coverBlob = null) {
|
||||||
const frames = [];
|
const frames = [];
|
||||||
|
|
||||||
if (metadata.title) {
|
if (metadata.title) {
|
||||||
frames.push(createTextFrame('TIT2', metadata.title));
|
frames.push(createTextFrame('TIT2', getTrackTitle(metadata)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const artistName = metadata.artist?.name || metadata.artists?.[0]?.name;
|
const artistName = metadata.artist?.name || metadata.artists?.[0]?.name;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getCoverBlob, detectAudioFormat } from './utils.js';
|
import { getCoverBlob, detectAudioFormat, getTrackTitle } from './utils.js';
|
||||||
import { addMp3Metadata } from './id3-writer.js';
|
import { addMp3Metadata } from './id3-writer.js';
|
||||||
|
|
||||||
const VENDOR_STRING = 'Monochrome';
|
const VENDOR_STRING = 'Monochrome';
|
||||||
|
|
@ -565,7 +565,7 @@ function createVorbisCommentBlock(track) {
|
||||||
|
|
||||||
// Add standard tags
|
// Add standard tags
|
||||||
if (track.title) {
|
if (track.title) {
|
||||||
comments.push(['TITLE', track.title]);
|
comments.push(['TITLE', getTrackTitle(track)]);
|
||||||
}
|
}
|
||||||
const artistStr = getFullArtistString(track);
|
const artistStr = getFullArtistString(track);
|
||||||
if (artistStr) {
|
if (artistStr) {
|
||||||
|
|
@ -587,6 +587,13 @@ function createVorbisCommentBlock(track) {
|
||||||
if (track.album?.numberOfTracks) {
|
if (track.album?.numberOfTracks) {
|
||||||
comments.push(['TRACKTOTAL', String(track.album.numberOfTracks)]);
|
comments.push(['TRACKTOTAL', String(track.album.numberOfTracks)]);
|
||||||
}
|
}
|
||||||
|
if (track.replayGain) {
|
||||||
|
const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain;
|
||||||
|
if (albumReplayGain) comments.push(['REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)]);
|
||||||
|
if (albumPeakAmplitude) comments.push(['REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)]);
|
||||||
|
if (trackReplayGain) comments.push(['REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)]);
|
||||||
|
if (trackPeakAmplitude) comments.push(['REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)]);
|
||||||
|
}
|
||||||
|
|
||||||
const releaseDateStr =
|
const releaseDateStr =
|
||||||
track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : '');
|
track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : '');
|
||||||
|
|
@ -930,7 +937,7 @@ function createMp4MetadataAtoms(track) {
|
||||||
// We'll create basic iTunes-style metadata
|
// We'll create basic iTunes-style metadata
|
||||||
|
|
||||||
const tags = {
|
const tags = {
|
||||||
'©nam': track.title || DEFAULT_TITLE,
|
'©nam': getTrackTitle(track) || DEFAULT_TITLE,
|
||||||
'©ART': getFullArtistString(track) || DEFAULT_ARTIST,
|
'©ART': getFullArtistString(track) || DEFAULT_ARTIST,
|
||||||
'©alb': track.album?.title || DEFAULT_ALBUM,
|
'©alb': track.album?.title || DEFAULT_ALBUM,
|
||||||
aART: track.album?.artist?.name || track.artist?.name || DEFAULT_ARTIST,
|
aART: track.album?.artist?.name || track.artist?.name || DEFAULT_ARTIST,
|
||||||
|
|
|
||||||
183
js/player.js
183
js/player.js
|
|
@ -47,6 +47,11 @@ export class Player {
|
||||||
this.sleepTimerEndTime = null;
|
this.sleepTimerEndTime = null;
|
||||||
this.sleepTimerInterval = null;
|
this.sleepTimerInterval = null;
|
||||||
|
|
||||||
|
// Apply audio effects when track is ready
|
||||||
|
this.audio.addEventListener('canplay', () => {
|
||||||
|
this.applyAudioEffects();
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize dash.js player
|
// Initialize dash.js player
|
||||||
this.dashPlayer = MediaPlayer().create();
|
this.dashPlayer = MediaPlayer().create();
|
||||||
this.dashPlayer.updateSettings({
|
this.dashPlayer.updateSettings({
|
||||||
|
|
@ -136,8 +141,24 @@ export class Player {
|
||||||
|
|
||||||
applyAudioEffects() {
|
applyAudioEffects() {
|
||||||
const speed = audioEffectsSettings.getSpeed();
|
const speed = audioEffectsSettings.getSpeed();
|
||||||
if (this.audio.playbackRate !== speed) {
|
|
||||||
this.audio.playbackRate = speed;
|
if (this.dashInitialized && this.dashPlayer) {
|
||||||
|
if (this.dashPlayer.getPlaybackRate() !== speed) {
|
||||||
|
this.dashPlayer.setPlaybackRate(speed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.audio.playbackRate !== speed) {
|
||||||
|
this.audio.playbackRate = speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preservePitch = audioEffectsSettings.isPreservePitchEnabled();
|
||||||
|
if (this.audio.preservesPitch !== preservePitch) {
|
||||||
|
this.audio.preservesPitch = preservePitch;
|
||||||
|
// Firefox support
|
||||||
|
if (this.audio.mozPreservesPitch !== undefined) {
|
||||||
|
this.audio.mozPreservesPitch = preservePitch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,6 +168,11 @@ export class Player {
|
||||||
this.applyAudioEffects();
|
this.applyAudioEffects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPreservePitch(enabled) {
|
||||||
|
audioEffectsSettings.setPreservePitch(enabled);
|
||||||
|
this.applyAudioEffects();
|
||||||
|
}
|
||||||
|
|
||||||
loadQueueState() {
|
loadQueueState() {
|
||||||
const savedState = queueManager.getQueue();
|
const savedState = queueManager.getQueue();
|
||||||
if (savedState) {
|
if (savedState) {
|
||||||
|
|
@ -256,70 +282,81 @@ export class Player {
|
||||||
setupMediaSession() {
|
setupMediaSession() {
|
||||||
if (!('mediaSession' in navigator)) return;
|
if (!('mediaSession' in navigator)) return;
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('play', async () => {
|
const setHandlers = () => {
|
||||||
// Initialize and resume audio context first (required for iOS lock screen)
|
navigator.mediaSession.setActionHandler('play', async () => {
|
||||||
// Must happen before audio.play() or audio won't route through Web Audio
|
// Initialize and resume audio context first (required for iOS lock screen)
|
||||||
if (!audioContextManager.isReady()) {
|
// Must happen before audio.play() or audio won't route through Web Audio
|
||||||
audioContextManager.init(this.audio);
|
if (!audioContextManager.isReady()) {
|
||||||
this.applyReplayGain();
|
audioContextManager.init(this.audio);
|
||||||
|
this.applyReplayGain();
|
||||||
|
}
|
||||||
|
await audioContextManager.resume();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.audio.play();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('MediaSession play failed:', e);
|
||||||
|
// If play fails, try to handle it like a regular play/pause
|
||||||
|
this.handlePlayPause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => {
|
||||||
|
this.audio.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', async () => {
|
||||||
|
// Ensure audio context is active for iOS lock screen controls
|
||||||
|
if (!audioContextManager.isReady()) {
|
||||||
|
audioContextManager.init(this.audio);
|
||||||
|
this.applyReplayGain();
|
||||||
|
}
|
||||||
|
await audioContextManager.resume();
|
||||||
|
this.playPrev();
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', async () => {
|
||||||
|
// Ensure audio context is active for iOS lock screen controls
|
||||||
|
if (!audioContextManager.isReady()) {
|
||||||
|
audioContextManager.init(this.audio);
|
||||||
|
this.applyReplayGain();
|
||||||
|
}
|
||||||
|
await audioContextManager.resume();
|
||||||
|
this.playNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.isIOS) {
|
||||||
|
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
|
||||||
|
const skipTime = details.seekOffset || 10;
|
||||||
|
this.seekBackward(skipTime);
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('seekforward', (details) => {
|
||||||
|
const skipTime = details.seekOffset || 10;
|
||||||
|
this.seekForward(skipTime);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await audioContextManager.resume();
|
|
||||||
|
|
||||||
try {
|
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
||||||
await this.audio.play();
|
if (details.seekTime !== undefined) {
|
||||||
} catch (e) {
|
this.audio.currentTime = Math.max(0, details.seekTime);
|
||||||
console.error('MediaSession play failed:', e);
|
this.updateMediaSessionPositionState();
|
||||||
// If play fails, try to handle it like a regular play/pause
|
}
|
||||||
this.handlePlayPause();
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('pause', () => {
|
navigator.mediaSession.setActionHandler('stop', () => {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
});
|
this.audio.currentTime = 0;
|
||||||
|
this.updateMediaSessionPlaybackState();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', async () => {
|
if (this.isIOS) {
|
||||||
// Ensure audio context is active for iOS lock screen controls
|
// iOS: set handlers only when playback starts. Setting them in the constructor makes
|
||||||
if (!audioContextManager.isReady()) {
|
// the lock screen show +10/-10. Registering on first 'playing' gives next/previous track
|
||||||
audioContextManager.init(this.audio);
|
this.audio.addEventListener('playing', () => setHandlers(), { once: true });
|
||||||
this.applyReplayGain();
|
} else {
|
||||||
}
|
setHandlers();
|
||||||
await audioContextManager.resume();
|
}
|
||||||
this.playPrev();
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', async () => {
|
|
||||||
// Ensure audio context is active for iOS lock screen controls
|
|
||||||
if (!audioContextManager.isReady()) {
|
|
||||||
audioContextManager.init(this.audio);
|
|
||||||
this.applyReplayGain();
|
|
||||||
}
|
|
||||||
await audioContextManager.resume();
|
|
||||||
this.playNext();
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
|
|
||||||
const skipTime = details.seekOffset || 10;
|
|
||||||
this.seekBackward(skipTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('seekforward', (details) => {
|
|
||||||
const skipTime = details.seekOffset || 10;
|
|
||||||
this.seekForward(skipTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
|
||||||
if (details.seekTime !== undefined) {
|
|
||||||
this.audio.currentTime = Math.max(0, details.seekTime);
|
|
||||||
this.updateMediaSessionPositionState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('stop', () => {
|
|
||||||
this.audio.pause();
|
|
||||||
this.audio.currentTime = 0;
|
|
||||||
this.updateMediaSessionPlaybackState();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuality(quality) {
|
setQuality(quality) {
|
||||||
|
|
@ -935,26 +972,22 @@ export class Player {
|
||||||
// Force a refresh for picky Bluetooth systems by clearing metadata first
|
// Force a refresh for picky Bluetooth systems by clearing metadata first
|
||||||
navigator.mediaSession.metadata = null;
|
navigator.mediaSession.metadata = null;
|
||||||
|
|
||||||
const artwork = [];
|
|
||||||
const sizes = ['320'];
|
|
||||||
const coverId = track.album?.cover;
|
const coverId = track.album?.cover;
|
||||||
const trackTitle = getTrackTitle(track);
|
const trackTitle = getTrackTitle(track);
|
||||||
|
|
||||||
if (coverId) {
|
|
||||||
sizes.forEach((size) => {
|
|
||||||
artwork.push({
|
|
||||||
src: this.api.getCoverUrl(coverId, size),
|
|
||||||
sizes: `${size}x${size}`,
|
|
||||||
type: 'image/jpeg',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: trackTitle || 'Unknown Title',
|
title: trackTitle || 'Unknown Title',
|
||||||
artist: getTrackArtists(track) || 'Unknown Artist',
|
artist: getTrackArtists(track) || 'Unknown Artist',
|
||||||
album: track.album?.title || 'Unknown Album',
|
album: track.album?.title || 'Unknown Album',
|
||||||
artwork: artwork.length > 0 ? artwork : undefined,
|
artwork: coverId
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
src: this.api.getCoverUrl(coverId, '1280'),
|
||||||
|
sizes: '1280x1280',
|
||||||
|
type: 'image/jpeg',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateMediaSessionPlaybackState();
|
this.updateMediaSessionPlaybackState();
|
||||||
|
|
|
||||||
|
|
@ -928,6 +928,18 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
playbackSpeedInput.addEventListener('blur', handleInputChange);
|
playbackSpeedInput.addEventListener('blur', handleInputChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Preserve Pitch Toggle
|
||||||
|
// ========================================
|
||||||
|
const preservePitchToggle = document.getElementById('preserve-pitch-toggle');
|
||||||
|
if (preservePitchToggle) {
|
||||||
|
preservePitchToggle.checked = audioEffectsSettings.isPreservePitchEnabled();
|
||||||
|
|
||||||
|
preservePitchToggle.addEventListener('change', (e) => {
|
||||||
|
player.setPreservePitch(e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Parametric Equalizer Settings (3-32 bands with custom ranges)
|
// Parametric Equalizer Settings (3-32 bands with custom ranges)
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
|
||||||
|
|
@ -1319,6 +1319,7 @@ export const exponentialVolumeSettings = {
|
||||||
|
|
||||||
export const audioEffectsSettings = {
|
export const audioEffectsSettings = {
|
||||||
SPEED_KEY: 'audio-effects-speed',
|
SPEED_KEY: 'audio-effects-speed',
|
||||||
|
PITCH_PRESERVE_KEY: 'audio-effects-pitch-preserve',
|
||||||
|
|
||||||
// Playback speed (0.01 to 100, default 1.0)
|
// Playback speed (0.01 to 100, default 1.0)
|
||||||
getSpeed() {
|
getSpeed() {
|
||||||
|
|
@ -1334,6 +1335,20 @@ export const audioEffectsSettings = {
|
||||||
const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0));
|
const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0));
|
||||||
localStorage.setItem(this.SPEED_KEY, validSpeed.toString());
|
localStorage.setItem(this.SPEED_KEY, validSpeed.toString());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Preserve pitch when changing speed (default true)
|
||||||
|
isPreservePitchEnabled() {
|
||||||
|
try {
|
||||||
|
const val = localStorage.getItem(this.PITCH_PRESERVE_KEY);
|
||||||
|
return val === null ? true : val === 'true';
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setPreservePitch(enabled) {
|
||||||
|
localStorage.setItem(this.PITCH_PRESERVE_KEY, enabled ? 'true' : 'false');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settingsUiState = {
|
export const settingsUiState = {
|
||||||
|
|
|
||||||
15454
package-lock.json
generated
Normal file
15454
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -32,7 +32,7 @@
|
||||||
"@neutralinojs/neu": "^11.7.0",
|
"@neutralinojs/neu": "^11.7.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"globals": "^17.3.0",
|
"globals": "^17.4.0",
|
||||||
"htmlhint": "^1.9.1",
|
"htmlhint": "^1.9.1",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"stylelint": "^16.26.1",
|
"stylelint": "^16.26.1",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue