Replace all {song, branch, measure, note} dictionaries with classes (#32)
This refactor will hopefully pave the way for further refactors that will make this code easier to understand and work with. Fixes #9.
This commit is contained in:
parent
bd27e9dade
commit
cd755bd646
@ -24,11 +24,11 @@ def main(argv=None):
|
|||||||
baseName = os.path.splitext(fnameTJA)[0]
|
baseName = os.path.splitext(fnameTJA)[0]
|
||||||
|
|
||||||
# Parse lines in TJA file
|
# Parse lines in TJA file
|
||||||
parsedTJACourses = parseTJA(fnameTJA)
|
parsedTJA = parseTJA(fnameTJA)
|
||||||
|
|
||||||
# Convert parsed TJA courses to Fumen data, and write each course to `.bin` files
|
# Convert parsed TJA courses to Fumen data, and write each course to `.bin` files
|
||||||
for parsedCourse in parsedTJACourses.items():
|
for course in parsedTJA.courses.items():
|
||||||
convert_and_write(parsedCourse, baseName, singleCourse=(len(parsedTJACourses) == 1))
|
convert_and_write(course, baseName, singleCourse=(len(parsedTJA.courses) == 1))
|
||||||
|
|
||||||
|
|
||||||
def convert_and_write(parsedCourse, baseName, singleCourse=False):
|
def convert_and_write(parsedCourse, baseName, singleCourse=False):
|
||||||
|
@ -92,6 +92,11 @@ sampleHeaderMetadata[77] = 97
|
|||||||
sampleHeaderMetadata[78] = 188
|
sampleHeaderMetadata[78] = 188
|
||||||
# Certain other bytes (8+9, 20) will need to be filled in on a song-by-song basis
|
# Certain other bytes (8+9, 20) will need to be filled in on a song-by-song basis
|
||||||
|
|
||||||
|
TJA_COURSE_NAMES = []
|
||||||
|
for difficulty in ['Ura', 'Oni', 'Hard', 'Normal', 'Easy']:
|
||||||
|
for player in ['', 'P1', 'P2']:
|
||||||
|
TJA_COURSE_NAMES.append(difficulty+player)
|
||||||
|
|
||||||
NORMALIZE_COURSE = {
|
NORMALIZE_COURSE = {
|
||||||
'0': 'Easy',
|
'0': 'Easy',
|
||||||
'Easy': 'Easy',
|
'Easy': 'Easy',
|
||||||
|
@ -1,29 +1,8 @@
|
|||||||
from copy import deepcopy
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from tja2fumen.utils import computeSoulGaugeBytes
|
from tja2fumen.utils import computeSoulGaugeBytes
|
||||||
from tja2fumen.constants import DIFFICULTY_BYTES, sampleHeaderMetadata, simpleHeaders
|
from tja2fumen.constants import DIFFICULTY_BYTES
|
||||||
|
from tja2fumen.types import TJAMeasureProcessed, FumenCourse, FumenNote
|
||||||
# Filler metadata that the `writeFumen` function expects
|
|
||||||
# TODO: Determine how to properly set the item byte (https://github.com/vivaria/tja2fumen/issues/17)
|
|
||||||
default_note = {'type': '', 'pos': 0.0, 'item': 0, 'padding': 0.0,
|
|
||||||
'scoreInit': 0, 'scoreDiff': 0, 'duration': 0.0}
|
|
||||||
default_branch = {'length': 0, 'padding': 0, 'speed': 1.0}
|
|
||||||
default_measure = {
|
|
||||||
'bpm': 0.0,
|
|
||||||
'fumenOffsetStart': 0.0,
|
|
||||||
'fumenOffsetEnd': 0.0,
|
|
||||||
'duration': 0.0,
|
|
||||||
'gogo': False,
|
|
||||||
'barline': True,
|
|
||||||
'padding1': 0,
|
|
||||||
'branchStart': None,
|
|
||||||
'branchInfo': [-1, -1, -1, -1, -1, -1],
|
|
||||||
'padding2': 0,
|
|
||||||
'normal': deepcopy(default_branch),
|
|
||||||
'advanced': deepcopy(default_branch),
|
|
||||||
'master': deepcopy(default_branch)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def processTJACommands(tja):
|
def processTJACommands(tja):
|
||||||
@ -44,136 +23,149 @@ def processTJACommands(tja):
|
|||||||
|
|
||||||
In the future, this logic should probably be moved into the TJA parser itself.
|
In the future, this logic should probably be moved into the TJA parser itself.
|
||||||
"""
|
"""
|
||||||
branches = tja['branches']
|
tjaBranchesProcessed = {branchName: [] for branchName in tja.branches.keys()}
|
||||||
branchesCorrected = {branchName: [] for branchName in branches.keys()}
|
for branchName, branchMeasuresTJA in tja.branches.items():
|
||||||
for branchName, branch in branches.items():
|
currentBPM = tja.BPM
|
||||||
currentBPM = float(tja['metadata']['bpm'])
|
|
||||||
currentScroll = 1.0
|
currentScroll = 1.0
|
||||||
currentGogo = False
|
currentGogo = False
|
||||||
currentBarline = True
|
currentBarline = True
|
||||||
currentDividend = 4
|
currentDividend = 4
|
||||||
currentDivisor = 4
|
currentDivisor = 4
|
||||||
for measure in branch:
|
for measureTJA in branchMeasuresTJA:
|
||||||
# Split measure into submeasure
|
# Split measure into submeasure
|
||||||
measure_cur = {'bpm': currentBPM, 'scroll': currentScroll, 'gogo': currentGogo, 'barline': currentBarline,
|
measureTJAProcessed = TJAMeasureProcessed(
|
||||||
'subdivisions': len(measure['data']), 'pos_start': 0, 'pos_end': 0, 'delay': 0,
|
bpm=currentBPM,
|
||||||
'branchStart': None, 'time_sig': [currentDividend, currentDivisor], 'data': []}
|
scroll=currentScroll,
|
||||||
for data in measure['combined']:
|
gogo=currentGogo,
|
||||||
|
barline=currentBarline,
|
||||||
|
time_sig=[currentDividend, currentDivisor],
|
||||||
|
subdivisions=len(measureTJA.notes),
|
||||||
|
)
|
||||||
|
for data in measureTJA.combined:
|
||||||
# Handle note data
|
# Handle note data
|
||||||
if data['type'] == 'note':
|
if data.name == 'note':
|
||||||
measure_cur['data'].append(data)
|
measureTJAProcessed.data.append(data)
|
||||||
|
|
||||||
# Handle commands that can only be placed between measures (i.e. no mid-measure variations)
|
# Handle commands that can only be placed between measures (i.e. no mid-measure variations)
|
||||||
elif data['type'] == 'delay':
|
elif data.name == 'delay':
|
||||||
measure_cur['delay'] = data['value'] * 1000 # ms -> s
|
measureTJAProcessed.delay = data.value * 1000 # ms -> s
|
||||||
elif data['type'] == 'branchStart':
|
elif data.name == 'branchStart':
|
||||||
measure_cur['branchStart'] = data['value']
|
measureTJAProcessed.branchStart = data.value
|
||||||
elif data['type'] == 'barline':
|
elif data.name == 'barline':
|
||||||
currentBarline = bool(int(data['value']))
|
currentBarline = bool(int(data.value))
|
||||||
measure_cur['barline'] = currentBarline
|
measureTJAProcessed.barline = currentBarline
|
||||||
elif data['type'] == 'measure':
|
elif data.name == 'measure':
|
||||||
matchMeasure = re.match(r"(\d+)/(\d+)", data['value'])
|
matchMeasure = re.match(r"(\d+)/(\d+)", data.value)
|
||||||
if not matchMeasure:
|
if not matchMeasure:
|
||||||
continue
|
continue
|
||||||
currentDividend = int(matchMeasure.group(1))
|
currentDividend = int(matchMeasure.group(1))
|
||||||
currentDivisor = int(matchMeasure.group(2))
|
currentDivisor = int(matchMeasure.group(2))
|
||||||
measure_cur['time_sig'] = [currentDividend, currentDivisor]
|
measureTJAProcessed.time_sig = [currentDividend, currentDivisor]
|
||||||
|
|
||||||
# Handle commands that can be placed in the middle of a measure.
|
# Handle commands that can be placed in the middle of a measure.
|
||||||
# NB: For fumen files, if there is a mid-measure change to BPM/SCROLL/GOGO, then the measure will
|
# NB: For fumen files, if there is a mid-measure change to BPM/SCROLL/GOGO, then the measure will
|
||||||
# actually be split into two small submeasures. So, we need to start a new measure in those cases.
|
# actually be split into two small submeasures. So, we need to start a new measure in those cases.
|
||||||
elif data['type'] in ['bpm', 'scroll', 'gogo']:
|
elif data.name in ['bpm', 'scroll', 'gogo']:
|
||||||
# Parse the values
|
# Parse the values
|
||||||
if data['type'] == 'bpm':
|
if data.name == 'bpm':
|
||||||
new_val = currentBPM = float(data['value'])
|
new_val = currentBPM = float(data.value)
|
||||||
elif data['type'] == 'scroll':
|
elif data.name == 'scroll':
|
||||||
new_val = currentScroll = data['value']
|
new_val = currentScroll = data.value
|
||||||
elif data['type'] == 'gogo':
|
elif data.name == 'gogo':
|
||||||
new_val = currentGogo = bool(int(data['value']))
|
new_val = currentGogo = bool(int(data.value))
|
||||||
# Check for mid-measure commands
|
# Check for mid-measure commands
|
||||||
# - Case 1: Command happens at the start of a measure; just change the value directly
|
# - Case 1: Command happens at the start of a measure; just change the value directly
|
||||||
if data['pos'] == 0:
|
if data.pos == 0:
|
||||||
measure_cur[data['type']] = new_val
|
measureTJAProcessed.__setattr__(data.name, new_val)
|
||||||
# - Case 2: Command occurs mid-measure, so start a new sub-measure
|
# - Case 2: Command occurs mid-measure, so start a new sub-measure
|
||||||
else:
|
else:
|
||||||
measure_cur['pos_end'] = data['pos']
|
measureTJAProcessed.pos_end = data.pos
|
||||||
branchesCorrected[branchName].append(measure_cur)
|
tjaBranchesProcessed[branchName].append(measureTJAProcessed)
|
||||||
measure_cur = {'bpm': currentBPM, 'scroll': currentScroll, 'gogo': currentGogo,
|
measureTJAProcessed = TJAMeasureProcessed(
|
||||||
'barline': currentBarline, 'subdivisions': len(measure['data']),
|
bpm=currentBPM,
|
||||||
'pos_start': data['pos'], 'pos_end': 0, 'delay': 0,
|
scroll=currentScroll,
|
||||||
'branchStart': None, 'time_sig': [currentDividend, currentDivisor], 'data': []}
|
gogo=currentGogo,
|
||||||
|
barline=currentBarline,
|
||||||
|
time_sig=[currentDividend, currentDivisor],
|
||||||
|
subdivisions=len(measureTJA.notes),
|
||||||
|
pos_start=data.pos
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"Unexpected event type: {data['type']}")
|
print(f"Unexpected event type: {data.name}")
|
||||||
|
|
||||||
measure_cur['pos_end'] = len(measure['data'])
|
measureTJAProcessed.pos_end = len(measureTJA.notes)
|
||||||
branchesCorrected[branchName].append(measure_cur)
|
tjaBranchesProcessed[branchName].append(measureTJAProcessed)
|
||||||
|
|
||||||
hasBranches = all(len(b) for b in branchesCorrected.values())
|
hasBranches = all(len(b) for b in tjaBranchesProcessed.values())
|
||||||
if hasBranches:
|
if hasBranches:
|
||||||
branch_lens = [len(b) for b in branches.values()]
|
branch_lens = [len(b) for b in tja.branches.values()]
|
||||||
if not branch_lens.count(branch_lens[0]) == len(branch_lens):
|
if not branch_lens.count(branch_lens[0]) == len(branch_lens):
|
||||||
raise ValueError("Branches do not have the same number of measures.")
|
raise ValueError("Branches do not have the same number of measures.")
|
||||||
else:
|
else:
|
||||||
branchCorrected_lens = [len(b) for b in branchesCorrected.values()]
|
branchCorrected_lens = [len(b) for b in tjaBranchesProcessed.values()]
|
||||||
if not branchCorrected_lens.count(branchCorrected_lens[0]) == len(branchCorrected_lens):
|
if not branchCorrected_lens.count(branchCorrected_lens[0]) == len(branchCorrected_lens):
|
||||||
raise ValueError("Branches do not have matching GOGO/SCROLL/BPM commands.")
|
raise ValueError("Branches do not have matching GOGO/SCROLL/BPM commands.")
|
||||||
|
|
||||||
return branchesCorrected
|
return tjaBranchesProcessed
|
||||||
|
|
||||||
|
|
||||||
def convertTJAToFumen(tja):
|
def convertTJAToFumen(tja):
|
||||||
# Preprocess commands
|
# Preprocess commands
|
||||||
tja['branches'] = processTJACommands(tja)
|
processedTJABranches = processTJACommands(tja)
|
||||||
|
|
||||||
# Pre-allocate the measures for the converted TJA
|
# Pre-allocate the measures for the converted TJA
|
||||||
tjaConverted = {'measures': [deepcopy(default_measure) for _ in range(len(tja['branches']['normal']))]}
|
fumen = FumenCourse(
|
||||||
|
measures=len(processedTJABranches['normal']),
|
||||||
|
hasBranches=all([len(b) for b in processedTJABranches.values()]),
|
||||||
|
scoreInit=tja.scoreInit,
|
||||||
|
scoreDiff=tja.scoreDiff,
|
||||||
|
)
|
||||||
|
|
||||||
# Iterate through the different branches in the TJA
|
# Iterate through the different branches in the TJA
|
||||||
for currentBranch, branch in tja['branches'].items():
|
for currentBranch, branchMeasuresTJAProcessed in processedTJABranches.items():
|
||||||
if not len(branch):
|
if not len(branchMeasuresTJAProcessed):
|
||||||
continue
|
continue
|
||||||
total_notes = 0
|
total_notes = 0
|
||||||
total_notes_branch = 0
|
total_notes_branch = 0
|
||||||
note_counter_branch = 0
|
note_counter_branch = 0
|
||||||
currentDrumroll = None
|
currentDrumroll = None
|
||||||
courseBalloons = tja['metadata']['balloon'].copy()
|
courseBalloons = tja.balloon.copy()
|
||||||
|
|
||||||
# Iterate through the measures within the branch
|
# Iterate through the measures within the branch
|
||||||
for idx_m, measureTJA in enumerate(branch):
|
for idx_m, measureTJAProcessed in enumerate(branchMeasuresTJAProcessed):
|
||||||
# Fetch a pair of measures
|
# Fetch a pair of measures
|
||||||
measureFumenPrev = tjaConverted['measures'][idx_m-1] if idx_m != 0 else None
|
measureFumenPrev = fumen.measures[idx_m-1] if idx_m != 0 else None
|
||||||
measureFumen = tjaConverted['measures'][idx_m]
|
measureFumen = fumen.measures[idx_m]
|
||||||
|
|
||||||
# Copy over basic measure properties from the TJA (that don't depend on notes or commands)
|
# Copy over basic measure properties from the TJA (that don't depend on notes or commands)
|
||||||
measureFumen[currentBranch]['speed'] = measureTJA['scroll']
|
measureFumen.branches[currentBranch].speed = measureTJAProcessed.scroll
|
||||||
measureFumen['gogo'] = measureTJA['gogo']
|
measureFumen.gogo = measureTJAProcessed.gogo
|
||||||
measureFumen['bpm'] = measureTJA['bpm']
|
measureFumen.bpm = measureTJAProcessed.bpm
|
||||||
|
|
||||||
# Compute the duration of the measure
|
# Compute the duration of the measure
|
||||||
# First, we compute the duration for a full 4/4 measure
|
# First, we compute the duration for a full 4/4 measure
|
||||||
measureDurationFullMeasure = 4 * 60_000 / measureTJA['bpm']
|
measureDurationFullMeasure = 4 * 60_000 / measureTJAProcessed.bpm
|
||||||
# Next, we adjust this duration based on both:
|
# Next, we adjust this duration based on both:
|
||||||
# 1. The *actual* measure size (e.g. #MEASURE 1/8, #MEASURE 5/4, etc.)
|
# 1. The *actual* measure size (e.g. #MEASURE 1/8, #MEASURE 5/4, etc.)
|
||||||
measureSize = measureTJA['time_sig'][0] / measureTJA['time_sig'][1]
|
measureSize = measureTJAProcessed.time_sig[0] / measureTJAProcessed.time_sig[1]
|
||||||
# 2. Whether this is a "submeasure" (i.e. it contains mid-measure commands, which split up the measure)
|
# 2. Whether this is a "submeasure" (i.e. it contains mid-measure commands, which split up the measure)
|
||||||
# - If this is a submeasure, then `measureLength` will be less than the total number of subdivisions.
|
# - If this is a submeasure, then `measureLength` will be less than the total number of subdivisions.
|
||||||
measureLength = measureTJA['pos_end'] - measureTJA['pos_start']
|
measureLength = measureTJAProcessed.pos_end - measureTJAProcessed.pos_start
|
||||||
# - In other words, `measureRatio` will be less than 1.0:
|
# - In other words, `measureRatio` will be less than 1.0:
|
||||||
measureRatio = (1.0 if measureTJA['subdivisions'] == 0.0 # Avoid division by 0 for empty measures
|
measureRatio = (1.0 if measureTJAProcessed.subdivisions == 0.0 # Avoid division by 0 for empty measures
|
||||||
else (measureLength / measureTJA['subdivisions']))
|
else (measureLength / measureTJAProcessed.subdivisions))
|
||||||
# Apply the 2 adjustments to the measure duration
|
# Apply the 2 adjustments to the measure duration
|
||||||
measureFumen['duration'] = measureDuration = measureDurationFullMeasure * measureSize * measureRatio
|
measureFumen.duration = measureDuration = measureDurationFullMeasure * measureSize * measureRatio
|
||||||
|
|
||||||
# Compute the millisecond offsets for the start and end of each measure
|
# Compute the millisecond offsets for the start and end of each measure
|
||||||
# - Start: When the notes first appear on screen (to the right)
|
# - Start: When the notes first appear on screen (to the right)
|
||||||
# - End: When the notes arrive at the judgment line, and the note gets hit.
|
# - End: When the notes arrive at the judgment line, and the note gets hit.
|
||||||
if idx_m == 0:
|
if idx_m == 0:
|
||||||
tjaOffset = float(tja['metadata']['offset']) * 1000 * -1
|
measureFumen.fumenOffsetStart = (tja.offset * 1000 * -1) - measureDurationFullMeasure
|
||||||
measureFumen['fumenOffsetStart'] = tjaOffset - measureDurationFullMeasure
|
|
||||||
else:
|
else:
|
||||||
# First, start the measure using the end timing of the previous measure (plus any #DELAY commands)
|
# First, start the measure using the end timing of the previous measure (plus any #DELAY commands)
|
||||||
measureFumen['fumenOffsetStart'] = measureFumenPrev['fumenOffsetEnd'] + measureTJA['delay']
|
measureFumen.fumenOffsetStart = measureFumenPrev.fumenOffsetEnd + measureTJAProcessed.delay
|
||||||
# Next, adjust the start timing to account for #BPMCHANGE commands (!!! Discovered by tana :3 !!!)
|
# Next, adjust the start timing to account for #BPMCHANGE commands (!!! Discovered by tana :3 !!!)
|
||||||
# To understand what's going on here, imagine the following simple example:
|
# To understand what's going on here, imagine the following simple example:
|
||||||
# * You have a very slow-moving note (i.e. low BPM), like the big DON in Donkama 2000.
|
# * You have a very slow-moving note (i.e. low BPM), like the big DON in Donkama 2000.
|
||||||
@ -182,12 +174,12 @@ def convertTJAToFumen(tja):
|
|||||||
# - An early start means you need to subtract a LOT of time from the starting fumenOffset.
|
# - An early start means you need to subtract a LOT of time from the starting fumenOffset.
|
||||||
# - Thankfully, the low BPM of the slow note will create a HUGE `measureOffsetAdjustment`,
|
# - Thankfully, the low BPM of the slow note will create a HUGE `measureOffsetAdjustment`,
|
||||||
# since we are dividing by the BPMs, and dividing by a small number will result in a big number.
|
# since we are dividing by the BPMs, and dividing by a small number will result in a big number.
|
||||||
measureOffsetAdjustment = (4 * 60_000 / measureTJA['bpm']) - (4 * 60_000 / measureFumenPrev['bpm'])
|
measureOffsetAdjustment = (4 * 60_000 / measureFumen.bpm) - (4 * 60_000 / measureFumenPrev.bpm)
|
||||||
# - When we subtract this adjustment from the fumenOffsetStart, we get the "START EARLY" part:
|
# - When we subtract this adjustment from the fumenOffsetStart, we get the "START EARLY" part:
|
||||||
measureFumen['fumenOffsetStart'] -= measureOffsetAdjustment
|
measureFumen.fumenOffsetStart -= measureOffsetAdjustment
|
||||||
# - The low BPM of the slow note will also create a HUGE measure duration.
|
# - The low BPM of the slow note will also create a HUGE measure duration.
|
||||||
# - When we add this long duration to the EARLY START, we end up with the "END LATE" part:
|
# - When we add this long duration to the EARLY START, we end up with the "END LATE" part:
|
||||||
measureFumen['fumenOffsetEnd'] = measureFumen['fumenOffsetStart'] + measureFumen['duration']
|
measureFumen.fumenOffsetEnd = measureFumen.fumenOffsetStart + measureFumen.duration
|
||||||
|
|
||||||
# Best guess at what 'barline' status means for each measure:
|
# Best guess at what 'barline' status means for each measure:
|
||||||
# - 'True' means the measure lands on a barline (i.e. most measures), and thus barline should be shown
|
# - 'True' means the measure lands on a barline (i.e. most measures), and thus barline should be shown
|
||||||
@ -195,18 +187,18 @@ def convertTJAToFumen(tja):
|
|||||||
# For example:
|
# For example:
|
||||||
# 1. Measures where #BARLINEOFF has been set
|
# 1. Measures where #BARLINEOFF has been set
|
||||||
# 2. Sub-measures that don't fall on the barline
|
# 2. Sub-measures that don't fall on the barline
|
||||||
if measureTJA['barline'] is False or (measureRatio != 1.0 and measureTJA['pos_start'] != 0):
|
if measureTJAProcessed.barline is False or (measureRatio != 1.0 and measureTJAProcessed.pos_start != 0):
|
||||||
measureFumen['barline'] = False
|
measureFumen.barline = False
|
||||||
|
|
||||||
# Check to see if the measure contains a branching condition
|
# Check to see if the measure contains a branching condition
|
||||||
if measureTJA['branchStart']:
|
if measureTJAProcessed.branchStart:
|
||||||
# Determine which values to assign based on the type of branching condition
|
# Determine which values to assign based on the type of branching condition
|
||||||
if measureTJA['branchStart'][0] == 'p':
|
if measureTJAProcessed.branchStart[0] == 'p':
|
||||||
vals = [int(total_notes_branch * v * 20) if 0 <= v <= 1 # Ensure value is actually a percentage
|
vals = [int(total_notes_branch * v * 20) if 0 <= v <= 1 # Ensure value is actually a percentage
|
||||||
else int(v * 100) # If it's not, pass the value as-is
|
else int(v * 100) # If it's not, pass the value as-is
|
||||||
for v in measureTJA['branchStart'][1:]]
|
for v in measureTJAProcessed.branchStart[1:]]
|
||||||
elif measureTJA['branchStart'][0] == 'r':
|
elif measureTJAProcessed.branchStart[0] == 'r':
|
||||||
vals = measureTJA['branchStart'][1:]
|
vals = measureTJAProcessed.branchStart[1:]
|
||||||
# Determine which bytes to assign the values to
|
# Determine which bytes to assign the values to
|
||||||
if currentBranch == 'normal':
|
if currentBranch == 'normal':
|
||||||
idx_b1, idx_b2 = 0, 1
|
idx_b1, idx_b2 = 0, 1
|
||||||
@ -215,94 +207,84 @@ def convertTJAToFumen(tja):
|
|||||||
elif currentBranch == 'master':
|
elif currentBranch == 'master':
|
||||||
idx_b1, idx_b2 = 4, 5
|
idx_b1, idx_b2 = 4, 5
|
||||||
# Assign the values to their intended bytes
|
# Assign the values to their intended bytes
|
||||||
measureFumen['branchInfo'][idx_b1] = vals[0]
|
measureFumen.branchInfo[idx_b1] = vals[0]
|
||||||
measureFumen['branchInfo'][idx_b2] = vals[1]
|
measureFumen.branchInfo[idx_b2] = vals[1]
|
||||||
# Reset the note counter corresponding to this branch
|
# Reset the note counter corresponding to this branch
|
||||||
total_notes_branch = 0
|
total_notes_branch = 0
|
||||||
total_notes_branch += note_counter_branch
|
total_notes_branch += note_counter_branch
|
||||||
|
|
||||||
# Create note dictionaries based on TJA measure data (containing 0's plus 1/2/3/4/etc. for notes)
|
# Create notes based on TJA measure data
|
||||||
note_counter_branch = 0
|
note_counter_branch = 0
|
||||||
note_counter = 0
|
note_counter = 0
|
||||||
for idx_d, data in enumerate(measureTJA['data']):
|
for idx_d, data in enumerate(measureTJAProcessed.data):
|
||||||
if data['type'] == 'note':
|
if data.name == 'note':
|
||||||
|
note = FumenNote()
|
||||||
# Note positions must be calculated using the base measure duration (that uses a single BPM value)
|
# Note positions must be calculated using the base measure duration (that uses a single BPM value)
|
||||||
# (In other words, note positions do not take into account any mid-measure BPM change adjustments.)
|
# (In other words, note positions do not take into account any mid-measure BPM change adjustments.)
|
||||||
note_pos = measureDuration * (data['pos'] - measureTJA['pos_start']) / measureLength
|
note.pos = measureDuration * (data.pos - measureTJAProcessed.pos_start) / measureLength
|
||||||
# Handle the note that represents the end of a drumroll/balloon
|
# Handle the note that represents the end of a drumroll/balloon
|
||||||
if data['value'] == "EndDRB":
|
if data.value == "EndDRB":
|
||||||
# If a drumroll spans a single measure, then add the difference between start/end position
|
# If a drumroll spans a single measure, then add the difference between start/end position
|
||||||
if 'multimeasure' not in currentDrumroll.keys():
|
if not currentDrumroll.multimeasure:
|
||||||
currentDrumroll['duration'] += (note_pos - currentDrumroll['pos'])
|
currentDrumroll.duration += (note.pos - currentDrumroll.pos)
|
||||||
# Otherwise, if a drumroll spans multiple measures, then we want to add the duration between
|
# Otherwise, if a drumroll spans multiple measures, then we want to add the duration between
|
||||||
# the start of the measure (i.e. pos=0.0) and the drumroll's end position.
|
# the start of the measure (i.e. pos=0.0) and the drumroll's end position.
|
||||||
else:
|
else:
|
||||||
currentDrumroll['duration'] += (note_pos - 0.0)
|
currentDrumroll.duration += (note.pos - 0.0)
|
||||||
# 1182, 1385, 1588, 2469, 1568, 752, 1568
|
# 1182, 1385, 1588, 2469, 1568, 752, 1568
|
||||||
currentDrumroll['duration'] = float(int(currentDrumroll['duration']))
|
currentDrumroll.duration = float(int(currentDrumroll.duration))
|
||||||
currentDrumroll = None
|
currentDrumroll = None
|
||||||
continue
|
continue
|
||||||
# The TJA spec technically allows you to place double-Kusudama notes:
|
# The TJA spec technically allows you to place double-Kusudama notes:
|
||||||
# "Use another 9 to specify when to lower the points for clearing."
|
# "Use another 9 to specify when to lower the points for clearing."
|
||||||
# But this is unsupported in fumens, so just skip the second Kusudama note.
|
# But this is unsupported in fumens, so just skip the second Kusudama note.
|
||||||
if data['value'] == "Kusudama" and currentDrumroll:
|
if data.value == "Kusudama" and currentDrumroll:
|
||||||
continue
|
continue
|
||||||
# Handle the remaining non-EndDRB, non-double Kusudama notes
|
# Handle the remaining non-EndDRB, non-double Kusudama notes
|
||||||
note = deepcopy(default_note)
|
note.type = data.value
|
||||||
note['pos'] = note_pos
|
note.scoreInit = tja.scoreInit
|
||||||
note['type'] = data['value']
|
note.scoreDiff = tja.scoreDiff
|
||||||
note['scoreInit'] = tja['metadata']['scoreInit'] # Probably not fully accurate
|
|
||||||
note['scoreDiff'] = tja['metadata']['scoreDiff'] # Probably not fully accurate
|
|
||||||
# Handle drumroll/balloon-specific metadata
|
# Handle drumroll/balloon-specific metadata
|
||||||
if note['type'] in ["Balloon", "Kusudama"]:
|
if note.type in ["Balloon", "Kusudama"]:
|
||||||
note['hits'] = courseBalloons.pop(0)
|
note.hits = courseBalloons.pop(0)
|
||||||
note['hitsPadding'] = 0
|
|
||||||
currentDrumroll = note
|
currentDrumroll = note
|
||||||
total_notes -= 1
|
total_notes -= 1
|
||||||
if note['type'] in ["Drumroll", "DRUMROLL"]:
|
if note.type in ["Drumroll", "DRUMROLL"]:
|
||||||
note['drumrollBytes'] = b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
|
||||||
currentDrumroll = note
|
currentDrumroll = note
|
||||||
total_notes -= 1
|
total_notes -= 1
|
||||||
# Count dons, kas, and balloons for the purpose of tracking branching accuracy
|
# Count dons, kas, and balloons for the purpose of tracking branching accuracy
|
||||||
if note['type'].lower() in ['don', 'ka']:
|
if note.type.lower() in ['don', 'ka']:
|
||||||
note_counter_branch += 1
|
note_counter_branch += 1
|
||||||
elif note['type'].lower() in ['balloon', 'kusudama']:
|
elif note.type.lower() in ['balloon', 'kusudama']:
|
||||||
note_counter_branch += 1.5
|
note_counter_branch += 1.5
|
||||||
measureFumen[currentBranch][note_counter] = note
|
measureFumen.branches[currentBranch].notes.append(note)
|
||||||
note_counter += 1
|
note_counter += 1
|
||||||
|
|
||||||
# If drumroll hasn't ended by the end of this measure, increase duration by measure timing
|
# If drumroll hasn't ended by the end of this measure, increase duration by measure timing
|
||||||
if currentDrumroll:
|
if currentDrumroll:
|
||||||
if currentDrumroll['duration'] == 0.0:
|
if currentDrumroll.duration == 0.0:
|
||||||
currentDrumroll['duration'] += (measureDuration - currentDrumroll['pos'])
|
currentDrumroll.duration += (measureDuration - currentDrumroll.pos)
|
||||||
currentDrumroll['multimeasure'] = True
|
currentDrumroll.multimeasure = True
|
||||||
else:
|
else:
|
||||||
currentDrumroll['duration'] += measureDuration
|
currentDrumroll.duration += measureDuration
|
||||||
|
|
||||||
measureFumen[currentBranch]['length'] = note_counter
|
measureFumen.branches[currentBranch].length = note_counter
|
||||||
total_notes += note_counter
|
total_notes += note_counter
|
||||||
|
|
||||||
# Take a stock header metadata sample and add song-specific metadata
|
# Take a stock header metadata sample and add song-specific metadata
|
||||||
headerMetadata = sampleHeaderMetadata.copy()
|
fumen.headerMetadata[8] = DIFFICULTY_BYTES[tja.course][0]
|
||||||
headerMetadata[8] = DIFFICULTY_BYTES[tja['metadata']['course']][0]
|
fumen.headerMetadata[9] = DIFFICULTY_BYTES[tja.course][1]
|
||||||
headerMetadata[9] = DIFFICULTY_BYTES[tja['metadata']['course']][1]
|
|
||||||
soulGaugeBytes = computeSoulGaugeBytes(
|
soulGaugeBytes = computeSoulGaugeBytes(
|
||||||
n_notes=total_notes,
|
n_notes=total_notes,
|
||||||
difficulty=tja['metadata']['course'],
|
difficulty=tja.course,
|
||||||
stars=tja['metadata']['level']
|
stars=tja.level
|
||||||
)
|
)
|
||||||
headerMetadata[12] = soulGaugeBytes[0]
|
fumen.headerMetadata[12] = soulGaugeBytes[0]
|
||||||
headerMetadata[13] = soulGaugeBytes[1]
|
fumen.headerMetadata[13] = soulGaugeBytes[1]
|
||||||
headerMetadata[16] = soulGaugeBytes[2]
|
fumen.headerMetadata[16] = soulGaugeBytes[2]
|
||||||
headerMetadata[17] = soulGaugeBytes[3]
|
fumen.headerMetadata[17] = soulGaugeBytes[3]
|
||||||
headerMetadata[20] = soulGaugeBytes[4]
|
fumen.headerMetadata[20] = soulGaugeBytes[4]
|
||||||
headerMetadata[21] = soulGaugeBytes[5]
|
fumen.headerMetadata[21] = soulGaugeBytes[5]
|
||||||
tjaConverted['headerMetadata'] = b"".join(i.to_bytes(1, 'little') for i in headerMetadata)
|
fumen.headerMetadata = b"".join(i.to_bytes(1, 'little') for i in fumen.headerMetadata)
|
||||||
tjaConverted['headerPadding'] = simpleHeaders[0] # Use a basic, known set of header bytes
|
|
||||||
tjaConverted['order'] = '<'
|
|
||||||
tjaConverted['unknownMetadata'] = 0
|
|
||||||
tjaConverted['branches'] = all([len(b) for b in tja['branches'].values()])
|
|
||||||
tjaConverted['scoreInit'] = tja['metadata']['scoreInit']
|
|
||||||
tjaConverted['scoreDiff'] = tja['metadata']['scoreDiff']
|
|
||||||
|
|
||||||
return tjaConverted
|
return fumen
|
||||||
|
@ -4,7 +4,7 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from tja2fumen.utils import readStruct, getBool, shortHex
|
from tja2fumen.utils import readStruct, getBool, shortHex
|
||||||
from tja2fumen.constants import NORMALIZE_COURSE, TJA_NOTE_TYPES, branchNames, noteTypes
|
from tja2fumen.constants import NORMALIZE_COURSE, TJA_NOTE_TYPES, branchNames, noteTypes
|
||||||
|
from tja2fumen.types import TJASong, TJAMeasure, TJAData, FumenCourse, FumenMeasure, FumenBranch, FumenNote
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# TJA-parsing functions ( Original source: https://github.com/WHMHammer/tja-tools/blob/master/src/js/parseTJA.js)
|
# TJA-parsing functions ( Original source: https://github.com/WHMHammer/tja-tools/blob/master/src/js/parseTJA.js)
|
||||||
@ -18,15 +18,15 @@ def parseTJA(fnameTJA):
|
|||||||
tja_text = open(fnameTJA, "r", encoding="shift-jis").read()
|
tja_text = open(fnameTJA, "r", encoding="shift-jis").read()
|
||||||
|
|
||||||
lines = [line for line in tja_text.splitlines() if line.strip() != '']
|
lines = [line for line in tja_text.splitlines() if line.strip() != '']
|
||||||
courses = getCourseData(lines)
|
parsedTJA = getCourseData(lines)
|
||||||
for courseData in courses.values():
|
for course in parsedTJA.courses.values():
|
||||||
courseData['branches'] = parseCourseMeasures(courseData['data'])
|
parseCourseMeasures(course)
|
||||||
|
|
||||||
return courses
|
return parsedTJA
|
||||||
|
|
||||||
|
|
||||||
def getCourseData(lines):
|
def getCourseData(lines):
|
||||||
courses = {}
|
parsedTJA = None
|
||||||
currentCourse = ''
|
currentCourse = ''
|
||||||
currentCourseCached = ''
|
currentCourseCached = ''
|
||||||
songBPM = 0
|
songBPM = 0
|
||||||
@ -40,31 +40,30 @@ def getCourseData(lines):
|
|||||||
value = match_header.group(2).strip()
|
value = match_header.group(2).strip()
|
||||||
|
|
||||||
# Global header fields
|
# Global header fields
|
||||||
if nameUpper == 'BPM':
|
if nameUpper in ['BPM', 'OFFSET']:
|
||||||
songBPM = value
|
if nameUpper == 'BPM':
|
||||||
elif nameUpper == 'OFFSET':
|
songBPM = value
|
||||||
songOffset = value
|
elif nameUpper == 'OFFSET':
|
||||||
|
songOffset = value
|
||||||
|
if songBPM and songOffset:
|
||||||
|
parsedTJA = TJASong(songBPM, songOffset)
|
||||||
|
|
||||||
# Course-specific header fields
|
# Course-specific header fields
|
||||||
elif nameUpper == 'COURSE':
|
elif nameUpper == 'COURSE':
|
||||||
currentCourse = NORMALIZE_COURSE[value]
|
currentCourse = NORMALIZE_COURSE[value]
|
||||||
currentCourseCached = currentCourse
|
currentCourseCached = currentCourse
|
||||||
if currentCourse not in courses.keys():
|
if currentCourse not in parsedTJA.courses.keys():
|
||||||
courses[currentCourse] = {
|
raise ValueError()
|
||||||
'metadata': {'course': currentCourse, 'bpm': songBPM, 'offset': songOffset, 'level': 0,
|
|
||||||
'balloon': [], 'scoreInit': 0, 'scoreDiff': 0},
|
|
||||||
'data': [],
|
|
||||||
}
|
|
||||||
elif nameUpper == 'LEVEL':
|
elif nameUpper == 'LEVEL':
|
||||||
courses[currentCourse]['metadata']['level'] = int(value) if value else 0
|
parsedTJA.courses[currentCourse].level = int(value) if value else 0
|
||||||
elif nameUpper == 'SCOREINIT':
|
elif nameUpper == 'SCOREINIT':
|
||||||
courses[currentCourse]['metadata']['scoreInit'] = int(value) if value else 0
|
parsedTJA.courses[currentCourse].scoreInit = int(value) if value else 0
|
||||||
elif nameUpper == 'SCOREDIFF':
|
elif nameUpper == 'SCOREDIFF':
|
||||||
courses[currentCourse]['metadata']['scoreDiff'] = int(value) if value else 0
|
parsedTJA.courses[currentCourse].scoreDiff = int(value) if value else 0
|
||||||
elif nameUpper == 'BALLOON':
|
elif nameUpper == 'BALLOON':
|
||||||
if value:
|
if value:
|
||||||
balloons = [int(v) for v in value.split(",") if v]
|
balloons = [int(v) for v in value.split(",") if v]
|
||||||
courses[currentCourse]['metadata']['balloon'] = balloons
|
parsedTJA.courses[currentCourse].balloon = balloons
|
||||||
elif nameUpper == 'STYLE':
|
elif nameUpper == 'STYLE':
|
||||||
# Reset the course name to remove "P1/P2" that may have been added by a previous STYLE:DOUBLE chart
|
# Reset the course name to remove "P1/P2" that may have been added by a previous STYLE:DOUBLE chart
|
||||||
if value == 'Single':
|
if value == 'Single':
|
||||||
@ -84,121 +83,123 @@ def getCourseData(lines):
|
|||||||
if nameUpper == "START":
|
if nameUpper == "START":
|
||||||
if value in ["P1", "P2"]:
|
if value in ["P1", "P2"]:
|
||||||
currentCourse = currentCourseCached + value
|
currentCourse = currentCourseCached + value
|
||||||
courses[currentCourse] = deepcopy(courses[currentCourseCached])
|
parsedTJA.courses[currentCourse] = deepcopy(parsedTJA.courses[currentCourseCached])
|
||||||
courses[currentCourse]['data'] = list() # Keep the metadata, but reset the note data
|
parsedTJA.courses[currentCourse].data = list() # Keep the metadata, but reset the note data
|
||||||
value = '' # Once we've made the new course, we can reset this to a normal #START command
|
value = '' # Once we've made the new course, we can reset this to a normal #START command
|
||||||
elif value:
|
elif value:
|
||||||
raise ValueError(f"Invalid value '{value}' for #START command.")
|
raise ValueError(f"Invalid value '{value}' for #START command.")
|
||||||
elif match_notes:
|
elif match_notes:
|
||||||
nameUpper = 'NOTES'
|
nameUpper = 'NOTES'
|
||||||
value = match_notes.group(1)
|
value = match_notes.group(1)
|
||||||
courses[currentCourse]['data'].append({"name": nameUpper, "value": value})
|
parsedTJA.courses[currentCourse].data.append(TJAData(nameUpper, value))
|
||||||
|
|
||||||
# If a course has no song data, then this is likely because the course has "STYLE: Double" but no "STYLE: Single".
|
# If a course has no song data, then this is likely because the course has "STYLE: Double" but no "STYLE: Single".
|
||||||
# To fix this, we copy over the P1 chart from "STYLE: Double" to fill the "STYLE: Single" role.
|
# To fix this, we copy over the P1 chart from "STYLE: Double" to fill the "STYLE: Single" role.
|
||||||
for courseName, course in courses.items():
|
for courseName, course in parsedTJA.courses.items():
|
||||||
if not course['data']:
|
if not course.data:
|
||||||
if courseName+"P1" in courses.keys():
|
if courseName+"P1" in parsedTJA.courses.keys():
|
||||||
courses[courseName] = deepcopy(courses[courseName+"P1"])
|
parsedTJA.courses[courseName] = deepcopy(parsedTJA.courses[courseName+"P1"])
|
||||||
|
|
||||||
return courses
|
# Remove any charts (e.g. P1/P2) not present in the TJA file
|
||||||
|
for course_name in [k for k, v in parsedTJA.courses.items() if not v.data]:
|
||||||
|
del parsedTJA.courses[course_name]
|
||||||
|
|
||||||
|
return parsedTJA
|
||||||
|
|
||||||
|
|
||||||
def parseCourseMeasures(lines):
|
def parseCourseMeasures(course):
|
||||||
# Check if the course has branches or not
|
# Check if the course has branches or not
|
||||||
hasBranches = True if [l for l in lines if l['name'] == 'BRANCHSTART'] else False
|
hasBranches = True if [l for l in course.data if l.name == 'BRANCHSTART'] else False
|
||||||
currentBranch = 'all' if hasBranches else 'normal'
|
currentBranch = 'all' if hasBranches else 'normal'
|
||||||
flagLevelhold = False
|
flagLevelhold = False
|
||||||
|
|
||||||
# Process course lines
|
# Process course lines
|
||||||
idx_m = 0
|
idx_m = 0
|
||||||
idx_m_branchstart = 0
|
idx_m_branchstart = 0
|
||||||
emptyMeasure = {'data': '', 'events': []}
|
for line in course.data:
|
||||||
branches = {'normal': [deepcopy(emptyMeasure)], 'advanced': [deepcopy(emptyMeasure)], 'master': [deepcopy(emptyMeasure)]}
|
|
||||||
for line in lines:
|
|
||||||
# 1. Parse measure notes
|
# 1. Parse measure notes
|
||||||
if line['name'] == 'NOTES':
|
if line.name == 'NOTES':
|
||||||
notes = line['value']
|
notes = line.value
|
||||||
# If measure has ended, then add notes to the current measure, then start a new one by incrementing idx_m
|
# If measure has ended, then add notes to the current measure, then start a new one by incrementing idx_m
|
||||||
if notes.endswith(','):
|
if notes.endswith(','):
|
||||||
for branch in branches.keys() if currentBranch == 'all' else [currentBranch]:
|
for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]:
|
||||||
branches[branch][idx_m]['data'] += notes[0:-1]
|
course.branches[branch][idx_m].notes += notes[0:-1]
|
||||||
branches[branch].append(deepcopy(emptyMeasure))
|
course.branches[branch].append(TJAMeasure())
|
||||||
idx_m += 1
|
idx_m += 1
|
||||||
# Otherwise, keep adding notes to the current measure ('idx_m')
|
# Otherwise, keep adding notes to the current measure ('idx_m')
|
||||||
else:
|
else:
|
||||||
for branch in branches.keys() if currentBranch == 'all' else [currentBranch]:
|
for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]:
|
||||||
branches[branch][idx_m]['data'] += notes
|
course.branches[branch][idx_m].notes += notes
|
||||||
|
|
||||||
# 2. Parse measure commands that produce an "event"
|
# 2. Parse measure commands that produce an "event"
|
||||||
elif line['name'] in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF', 'DELAY',
|
elif line.name in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF', 'DELAY',
|
||||||
'SCROLL', 'BPMCHANGE', 'MEASURE', 'BRANCHSTART']:
|
'SCROLL', 'BPMCHANGE', 'MEASURE', 'BRANCHSTART']:
|
||||||
# Get position of the event
|
# Get position of the event
|
||||||
for branch in branches.keys() if currentBranch == 'all' else [currentBranch]:
|
for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]:
|
||||||
pos = len(branches[branch][idx_m]['data'])
|
pos = len(course.branches[branch][idx_m].notes)
|
||||||
|
|
||||||
# Parse event type
|
# Parse event type
|
||||||
if line['name'] == 'GOGOSTART':
|
if line.name == 'GOGOSTART':
|
||||||
currentEvent = {"name": 'gogo', "position": pos, "value": '1'}
|
currentEvent = TJAData('gogo', '1', pos)
|
||||||
elif line['name'] == 'GOGOEND':
|
elif line.name == 'GOGOEND':
|
||||||
currentEvent = {"name": 'gogo', "position": pos, "value": '0'}
|
currentEvent = TJAData('gogo', '0', pos)
|
||||||
elif line['name'] == 'BARLINEON':
|
elif line.name == 'BARLINEON':
|
||||||
currentEvent = {"name": 'barline', "position": pos, "value": '1'}
|
currentEvent = TJAData('barline', '1', pos)
|
||||||
elif line['name'] == 'BARLINEOFF':
|
elif line.name == 'BARLINEOFF':
|
||||||
currentEvent = {"name": 'barline', "position": pos, "value": '0'}
|
currentEvent = TJAData('barline', '0', pos)
|
||||||
elif line['name'] == 'DELAY':
|
elif line.name == 'DELAY':
|
||||||
currentEvent = {"name": 'delay', "position": pos, "value": float(line['value'])}
|
currentEvent = TJAData('delay', float(line.value), pos)
|
||||||
elif line['name'] == 'SCROLL':
|
elif line.name == 'SCROLL':
|
||||||
currentEvent = {"name": 'scroll', "position": pos, "value": float(line['value'])}
|
currentEvent = TJAData('scroll', float(line.value), pos)
|
||||||
elif line['name'] == 'BPMCHANGE':
|
elif line.name == 'BPMCHANGE':
|
||||||
currentEvent = {"name": 'bpm', "position": pos, "value": float(line['value'])}
|
currentEvent = TJAData('bpm', float(line.value), pos)
|
||||||
elif line['name'] == 'MEASURE':
|
elif line.name == 'MEASURE':
|
||||||
currentEvent = {"name": 'measure', "position": pos, "value": line['value']}
|
currentEvent = TJAData('measure', line.value, pos)
|
||||||
elif line["name"] == 'BRANCHSTART':
|
elif line.name == 'BRANCHSTART':
|
||||||
if flagLevelhold:
|
if flagLevelhold:
|
||||||
continue
|
continue
|
||||||
currentBranch = 'all' # Ensure that the #BRANCHSTART command is present for all branches
|
currentBranch = 'all' # Ensure that the #BRANCHSTART command is present for all branches
|
||||||
values = line['value'].split(',')
|
values = line.value.split(',')
|
||||||
if values[0] == 'r': # r = drumRoll
|
if values[0] == 'r': # r = drumRoll
|
||||||
values[1] = int(values[1]) # # of drumrolls
|
values[1] = int(values[1]) # # of drumrolls
|
||||||
values[2] = int(values[2]) # # of drumrolls
|
values[2] = int(values[2]) # # of drumrolls
|
||||||
elif values[0] == 'p': # p = Percentage
|
elif values[0] == 'p': # p = Percentage
|
||||||
values[1] = float(values[1]) / 100 # %
|
values[1] = float(values[1]) / 100 # %
|
||||||
values[2] = float(values[2]) / 100 # %
|
values[2] = float(values[2]) / 100 # %
|
||||||
currentEvent = {"name": 'branchStart', "position": pos, "value": values}
|
currentEvent = TJAData('branchStart', values, pos)
|
||||||
idx_m_branchstart = idx_m # Preserve the index of the BRANCHSTART command to re-use for each branch
|
idx_m_branchstart = idx_m # Preserve the index of the BRANCHSTART command to re-use for each branch
|
||||||
|
|
||||||
# Append event to the current measure's events
|
# Append event to the current measure's events
|
||||||
for branch in branches.keys() if currentBranch == 'all' else [currentBranch]:
|
for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]:
|
||||||
branches[branch][idx_m]['events'].append(currentEvent)
|
course.branches[branch][idx_m].events.append(currentEvent)
|
||||||
elif line['name'] == 'SECTION':
|
elif line.name == 'SECTION':
|
||||||
# Simply repeat the same #BRANCHSTART condition that happened previously
|
# Simply repeat the same #BRANCHSTART condition that happened previously
|
||||||
# The purpose of #SECTION is to "Reset accuracy values for notes and drumrolls on the next measure."
|
# The purpose of #SECTION is to "Reset accuracy values for notes and drumrolls on the next measure."
|
||||||
branches[branch][idx_m]['events'].append({"name": 'branchStart', "position": pos, "value": values})
|
course.branches[branch][idx_m].events.append(TJAData('branchStart', values, pos))
|
||||||
|
|
||||||
# 3. Parse commands that don't create an event (e.g. simply changing the current branch)
|
# 3. Parse commands that don't create an event (e.g. simply changing the current branch)
|
||||||
else:
|
else:
|
||||||
if line["name"] == 'START' or line['name'] == 'END':
|
if line.name == 'START' or line.name == 'END':
|
||||||
currentBranch = 'all' if hasBranches else 'normal'
|
currentBranch = 'all' if hasBranches else 'normal'
|
||||||
flagLevelhold = False
|
flagLevelhold = False
|
||||||
elif line['name'] == 'LEVELHOLD':
|
elif line.name == 'LEVELHOLD':
|
||||||
flagLevelhold = True
|
flagLevelhold = True
|
||||||
elif line["name"] == 'N':
|
elif line.name == 'N':
|
||||||
currentBranch = 'normal'
|
currentBranch = 'normal'
|
||||||
idx_m = idx_m_branchstart
|
idx_m = idx_m_branchstart
|
||||||
elif line["name"] == 'E':
|
elif line.name == 'E':
|
||||||
currentBranch = 'advanced'
|
currentBranch = 'advanced'
|
||||||
idx_m = idx_m_branchstart
|
idx_m = idx_m_branchstart
|
||||||
elif line["name"] == 'M':
|
elif line.name == 'M':
|
||||||
currentBranch = 'master'
|
currentBranch = 'master'
|
||||||
idx_m = idx_m_branchstart
|
idx_m = idx_m_branchstart
|
||||||
elif line["name"] == 'BRANCHEND':
|
elif line.name == 'BRANCHEND':
|
||||||
currentBranch = 'all'
|
currentBranch = 'all'
|
||||||
|
|
||||||
# Ignored commands
|
# Ignored commands
|
||||||
elif line['name'] == 'LYRIC':
|
elif line.name == 'LYRIC':
|
||||||
pass
|
pass
|
||||||
elif line['name'] == 'NEXTSONG':
|
elif line.name == 'NEXTSONG':
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Not implemented commands
|
# Not implemented commands
|
||||||
@ -206,38 +207,33 @@ def parseCourseMeasures(lines):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
# Delete the last measure in the branch if no notes or events were added to it (due to preallocating empty measures)
|
# Delete the last measure in the branch if no notes or events were added to it (due to preallocating empty measures)
|
||||||
for branch in branches.values():
|
for branch in course.branches.values():
|
||||||
if not branch[-1]['data'] and not branch[-1]['events']:
|
if not branch[-1].notes and not branch[-1].events:
|
||||||
del branch[-1]
|
del branch[-1]
|
||||||
|
|
||||||
# Merge measure data and measure events in chronological order
|
# Merge measure data and measure events in chronological order
|
||||||
for branchName, branch in branches.items():
|
for branchName, branch in course.branches.items():
|
||||||
for measure in branch:
|
for measure in branch:
|
||||||
notes = [{'pos': i, 'type': 'note', 'value': TJA_NOTE_TYPES[note]}
|
notes = [TJAData('note', TJA_NOTE_TYPES[note], i)
|
||||||
for i, note in enumerate(measure['data']) if note != '0']
|
for i, note in enumerate(measure.notes) if note != '0']
|
||||||
events = [{'pos': e['position'], 'type': e['name'], 'value': e['value']}
|
events = measure.events
|
||||||
for e in measure['events']]
|
|
||||||
combined = []
|
|
||||||
while notes or events:
|
while notes or events:
|
||||||
if events and notes:
|
if events and notes:
|
||||||
if notes[0]['pos'] >= events[0]['pos']:
|
if notes[0].pos >= events[0].pos:
|
||||||
combined.append(events.pop(0))
|
measure.combined.append(events.pop(0))
|
||||||
else:
|
else:
|
||||||
combined.append(notes.pop(0))
|
measure.combined.append(notes.pop(0))
|
||||||
elif events:
|
elif events:
|
||||||
combined.append(events.pop(0))
|
measure.combined.append(events.pop(0))
|
||||||
elif notes:
|
elif notes:
|
||||||
combined.append(notes.pop(0))
|
measure.combined.append(notes.pop(0))
|
||||||
measure['combined'] = combined
|
|
||||||
|
|
||||||
# Ensure all branches have the same number of measures
|
# Ensure all branches have the same number of measures
|
||||||
if hasBranches:
|
if hasBranches:
|
||||||
branch_lens = [len(b) for b in branches.values()]
|
branch_lens = [len(b) for b in course.branches.values()]
|
||||||
if not branch_lens.count(branch_lens[0]) == len(branch_lens):
|
if not branch_lens.count(branch_lens[0]) == len(branch_lens):
|
||||||
raise ValueError("Branches do not have the same number of measures.")
|
raise ValueError("Branches do not have the same number of measures.")
|
||||||
|
|
||||||
return branches
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# Fumen-parsing functions
|
# Fumen-parsing functions
|
||||||
@ -275,18 +271,13 @@ def readFumen(fumenFile, exclude_empty_measures=False):
|
|||||||
totalMeasures = measuresLittle
|
totalMeasures = measuresLittle
|
||||||
|
|
||||||
# Initialize the dict that will contain the chart information
|
# Initialize the dict that will contain the chart information
|
||||||
song = {'measures': []}
|
song = FumenCourse(
|
||||||
song['headerPadding'] = fumenHeader[:432]
|
headerPadding=fumenHeader[:432],
|
||||||
song['headerMetadata'] = fumenHeader[-80:]
|
headerMetadata=fumenHeader[-80:],
|
||||||
song['order'] = order
|
order=order,
|
||||||
|
unknownMetadata=readStruct(file, order, format_string="I", seek=0x204)[0],
|
||||||
# I am unsure what byte this represents
|
hasBranches=getBool(readStruct(file, order, format_string="B", seek=0x1b0)[0])
|
||||||
unknownMetadata = readStruct(file, order, format_string="I", seek=0x204)[0]
|
)
|
||||||
song["unknownMetadata"] = unknownMetadata
|
|
||||||
|
|
||||||
# Determine whether the song has branches from byte 0x1b0 (decimal 432)
|
|
||||||
hasBranches = getBool(readStruct(file, order, format_string="B", seek=0x1b0)[0])
|
|
||||||
song["branches"] = hasBranches
|
|
||||||
|
|
||||||
# Start reading measure data from position 0x208 (decimal 520)
|
# Start reading measure data from position 0x208 (decimal 520)
|
||||||
file.seek(0x208)
|
file.seek(0x208)
|
||||||
@ -303,17 +294,18 @@ def readFumen(fumenFile, exclude_empty_measures=False):
|
|||||||
measureStruct = readStruct(file, order, format_string="ffBBHiiiiiii")
|
measureStruct = readStruct(file, order, format_string="ffBBHiiiiiii")
|
||||||
|
|
||||||
# Create the measure dictionary using the newly-parsed measure data
|
# Create the measure dictionary using the newly-parsed measure data
|
||||||
measure = {}
|
measure = FumenMeasure(
|
||||||
measure["bpm"] = measureStruct[0]
|
bpm=measureStruct[0],
|
||||||
measure["fumenOffsetStart"] = measureStruct[1]
|
fumenOffsetStart=measureStruct[1],
|
||||||
measure["gogo"] = getBool(measureStruct[2])
|
gogo=getBool(measureStruct[2]),
|
||||||
measure["barline"] = getBool(measureStruct[3])
|
barline=getBool(measureStruct[3]),
|
||||||
measure["padding1"] = measureStruct[4]
|
padding1=measureStruct[4],
|
||||||
measure["branchInfo"] = list(measureStruct[5:11])
|
branchInfo=list(measureStruct[5:11]),
|
||||||
measure["padding2"] = measureStruct[11]
|
padding2=measureStruct[11]
|
||||||
|
)
|
||||||
|
|
||||||
# Iterate through the three branch types
|
# Iterate through the three branch types
|
||||||
for branchNumber in range(len(branchNames)):
|
for branchName in branchNames:
|
||||||
# Parse the measure data using the following `format_string`:
|
# Parse the measure data using the following `format_string`:
|
||||||
# "HHf" (3 format characters, 8 bytes per branch)
|
# "HHf" (3 format characters, 8 bytes per branch)
|
||||||
# - 'H': totalNotes (represented by one unsigned short (2 bytes))
|
# - 'H': totalNotes (represented by one unsigned short (2 bytes))
|
||||||
@ -322,11 +314,12 @@ def readFumen(fumenFile, exclude_empty_measures=False):
|
|||||||
branchStruct = readStruct(file, order, format_string="HHf")
|
branchStruct = readStruct(file, order, format_string="HHf")
|
||||||
|
|
||||||
# Create the branch dictionary using the newly-parsed branch data
|
# Create the branch dictionary using the newly-parsed branch data
|
||||||
branch = {}
|
|
||||||
totalNotes = branchStruct[0]
|
totalNotes = branchStruct[0]
|
||||||
branch["length"] = totalNotes
|
branch = FumenBranch(
|
||||||
branch["padding"] = branchStruct[1]
|
length=totalNotes,
|
||||||
branch["speed"] = branchStruct[2]
|
padding=branchStruct[1],
|
||||||
|
speed=branchStruct[2],
|
||||||
|
)
|
||||||
|
|
||||||
# Iterate through each note in the measure (per branch)
|
# Iterate through each note in the measure (per branch)
|
||||||
for noteNumber in range(totalNotes):
|
for noteNumber in range(totalNotes):
|
||||||
@ -351,39 +344,41 @@ def readFumen(fumenFile, exclude_empty_measures=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create the note dictionary using the newly-parsed note data
|
# Create the note dictionary using the newly-parsed note data
|
||||||
note = {}
|
note = FumenNote(
|
||||||
note["type"] = noteTypes[noteType]
|
note_type=noteTypes[noteType],
|
||||||
note["pos"] = noteStruct[1]
|
pos=noteStruct[1],
|
||||||
note["item"] = noteStruct[2]
|
item=noteStruct[2],
|
||||||
note["padding"] = noteStruct[3]
|
padding=noteStruct[3],
|
||||||
|
)
|
||||||
|
|
||||||
if noteType == 0xa or noteType == 0xc:
|
if noteType == 0xa or noteType == 0xc:
|
||||||
# Balloon hits
|
# Balloon hits
|
||||||
note["hits"] = noteStruct[4]
|
note.hits = noteStruct[4]
|
||||||
note["hitsPadding"] = noteStruct[5]
|
note.hitsPadding = noteStruct[5]
|
||||||
else:
|
else:
|
||||||
note['scoreInit'] = noteStruct[4]
|
note.scoreInit = noteStruct[4]
|
||||||
note['scoreDiff'] = noteStruct[5] // 4
|
note.scoreDiff = noteStruct[5] // 4
|
||||||
if "scoreInit" not in song:
|
if not song.scoreInit:
|
||||||
song["scoreInit"] = note['scoreInit']
|
song.scoreInit = note.scoreInit
|
||||||
song["scoreDiff"] = note['scoreDiff']
|
song.scoreDiff = note.scoreDiff
|
||||||
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]
|
||||||
else:
|
else:
|
||||||
note['duration'] = noteStruct[6]
|
note.duration = noteStruct[6]
|
||||||
|
|
||||||
# Seek forward 8 bytes to account for padding bytes at the end of drumrolls
|
# Seek forward 8 bytes to account for padding bytes at the end of drumrolls
|
||||||
if noteType == 0x6 or noteType == 0x9 or noteType == 0x62:
|
if noteType == 0x6 or noteType == 0x9 or noteType == 0x62:
|
||||||
note["drumrollBytes"] = file.read(8)
|
note.drumrollBytes = file.read(8)
|
||||||
|
|
||||||
# Assign the note to the branch
|
# Assign the note to the branch
|
||||||
branch[noteNumber] = note
|
branch.notes.append(note)
|
||||||
|
|
||||||
# Assign the branch to the measure
|
# Assign the branch to the measure
|
||||||
measure[branchNames[branchNumber]] = branch
|
measure.branches[branchName] = branch
|
||||||
|
|
||||||
# Assign the measure to the song
|
# Assign the measure to the song
|
||||||
song['measures'].append(measure)
|
song.measures.append(measure)
|
||||||
if file.tell() >= size:
|
if file.tell() >= size:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -394,7 +389,7 @@ def readFumen(fumenFile, exclude_empty_measures=False):
|
|||||||
# So, in tests, if we want to only compare the timing of the non-empty measures between an official fumen and
|
# So, in tests, if we want to only compare the timing of the non-empty measures between an official fumen and
|
||||||
# a converted non-official TJA, then it's useful to exclude the empty measures.
|
# a converted non-official TJA, then it's useful to exclude the empty measures.
|
||||||
if exclude_empty_measures:
|
if exclude_empty_measures:
|
||||||
song['measures'] = [m for m in song['measures']
|
song.measures = [m for m in song.measures
|
||||||
if m['normal']['length'] or m['advanced']['length'] or m['master']['length']]
|
if m.branches['normal'].length or m.branches['advanced'].length or m.branches['master'].length]
|
||||||
|
|
||||||
return song
|
return song
|
||||||
|
140
src/tja2fumen/types.py
Normal file
140
src/tja2fumen/types.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
from tja2fumen.constants import sampleHeaderMetadata, simpleHeaders, TJA_COURSE_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
class TJASong:
|
||||||
|
def __init__(self, BPM=None, offset=None):
|
||||||
|
self.BPM = float(BPM)
|
||||||
|
self.offset = float(offset)
|
||||||
|
self.courses = {course: TJACourse(self.BPM, self.offset, course) for course in TJA_COURSE_NAMES}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{{'BPM': {self.BPM}, 'offset': {self.offset}, 'courses': {list(self.courses.keys())}}}"
|
||||||
|
|
||||||
|
|
||||||
|
class TJACourse:
|
||||||
|
def __init__(self, BPM, offset, course, level=0, balloon=None, scoreInit=0, scoreDiff=0):
|
||||||
|
self.level = level
|
||||||
|
self.balloon = [] if balloon is None else balloon
|
||||||
|
self.scoreInit = scoreInit
|
||||||
|
self.scoreDiff = scoreDiff
|
||||||
|
self.BPM = BPM
|
||||||
|
self.offset = offset
|
||||||
|
self.course = course
|
||||||
|
self.data = []
|
||||||
|
self.branches = {
|
||||||
|
'normal': [TJAMeasure()],
|
||||||
|
'advanced': [TJAMeasure()],
|
||||||
|
'master': [TJAMeasure()]
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__) if self.data else "{'data': []}"
|
||||||
|
|
||||||
|
|
||||||
|
class TJAMeasure:
|
||||||
|
def __init__(self, notes=None, events=None):
|
||||||
|
self.notes = [] if notes is None else notes
|
||||||
|
self.events = [] if events is None else events
|
||||||
|
self.combined = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class TJAMeasureProcessed:
|
||||||
|
def __init__(self, bpm, scroll, gogo, barline, time_sig, subdivisions,
|
||||||
|
pos_start=0, pos_end=0, delay=0, branchStart=None, data=None):
|
||||||
|
self.bpm = bpm
|
||||||
|
self.scroll = scroll
|
||||||
|
self.gogo = gogo
|
||||||
|
self.barline = barline
|
||||||
|
self.time_sig = time_sig
|
||||||
|
self.subdivisions = subdivisions
|
||||||
|
self.pos_start = pos_start
|
||||||
|
self.pos_end = pos_end
|
||||||
|
self.delay = delay
|
||||||
|
self.branchStart = branchStart
|
||||||
|
self.data = [] if data is None else data
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class TJAData:
|
||||||
|
def __init__(self, name, value, pos=None):
|
||||||
|
self.pos = pos
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class FumenCourse:
|
||||||
|
def __init__(self, measures=None, hasBranches=False, scoreInit=0, scoreDiff=0,
|
||||||
|
order='<', headerPadding=None, headerMetadata=None, unknownMetadata=0):
|
||||||
|
if isinstance(measures, int):
|
||||||
|
self.measures = [FumenMeasure() for _ in range(measures)]
|
||||||
|
else:
|
||||||
|
self.measures = [] if measures is None else measures
|
||||||
|
self.hasBranches = hasBranches
|
||||||
|
self.scoreInit = scoreInit
|
||||||
|
self.scoreDiff = scoreDiff
|
||||||
|
self.order = order
|
||||||
|
self.headerPadding = simpleHeaders.copy()[0] if headerPadding is None else headerPadding
|
||||||
|
self.headerMetadata = sampleHeaderMetadata.copy() if headerMetadata is None else headerMetadata
|
||||||
|
self.unknownMetadata = unknownMetadata
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class FumenMeasure:
|
||||||
|
def __init__(self, bpm=0.0, fumenOffsetStart=0.0, fumenOffsetEnd=0.0, duration=0.0,
|
||||||
|
gogo=False, barline=True, branchStart=None, branchInfo=None, padding1=0, padding2=0):
|
||||||
|
self.bpm = bpm
|
||||||
|
self.fumenOffsetStart = fumenOffsetStart
|
||||||
|
self.fumenOffsetEnd = fumenOffsetEnd
|
||||||
|
self.duration = duration
|
||||||
|
self.gogo = gogo
|
||||||
|
self.barline = barline
|
||||||
|
self.branchStart = branchStart
|
||||||
|
self.branchInfo = [-1, -1, -1, -1, -1, -1] if branchInfo is None else branchInfo
|
||||||
|
self.branches = {'normal': FumenBranch(), 'advanced': FumenBranch(), 'master': FumenBranch()}
|
||||||
|
self.padding1 = padding1
|
||||||
|
self.padding2 = padding2
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class FumenBranch:
|
||||||
|
def __init__(self, length=0, speed=0.0, padding=0):
|
||||||
|
self.length = length
|
||||||
|
self.speed = speed
|
||||||
|
self.padding = padding
|
||||||
|
self.notes = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
||||||
|
|
||||||
|
|
||||||
|
class FumenNote:
|
||||||
|
def __init__(self, note_type='', pos=0.0, scoreInit=0, scoreDiff=0, padding=0, item=0, duration=0.0,
|
||||||
|
multimeasure=False, hits=0, hitsPadding=0, drumrollBytes=b'\x00\x00\x00\x00\x00\x00\x00\x00'):
|
||||||
|
self.note_type = note_type
|
||||||
|
self.pos = pos
|
||||||
|
self.scoreInit = scoreInit
|
||||||
|
self.scoreDiff = scoreDiff
|
||||||
|
self.padding = padding
|
||||||
|
# TODO: Determine how to properly set the item byte (https://github.com/vivaria/tja2fumen/issues/17)
|
||||||
|
self.item = item
|
||||||
|
# These attributes are only used for drumrolls/balloons
|
||||||
|
self.duration = duration
|
||||||
|
self.multimeasure = multimeasure
|
||||||
|
self.hits = hits
|
||||||
|
self.hitsPadding = hitsPadding
|
||||||
|
self.drumrollBytes = drumrollBytes
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self.__dict__)
|
@ -4,58 +4,58 @@ from tja2fumen.constants import branchNames, typeNotes
|
|||||||
|
|
||||||
def writeFumen(path_out, song):
|
def writeFumen(path_out, song):
|
||||||
# Fetch the byte order (little/big endian)
|
# Fetch the byte order (little/big endian)
|
||||||
order = song['order']
|
order = song.order
|
||||||
|
|
||||||
# Write the header
|
# Write the header
|
||||||
file = open(path_out, "wb")
|
file = open(path_out, "wb")
|
||||||
file.write(song['headerPadding']) # Write header padding bytes
|
file.write(song.headerPadding) # Write header padding bytes
|
||||||
file.write(song['headerMetadata']) # Write header metadata bytes
|
file.write(song.headerMetadata) # Write header metadata bytes
|
||||||
|
|
||||||
# Preallocate space in the file
|
# Preallocate space in the file
|
||||||
len_metadata = 8
|
len_metadata = 8
|
||||||
len_measures = 0
|
len_measures = 0
|
||||||
for measureNumber in range(len(song['measures'])):
|
for measureNumber in range(len(song.measures)):
|
||||||
len_measures += 40
|
len_measures += 40
|
||||||
measure = song['measures'][measureNumber]
|
measure = song.measures[measureNumber]
|
||||||
for branchNumber in range(len(branchNames)):
|
for branchNumber in range(len(branchNames)):
|
||||||
len_measures += 8
|
len_measures += 8
|
||||||
branch = measure[branchNames[branchNumber]]
|
branch = measure.branches[branchNames[branchNumber]]
|
||||||
for noteNumber in range(branch['length']):
|
for noteNumber in range(branch.length):
|
||||||
len_measures += 24
|
len_measures += 24
|
||||||
note = branch[noteNumber]
|
note = branch.notes[noteNumber]
|
||||||
if note['type'].lower() == "drumroll":
|
if note.type.lower() == "drumroll":
|
||||||
len_measures += 8
|
len_measures += 8
|
||||||
file.write(b'\x00' * (len_metadata + len_measures))
|
file.write(b'\x00' * (len_metadata + len_measures))
|
||||||
|
|
||||||
# Write metadata
|
# Write metadata
|
||||||
writeStruct(file, order, format_string="B", value_list=[putBool(song['branches'])], seek=0x1b0)
|
writeStruct(file, order, format_string="B", value_list=[putBool(song.hasBranches)], seek=0x1b0)
|
||||||
writeStruct(file, order, format_string="I", value_list=[len(song['measures'])], seek=0x200)
|
writeStruct(file, order, format_string="I", value_list=[len(song.measures)], seek=0x200)
|
||||||
writeStruct(file, order, format_string="I", value_list=[song['unknownMetadata']], seek=0x204)
|
writeStruct(file, order, format_string="I", value_list=[song.unknownMetadata], seek=0x204)
|
||||||
|
|
||||||
# Write measure data
|
# Write measure data
|
||||||
file.seek(0x208)
|
file.seek(0x208)
|
||||||
for measureNumber in range(len(song['measures'])):
|
for measureNumber in range(len(song.measures)):
|
||||||
measure = song['measures'][measureNumber]
|
measure = song.measures[measureNumber]
|
||||||
measureStruct = [measure['bpm'], measure['fumenOffsetStart'], int(measure['gogo']), int(measure['barline'])]
|
measureStruct = [measure.bpm, measure.fumenOffsetStart, int(measure.gogo), int(measure.barline)]
|
||||||
measureStruct.extend([measure['padding1']] + measure['branchInfo'] + [measure['padding2']])
|
measureStruct.extend([measure.padding1] + measure.branchInfo + [measure.padding2])
|
||||||
writeStruct(file, order, format_string="ffBBHiiiiiii", value_list=measureStruct)
|
writeStruct(file, order, format_string="ffBBHiiiiiii", value_list=measureStruct)
|
||||||
|
|
||||||
for branchNumber in range(len(branchNames)):
|
for branchNumber in range(len(branchNames)):
|
||||||
branch = measure[branchNames[branchNumber]]
|
branch = measure.branches[branchNames[branchNumber]]
|
||||||
branchStruct = [branch['length'], branch['padding'], branch['speed']]
|
branchStruct = [branch.length, branch.padding, branch.speed]
|
||||||
writeStruct(file, order, format_string="HHf", value_list=branchStruct)
|
writeStruct(file, order, format_string="HHf", value_list=branchStruct)
|
||||||
|
|
||||||
for noteNumber in range(branch['length']):
|
for noteNumber in range(branch.length):
|
||||||
note = branch[noteNumber]
|
note = branch.notes[noteNumber]
|
||||||
noteStruct = [typeNotes[note['type']], note['pos'], note['item'], note['padding']]
|
noteStruct = [typeNotes[note.type], note.pos, note.item, note.padding]
|
||||||
# Balloon hits
|
# Balloon hits
|
||||||
if 'hits' in note.keys():
|
if note.hits:
|
||||||
noteStruct.extend([note["hits"], note['hitsPadding']])
|
noteStruct.extend([note.hits, note.hitsPadding])
|
||||||
else:
|
else:
|
||||||
noteStruct.extend([note['scoreInit'], note['scoreDiff'] * 4])
|
noteStruct.extend([note.scoreInit, note.scoreDiff * 4])
|
||||||
# Drumroll or balloon duration
|
# Drumroll or balloon duration
|
||||||
noteStruct.append(note['duration'])
|
noteStruct.append(note.duration)
|
||||||
writeStruct(file, order, format_string="ififHHf", value_list=noteStruct)
|
writeStruct(file, order, format_string="ififHHf", value_list=noteStruct)
|
||||||
if note['type'].lower() == "drumroll":
|
if note.type.lower() == "drumroll":
|
||||||
file.write(note['drumrollBytes'])
|
file.write(note.drumrollBytes)
|
||||||
file.close()
|
file.close()
|
||||||
|
@ -65,20 +65,20 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
|||||||
co_song = readFumen(path_out, exclude_empty_measures=True)
|
co_song = readFumen(path_out, exclude_empty_measures=True)
|
||||||
ca_song = readFumen(os.path.join(path_bin, os.path.basename(path_out)), exclude_empty_measures=True)
|
ca_song = readFumen(os.path.join(path_bin, os.path.basename(path_out)), exclude_empty_measures=True)
|
||||||
# 1. Check song headers
|
# 1. Check song headers
|
||||||
checkValidHeader(co_song['headerPadding']+co_song['headerMetadata'], strict=True)
|
checkValidHeader(co_song.headerPadding+co_song.headerMetadata, strict=True)
|
||||||
checkValidHeader(ca_song['headerPadding']+ca_song['headerMetadata'])
|
checkValidHeader(ca_song.headerPadding+ca_song.headerMetadata)
|
||||||
# 2. Check song metadata
|
# 2. Check song metadata
|
||||||
assert_song_property(co_song, ca_song, 'order')
|
assert_song_property(co_song, ca_song, 'order')
|
||||||
assert_song_property(co_song, ca_song, 'branches')
|
assert_song_property(co_song, ca_song, 'hasBranches')
|
||||||
assert_song_property(co_song, ca_song, 'scoreInit')
|
assert_song_property(co_song, ca_song, 'scoreInit')
|
||||||
assert_song_property(co_song, ca_song, 'scoreDiff')
|
assert_song_property(co_song, ca_song, 'scoreDiff')
|
||||||
# 3. Check measure data
|
# 3. Check measure data
|
||||||
for i_measure in range(max([len(co_song['measures']), len(ca_song['measures'])])):
|
for i_measure in range(max([len(co_song.measures), len(ca_song.measures)])):
|
||||||
# NB: We could assert that len(measures) is the same for both songs, then iterate through zipped measures.
|
# NB: We could assert that len(measures) is the same for both songs, then iterate through zipped measures.
|
||||||
# But, if there is a mismatched number of measures, we want to know _where_ it occurs. So, we let the
|
# But, if there is a mismatched number of measures, we want to know _where_ it occurs. So, we let the
|
||||||
# comparison go on using the max length of both songs until something else fails.
|
# comparison go on using the max length of both songs until something else fails.
|
||||||
co_measure = co_song['measures'][i_measure]
|
co_measure = co_song.measures[i_measure]
|
||||||
ca_measure = ca_song['measures'][i_measure]
|
ca_measure = ca_song.measures[i_measure]
|
||||||
# 3a. Check measure metadata
|
# 3a. Check measure metadata
|
||||||
assert_song_property(co_measure, ca_measure, 'bpm', i_measure, abs=0.01)
|
assert_song_property(co_measure, ca_measure, 'bpm', i_measure, abs=0.01)
|
||||||
assert_song_property(co_measure, ca_measure, 'fumenOffsetStart', i_measure, abs=0.15)
|
assert_song_property(co_measure, ca_measure, 'fumenOffsetStart', i_measure, abs=0.15)
|
||||||
@ -87,19 +87,19 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
|||||||
assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure)
|
assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure)
|
||||||
# 3b. Check measure notes
|
# 3b. Check measure notes
|
||||||
for i_branch in ['normal', 'advanced', 'master']:
|
for i_branch in ['normal', 'advanced', 'master']:
|
||||||
co_branch = co_measure[i_branch]
|
co_branch = co_measure.branches[i_branch]
|
||||||
ca_branch = ca_measure[i_branch]
|
ca_branch = ca_measure.branches[i_branch]
|
||||||
# NB: We check for branching before checking speed as fumens store speed changes even for empty branches
|
# NB: We check for branching before checking speed as fumens store speed changes even for empty branches
|
||||||
if co_branch['length'] == 0:
|
if co_branch.length == 0:
|
||||||
continue
|
continue
|
||||||
assert_song_property(co_branch, ca_branch, 'speed', i_measure, i_branch)
|
assert_song_property(co_branch, ca_branch, 'speed', i_measure, i_branch)
|
||||||
# NB: We could assert that len(notes) is the same for both songs, then iterate through zipped notes.
|
# NB: We could assert that len(notes) is the same for both songs, then iterate through zipped notes.
|
||||||
# But, if there is a mismatched number of notes, we want to know _where_ it occurs. So, we let the
|
# But, if there is a mismatched number of notes, we want to know _where_ it occurs. So, we let the
|
||||||
# comparison go on using the max length of both branches until something else fails.
|
# comparison go on using the max length of both branches until something else fails.
|
||||||
for i_note in range(max([co_branch['length'], ca_branch['length']])):
|
for i_note in range(max([co_branch.length, ca_branch.length])):
|
||||||
co_note = co_branch[i_note]
|
co_note = co_branch.notes[i_note]
|
||||||
ca_note = ca_branch[i_note]
|
ca_note = ca_branch.notes[i_note]
|
||||||
assert_song_property(co_note, ca_note, 'type', i_measure, i_branch, i_note, func=normalize_type)
|
assert_song_property(co_note, ca_note, 'note_type', i_measure, i_branch, i_note, func=normalize_type)
|
||||||
assert_song_property(co_note, ca_note, 'pos', i_measure, i_branch, i_note, abs=0.1)
|
assert_song_property(co_note, ca_note, 'pos', i_measure, i_branch, i_note, abs=0.1)
|
||||||
# NB: Drumroll duration doesn't always end exactly on a beat. Plus, TJA charters often eyeball
|
# NB: Drumroll duration doesn't always end exactly on a beat. Plus, TJA charters often eyeball
|
||||||
# drumrolls, leading them to be often off by a 1/4th/8th/16th/32th/etc. These charting errors
|
# drumrolls, leading them to be often off by a 1/4th/8th/16th/32th/etc. These charting errors
|
||||||
@ -110,7 +110,7 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
|||||||
assert_song_property(co_note, ca_note, 'duration', i_measure, i_branch, i_note, abs=25.0)
|
assert_song_property(co_note, ca_note, 'duration', i_measure, i_branch, i_note, abs=25.0)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
pass
|
pass
|
||||||
if ca_note['type'] not in ["Balloon", "Kusudama"]:
|
if ca_note.note_type not in ["Balloon", "Kusudama"]:
|
||||||
assert_song_property(co_note, ca_note, 'scoreInit', i_measure, i_branch, i_note)
|
assert_song_property(co_note, ca_note, 'scoreInit', i_measure, i_branch, i_note)
|
||||||
assert_song_property(co_note, ca_note, 'scoreDiff', i_measure, i_branch, i_note)
|
assert_song_property(co_note, ca_note, 'scoreDiff', i_measure, i_branch, i_note)
|
||||||
# NB: 'item' still needs to be implemented: https://github.com/vivaria/tja2fumen/issues/17
|
# NB: 'item' still needs to be implemented: https://github.com/vivaria/tja2fumen/issues/17
|
||||||
@ -124,12 +124,14 @@ def assert_song_property(converted_obj, cached_obj, prop, measure=None, branch=N
|
|||||||
msg_failure += f": measure '{measure+1}'" if measure is not None else ""
|
msg_failure += f": measure '{measure+1}'" if measure is not None else ""
|
||||||
msg_failure += f", branch '{branch}'" if branch is not None else ""
|
msg_failure += f", branch '{branch}'" if branch is not None else ""
|
||||||
msg_failure += f", note '{note+1}'" if note is not None else ""
|
msg_failure += f", note '{note+1}'" if note is not None else ""
|
||||||
|
converted_val = converted_obj.__getattribute__(prop)
|
||||||
|
cached_val = cached_obj.__getattribute__(prop)
|
||||||
if func:
|
if func:
|
||||||
assert func(converted_obj[prop]) == func(cached_obj[prop]), msg_failure
|
assert func(converted_val) == func(cached_val), msg_failure
|
||||||
elif abs:
|
elif abs:
|
||||||
assert converted_obj[prop] == pytest.approx(cached_obj[prop], abs=abs), msg_failure
|
assert converted_val == pytest.approx(cached_val, abs=abs), msg_failure
|
||||||
else:
|
else:
|
||||||
assert converted_obj[prop] == cached_obj[prop], msg_failure
|
assert converted_val == cached_val, msg_failure
|
||||||
|
|
||||||
|
|
||||||
def normalize_type(note_type):
|
def normalize_type(note_type):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user