Format the code
This commit is contained in:
parent
de5f6d38f5
commit
9af51d54c4
@ -1,3 +1,3 @@
|
|||||||
from .song import Song
|
from .song import Song
|
||||||
|
|
||||||
__version__ = '0.1.0'
|
__version__ = "0.1.0"
|
||||||
|
@ -3,6 +3,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
def convert():
|
def convert():
|
||||||
...
|
...
|
||||||
|
@ -1,28 +1,33 @@
|
|||||||
"""
|
"""
|
||||||
Base class for all file formats
|
Module containing all the load/dump code for all file formats
|
||||||
"""
|
"""
|
||||||
from path import Path
|
from path import Path
|
||||||
from typing import Any, Callable, Iterable, Tuple, IO
|
from typing import Callable, Dict, IO
|
||||||
|
|
||||||
from jubeatools.song import Song
|
from jubeatools.song import Song
|
||||||
from .memon import *
|
from .memon import (
|
||||||
from ._filekind import FileKind
|
dump_memon_legacy,
|
||||||
|
dump_memon_0_1_0,
|
||||||
|
dump_memon_0_2_0,
|
||||||
|
load_memon_legacy,
|
||||||
|
load_memon_0_1_0,
|
||||||
|
load_memon_0_2_0,
|
||||||
|
)
|
||||||
|
|
||||||
ALIASES = {
|
ALIASES = {
|
||||||
"memon": "memon:v0.2.0",
|
"memon": "memon:v0.2.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Loaders take in a folder containing the files to be converted
|
# Loaders deserialize a folder or a file to a Song object
|
||||||
# and return a Song object
|
LOADERS: Dict[str, Callable[[Path], Song]] = {
|
||||||
LOADERS: Mapping[str, Callable[[Path], Song]] = {
|
|
||||||
"memon:legacy": load_memon_legacy,
|
"memon:legacy": load_memon_legacy,
|
||||||
"memon:v0.1.0": load_memon_0_1_0,
|
"memon:v0.1.0": load_memon_0_1_0,
|
||||||
"memon:v0.2.0": load_memon_0_2_0
|
"memon:v0.2.0": load_memon_0_2_0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dumpers take in the song object and return a list of tuples
|
# Dumpers serialize a Song object into a (filename -> file) mapping
|
||||||
DUMPERS: Mapping[str, Callable[[Song], Iterable[Tuple[Any, IO]]]] = {
|
DUMPERS: Dict[str, Callable[[Song], Dict[str, IO]]] = {
|
||||||
"memon:legacy": dump_memon_legacy,
|
"memon:legacy": dump_memon_legacy,
|
||||||
"memon:v0.1.0": dump_memon_0_1_0,
|
"memon:v0.1.0": dump_memon_0_1_0,
|
||||||
"memon:v0.2.0": dump_memon_0_2_0
|
"memon:v0.2.0": dump_memon_0_2_0,
|
||||||
}
|
}
|
@ -14,7 +14,15 @@ from itertools import chain
|
|||||||
|
|
||||||
from path import Path
|
from path import Path
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from marshmallow import Schema, fields, RAISE, validate, validates_schema, ValidationError, post_load
|
from marshmallow import (
|
||||||
|
Schema,
|
||||||
|
fields,
|
||||||
|
RAISE,
|
||||||
|
validate,
|
||||||
|
validates_schema,
|
||||||
|
ValidationError,
|
||||||
|
post_load,
|
||||||
|
)
|
||||||
|
|
||||||
from jubeatools.song import *
|
from jubeatools.song import *
|
||||||
from jubeatools.utils import lcm
|
from jubeatools.utils import lcm
|
||||||
@ -41,10 +49,10 @@ X_Y_OFFSET_TO_P_VALUE = {
|
|||||||
(3, 0): 9,
|
(3, 0): 9,
|
||||||
(-1, 0): 3,
|
(-1, 0): 3,
|
||||||
(-2, 0): 7,
|
(-2, 0): 7,
|
||||||
(-3, 0): 11
|
(-3, 0): 11,
|
||||||
}
|
}
|
||||||
|
|
||||||
P_VALUE_TO_X_Y_OFFSET = { v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items() }
|
P_VALUE_TO_X_Y_OFFSET = {v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items()}
|
||||||
|
|
||||||
|
|
||||||
class StrictSchema(Schema):
|
class StrictSchema(Schema):
|
||||||
@ -64,7 +72,7 @@ class MemonNote(StrictSchema):
|
|||||||
x = data["n"] % 4
|
x = data["n"] % 4
|
||||||
y = data["n"] // 4
|
y = data["n"] // 4
|
||||||
dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]]
|
dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]]
|
||||||
if (not (0 <= x + dx < 4 and 0 <= y + dy < 4)):
|
if not (0 <= x + dx < 4 and 0 <= y + dy < 4):
|
||||||
raise ValidationError("Invalid tail position : {data}")
|
raise ValidationError("Invalid tail position : {data}")
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +91,9 @@ class MemonMetadata_legacy(StrictSchema):
|
|||||||
artist = fields.String(required=True)
|
artist = fields.String(required=True)
|
||||||
audio = fields.String(required=True, data_key="music path")
|
audio = fields.String(required=True, data_key="music path")
|
||||||
cover = fields.String(required=True, data_key="jacket path")
|
cover = fields.String(required=True, data_key="jacket path")
|
||||||
BPM = fields.Decimal(required=True, validate=validate.Range(min=0, min_inclusive=False))
|
BPM = fields.Decimal(
|
||||||
|
required=True, validate=validate.Range(min=0, min_inclusive=False)
|
||||||
|
)
|
||||||
offset = fields.Decimal(required=True)
|
offset = fields.Decimal(required=True)
|
||||||
|
|
||||||
|
|
||||||
@ -93,7 +103,9 @@ class MemonMetadata_0_1_0(MemonMetadata_legacy):
|
|||||||
|
|
||||||
class MemonPreview(StrictSchema):
|
class MemonPreview(StrictSchema):
|
||||||
position = fields.Decimal(required=True, validate=validate.Range(min=0))
|
position = fields.Decimal(required=True, validate=validate.Range(min=0))
|
||||||
length = fields.Decimal(required=True, validate=validate.Range(min=0, min_inclusive=False))
|
length = fields.Decimal(
|
||||||
|
required=True, validate=validate.Range(min=0, min_inclusive=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
|
class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
|
||||||
@ -138,7 +150,7 @@ def _search_and_load(file_or_folder: Path) -> Any:
|
|||||||
def _load_memon_note_v0(note: dict, resolution: int) -> Union[TapNote, LongNote]:
|
def _load_memon_note_v0(note: dict, resolution: int) -> Union[TapNote, LongNote]:
|
||||||
position = NotePosition.from_index(note["n"])
|
position = NotePosition.from_index(note["n"])
|
||||||
time = BeatsTime.from_ticks(ticks=note["t"], resolution=resolution)
|
time = BeatsTime.from_ticks(ticks=note["t"], resolution=resolution)
|
||||||
if (note["l"] > 0):
|
if note["l"] > 0:
|
||||||
duration = BeatsTime.from_ticks(ticks=note["l"], resolution=resolution)
|
duration = BeatsTime.from_ticks(ticks=note["l"], resolution=resolution)
|
||||||
tail_tip = NotePosition(*P_VALUE_TO_X_Y_OFFSET[note["p"]])
|
tail_tip = NotePosition(*P_VALUE_TO_X_Y_OFFSET[note["p"]])
|
||||||
return LongNote(time, position, duration, tail_tip)
|
return LongNote(time, position, duration, tail_tip)
|
||||||
@ -146,20 +158,16 @@ def _load_memon_note_v0(note: dict, resolution: int) -> Union[TapNote, LongNote]
|
|||||||
return TapNote(time, position)
|
return TapNote(time, position)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_memon_legacy(file_or_folder: Path) -> Song:
|
def load_memon_legacy(file_or_folder: Path) -> Song:
|
||||||
raw_memon = _search_and_load(file_or_folder)
|
raw_memon = _search_and_load(file_or_folder)
|
||||||
schema = Memon_legacy()
|
schema = Memon_legacy()
|
||||||
memon = schema.load(raw_memon)
|
memon = schema.load(raw_memon)
|
||||||
metadata = Metadata(
|
metadata = Metadata(
|
||||||
**{
|
**{key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]}
|
||||||
key: memon["metadata"][key]
|
|
||||||
for key in ["title", "artist", "audio", "cover"]
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
global_timing = Timing(
|
global_timing = Timing(
|
||||||
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
|
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
|
||||||
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"])
|
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
|
||||||
)
|
)
|
||||||
charts: Mapping[str, Chart] = MultiDict()
|
charts: Mapping[str, Chart] = MultiDict()
|
||||||
for memon_chart in memon["data"]:
|
for memon_chart in memon["data"]:
|
||||||
@ -170,17 +178,11 @@ def load_memon_legacy(file_or_folder: Path) -> Song:
|
|||||||
notes=[
|
notes=[
|
||||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||||
for note in memon_chart["notes"]
|
for note in memon_chart["notes"]
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Song(
|
return Song(metadata=metadata, charts=charts, global_timing=global_timing)
|
||||||
metadata=metadata,
|
|
||||||
charts=charts,
|
|
||||||
global_timing=global_timing
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_memon_0_1_0(file_or_folder: Path) -> Song:
|
def load_memon_0_1_0(file_or_folder: Path) -> Song:
|
||||||
@ -188,14 +190,11 @@ def load_memon_0_1_0(file_or_folder: Path) -> Song:
|
|||||||
schema = Memon_0_1_0()
|
schema = Memon_0_1_0()
|
||||||
memon = schema.load(raw_memon)
|
memon = schema.load(raw_memon)
|
||||||
metadata = Metadata(
|
metadata = Metadata(
|
||||||
**{
|
**{key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]}
|
||||||
key: memon["metadata"][key]
|
|
||||||
for key in ["title", "artist", "audio", "cover"]
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
global_timing = Timing(
|
global_timing = Timing(
|
||||||
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
|
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
|
||||||
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"])
|
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
|
||||||
)
|
)
|
||||||
charts: Mapping[str, Chart] = MultiDict()
|
charts: Mapping[str, Chart] = MultiDict()
|
||||||
for difficulty, memon_chart in memon["data"]:
|
for difficulty, memon_chart in memon["data"]:
|
||||||
@ -206,15 +205,11 @@ def load_memon_0_1_0(file_or_folder: Path) -> Song:
|
|||||||
notes=[
|
notes=[
|
||||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||||
for note in memon_chart["notes"]
|
for note in memon_chart["notes"]
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Song(
|
return Song(metadata=metadata, charts=charts, global_timing=global_timing)
|
||||||
metadata=metadata,
|
|
||||||
charts=charts,
|
|
||||||
global_timing=global_timing
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_memon_0_2_0(file_or_folder: Path) -> Song:
|
def load_memon_0_2_0(file_or_folder: Path) -> Song:
|
||||||
@ -222,8 +217,7 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
|
|||||||
schema = Memon_0_2_0()
|
schema = Memon_0_2_0()
|
||||||
memon = schema.load(raw_memon)
|
memon = schema.load(raw_memon)
|
||||||
metadata_dict = {
|
metadata_dict = {
|
||||||
key: memon["metadata"][key]
|
key: memon["metadata"][key] for key in ["title", "artist", "audio", "cover"]
|
||||||
for key in ["title", "artist", "audio", "cover"]
|
|
||||||
}
|
}
|
||||||
if "preview" in memon["metadata"]:
|
if "preview" in memon["metadata"]:
|
||||||
metadata_dict["preview_start"] = memon["metadata"]["preview"]["position"]
|
metadata_dict["preview_start"] = memon["metadata"]["preview"]["position"]
|
||||||
@ -232,7 +226,7 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
|
|||||||
metadata = Metadata(**metadata_dict)
|
metadata = Metadata(**metadata_dict)
|
||||||
global_timing = Timing(
|
global_timing = Timing(
|
||||||
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
|
events=[BPMEvent(time=0, BPM=memon["metadata"]["BPM"])],
|
||||||
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"])
|
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
|
||||||
)
|
)
|
||||||
charts: Mapping[str, Chart] = MultiDict()
|
charts: Mapping[str, Chart] = MultiDict()
|
||||||
for difficulty, memon_chart in memon["data"]:
|
for difficulty, memon_chart in memon["data"]:
|
||||||
@ -243,15 +237,11 @@ def load_memon_0_2_0(file_or_folder: Path) -> Song:
|
|||||||
notes=[
|
notes=[
|
||||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||||
for note in memon_chart["notes"]
|
for note in memon_chart["notes"]
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Song(
|
return Song(metadata=metadata, charts=charts, global_timing=global_timing)
|
||||||
metadata=metadata,
|
|
||||||
charts=charts,
|
|
||||||
global_timing=global_timing
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _long_note_tail_value_v0(note: LongNote) -> int:
|
def _long_note_tail_value_v0(note: LongNote) -> int:
|
||||||
@ -260,7 +250,9 @@ def _long_note_tail_value_v0(note: LongNote) -> int:
|
|||||||
try:
|
try:
|
||||||
return X_Y_OFFSET_TO_P_VALUE[dx, dy]
|
return X_Y_OFFSET_TO_P_VALUE[dx, dy]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note") from None
|
raise ValueError(
|
||||||
|
f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
|
||||||
def check_representable_in_v0(song: Song, version: str) -> None:
|
def check_representable_in_v0(song: Song, version: str) -> None:
|
||||||
@ -269,31 +261,32 @@ def check_representable_in_v0(song: Song, version: str) -> None:
|
|||||||
that cannot be represented in a memon v0.x.x file (includes legacy)"""
|
that cannot be represented in a memon v0.x.x file (includes legacy)"""
|
||||||
|
|
||||||
if any(chart.timing is not None for chart in song.charts.values()):
|
if any(chart.timing is not None for chart in song.charts.values()):
|
||||||
raise ValueError(f"memon:{version} cannot represent a song with per-chart timing")
|
raise ValueError(
|
||||||
|
f"memon:{version} cannot represent a song with per-chart timing"
|
||||||
|
)
|
||||||
|
|
||||||
if song.global_timing is None:
|
if song.global_timing is None:
|
||||||
raise ValueError("The song has no timing information")
|
raise ValueError("The song has no global timing information")
|
||||||
|
|
||||||
number_of_timing_events = len(song.global_timing.events)
|
number_of_timing_events = len(song.global_timing.events)
|
||||||
if number_of_timing_events != 1:
|
if number_of_timing_events != 1:
|
||||||
if number_of_timing_events == 0:
|
if number_of_timing_events == 0:
|
||||||
raise ValueError("The song has no BPM")
|
raise ValueError("The song has no BPM")
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"memon:{version} does not handle Stops or BPM changes")
|
raise ValueError(f"memon:{version} does not handle BPM changes")
|
||||||
|
|
||||||
event = song.global_timing.events[0]
|
event = song.global_timing.events[0]
|
||||||
if not isinstance(event, BPMEvent):
|
|
||||||
raise ValueError("The song file has no BPM")
|
|
||||||
|
|
||||||
if event.BPM <= 0:
|
if event.BPM <= 0:
|
||||||
raise ValueError("memon:legacy only accepts strictly positive BPMs")
|
raise ValueError("memon:{version} only accepts strictly positive BPMs")
|
||||||
|
|
||||||
if event.time != 0:
|
if event.time != 0:
|
||||||
raise ValueError("memon:legacy only accepts a BPM on the first beat")
|
raise ValueError(f"memon:{version} only accepts a BPM on the first beat")
|
||||||
|
|
||||||
for difficulty, chart in song.charts.items():
|
for difficulty, chart in song.charts.items():
|
||||||
if len(set(chart.notes)) != len(chart.notes):
|
if len(set(chart.notes)) != len(chart.notes):
|
||||||
raise ValueError(f"{difficulty} chart has duplicate notes, these cannot be represented")
|
raise ValueError(
|
||||||
|
f"{difficulty} chart has duplicate notes, these cannot be represented"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _dump_to_json(memon: dict) -> IO:
|
def _dump_to_json(memon: dict) -> IO:
|
||||||
@ -306,27 +299,35 @@ def _compute_resolution(notes: List[Union[TapNote, LongNote]]) -> int:
|
|||||||
return lcm(
|
return lcm(
|
||||||
*chain(
|
*chain(
|
||||||
iter(note.time.denominator for note in notes),
|
iter(note.time.denominator for note in notes),
|
||||||
iter(note.duration.denominator for note in notes if isinstance(note, LongNote))
|
iter(
|
||||||
|
note.duration.denominator
|
||||||
|
for note in notes
|
||||||
|
if isinstance(note, LongNote)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _dump_memon_note_v0(note: Union[TapNote, LongNote], resolution: int) -> Dict[str, int]:
|
def _dump_memon_note_v0(
|
||||||
|
note: Union[TapNote, LongNote], resolution: int
|
||||||
|
) -> Dict[str, int]:
|
||||||
"""converts a note into the {n, t, l, p} form"""
|
"""converts a note into the {n, t, l, p} form"""
|
||||||
memon_note = {
|
memon_note = {
|
||||||
"n": note.index,
|
"n": note.index,
|
||||||
"t": note.time.numerator * (resolution // note.time.denominator),
|
"t": note.time.numerator * (resolution // note.time.denominator),
|
||||||
"l": 0,
|
"l": 0,
|
||||||
"p": 0
|
"p": 0,
|
||||||
}
|
}
|
||||||
if isinstance(note, LongNote):
|
if isinstance(note, LongNote):
|
||||||
memon_note["l"] = note.duration.numerator * (resolution // note.duration.denominator)
|
memon_note["l"] = note.duration.numerator * (
|
||||||
|
resolution // note.duration.denominator
|
||||||
|
)
|
||||||
memon_note["p"] = _long_note_tail_value_v0(note)
|
memon_note["p"] = _long_note_tail_value_v0(note)
|
||||||
|
|
||||||
return memon_note
|
return memon_note
|
||||||
|
|
||||||
|
|
||||||
def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]:
|
def dump_memon_legacy(song: Song) -> Dict[str, IO]:
|
||||||
|
|
||||||
check_representable_in_v0(song, "legacy")
|
check_representable_in_v0(song, "legacy")
|
||||||
|
|
||||||
@ -337,26 +338,30 @@ def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]:
|
|||||||
"music path": str(song.metadata.audio),
|
"music path": str(song.metadata.audio),
|
||||||
"jacket path": str(song.metadata.cover),
|
"jacket path": str(song.metadata.cover),
|
||||||
"BPM": song.global_timing.events[0].BPM,
|
"BPM": song.global_timing.events[0].BPM,
|
||||||
"offset": -song.global_timing.beat_zero_offset
|
"offset": -song.global_timing.beat_zero_offset,
|
||||||
},
|
},
|
||||||
"data": []
|
"data": [],
|
||||||
}
|
}
|
||||||
for difficulty, chart in song.charts.items():
|
for difficulty, chart in song.charts.items():
|
||||||
resolution = _compute_resolution(chart.notes)
|
resolution = _compute_resolution(chart.notes)
|
||||||
memon["data"].append({
|
memon["data"].append(
|
||||||
"dif_name": difficulty,
|
{
|
||||||
"level": chart.level,
|
"dif_name": difficulty,
|
||||||
"resolution": resolution,
|
"level": chart.level,
|
||||||
"notes": [
|
"resolution": resolution,
|
||||||
_dump_memon_note_v0(note, resolution)
|
"notes": [
|
||||||
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
_dump_memon_note_v0(note, resolution)
|
||||||
]
|
for note in sorted(
|
||||||
})
|
set(chart.notes), key=lambda n: (n.time, n.position)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return [(song, _dump_to_json(memon))]
|
return [(song, _dump_to_json(memon))]
|
||||||
|
|
||||||
|
|
||||||
def dump_memon_0_1_0(song: Song, folder: Path) -> None:
|
def dump_memon_0_1_0(song: Song) -> Dict[str, IO]:
|
||||||
|
|
||||||
check_representable_in_v0(song, "legacy")
|
check_representable_in_v0(song, "legacy")
|
||||||
|
|
||||||
@ -368,9 +373,9 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
|
|||||||
"music path": str(song.metadata.audio),
|
"music path": str(song.metadata.audio),
|
||||||
"album cover path": str(song.metadata.cover),
|
"album cover path": str(song.metadata.cover),
|
||||||
"BPM": song.global_timing.events[0].BPM,
|
"BPM": song.global_timing.events[0].BPM,
|
||||||
"offset": -song.global_timing.beat_zero_offset
|
"offset": -song.global_timing.beat_zero_offset,
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {},
|
||||||
}
|
}
|
||||||
for difficulty, chart in song.charts.items():
|
for difficulty, chart in song.charts.items():
|
||||||
resolution = _compute_resolution(chart.notes)
|
resolution = _compute_resolution(chart.notes)
|
||||||
@ -380,13 +385,13 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
|
|||||||
"notes": [
|
"notes": [
|
||||||
_dump_memon_note_v0(note, resolution)
|
_dump_memon_note_v0(note, resolution)
|
||||||
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return [(song, _dump_to_json(memon))]
|
return [(song, _dump_to_json(memon))]
|
||||||
|
|
||||||
|
|
||||||
def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
def dump_memon_0_2_0(song: Song) -> Dict[str, IO]:
|
||||||
|
|
||||||
check_representable_in_v0(song, "legacy")
|
check_representable_in_v0(song, "legacy")
|
||||||
|
|
||||||
@ -400,7 +405,7 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
|||||||
"BPM": song.global_timing.events[0].BPM,
|
"BPM": song.global_timing.events[0].BPM,
|
||||||
"offset": -song.global_timing.beat_zero_offset,
|
"offset": -song.global_timing.beat_zero_offset,
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if song.metadata.preview_length != 0:
|
if song.metadata.preview_length != 0:
|
||||||
@ -417,7 +422,7 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
|||||||
"notes": [
|
"notes": [
|
||||||
_dump_memon_note_v0(note, resolution)
|
_dump_memon_note_v0(note, resolution)
|
||||||
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return [(song, _dump_to_json(memon))]
|
return [(song, _dump_to_json(memon))]
|
@ -19,7 +19,7 @@ from multidict import MultiDict
|
|||||||
|
|
||||||
class BeatsTime(Fraction):
|
class BeatsTime(Fraction):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_ticks(cls: Type[Fraction], ticks: int, resolution: int) -> 'BeatsTime':
|
def from_ticks(cls: Type[Fraction], ticks: int, resolution: int) -> "BeatsTime":
|
||||||
if resolution < 1:
|
if resolution < 1:
|
||||||
raise ValueError(f"resolution cannot be negative : {resolution}")
|
raise ValueError(f"resolution cannot be negative : {resolution}")
|
||||||
return cls(ticks, resolution)
|
return cls(ticks, resolution)
|
||||||
@ -36,14 +36,14 @@ class NotePosition:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def index(self):
|
def index(self):
|
||||||
return self.x + 4*self.y
|
return self.x + 4 * self.y
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_index(cls: Type[NotePosition], index: int) -> 'NotePosition':
|
def from_index(cls: Type[NotePosition], index: int) -> "NotePosition":
|
||||||
if not (0 <= index < 16):
|
if not (0 <= index < 16):
|
||||||
raise ValueError(f"Note position index out of range : {index}")
|
raise ValueError(f"Note position index out of range : {index}")
|
||||||
|
|
||||||
return cls(x = index%4, y = index//4)
|
return cls(x=index % 4, y=index // 4)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -69,15 +69,9 @@ class BPMEvent:
|
|||||||
BPM: Decimal
|
BPM: Decimal
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StopEvent:
|
|
||||||
time: BeatsTime
|
|
||||||
duration: BeatsTime
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Timing:
|
class Timing:
|
||||||
events: List[Union[BPMEvent, StopEvent]]
|
events: List[BPMEvent]
|
||||||
beat_zero_offset: SecondsTime
|
beat_zero_offset: SecondsTime
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import gcd
|
from math import gcd
|
||||||
|
|
||||||
|
|
||||||
def single_lcm(a: int, b: int):
|
def single_lcm(a: int, b: int):
|
||||||
"""Return lowest common multiple of two numbers"""
|
"""Return lowest common multiple of two numbers"""
|
||||||
return a * b // gcd(a, b)
|
return a * b // gcd(a, b)
|
||||||
|
|
||||||
|
|
||||||
def lcm(*args):
|
def lcm(*args):
|
||||||
"""Return lcm of args."""
|
"""Return lcm of args."""
|
||||||
return reduce(single_lcm, args)
|
return reduce(single_lcm, args)
|
@ -2,4 +2,4 @@ from jubeatools import __version__
|
|||||||
|
|
||||||
|
|
||||||
def test_version():
|
def test_version():
|
||||||
assert __version__ == '0.1.0'
|
assert __version__ == "0.1.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user