Skip to content

Letras (.lyr)

Índice

  1. Resumen
  2. Formato del archivo
  3. Campos de cabecera
  4. Líneas de letra con timestamp
  5. Ejemplo completo
  6. Constantes del motor
  7. Funciones del motor
  8. Flujo de carga
  9. Notas y consejos

Resumen

El motor de letras de la barra AGS lee archivos con extensión .lyr almacenados en ~/lyrics/. Cada archivo contiene los metadatos de una canción y sus letras con timestamps de inicio y fin por línea. El motor indexa todos los archivos al arrancar y los mantiene en caché en memoria para minimizar lecturas de disco.

Las letras se muestran en la píldora derecha de la barra de música (ver AGS Bar → LyricsViewer).


Formato del archivo

Los archivos .lyr son texto plano. Tienen dos secciones:

  1. Cabecera: tags name:"..." / author:"..." (+ opcional isinstrumental:).
  2. Letra: líneas en formato LRC [MM:SS.xx] texto.

Las líneas que empiezan con # son comentarios y se ignoran. Las líneas vacías también.

# Comentario opcional
name:"Nombre oficial de la canción"
author:"Artista principal"

[00:10.00] Primera línea de letra
[00:14.50] Segunda línea
[00:18.75] Con precisión de centésimas

Campos de cabecera

Campo Requerido Descripción
name:"..." Recomendado Título de la canción. Acepta alias separados por \|\|\|: el primero es el principal, el resto matchean títulos alternativos.
author:"..." Recomendado Artista principal. También acepta alias con \|\|\| (feat / karaoke / remix, distintos nombres del artista).
isinstrumental: Opcional true → la píldora muestra "Instrumental" y se ignoran las líneas de letra. Acepta también "isinstrumental": true o is_instrumental.

other: y author_other: fueron eliminados

Los tags other: / author_other: (separados por comas) ya no existen. Ahora los alias van dentro de name: / author: separados por ||| (solo la subcadena exacta ||| separa — un | o || suelto no divide).

Ejemplo de alias con |||

name:"Flowers|||Flowers (Official)"
author:"Miley Cyrus|||Miles C"

Permite que el archivo matchee aunque el reproductor muestre "Miles C" o "Flowers (Official)".


Líneas de letra con timestamp

Cada línea usa el formato LRC (un solo timestamp de inicio):

[MM:SS.xx] texto de la letra
  • MM:SS.xx: minutos:segundos con fracción opcional. El motor lo convierte a microsegundos para sincronía exacta.
  • El texto va sin comillas después del ].
  • El fin de cada línea se infiere como el inicio de la siguiente (la última recibe una cola sintética de +8 s) — no se escribe.
  • Una línea con timestamp y texto vacío es una pausa (se muestra ♫ ... ♫).
  • Las líneas se ordenan por tiempo al parsear, así que el orden en el archivo no importa.

Ejemplo completo

# flowers.lyr
name:"Flowers|||Flowers (Official)"
author:"Miley Cyrus|||Miles C"

[00:00.00]
[00:18.00] We were good, we were gold
[00:22.00] Kinda dream that can't be sold
[00:26.00] We were right till we weren't
[00:30.00] Built a home and watched it burn
[00:34.00]
[00:42.00] I didn't want to leave you
[00:46.00] I didn't want to lie

Ejemplo instrumental (guardado en ~/lyrics/instrumental/):

name:"Loud Pipes"
author:"Ratatat"
isinstrumental: true

Nombre del archivo

El nombre del archivo (sin extensión) también se usa como clave de búsqueda. flowers.lyr permite encontrar la canción buscando "flowers".


Constantes del motor

Definidas en ~/.config/ags/widget/Bar/MusicBar.tsx:

Constante Valor Descripción
LYRICS_DIR ~/lyrics Directorio donde se buscan los .lyr
INSTRUMENTAL_DIR ~/lyrics/instrumental Donde se guardan los .lyr marcados isinstrumental: true
MAX_CACHE 200 Máximo de entradas en caché de memoria
DECAY_λ 0.001 Factor de decaimiento para puntuación de caché
NO_TRACK /org/mpris/.../NoTrack ID MPRIS de "sin pista activa"

Funciones del motor

normalizeSongStr(s)

Normaliza una cadena para comparación fuzzy: - Convierte a minúsculas. - Elimina paréntesis comunes: (Remastered), (Live), (feat. X), (Explicit), etc. - Elimina caracteres no alfanuméricos. - Colapsa espacios múltiples.

normalizeSongStr("Flowers (Remastered 2023)") // → "flowers"
normalizeSongStr("Lo Que Siento (feat. Cuco)") // → "lo que siento"

parseTimeMicros(raw)

Convierte un string de timestamp "MM:SS.ffffff" a un número de microsegundos.

parseTimeMicros("1:23")        // → 83_000_000
parseTimeMicros("1:23.5")      // → 83_500_000
parseTimeMicros("0:18.500000") // → 18_500_000

parseLyrFile(path, content)

Parsea el contenido completo de un archivo .lyr y devuelve un objeto LyricFile:

interface LyricFile {
  path:           string      // Ruta absoluta del archivo
  names:          string[]    // name: → alias por |||; [0] = principal
  authors:        string[]    // author: → alias por |||; [0] = principal
  lines:          LyricLine[] // Líneas de letra (LRC)
  isInstrumental: boolean     // tag isinstrumental: true → píldora muestra "Instrumental"
}

interface LyricLine {
  start: number  // microsegundos
  end:   number  // microsegundos
  text:  string
}

El parser recorre el archivo línea a línea:

  1. Ignora líneas vacías y comentarios (#).
  2. Detecta cabecera con regex: name:, author: (alias por |||), isinstrumental:.
  3. Detecta líneas de letra con el patrón LRC [MM:SS.xx] texto.
  4. Ordena las líneas por tiempo e infiere el end de cada una como el start de la siguiente (la última recibe +8 s).

buildIndex()

Escanea ~/lyrics/ y ~/lyrics/instrumental/ (helper indexDir) y construye lyricsIndex, un Map<string, LyricFile[]> donde las claves son:

  • El nombre del archivo (sin .lyr).
  • Cada alias de name: (separados por |||).

Todo en minúsculas. Múltiples archivos pueden compartir una clave.


watchLyricsDir()

Usa Gio.FileMonitor para vigilar el directorio ~/lyrics/. Cuando se detecta un cambio (archivo añadido, modificado o eliminado), llama a buildIndex() después de un debounce de 400 ms.

Esto significa que puedes añadir o editar archivos .lyr sin reiniciar AGS.


lookupOnDisk(title, artist)

Busca el mejor archivo .lyr para una canción dada. Algoritmo:

  1. Normaliza title y artist.
  2. Busca en lyricsIndex todas las claves que contengan o estén contenidas en el título normalizado.
  3. Para cada candidato, calcula una puntuación con scoreOf().
  4. Devuelve el candidato con la mayor puntuación (≥ 0).

Puntuación scoreOf(lf)

Condición Puntuación
Ningún alias de authors coincide con el artista -1 (descartado)
El nombre principal (names[0]) coincide exactamente con el título 3
names[0] contiene o es contenido por el título 2
Algún alias de names coincide exactamente 2
Algún alias de names contiene o es contenido por el título 1
Sin metadatos de nombre 0

cacheLookup(songId, title, artist)

Wrapper sobre lookupOnDisk con caché en memoria (lyricCache). La caché:

  • Identifica canciones por songId (basado en trackid MPRIS o artista|título normalizado).
  • Incrementa un contador de reproducciones y actualiza lastPlayedAt.
  • Si la caché supera MAX_CACHE entradas, expulsa la de menor puntuación usando decaimiento exponencial:
score = playCount * Math.exp(-DECAY_λ * segundos_desde_última_vez)

findActiveLine(lines, posMicros)

Dado un array de LyricLine[] y la posición actual de reproducción en microsegundos, devuelve el índice de la línea activa: aquella cuyo start <= pos < end. Retorna -1 si ninguna línea está activa en ese momento.


Flujo de carga

AGS arranque
    └── buildIndex()          — lee ~/lyrics/, llena lyricsIndex
    └── watchLyricsDir()      — inicia monitor de cambios

Track cambia (MPRIS event)
    └── buildSongId()                    — genera ID único para la pista
    └── cacheLookupWithApiFallback()
            └── lyricCache hit?          → retorna inmediatamente
            └── on-disk (.lyr / instrumental) → si hay, retorna
            └── fetchLrclib()            → lrclib.net (1 request; retry con título limpio)
                    └── encuentra letra  → escribe un .lyr nuevo en ~/lyrics/ (reindexa)
                    └── instrumental     → guarda en ~/lyrics/instrumental/
                    └── nada             → neg-cache 30 días (no vuelve a pedir)
    └── LyricsViewer actualiza líneas

Cada LYRIC_SYNC_MS (default 5 ms; low-end: 300 ms)
    └── posición interpolada → microsegundos
    └── findActiveLine() → índice de línea activa
    └── LyricsViewer resalta la línea activa

lrclib.net + logs

Si no hay .lyr local, el motor (Helpers/Lyrics.ts) busca en lrclib.net y cachea el resultado como .lyr para la próxima vez. Los eventos (REQ, LYR, MISS/CACHE, MISS/NOLYR, NET ERR) se ven en el Dashboard → Logs → lrclib.net. La prioridad local-vs-online se ajusta en Settings → MUSIC BAR (lyricsPriority).


Notas y consejos

Cómo añadir letras

  1. Crea ~/lyrics/nombre-cancion.lyr
  2. Añade la cabecera con name: y author:
  3. Añade las líneas con timestamps
  4. AGS detecta el archivo automáticamente (sin reiniciar)

Canciones con múltiples artistas o títulos

Usa alias con ||| en author: (o name:) para cubrir casos donde la misma canción aparece bajo distintos nombres. Ej: author:"Artista|||Nombre alternativo". Tantos alias como quieras; el fin de cada línea se infiere, no hay que preocuparse por timestamps de fin.

Herramientas útiles

  • LRC Maker online — para crear letras con timestamps exportables
  • Las líneas .lyr ya usan el formato de línea LRC ([MM:SS.xx] texto): tomá un .lrc, quitá los tags tipo [ti:]/[ar:], agregá name:"..." / author:"..." arriba, guardá como .lyr
  • Si la canción no tiene letra pero es instrumental, marcala con isinstrumental: true (se guarda en ~/lyrics/instrumental/)