mirror of
https://github.com/cainan-c/TaikoPythonTools.git
synced 2024-11-27 17:00:50 +01:00
Add new tool, update README for others.
This commit is contained in:
parent
6878ea4b82
commit
7cb4e6328d
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,3 +1,8 @@
|
|||||||
|
*.bin
|
||||||
|
*.def
|
||||||
|
*.DS_Store
|
||||||
|
*.tja
|
||||||
*.ini
|
*.ini
|
||||||
.DS_Store
|
*.json
|
||||||
|
*.mp3
|
||||||
|
*.ogg
|
@ -26,3 +26,18 @@ Currently, due to the nature of this relying on some Windows executables, this t
|
|||||||
I will be looking into getting it running on Unix-based operating systems. (Linux/macOS)
|
I will be looking into getting it running on Unix-based operating systems. (Linux/macOS)
|
||||||
|
|
||||||
![song conversion tool](https://i.imgur.com/TnRlAxR.png)
|
![song conversion tool](https://i.imgur.com/TnRlAxR.png)
|
||||||
|
|
||||||
|
## Tools Used
|
||||||
|
at9tool - Used to convert audio to the Sony AT9 format.
|
||||||
|
[VGAudioCli](https://github.com/Thealexbarney/VGAudio) - Used to convert audio to Nintendo IDSP and Nintendo OPUS.
|
||||||
|
[G.722.1 Reference Tool](https://www.itu.int/rec/T-REC-G.722.1-200505-I/en) - Used to convert audio to Polycom Siren 14
|
||||||
|
|
||||||
|
### Special Thanks
|
||||||
|
Steam User [descatal](https://steamcommunity.com/id/descatal) for writing [this](https://exvsfbce.home.blog/2020/02/04/guide-to-encoding-bnsf-is14-audio-files-converting-wav-back-to-bnsf-is14/) guide on how to create/encode `bnsf` files.
|
||||||
|
[korenkonder](https://github.com/korenkonder) for compiling the G.722.1 tool used in this project.
|
||||||
|
[Kamui/despairoharmony](https://github.com/despairoharmony) for some of the Nijiiro `.nus3bank` template research.
|
||||||
|
|
||||||
|
## Related Tools
|
||||||
|
[tja2fumen](https://github.com/vivaria/tja2fumen)
|
||||||
|
[TjaOffsetNeutralise](https://github.com/cainan-c/TaikoPythonTools/tree/main/TjaBatchConvert)
|
||||||
|
[TjaBatchConvert](https://github.com/cainan-c/TaikoPythonTools/tree/main/TjaBatchConvert)
|
17
TjaBatchConvert/README.md
Normal file
17
TjaBatchConvert/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# TJA Batch Conversion
|
||||||
|
|
||||||
|
Python script that converts TJA files and audio for use with [TaikoSongConversionTool](https://github.com/cainan-c/TaikoPythonTools/tree/main/TaikoSongConversionTool)
|
||||||
|
The tool isn't very fast, due to the JSON files becoming malformed when multi-threading is involved.
|
||||||
|
|
||||||
|
This tool takes advantage of vivaria's [tja2fumen](https://github.com/vivaria/tja2fumen) for conversion.
|
||||||
|
|
||||||
|
Usage: batch_convert.py [-h] input_folder output_folder
|
||||||
|
|
||||||
|
Process TJA files and generate related files.
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
input_folder The input folder containing TJA files.
|
||||||
|
output_folder The output folder where processed files will be saved.
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
304
TjaBatchConvert/batch_convert.py
Normal file
304
TjaBatchConvert/batch_convert.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pydub import AudioSegment
|
||||||
|
import subprocess
|
||||||
|
import concurrent.futures
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
json_lock = Lock()
|
||||||
|
|
||||||
|
# Define genre mapping
|
||||||
|
GENRE_MAPPING = {
|
||||||
|
"J-POP": 0,
|
||||||
|
"アニメ": 1,
|
||||||
|
"VOCALOID": 2,
|
||||||
|
"バラエティー": 3,
|
||||||
|
"どうよう": 3,
|
||||||
|
"クラシック": 5,
|
||||||
|
"ゲームミュージック": 6,
|
||||||
|
"ナムコオリジナル": 7
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_genre_no(box_def_path):
|
||||||
|
if not os.path.exists(box_def_path):
|
||||||
|
return 3 # Default genre if box.def does not exist
|
||||||
|
|
||||||
|
with open(box_def_path, "r", encoding="utf-8-sig") as file:
|
||||||
|
for line in file:
|
||||||
|
if line.startswith("#GENRE:"):
|
||||||
|
genre = line.split(":")[1].strip()
|
||||||
|
return GENRE_MAPPING.get(genre, 3)
|
||||||
|
return 3
|
||||||
|
|
||||||
|
def parse_tja(tja_path):
|
||||||
|
info = {}
|
||||||
|
note_counts = {
|
||||||
|
"Easy": 0,
|
||||||
|
"Normal": 0,
|
||||||
|
"Hard": 0,
|
||||||
|
"Oni": 0,
|
||||||
|
"Edit": 0
|
||||||
|
}
|
||||||
|
current_difficulty = None
|
||||||
|
inside_notes_section = False
|
||||||
|
|
||||||
|
difficulty_tags = {
|
||||||
|
"Easy": "starEasy",
|
||||||
|
"Normal": "starNormal",
|
||||||
|
"Hard": "starHard",
|
||||||
|
"Oni": "starMania",
|
||||||
|
"Edit": "starUra"
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(tja_path, "r", encoding="utf-8-sig") as file:
|
||||||
|
for line in file:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("TITLE:"):
|
||||||
|
info['TITLE'] = line.split(":")[1].strip()
|
||||||
|
elif line.startswith("TITLEJA:"):
|
||||||
|
info['TITLEJA'] = line.split(":")[1].strip()
|
||||||
|
elif line.startswith("SUBTITLE:"):
|
||||||
|
info['SUBTITLE'] = line.split(":")[1].strip()
|
||||||
|
elif line.startswith("SUBTITLEJA:"):
|
||||||
|
info['SUBTITLEJA'] = line.split(":")[1].strip()
|
||||||
|
elif line.startswith("BPM:"):
|
||||||
|
info['BPM'] = float(line.split(":")[1].strip())
|
||||||
|
elif line.startswith("WAVE:"):
|
||||||
|
info['WAVE'] = line.split(":")[1].strip()
|
||||||
|
elif line.startswith("OFFSET:"):
|
||||||
|
info['OFFSET'] = float(line.split(":")[1].strip())
|
||||||
|
elif line.startswith("DEMOSTART:"):
|
||||||
|
info['DEMOSTART'] = int(float(line.split(":")[1].strip()) * 1000) # convert to ms
|
||||||
|
elif line.startswith("COURSE:"):
|
||||||
|
course = line.split(":")[1].strip()
|
||||||
|
if course in difficulty_tags:
|
||||||
|
current_difficulty = course
|
||||||
|
inside_notes_section = False
|
||||||
|
elif line.startswith("LEVEL:") and current_difficulty:
|
||||||
|
info[difficulty_tags[current_difficulty]] = int(line.split(":")[1].strip())
|
||||||
|
elif line.startswith("#START") and current_difficulty:
|
||||||
|
inside_notes_section = True
|
||||||
|
elif line.startswith("#END") and current_difficulty:
|
||||||
|
inside_notes_section = False
|
||||||
|
current_difficulty = None
|
||||||
|
elif inside_notes_section and current_difficulty:
|
||||||
|
note_counts[current_difficulty] += sum(line.count(ch) for ch in '1234')
|
||||||
|
|
||||||
|
# Calculate shinuti and scores based on note counts
|
||||||
|
for difficulty, count in note_counts.items():
|
||||||
|
if difficulty in difficulty_tags and count > 0:
|
||||||
|
tag = difficulty_tags[difficulty]
|
||||||
|
shinuti = -(-1000000 // count) # equivalent to math.ceil(1000000 / count)
|
||||||
|
info[f'shinuti{difficulty}'] = shinuti
|
||||||
|
info[f'shinuti{difficulty}Duet'] = shinuti
|
||||||
|
info[f'score{difficulty}'] = shinuti * count
|
||||||
|
else:
|
||||||
|
tag = difficulty_tags[difficulty]
|
||||||
|
info[f'shinuti{difficulty}'] = 0
|
||||||
|
info[f'shinuti{difficulty}Duet'] = 0
|
||||||
|
info[f'score{difficulty}'] = 0
|
||||||
|
|
||||||
|
info['NOTE_COUNT'] = sum(note_counts.values())
|
||||||
|
return info
|
||||||
|
|
||||||
|
def convert_audio(wave_path, output_path):
|
||||||
|
audio = AudioSegment.from_file(wave_path)
|
||||||
|
audio.export(output_path, format="mp3")
|
||||||
|
|
||||||
|
def append_to_json_file(filepath, data):
|
||||||
|
with json_lock:
|
||||||
|
try:
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
existing_data = json.load(f)
|
||||||
|
existing_data["items"].append(data)
|
||||||
|
else:
|
||||||
|
existing_data = {"items": [data]}
|
||||||
|
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(existing_data, f, ensure_ascii=False, indent=4)
|
||||||
|
#logging.info(f"Appended data to {filepath} successfully.")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logging.error(f"JSON decoding error while processing {filepath}: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error while appending data to {filepath}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def append_to_json_list_file(filepath, data):
|
||||||
|
with json_lock:
|
||||||
|
try:
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
existing_data = json.load(f)
|
||||||
|
existing_data.append(data)
|
||||||
|
else:
|
||||||
|
existing_data = [data]
|
||||||
|
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(existing_data, f, ensure_ascii=False, indent=4)
|
||||||
|
#logging.info(f"Appended data to {filepath} successfully.")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logging.error(f"JSON decoding error while processing {filepath}: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error while appending data to {filepath}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_json_files(info, genre_no, unique_id, output_dir):
|
||||||
|
musicinfo = {
|
||||||
|
"uniqueId": unique_id,
|
||||||
|
"id": f"cs{unique_id:04d}",
|
||||||
|
"songFileName": f"sound/song_cs{unique_id:04d}",
|
||||||
|
"order": unique_id,
|
||||||
|
"genreNo": genre_no,
|
||||||
|
"branchEasy": False,
|
||||||
|
"branchNormal": False,
|
||||||
|
"branchHard": False,
|
||||||
|
"branchMania": False,
|
||||||
|
"branchUra": False,
|
||||||
|
"starEasy": info.get("starEasy", 0),
|
||||||
|
"starNormal": info.get("starNormal", 0),
|
||||||
|
"starHard": info.get("starHard", 0),
|
||||||
|
"starMania": info.get("starMania", 0),
|
||||||
|
"starUra": info.get("starUra", 0),
|
||||||
|
"shinutiEasy": info.get("shinutiEasy", 0),
|
||||||
|
"shinutiNormal": info.get("shinutiNormal", 0),
|
||||||
|
"shinutiHard": info.get("shinutiHard", 0),
|
||||||
|
"shinutiMania": info.get("shinutiMania", 0),
|
||||||
|
"shinutiUra": info.get("shinutiUra", 0),
|
||||||
|
"shinutiEasyDuet": info.get("shinutiEasyDuet", 0),
|
||||||
|
"shinutiNormalDuet": info.get("shinutiNormalDuet", 0),
|
||||||
|
"shinutiHardDuet": info.get("shinutiHardDuet", 0),
|
||||||
|
"shinutiManiaDuet": info.get("shinutiManiaDuet", 0),
|
||||||
|
"shinutiUraDuet": info.get("shinutiUraDuet", 0),
|
||||||
|
"scoreEasy": info.get("scoreEasy", 0),
|
||||||
|
"scoreNormal": info.get("scoreNormal", 0),
|
||||||
|
"scoreHard": info.get("scoreHard", 0),
|
||||||
|
"scoreMania": info.get("scoreMania", 0),
|
||||||
|
"scoreUra": info.get("scoreUra", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
previewpos = {
|
||||||
|
"id": f"cs{unique_id:04d}",
|
||||||
|
"previewPos": info['DEMOSTART']
|
||||||
|
}
|
||||||
|
|
||||||
|
wordlist = {
|
||||||
|
"key": f"song_cs{unique_id:04d}",
|
||||||
|
"japaneseText": info['TITLE'],
|
||||||
|
"japaneseFontType": 0,
|
||||||
|
"englishUsText": info['TITLE'],
|
||||||
|
"englishUsFontType": 1,
|
||||||
|
"chineseTText": info['TITLE'],
|
||||||
|
"chineseTFontType": 0,
|
||||||
|
"koreanText": info['TITLE'],
|
||||||
|
"koreanFontType": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
wordlist_sub = {
|
||||||
|
"key": f"song_sub_cs{unique_id:04d}",
|
||||||
|
"japaneseText": info.get('SUBTITLE', ''),
|
||||||
|
"japaneseFontType": 0,
|
||||||
|
"englishUsText": info.get('SUBTITLE', ''),
|
||||||
|
"englishUsFontType": 0,
|
||||||
|
"chineseTText": info.get('SUBTITLE', ''),
|
||||||
|
"chineseTFontType": 1,
|
||||||
|
"koreanText": info.get('SUBTITLE', ''),
|
||||||
|
"koreanFontType": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
wordlist_detail = {
|
||||||
|
"key": f"song_detail_cs{unique_id:04d}",
|
||||||
|
"japaneseText": "",
|
||||||
|
"japaneseFontType": 0,
|
||||||
|
"englishUsText": "",
|
||||||
|
"englishUsFontType": 1,
|
||||||
|
"chineseTText": "",
|
||||||
|
"chineseTFontType": 0,
|
||||||
|
"koreanText": "",
|
||||||
|
"koreanFontType": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
append_to_json_file(os.path.join(output_dir, "datatable/musicinfo.json"), musicinfo)
|
||||||
|
append_to_json_list_file(os.path.join(output_dir, "datatable/previewpos.json"), previewpos)
|
||||||
|
append_to_json_file(os.path.join(output_dir, "datatable/wordlist.json"), wordlist)
|
||||||
|
append_to_json_file(os.path.join(output_dir, "datatable/wordlist.json"), wordlist_sub)
|
||||||
|
append_to_json_file(os.path.join(output_dir, "datatable/wordlist.json"), wordlist_detail)
|
||||||
|
|
||||||
|
def convert_tja_to_fumen(tja_path):
|
||||||
|
subprocess.run(["bin/tja2fumen", tja_path], check=True)
|
||||||
|
|
||||||
|
def move_and_rename_bin_files(tja_path, output_dir, unique_id):
|
||||||
|
base_dir = os.path.dirname(tja_path)
|
||||||
|
output_path = os.path.join(output_dir, f"fumen/cs{unique_id:04d}")
|
||||||
|
os.makedirs(output_path, exist_ok=True)
|
||||||
|
|
||||||
|
for difficulty in ["_e", "_n", "_h", "_x", "_m"]:
|
||||||
|
bin_file = f"{os.path.splitext(tja_path)[0]}{difficulty}.bin"
|
||||||
|
if os.path.exists(bin_file):
|
||||||
|
shutil.move(bin_file, os.path.join(output_path, f"cs{unique_id:04d}{difficulty}.bin"))
|
||||||
|
|
||||||
|
def process_song_charts(dir_path, file, output_folder, unique_id):
|
||||||
|
tja_path = os.path.join(dir_path, file)
|
||||||
|
convert_tja_to_fumen(tja_path)
|
||||||
|
move_and_rename_bin_files(tja_path, output_folder, unique_id)
|
||||||
|
|
||||||
|
def process_song_metadata(dir_path, file, genre_no, unique_id, output_folder):
|
||||||
|
tja_path = os.path.join(dir_path, file)
|
||||||
|
info = parse_tja(tja_path)
|
||||||
|
create_json_files(info, genre_no, unique_id, output_folder)
|
||||||
|
|
||||||
|
def process_song_audio(dir_path, file, unique_id, output_folder):
|
||||||
|
tja_path = os.path.join(dir_path, file)
|
||||||
|
info = parse_tja(tja_path)
|
||||||
|
convert_audio(os.path.join(dir_path, info['WAVE']), os.path.join(output_folder, f"sound/song_cs{unique_id:04d}.mp3"))
|
||||||
|
|
||||||
|
def process_song(dir_path, file, genre_no, unique_id, output_folder):
|
||||||
|
try:
|
||||||
|
process_song_charts(dir_path, file, output_folder, unique_id)
|
||||||
|
process_song_audio(dir_path, file, unique_id, output_folder)
|
||||||
|
process_song_metadata(dir_path, file, genre_no, unique_id, output_folder)
|
||||||
|
#logging.info(f"Processed {file} successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to process {file} in {dir_path}: {e}")
|
||||||
|
|
||||||
|
def main(input_folder, output_folder):
|
||||||
|
unique_id = 1
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(input_folder):
|
||||||
|
if "box.def" in files:
|
||||||
|
genre_no = get_genre_no(os.path.join(root, "box.def"))
|
||||||
|
for dir in dirs:
|
||||||
|
dir_path = os.path.join(root, dir)
|
||||||
|
tja_files = [file for file in os.listdir(dir_path) if file.endswith(".tja")]
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
futures = [
|
||||||
|
executor.submit(process_song, dir_path, file, genre_no, unique_id + i, output_folder)
|
||||||
|
for i, file in enumerate(tja_files)
|
||||||
|
]
|
||||||
|
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
try:
|
||||||
|
future.result()
|
||||||
|
unique_id += 1
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error in future: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Process TJA files and generate related files.")
|
||||||
|
parser.add_argument("input_folder", help="The input folder containing TJA files.")
|
||||||
|
parser.add_argument("output_folder", help="The output folder where processed files will be saved.")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
os.makedirs(args.output_folder, exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(args.output_folder, "sound"), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(args.output_folder, "datatable"), exist_ok=True)
|
||||||
|
|
||||||
|
main(args.input_folder, args.output_folder)
|
BIN
TjaBatchConvert/bin/tja2fumen.exe
Normal file
BIN
TjaBatchConvert/bin/tja2fumen.exe
Normal file
Binary file not shown.
@ -3,6 +3,8 @@
|
|||||||
Incredibly simple script which can take `.tja` files and remove any audio offsets in them. Modifying both the .ogg audio file and the chart iself.
|
Incredibly simple script which can take `.tja` files and remove any audio offsets in them. Modifying both the .ogg audio file and the chart iself.
|
||||||
This tool will add a blank measure to make sure no audio is cut off, etc.
|
This tool will add a blank measure to make sure no audio is cut off, etc.
|
||||||
|
|
||||||
|
Recomended to run your TJA files through this tool before using [TjaBatchConvert](https://github.com/cainan-c/TaikoPythonTools/tree/main/TjaBatchConvert)
|
||||||
|
|
||||||
Usage: offset_adjust.py [-h] [--file FILE] [--path PATH] [--encoding {shift_jis,utf-8}]
|
Usage: offset_adjust.py [-h] [--file FILE] [--path PATH] [--encoding {shift_jis,utf-8}]
|
||||||
|
|
||||||
Process TJA and OGG files.
|
Process TJA and OGG files.
|
||||||
|
Loading…
Reference in New Issue
Block a user