Add support for HAKUs for konami formats
This commit is contained in:
parent
efaa52d10b
commit
692b63cb96
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user