about summary refs log tree commit diff
path: root/users/Profpatsch/lyric/extension/src/quantize-lrc.ts
blob: 83c31348e26e1f8b495773a1d23911601d57b3fc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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);
}