Is it possible to make a +LYRICS track fill width / have a huge font?

I’m trying to build a track for fill in musicians… Have a scrolling chart - take a bass player for this example…

After lots of hours trying pngs ( too much time to build out dozens and dozens of pngs per song ) Ive landed on this method using a stabdard +LYRICS track… BUT, i want to be able to make the font / text a lot bigger to fill the screen. Currently using [large] attribute.

Is there any way to increase the font? Have it either fill to width, or be able to be 75% width / a specific width etc…

Goal is to see the current line highlighted and see the next few lines or so / whats coming as well…

Leo has helped me out with this. I’ve used textFit.js but have had some better success with fitty.js. I’d be happy to share the files I have tomorrow. It basically lets you set a max and min font and then the font increases depending on the amount of text in the space.

1 Like

This is great to hear. Thanks so much! Looking forward to hearing back from you.

// script.js
// Full script using CDN for Fitty.js loading
// Updated selectors for AbleSet update (approx. April 2025)

// --- Load Fitty via CDN ---
const fittyScript = document.createElement('script');
fittyScript.src = 'https://unpkg.com/fitty@2.3.6/dist/fitty.min.js'; // CDN URL
fittyScript.onerror = () => {
    console.error("FATAL ERROR: Could not load Fitty.js from CDN. Check internet connection or CDN status.");
};
fittyScript.onload = () => {
    if (typeof fitty === 'undefined') {
        console.error("Fitty CDN onload event fired, but 'fitty' function is STILL undefined!");
    } else {
        console.log("Fitty loaded successfully from CDN. Type:", typeof fitty);
        init(); // Initialize main logic
    }
};
document.head.appendChild(fittyScript);
// --- End Fitty Loading ---


// --- Global Variables ---
let observer = null;
let resizeTimeout = null;
let lastFittedSpans = new Set();
let observerInitialized = false;
let integrationInitialized = false;
let lastLocation = "";

// --- Utility Functions ---
function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

const fitLyrics = (span) => {
    // --- START: MODIFICATION TO IGNORE 'Measures' TRACK ---
    // Traverse up from the span to see if any parent container has the class 'measures'.
    let parentElement = span.parentElement;
    let isMeasuresTrack = false;

    while (parentElement) {
        // Check if the current parentElement contains both 'lyrics' and 'measures' classes
        // Or, more simply, just check for 'measures' if that's unique enough to this context.
        // Given the screenshot, the element with "lyrics measures sans" seems to be a good target.
        if (parentElement.classList && parentElement.classList.contains('measures')) {
            isMeasuresTrack = true;
            break; // Found the 'measures' class, no need to go further up
        }
        // Stop if we reach a very high-level container or the body, to prevent infinite loops on detached elements.
        if (parentElement.classList && parentElement.classList.contains('element-wrapper') && parentElement.classList.contains('element-lyrics')) {
            break; // Reached the main lyrics wrapper, probably not a measures track if not found by now
        }
        if (parentElement.tagName === 'BODY') {
            break;
        }
        parentElement = parentElement.parentElement;
    }

    if (isMeasuresTrack) {
        console.log("Ignoring lyric from 'Measures' track (detected by 'measures' class on parent):", span.textContent.substring(0, 30) + "...");
        // Optional: Reset styles if Fitty might have been applied previously by another path,
        // though with this check at the start, it should prevent initial application.
        // span.style.fontSize = ''; // Example: reset font size
        return; // Exit the function, skipping Fitty scaling for this span
    }
    // --- END: MODIFICATION TO IGNORE 'Measures' TRACK ---

    // Safety Check: Ensure fitty is loaded (existing check)
    if (typeof fitty === 'undefined') {
        console.error("fitLyrics called, but 'fitty' is not defined! Aborting fit for span:", span);
        return;
    }

    // Basic checks and visibility (existing checks)
    if (!span || !span.parentNode) return;
    const rect = span.getBoundingClientRect();
    if (rect.width === 0 || rect.height === 0 || span.offsetParent === null) {
         if(lastFittedSpans.has(span)) lastFittedSpans.delete(span);
        return;
    }

    // 1. Ensure parent (p) has correct width/display FOR WRAPPING (existing logic)
    const parentP = span.parentNode;
    if (parentP.tagName === 'P') {
        if (parentP.style.width !== '100%') parentP.style.width = '100%';
        if (parentP.style.display !== 'block') parentP.style.display = 'block';
    } else {
        console.warn("FitLyrics: Span parent is not a <p> tag", span);
    }

    // 2. Apply Fitty (existing logic - your minSize/maxSize are already in your pasted code)
    try {
        fitty(span, {
            minSize: 175, // Your preferred min size from your code
            maxSize: 250, // Your preferred max size from your code
            multiLine: true,
        });
        lastFittedSpans.add(span);
    } catch (e) {
        console.error("Error during fitty() call:", e, span);
    }
};

// Debounced function for handling window resize
const debouncedFitAll = debounce(() => {
    lastFittedSpans.forEach(span => {
        const rect = span.getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0 && span.offsetParent !== null) {
            fitLyrics(span);
        } else {
             lastFittedSpans.delete(span);
        }
    });
}, 150);

// Sets up the MutationObserver to watch for DOM changes
const setupMutationObserver = () => {
    // ** UPDATED SELECTOR: Target the stable 'element-lyrics' wrapper **
    // We'll observe this element and its subtree.
    const lyricsWrapper = document.querySelector(".element-wrapper.element-lyrics");
    if (!lyricsWrapper) {
        // Fallback if the combined class isn't found, try just element-lyrics
        lyricsWrapper = document.querySelector(".element-lyrics");
    }

    if (!lyricsWrapper) {
        console.warn("Lyrics wrapper (.element-lyrics) not found for observer.");
        return false; // Indicate failure
    }

    if (observer) {
        observer.disconnect();
        // console.log("Disconnected old observer");
    }

    observer = new MutationObserver((mutations) => {
        requestAnimationFrame(() => { // Batch updates
            let spansToFit = new Set();
            mutations.forEach((mutation) => {
                // ** UPDATED SELECTORS inside observer callback **
                const targetSpanSelector = '.lyrics-line span'; // Use stable class

                if (mutation.type === 'characterData') {
                    let targetNode = mutation.target;
                    let currentElement = targetNode.parentElement;
                    // Traverse up to find the containing span within a lyrics-line
                    while (currentElement && !currentElement.matches(targetSpanSelector)) {
                        // If we hit the wrapper before finding the span, stop
                        if (currentElement === lyricsWrapper) {
                             currentElement = null;
                             break;
                        }
                        currentElement = currentElement.parentElement;
                    }
                    if (currentElement) spansToFit.add(currentElement);

                } else if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                           // Find spans within the added node(s) using stable class
                           node.querySelectorAll(targetSpanSelector).forEach(span => spansToFit.add(span));
                           // If the node itself is the span we need (less likely but possible)
                           if (node.matches && node.matches(targetSpanSelector)) spansToFit.add(node);
                        }
                    });
                }
            });
            spansToFit.forEach(span => fitLyrics(span));
        });
    });

    // Observe the wrapper element's subtree
    observer.observe(lyricsWrapper, {
        childList: true,
        subtree: true,
        characterData: true,
    });

    console.log("MutationObserver setup on", lyricsWrapper);
    observerInitialized = true; // Mark as initialized
    return true; // Indicate success
};

// Handles the 'tracksUpdated' event from AbleSet's socket
const handleTracksUpdated = () => {
    requestAnimationFrame(() => {
        // ** UPDATED SELECTOR: Use stable classes **
        // Target lines that are not marked as 'after-current'. '.active' might also be useful.
        const visibleLinesSelector = '.lyrics-line:not(.after-current)';
        const visibleSpans = document.querySelectorAll(visibleLinesSelector + ' span');

        if (visibleSpans.length === 0) {
            // Fallback: Try the first lyrics line span if primary fails
            const firstSpan = document.querySelector('.lyrics-line span');
             if (firstSpan) fitLyrics(firstSpan);
        } else {
            visibleSpans.forEach(span => fitLyrics(span));
        }
    });
};

// Sets up the connection to AbleSet's WebSocket for lyrics updates
const initAbleSetIntegration = () => {
    if (window.ableset && ableset.getSocket) {
        const socket = ableset.getSocket("lyrics");
        if (socket) {
            socket.off('tracksUpdated', handleTracksUpdated); // Prevent duplicates
            socket.on('tracksUpdated', handleTracksUpdated);
            integrationInitialized = true;
             setTimeout(handleTracksUpdated, 150); // Initial fit trigger
             console.log("AbleSet socket integration initialized.");
        } else {
            console.warn("AbleSet lyrics socket not available.");
            integrationInitialized = false;
        }
    } else {
        console.warn("AbleSet API or getSocket not found.");
        integrationInitialized = false;
    }
};

// Cleans up observers and listeners
const cleanup = () => {
    console.log("Cleaning up lyrics script...");
    if (observer) {
        observer.disconnect();
        observer = null;
    }
    window.removeEventListener('resize', debouncedFitAll);
    if (window.ableset && ableset.getSocket) {
        const socket = ableset.getSocket("lyrics");
        if (socket) {
            socket.off('tracksUpdated', handleTracksUpdated);
        }
    }
    lastFittedSpans.clear();
    observerInitialized = false;
    integrationInitialized = false;
     console.log("Cleanup complete.");
};

// --- Initialization and Main Loop ---
const init = () => {
    console.log("Initializing lyrics script main logic...");
    setInterval(() => {
        const currentHref = location.href;
        const isOnLyricsPage = currentHref.includes("/lyrics");

        if (isOnLyricsPage && (!observerInitialized || currentHref !== lastLocation)) {
            if (currentHref !== lastLocation) {
                 console.log("Navigated to lyrics page:", currentHref);
                 cleanup();
            } else {
                 console.log("Initial load on lyrics page or re-init attempt.");
            }
            lastLocation = currentHref;

            if (!observerInitialized) setupMutationObserver(); // Initialize observer
            if (observerInitialized && !integrationInitialized) initAbleSetIntegration(); // Initialize socket integration
            if (observerInitialized) {
                 window.removeEventListener('resize', debouncedFitAll); // Add resize listener
                 window.addEventListener('resize', debouncedFitAll);
            }
        }
        else if (!isOnLyricsPage && observerInitialized) {
            console.log("Navigated away from lyrics page.");
            cleanup();
            lastLocation = currentHref;
        }
         else if (isOnLyricsPage && currentHref === lastLocation) {
             if (!observerInitialized) { // Retry setup if failed
                 console.log("Re-attempting observer setup on lyrics page.");
                 setupMutationObserver();
             }
             if (observerInitialized && !integrationInitialized) { // Retry setup if failed
                 console.log("Re-attempting AbleSet integration.");
                 initAbleSetIntegration();
                 window.removeEventListener('resize', debouncedFitAll);
                 window.addEventListener('resize', debouncedFitAll);
             }
         }
         else if (!isOnLyricsPage) {
             lastLocation = currentHref;
         }
    }, 750);

    // Initial check for page load
     if (location.href.includes("/lyrics")) {
         console.log("Already on lyrics page at Fitty load time.");
         lastLocation = location.href;
         if (!observerInitialized) setupMutationObserver();
         if (observerInitialized && !integrationInitialized) initAbleSetIntegration();
         if (observerInitialized) window.addEventListener('resize', debouncedFitAll);
     } else {
         lastLocation = location.href;
     }
};

// --- End of script ---

This is using the CDN. I actually forgot I had it set that way but I do have the fitty.min.js file in my folder just in case I need to swap it over.

Ok wow…

I have zero idea of what that is implying / how that works!

So… am I simply going to copy and paste that into my styles.css file?

Thanks!
T

Haha. Don’t worry. I only barely understand it and it was only because of Leo that it works at all. But No that will go into the script.js file. I just realized I didn’t send my css file contents but I don’t think it is necessary for the functionality but I will send that tomorrow!

1 Like

I added it to the scripts file but isn’t doing anything…

Do i need to adjust any elements?

Make sure the lyrics track is called Lyrics +LYRICS.

And I also forgot to mention that I had to heavily modify it to make it work in a canvas. So make a canvas and add the Lyric Tracks to it and see if that works.

+LYRICS is definetly correct.

I’m not using canvas… I’m on 2.7.5

Ooooh gotcha. Yeah that’d be a problem. Download version 3 and give it a try. You can have both version 3 and version 2 on your computer at the same time. The only thing you’d have to do to swap between the two is restart Abelton so it can put the right version of the plug in, in.

Have downloaded 3 beta .12

Canvas working… But still not filling screen with chords / font…

This is my tracks info:

BASS +LYRICS +CC [full] [nosections][linemarker][large][center]

Any issues there?

Change BASS to Lyrics and see if that does it

Nah… No change still…

The chords should be filling to width of Lyrics window yeah?

Yeah it should be. I’ll send a screenshot of what I have and my css stuff too and see if it works for you then. I’ll also send the fitty.min.js file. I might have referenced it and not used the CDN. All you would do is put that .js file into the Custom Styles folder with the styles.css and script.js file

1 Like

Very Muchly appreciated mate!

.lyrics-page.lyrics .line {
  height: calc(var(--height) * 0.5);
  outline: 5px solid gray;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 99%;
  overflow: hidden;
  padding-left: 50px;
  padding-right: 50px;
  padding-top: 50px;
  padding-bottom: 50px;
  
}

div.lyrics.measures .lyrics-line span {
  font-size: 195px !important;
}

div.lyrics.key .lyrics-line span {
  font-size: 100px !important;
}

.lyrics .lyrics-line,
.lyrics .lyrics-line p {
  width: 100%;
  line-height: 1;
  
}

.lyrics strong {
  color: var(--color-white-400);
  font-weight: normal;
}

.lyrics em {
  color: var(--color-yellow-400);
  font-style: normal;
  
}

div[class*="measures"] {
  --transition-time: 0ms !important;
}


.lyrics .measures{
  /* default value is 500ms */
  
}

 /* any lyrics container */
 .lyrics {
  /* default value is 500ms */
  --transition-time: 500ms;
}



/* .lyrics .image-line .full img {
  height: calc(var(--height) * 0.5);
  max-height: calc(var(--height) * 0.5);
} */
/* (Same as before - no changes required for this fix) */
.element-lyrics {
    padding: 20px; /* Example padding */
}

/* #el-mHMWnDEhPnenUR9Qjhc7iv > div.element-wrapper.element-lyrics.absolute.top-0.left-0.bottom-0.right-0.rounded-2xl > div > div {
  font-size: 200px;
} */

/* .lyrics .measures{
  font-size: 200px;
} */

.lyrics-page .Measures
  {
    --transition-time: 0;
}


This is the contents of the CSS file. Not all of this is relevant.

And then go here:

Download the source code (zip), expand it, then get the fitty.min.js file and drop that into your custom styles folder.

And here is what my LYRICS files look like.

Really appreciate all your time on this.

I followed your steps, and it did something ( see attached pics ) but not what i was hoping for… It didn’t fit to a set width / fill the width etc… And it lost all my formatting.


I think at this stage I’ll cut my losses and just deal with what i have for the time being.

The styling i have working (see pics in above posts) on lyrics page works for me, I would just love to be able to have each line a set width / dynamic width. So if song title is long it shrinks down to the width / or break into two lines etc etc… For the most part it will be 4 bar chunks across the page, 1 chord per bar, but occasionally there will be 1 bar with 4 or more notes in it so want that 1 bar to stretch the width of the page for example.

Again - unless there’s a simple fix i’m missing, I’ll just push on with the system i have working for now and abandoned this fitty.js approach.

Really appreciate your effort mate! Legend.

T

1 Like