1
0
mirror of synced 2024-12-04 03:07:16 +01:00

Add support for HAKUs for konami formats

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

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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(

View File

@ -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 = {

View File

@ -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