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:
parent
9b6f05b420
commit
85ea4d6efc
43
README.md
43
README.md
@ -143,27 +143,28 @@ 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. |
|
||||||
| `#START`, `#END` | `✅` | `✅` | |
|
| `#SENOTECHANGE` | `✅` | `❌` | Recently added. See [#69](https://github.com/vivaria/tja2fumen/issues/69) and [#81](https://github.com/vivaria/tja2fumen/issues/81) for details. |
|
||||||
| `#START P1`, `#START P2` | `✅` | `❌` | |
|
| `#START`, `#END` | `✅` | `✅` | |
|
||||||
| `#BPMCHANGE` | `✅` | `⚠️` | See [#16](https://github.com/Fluto/TakoTako/issues/16) |
|
| `#START P1`, `#START P2` | `✅` | `❌` | |
|
||||||
| `#MEASURE` | `✅` | `✅` | |
|
| `#BPMCHANGE` | `✅` | `⚠️` | See [#16](https://github.com/Fluto/TakoTako/issues/16) |
|
||||||
| `#SCROLL` | `✅` | `✅` | |
|
| `#MEASURE` | `✅` | `✅` | |
|
||||||
| `#GOGOSTART`, `#GOGOEND` | `✅` | `✅` | |
|
| `#SCROLL` | `✅` | `✅` | |
|
||||||
| `#BARLINEOFF`, `#BARLINEON` | `✅` | `✅` | |
|
| `#GOGOSTART`, `#GOGOEND` | `✅` | `✅` | |
|
||||||
| `#DELAY` | `✅` | `❌` | See [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
| `#BARLINEOFF`, `#BARLINEON` | `✅` | `✅` | |
|
||||||
| `#BRANCHSTART`, `#BRANCHEND` | `✅` | `✅` | |
|
| `#DELAY` | `✅` | `❌` | See [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
||||||
| `#N`, `#E`, `#M` | `✅` | `✅` | |
|
| `#BRANCHSTART`, `#BRANCHEND` | `✅` | `✅` | |
|
||||||
| `#SECTION` | `⚠️` | `❌` | See [#53](https://github.com/vivaria/tja2fumen/issues/53), [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
| `#N`, `#E`, `#M` | `✅` | `✅` | |
|
||||||
| `#LEVELHOLD` | `✅` | `❌` | |
|
| `#SECTION` | `⚠️` | `❌` | See [#53](https://github.com/vivaria/tja2fumen/issues/53), [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
||||||
| `#BMSCROLL`, `#LYRIC`,<br>`#DIRECTION`, etc. | `⚪️` | `❌` | Other simulator-specific chart commands are currently ignored. |
|
| `#LEVELHOLD` | `✅` | `❌` | |
|
||||||
|
| `#BMSCROLL`, `#LYRIC`,<br>`#DIRECTION`, etc. | `⚪️` | `❌` | Other simulator-specific chart commands are currently ignored. |
|
||||||
|
|
||||||
## Reporting bugs
|
## Reporting bugs
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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", # ドン
|
||||||
|
@ -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,7 +336,15 @@ 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
|
||||||
note.note_type = note_tja.value
|
# 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.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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
21
testing/data/dummy_tjas/notes_senotechange.tja
Normal file
21
testing/data/dummy_tjas/notes_senotechange.tja
Normal 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
|
@ -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:"],
|
||||||
|
Loading…
Reference in New Issue
Block a user