1
0
mirror of synced 2025-01-24 07:04:09 +01:00

Simplify how #BPMCHANGEs 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:
Viv 2023-07-09 14:08:55 -04:00
parent 5664c7c8d0
commit 57edc7aecf
4 changed files with 34 additions and 22 deletions

View File

@ -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_branch = {'length': 0, 'padding': 0, 'speed': 1.0}
default_measure = { default_measure = {
'bpm': 0.0, 'bpm': 0.0,
'fumenOffsetStart': 0.0,
'fumenOffsetEnd': 0.0,
'duration': 0.0, 'duration': 0.0,
'fumenOffset': 0.0,
'gogo': False, 'gogo': False,
'barline': True, 'barline': True,
'padding1': 0, 'padding1': 0,
@ -135,6 +136,8 @@ def convertTJAToFumen(tja):
currentDrumroll = None currentDrumroll = None
courseBalloons = tja['metadata']['balloon'].copy() courseBalloons = tja['metadata']['balloon'].copy()
for idx_m, measureTJA in enumerate(branch): 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] measureFumen = tjaConverted['measures'][idx_m]
# Check to see if the measure contains a branching condition # 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] measureSize = measureTJA['time_sig'][0] / measureTJA['time_sig'][1]
measureLength = measureTJA['pos_end'] - measureTJA['pos_start'] measureLength = measureTJA['pos_end'] - measureTJA['pos_start']
measureRatio = 1.0 if measureTJA['subdivisions'] == 0.0 else (measureLength / measureTJA['subdivisions']) 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'] measureDurationFullMeasure = 4 * 60_000 / measureTJA['bpm']
measureDurationBase = measureDuration = (measureDurationFullMeasure * measureSize * measureRatio) # Adjust the duration based on both:
# The following adjustment accounts for BPM changes. (!!! Discovered by tana :3 !!!) # 1. Measure size (e.g. #MEASURE 1/8, #MEASURE 5/4, etc.)
if idx_m != len(branch)-1: # 2. Whether this is a "submeasure" (i.e. it contains mid-measure commands, splitting up the measure)
measureTJANext = branch[idx_m + 1] measureFumen['duration'] = measureDuration = measureDurationFullMeasure * measureSize * measureRatio
if measureTJA['bpm'] != measureTJANext['bpm']:
measureDuration -= (4 * 60_000 * ((1 / measureTJANext['bpm']) - (1 / measureTJA['bpm'])))
measureFumen['duration'] = measureDuration
# 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: if idx_m == 0:
tjaOffset = float(tja['metadata']['offset']) * 1000 * -1 tjaOffset = float(tja['metadata']['offset']) * 1000 * -1
tjaConverted['measures'][idx_m]['fumenOffset'] = tjaOffset - measureDurationFullMeasure measureFumen['fumenOffsetStart'] = tjaOffset - measureDurationFullMeasure
else: else:
# Use the previous measure's offset plus the previous duration to compute the current measure's offset # Start the measure using the end timing of the previous measure (plus any #DELAY commands)
measureFumenPrev = tjaConverted['measures'][idx_m-1] measureFumen['fumenOffsetStart'] = measureFumenPrev['fumenOffsetEnd'] + measureTJA['delay']
measureFumen['fumenOffset'] = (measureFumenPrev['fumenOffset'] + measureFumenPrev['duration'] # Adjust the start of this measure to account for #BPMCHANGE commands (!!! Discovered by tana :3 !!!)
+ measureTJA['delay']) # 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: # 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
@ -194,7 +206,7 @@ def convertTJAToFumen(tja):
if data['type'] == 'note': if data['type'] == 'note':
# 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 = 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 # 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
@ -244,10 +256,10 @@ def convertTJAToFumen(tja):
# 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'] += (measureDurationBase - currentDrumroll['pos']) currentDrumroll['duration'] += (measureDuration - currentDrumroll['pos'])
currentDrumroll['multimeasure'] = True currentDrumroll['multimeasure'] = True
else: else:
currentDrumroll['duration'] += measureDurationBase currentDrumroll['duration'] += measureDuration
total_notes += note_counter total_notes += note_counter

View File

@ -303,7 +303,7 @@ def readFumen(fumenFile, exclude_empty_measures=False):
# Create the measure dictionary using the newly-parsed measure data # Create the measure dictionary using the newly-parsed measure data
measure = {} measure = {}
measure["bpm"] = measureStruct[0] measure["bpm"] = measureStruct[0]
measure["fumenOffset"] = measureStruct[1] measure["fumenOffsetStart"] = measureStruct[1]
# if measureNumber == 0: # if measureNumber == 0:
# measure["offset"] = measure["fumenOffset"] + 240000 / measure["bpm"] # measure["offset"] = measure["fumenOffset"] + 240000 / measure["bpm"]
# else: # else:

View File

@ -36,7 +36,7 @@ def writeFumen(path_out, song):
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['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']]) 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)

View File

@ -79,7 +79,7 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
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, '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, 'gogo', i_measure)
assert_song_property(co_measure, ca_measure, 'barline', i_measure) assert_song_property(co_measure, ca_measure, 'barline', i_measure)
assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure) assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure)