diff options
-rw-r--r-- | users/Profpatsch/lyric/extension/package.json | 5 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/src/extension.ts | 100 | ||||
-rw-r--r-- | users/Profpatsch/lyric/extension/src/quantize-lrc.ts | 82 |
3 files changed, 186 insertions, 1 deletions
diff --git a/users/Profpatsch/lyric/extension/package.json b/users/Profpatsch/lyric/extension/package.json index 389df271cf5a..76fd8d63e73d 100644 --- a/users/Profpatsch/lyric/extension/package.json +++ b/users/Profpatsch/lyric/extension/package.json @@ -29,6 +29,11 @@ "command": "extension.shiftLyricsUp", "title": "Shift Lyrics Up from Current Line", "category": "LRC" + }, + { + "command": "extension.quantizeToEigthNote", + "title": "Quantize timestamps to nearest eighth note", + "category": "LRC" } ], "languages": [ diff --git a/users/Profpatsch/lyric/extension/src/extension.ts b/users/Profpatsch/lyric/extension/src/extension.ts index 83b4ab093eda..f09be13d04bd 100644 --- a/users/Profpatsch/lyric/extension/src/extension.ts +++ b/users/Profpatsch/lyric/extension/src/extension.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import * as net from 'net'; +import { adjustTimestampToEighthNote, bpmToEighthNoteDuration } from './quantize-lrc'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(...registerCheckLineTimestamp(context)); @@ -7,6 +8,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('extension.jumpToLrcPosition', jumpToLrcPosition), vscode.commands.registerCommand('extension.shiftLyricsDown', shiftLyricsDown), vscode.commands.registerCommand('extension.shiftLyricsUp', shiftLyricsUp), + vscode.commands.registerCommand('extension.quantizeToEigthNote', quantizeLrc), ); } @@ -160,6 +162,102 @@ async function shiftLyricsUp() { }); } +/** first ask the user for the BPM of the track, then quantize the timestamps in the active text editor to the closest eighth note based on the given BPM */ +async function quantizeLrc() { + const bpm = await timeInputBpm(); + + if (bpm === undefined) { + return; + } + + const editor = vscode.window.activeTextEditor; + + if (!editor) { + vscode.window.showInformationMessage('No active editor found.'); + return; + } + + const ext = new Ext(editor.document); + + const getLine = (line: number) => ({ + number: line, + range: editor.document.lineAt(line), + }); + + const documentRange = new vscode.Range( + getLine(0).range.range.start, + editor.document.lineAt(editor.document.lineCount - 1).range.end, + ); + + const eighthNoteDuration = bpmToEighthNoteDuration(bpm); + + let newLines: string = ''; + for ( + let line = getLine(0); + line.number < editor.document.lineCount - 1; + line = getLine(line.number + 1) + ) { + const timestamp = ext.getTimestampFromLine(line.number); + if (timestamp === undefined) { + newLines += line.range.text + '\n'; + continue; + } + const adjustedMs = adjustTimestampToEighthNote( + timestamp.milliseconds, + eighthNoteDuration, + ); + newLines += `[${formatTimestamp(adjustedMs)}]${timestamp.text}\n`; + } + + await editor.edit(editBuilder => { + editBuilder.replace(documentRange, newLines); + }); +} + +// Show input boxes in a loop, and record the time between each input, averaging the last 5 inputs over a sliding window, then calculate the BPM of the average +async function timeInputBpm() { + const timeDifferences: number[] = [500, 500, 500, 500, 500]; + // assign a weight to the time differences, so that the most recent time differences have more weight + const weights = [0.1, 0.1, 0.2, 0.3, 0.3]; + + const calculateBPM = () => { + // use a weighted average here + let avg = 0; + for (let i = 0; i < timeDifferences.length; i++) { + avg += timeDifferences[i] * weights[i]; + } + + return Math.floor(60000 / avg); + }; + + let lastPressTime = Date.now(); + while (true) { + const res = await vscode.window.showInputBox({ + prompt: `Press enter to record BPM (current BPM: ${calculateBPM()}), enter the final BPM once you know, or press esc to finish`, + placeHolder: 'BPM', + }); + if (res === undefined) { + return undefined; + } + if (res !== '') { + const resBpm = parseInt(res, 10); + if (isNaN(resBpm)) { + vscode.window.showErrorMessage('Invalid BPM'); + continue; + } + return resBpm; + } + + const now = Date.now(); + const timeDiff = now - lastPressTime; + // Add the time difference to the array (limit to last 5 key presses) + timeDifferences.shift(); // Remove the oldest time difference + timeDifferences.push(timeDiff); + + lastPressTime = now; + } +} + // If the difference to the timestamp on the next line is larger than 10 seconds, underline the next line and show a warning message on hover export function registerCheckLineTimestamp(_context: vscode.ExtensionContext) { const changesToCheck: Set<vscode.TextDocument> = new Set(); @@ -318,7 +416,7 @@ function formatTimestamp(ms: number): string { ms %= 60000; const seconds = (ms / 1000).toFixed(2); - return `${String(minutes).padStart(2, '0')}:${seconds}`; + return `${String(minutes).padStart(2, '0')}:${seconds.padStart(5, '0')}`; } class ManageWarnings { diff --git a/users/Profpatsch/lyric/extension/src/quantize-lrc.ts b/users/Profpatsch/lyric/extension/src/quantize-lrc.ts new file mode 100644 index 000000000000..83c31348e26e --- /dev/null +++ b/users/Profpatsch/lyric/extension/src/quantize-lrc.ts @@ -0,0 +1,82 @@ +export function bpmToEighthNoteDuration(bpm: number): number { + // Convert BPM to eighth-note duration in milliseconds + const quarterNoteDuration = (60 / bpm) * 1000; // in ms + const eighthNoteDuration = quarterNoteDuration / 2; + return eighthNoteDuration; +} + +function parseTimestamp(timestamp: string): number { + // Parse [mm:ss.ms] format into milliseconds + const [min, sec] = timestamp.split(':'); + + const minutes = parseInt(min, 10); + const seconds = parseFloat(sec); + + return minutes * 60 * 1000 + seconds * 1000; +} + +function formatTimestamp(ms: number): string { + // Format milliseconds back into [mm:ss.ms] + const minutes = Math.floor(ms / 60000); + ms %= 60000; + const seconds = (ms / 1000).toFixed(2); + + return `${String(minutes).padStart(2, '0')}:${seconds}`; +} + +export function adjustTimestampToEighthNote( + timestampMs: number, + eighthNoteDuration: number, +): number { + // Find the closest multiple of the eighth-note duration + return Math.round(timestampMs / eighthNoteDuration) * eighthNoteDuration; +} + +function adjustTimestamps(bpm: number, timestamps: string[]): string[] { + const eighthNoteDuration = bpmToEighthNoteDuration(bpm); + + return timestamps.map(timestamp => { + const timestampMs = parseTimestamp(timestamp); + const adjustedMs = adjustTimestampToEighthNote(timestampMs, eighthNoteDuration); + return formatTimestamp(adjustedMs); + }); +} + +// Parse a .lrc file into an array of objects with timestamp and text +// Then adjust the timestamps to the closest eighth note +// Finally, format the adjusted timestamps back into [mm:ss.ms] format and put them back into the lrc object +// +// Example .lrc file: +// [01:15.66] And the reviewers bewail +// [01:18.18] There'll be no encore +// [01:21.65] 'Cause you're not begging for more +// [01:25.00] +// [01:34.64] She may seem self-righteous and holier-than-thou +// [01:39.77] She may sound like she has all the answers +// [01:45.20] But beyond she may feel just a bit anyhow +function parseLrc(lrc: string): { timestamp: string; text: string }[] { + return lrc + .trimEnd() + .split('\n') + .map(line => { + const match = line.match(/\[(\d+:\d+\.\d+)\](.*)/); + const [, timestamp, text] = match!; + return { timestamp, text }; + }); +} + +function formatLrc(lrc: { timestamp: string; text: string }[]): string { + return lrc.map(({ timestamp, text }) => `[${timestamp}] ${text}`).join('\n'); +} + +function adjustLrc(lrc: string, bpm: number): string { + const lrcArray = parseLrc(lrc); + const timestamps = lrcArray.map(({ timestamp }) => timestamp); + const adjustedTimestamps = adjustTimestamps(bpm, timestamps); + + lrcArray.forEach((line, i) => { + line.timestamp = adjustedTimestamps[i]; + }); + + return formatLrc(lrcArray); +} |