lyrics offset
This commit is contained in:
parent
b59c85e108
commit
a25f05a66e
2 changed files with 124 additions and 2 deletions
94
js/lyrics.js
94
js/lyrics.js
|
|
@ -133,6 +133,40 @@ export class LyricsManager {
|
|||
this.geniusManager = new GeniusManager();
|
||||
this.isGeniusMode = false;
|
||||
this.currentGeniusData = null;
|
||||
this.timingOffset = 0; // Offset in milliseconds (positive = delay lyrics, negative = advance lyrics)
|
||||
}
|
||||
|
||||
// Get timing offset for current track
|
||||
getTimingOffset(trackId) {
|
||||
try {
|
||||
const key = `lyrics-offset-${trackId}`;
|
||||
const stored = localStorage.getItem(key);
|
||||
return stored ? parseInt(stored, 10) : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Set timing offset for current track
|
||||
setTimingOffset(trackId, offsetMs) {
|
||||
try {
|
||||
const key = `lyrics-offset-${trackId}`;
|
||||
localStorage.setItem(key, offsetMs.toString());
|
||||
} catch (e) {
|
||||
console.warn('Failed to save lyrics timing offset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset timing offset for current track
|
||||
resetTimingOffset(trackId) {
|
||||
this.setTimingOffset(trackId, 0);
|
||||
}
|
||||
|
||||
// Get formatted offset display string
|
||||
getOffsetDisplayString(offsetMs) {
|
||||
const sign = offsetMs >= 0 ? '+' : '';
|
||||
const seconds = Math.abs(offsetMs) / 1000;
|
||||
return `${sign}${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
// Load Kuroshiro from CDN (npm package uses Node.js path which doesn't work in browser)
|
||||
|
|
@ -715,15 +749,38 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
|||
});
|
||||
}
|
||||
|
||||
// Load saved timing offset for this track
|
||||
manager.timingOffset = manager.getTimingOffset(track.id);
|
||||
|
||||
const renderControls = (container) => {
|
||||
const isRomajiMode = manager.getRomajiMode();
|
||||
manager.isRomajiMode = isRomajiMode;
|
||||
const isGeniusMode = manager.isGeniusMode;
|
||||
const offsetDisplay = manager.getOffsetDisplayString(manager.timingOffset);
|
||||
|
||||
container.innerHTML = `
|
||||
<button id="close-side-panel-btn" class="btn-icon" title="Close">
|
||||
${SVG_CLOSE}
|
||||
</button>
|
||||
<div class="lyrics-timing-controls">
|
||||
<button id="lyrics-timing-minus-btn" class="btn-icon" title="Decrease delay (lyrics earlier) -0.5s">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="lyrics-timing-display" class="lyrics-timing-display" title="Current timing offset">${offsetDisplay}</span>
|
||||
<button id="lyrics-timing-plus-btn" class="btn-icon" title="Increase delay (lyrics later) +0.5s">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14M12 5v14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="lyrics-timing-reset-btn" class="btn-icon" title="Reset timing offset">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button id="romaji-toggle-btn" class="btn-icon" title="Toggle Romaji (Japanese to Latin)" data-enabled="${isRomajiMode}" style="color: ${isRomajiMode ? 'var(--primary)' : ''}">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
|
|
@ -740,6 +797,32 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
|||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
||||
});
|
||||
|
||||
// Timing adjustment controls
|
||||
const updateTimingDisplay = () => {
|
||||
const display = container.querySelector('#lyrics-timing-display');
|
||||
if (display) {
|
||||
display.textContent = manager.getOffsetDisplayString(manager.timingOffset);
|
||||
}
|
||||
};
|
||||
|
||||
container.querySelector('#lyrics-timing-minus-btn')?.addEventListener('click', () => {
|
||||
manager.timingOffset -= 500; // Decrease by 0.5 seconds
|
||||
manager.setTimingOffset(track.id, manager.timingOffset);
|
||||
updateTimingDisplay();
|
||||
});
|
||||
|
||||
container.querySelector('#lyrics-timing-plus-btn')?.addEventListener('click', () => {
|
||||
manager.timingOffset += 500; // Increase by 0.5 seconds
|
||||
manager.setTimingOffset(track.id, manager.timingOffset);
|
||||
updateTimingDisplay();
|
||||
});
|
||||
|
||||
container.querySelector('#lyrics-timing-reset-btn')?.addEventListener('click', () => {
|
||||
manager.timingOffset = 0;
|
||||
manager.resetTimingOffset(track.id);
|
||||
updateTimingDisplay();
|
||||
});
|
||||
|
||||
// Romaji toggle button handler
|
||||
const romajiBtn = container.querySelector('#romaji-toggle-btn');
|
||||
if (romajiBtn) {
|
||||
|
|
@ -945,11 +1028,17 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
|||
let lastTimestamp = performance.now();
|
||||
let animationFrameId = null;
|
||||
|
||||
// Get timing offset from lyrics manager (in milliseconds)
|
||||
const getTimingOffset = () => {
|
||||
return lyricsManager?.timingOffset || 0;
|
||||
};
|
||||
|
||||
const updateTime = () => {
|
||||
const currentMs = audioPlayer.currentTime * 1000;
|
||||
baseTimeMs = currentMs;
|
||||
lastTimestamp = performance.now();
|
||||
amLyrics.currentTime = currentMs;
|
||||
// Apply timing offset: positive offset delays lyrics, negative advances them
|
||||
amLyrics.currentTime = currentMs - getTimingOffset();
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
|
|
@ -957,7 +1046,8 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
|||
const now = performance.now();
|
||||
const elapsed = now - lastTimestamp;
|
||||
const nextMs = baseTimeMs + elapsed;
|
||||
amLyrics.currentTime = nextMs;
|
||||
// Apply timing offset: positive offset delays lyrics, negative advances them
|
||||
amLyrics.currentTime = nextMs - getTimingOffset();
|
||||
animationFrameId = requestAnimationFrame(tick);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
32
styles.css
32
styles.css
|
|
@ -3268,6 +3268,38 @@ input:checked + .slider::before {
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Lyrics timing adjustment controls */
|
||||
.lyrics-timing-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-right: auto;
|
||||
padding-right: 0.5rem;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.lyrics-timing-display {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
min-width: 3.5rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.lyrics-timing-controls .btn-icon {
|
||||
padding: 0.4rem;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.lyrics-timing-controls .btn-icon:hover {
|
||||
background: var(--secondary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
|
|||
Loading…
Reference in a new issue