Embedded lyrics and which tags to use?

Over the last couple of weeks I've refined a python script I have used in the past to find all .lrc files in my collection that don't have a corresponding song with the same name (which, prior to mp3tag renaming .lrc files with the songs, happened quite a few times).

In its current state my script recursively scans a given directory for .lrc and .txt files, tries to find matching songs (it searches .flac and .mp3 per default but a list of extensions can be supplied as an argument), logs the paths of .lrc and .txt files that do not have matching songs and it can open the songs that do have matching .lrc or .txt files in mp3tag via the mp3tag cli.

It also optionally creates actions for mp3tag to backup existing embedded lrics, import them (inspired by this post) and delete previously created backups of existing embedded lyrics.

Now to my question:
If you embed lyrics in your songs, which tags do you use and which software supports them?
Both for synced and unsynced lyrics and also which tag for which file format?
I know of SYLT and USLT frames for mp3 and LYRICS and UNSYNCEDLYRICS vorbis tags for flac.

Upon testing I learned that mp3tag for example does not support the SYLT frame used in mp3 files.
This is the latest post I found concerning SYLT tags, which states that support is not planned.
However what I have not found yet is a reason for that decision.

Has the SYLT tag limitations I'm not aware of and should be avoided?

I could use the cli of kid3 instead of mp3tag to embed lyrics in my script as that supports the SYLT frame but I'd prefer not to.
Being able to review all files that will be affected by an import prior to importing is a neat feature of my current solution with mp3tag.

Here's a quick example usage of the current version of the script to get an idea of what I aim for. I deleted the existing mp3tag actions prior to running it to show how they are created, if they already exist, creation is skipped.
As it's a fullscreen capture, if you want to see it sharp, download the gif or display it fullscreen if you have a 1080p monitor or bigger.

The script can also import .txt files, export LYRICS to .lrc and UNSYNCEDLYRICS to .txt (via ffprobe), standardize synced lyrics timestamp formatting, log results to disk etc. but I wanted to get the tag question out of the way before writing documentation and putting it on github so others can use it too.

I use UNSYNCEDLYRICS only.
The reason is easy: There are more online sources for this format (song text without timestamps).

The other reason: I never use Karaoke.
Synchronized Lyrics are cool if you want to sing (& dance :wink:) to a music track or -video and see the lyrics matching the song.


Technical answer:

According the specification for ID3 the tags are USLT
for unsynchronised lyrics - Mp3tag is using UNSYNCEDLYRICS

and SYLT
for the synchronised lyrics - Mp3tag does currently NOT support SYLT


This can only be answered by the developer of Mp3tag.

The developer does not have to give a reason. Better to simply accept that and move on. Let's be grateful for all that he has given us..

The main reason is, that it would require dedicated UI to enter timestamp/lyrics and that this would only be available for ID3v2-tagged files.

Another reason is the relatively complex format of the synced lyrics ID3v2 frame, which makes is difficult (but I guess not impossible) to create a textual representation that can then be used with all other operations Mp3tag has to offer.

Thanks, Doug. I've answered anyway, mainly because of this question:

Simply to clarify that it's not the SYLT frame per se, but all the implementation effort and the resulting inconsistencies in Mp3tag's UI and internals that motivate this decision.

That's useful information. I was not aware of that mapping, thanks.

Thank you for going into detail. After spending a few more hours on this today I have to agree with you. The SYLT frame seems more complicated and strict than it needs to be (all times in ms, no support for tags in [xxx:yyy] format etc.).

At this point I have to say that internal mappings are often quite painful for scripting.

My initial approach was using ffprobe to extract the lyrics like this (I reduced the lyrics to 2 lines to avoid spam):

ffprobe

synced:

ffprobe -v quiet -print_format json -show_entries format_tags=LYRICS "01 24‐25.flac"
{
    "format": {
        "tags": {
            "LYRICS": "[0:17.576]She'll be gone soon\r\n[0:19.127]You can have me for yourself\r\n"
        }
    }
}

unsynced:

ffprobe -v quiet -print_format json -show_entries format_tags=UNSYNCEDLYRICS "01 24‐25.flac"
{
    "format": {
        "tags": {
            "UNSYNCEDLYRICS": "She'll be gone soon\r\nYou can have me for yourself\r\n"
        }
    }
}

That works perfectly for flac files with vorbis tags (I fixed the linebreaks (\r\n) while parsing the JSON response before writing .lrc/.txt files).

When used with mp3 files however, only LYRICS works:

ffprobe -v quiet -print_format json -show_entries format_tags=LYRICS "01 24‐25.mp3"
{
    "format": {
        "tags": {
            "LYRICS": "[0:17.576]She'll be gone soon\r\n[0:19.127]You can have me for yourself\r\n"
        }
    }
}

UNSYNCEDLYRICS does not:

ffprobe -v quiet -print_format json -show_entries format_tags=UNSYNCEDLYRICS "01 24‐25.mp3"
{
    "format": {
        "tags": {

        }
    }
}

Neither does USLT, which is the frame mp3tag writes for mp3 files:

ffprobe -v quiet -print_format json -show_entries format_tags=USLT "01 24‐25.mp3"
{
    "format": {
        "tags": {

        }
    }
}

Turns out ffprobe maps the USLT tag internally to "lyrics-" + whatever language is stored in the frame:

Syntax: xxx||Lyrics where xxx = Language of the lyrics, abbreviated by 3 characters according to ISO-639-2.

Which is why this works.

ffprobe -v quiet -print_format json -show_entries format_tags=lyrics-eng "01 24‐25.mp3"
{
    "format": {
        "tags": {
            "lyrics-eng": "She'll be gone soon\r\nYou can have me for yourself\r\n"
        }
    }
}

Since that depends on this mp3tag setting (at least when embedding the lyrics through mp3tag):
eng

That makes it pretty much useless for scripting as I don't have a way to check what language is set.

kid3-cli

As an alternative I tried using kid3-cli to achieve the same.
Which once again works for flac files:

Synced:

kid3-cli -c "get LYRICS" "01 24‐25.flac"
[0:17.576]She'll be gone soon
[0:19.127]You can have me for yourself

Unsynced:

kid3-cli -c "get UNSYNCEDLYRICS" "01 24‐25.flac"
She'll be gone soon
You can have me for yourself

But for mp3 files it returns unsynced lyrics when called with LYRICS:

kid3-cli -c "get LYRICS" "01 24‐25.mp3"
She'll be gone soon
You can have me for yourself

Nothing for UNSYNCEDLYRICS.

kid3-cli -c "get UNSYNCEDLYRICS" "01 24‐25.mp3"

And once again unsynced lyrics when called with USLT:

kid3-cli -c "get USLT" "01 24‐25.mp3"
She'll be gone soon
You can have me for yourself

Turns out kid3-cli maps USLT to "Lyrics" internally and the vorbis tag LYRICS to "LYRICS", since it is not case sensitive tho, I have not found a way to make it return the TXXX LYRICS tag instead of USLT.

When using SYLT instead of LYRICS for mp3 files, kid3-cli would work, however I'd like to avoid using that frame as mp3tag does not support it and I dislike how restrictive it is.

I guess I could use ffprobe to extract the vorbis tags for flac files and the synced lyrics for mp3 files and then use kid3-cli to get the USLT frame from mp3 files, but that adds one more dependency and feels quite hacky.

So if anyone knows a cli program that can reliably return the vorbis tags LYRICS and UNSYNCEDLYRICS as well as the id3 tags for SYLT and USLT (I do want to support exporting the SYLT frame to .lrc so people can get rid of it), please let me know.

Thanks for the interesting information you have shared with us.

I could have a look if you can provide your 4 test files (2 x mp3 and 2 x FLAC).
Please let me know in a PM, where I can DL those files.
(Maybe you can use some service like "WeTransfer" or similar?)

Thanks for your help.

As it turns out, mutagen offers everything I needed.

I've added some more functions to the script, now it has 4 modes:
test, mp3tag, import, export

I've called the final script lyrict, short for lyrictest or lyrictool.
If anyone wants to test it and/or give me feedback, here's the link to the github repo with detailed instructions on how to install and use it (perhaps too detailed).
When in doubt, skip to the Common examples to get a running start.

However, I strongly suggest that you only use this on test files or when you have an up-to-date backup of your library. :wink:

Thought I'd update this since I've put some more work into my script in the meantime.
Now it supports a wider range of timestamps and also fixes nonsensical timestamps with more than 59 minutes or seconds:
[hh:mm:ss.xxx] [mm:ss.xxx] [mm:ss] [hh:mm:ss.xx] [mm:ss.xx]
I've also added a 5th mode "tag_external" which allows to tag external lyrics files based on existing tags and tags from linked mp3/flac files.

Example:
01 24‐25.flac is tagged like this:
grafik
01 24‐25.lrc contains these lyrics:

[00:17.576]She'll be gone soon
[00:19.127]You can have me for yourself
[00:25.307]She'll be gone soon
[00:26.807]You can have me for yourself
[00:31.607]But do give
[00:35.927]Just give me today
[00:39.287]Or you will just scare me away
[00:47.131]
[01:04.014]What we built is bigger
...

Running lyrict.py -m tag_external in that folder changes the .lrc file to:

[ar:Kings of Convenience]
[al:Declaration of Dependence]
[ti:24-25]
[au:Eirik Glambek Bøe; Erlend Øye]
[length:03:38]
[00:17.576]She'll be gone soon
[00:19.127]You can have me for yourself
[00:25.307]She'll be gone soon
[00:26.807]You can have me for yourself
[00:31.607]But do give
[00:35.927]Just give me today
[00:39.287]Or you will just scare me away
[00:47.131]
[01:04.014]What we built is bigger
...

These are the tags/properties it currently extracts from mp3 and flac files.
It also updates existing tags, so if the .lrc file starts out like this:

[al:garbage]
[ar:garbage]
[ti:garbage]
[la:eng]
[by:Casual_Tea]
[offset:+0]
[re:MusicBee]
[tool:MusicBee]
[ve:3.5.8698P]
[00:17.576]She'll be gone soon
[00:19.127]You can have me for yourself
[00:25.307]She'll be gone soon
[00:26.807]You can have me for yourself
[00:31.607]But do give
[00:35.927]Just give me today
[00:39.287]Or you will just scare me away
[00:47.131]
[01:04.014]What we built is bigger
...

running the same command changes the order and updates the garbage tags to:

[ar:Kings of Convenience]
[al:Declaration of Dependence]
[ti:24-25]
[au:Eirik Glambek Bøe; Erlend Øye]
[length:03:38]
[la:eng]
[offset:+0]
[by:Casual_Tea]
[re:MusicBee]
[ve:3.5.8698P]
[00:17.576]She'll be gone soon
[00:19.127]You can have me for yourself
[00:25.307]She'll be gone soon
[00:26.807]You can have me for yourself
[00:31.607]But do give
[00:35.927]Just give me today
[00:39.287]Or you will just scare me away
[00:47.131]
[01:04.014]What we built is bigger
...

I've also overhauled the --standardize argument, it now has 3 modes to either preserve existing timestamp formatting (but still fixing errors) or to force all timestamps into one of 2 patterns.
The default behavior is to keep the timestamp formats as they are, so a source .lrc file containing.

[00:01]text
[00:02.34]text
[00:03.456]text

will not be changed (unless there's more than 59 minutes or 59 seconds in the timestamp):
[00:62.123]text would become [01:02.123]text and
[68:05.234]text would become [01:08:05.234]text

The other 2 modes allow enforcing a strict timestamp format:
Source:

[00:01]text
[00:02.34]text
[00:03.456]text

using lyrict.py -m tag_external --standardize force.xx on this would yield:
Result 1:

[00:01.00]text
[00:02.34]text
[00:03.45]text

and using lyrict.py -m tag_external --standardize force.xxx would yield:
Result 2:

[00:01.000]text
[00:02.340]text
[00:03.456]text

The tag_external mode also reduces consecutive empty lines to 1 empty line and removes spaces following timestamps, so:

[00:17.576]      She'll be gone soon



[00:19.127]You can have me for yourself
[00:25.307]                   She'll be gone soon




[00:26.807]You can have me for yourself

would become:

[00:17.576]She'll be gone soon

[00:19.127]You can have me for yourself
[00:25.307]She'll be gone soon

[00:26.807]You can have me for yourself

Let me know if you find it useful (or if you encounter errors). :grinning: