Skip to content

kvaell14/P

 
 

Repository files navigation

#!/usr/bin/env python3
import os, sys, re, shutil, difflib, tempfile, requests
import numpy as np
import sounddevice as sd
import soundfile as sf
import pyfiglet

#if the lyrics don't appear, make sure the file is the song name, preferably (songname, author).mp3
AUDIO_FILE   = "Sweather Weather.mp3"   # your song hear
START_TIME   = 39  # if you want to skip in certain parts

LRC_FOLDER   = os.path.expanduser("~/lyrics")
BLOCKSIZE    = 2048



# if the lyrics don't sync you can manually adjust its' offset here
# in seconds.,, positive number for backwards, negative for forwards.
LYRIC_OFFSET = -1.7


def fetch_lrc_from_lrclib(title: str, artist: str = "") -> str | None:
    try:
        r = requests.get(
            "https://lrclib.net/api/search",
            params={"q": f"{title} {artist}".strip()},
            timeout=10,
        )
        r.raise_for_status()
        for item in r.json():
            if item.get("syncedLyrics"):
                return item["syncedLyrics"]
    except Exception as e:
        print("LRCLIB fetch failed:", e)
    return None

def find_closest_lrc(song_title: str, folder: str) -> str | None:
    if not os.path.isdir(folder):
        return None
    bases = [os.path.splitext(f)[0] for f in os.listdir(folder)
             if f.lower().endswith(".lrc")]
    closest = difflib.get_close_matches(song_title, bases, n=1, cutoff=0.4)
    return os.path.join(folder, closest[0] + ".lrc") if closest else None

def parse_lrc(path: str):
    pattern = re.compile(r"\[(\d+):(\d+(?:\.\d+)?)\](.*)")
    out = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            m = pattern.match(line.strip())
            if m:
                mins, secs, txt = m.groups()
                t = int(mins)*60 + float(secs) + LYRIC_OFFSET
                out.append((max(0.0, t), txt.strip()))
    return sorted(out)

base = os.path.splitext(os.path.basename(AUDIO_FILE))[0]
artist_guess, title_guess = ("", base)
if "-" in base:
    artist_guess, title_guess = [x.strip() for x in base.split("-", 1)]

lyrics_path = None
lrc_text = fetch_lrc_from_lrclib(title_guess, artist_guess)
if lrc_text:
    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".lrc")
    tmp.write(lrc_text.encode("utf-8")); tmp.close()
    lyrics_path = tmp.name
    print("Fetched synced lyrics from LRCLIB.")
else:
    match = find_closest_lrc(title_guess, LRC_FOLDER)
    if match:
        lyrics_path = match
        print(f"Using closest local match: {match}")
    else:
        print("No lyrics found online or locally.")

lyrics = parse_lrc(lyrics_path) if lyrics_path else []

data, samplerate = sf.read(AUDIO_FILE, dtype="float32")
if data.ndim > 1:
    data = np.mean(data, axis=1)
data = data / np.max(np.abs(data))
start_idx = int(START_TIME * samplerate)

term_columns, _ = shutil.get_terminal_size((200, 80))
columns = min(80, term_columns)
center  = 6
lyric_index = 0
for i, (t, _) in enumerate(lyrics):
    if t >= START_TIME:
        lyric_index = max(0, i - 1)
        break

def smooth(vals, window=5):
    if len(vals) < window: return vals
    k = np.ones(window)/window
    return np.convolve(vals, k, mode="same")

def hsv_to_rgb(h,s,v):
    i = int(h*6); f = h*6 - i
    p = int(255*v*(1-s)); q = int(255*v*(1-f*s)); t = int(255*v*(1-(1-f)*s))
    v = int(255*v); i = i % 6
    return [(v,t,p),(q,v,p),(p,v,t),(p,q,v),(t,p,v),(v,p,q)][i]

def colorize(text, r,g,b):
    return f"\033[38;2;{r};{g};{b}m{text}\033[0m"

def dim_color(text, r, g, b, factor=0.4):
    rr = int(r*factor); gg = int(g*factor); bb = int(b*factor)
    return f"\033[38;2;{rr};{gg};{bb}m{text}\033[0m"

def render_lyrics_block(idx, r, g, b):
    lines = []
    if idx < len(lyrics):
        lines.append(colorize(lyrics[idx][1], r, g, b))
    for n in range(1, 3):
        if idx + n < len(lyrics):
            lines.append("")
            lines.append(dim_color(lyrics[idx + n][1], r, g, b, 0.4))
    return "\n".join(lines)

def callback(outdata, frames, time_info, status):
    global start_idx, lyric_index
    if status: print(status, file=sys.stderr)

    chunk = data[start_idx:start_idx+frames]
    if len(chunk) < frames:
        outdata[:len(chunk),0] = chunk
        outdata[len(chunk):] = 0
        raise sd.CallbackStop
    else:
        outdata[:,0] = chunk

    play_time = start_idx / samplerate
    if lyrics:
        while lyric_index + 1 < len(lyrics) and play_time >= lyrics[lyric_index + 1][0]:
            lyric_index += 1

    step   = max(1, len(chunk)//columns)
    levels = np.abs(chunk[::step])
    levels = smooth(levels, window=6)
    levels = np.interp(np.clip(levels,0,1), [0,1], [0,center-1]).astype(int)

    hue = [0.15,0.3,0.6,0.8][int((play_time*0.2)%4)]
    r,g,b = hsv_to_rgb(hue,0.5,0.9)

    screen=[]
    for row in range(center*2):
        line=[]
        for lvl in levels:
            if row==center:
                line.append(colorize("─",r,g,b))
            elif row<center and (center-row)<=lvl:
                line.append(colorize("█",r,g,b))
            elif row>center and (row-center)<=lvl:
                line.append(colorize("█",r,g,b))
            else:
                line.append(" ")
        screen.append("".join(line))

    if lyrics:
        screen.append("")
        screen.append(render_lyrics_block(lyric_index, r, g, b))

    sys.stdout.write("\033[H\033[J" + "\n".join(screen))
    sys.stdout.flush()
    start_idx += frames

with sd.OutputStream(channels=1, samplerate=samplerate,
                     callback=callback, blocksize=BLOCKSIZE, latency="low"):
    sd.sleep(int((len(data) - start_idx) / samplerate * 1000))

About

The P programming language.

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 61.1%
  • OpenEdge ABL 27.7%
  • Java 9.1%
  • Python 0.8%
  • Shell 0.6%
  • ANTLR 0.5%
  • Other 0.2%