Simplify how #BPMCHANGE
s adjust the fumenOffset
Before: - If there was a #BPMCHANGE, tana's adjustment would be applied to _the current measure's duration_. This would result in very strange looking, impossible durations (e.g. negative) - Additionally, this method required us to look ahead to the _next_ measure to determine the adjustment. - Finally, it required us to keep track of two different durations: measureDurationBase (unadjusted) and measureDuration (adjusted) After: - Instead, we now leave the measureDuration as purely as "unadjusted". - Then, we apply the "BPMCHANGE adjustment" _only_ when computing the fumenOffset start of the next measure. - This requires us to keep track of both the start *and* end fumenOffset (where end = start + duration) - However, this greatly simplifies and clarifies the code, since we: - No longer need to "look ahead" to the next measure to compute the offset adjustment. - No longer need to keep track of two different measureDurations (adjusted/unadjusted) - Only need to work with a single pair of measures at a time (measureFumen, measureFumenPrev) - The measure duration (and fumenOffsetEnd) times are now more comprehensible, since any negative offsets are only applied to the fumenOffsetStart value of the next measure. After:
This commit is contained in:
parent
5664c7c8d0
commit
57edc7aecf
@ -11,8 +11,9 @@ default_note = {'type': '', 'pos': 0.0, 'item': 0, 'padding': 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,
|
||||
'fumenOffset': 0.0,
|
||||
'gogo': False,
|
||||
'barline': True,
|
||||
'padding1': 0,
|
||||
@ -135,6 +136,8 @@ def convertTJAToFumen(tja):
|
||||
currentDrumroll = None
|
||||
courseBalloons = tja['metadata']['balloon'].copy()
|
||||
for idx_m, measureTJA in enumerate(branch):
|
||||
# Fetch a pair of measures
|
||||
measureFumenPrev = tjaConverted['measures'][idx_m-1] if idx_m != 0 else None
|
||||
measureFumen = tjaConverted['measures'][idx_m]
|
||||
|
||||
# Check to see if the measure contains a branching condition
|
||||
@ -157,26 +160,35 @@ def convertTJAToFumen(tja):
|
||||
measureSize = measureTJA['time_sig'][0] / measureTJA['time_sig'][1]
|
||||
measureLength = measureTJA['pos_end'] - measureTJA['pos_start']
|
||||
measureRatio = 1.0 if measureTJA['subdivisions'] == 0.0 else (measureLength / measureTJA['subdivisions'])
|
||||
# - measureDurationBase: The "base" measure duration, computed using a single BPM value.
|
||||
# - measureDuration: The actual measure duration, which may be adjusted if there is a mid-measure BPM change.
|
||||
measureDurationFullMeasure = 4 * 60_000 / measureTJA['bpm']
|
||||
measureDurationBase = measureDuration = (measureDurationFullMeasure * measureSize * measureRatio)
|
||||
# The following adjustment accounts for BPM changes. (!!! Discovered by tana :3 !!!)
|
||||
if idx_m != len(branch)-1:
|
||||
measureTJANext = branch[idx_m + 1]
|
||||
if measureTJA['bpm'] != measureTJANext['bpm']:
|
||||
measureDuration -= (4 * 60_000 * ((1 / measureTJANext['bpm']) - (1 / measureTJA['bpm'])))
|
||||
measureFumen['duration'] = measureDuration
|
||||
# Adjust the duration based on both:
|
||||
# 1. Measure size (e.g. #MEASURE 1/8, #MEASURE 5/4, etc.)
|
||||
# 2. Whether this is a "submeasure" (i.e. it contains mid-measure commands, splitting up the measure)
|
||||
measureFumen['duration'] = measureDuration = measureDurationFullMeasure * measureSize * measureRatio
|
||||
|
||||
# Compute the millisecond offset for 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)
|
||||
# - End: When the notes arrive at the judgment line, and the note gets hit.
|
||||
if idx_m == 0:
|
||||
tjaOffset = float(tja['metadata']['offset']) * 1000 * -1
|
||||
tjaConverted['measures'][idx_m]['fumenOffset'] = tjaOffset - measureDurationFullMeasure
|
||||
measureFumen['fumenOffsetStart'] = tjaOffset - measureDurationFullMeasure
|
||||
else:
|
||||
# Use the previous measure's offset plus the previous duration to compute the current measure's offset
|
||||
measureFumenPrev = tjaConverted['measures'][idx_m-1]
|
||||
measureFumen['fumenOffset'] = (measureFumenPrev['fumenOffset'] + measureFumenPrev['duration']
|
||||
+ measureTJA['delay'])
|
||||
# Start the measure using the end timing of the previous measure (plus any #DELAY commands)
|
||||
measureFumen['fumenOffsetStart'] = measureFumenPrev['fumenOffsetEnd'] + measureTJA['delay']
|
||||
# Adjust the start of this measure to account for #BPMCHANGE commands (!!! Discovered by tana :3 !!!)
|
||||
# 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.
|
||||
# * All the other notes move fast (i.e. high BPM), moving past the big slow note.
|
||||
# * To get this overlapping to work, you need the big slow note to START EARLY, but also END LATE:
|
||||
# - 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`,
|
||||
# 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'])
|
||||
# - When we subtract this adjustment from the fumenOffsetStart, we get the "START EARLY" part:
|
||||
measureFumen['fumenOffsetStart'] -= measureOffsetAdjustment
|
||||
# - 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:
|
||||
measureFumen['fumenOffsetEnd'] = measureFumen['fumenOffsetStart'] + measureFumen['duration']
|
||||
|
||||
# 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
|
||||
@ -194,7 +206,7 @@ def convertTJAToFumen(tja):
|
||||
if data['type'] == 'note':
|
||||
# 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.)
|
||||
note_pos = measureDurationBase * (data['pos'] - measureTJA['pos_start']) / measureLength
|
||||
note_pos = measureDuration * (data['pos'] - measureTJA['pos_start']) / measureLength
|
||||
# Handle the note that represents the end of a drumroll/balloon
|
||||
if data['value'] == "EndDRB":
|
||||
# If a drumroll spans a single measure, then add the difference between start/end position
|
||||
@ -244,10 +256,10 @@ def convertTJAToFumen(tja):
|
||||
# If drumroll hasn't ended by the end of this measure, increase duration by measure timing
|
||||
if currentDrumroll:
|
||||
if currentDrumroll['duration'] == 0.0:
|
||||
currentDrumroll['duration'] += (measureDurationBase - currentDrumroll['pos'])
|
||||
currentDrumroll['duration'] += (measureDuration - currentDrumroll['pos'])
|
||||
currentDrumroll['multimeasure'] = True
|
||||
else:
|
||||
currentDrumroll['duration'] += measureDurationBase
|
||||
currentDrumroll['duration'] += measureDuration
|
||||
|
||||
total_notes += note_counter
|
||||
|
||||
|
@ -303,7 +303,7 @@ def readFumen(fumenFile, exclude_empty_measures=False):
|
||||
# Create the measure dictionary using the newly-parsed measure data
|
||||
measure = {}
|
||||
measure["bpm"] = measureStruct[0]
|
||||
measure["fumenOffset"] = measureStruct[1]
|
||||
measure["fumenOffsetStart"] = measureStruct[1]
|
||||
# if measureNumber == 0:
|
||||
# measure["offset"] = measure["fumenOffset"] + 240000 / measure["bpm"]
|
||||
# else:
|
||||
|
@ -36,7 +36,7 @@ def writeFumen(path_out, song):
|
||||
file.seek(0x208)
|
||||
for measureNumber in range(len(song['measures'])):
|
||||
measure = song['measures'][measureNumber]
|
||||
measureStruct = [measure['bpm'], measure['fumenOffset'], 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']])
|
||||
writeStruct(file, order, format_string="ffBBHiiiiiii", value_list=measureStruct)
|
||||
|
||||
|
@ -79,7 +79,7 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
||||
ca_measure = ca_song['measures'][i_measure]
|
||||
# 3a. Check measure metadata
|
||||
assert_song_property(co_measure, ca_measure, 'bpm', i_measure, abs=0.01)
|
||||
assert_song_property(co_measure, ca_measure, 'fumenOffset', i_measure, abs=0.15)
|
||||
assert_song_property(co_measure, ca_measure, 'fumenOffsetStart', i_measure, abs=0.15)
|
||||
assert_song_property(co_measure, ca_measure, 'gogo', i_measure)
|
||||
assert_song_property(co_measure, ca_measure, 'barline', i_measure)
|
||||
assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure)
|
||||
|
Loading…
x
Reference in New Issue
Block a user