Add support for HAKUs for konami formats
This commit is contained in:
parent
f5f458bf6d
commit
15b690619b
@ -1,7 +1,9 @@
|
|||||||
# v1.4.0
|
# v1.4.0
|
||||||
## Added
|
## Added
|
||||||
- Jubeatools can now handle HAKUs, in the following formats :
|
- Jubeatools can now handle HAKUs in the following formats :
|
||||||
- [memon:v1.0.0]
|
- [memon:v1.0.0]
|
||||||
|
- [eve]
|
||||||
|
- [jbsq]
|
||||||
- [memon]
|
- [memon]
|
||||||
- 🎉 inital support for v1.0.0 !
|
- 🎉 inital support for v1.0.0 !
|
||||||
- `--merge` option allows for several memon files to be merged when
|
- `--merge` option allows for several memon files to be merged when
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import math
|
import math
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from functools import singledispatch
|
from functools import singledispatch
|
||||||
from typing import List
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
from more_itertools import numeric_range
|
from more_itertools import numeric_range
|
||||||
|
|
||||||
@ -11,10 +11,12 @@ from jubeatools.formats.timemap import TimeMap
|
|||||||
from .commons import AnyNote, Command, Event, bpm_to_value, ticks_at_beat
|
from .commons import AnyNote, Command, Event, bpm_to_value, ticks_at_beat
|
||||||
|
|
||||||
|
|
||||||
def make_events_from_chart(notes: List[AnyNote], timing: song.Timing) -> List[Event]:
|
def make_events_from_chart(
|
||||||
|
notes: List[AnyNote], timing: song.Timing, hakus: Optional[Set[song.BeatsTime]]
|
||||||
|
) -> List[Event]:
|
||||||
time_map = TimeMap.from_timing(timing)
|
time_map = TimeMap.from_timing(timing)
|
||||||
note_events = make_note_events(notes, time_map)
|
note_events = make_note_events(notes, time_map)
|
||||||
timing_events = make_timing_events(notes, timing, time_map)
|
timing_events = make_timing_events(notes, timing, hakus, time_map)
|
||||||
return sorted(note_events + timing_events)
|
return sorted(note_events + timing_events)
|
||||||
|
|
||||||
|
|
||||||
@ -38,14 +40,21 @@ def make_long_note_event(note: song.LongNote, time_map: TimeMap) -> Event:
|
|||||||
|
|
||||||
|
|
||||||
def make_timing_events(
|
def make_timing_events(
|
||||||
notes: List[AnyNote], timing: song.Timing, time_map: TimeMap
|
notes: List[AnyNote],
|
||||||
|
timing: song.Timing,
|
||||||
|
hakus: Optional[Set[song.BeatsTime]],
|
||||||
|
time_map: TimeMap,
|
||||||
) -> List[Event]:
|
) -> List[Event]:
|
||||||
bpm_events = [make_bpm_event(e, time_map) for e in timing.events]
|
bpm_events = [make_bpm_event(e, time_map) for e in timing.events]
|
||||||
end_beat = choose_end_beat(notes)
|
end_beat = choose_end_beat(notes)
|
||||||
end_event = make_end_event(end_beat, time_map)
|
end_event = make_end_event(end_beat, time_map)
|
||||||
measure_events = make_measure_events(end_beat, time_map)
|
measure_events = make_measure_events(end_beat, time_map)
|
||||||
beat_events = make_beat_events(end_beat, time_map)
|
if hakus is not None:
|
||||||
return bpm_events + measure_events + beat_events + [end_event]
|
haku_events = dump_hakus(hakus, time_map)
|
||||||
|
else:
|
||||||
|
haku_events = make_regular_hakus(end_beat, time_map)
|
||||||
|
|
||||||
|
return bpm_events + measure_events + haku_events + [end_event]
|
||||||
|
|
||||||
|
|
||||||
def make_bpm_event(bpm_change: song.BPMEvent, time_map: TimeMap) -> Event:
|
def make_bpm_event(bpm_change: song.BPMEvent, time_map: TimeMap) -> Event:
|
||||||
@ -94,14 +103,18 @@ def make_measure_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
|
|||||||
return Event(time=ticks, command=Command.MEASURE, value=0)
|
return Event(time=ticks, command=Command.MEASURE, value=0)
|
||||||
|
|
||||||
|
|
||||||
def make_beat_events(end_beat: song.BeatsTime, time_map: TimeMap) -> List[Event]:
|
def dump_hakus(hakus: Set[song.BeatsTime], time_map: TimeMap) -> List[Event]:
|
||||||
|
return [make_haku_event(beat, time_map) for beat in sorted(hakus)]
|
||||||
|
|
||||||
|
|
||||||
|
def make_regular_hakus(end_beat: song.BeatsTime, time_map: TimeMap) -> List[Event]:
|
||||||
start = song.BeatsTime(0)
|
start = song.BeatsTime(0)
|
||||||
stop = end_beat + song.BeatsTime(1, 2)
|
stop = end_beat + song.BeatsTime(1, 2)
|
||||||
step = song.BeatsTime(1)
|
step = song.BeatsTime(1)
|
||||||
beats = numeric_range(start, stop, step)
|
beats = numeric_range(start, stop, step)
|
||||||
return [make_beat_event(beat, time_map) for beat in beats]
|
return [make_haku_event(beat, time_map) for beat in beats]
|
||||||
|
|
||||||
|
|
||||||
def make_beat_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
|
def make_haku_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
|
||||||
ticks = ticks_at_beat(beat, time_map)
|
ticks = ticks_at_beat(beat, time_map)
|
||||||
return Event(time=ticks, command=Command.HAKU, value=0)
|
return Event(time=ticks, command=Command.HAKU, value=0)
|
||||||
|
@ -10,8 +10,8 @@ from ..dump_tools import make_events_from_chart
|
|||||||
|
|
||||||
def _dump_eve(song: song.Song, **kwargs: dict) -> List[ChartFile]:
|
def _dump_eve(song: song.Song, **kwargs: dict) -> List[ChartFile]:
|
||||||
res = []
|
res = []
|
||||||
for dif, chart, timing in song.iter_charts_with_applicable_timing():
|
for dif, chart, timing, hakus in song.iter_charts():
|
||||||
events = make_events_from_chart(chart.notes, timing)
|
events = make_events_from_chart(chart.notes, timing, hakus)
|
||||||
chart_text = "\n".join(e.dump() for e in events)
|
chart_text = "\n".join(e.dump() for e in events)
|
||||||
chart_bytes = chart_text.encode("ascii")
|
chart_bytes = chart_text.encode("ascii")
|
||||||
res.append(ChartFile(chart_bytes, song, dif, chart))
|
res.append(ChartFile(chart_bytes, song, dif, chart))
|
||||||
|
@ -15,8 +15,8 @@ from . import construct
|
|||||||
|
|
||||||
def _dump_jbsq(song: song.Song, **kwargs: dict) -> List[ChartFile]:
|
def _dump_jbsq(song: song.Song, **kwargs: dict) -> List[ChartFile]:
|
||||||
res = []
|
res = []
|
||||||
for dif, chart, timing in song.iter_charts_with_applicable_timing():
|
for dif, chart, timing, hakus in song.iter_charts():
|
||||||
events = make_events_from_chart(chart.notes, timing)
|
events = make_events_from_chart(chart.notes, timing, hakus)
|
||||||
jbsq_chart = make_jbsq_chart(events, chart.notes)
|
jbsq_chart = make_jbsq_chart(events, chart.notes)
|
||||||
chart_bytes = construct.jbsq.build(jbsq_chart)
|
chart_bytes = construct.jbsq.build(jbsq_chart)
|
||||||
res.append(ChartFile(chart_bytes, song, dif, chart))
|
res.append(ChartFile(chart_bytes, song, dif, chart))
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Iterable, List
|
from typing import Iterable, List, Optional, Set
|
||||||
|
|
||||||
|
from more_itertools import numeric_range
|
||||||
|
|
||||||
from jubeatools import song
|
from jubeatools import song
|
||||||
from jubeatools.formats.load_tools import round_beats
|
from jubeatools.formats.load_tools import round_beats
|
||||||
@ -36,17 +38,22 @@ def make_chart_from_events(events: Iterable[Event], beat_snap: int = 240) -> son
|
|||||||
]
|
]
|
||||||
all_notes = sorted(tap_notes + long_notes, key=lambda n: (n.time, n.position))
|
all_notes = sorted(tap_notes + long_notes, key=lambda n: (n.time, n.position))
|
||||||
timing = time_map.convert_to_timing_info(beat_snap=beat_snap)
|
timing = time_map.convert_to_timing_info(beat_snap=beat_snap)
|
||||||
return song.Chart(level=Decimal(0), timing=timing, notes=all_notes)
|
end_tick = events_by_command[Command.END].pop().time
|
||||||
|
hakus = make_hakus(
|
||||||
|
[e.time for e in events_by_command[Command.HAKU]],
|
||||||
|
end_tick,
|
||||||
|
time_map,
|
||||||
|
beat_snap,
|
||||||
|
)
|
||||||
|
return song.Chart(level=Decimal(0), timing=timing, notes=all_notes, hakus=hakus)
|
||||||
|
|
||||||
|
|
||||||
def make_tap_note(
|
def make_tap_note(
|
||||||
ticks: int, value: int, time_map: TimeMap, beat_snap: int
|
ticks: int, value: int, time_map: TimeMap, beat_snap: int
|
||||||
) -> song.TapNote:
|
) -> song.TapNote:
|
||||||
seconds = ticks_to_seconds(ticks)
|
time = beats_at_tick(ticks, time_map, beat_snap)
|
||||||
raw_beats = time_map.beats_at(seconds)
|
|
||||||
beats = round_beats(raw_beats, beat_snap)
|
|
||||||
position = song.NotePosition.from_index(value)
|
position = song.NotePosition.from_index(value)
|
||||||
return song.TapNote(time=beats, position=position)
|
return song.TapNote(time=time, position=position)
|
||||||
|
|
||||||
|
|
||||||
def make_long_note(
|
def make_long_note(
|
||||||
@ -67,3 +74,52 @@ def make_long_note(
|
|||||||
return song.LongNote(
|
return song.LongNote(
|
||||||
time=beats, position=position, duration=beats_duration, tail_tip=tail_pos
|
time=beats, position=position, duration=beats_duration, tail_tip=tail_pos
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_hakus(
|
||||||
|
hakus: List[int], end: int, time_map: TimeMap, beat_snap: int
|
||||||
|
) -> Optional[Set[song.BeatsTime]]:
|
||||||
|
"""Try to detect if the haku pattern is regular, in which case return None,
|
||||||
|
otherwise return the parsed hakus"""
|
||||||
|
roughly_rounded_hakus = make_raw_hakus(hakus, time_map, beat_snap=4)
|
||||||
|
rough_end = beats_at_tick(end, time_map, beat_snap=4)
|
||||||
|
if follows_regular_haku_pattern(roughly_rounded_hakus, rough_end):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return make_raw_hakus(hakus, time_map, beat_snap)
|
||||||
|
|
||||||
|
|
||||||
|
def make_raw_hakus(
|
||||||
|
hakus: List[int], time_map: TimeMap, beat_snap: int
|
||||||
|
) -> Set[song.BeatsTime]:
|
||||||
|
return set(beats_at_tick(haku, time_map, beat_snap) for haku in hakus)
|
||||||
|
|
||||||
|
|
||||||
|
def follows_regular_haku_pattern(
|
||||||
|
hakus: Set[song.BeatsTime], end_command: song.BeatsTime
|
||||||
|
) -> bool:
|
||||||
|
"""Regular hakus extend at least till the END command in a 4/4 rhythm"""
|
||||||
|
if len(hakus) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
start = min(hakus)
|
||||||
|
if (start % 1) != 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
haku_end = max(hakus)
|
||||||
|
if (haku_end % 1) != 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if haku_end < end_command:
|
||||||
|
return False
|
||||||
|
|
||||||
|
stop = haku_end + song.BeatsTime(1, 2)
|
||||||
|
step = song.BeatsTime(1)
|
||||||
|
regular = numeric_range(start, stop, step)
|
||||||
|
return sorted(hakus) == list(regular)
|
||||||
|
|
||||||
|
|
||||||
|
def beats_at_tick(tick: int, time_map: TimeMap, beat_snap: int) -> song.BeatsTime:
|
||||||
|
seconds = ticks_to_seconds(tick)
|
||||||
|
raw_beats = time_map.beats_at(seconds)
|
||||||
|
return round_beats(raw_beats, beat_snap)
|
||||||
|
@ -12,7 +12,7 @@ simple_beat_strat = jbst.beat_time(
|
|||||||
|
|
||||||
@st.composite
|
@st.composite
|
||||||
def eve_compatible_song(draw: st.DrawFn) -> song.Song:
|
def eve_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||||
"""eve only keeps notes, timing info and difficulty,
|
"""eve only keeps notes, hakus, timing info and difficulty,
|
||||||
the precision you can get out of it is also severly limited"""
|
the precision you can get out of it is also severly limited"""
|
||||||
diff = draw(st.sampled_from(list(song.Difficulty)))
|
diff = draw(st.sampled_from(list(song.Difficulty)))
|
||||||
chart = draw(
|
chart = draw(
|
||||||
@ -42,6 +42,7 @@ def eve_compatible_song(draw: st.DrawFn) -> song.Song:
|
|||||||
beat_time_strat=simple_beat_strat,
|
beat_time_strat=simple_beat_strat,
|
||||||
),
|
),
|
||||||
level_strat=st.just(Decimal(0)),
|
level_strat=st.just(Decimal(0)),
|
||||||
|
hakus_strat=st.one_of(st.none(), st.sets(simple_beat_strat)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return song.Song(
|
return song.Song(
|
||||||
|
@ -12,9 +12,7 @@ from ..tools import make_memon_dumper
|
|||||||
from . import schema as memon
|
from . import schema as memon
|
||||||
|
|
||||||
|
|
||||||
def _dump_memon_1_0_0(
|
def _dump_memon_1_0_0(song: jbt.Song, **kwargs: Any) -> SongFile:
|
||||||
song: jbt.Song, use_fractions: bool = False, **kwargs: Any
|
|
||||||
) -> SongFile:
|
|
||||||
metadata = dump_metadata(song.metadata)
|
metadata = dump_metadata(song.metadata)
|
||||||
common_timing = dump_file_timing(song)
|
common_timing = dump_file_timing(song)
|
||||||
charts = {
|
charts = {
|
||||||
|
@ -349,3 +349,15 @@ class Song:
|
|||||||
f"Neither song nor {dif} chart have any timing information"
|
f"Neither song nor {dif} chart have any timing information"
|
||||||
)
|
)
|
||||||
yield dif, chart, timing
|
yield dif, chart, timing
|
||||||
|
|
||||||
|
def iter_charts(
|
||||||
|
self,
|
||||||
|
) -> Iterator[Tuple[str, Chart, Timing, Optional[Set[BeatsTime]]]]:
|
||||||
|
for dif, chart in self.charts.items():
|
||||||
|
timing = chart.timing or self.common_timing
|
||||||
|
if timing is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Neither song nor {dif} chart have any timing information"
|
||||||
|
)
|
||||||
|
hakus = chart.hakus if chart.hakus is not None else self.common_hakus
|
||||||
|
yield dif, chart, timing, hakus
|
||||||
|
Loading…
Reference in New Issue
Block a user