1
0
mirror of synced 2024-11-23 21:20:56 +01:00

Add support for #SENOTECHANGE command (#81)

TJAPlayer3 implementation details
(https://github.com/IepIweidieng/TJAPlayer3/blob/gh-pages/tja.md#senotechange):

- `#SENOTECHANGE` can be placed at the start of a measure, or in the
middle of the measure
- It will only apply to 1 note immediately after the command
- It will override any automatic Don2/Don3/Ka2 assignment (from note
clusters)

How it works in `tja2fumen`:

- `#SENOTECHANGE` will be tracked in the TJA measure object
- It will split the measure into sub-measures (similar to gogo/bpm/etc.)
- It will only be applied to the first note, after which it will be
overwritten
- We set a `manually_set` flag to ensure that the manually-chosen value
doesn't get overwritten.

Fixes #69.
This commit is contained in:
Viv 2024-10-30 00:12:26 -04:00 committed by GitHub
parent 9b6f05b420
commit 85ea4d6efc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 87 additions and 30 deletions

View File

@ -144,13 +144,14 @@ If there is an unsupported feature that you would like support for, please make
> **Legend**: `✅` = Fully supported, `⚪️` = Ignored, `⚠️` = Incorrect behavior, `❌` = Not supported > **Legend**: `✅` = Fully supported, `⚪️` = Ignored, `⚠️` = Incorrect behavior, `❌` = Not supported
| | tja 2 fumen | tja 2 bin | Comment | | | tja 2 fumen | tja 2 bin | Comment |
|----------------------------------------------|-------------|-----------|---------------------------------------------------------------------------------------------------------------| |----------------------------------------------|-----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| `0`, `1`, `2`, `3`, `4` | `✅` | `⚠️` | tja2fumen will write proper SENOTES (ド, コ, ドン, カ, カッ), see [#41](https://github.com/vivaria/tja2fumen/issues/41). | | `0`, `1`, `2`, `3`, `4` | `✅` | `⚠️` | tja2fumen will write proper SENOTES (ド, コ, ドン, カ, カッ), see [#41](https://github.com/vivaria/tja2fumen/issues/41). |
| `5008,`, `6008,`, `7008,` | `✅` | `✅` | | | `5008,`, `6008,`, `7008,` | `✅` | `✅` | |
| `9008,` | `✅` | `⚠️` | | | `9008,` | `✅` | `⚠️` | |
| `9000,`<br>`9008,` | `⚪️` | `⚠️` | Double Kusudama note treated as 1 drumroll by tja2fumen, but 2 overlapping drumrolls by tja2bin. | | `9000,`<br>`9008,` | `⚪️` | `⚠️` | Double Kusudama note treated as 1 drumroll by tja2fumen, but 2 overlapping drumrolls by tja2bin. |
| `A`, `B` | `✅` | `❌` | Multiplayer "hands" notes are valid in fumens, but unrecognized by tja2bin. | | `A`, `B` | `✅` | `❌` | Multiplayer "hands" notes are valid in fumens, but unrecognized by tja2bin. |
| `C`, `D`, `E`, `F`, `G`, `H`, `I` | `⚠️` | `❌` | Replaced by normal notes/rolls in tja2fumen. | | `C`, `D`, `E`, `F`, `G`, `H`, `I` | `⚠️` | `❌` | Replaced by normal notes/rolls in tja2fumen. |
| `#SENOTECHANGE` | `✅` | `❌` | Recently added. See [#69](https://github.com/vivaria/tja2fumen/issues/69) and [#81](https://github.com/vivaria/tja2fumen/issues/81) for details. |
| `#START`, `#END` | `✅` | `✅` | | | `#START`, `#END` | `✅` | `✅` | |
| `#START P1`, `#START P2` | `✅` | `❌` | | | `#START P1`, `#START P2` | `✅` | `❌` | |
| `#BPMCHANGE` | `✅` | `⚠️` | See [#16](https://github.com/Fluto/TakoTako/issues/16) | | `#BPMCHANGE` | `✅` | `⚠️` | See [#16](https://github.com/Fluto/TakoTako/issues/16) |

View File

@ -72,6 +72,7 @@ class TJAMeasureProcessed:
delay: float = 0.0 delay: float = 0.0
section: bool = False section: bool = False
levelhold: bool = False levelhold: bool = False
senote: str = ''
branch_type: str = '' branch_type: str = ''
branch_cond: Tuple[float, float] = (0.0, 0.0) branch_cond: Tuple[float, float] = (0.0, 0.0)
notes: List[TJAData] = field(default_factory=list) notes: List[TJAData] = field(default_factory=list)
@ -93,6 +94,7 @@ class FumenNote:
hits: int = 0 hits: int = 0
hits_padding: int = 0 hits_padding: int = 0
drumroll_bytes: bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00' drumroll_bytes: bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00'
manually_set: bool = False
@dataclass() @dataclass()

View File

@ -28,6 +28,15 @@ TJA_NOTE_TYPES = {
'I': 'Drumroll', # green roll 'I': 'Drumroll', # green roll
} }
# Conversion for TJAPlayer3's #SENOTECHANGE command
SENOTECHANGE_TYPES = {
1: "Don", # ドン
2: "Don2", # ド
3: "Don3", # コ
4: "Ka", # カッ
5: "Ka2", # カ
}
# Types of notes that can be found in fumen files # Types of notes that can be found in fumen files
FUMEN_NOTE_TYPES = { FUMEN_NOTE_TYPES = {
0x1: "Don", # ドン 0x1: "Don", # ドン

View File

@ -9,7 +9,7 @@ from typing import List, Dict, Tuple, Union
from tja2fumen.classes import (TJACourse, TJAMeasure, TJAMeasureProcessed, from tja2fumen.classes import (TJACourse, TJAMeasure, TJAMeasureProcessed,
FumenCourse, FumenHeader, FumenMeasure, FumenCourse, FumenHeader, FumenMeasure,
FumenNote) FumenNote)
from tja2fumen.constants import BRANCH_NAMES from tja2fumen.constants import BRANCH_NAMES, SENOTECHANGE_TYPES
def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
@ -38,6 +38,7 @@ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
current_scroll = 1.0 current_scroll = 1.0
current_gogo = False current_gogo = False
current_barline = True current_barline = True
current_senote = ""
current_dividend = 4 current_dividend = 4
current_divisor = 4 current_divisor = 4
for measure_tja in branch_measures_tja: for measure_tja in branch_measures_tja:
@ -95,15 +96,18 @@ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
# to BPM/SCROLL/GOGO, then the measure will actually be split # to BPM/SCROLL/GOGO, then the measure will actually be split
# into two small submeasures. So, we need to start a new # into two small submeasures. So, we need to start a new
# measure in those cases.) # measure in those cases.)
elif data.name in ['bpm', 'scroll', 'gogo']: elif data.name in ['bpm', 'scroll', 'gogo', 'senote']:
# Parse the values # Parse the values
new_val: Union[bool, float] new_val: Union[bool, float, str]
if data.name == 'bpm': if data.name == 'bpm':
new_val = current_bpm = float(data.value) new_val = current_bpm = float(data.value)
elif data.name == 'scroll': elif data.name == 'scroll':
new_val = current_scroll = float(data.value) new_val = current_scroll = float(data.value)
elif data.name == 'gogo': elif data.name == 'gogo':
new_val = current_gogo = bool(int(data.value)) new_val = current_gogo = bool(int(data.value))
elif data.name == 'senote':
new_val = current_senote \
= SENOTECHANGE_TYPES[int(data.value)]
# Check for mid-measure commands # Check for mid-measure commands
# - Case 1: Command happens at the start of a measure; # - Case 1: Command happens at the start of a measure;
# just change the value directly # just change the value directly
@ -123,8 +127,13 @@ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
barline=current_barline, barline=current_barline,
time_sig=[current_dividend, current_divisor], time_sig=[current_dividend, current_divisor],
subdivisions=len(measure_tja.notes), subdivisions=len(measure_tja.notes),
pos_start=data.pos pos_start=data.pos,
senote=current_senote
) )
# SENOTECHANGE commands don't carry over to next branch.
# (But they CAN happen mid-measure, which is why we
# process them here.)
current_senote = ""
else: else:
warnings.warn(f"Unexpected event type: {data.name}") warnings.warn(f"Unexpected event type: {data.name}")
@ -327,6 +336,14 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
# we can initialize a note and handle general note metadata. # we can initialize a note and handle general note metadata.
note = FumenNote() note = FumenNote()
note.pos = note_pos note.pos = note_pos
# Account for a measure's #SENOTECHANGE command
if measure_tja.senote:
note.note_type = measure_tja.senote
note.manually_set = True
# SENOTECHANGE only applies to the note immediately after
# So, we erase it once it's been applied.
measure_tja.senote = ""
else:
note.note_type = note_tja.value note.note_type = note_tja.value
note.score_init = tja.score_init note.score_init = tja.score_init
note.score_diff = tja.score_diff note.score_diff = tja.score_diff
@ -508,12 +525,13 @@ def replace_alternate_don_kas(note_clusters: List[List[FumenNote]],
positions within a cluster of notes. positions within a cluster of notes.
NB: Modifies FumenNote objects in-place NB: Modifies FumenNote objects in-place
NB: FumenNote values are only updated if not manually set by #SENOTECHANGE
""" """
big_notes = ['DON', 'DON2', 'KA', 'KA2'] big_notes = ['DON', 'DON2', 'KA', 'KA2']
for cluster in note_clusters: for cluster in note_clusters:
# Replace all small notes with the basic do/ka notes ("Don2", "Ka2") # Replace all small notes with the basic do/ka notes ("Don2", "Ka2")
for note in cluster: for note in cluster:
if note.note_type not in big_notes: if note.note_type not in big_notes and not note.manually_set:
if note.note_type[-1].isdigit(): if note.note_type[-1].isdigit():
note.note_type = note.note_type[:-1] + "2" note.note_type = note.note_type[:-1] + "2"
else: else:
@ -524,7 +542,8 @@ def replace_alternate_don_kas(note_clusters: List[List[FumenNote]],
all_dons = all(note.note_type.startswith("Don") for note in cluster) all_dons = all(note.note_type.startswith("Don") for note in cluster)
for i, note in enumerate(cluster): for i, note in enumerate(cluster):
if (all_dons and (len(cluster) % 2 == 1) and (i % 2 == 1) if (all_dons and (len(cluster) % 2 == 1) and (i % 2 == 1)
and note.note_type not in big_notes): and note.note_type not in big_notes
and not note.manually_set):
note.note_type = "Don3" note.note_type = "Don3"
# Replace the last note in a cluster with the ending Don/Kat # Replace the last note in a cluster with the ending Don/Kat
@ -538,7 +557,8 @@ def replace_alternate_don_kas(note_clusters: List[List[FumenNote]],
pass pass
else: else:
# Replace last Don2/Ka2 with Don/Ka # Replace last Don2/Ka2 with Don/Ka
if cluster[-1].note_type not in big_notes: if (cluster[-1].note_type not in big_notes
and not cluster[-1].manually_set):
cluster[-1].note_type = cluster[-1].note_type[:-1] cluster[-1].note_type = cluster[-1].note_type[:-1]

View File

@ -262,7 +262,8 @@ def parse_tja_course_data(data: List[str]) \
# 2. Parse measure commands that produce an "event" # 2. Parse measure commands that produce an "event"
elif command in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF', elif command in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF',
'DELAY', 'SCROLL', 'BPMCHANGE', 'MEASURE', 'DELAY', 'SCROLL', 'BPMCHANGE', 'MEASURE',
'LEVELHOLD', 'SECTION', 'BRANCHSTART']: 'LEVELHOLD', 'SENOTECHANGE', 'SECTION',
'BRANCHSTART']:
# Get position of the event # Get position of the event
pos = 0 pos = 0
for branch_name in (BRANCH_NAMES if current_branch == 'all' for branch_name in (BRANCH_NAMES if current_branch == 'all'
@ -290,6 +291,8 @@ def parse_tja_course_data(data: List[str]) \
name = 'measure' name = 'measure'
elif command == 'LEVELHOLD': elif command == 'LEVELHOLD':
name = 'levelhold' name = 'levelhold'
elif command == "SENOTECHANGE":
name = 'senote'
elif command == 'SECTION': elif command == 'SECTION':
# If #SECTION occurs before a #BRANCHSTART, then ensure that # If #SECTION occurs before a #BRANCHSTART, then ensure that
# it's present on every branch. Otherwise, #SECTION will only # it's present on every branch. Otherwise, #SECTION will only

View File

@ -0,0 +1,21 @@
// This song contains only basic notes.
BPM:120
OFFSET:-1.00
COURSE:Oni
LEVEL:10
BALLOON:8,8
SCOREINIT:400
SCOREDIFF:100
#START
#SENOTECHANGE 3
1
#SENOTECHANGE 2
111111,
1020304,
5000008,
6000008,
7000008,
9000008,
#END

View File

@ -13,6 +13,7 @@ from conftest import convert
['notes_double_kusudama', None], ['notes_double_kusudama', None],
['notes_hands', None], ['notes_hands', None],
['notes_sim_only', None], ['notes_sim_only', None],
['notes_senotechange', None],
['missing_score', None], ['missing_score', None],
['missing_balloon', "UserWarning"], ['missing_balloon', "UserWarning"],
['missing_course', "Invalid COURSE value:"], ['missing_course', "Invalid COURSE value:"],