1
0
mirror of synced 2025-01-24 23:13:40 +01:00

tja2fumen.py: Convert tabs to spaces

This commit is contained in:
Viv 2023-06-02 16:33:40 -04:00
parent 7e7591e688
commit 9b1ee88510

View File

@ -5,184 +5,184 @@ fumen2osu_version = "v1.4"
branchNames = ("normal", "advanced", "master") branchNames = ("normal", "advanced", "master")
def readFumen(inputFile, byteOrder=None, debug=False): def readFumen(inputFile, byteOrder=None, debug=False):
if type(inputFile) is str: if type(inputFile) is str:
file = open(inputFile, "rb") file = open(inputFile, "rb")
else: else:
file = inputFile file = inputFile
size = os.fstat(file.fileno()).st_size size = os.fstat(file.fileno()).st_size
noteTypes = { noteTypes = {
0x1: "Don", # ドン 0x1: "Don", # ドン
0x2: "Don", # ド 0x2: "Don", # ド
0x3: "Don", # コ 0x3: "Don", # コ
0x4: "Ka", # カッ 0x4: "Ka", # カッ
0x5: "Ka", # カ 0x5: "Ka", # カ
0x6: "Drumroll", 0x6: "Drumroll",
0x7: "DON", 0x7: "DON",
0x8: "KA", 0x8: "KA",
0x9: "DRUMROLL", 0x9: "DRUMROLL",
0xa: "Balloon", 0xa: "Balloon",
0xb: "DON", # hands 0xb: "DON", # hands
0xc: "Kusudama", 0xc: "Kusudama",
0xd: "KA", # hands 0xd: "KA", # hands
0x62: "Drumroll" # ? 0x62: "Drumroll" # ?
} }
song = {} song = {}
def readStruct(format, seek=None): def readStruct(format, seek=None):
if seek: if seek:
file.seek(seek) file.seek(seek)
return struct.unpack(order + format, file.read(struct.calcsize(order + format))) return struct.unpack(order + format, file.read(struct.calcsize(order + format)))
if byteOrder: if byteOrder:
order = ">" if byteOrder == "big" else "<" order = ">" if byteOrder == "big" else "<"
totalMeasures = readStruct("I", 0x200)[0] totalMeasures = readStruct("I", 0x200)[0]
else: else:
order = "" order = ""
measuresBig = readStruct(">I", 0x200)[0] measuresBig = readStruct(">I", 0x200)[0]
measuresLittle = readStruct("<I", 0x200)[0] measuresLittle = readStruct("<I", 0x200)[0]
if measuresBig < measuresLittle: if measuresBig < measuresLittle:
order = ">" order = ">"
totalMeasures = measuresBig totalMeasures = measuresBig
else: else:
order = "<" order = "<"
totalMeasures = measuresLittle totalMeasures = measuresLittle
hasBranches = getBool(readStruct("B", 0x1b0)[0]) hasBranches = getBool(readStruct("B", 0x1b0)[0])
song["branches"] = hasBranches song["branches"] = hasBranches
if debug: if debug:
debugPrint("Total measures: {0}, {1} branches, {2}-endian".format( debugPrint("Total measures: {0}, {1} branches, {2}-endian".format(
totalMeasures, totalMeasures,
"has" if hasBranches else "no", "has" if hasBranches else "no",
"Big" if order == ">" else "Little" "Big" if order == ">" else "Little"
)) ))
file.seek(0x208) file.seek(0x208)
for measureNumber in range(totalMeasures): for measureNumber in range(totalMeasures):
measure = {} measure = {}
# measureStruct: bpm 4, offset 4, gogo 1, hidden 1, dummy 2, branchInfo 4 * 6, dummy 4 # measureStruct: bpm 4, offset 4, gogo 1, hidden 1, dummy 2, branchInfo 4 * 6, dummy 4
measureStruct = readStruct("ffBBHiiiiiii") measureStruct = readStruct("ffBBHiiiiiii")
measure["bpm"] = measureStruct[0] measure["bpm"] = measureStruct[0]
measure["fumenOffset"] = measureStruct[1] measure["fumenOffset"] = measureStruct[1]
if measureNumber == 0: if measureNumber == 0:
measure["offset"] = measure["fumenOffset"] + 240000 / measure["bpm"] measure["offset"] = measure["fumenOffset"] + 240000 / measure["bpm"]
else: else:
prev = song[measureNumber - 1] prev = song[measureNumber - 1]
measure["offset"] = prev["offset"] + measure["fumenOffset"] + 240000 / measure["bpm"] - prev["fumenOffset"] - 240000 / prev["bpm"] measure["offset"] = prev["offset"] + measure["fumenOffset"] + 240000 / measure["bpm"] - prev["fumenOffset"] - 240000 / prev["bpm"]
measure["gogo"] = getBool(measureStruct[2]) measure["gogo"] = getBool(measureStruct[2])
measure["hidden"] = getBool(measureStruct[3]) measure["hidden"] = getBool(measureStruct[3])
for branchNumber in range(3): for branchNumber in range(3):
branch = {} branch = {}
# branchStruct: totalNotes 2, dummy 2, speed 4 # branchStruct: totalNotes 2, dummy 2, speed 4
branchStruct = readStruct("HHf") branchStruct = readStruct("HHf")
totalNotes = branchStruct[0] totalNotes = branchStruct[0]
branch["speed"] = branchStruct[2] branch["speed"] = branchStruct[2]
if debug and (hasBranches or branchNumber == 0 or totalNotes != 0): if debug and (hasBranches or branchNumber == 0 or totalNotes != 0):
branchName = " ({0})".format( branchName = " ({0})".format(
branchNames[branchNumber] branchNames[branchNumber]
) if hasBranches or branchNumber != 0 else "" ) if hasBranches or branchNumber != 0 else ""
fileOffset = file.tell() fileOffset = file.tell()
debugPrint("") debugPrint("")
debugPrint("Measure #{0}{1} at {2}-{3} ({4})".format( debugPrint("Measure #{0}{1} at {2}-{3} ({4})".format(
measureNumber + 1, measureNumber + 1,
branchName, branchName,
shortHex(fileOffset - 0x8), shortHex(fileOffset - 0x8),
shortHex(fileOffset + 0x18 * totalNotes), shortHex(fileOffset + 0x18 * totalNotes),
nameValue(measure, branch) nameValue(measure, branch)
)) ))
debugPrint("Total notes: {0}".format(totalNotes)) debugPrint("Total notes: {0}".format(totalNotes))
for noteNumber in range(totalNotes): for noteNumber in range(totalNotes):
if debug: if debug:
fileOffset = file.tell() fileOffset = file.tell()
debugPrint("Note #{0} at {1}-{2}".format( debugPrint("Note #{0} at {1}-{2}".format(
noteNumber + 1, noteNumber + 1,
shortHex(fileOffset), shortHex(fileOffset),
shortHex(fileOffset + 0x17) shortHex(fileOffset + 0x17)
), end="") ), end="")
note = {} note = {}
# noteStruct: type 4, pos 4, item 4, dummy 4, init 2, diff 2, duration 4 # noteStruct: type 4, pos 4, item 4, dummy 4, init 2, diff 2, duration 4
noteStruct = readStruct("ififHHf") noteStruct = readStruct("ififHHf")
noteType = noteStruct[0] noteType = noteStruct[0]
if noteType not in noteTypes: if noteType not in noteTypes:
if debug: if debug:
debugPrint("") debugPrint("")
debugPrint("Error: Unknown note type '{0}' at offset {1}".format( debugPrint("Error: Unknown note type '{0}' at offset {1}".format(
shortHex(noteType).upper(), shortHex(noteType).upper(),
hex(file.tell() - 0x18)) hex(file.tell() - 0x18))
) )
return False return False
note["type"] = noteTypes[noteType] note["type"] = noteTypes[noteType]
note["pos"] = noteStruct[1] note["pos"] = noteStruct[1]
if noteType == 0xa or noteType == 0xc: if noteType == 0xa or noteType == 0xc:
# Balloon hits # Balloon hits
note["hits"] = noteStruct[4] note["hits"] = noteStruct[4]
elif "scoreInit" not in song: elif "scoreInit" not in song:
song["scoreInit"] = noteStruct[4] song["scoreInit"] = noteStruct[4]
song["scoreDiff"] = noteStruct[5] / 4.0 song["scoreDiff"] = noteStruct[5] / 4.0
if noteType == 0x6 or noteType == 0x9 or noteType == 0xa or noteType == 0xc: if noteType == 0x6 or noteType == 0x9 or noteType == 0xa or noteType == 0xc:
# Drumroll and balloon duration in ms # Drumroll and balloon duration in ms
note["duration"] = noteStruct[6] note["duration"] = noteStruct[6]
branch[noteNumber] = note branch[noteNumber] = note
if debug: if debug:
debugPrint(" ({0})".format(nameValue(note))) debugPrint(" ({0})".format(nameValue(note)))
if noteType == 0x6 or noteType == 0x9 or noteType == 0x62: if noteType == 0x6 or noteType == 0x9 or noteType == 0x62:
# Drumrolls have 8 dummy bytes at the end # Drumrolls have 8 dummy bytes at the end
file.seek(0x8, os.SEEK_CUR) file.seek(0x8, os.SEEK_CUR)
branch["length"] = totalNotes branch["length"] = totalNotes
measure[branchNames[branchNumber]] = branch measure[branchNames[branchNumber]] = branch
song[measureNumber] = measure song[measureNumber] = measure
if file.tell() >= size: if file.tell() >= size:
break break
song["length"] = totalMeasures song["length"] = totalMeasures
file.close() file.close()
return song return song
def writeOsu(song, globalOffset=0, title=None, subtitle="", wave=None, selectedBranch=None, outputFile=None, inputFile=None): def writeOsu(song, globalOffset=0, title=None, subtitle="", wave=None, selectedBranch=None, outputFile=None, inputFile=None):
if not song or len(song) == 0: if not song or len(song) == 0:
return False return False
if inputFile: if inputFile:
if type(inputFile) is str: if type(inputFile) is str:
filename = inputFile filename = inputFile
else: else:
filename = inputFile.name filename = inputFile.name
filenameNoExt = os.path.splitext(filename)[0] filenameNoExt = os.path.splitext(filename)[0]
title = title or filenameNoExt title = title or filenameNoExt
wave = wave or "SONG_{0}.wav".format( wave = wave or "SONG_{0}.wav".format(
filenameNoExt.split("_")[0].upper() filenameNoExt.split("_")[0].upper()
) )
outputFile = outputFile or "{0}.osu".format(filenameNoExt) outputFile = outputFile or "{0}.osu".format(filenameNoExt)
else: else:
title = title or "Song Title" title = title or "Song Title"
wave = wave or "song.wav" wave = wave or "song.wav"
if song["branches"] == True: if song["branches"] == True:
if selectedBranch not in branchNames: if selectedBranch not in branchNames:
selectedBranch = branchNames[-1] selectedBranch = branchNames[-1]
debugPrint("Warning: Using the {0} branch in a branched song.".format(selectedBranch)) debugPrint("Warning: Using the {0} branch in a branched song.".format(selectedBranch))
else: else:
selectedBranch = branchNames[0] selectedBranch = branchNames[0]
osu = [] osu = []
osu.append(b"""osu file format v14 osu.append(b"""osu file format v14
[General]""") [General]""")
osu.append(b"AudioFilename: " + bytes(wave, "utf8")) osu.append(b"AudioFilename: " + bytes(wave, "utf8"))
osu.append(b"""AudioLeadIn: 0 osu.append(b"""AudioLeadIn: 0
PreviewTime: 0 PreviewTime: 0
CountDown: 0 CountDown: 0
SampleSet: Normal SampleSet: Normal
@ -198,11 +198,11 @@ GridSize: 4
TimelineZoom: 1 TimelineZoom: 1
[Metadata]""") [Metadata]""")
osu.append(b"Title:" + bytes(title, "utf8")) osu.append(b"Title:" + bytes(title, "utf8"))
osu.append(b"TitleUnicode:" + bytes(title, "utf8")) osu.append(b"TitleUnicode:" + bytes(title, "utf8"))
osu.append(b"Artist:" + bytes(subtitle, "utf8")) osu.append(b"Artist:" + bytes(subtitle, "utf8"))
osu.append(b"ArtistUnicode:" + bytes(subtitle, "utf8")) osu.append(b"ArtistUnicode:" + bytes(subtitle, "utf8"))
osu.append(b"""Creator: osu.append(b"""Creator:
Version: Version:
Source: Source:
Tags: Tags:
@ -216,180 +216,180 @@ SliderMultiplier:1.4
SliderTickRate:4 SliderTickRate:4
[TimingPoints]""") [TimingPoints]""")
globalOffset = globalOffset * 1000.0 globalOffset = globalOffset * 1000.0
for i in range(song["length"]): for i in range(song["length"]):
prevMeasure = song[i - 1] if i != 0 else None prevMeasure = song[i - 1] if i != 0 else None
prevBranch = prevMeasure[selectedBranch] if i != 0 else None prevBranch = prevMeasure[selectedBranch] if i != 0 else None
measure = song[i] measure = song[i]
branch = measure[selectedBranch] branch = measure[selectedBranch]
if i == 0 or prevMeasure["bpm"] != measure["bpm"] or prevMeasure["gogo"] != measure["gogo"] or prevBranch["speed"] != branch["speed"]: if i == 0 or prevMeasure["bpm"] != measure["bpm"] or prevMeasure["gogo"] != measure["gogo"] or prevBranch["speed"] != branch["speed"]:
offset = measure["offset"] - globalOffset offset = measure["offset"] - globalOffset
gogo = 1 if measure["gogo"] else 0 gogo = 1 if measure["gogo"] else 0
if i == 0 or prevMeasure["bpm"] != measure["bpm"]: if i == 0 or prevMeasure["bpm"] != measure["bpm"]:
msPerBeat = 1000 / measure["bpm"] * 60 msPerBeat = 1000 / measure["bpm"] * 60
osu.append(bytes("{0},{1},4,1,0,100,1,{2}".format( osu.append(bytes("{0},{1},4,1,0,100,1,{2}".format(
int(offset), int(offset),
msPerBeat, msPerBeat,
gogo gogo
), "ascii")) ), "ascii"))
if branch["speed"] != 1 or i != 0 and (prevBranch["speed"] != branch["speed"] or prevMeasure["bpm"] == measure["bpm"]): if branch["speed"] != 1 or i != 0 and (prevBranch["speed"] != branch["speed"] or prevMeasure["bpm"] == measure["bpm"]):
msPerBeat = -100 / branch["speed"] msPerBeat = -100 / branch["speed"]
osu.append(bytes("{0},{1},4,1,0,100,1,{2}".format( osu.append(bytes("{0},{1},4,1,0,100,1,{2}".format(
int(offset), int(offset),
msPerBeat, msPerBeat,
gogo gogo
), "ascii")) ), "ascii"))
osu.append(b""" osu.append(b"""
[HitObjects]""") [HitObjects]""")
osuSounds = { osuSounds = {
"Don": 0, "Don": 0,
"Ka": 8, "Ka": 8,
"DON": 4, "DON": 4,
"KA": 12, "KA": 12,
"Drumroll": 0, "Drumroll": 0,
"DRUMROLL": 4, "DRUMROLL": 4,
"Balloon": 0, "Balloon": 0,
"Kusudama": 0 "Kusudama": 0
} }
for i in range(song["length"]): for i in range(song["length"]):
measure = song[i] measure = song[i]
branch = song[i][selectedBranch] branch = song[i][selectedBranch]
for j in range(branch["length"]): for j in range(branch["length"]):
note = branch[j] note = branch[j]
noteType = note["type"] noteType = note["type"]
offset = measure["offset"] + note["pos"] - globalOffset offset = measure["offset"] + note["pos"] - globalOffset
if noteType == "Don" or noteType == "Ka" or noteType == "DON" or noteType == "KA": if noteType == "Don" or noteType == "Ka" or noteType == "DON" or noteType == "KA":
sound = osuSounds[noteType] sound = osuSounds[noteType]
osu.append(bytes("416,176,{0},1,{1},0:0:0:0:".format( osu.append(bytes("416,176,{0},1,{1},0:0:0:0:".format(
int(offset), int(offset),
sound sound
), "ascii")) ), "ascii"))
elif noteType == "Drumroll" or noteType == "DRUMROLL": elif noteType == "Drumroll" or noteType == "DRUMROLL":
sound = osuSounds[noteType] sound = osuSounds[noteType]
velocity = 1.4 * branch["speed"] * 100 / (1000 / measure["bpm"] * 60) velocity = 1.4 * branch["speed"] * 100 / (1000 / measure["bpm"] * 60)
pixelLength = note["duration"] * velocity pixelLength = note["duration"] * velocity
osu.append(bytes("416,176,{0},2,{1},L|696:176,1,{2},0|0,0:0|0:0,0:0:0:0:".format( osu.append(bytes("416,176,{0},2,{1},L|696:176,1,{2},0|0,0:0|0:0,0:0:0:0:".format(
int(offset), int(offset),
sound, sound,
int(pixelLength) int(pixelLength)
), "ascii")) ), "ascii"))
elif noteType == "Balloon" or noteType == "Kusudama": elif noteType == "Balloon" or noteType == "Kusudama":
sound = osuSounds[noteType] sound = osuSounds[noteType]
endTime = offset + note["duration"] endTime = offset + note["duration"]
osu.append(bytes("416,176,{0},12,0,{1},0:0:0:0:".format( osu.append(bytes("416,176,{0},12,0,{1},0:0:0:0:".format(
int(offset), int(offset),
int(endTime) int(endTime)
), "ascii")) ), "ascii"))
osu.append(b"") osu.append(b"")
osuContents = b"\n".join(osu) osuContents = b"\n".join(osu)
if outputFile: if outputFile:
if type(outputFile) is str: if type(outputFile) is str:
file = open(outputFile, "bw+") file = open(outputFile, "bw+")
else: else:
file = outputFile file = outputFile
if type(outputFile) is io.TextIOWrapper: if type(outputFile) is io.TextIOWrapper:
osuContents = osuContents.decode("utf-8") osuContents = osuContents.decode("utf-8")
try: try:
file.write(osuContents) file.write(osuContents)
except UnicodeEncodeError as e: except UnicodeEncodeError as e:
print(e) print(e)
file.close() file.close()
return True return True
else: else:
return osuContents return osuContents
def shortHex(number): def shortHex(number):
return hex(number)[2:] return hex(number)[2:]
def getBool(number): def getBool(number):
return True if number == 0x1 else False if number == 0x0 else number return True if number == 0x1 else False if number == 0x0 else number
def nameValue(*lists): def nameValue(*lists):
string = [] string = []
for list in lists: for list in lists:
for name in list: for name in list:
if name == "type": if name == "type":
string.append(list[name]) string.append(list[name])
elif name != "length" and type(name) is not int: elif name != "length" and type(name) is not int:
value = list[name] value = list[name]
if type(value) == float and value % 1 == 0.0: if type(value) == float and value % 1 == 0.0:
value = int(value) value = int(value)
string.append("{0}: {1}".format(name, value)) string.append("{0}: {1}".format(name, value))
return ", ".join(string) return ", ".join(string)
def debugPrint(*args, **kwargs): def debugPrint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs) print(*args, file=sys.stderr, **kwargs)
if __name__=="__main__": if __name__=="__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="fumen2osu {0}".format(fumen2osu_version) description="fumen2osu {0}".format(fumen2osu_version)
) )
parser.add_argument( parser.add_argument(
"file_m.bin", "file_m.bin",
help="Path to a Taiko no Tatsujin fumen file.", help="Path to a Taiko no Tatsujin fumen file.",
type=argparse.FileType("rb") type=argparse.FileType("rb")
) )
parser.add_argument( parser.add_argument(
"offset", "offset",
help="Note offset in seconds, negative values will make the notes appear later. Example: -1.9", help="Note offset in seconds, negative values will make the notes appear later. Example: -1.9",
nargs="?", nargs="?",
type=float, type=float,
default=0 default=0
) )
group = parser.add_mutually_exclusive_group() group = parser.add_mutually_exclusive_group()
group.add_argument( group.add_argument(
"--big", "--big",
help="Force big endian byte order for parsing.", help="Force big endian byte order for parsing.",
action="store_const", action="store_const",
dest="order", dest="order",
const="big" const="big"
) )
group.add_argument( group.add_argument(
"--little", "--little",
help="Force little endian byte order for parsing.", help="Force little endian byte order for parsing.",
action="store_const", action="store_const",
dest="order", dest="order",
const="little" const="little"
) )
parser.add_argument( parser.add_argument(
"-o", "-o",
metavar="file.osu", metavar="file.osu",
help="Set the filename of the output file.", help="Set the filename of the output file.",
type=argparse.FileType("bw+") type=argparse.FileType("bw+")
) )
parser.add_argument( parser.add_argument(
"--title", "--title",
metavar="\"Title\"", metavar="\"Title\"",
help="Set the title in the output file." help="Set the title in the output file."
) )
parser.add_argument( parser.add_argument(
"--subtitle", "--subtitle",
metavar="\"Subtitle\"", metavar="\"Subtitle\"",
help="Set the subtitle (artist field) in the output file.", help="Set the subtitle (artist field) in the output file.",
default="" default=""
) )
parser.add_argument( parser.add_argument(
"--wave", "--wave",
metavar="file.wav", metavar="file.wav",
help="Set the audio filename in the output file." help="Set the audio filename in the output file."
) )
parser.add_argument( parser.add_argument(
"--branch", "--branch",
metavar="master", metavar="master",
help="Select a branch from a branched song ({0}).".format(", ".join(branchNames)), help="Select a branch from a branched song ({0}).".format(", ".join(branchNames)),
choices=branchNames choices=branchNames
) )
parser.add_argument( parser.add_argument(
"-v", "--debug", "-v", "--debug",
help="Print verbose debug information.", help="Print verbose debug information.",
action="store_true" action="store_true"
) )
if len(sys.argv) == 1: if len(sys.argv) == 1:
parser.print_help() parser.print_help()
else: else:
args = parser.parse_args() args = parser.parse_args()
inputFile = getattr(args, "file_m.bin") inputFile = getattr(args, "file_m.bin")
song = readFumen(inputFile, args.order, args.debug) song = readFumen(inputFile, args.order, args.debug)
writeOsu(song, args.offset, args.title, args.subtitle, args.wave, args.branch, args.o, inputFile) writeOsu(song, args.offset, args.title, args.subtitle, args.wave, args.branch, args.o, inputFile)