Skip to content

Commit

Permalink
Add support for LRC Lyrics
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Nov 10, 2024
1 parent 0a39bd6 commit bed8aae
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 4 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,13 @@ Following tag header formats are supported:
- [RIFF](https://wikipedia.org/wiki/Resource_Interchange_File_Format)/INFO
- [Vorbis comment](https://wikipedia.org/wiki/Vorbis_comment)
- [AIFF](https://wikipedia.org/wiki/Audio_Interchange_File_Format)

It allows many tags to be accessed in audio format, and tag format independent way.

Following lyric formats are supported:
- [LRC](https://en.wikipedia.org/wiki/LRC_(file_format))
- Synchronized lyrics (SYLT)
- Unsynchronized lyrics (USULT)
[
It allows many tags to be]() accessed in audio format, and tag format independent way.

Support for [MusicBrainz](https://musicbrainz.org/) tags as written by [Picard](https://picard.musicbrainz.org/).
[ReplayGain](https://wiki.hydrogenaud.io/index.php?title=ReplayGain) tags are supported.
Expand Down
7 changes: 7 additions & 0 deletions lib/common/MetadataCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CombinedTagMapper } from './CombinedTagMapper.js';
import { CommonTagMapper } from './GenericTagMapper.js';
import { toRatio } from './Util.js';
import { fileTypeFromBuffer } from 'file-type';
import { parseLrc } from '../lrc/LyricsParser.js';

const debug = initDebug('music-metadata:collector');

Expand Down Expand Up @@ -265,6 +266,12 @@ export class MetadataCollector implements INativeMetadataCollector {
}
break;

case 'lyrics':
if (typeof tag.value === 'string') {
tag.value = parseLrc(tag.value);
}
break;

default:
// nothing to do
}
Expand Down
39 changes: 39 additions & 0 deletions lib/lrc/LyricsParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type ILyricsText, type ILyricsTag, LyricsContentType, TimestampFormat } from '../type.js';

/**
* Parse LRC (Lyrics) formatted text
* Ref: https://en.wikipedia.org/wiki/LRC_(file_format)
* @param lrcString
*/
export function parseLrc(lrcString: string): ILyricsTag {
const lines = lrcString.split('\n');
const syncText: ILyricsText[] = [];

// Regular expression to match LRC timestamps (e.g., [00:45.52])
const timestampRegex = /\[(\d{2}):(\d{2})\.(\d{2})\]/;

for (const line of lines) {
const match = line.match(timestampRegex);

if (match) {
const minutes = Number.parseInt(match[1], 10);
const seconds = Number.parseInt(match[2], 10);
const hundredths = Number.parseInt(match[3], 10);

// Convert the timestamp to milliseconds, as per TimestampFormat.milliseconds
const timestamp = (minutes * 60 + seconds) * 1000 + hundredths * 10;

// Get the text portion of the line (e.g., "あの蝶は自由になれたかな")
const text = line.replace(timestampRegex, '').trim();

syncText.push({ timestamp, text });
}
}

// Creating the ILyricsTag object
return {
contentType: LyricsContentType.lyrics,
timeStampFormat: TimestampFormat.milliseconds,
syncText,
};
}
2 changes: 1 addition & 1 deletion lib/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ export interface IRandomReader {
randomRead(buffer: Uint8Array, offset: number, length: number, position: number): Promise<number>;
}

interface ILyricsText {
export interface ILyricsText {
text: string;
timestamp?: number;
}
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@
"info",
"parse",
"parser",
"bwf"
"bwf",
"slt",
"lyrics"
],
"scripts": {
"clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'src/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map' 'test/**/*.js' 'test/**/*.js.map' 'doc-gen/**/*.js' 'doc-gen/**/*.js.map'",
Expand Down
Binary file not shown.
23 changes: 23 additions & 0 deletions test/test-file-flac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'node:fs';
import * as path from 'node:path';

import * as mm from '../lib/index.js';
import { LyricsContentType, TimestampFormat } from '../lib/index.js';
import { Parsers } from './metadata-parsers.js';
import { samplePath } from './util.js';

Expand Down Expand Up @@ -173,5 +174,27 @@ describe('Parse FLAC Vorbis comment', () => {
assert.equal(mm.ratingToStars(common.rating[0].rating), 4, 'Vorbis tag rating conversion');
});

it('Should decode LRC lyrics', async () => {

const filePath = path.join(flacFilePath, 'Dance In The Game - ZAQ - LRC.flac');
const {common} = await mm.parseFile(filePath);

assert.isArray(common.lyrics, 'common.lyrics');
assert.strictEqual(common.lyrics.length, 1, 'common.lyrics.length');
const lrcLyrics = common.lyrics[0];
assert.strictEqual(lrcLyrics.contentType, LyricsContentType.lyrics, 'lrcLyrics.contentType');
assert.strictEqual(lrcLyrics.timeStampFormat, TimestampFormat.milliseconds, 'lrcLyrics.timeStampFormat');
assert.isArray(lrcLyrics.syncText, 'lrcLyrics.syncText');
assert.strictEqual(lrcLyrics.syncText.length, 39, 'lrcLyrics.syncText.length');
assert.strictEqual(lrcLyrics.syncText[0].timestamp, 0, 'syncText[0].timestamp');
assert.strictEqual(lrcLyrics.syncText[0].text, '作词 : ZAQ', 'lrcLyrics.syncText[0].text');
assert.strictEqual(lrcLyrics.syncText[1].timestamp, 300, 'syncText[1].timestamp');
assert.strictEqual(lrcLyrics.syncText[1].text, '作曲 : ZAQ', 'lrcLyrics.syncText[1].text');

const syncText = lrcLyrics.syncText
assert.isArray(common.lyrics, 'common.lyrics');

});

});

0 comments on commit bed8aae

Please sign in to comment.