Add custom songs to Let's Sing! (2022) from UltraStar

First of all massive credit to @Dh4rry, check out their original post on replacing a DLC song with your own here: https://gbatemp.net/threads/add-your-own-songs-to-lets-sing-2019.538379/
In this tutorial we will be making use of a customized version of their script to convert an UltraStar (PC karaoke game) TXT file to Let's Sing's VXLA format. Their research was also used by me as the jumping off point. Importantly however this tutorial will delve into how to ADD, NOT REPLACE songs!

NOTICE:
This tutorial is no longer actively supported, and is provided as-is. I the author will not be providing any help in regards to this tutorial. Please feel free to ask questions in the thread, in the hopes someone can help you, do not expect me to reply.
Secondly it has come to my attention that nxdumptool does not work on the latest firmwares (17.0+), this is not a required part of this tutorial. Any other method of dumping romfs will work just fine. (DBI is a good example)


Demo:

(I imported more than one song though!)

Step 0: Prerequisites
- Homebrew capable Nintendo Switch
- AtmosphereNX installed
- A Let's Sing game (Only tested on 2022, should work on all games)
- A DLC for the Let's Sing game
- Basic knowledge of directories
- Linux/Windows PC (MacOS might work?)
- FFMPEG (Linux: "sudo apt install ffmpeg" or your package managers equivalent", Windows: Download the binaries here or something: https://ffmpeg.org/download.html)
- Python3

Step 1: Dumping the required assets
1. Get "nxdumptool" if you don't have it installed already (https://github.com/DarkMatterCore/nxdumptool/releases/tag/v1.1.14)
2. Launch "nxdumptool"
3. Choose "Dump installed SD card / eMMC content"
4. Choose "Let's Sing 2022" (or your version, it should not actually matter, but I only tested 2022)
5. Choose "RomFS options"
6. Navigate to "Use update/DLC" and use the right/left arrows to select the update (v131072/0.0.2.0 for LS2022 as of writing)
7. Choose "RomFS section data dump" (You might want to take note of this as the "Core titleid")
8. Navigate back to the "RomFS options" after dumping has finished
9. Navigate to "Use update/DLC" and use the right/left arrows to select the first DLC you see (Does not matter which DLC you have)
10. Choose "RomFS section data dump" (You might want to take not of this as the "DLC titleid")
11. Done!

Step 2: Getting the files you need
0. Create a folder somewhere where you can modify files, and create your "mod". We'll be calling this folder the "Working folder" from now on.
1. Access your Switch's SD card contents from your PC either directly or through FTP
2. Find the following file: sd:/atmosphere/contents/[CORE TITLEID]/romfs/Data/StreamingAssets/SongsDLC.tsv and copy it to your working folder
3. Find the following file: sd:/atmosphere/contents/[DLC TITLEID]/romfs/name.txt and copy it to your working folder
4. Find the following file: sd:/atmosphere/contents/[DLC TITLEID]/romfs/SomeSongName_meta.xml and copy it to your working folder (SomeSongName is not the actual name, but the song name is dependand on the specific DLC, just copy a *_meta.xml file)
5. Download the following file: https://raw.githubusercontent.com/MrJPGames/ultrastar2singIt-Converter/master/ultrastar2singit.py and place it in your working folder
(Again huge thanks to @Dh4rry for making the orignal version of this script!)


Step 3: Getting your song files
Sadly I cannot link you to the actually useful resources for UltraStar song files. There exist piracy free sites, but finding the exact right music/video for these can be difficult. Instead try to search around the internet for a certain spanish UltraStar site. This will provide torrents to many files (including many non-spanish ones!)

For the next steps I'll be assuming you have gathered the following files:
- A UltraStar TXT
- A music video (most likely AVI but format is not important, video should not contain audio!)
- The music (most likely mp3, but format is not important)
- A 1:1 (square) image (most likely jpg, but format is not important) of the cover art


Step 4: Getting the song files ready for Let's Sing!
0. Before we get started we have to decide on a unique name for our song. The songs title without spaces or non letters is the normal choice (but not necissary). Eg. "The Man Who Sold The World" becomes "TheManWhoSoldTheWorld". This name has to be the same EVERYWHERE, and spelled exactly the same (CASE SENSITIVE!). In the rest of the tutorial this will be referred to as SONGNAME. So replace any SONGNAME with your song id. (eg. SONGNAME.mp4 would become TheManWhoSoldTheWorld.mp4)
1. The music video should be in mp4 format. To do this you can use ffmpeg:
Code:
ffmpeg -i input_video.avi SONGNAME.mp4
2. The music should be in ogg format. To do this you can use ffmpeg:
Code:
ffmpeg -i input_song.mp3 SONGNAME.ogg
2 1/2. Optionally open the music file in Audacity and cut a part 30 seconds to a minute long. Save this as SONGNAME_preview.ogg. Alternativly you can create a copy of the full song and give it the name SONGNAME_preview.ogg, and this should also work, though has not been stress tested with many songs doing this!
3. The cover art should be in png format. Open up any other formats in an image editor (gimp, photoshop, paint.net, mspaint etc.). Make sure the image width and height are the same (thus a square image) and save this as "SONGNAME.png". (Only tested with "low resolution" images, if crashes occur maybe lower the image resolution.)
4. Copy your _meta.xml file and name it SONGNAME_meta.xml
5. Open the _meta.xml file with a text editor and replace the values so they match your song. "Id" should be SONGNAME (case sensitive!). Difficulty ranges from 0-4 (with 0 being 1 star, and 4 being 5 star).


Step 5: UltraStar TXT to VXLA
1. Place the ultrastar TXT in your working folder
2. Open a terminal (bash/cmd) in your working folder
3. Run the command "python3 ultrastar2singit.py input.txt -S SONGNAME"
4. This should result in SONGNAME.vxla being created in your working folder


Step 6: Adding your song!
1. Create a folder called "Patch' (This is a clear name, but you can choose your own)
2. Create the following folder structure inside of that folder: (Of course replace COREID/DLCID with the ids you noted down during the game assets dump step)
oKL5eCY.png

3. Next place your song files in the correct places as shown below (with the SONGID in this case being "TheManWhoSoldTheWorld")
d2pxbGp.png

(Note the backgrounds are optional, hence being left out of this tutorial, look at the offical files for guidance. They are the background in the menu, pause menu and after completing a song (_Result), Not including them will result in a white background instead.)
4. Open up name.txt ("DLCID/romfs/name.txt) and add a new line to the end of the file and put your SONGID there
5. Open up SongsDLC.tsv with LibreOffice Calc or Excel. Copy an existing row. Insert a row and paste the existing row there (so you now have that song in the table twice). Change the UID (a small number, second column) to be unique.
Change the ID to be your SONGNAME. The artist, title and year columns can also be updated (I did not test not updating them), all other collums can be left alone, but you might change them if you so choose (difficulty and some other data is taken from _meta.xml file instead of this table). This is the entry for TheManWhoSoldTheWorld:
jywweka.png


Step 7: The moment of truth!
0. (Optional, but saves space) remove COREID, and DLCID forlders from sd:/atmosphere/contents
1. Copy your COREID and DLCID folders to sd:/atmosphere/contents
2. Boot Let's Sing (2022)! And enjoy your newly added custom song!

Step 8 (not really): Feedback!
Hey, I was wondering if there was interest in the community for a (GUI) tool to easily import songs into Let's Sing. As shown in this tutorial most of the steps can be easily automated. And especially for importing larger libraries doing it by hand can get annoying. But I feel I need to at least know some people will actually want to use such software before I invest the time to make it (or maybe I do it anyways if i get bored who knows).

Other than that please let me know how it went if you followed this tutorial, and please let me know if I made a mistake somewhere so I can fix it!
 
Last edited by MRJPGames,

Jujube

Active Member
Newcomer
Joined
Oct 29, 2021
Messages
29
Trophies
0
Age
24
XP
173
Country
United States
To add to the reply, as the question implies you're not deeply familiar with Python:
1. You might need to install pip (unlikely on Windows I believe, but on Linux I had to install pip)
2. You're pip command might be pip3 instead of pip, thus making it pip3 install pillow

You might also actually want to learn how you can solve similar issues in the future:
"ModuleNotFoundError" is an error Python gives when code you're trying to run relies on code you haven't yet downloaded/installed. Usually googling something along the lines of "python MODULE_NAME" will provide you with the actual "package" name. As "pip3 install PIL" wouldn't work here, googling "Python PIL" would provide you with the package name "pillow" you need for install.
Thanks! Trying rn!
Post automatically merged:

yes you miss some of the prerequisites, for module "PIL" you could run pip install pillow
Thank you!
 

Jujube

Active Member
Newcomer
Joined
Oct 29, 2021
Messages
29
Trophies
0
Age
24
XP
173
Country
United States
hey, I fixed my problem at step 5 thanks to you guys, but i'm now runnin into another one ;( ... Whenever I input the command(with pillow installed) nothing happens. No vxla file is created, no error is shown. Here's the command i'm inputting: C:\Users\jujube\Documents\Working>python ultrastar2singit.py input.txt -s Heathens
 

kosei

New Member
Newbie
Joined
Dec 30, 2022
Messages
3
Trophies
0
Age
41
XP
20
Country
United States
Hi there, I hope there is still some people reading this post.
I tried on let's Sing 2023 and let's Sing 2022 but I cannot make it work. Generating all the files with the correct names is not a problem. I even wrote a python script to make everything easier (btw did you know ffmpeg can also be used to convert jpg into png to the format you want for cover and backgrounds and also generate a preview 30sec ogg sounds?)
But once I put all the file at the right place it doesn't work. Either nothing is showing up or it freeze the game when I scroll up the list. I think something is wrong when loading the _meta.xml file
I can edit some existing _meta.xml and it will show the change in game but that's it. I might probably be able to replace a song like the old thread but I cannot seems to be able to add a new song. I wonder if there is more than 2 users here who has been successful? If yes, which Id did you use? any thing different from the tuto?
Post automatically merged:

hey, I fixed my problem at step 5 thanks to you guys, but i'm now runnin into another one ;( ... Whenever I input the command(with pillow installed) nothing happens. No vxla file is created, no error is shown. Here's the command i'm inputting: C:\Users\jujube\Documents\Working>python ultrastar2singit.py input.txt -s Heathens
instead of
Code:
input.txt
you should put the name of the ultrastar text file you got, in your case the file need to be in the same folder than the script
 
Last edited by kosei,
  • Like
Reactions: impeeza

kosei

New Member
Newbie
Joined
Dec 30, 2022
Messages
3
Trophies
0
Age
41
XP
20
Country
United States
Ok I succeeded to make it work! The issue I had was how I was generating the xml file. It has a specific encoding and you can't just generate it like a text file. That is why the best option was previously to copy/paste an existing meta.xml file from the game and modifying it directly.

That being said I wrote my own scripts based of this tutorial and put 145 new songs to Let's Sing 2022! It is not slow or whatsoever.
Here how I did :
1. you need in the same folder
- the Patch folder containing the COREID and DLCID and all the other folders explained in this tuto. You might want to add the covers_long folder next to covers.
- the ffmpeg folder
- songs from ultrastar(spain) forum directly. Keep the name folder and everything as they are, my script will generate a nameID automatically without touching them.
- the ultrastar2singit.py (I modified it to be slightly more pythonic)
-convert2mp4.py (yeah I could have come with a better name)
-name.txt from the DLC (read the tuto to know how)
-SongsDLC.tsv from the CORE (read the tuto to know how)
2022-12-31 12_08_45-ModLetsSing2022.png

2022-12-31 12_24_20-Songs.png


2. you need to install python, and some module like Pandas. I removed the need of Pillow

ultrastar2singit . py

Code:
import io
import xml.etree.cElementTree as ET
from xml.dom import minidom
import sys
import re
import os
import unicodedata

def strip_accents(s):
   return ''.join(c for c in unicodedata.normalize('NFD', s)
                  if unicodedata.category(c) != 'Mn')

def parse_file(filename):
    data = {"notes": []}
    with io.open(filename, "r", encoding="ISO-8859-1", errors='ignore') as f:
        for line in f:
            line = line.replace('\n', '')
            if line.startswith("#"):
                p = line.split(":", 1)
                if len(p) == 2:
                    data[p[0][1:]] = p[1]
            else:
                note_arr = line.split(" ", 4)
                data["notes"].append(note_arr)
    return data


def map_data(us_data, pitch_corr):
    sing_it = {"text": [], "notes": [], "pages": [], "notes_golden": []}
    bpm = float(us_data["BPM"].replace(',', '.'))
    gap = float(us_data["GAP"].replace(',', '.')) / 1000
    min_note = 1
    last_page = 0.0
    for note in us_data["notes"]:
        if note[0] == ":" or note[0] == "*":
            start = float(note[1]) * 60 / bpm / 4 + gap
            end = start + float(note[2]) * 60 / bpm / 4
            note4 = strip_accents(note[4])
            note4 = note4.replace('œ','oe')
            sing_it["text"].append({"t1": start, "t2": end, "value": note4})
            nint = int(note[3])
            if nint < min_note:
                nint = min_note

            sing_it["notes"].append(
                {"t1": start, "t2": end, "value": nint + pitch_corr})
            if note[0] == "*":
                sing_it["notes_golden"].append(
                    {"t1": start, "t2": end, "value": nint + pitch_corr})
        elif note[0] == "-":
            start = last_page
            end = float(note[1]) * 60 / bpm / 4 + gap
            last_page = end
            sing_it["pages"].append(
                {"t1": start, "t2": end, "value": ""})
        elif note[0] == "E":
            if end > last_page:
                start = last_page
                sing_it["pages"].append(
                    {"t1": start, "t2": end, "value": ""})
    return sing_it


def write_intervals(interval_arr, parent):
    for interval in interval_arr:
        ET.SubElement(parent, "Interval",
                      t1="{0:.3f}".format(interval["t1"]), t2="{0:.3f}".format(interval["t2"]),
                      value=str(interval["value"]))


def write_metadata_file(us_data, songname):
    root = ET.Element("DLCSong")
    root.set("xmlns:xsi", "insert link from the xml")
    root.set("xmlns:xsd", "insert link from the xml")
    ET.SubElement(root, "Genre").text = us_data.get("GENRE", "Rock")
    ET.SubElement(root, "Id").text = songname
    ET.SubElement(root, "Uid").text = "160"
    ET.SubElement(root, "Artist").text = us_data.get("ARTIST", "Unknown")
    ET.SubElement(root, "Title").text = us_data.get("TITLE", "Unknown")
    ET.SubElement(root, "Year").text = us_data.get("YEAR", "2000")
    ET.SubElement(root, "Ratio").text = "Ratio_16_9"
    ET.SubElement(root, "Difficulty").text = "Difficulty0"
    ET.SubElement(root, "Feat")
    ET.SubElement(root, "Line1").text = us_data.get("ARTIST", "Unknown")
    ET.SubElement(root, "Line2")
    xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(
        encoding="utf-8", indent="   ").decode('utf-8')
    xmlbin = xmlstr.replace('\n', '\r\n').encode('utf-8-sig')
    with open("titleid/romfs/" + songname + "_meta.xml", "wb") as f:
        f.write(xmlbin)


def write_vxla_file(sing_it, filename, directory):
    root = ET.Element("AnnotationFile", version="2.0")

    doc = ET.SubElement(root, "IntervalLayer", datatype="STRING",
                        name="structure", units="", description="")
    ET.SubElement(doc, "Interval", t1="2.000", t2="3.000", value="couplet1")

    doc = ET.SubElement(root, "IntervalLayer", datatype="STRING",
                        name="shortversion", units="", description="")
    ET.SubElement(doc, "Interval", t1="0.000",
                  t2="60.000", value="shortversion")

    doc = ET.SubElement(root, "IntervalLayer", datatype="STRING",
                        name="lyrics", units="", description="")
    write_intervals(sing_it["text"], doc)

    doc = ET.SubElement(root, "IntervalLayer", datatype="STRING",
                        name="lyrics_cut", units="", description="")
    write_intervals(sing_it["text"], doc)

    doc = ET.SubElement(root, "IntervalLayer", datatype="UINT8",
                        name="notes", units="", description="")
    write_intervals(sing_it["notes"], doc)

    doc = ET.SubElement(root, "IntervalLayer", datatype="UINT8",
                        name="notes_golden", units="", description="")
    write_intervals(sing_it["notes_golden"], doc)

    doc = ET.SubElement(root, "IntervalLayer", datatype="STRING",
                        name="pages", units="", description="")
    write_intervals(sing_it["pages"], doc)
    xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(
        encoding="ISO-8859-1", indent="   ")
    with open(os.path.join(directory, filename), "wb") as f:
        f.write(xmlstr)


def main(input_file, p=48, s='', dir=''):
    us_data = parse_file(input_file)

    if s:
        output_file = s
    else:
        output_file = re.sub('[^A-Za-z0-9]+', '', us_data["TITLE"])

    sing_it = map_data(us_data, p)
    write_vxla_file(sing_it, output_file + '.vxla', directory=dir)


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])


convert2mp4.py
Code:
import subprocess
from pathlib import Path, PurePath
import os
import ultrastar2singit
import xml.etree.cElementTree as ET
from xml.dom import minidom
import pandas as pd
import shutil
import unicodedata

def strip_accents(s):
   return ''.join(c for c in unicodedata.normalize('NFD', s)
                  if unicodedata.category(c) != 'Mn')


XMLtemplate = 'Zombie_meta.xml'
SongDLCfile = 'SongsDLC.tsv'
nameTxtFile = 'name.txt'
#Lets Sing 2022 IDs
COREID = '0100CC30149B8000'
DLCID = '0100CC30149B9001'

p = Path('.')
localDir = os.getcwd()
dirToConvert = [os.fspath(x) for x in p.iterdir() if x.is_dir()]

dirToRemove = [x for x in dirToConvert if x.startswith('__')]
dirToRemove = dirToRemove + ['ffmpeg', 'Patch']

for x in dirToRemove:
    if x in dirToConvert:
        dirToConvert.remove(x)

for dirLongName in dirToConvert:

    #only directory with the format Artist - Title
    if not ' - ' in dirLongName:
        continue

    splitDirName = dirLongName.split(' - ')
    ArtistDirName = strip_accents(splitDirName[0])
    TitleDirName = strip_accents(splitDirName[1])
    AristCaps = [word[0].upper() for word in ArtistDirName.split()]
    AristCap = ''.join(AristCaps)
    TitleLower = ''.join(e.lower() for e in TitleDirName if e.isalnum())
    nameID = AristCap + TitleLower

    print(nameID)

    # nameID = dirToConvert[0]
    listInDir = Path(dirLongName)

    filesAll = [os.fspath(x.name) for x in listInDir.iterdir()]
    filesAvi = [x for x in listInDir.iterdir() if x.suffix == '.avi' or x.suffix == '.divx' or x.suffix == '.mp4' or x.suffix == '.flv']
    filesMp3 = [x for x in listInDir.iterdir() if x.suffix == '.mp3']
    filesJpg = [x for x in listInDir.iterdir() if x.suffix == '.jpg' or x.suffix == '.jpeg']
    filesTxt = [x for x in listInDir.iterdir() if x.suffix == '.txt']

    mp4FileName = nameID + '.mp4'
    pngFileName = nameID + '.png'
    pngInGameFileName = nameID + '_InGameLoading.png'
    pngLongFileName = nameID + '_long.png'
    pngResultFileName = nameID + '_Result.png'
    vxlaFileName = nameID + '.vxla'
    oggFileName = nameID + '.ogg'
    oggPreviewFileName = nameID + '_preview.ogg'
    xmlFileName = nameID + '_meta.xml'

    if filesAvi and mp4FileName not in filesAll:
        file = filesAvi[0]
        #convert avi or mp4 files into mp4 files without song
        ffmpegCmd = [os.fspath(p / 'ffmpeg/bin/ffmpeg.exe'), '-i', os.fspath(file), '-c:v', 'libx264', '-preset',  'veryfast', '-crf', '18', '-c:a', 'copy', '-an', '-vf',
                     'scale=1280:720:force_original_aspect_ratio=increase,crop=1280:720,fps=25',
                     os.fspath(listInDir / mp4FileName)]
        subprocess.run(ffmpegCmd)
        print('created : '+mp4FileName)


    if oggFileName not in filesAll:
        if filesMp3:
            file = filesMp3[0]
        else:
            file = filesAvi[0]
        #convert Mp3 file into Ogg file
        ffmpegCmd = [os.fspath(p / 'ffmpeg/bin/ffmpeg.exe'), '-i' , os.fspath(file), '-vn', '-ar', '48000',
                     os.fspath(listInDir / oggFileName)]
        subprocess.run(ffmpegCmd)
        print('created : ' + oggFileName)

    if oggPreviewFileName not in filesAll:
        if filesMp3:
            file = filesMp3[0]
        else:
            file = filesAvi[0]
        #convert Mp3 file into preview Ogg file of 30sec
        ffmpegCmd = [os.fspath(p / 'ffmpeg/bin/ffmpeg.exe'), '-ss', '5', '-i' , os.fspath(file), '-vn', '-t', '30', '-ar', '48000',
                     os.fspath(listInDir / oggPreviewFileName)]
        subprocess.run(ffmpegCmd)
        print('created : ' + oggPreviewFileName)

    if filesJpg and pngFileName not in filesAll:
        file = filesJpg[0]
        #convert Jpg file into a 256x256 png
        ffmpegCmd = [os.fspath(p / 'ffmpeg/bin/ffmpeg.exe'), '-i' , os.fspath(file), '-vf',
                     'scale=256:256:force_original_aspect_ratio=increase,crop=256:256',
                     os.fspath(listInDir / pngFileName)]
        subprocess.run(ffmpegCmd)
        print('created : ' + pngFileName)

    if filesJpg and pngLongFileName not in filesAll:
        file = filesJpg[0]
        #convert Jpg file into a 191x396 png
        ffmpegCmd = [os.fspath(p / 'ffmpeg/bin/ffmpeg.exe'), '-i' , os.fspath(file), '-vf',
                     'scale=191:396:force_original_aspect_ratio=increase,crop=191:396',
                     os.fspath(listInDir / pngLongFileName)]
        subprocess.run(ffmpegCmd)
        print('created : ' + pngLongFileName)

    if filesJpg and pngInGameFileName not in filesAll:
        file = filesJpg[0]
        #convert Jpg file into a 512x512 png
        ffmpegCmd = [os.fspath(p / 'ffmpeg/bin/ffmpeg.exe'), '-i' , os.fspath(file), '-vf',
                     'scale=512:512:force_original_aspect_ratio=increase,crop=512:512',
                     os.fspath(listInDir / pngInGameFileName)]
        subprocess.run(ffmpegCmd)
        print('created : ' + pngInGameFileName)



    musicGenreList = ['Pop', 'Rap', 'Rock', 'Ballad', 'Electro']

    ## Getting info from the text file
    if not filesTxt:
        print('need an ultrastar filetext!!')

    datas = ultrastar2singit.parse_file(filesTxt[-1])
    title = datas.get('TITLE', 'title')
    artist = datas.get('ARTIST', 'artist')
    year = datas.get('YEAR', '2000')
    genre = 'Pop'
    try:
        if datas['GENRE'] in musicGenreList:
            genre = datas['GENRE']
    except Exception:
        pass

    ## TSV file
    songsXls = pd.read_csv(SongDLCfile, sep='\t', index_col=0)

    highestCurrentUID = max(songsXls['UID'].values)
    if highestCurrentUID < 200:
        UID = 200
    else:
        UID = highestCurrentUID + 1

    newRow = len(songsXls.index)
    songsXls.loc[newRow] = songsXls.loc[newRow-1]
    songsXls.loc[newRow, 'UID'] = UID
    songsXls.loc[newRow, 'ID'] = nameID
    songsXls.loc[newRow, 'ARTIST'] = artist
    songsXls.loc[newRow, 'TITLE'] = title
    songsXls.loc[newRow, 'YEAR'] = year

    songsXls.to_csv(SongDLCfile, sep='\t')

    ## XML file
    root = ET.Element("DLCSong")
    root.set("xmlns:xsi", "insert link from the xml /XMLSchema-instance")
    root.set("xmlns:xsd", "insert link from the xml /XMLSchema")
    ET.SubElement(root, "Genre").text = genre
    ET.SubElement(root, "Id").text = nameID
    ET.SubElement(root, "Uid").text = str(UID)
    ET.SubElement(root, "Artist").text = artist
    ET.SubElement(root, "Title").text = title
    ET.SubElement(root, "Year").text = year
    ET.SubElement(root, "Ratio").text = "Ratio_16_9"
    ET.SubElement(root, "Difficulty").text = "Difficulty0"
    ET.SubElement(root, "Feat")
    ET.SubElement(root, "Line1").text = artist
    ET.SubElement(root, "Line2")
    xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(
            encoding="utf-8", indent="   ").decode('utf-8')
    xmlbin = xmlstr.replace('\n', '\r\n').encode('utf-8-sig')
    with open(os.fspath(listInDir / xmlFileName), "wb") as f:
        f.write(xmlbin)
    print('created : ' + xmlFileName)

    ## adding new entry to name.txt
    with open(nameTxtFile, 'a') as outfile:
        outfile.write(nameID+'\n')

    ## generating vxla file
    ultrastar2singit.main(filesTxt[0], p=48, s=nameID, dir=listInDir) # p=48 by default. Change pitch sometime

    ##placing all files into the correct folders
    shutil.copy2(SongDLCfile, os.path.join(localDir, 'Patch', COREID, 'romfs/Data/StreamingAssets', SongDLCfile))
    shutil.copy2(nameTxtFile, os.path.join(localDir, 'Patch', DLCID, 'romfs', nameTxtFile))
    shutil.copy2(os.fspath(listInDir / xmlFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs'))
    shutil.copy2(os.fspath(listInDir / oggFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/audio'))
    shutil.copy2(os.fspath(listInDir / oggPreviewFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/audio_preview'))
    shutil.copy2(os.fspath(listInDir / pngFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/covers'))
    shutil.copy2(os.fspath(listInDir / pngLongFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/covers_long'))
    shutil.copy2(os.fspath(listInDir / pngInGameFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/backgrounds/Result', pngResultFileName))
    shutil.copy2(os.fspath(listInDir / pngInGameFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/backgrounds/InGameLoading'))
    shutil.copy2(os.fspath(listInDir / mp4FileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/videos'))
    shutil.copy2(os.fspath(listInDir / vxlaFileName), os.path.join(localDir, 'Patch', DLCID, 'romfs/Songs/vxla'))

3. All you have to do in Terminal is to execute convert2mp4.py that's it. It will :
- create the special nameID from the folder name, add it to name.txt and SongsDlc.tsv
- extract infos from the ultrastar.txt file
- create everything needed (videos, ogg, vxla, pngs, xml) under the song folder
- copy them in the Patch folder where they should be
2022-12-31 12_30_44-Adele - Rolling In The Deep.png


4. Same as the tuto, now you can put the COREID and DLCID folder on your sd card

5. some notes :
- it took me about 35/40minutes to convert 145 songs in 1 batch. I did something else in the mean time.
- 145 songs are about 12.5Gb
- encoding video is what took the most time, I convert any video to a 1280x720 in h264 codec ... we don't have to do that, I just thought that was my initial issue. We could probably save some spaces with a different resolution.
- some songs might have some pitch issue. You can see it when the song doesn't have notes in the game. So in the code change p=48 to p=0 . I only had to do it on 1 song, Aladdin french version of the whole new world.
ultrastar2singit.main(filesTxt[0], p=48, s=nameID, dir=listInDir) to ultrastar2singit.main(filesTxt[0], p=0, s=nameID, dir=listInDir)
- as a new user I cannot post links so you might want to change the code where there was some links "insert link from the xml". It might matter or not. Just tell me and I ll put the code on a Git repository.
- if you use a different game or DLC we might have not the same IDs, in this case change them in the script and put yours
#Lets Sing 2022 IDs
COREID = '0100CC30149B8000'
DLCID = '0100CC30149B9001'
 
Last edited by kosei,

PoopLevel9000

New Member
Newbie
Joined
Jan 8, 2023
Messages
1
Trophies
0
Age
25
XP
24
Country
United States
error: unrecognized arguments: -S BlindingLights, this is what shows up everytime I use python ultrastar2singit.py input.txt -S BlindingLights, ive also used lowercase s and it still doesnt work. If anyone has advice, please give
 

Jujube

Active Member
Newcomer
Joined
Oct 29, 2021
Messages
29
Trophies
0
Age
24
XP
173
Country
United States
Hi there, I hope there is still some people reading this post.
I tried on let's Sing 2023 and let's Sing 2022 but I cannot make it work. Generating all the files with the correct names is not a problem. I even wrote a python script to make everything easier (btw did you know ffmpeg can also be used to convert jpg into png to the format you want for cover and backgrounds and also generate a preview 30sec ogg sounds?)
But once I put all the file at the right place it doesn't work. Either nothing is showing up or it freeze the game when I scroll up the list. I think something is wrong when loading the _meta.xml file
I can edit some existing _meta.xml and it will show the change in game but that's it. I might probably be able to replace a song like the old thread but I cannot seems to be able to add a new song. I wonder if there is more than 2 users here who has been successful? If yes, which Id did you use? any thing different from the tuto?
Post automatically merged:


instead of
Code:
input.txt
you should put the name of the ultrastar text file you got, in your case the file need to be in the same folder than the script
I did that, my ultrastar txt file is named input.txt
 
  • Like
Reactions: impeeza

kosei

New Member
Newbie
Joined
Dec 30, 2022
Messages
3
Trophies
0
Age
41
XP
20
Country
United States
How does it run with the additional 145 songs?
There is no slowness whatsoever however after singing maybe 8/12 songs, the game would crash. No biggie I would just launch it again then another 8/12 songs before crash, it seems to occurs every 45minutes?
Also in the 145 song I might have 10 who doesn't work (appear but no song), and another 10 with incorrect pitch (Easy to fix by changing it like I said in my notes)
 

issayloki

Well-Known Member
Member
Joined
Mar 4, 2018
Messages
146
Trophies
0
Age
43
XP
1,386
Country
Thailand
There is no slowness whatsoever however after singing maybe 8/12 songs, the game would crash. No biggie I would just launch it again then another 8/12 songs before crash, it seems to occurs every 45minutes?
Also in the 145 song I might have 10 who doesn't work (appear but no song), and another 10 with incorrect pitch (Easy to fix by changing it like I said in my notes)
Sorry wake the old post up, what is the Execute command for your script py (python something.py then enter?). Thank you
 
Last edited by issayloki,

issayloki

Well-Known Member
Member
Joined
Mar 4, 2018
Messages
146
Trophies
0
Age
43
XP
1,386
Country
Thailand
Sorry wake the old post up, what is the Execute command for your script py (python something.py then enter?). Thank you
Thank you for your script, it work !!! Only it doesn't write song detail in Songdlc.tvs file and name.txt, which you need to do it manually.
 

Valiran

Active Member
Newcomer
Joined
Jan 31, 2018
Messages
40
Trophies
0
Age
41
XP
138
Country
France
The part 1 doesn't work anymore with the NxDumpTool rewrite, the one that works with Hos 17.0 :(
Could you update the How-To please?
 

Omnia

Member
Newcomer
Joined
Nov 15, 2021
Messages
6
Trophies
0
Age
34
XP
41
Country
Brazil
Did anyone try modding Let's Sing 2024 already? Looks like they changed a lot of things in the file formats, VXLA structure changed a lot, seems like they simplified "notes", "notes_golden" and "lyrics_cut" into a single "notes_full" string, there's some other small changes, a new "passages" string and a new "beats" json file that I'm still not sure what's used for. Videos are now bk2 format. The UI and engine seems improved though, so I can imagine that if we crack this it would be able to support a much larger song database (as the new online VIP pass is supposed to provide access to all songs from all years and new ones in the future)
 

wolfyboy0513

New Member
Newbie
Joined
Sep 18, 2023
Messages
3
Trophies
0
Age
19
XP
18
Country
United Kingdom
Hey,
thx for your work. I just tried your tutorial with 2 songs and didn't get it working. the songs didn't show up in the menu.
i had also some problems, like "-s" instead of "-S" and dump with "Save data to CFW directory (LayeredFS) < Yes" for the folders to show up in "/atmosphere/contents".
after this i got 0100CC30149B8000 as [CORE TITLEID] and 0100CC30149B9001 as [DLC TITLEID] and followed your steps.
is there something more i have to do like turning layeredFS on?
Same problem for me did you manage to solve this?
Post automatically merged:

Hey,
thx for your work. I just tried your tutorial with 2 songs and didn't get it working. the songs didn't show up in the menu.
i had also some problems, like "-s" instead of "-S" and dump with "Save data to CFW directory (LayeredFS) < Yes" for the folders to show up in "/atmosphere/contents".
after this i got 0100CC30149B8000 as [CORE TITLEID] and 0100CC30149B9001 as [DLC TITLEID] and followed your steps.
is there something more i have to do like turning layeredFS on

Hey,
thx for your work. I just tried your tutorial with 2 songs and didn't get it working. the songs didn't show up in the menu.
i had also some problems, like "-s" instead of "-S" and dump with "Save data to CFW directory (LayeredFS) < Yes" for the folders to show up in "/atmosphere/contents".
after this i got 0100CC30149B8000 as [CORE TITLEID] and 0100CC30149B9001 as [DLC TITLEID] and followed your steps.
is there something more i have to do like turning layeredFS on?
I have the same problem did you fix it?
When I first started it ages ago I got the songs to show up on the list but because I did the python wrong it wouldnt work. I fixed the python but now I don't know how to get them back on the list I'm not sure what I'm doing wrong.
 
Last edited by wolfyboy0513,

krikor991

Member
Newcomer
Joined
Jan 15, 2024
Messages
6
Trophies
0
Age
32
XP
31
Country
Canada
I wonder if something like this would be possible on samba de amigo. I've been a big fan of that game since I was a little booger nosed kid, but wanted to add custom songs.
 

Site & Scene News

Popular threads in this forum

General chit-chat
Help Users
    AncientBoi @ AncientBoi: Tattle-tale :creep: