mirror of
https://github.com/cainan-c/TaikoPythonTools.git
synced 2024-11-14 10:27:35 +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
|
||||
.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)
|
||||
|
||||
![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,14 +3,16 @@
|
||||
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.
|
||||
|
||||
Usage: offset_adjust.py [-h] [--file FILE] [--path PATH] [--encoding {shift_jis,utf-8}]
|
||||
Recomended to run your TJA files through this tool before using [TjaBatchConvert](https://github.com/cainan-c/TaikoPythonTools/tree/main/TjaBatchConvert)
|
||||
|
||||
Process TJA and OGG files.
|
||||
Usage: offset_adjust.py [-h] [--file FILE] [--path PATH] [--encoding {shift_jis,utf-8}]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--file FILE Path to a single TJA file
|
||||
--path PATH Path to a directory containing folders with TJA files
|
||||
--encoding {shift_jis,utf-8}
|
||||
Encoding type (shift_jis or utf-8)
|
||||
Process TJA and OGG files.
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--file FILE Path to a single TJA file
|
||||
--path PATH Path to a directory containing folders with TJA files
|
||||
--encoding {shift_jis,utf-8}
|
||||
Encoding type (shift_jis or utf-8)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user