1
0
mirror of synced 2024-12-12 15:01:09 +01:00

[malody] test pass !

This commit is contained in:
Stepland 2021-05-28 01:11:27 +02:00
parent 7b110e2462
commit b613a2d960
11 changed files with 120 additions and 55 deletions

View File

@ -1,3 +1,7 @@
# v1.2.0
## Added
- [malody] 🎉 initial malody support !
# v1.1.3
## Fixed
- [jubeat-analyser] All files are read and written in `surrogateescape` error

View File

@ -4,6 +4,7 @@ from enum import Enum
class Format(str, Enum):
EVE = "eve"
JBSQ = "jbsq"
MALODY = "malody"
MEMON_LEGACY = "memon:legacy"
MEMON_0_1_0 = "memon:v0.1.0"
MEMON_0_2_0 = "memon:v0.2.0"

View File

@ -12,6 +12,7 @@ from .jubeat_analyser import (
load_mono_column,
)
from .konami import dump_eve, dump_jbsq, load_eve, load_jbsq
from .malody import dump_malody, load_malody
from .memon import (
dump_memon_0_1_0,
dump_memon_0_2_0,
@ -25,6 +26,7 @@ from .typing import Dumper, Loader
LOADERS: Dict[Format, Loader] = {
Format.EVE: load_eve,
Format.JBSQ: load_jbsq,
Format.MALODY: load_malody,
Format.MEMON_LEGACY: load_memon_legacy,
Format.MEMON_0_1_0: load_memon_0_1_0,
Format.MEMON_0_2_0: load_memon_0_2_0,
@ -37,6 +39,7 @@ LOADERS: Dict[Format, Loader] = {
DUMPERS: Dict[Format, Dumper] = {
Format.EVE: dump_eve,
Format.JBSQ: dump_jbsq,
Format.MALODY: dump_malody,
Format.MEMON_LEGACY: dump_memon_legacy,
Format.MEMON_0_1_0: dump_memon_0_1_0,
Format.MEMON_0_2_0: dump_memon_0_2_0,

View File

@ -10,7 +10,7 @@ def guess_format(path: Path) -> Format:
raise ValueError("Can't guess chart format for a folder")
try:
return recognize_memon_version(path)
return recognize_json_formats(path)
except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
pass
@ -28,19 +28,26 @@ def guess_format(path: Path) -> Format:
raise ValueError("Unrecognized file format")
def recognize_memon_version(path: Path) -> Format:
def recognize_json_formats(path: Path) -> Format:
with path.open() as f:
obj = json.load(f)
if not isinstance(obj, dict):
raise ValueError("Top level value is not an object")
if obj.keys() >= {"metadata", "data"}:
return recognize_memon_version(obj)
elif obj.keys() >= {"meta", "time", "note"}:
return Format.MALODY
else:
raise ValueError("Unrecognized file format")
def recognize_memon_version(obj: dict) -> Format:
try:
version = obj["version"]
except KeyError:
return Format.MEMON_LEGACY
except TypeError:
raise ValueError(
"This JSON file is not a correct memon file : the top-level "
"value is not an object"
)
if version == "0.1.0":
return Format.MEMON_0_1_0

View File

@ -5,7 +5,7 @@ from jubeatools.formats import Format
from jubeatools.formats.konami.testutils import eve_compatible_song, open_temp_dir
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
from ..construct import jbsq
from .construct import jbsq
@given(eve_compatible_song())

View File

@ -4,3 +4,6 @@ many different games or "Modes", including jubeat (known as "Pad" Mode)
The file format it uses is not that well documented but is simple enough to
make sense of without docs. It's a json file with some defined schema"""
from .dump import dump_malody
from .load import load_malody

View File

@ -1,11 +1,14 @@
from pathlib import Path
from typing import List
import json
import time
from functools import singledispatch
from pathlib import Path
from typing import List, Tuple, Union
import simplejson as json
from jubeatools import song
from jubeatools.formats.dump_tools import make_dumper_from_chart_file_dumper
from jubeatools.formats.filetypes import ChartFile
from jubeatools.utils import none_or
from . import schema as malody
@ -14,8 +17,8 @@ def dump_malody_song(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_timing():
malody_chart = dump_malody_chart(song.metadata, dif, chart, timing)
json_chart = malody.Chart.Schema().dump(malody_chart)
chart_bytes = json.dumps(json_chart).encode("utf-8")
json_chart = malody.CHART_SCHEMA.dump(malody_chart)
chart_bytes = json.dumps(json_chart, indent=4, use_decimal=True).encode("utf-8")
res.append(ChartFile(chart_bytes, song, dif, chart))
return res
@ -26,14 +29,17 @@ dump_malody = make_dumper_from_chart_file_dumper(
)
def dump_malody_chart(metadata: song.Metadata, dif: str, chart: song.Chart, timing: song.Timing) -> malody.Chart:
def dump_malody_chart(
metadata: song.Metadata, dif: str, chart: song.Chart, timing: song.Timing
) -> malody.Chart:
meta = dump_metadata(metadata, dif)
time = dump_timing(timing)
notes = dump_notes(chart.notes)
if metadata.audio is not None:
notes += dump_bgm(metadata.audio, timing)
notes += [dump_bgm(metadata.audio, timing)]
return malody.Chart(meta=meta, time=time, note=notes)
def dump_metadata(metadata: song.Metadata, dif: str) -> malody.Metadata:
return malody.Metadata(
cover="",
@ -47,20 +53,23 @@ def dump_metadata(metadata: song.Metadata, dif: str) -> malody.Metadata:
title=metadata.title,
artist=metadata.artist,
id=0,
)
),
)
def dump_timing(timing: song.Timing) -> List[malody.BPMEvent]:
sorted_events = sorted(timing.events, key=lambda e: e.time)
return [dump_bpm_change(e) for e in sorted_events]
def dump_bpm_change(b: song.BPMEvent) -> malody.BPMEvent:
return malody.BPMEvent(
beat=beats_to_tuple(b.time),
bpm=b.BPM,
)
def dump_notes(notes: List[Union[song.TapNote, song.LongNote]]) -> List[Union[malody.TapNote, malody.LongNote]]:
def dump_notes(notes: List[Union[song.TapNote, song.LongNote]]) -> List[malody.Event]:
return [dump_note(n) for n in notes]
@ -78,26 +87,29 @@ def dump_tap_note(n: song.TapNote) -> malody.TapNote:
index=n.position.index,
)
@dump_note.register
def dump_long_note(n: song.LongNote) -> malody.LongNote:
return malody.LongNote(
beat=beats_to_tuple(n.time)
beat=beats_to_tuple(n.time),
index=n.position.index,
endbeat=beats_to_tuple(n.time + n.duration),
endindex=n.tail_tip.index,
)
def dump_bgm(audio: Path, timing: song.Timing) -> malody.Sound:
return malody.Sound(
beat=beats_to_tuple(song.BeatsTime(0)),
sound=str(audio),
vol=100,
offset=-int(timing.beat_zero_offset*1000),
offset=-int(timing.beat_zero_offset * 1000),
type=malody.SoundType.BACKGROUND_MUSIC,
isBgm=None,
x=None,
)
def beats_to_tuple(b: song.BeatsTime) -> Tuple[int, int, int]:
integer_part = int(b)
remainder = b % 1

View File

@ -1,4 +1,3 @@
import json
import warnings
from decimal import Decimal
from fractions import Fraction
@ -6,6 +5,8 @@ from functools import reduce, singledispatch
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union
import simplejson as json
from jubeatools import song
from jubeatools.formats import timemap
from jubeatools.formats.load_tools import make_folder_loader
@ -22,14 +23,14 @@ def load_malody(path: Path, **kwargs: Any) -> song.Song:
def load_file(path: Path) -> Any:
with path.open() as f:
return json.load(f)
return json.load(f, use_decimal=True)
load_folder = make_folder_loader("*.mc", load_file)
def load_malody_file(raw_dict: dict) -> song.Song:
file: malody.Chart = malody.Chart.Schema().load(raw_dict)
file: malody.Chart = malody.CHART_SCHEMA.load(raw_dict)
if file.meta.mode != malody.Mode.PAD:
raise ValueError("This file is not a Malody Pad Chart (Malody's jubeat mode)")
@ -86,7 +87,7 @@ def load_timing_info(
def load_notes(events: List[malody.Event]) -> List[Union[song.TapNote, song.LongNote]]:
# filter out sound events
notes = filter(lambda e: isinstance(e, (malody.TapNote, malody.TapNote)), events)
notes = filter(lambda e: isinstance(e, (malody.TapNote, malody.LongNote)), events)
return [load_note(n) for n in notes]

View File

@ -1,34 +1,22 @@
from __future__ import annotations
from dataclasses import field
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import ClassVar, List, Optional, Tuple, Type, Union
from typing import List, Optional, Tuple, Union
from marshmallow import Schema as ms_Schema
from marshmallow.validate import Range
from marshmallow_dataclass import NewType, dataclass
from marshmallow_dataclass import NewType, class_schema
class Ordered:
class Meta:
ordered = True
@dataclass
class Chart:
meta: Metadata
time: List[BPMEvent] = field(default_factory=list)
note: List[Event] = field(default_factory=list)
Schema: ClassVar[Type[ms_Schema]] = ms_Schema
@dataclass
class Metadata:
cover: Optional[str] # path to album art ?
creator: Optional[str] # Chart author
background: Optional[str] # path to background image
version: Optional[str] # freeform difficulty name
class SongInfo(Ordered):
title: Optional[str]
artist: Optional[str]
id: Optional[int]
mode: int
time: Optional[int] # creation timestamp ?
song: SongInfo
class Mode(int, Enum):
@ -42,22 +30,26 @@ class Mode(int, Enum):
@dataclass
class SongInfo:
title: Optional[str]
artist: Optional[str]
class Metadata(Ordered):
cover: Optional[str] # path to album art ?
creator: Optional[str] # Chart author
background: Optional[str] # path to background image
version: Optional[str] # freeform difficulty name
id: Optional[int]
mode: int
time: Optional[int] # creation timestamp ?
song: SongInfo
PositiveInt = NewType("PositiveInt", int, validate=Range(min=0))
BeatTime = Tuple[PositiveInt, PositiveInt, PositiveInt]
StrictlyPositiveDecimal = NewType(
"StrictlyPositiveDecimal", Decimal, validate=Range(min=0, min_inclusive=False)
)
@dataclass
class BPMEvent:
class BPMEvent(Ordered):
beat: BeatTime
bpm: StrictlyPositiveDecimal
@ -66,13 +58,13 @@ ButtonIndex = NewType("ButtonIndex", int, validate=Range(min=0, max=15))
@dataclass
class TapNote:
class TapNote(Ordered):
beat: BeatTime
index: ButtonIndex
@dataclass
class LongNote:
class LongNote(Ordered):
beat: BeatTime
index: ButtonIndex
endbeat: BeatTime
@ -80,7 +72,7 @@ class LongNote:
@dataclass
class Sound:
class Sound(Ordered):
"""Used both for the background music and keysounds"""
beat: BeatTime
@ -98,3 +90,13 @@ class SoundType(int, Enum):
Event = Union[Sound, LongNote, TapNote]
@dataclass
class Chart(Ordered):
meta: Metadata
time: List[BPMEvent] = field(default_factory=list)
note: List[Event] = field(default_factory=list)
CHART_SCHEMA = class_schema(Chart)()

View File

@ -0,0 +1,32 @@
from decimal import Decimal
from hypothesis import given
from hypothesis import strategies as st
from jubeatools import song
from jubeatools.formats import Format
from jubeatools.formats.konami.testutils import open_temp_dir
from jubeatools.testutils import strategies as jbst
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
from jubeatools.testutils.typing import DrawFunc
@st.composite
def malody_compatible_song(draw: DrawFunc) -> song.Song:
"""Malody files only hold one chart and have limited metadata"""
diff = draw(st.sampled_from(list(song.Difficulty))).value
chart = draw(jbst.chart(level_strat=st.just(Decimal(0))))
metadata = draw(jbst.metadata())
metadata.preview = None
metadata.preview_file = None
return song.Song(metadata=metadata, charts={diff: chart})
@given(malody_compatible_song())
def test_that_full_chart_roundtrips(song: song.Song) -> None:
dump_and_load_then_compare(
Format.MALODY,
song,
temp_path=open_temp_dir(),
bytes_decoder=lambda b: b.decode("utf-8"),
)