1
0
mirror of synced 2024-11-13 18:10:48 +01:00

Add support for HAKUs for konami formats

This commit is contained in:
Stepland 2021-12-28 16:49:14 +01:00
parent efaa52d10b
commit 692b63cb96
8 changed files with 106 additions and 24 deletions

View File

@ -1,7 +1,9 @@
# v1.4.0
## Added
- Jubeatools can now handle HAKUs, in the following formats :
- Jubeatools can now handle HAKUs in the following formats :
- [memon:v1.0.0]
- [eve]
- [jbsq]
- [memon]
- 🎉 inital support for v1.0.0 !
- `--merge` option allows for several memon files to be merged when

View File

@ -1,7 +1,7 @@
import math
from fractions import Fraction
from functools import singledispatch
from typing import List
from typing import List, Optional, Set
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
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)
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)
@ -38,14 +40,21 @@ def make_long_note_event(note: song.LongNote, time_map: TimeMap) -> Event:
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]:
bpm_events = [make_bpm_event(e, time_map) for e in timing.events]
end_beat = choose_end_beat(notes)
end_event = make_end_event(end_beat, time_map)
measure_events = make_measure_events(end_beat, time_map)
beat_events = make_beat_events(end_beat, time_map)
return bpm_events + measure_events + beat_events + [end_event]
if hakus is not None:
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:
@ -94,14 +103,18 @@ def make_measure_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
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)
stop = end_beat + song.BeatsTime(1, 2)
step = song.BeatsTime(1)
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)
return Event(time=ticks, command=Command.HAKU, value=0)

View File

@ -10,8 +10,8 @@ from ..dump_tools import make_events_from_chart
def _dump_eve(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_applicable_timing():
events = make_events_from_chart(chart.notes, timing)
for dif, chart, timing, hakus in song.iter_charts():
events = make_events_from_chart(chart.notes, timing, hakus)
chart_text = "\n".join(e.dump() for e in events)
chart_bytes = chart_text.encode("ascii")
res.append(ChartFile(chart_bytes, song, dif, chart))

View File

@ -15,8 +15,8 @@ from . import construct
def _dump_jbsq(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_applicable_timing():
events = make_events_from_chart(chart.notes, timing)
for dif, chart, timing, hakus in song.iter_charts():
events = make_events_from_chart(chart.notes, timing, hakus)
jbsq_chart = make_jbsq_chart(events, chart.notes)
chart_bytes = construct.jbsq.build(jbsq_chart)
res.append(ChartFile(chart_bytes, song, dif, chart))

View File

@ -1,5 +1,7 @@
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.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))
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(
ticks: int, value: int, time_map: TimeMap, beat_snap: int
) -> song.TapNote:
seconds = ticks_to_seconds(ticks)
raw_beats = time_map.beats_at(seconds)
beats = round_beats(raw_beats, beat_snap)
time = beats_at_tick(ticks, time_map, beat_snap)
position = song.NotePosition.from_index(value)
return song.TapNote(time=beats, position=position)
return song.TapNote(time=time, position=position)
def make_long_note(
@ -67,3 +74,52 @@ def make_long_note(
return song.LongNote(
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)

View File

@ -12,7 +12,7 @@ simple_beat_strat = jbst.beat_time(
@st.composite
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"""
diff = draw(st.sampled_from(list(song.Difficulty)))
chart = draw(
@ -42,6 +42,7 @@ def eve_compatible_song(draw: st.DrawFn) -> song.Song:
beat_time_strat=simple_beat_strat,
),
level_strat=st.just(Decimal(0)),
hakus_strat=st.one_of(st.none(), st.sets(simple_beat_strat)),
)
)
return song.Song(

View File

@ -12,9 +12,7 @@ from ..tools import make_memon_dumper
from . import schema as memon
def _dump_memon_1_0_0(
song: jbt.Song, use_fractions: bool = False, **kwargs: Any
) -> SongFile:
def _dump_memon_1_0_0(song: jbt.Song, **kwargs: Any) -> SongFile:
metadata = dump_metadata(song.metadata)
common_timing = dump_file_timing(song)
charts = {

View File

@ -349,3 +349,15 @@ class Song:
f"Neither song nor {dif} chart have any timing information"
)
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