Lyrics Page Layout Similar to ProPresenter7 Stage Layout

I love seeing socket integration into the script! Thanks for making this possible for us Leo!

Do you think the textfit import would need moved up to to the top of the code? It currently appears to be re-imported on every page refresh.

Currently this isn’t possible because the script itself isn’t tagged as a module, so top-level import statements aren’t available, but I could change that in the next version :slight_smile:

I wouldn’t worry about the import running multiple times though – those requests are most likely cached so it shouldn’t have any impact on performance.

The latest beta, AbleSet 2.5.0-beta.1, now supports top-level imports in the custom script. Starting with this version, you can use the following syntax to import modules:

import { textFit } from "./textFit.js";

let observer;

const init = () => {
  const target = document.querySelector(".lyrics-page .lyrics div");

  if (!target) {
    console.log("This doesn't seem to be a lyrics page");
    return;
  }

  const callback = async (mutationList) => {
    const lyrics = document.querySelectorAll(".lyrics-line span");

    lyrics.forEach((el) => {
      el.style.display = "block";
      el.style.width = el.parentElement.clientWidth + "px";
      el.style.height = el.parentElement.clientHeight + "px";
    });

    // maxFontSize can be adjusted if the font becomes too large
    textFit(lyrics, { multiLine: true, maxFontSize: 300 });
  };

  observer = new MutationObserver(callback);
  observer.observe(target, { childList: true });
  callback();

  // Since AbleSet 2.4.0, you have access to real-time updates
  ableset.getSocket("lyrics").on("tracksUpdated", () => {
    setTimeout(callback, 1000);
  });
};

// Keep track of the current URL so we can run
// the script again when navigating to a lyrics page
let lastLocation = "";

setInterval(() => {
  if (location.href !== lastLocation && location.href.includes("/lyrics")) {
    lastLocation = location.href;
    observer?.disconnect();
    init();
  }
}, 1000);
1 Like

That’s awesome Leo! This will also allow me to clean up the code for loading my custom plugin for Ableset

1 Like

Leo continues to amaze again and again!

This works incredibly. It is centered. For longer lines, it looks like there is zero padding between the text and the end of the screen. It might be nice to add a very small pad on the left and right sides of the screen.

Another thing… Is it possible for Ableset to remember which lyric page you selected when you tab back and forth? Right now, it just selected which ever one is first every time. This isn’t a huge deal, but it is annoying sometimes to have to reselect it if I tab out for a second to check something, etc.

I also like how Ableset scrolls along with it right now, but in ProPresenter it does not scroll and I want to make the transition as seamless as possible for the team. (hopefully they don’t even know until I tell them haha)

Is there a tag modifier to stop the auto-scroll? but instead jump between lyrics almost like frames?

It also could be a good option to allow you to target named lyrics tracks with tags within the CSS script or JS script so that the lyric page in Ableton session doesn’t need it, making it ever so much cleaner. So any track with “+LYRICS” called “Confidence-Monitor” automatically receives the [nozoom] [top] [nofade]

1 Like

Hey @CodyWright,

I just released AbleSet 2.5.0-beta.2 which remembers the most recent lyrics track when you switch between views.

To add some padding to your lines, you should be able to use the following CSS:

.lyrics .lyrics-line {
  padding: 0 10px; /* change to taste */
}

Some of the lyrics attributes are just styles, so you can add them to your custom CSS to target specific lines, e.g.

.lyrics.confidence-monitor .lyrics-line, 
.lyrics.confidence-monitor .image-line {
  /* disable the zoom */
  transform: scale(1);
}

.lyrics.confidence-monitor {
  /* disable the fade */
  --fade-top: 1 !important;
  --fade-bottom: 1 !important;
}

The [top] attribute changes the scroll behavior which unfortunately can’t be overridden with CSS.

This is currently not possible, but it shouldn’t be too hard to implement. I’ll add it to my todo list.

1 Like

I am having an issue with the text scaling. It often times (especially when making changes to the lyrics or to any of the “Lyrics” tracks) changes back to the original size (much smaller than the scaled text), and needs to be refreshed each time this happens otherwise the text is too small to see on our Stage Display. This has always been the case since using Textfit.js and while it is annoying to not be able to make changes on the fly without the text refreshing and randomly changing to sizes much larger than the original or much smaller, it is manageable.

But what it has started doing (honestly, I don’t remember what version it started on), is reverting back to the original text size between each song and if myself or our sound engineer do not catch it, there is a good bit of time that goes by that the lyrics are very difficult to read.

I know this is a weird and unique setup but was just curious if there was a possible fix for this?

Hey @iamderkis,

I just made a small modification to the code that checks the lines every second and if it notices that a line hasn’t been adjusted to the screen yet, it runs the script again. Here’s the new code:

import { textFit } from "./textFit.js";

let observer;
let interval;

const init = () => {
  const target = document.querySelector(".lyrics-page .lyrics div");

  if (!target) {
    console.log("This doesn't seem to be a lyrics page");
    return;
  }

  const callback = async (_a, _b, options) => {
    const lyrics = document.querySelectorAll(".lyrics-line span");

    lyrics.forEach((el) => {
      el.style.display = "block";
      el.style.width = el.parentElement.clientWidth + "px";
      el.style.height = el.parentElement.clientHeight + "px";
    });

    // maxFontSize can be adjusted if the font becomes too large
    textFit(lyrics, { multiLine: true, maxFontSize: 300, ...options });
  };

  observer = new MutationObserver(callback);
  observer.observe(target, { childList: true });
  callback();

  // Since AbleSet 2.4.0, you have access to real-time updates
  ableset.getSocket("lyrics").on("tracksUpdated", () => {
    setTimeout(callback, 1000);
  });

  if (interval) {
    clearInterval(interval);
  }

  // If a line is replaced, re-fit its text.
  // This is a bit of a brute-force approach that just runs the fitting
  // process every second and ignores lines that have already been adjusted.
  setInterval(() => callback(null, null, { reProcess: false }), 1000);
};

// Keep track of the current URL so we can run
// the script again when navigating to a lyrics page
let lastLocation = "";

setInterval(() => {
  if (location.href !== lastLocation && location.href.includes("/lyrics")) {
    lastLocation = location.href;
    observer?.disconnect();
    init();
  }
}, 1000);

Let me know if this works for you :slight_smile:

I am attaching a screen recording of what is happening. It does fix the problem of the text getting smaller in between songs and increases the size of that text but a weird issue is occurring as it in “debouncing”.

Oh, that’s not looking good! Does this version work better?

import { textFit } from "./textFit.js";

let observer;
let interval;

const init = () => {
  const target = document.querySelector(".lyrics-page .lyrics div");

  if (!target) {
    console.log("This doesn't seem to be a lyrics page");
    return;
  }

  const callback = async (_a, _b, options) => {
    const lyrics = document.querySelectorAll(
      ".lyrics-line span:not(.text-fit)"
    );

    lyrics.forEach((el) => {
      el.style.display = "block";
      el.style.width = el.parentElement.clientWidth + "px";
      el.style.height = el.parentElement.clientHeight + "px";
      el.classList.add("text-fit");
    });

    // maxFontSize can be adjusted if the font becomes too large
    textFit(lyrics, { multiLine: true, maxFontSize: 300, ...options });
  };

  observer = new MutationObserver(callback);
  observer.observe(target, { childList: true });
  callback();

  // Since AbleSet 2.4.0, you have access to real-time updates
  ableset.getSocket("lyrics").on("tracksUpdated", () => {
    setTimeout(callback, 1000);
  });

  if (interval) {
    clearInterval(interval);
  }

  // If a line is replaced, re-fit its text.
  // This is a bit of a brute-force approach that just runs the fitting
  // process every second and ignores lines that have already been adjusted.
  interval = setInterval(() => callback(null, null, { reProcess: false }), 1000);
};

// Keep track of the current URL so we can run
// the script again when navigating to a lyrics page
let lastLocation = "";

setInterval(() => {
  if (location.href !== lastLocation && location.href.includes("/lyrics")) {
    lastLocation = location.href;
    observer?.disconnect();
    init();
  }
}, 1000);
1 Like

That works brilliantly! As always, thank you so much for your help!

1 Like

An issue I have run into is that over time (usually about 2 or 3 minutes) the page begins to lag quite a bit when swapping lyrics and then goes unresponsive. I reverted back to the previous script and it worked fine so something is bogging down. As far as sizing between songs, it works brilliantly, the only issue is that it bogs down over time until the lyrics stop progressing. Also if I load the lyrics page and it sits without running the tracks, it will do the same thing so it seems to be happening the moment the page is loaded.

Hey @iamderkis,

I’ve just made some adjustments to the code in my last post. Could you try if this works better?

It’s still bogging down over time. It’s creating these elements over and over again (screenshot attached) and then it starts bogging down.

Hey @iamderkis , I was able to reproduce the error on my computer. It all has to do with the CSS selector.

const lyrics = document.querySelectorAll(
  ".lyrics-line span:not(.text-fit)"
);

The problem is that the document.querySelectorAll keeps looking down into the span child list for an element that doesn’t contain the class name: text-fit. Just replace the above code block with this:

const lyrics = document.querySelectorAll(
  ".lyrics-line p > span:not(.text-fit)"
);
1 Like

That was it! Been running through multiple songs and appears to be working like a charm! Thank you!

1 Like

Thank you for helping with this bug, @RichardB!

2 Likes

I wanted to say thank you both again! It worked flawlessly on Sunday. One quick question that I am curious about. Would it be possible to have the section of the song show directly to the left of the lyrics? I actually did this with Pro Presenter 7 and just made the NDI feed from Ableset a width that allowed me to put a Stage Layout from Pro Presenter 7 that showed the groups from Pro7 beside the lyrics. Since our lyrics are automated with Ableset for our vocalists, we do not always have someone running Pro7 at rehearsals so the groups will not match up.

I’m attaching a sketch of what I am thinking. Essentially the lyrics would be pushed far enough to the right to never overlap with the sections but the sections would always show. Just an easy indicator of where we are in the song.

I am using the Browsers in OBS so I am able to manipulate the layout and I have already added a section to show my “measures-track” and then cropped out the measures before and after the current, so the band can always see the current measure we are at but that does not help the vocalists. I believe I can setup a workaround for this where I duplicate my “SECTIONS” track into a separate “LYRICS” track and then just open another browser in OBS and line it up with the lyrics so that it will show the section of the song we are currently at. Just wondering if there is a cleaner way it could be done without creation another lyric track with duplicated content in it.

There was a thread that mentioned this a long time ago and I tried merging it into my codebase but myself (nor ChatGPT) could make it work with this current layout.

Please excuse my late reply!

Unfortunately, this layout isn’t possible with the current version of AbleSet. I’ll add some more dynamic class names to sections in the next update so you can style the current and next section headers specifically.

In the meantime, if you’d like to create a track with just sections, you could create an empty lyrics track and use the section headers that AbleSet automatically displays. Those could be cropped in OBS and placed on the side of your screen.

1 Like

I figured that’s the case and it’s not a major deal. I have done this in the meantime and it has worked great. Used it this past Sunday. I just created a track called “groups” for the sections and a track called “title” for the song title to show at the top. It worked great. The band has loved it and loves that it shows the current measure at the top right. Thanks for all your hard work!

1 Like