1
0
mirror of synced 2024-12-05 03:27:54 +01:00

pre-debug #memo load and dump !

This commit is contained in:
Stepland 2020-07-18 11:30:40 +02:00
parent 8abdf8edd6
commit 0fc63e08aa
9 changed files with 523 additions and 199 deletions

View File

@ -0,0 +1,214 @@
"""Collection of tools realted to dumping to jubeat analyser formats"""
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass, field
from decimal import Decimal
from fractions import Fraction
from itertools import chain
from typing import Dict, Optional, List, Union, Iterator, Callable, Mapping
from more_itertools import collapse, intersperse, mark_ends, windowed
from sortedcontainers import SortedDict, SortedKeyList
from jubeatools.song import BeatsTime, TapNote, LongNote, NotePosition, Chart, Timing, Metadata
from .command import dump_command
from .symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
COMMAND_ORDER = [
"b",
"t",
"m",
"o",
"r",
"title",
"artist",
"lev",
"dif",
"jacket",
"prevpos",
"holdbyarrow",
"circlefree"
]
BEATS_TIME_TO_SYMBOL = {
BeatsTime(1, 4) * index: symbol for index, symbol in enumerate(NOTE_SYMBOLS)
}
BEATS_TIME_TO_CIRCLE_FREE = {
BeatsTime(1, 4) * index: symbol for index, symbol in enumerate(CIRCLE_FREE_SYMBOLS)
}
NOTE_TO_CIRCLE_FREE_SYMBOL = dict(zip(NOTE_SYMBOLS, CIRCLE_FREE_SYMBOLS))
DIRECTION_TO_ARROW = {
NotePosition(-1, 0): "", # U+FF1E : FULLWIDTH GREATER-THAN SIGN
NotePosition(1, 0): "", # U+FF1C : FULLWIDTH LESS-THAN SIGN
NotePosition(0, -1): "", # U+2228 : LOGICAL OR
NotePosition(0, 1): "", # U+2227 : LOGICAL AND
}
DIRECTION_TO_LINE = {
NotePosition(-1, 0): "", # U+2015 : HORIZONTAL BAR
NotePosition(1, 0): "",
NotePosition(0, -1): "", # U+FF5C : FULLWIDTH VERTICAL LINE
NotePosition(0, 1): "",
}
DIFFICULTIES = {"BSC": 1, "ADV": 2, "EXT": 3}
# I put a FUCKTON of extra characters just in case some insane chart uses
# loads of unusual beat divisions
DEFAULT_EXTRA_SYMBOLS = (
""
""
"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん"
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
)
def fraction_to_decimal(frac: Fraction):
"Thanks stackoverflow ! https://stackoverflow.com/a/40468867/10768117"
return frac.numerator / Decimal(frac.denominator)
@dataclass(frozen=True)
class LongNoteEnd:
time: BeatsTime
position: NotePosition
class SortedDefaultDict(SortedDict):
"""Custom SortedDict that also acts as a defaultdict,
passes the key to the value factory"""
def __init__(self, factory, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__factory__ = factory
def add_key(self, key):
if key not in self:
value = self.__factory__(key)
self.__setitem__(key, value)
def __missing__(self, key):
value = self.__factory__(key)
self.__setitem__(key, value)
return value
@dataclass
class JubeatAnalyserDumpedSection(ABC):
current_beat: BeatsTime
length: Decimal = 4
commands: Dict[str, Optional[str]] = field(default_factory=dict)
symbol_definitions: Dict[BeatsTime, str] = field(default_factory=dict)
symbols: Dict[BeatsTime, str] = field(default_factory=dict)
notes: List[Union[TapNote, LongNote, LongNoteEnd]] = field(default_factory=list)
def render(self, circle_free: bool = False) -> str:
blocs = []
commands = list(self._dump_commands())
if commands:
blocs.append(commands)
symbols = list(self._dump_symbol_definitions())
if symbols:
blocs.append(symbols)
notes = list(self._dump_notes(circle_free))
if notes:
blocs.append(notes)
return "\n".join(collapse([intersperse("", blocs), "--"]))
def _dump_commands(self) -> Iterator[str]:
keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER))
for key in keys:
try:
value = self.commands[key]
except KeyError:
continue
yield dump_command(key, value)
def _dump_symbol_definitions(self) -> Iterator[str]:
for time, symbol in self.symbol_definitions.items():
decimal_time = fraction_to_decimal(time)
yield f"*{symbol}:{decimal_time:.6f}"
@abstractmethod
def _dump_notes(self, circle_free: bool = False,) -> Iterator[str]:
...
def create_sections_from_chart(
section_factory: Callable[[BeatsTime], JubeatAnalyserDumpedSection],
chart: Chart,
difficulty: str,
timing: Timing,
metadata: Metadata,
circle_free: bool
) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]:
sections = SortedDefaultDict(section_factory)
timing_events = sorted(timing.events, key=lambda e: e.time)
notes = SortedKeyList(set(chart.notes), key=lambda n: n.time)
for note in chart.notes:
if isinstance(note, LongNote):
notes.add(LongNoteEnd(note.time + note.duration, note.position))
all_events = SortedKeyList(timing_events + notes, key=lambda n: n.time)
last_event = all_events[-1]
last_measure = last_event.time // 4
for i in range(last_measure + 1):
beat = BeatsTime(4) * i
sections.add_key(beat)
header = sections[0].commands
header["o"] = int(timing.beat_zero_offset * 1000)
header["lev"] = int(chart.level)
header["dif"] = DIFFICULTIES.get(difficulty, 1)
if metadata.audio:
header["m"] = metadata.audio
if metadata.title:
header["title"] = metadata.title
if metadata.artist:
header["artist"] = metadata.artist
if metadata.cover:
header["jacket"] = metadata.cover
if metadata.preview is not None:
header["prevpos"] = int(metadata.preview.start * 1000)
if any(isinstance(note, LongNote) for note in chart.notes):
header["holdbyarrow"] = 1
if circle_free:
header["circlefree"] = 1
# Potentially create sub-sections for bpm changes
for event in timing_events:
sections[event.time].commands["t"] = event.BPM
# First, Set every single b=… value
for key, next_key in windowed(chain(sections.keys(), [None]), 2):
if next_key is None:
length = 4
else:
length = fraction_to_decimal(next_key - key)
sections[key].commands["b"] = length
sections[key].length = length
# Then, trim all the redundant b=…
last_b = 4
for section in sections.values():
current_b = section.commands["b"]
if current_b == last_b:
del section.commands["b"]
else:
last_b = current_b
# Fill sections with notes
for key, next_key in windowed(chain(sections.keys(), [None]), 2):
sections[key].notes = list(
notes.irange_key(min_key=key, max_key=next_key, inclusive=(True, False))
)
return sections

View File

@ -1,4 +1,4 @@
"""Collection of tools that are common to all the jubeat analyser formats"""
"""Collection of parsing tools that are common to all the jubeat analyser formats"""
import re
from collections import Counter
from copy import deepcopy

View File

@ -1,2 +1,250 @@
def dump_memo(song):
from collections import ChainMap, defaultdict
from copy import deepcopy
from dataclasses import dataclass, field
from decimal import Decimal
from fractions import Fraction
from functools import partial
from io import StringIO
from itertools import chain, zip_longest
from math import ceil
from typing import Dict, Iterator, List, Optional, Tuple, Union, Set
from more_itertools import chunked, collapse, intersperse, mark_ends, windowed
from path import Path
from sortedcontainers import SortedKeyList
from jubeatools import __version__
from jubeatools.formats.filetypes import ChartFile, JubeatFile
from jubeatools.song import (
BeatsTime,
Chart,
LongNote,
Metadata,
NotePosition,
Song,
TapNote,
Timing,
)
from ..command import dump_command
from ..dump_tools import (
BEATS_TIME_TO_SYMBOL,
COMMAND_ORDER,
DEFAULT_EXTRA_SYMBOLS,
DIFFICULTIES,
DIRECTION_TO_ARROW,
DIRECTION_TO_LINE,
NOTE_TO_CIRCLE_FREE_SYMBOL,
JubeatAnalyserDumpedSection,
create_sections_from_chart,
LongNoteEnd,
SortedDefaultDict,
fraction_to_decimal,
)
from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
AnyNote = Union[TapNote, LongNote, LongNoteEnd]
EMPTY_BEAT_SYMBOL = "" # U+0FF0D : FULLWIDTH HYPHEN-MINUS
EMPTY_POSITION_SYMBOL = "" # U+025A1 : WHITE SQUARE
@dataclass
class Frame:
positions: Dict[NotePosition, str] = field(default_factory=dict)
bars: Dict[int, Dict[int, str]] = field(default_factory=dict)
def dump(self, length: Decimal) -> Iterator[str]:
# Check that bars are contiguous
for a, b in windowed(sorted(self.bars), 2):
if b is not None and b-a != 1:
raise ValueError("Frame has discontinuous bars")
# Check all bars are in the same 4-bar group
if self.bars.keys() != set(bar%4 for bar in self.bars):
raise ValueError("Frame contains bars from different 4-bar groups")
for pos, bar in zip_longest(self.dump_positions(), self.dump_bars(length)):
if bar is None:
bar = ""
yield f"{pos} {bar}"
def dump_positions(self) -> Iterator[str]:
for y in range(4):
yield "".join(self.positions.get(NotePosition(x, y), EMPTY_POSITION_SYMBOL) for x in range(4))
def dump_bars(self, length: Decimal) -> Iterator[str]:
all_bars = []
for i in range(ceil(length * 4)):
bar_index = i // 4
time_index = i % 4
symbol = self.bars.get(bar_index, {}).get(time_index, EMPTY_BEAT_SYMBOL)
all_bars.append(symbol)
for i, bar in enumerate(chunked(all_bars, 4)):
if i in self.bars:
yield f"|{''.join(bar)}|"
else:
yield ""
class MemoDumpedSection(JubeatAnalyserDumpedSection):
def _dump_notes(self, circle_free: bool = False) -> Iterator[str]:
notes_by_bar: Dict[int, List[AnyNote]] = defaultdict(list)
bars: Dict[int, Dict[int, str]] = defaultdict(dict)
chosen_symbols: Dict[BeatsTime, str] = {}
symbols_iterator = iter(NOTE_SYMBOLS)
for note in self.notes:
time_in_section = note.time - self.current_beat
bar_index = int(time_in_section)
notes_by_bar[bar_index].append(note)
if time_in_section % Fraction(1, 4) == 0:
time_index = int(time_in_section * 4)
if time_index not in bars[bar_index]:
symbol = next(symbols_iterator)
chosen_symbols[time_in_section] = symbol
bars[bar_index][time_index] = symbol
elif time_in_section not in self.symbols:
raise ValueError(f"No symbol defined for time in section : {time_in_section}")
# Create frame by bar
section_symbols = ChainMap(chosen_symbols, self.symbols)
frames_by_bar: Dict[int, List[Frame]] = defaultdict(list)
for bar_index in range(ceil(self.length)):
bar = bars.get(bar_index, {})
frame = Frame()
frame.bars[bar_index] = bar
for note in notes_by_bar[bar_index]:
time_in_section = note.time - self.current_beat
symbol = section_symbols[time_in_section]
if isinstance(note, TapNote):
if note.position in frame.positions:
frames_by_bar[bar_index].append(frame)
frame = Frame()
frame.positions[note.position] = symbol
elif isinstance(note, LongNote):
needed_positions = set(note.positions_covered())
if needed_positions & frame.positions.keys():
frames_by_bar[bar_index].append(frame)
frame = Frame()
direction = note.tail_direction()
arrow = DIRECTION_TO_ARROW[direction]
line = DIRECTION_TO_LINE[direction]
for is_first, is_last, pos in mark_ends(note.positions_covered()):
if is_first:
frame.positions[pos] = symbol
elif is_last:
frame.positions[pos] = arrow
else:
frame.positions[pos] = line
elif isinstance(note, LongNoteEnd):
if note.position in frame.positions:
frames_by_bar[bar_index].append(frame)
frame = Frame()
if circle_free and symbol in NOTE_TO_CIRCLE_FREE_SYMBOL:
symbol = NOTE_TO_CIRCLE_FREE_SYMBOL[symbol]
frame.positions[note.position] = symbol
frames_by_bar[bar_index].append(frame)
# Merge bar-specific frames is possible
final_frames: List[Frame] = []
for bar_index in range(ceil(self.length)):
frames = frames_by_bar[bar_index]
# Merge if :
# - No split in current bar (only one frame)
# - There is a previous frame
# - The previous frame is not a split frame (it holds a bar)
# - The previous and current bars are all in the same 4-bar group
# - The note positions in the previous frame do not clash with the current frame
if (
len(frames) == 1 and
final_frames and
final_frames[-1].bars and
max(final_frames[-1].bars.keys()) // 4 == min(frames[0].bars.keys()) // 4 and
(not (final_frames[-1].positions.keys() & frames[0].positions.keys()))
):
final_frames[-1].bars.update(frames[0].bars)
final_frames[-1].positions.update(frames[0].positions)
else:
final_frames.extend(frames)
dumped_frames = map(lambda f: f.dump(self.length), final_frames)
yield from collapse(intersperse("", dumped_frames))
def _raise_if_unfit_for_memo(chart: Chart, timing: Timing, circle_free: bool = False):
if len(timing.events) < 1:
raise ValueError("No BPM found in file") from None
first_bpm = min(timing.events, key=lambda e: e.time)
if first_bpm.time != 0:
raise ValueError("First BPM event does not happen on beat zero")
if any(
not note.tail_is_straight()
for note in chart.notes
if isinstance(note, LongNote)
):
raise ValueError(
"Chart contains diagonal long notes, reprensenting these in"
" memo format is not supported by jubeatools"
)
def _dump_memo_chart(
difficulty: str,
chart: Chart,
metadata: Metadata,
timing: Timing,
circle_free: bool = False,
) -> StringIO:
_raise_if_unfit_for_memo(chart, timing, circle_free)
sections = create_sections_from_chart(MemoDumpedSection, chart, difficulty, timing, metadata, circle_free)
# Jubeat Analyser format command
sections[0].commands["memo"] = None
# Define extra symbols
existing_symbols: Dict[BeatsTime, str] = {}
extra_symbols = iter(DEFAULT_EXTRA_SYMBOLS)
for section_start, section in sections.items():
# intentionally not a copy : at the end of this loop every section
# holds a reference to a dict containing every defined symbol
section.symbols = existing_symbols
for note in section.notes:
time_in_section = note.time - section_start
if time_in_section % Fraction(1, 4) != 0 and time_in_section not in existing_symbols:
new_symbol = next(extra_symbols)
section.symbol_definitions[time_in_section] = new_symbol
existing_symbols[time_in_section] = new_symbol
# Actual output to file
file = StringIO()
file.write(f"// Converted using jubeatools {__version__}\n")
file.write(f"// https://github.com/Stepland/jubeatools\n\n")
for _, section in sections.items():
file.write(section.render(circle_free) + "\n")
return file
def _dump_memo_internal(song: Song, circle_free: bool = False) -> List[JubeatFile]:
files = []
for difficulty, chart in song.charts.items():
contents = _dump_memo_chart(
difficulty,
chart,
song.metadata,
chart.timing or song.global_timing,
circle_free,
)
files.append(ChartFile(contents, song, difficulty, chart))
return files
def dump_memo(song: Song, circle_free: bool, folder: Path, name_pattern: str = None):
...

View File

@ -5,10 +5,10 @@ from dataclasses import dataclass
from decimal import Decimal
from functools import reduce
from itertools import chain, product, zip_longest
from typing import Mapping, Dict, Iterator, List, Optional, Set, Tuple, Union
from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union
import constraint
from more_itertools import mark_ends
from more_itertools import mark_ends, collapse
from parsimonious import Grammar, NodeVisitor, ParseError
from path import Path
@ -25,7 +25,7 @@ from jubeatools.song import (
from ..command import is_command, parse_command
from ..files import load_files
from ..parser import (
from ..load_tools import (
CIRCLE_FREE_TO_DECIMAL_TIME,
CIRCLE_FREE_TO_NOTE_SYMBOL,
LONG_ARROWS,
@ -44,8 +44,9 @@ from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
memo_chart_line_grammar = Grammar(
r"""
line = ws position_part ws (timing_part ws)? comment?
position_part = ~r"[^*#:|/\s]{4,8}"
timing_part = "|" ~r"[^*#:|/\s]+" "|"
position_part = ~r"[^*#:|/\s]{4,8}"
timing_part = bar ~r"[^*#:|/\s]+" bar
bar = "|" / ""
ws = ~r"[\t ]*"
comment = ~r"//.*"
"""
@ -141,16 +142,16 @@ class MemoLoadedSection:
# - encodable in shift_jis_2004
# Gets added to the list of characters to be ignored in the timing section
EMPTY_BEAT_SYMBOLS = {
"", # U+04E00 - CJK UNIFIED IDEOGRAPH-4E00
"", # U+0FF0D - FULLWIDTH HYPHEN-MINUS
"", # U+030FC - KATAKANA-HIRAGANA PROLONGED SOUND MARK
"", # U+02500 - BOX DRAWINGS LIGHT HORIZONTAL
"", # U+02015 - HORIZONTAL BAR
"", # U+02501 - BOX DRAWINGS HEAVY HORIZONTAL
"", # U+02013 - EN DASH
"", # U+02010 - HYPHEN
"-", # U+0002D - HYPHEN-MINUS
"", # U+02212 - MINUS SIGN
"", # U+4E00 - CJK UNIFIED IDEOGRAPH-4E00
"", # U+FF0D - FULLWIDTH HYPHEN-MINUS
"", # U+30FC - KATAKANA-HIRAGANA PROLONGED SOUND MARK
"", # U+2500 - BOX DRAWINGS LIGHT HORIZONTAL
"", # U+2015 - HORIZONTAL BAR
"", # U+2501 - BOX DRAWINGS HEAVY HORIZONTAL
"", # U+2013 - EN DASH
"", # U+2010 - HYPHEN
"-", # U+002D - HYPHEN-MINUS
"", # U+2212 - MINUS SIGN
}
@ -216,6 +217,7 @@ class MemoParser(JubeatAnalyserParser):
self._push_section()
self.frames.append(frame)
self.current_chart_lines = []
def _push_section(self):
self.sections.append(
@ -227,7 +229,6 @@ class MemoParser(JubeatAnalyserParser):
)
)
self.frames = []
self.current_chart_lines = []
self.section_starting_beat += self.beats_per_section
def finish_last_few_notes(self):
@ -277,7 +278,7 @@ class MemoParser(JubeatAnalyserParser):
if frame.timing_part:
local_symbols = {
symbol: Decimal("0.25") * i
for i, symbol in enumerate(chain(frame.timing_part))
for i, symbol in enumerate(collapse(frame.timing_part))
if symbol not in EMPTY_BEAT_SYMBOLS
}
currently_defined_symbols = ChainMap(local_symbols, section.symbols)

View File

@ -3,13 +3,14 @@ from copy import deepcopy
from dataclasses import dataclass, field
from decimal import Decimal
from fractions import Fraction
from functools import partial
from io import StringIO
from itertools import chain
from typing import Dict, Iterator, List, Optional, Tuple
from typing import Dict, Iterator, List, Mapping, Optional, Tuple
from more_itertools import collapse, intersperse, mark_ends, windowed
from path import Path
from sortedcontainers import SortedDict, SortedKeyList, SortedSet
from sortedcontainers import SortedKeyList
from jubeatools import __version__
from jubeatools.formats.filetypes import ChartFile, JubeatFile
@ -24,97 +25,23 @@ from jubeatools.song import (
Timing,
)
from ..command import dump_command
from ..dump_tools import (
BEATS_TIME_TO_SYMBOL,
COMMAND_ORDER,
DEFAULT_EXTRA_SYMBOLS,
DIFFICULTIES,
DIRECTION_TO_ARROW,
DIRECTION_TO_LINE,
JubeatAnalyserDumpedSection,
create_sections_from_chart,
LongNoteEnd,
SortedDefaultDict,
fraction_to_decimal,
)
from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
COMMAND_ORDER = [
"b",
"t",
"m",
"o",
"r",
"title",
"artist",
"lev",
"dif",
"jacket",
"prevpos",
]
BEATS_TIME_TO_SYMBOL = {
BeatsTime(1, 4) * index: symbol for index, symbol in enumerate(NOTE_SYMBOLS)
}
BEATS_TIME_TO_CIRCLE_FREE = {
BeatsTime(1, 4) * index: symbol for index, symbol in enumerate(CIRCLE_FREE_SYMBOLS)
}
DIRECTION_TO_ARROW = {
NotePosition(-1, 0): "", # U+FF1E : FULLWIDTH GREATER-THAN SIGN
NotePosition(1, 0): "", # U+FF1C : FULLWIDTH LESS-THAN SIGN
NotePosition(0, -1): "", # U+2228 : LOGICAL OR
NotePosition(0, 1): "", # U+2227 : LOGICAL AND
}
DIRECTION_TO_LINE = {
NotePosition(-1, 0): "", # U+2015 : HORIZONTAL BAR
NotePosition(1, 0): "",
NotePosition(0, -1): "", # U+FF5C : FULLWIDTH VERTICAL LINE
NotePosition(0, 1): "",
}
DIFFICULTIES = {"BSC": 1, "ADV": 2, "EXT": 3}
# I put a FUCKTON of extra characters just in case some insane chart uses
# loads of unusual beat divisions
DEFAULT_EXTRA_SYMBOLS = (
""
""
"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん"
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
)
def fraction_to_decimal(frac: Fraction):
"Thanks stackoverflow ! https://stackoverflow.com/a/40468867/10768117"
return frac.numerator / Decimal(frac.denominator)
@dataclass
class MonoColumnDumpedSection:
current_beat: BeatsTime
commands: Dict[str, Optional[str]] = field(default_factory=dict)
symbol_definitions: Dict[BeatsTime, str] = field(default_factory=dict)
symbols: Dict[BeatsTime, str] = field(default_factory=dict)
notes: List[TapNote] = field(default_factory=list)
def render(self, circle_free: bool = False) -> str:
blocs = []
commands = list(self._dump_commands())
if commands:
blocs.append(commands)
symbols = list(self._dump_symbol_definitions())
if symbols:
blocs.append(symbols)
notes = list(self._dump_notes(circle_free))
if notes:
blocs.append(notes)
return "\n".join(collapse([intersperse("", blocs), "--"]))
def _dump_commands(self) -> Iterator[str]:
keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER))
for key in keys:
try:
value = self.commands[key]
except KeyError:
continue
yield dump_command(key, value)
def _dump_symbol_definitions(self) -> Iterator[str]:
for time, symbol in self.symbol_definitions.items():
decimal_time = fraction_to_decimal(time)
yield f"*{symbol}:{decimal_time:.6f}"
class MonoColumnDumpedSection(JubeatAnalyserDumpedSection):
def _dump_notes(self, circle_free: bool = False,) -> Iterator[str]:
frames: List[Dict[NotePosition, str]] = []
frame: Dict[NotePosition, str] = {}
@ -165,28 +92,6 @@ class MonoColumnDumpedSection:
yield "".join(frame.get(NotePosition(x, y), "") for x in range(4))
class Sections(SortedDict):
"""Custom SortedDict that also acts as a defaultdict of
MonoColumnDumpedSection"""
def add_section(self, beat):
if beat not in self:
section = MonoColumnDumpedSection(beat)
self.__setitem__(beat, section)
def __missing__(self, beat):
section = MonoColumnDumpedSection(beat)
self.__setitem__(beat, section)
return section
@dataclass(frozen=True)
class LongNoteEnd:
time: BeatsTime
position: NotePosition
def _raise_if_unfit_for_mono_column(
chart: Chart, timing: Timing, circle_free: bool = False
):
@ -217,7 +122,6 @@ def _raise_if_unfit_for_mono_column(
" representable in #circlefree mode"
)
def _dump_mono_column_chart(
difficulty: str,
chart: Chart,
@ -228,65 +132,8 @@ def _dump_mono_column_chart(
_raise_if_unfit_for_mono_column(chart, timing, circle_free)
timing_events = sorted(timing.events, key=lambda e: e.time)
notes = SortedKeyList(set(chart.notes), key=lambda n: n.time)
for note in chart.notes:
if isinstance(note, LongNote):
notes.add(LongNoteEnd(note.time + note.duration, note.position))
all_events = SortedKeyList(timing_events + notes, key=lambda n: n.time)
last_event = all_events[-1]
last_measure = last_event.time // 4
sections = Sections()
for i in range(last_measure + 1):
beat = BeatsTime(4) * i
sections.add_section(beat)
header = sections[0].commands
header["o"] = int(timing.beat_zero_offset * 1000)
header["lev"] = int(chart.level)
header["dif"] = DIFFICULTIES.get(difficulty, 1)
if metadata.audio:
header["m"] = metadata.audio
if metadata.title:
header["title"] = metadata.title
if metadata.artist:
header["artist"] = metadata.artist
if metadata.cover:
header["jacket"] = metadata.cover
if metadata.preview is not None:
header["prevpos"] = int(metadata.preview.start * 1000)
if any(isinstance(note, LongNote) for note in chart.notes):
header["holdbyarrow"] = 1
# Potentially create sub-sections for bpm changes
for event in timing_events:
sections[event.time].commands["t"] = event.BPM
# First, Set every single b=… value
for key, next_key in windowed(chain(sections.keys(), [None]), 2):
if next_key is None:
sections[key].commands["b"] = 4
else:
sections[key].commands["b"] = fraction_to_decimal(next_key - key)
# Then, trim all the redundant b=…
last_b = 4
for section in sections.values():
current_b = section.commands["b"]
if current_b == last_b:
del section.commands["b"]
else:
last_b = current_b
# Fill sections with notes
for key, next_key in windowed(chain(sections.keys(), [None]), 2):
sections[key].notes = list(
notes.irange_key(min_key=key, max_key=next_key, inclusive=(True, False))
)
sections = create_sections_from_chart(MonoColumnDumpedSection, chart, difficulty, timing, metadata, circle_free)
# Define extra symbols
existing_symbols = deepcopy(BEATS_TIME_TO_SYMBOL)
extra_symbols = iter(DEFAULT_EXTRA_SYMBOLS)
@ -305,7 +152,7 @@ def _dump_mono_column_chart(
file = StringIO()
file.write(f"// Converted using jubeatools {__version__}\n")
file.write(f"// https://github.com/Stepland/jubeatools\n\n")
for section_start, section in sections.items():
for _, section in sections.items():
file.write(section.render(circle_free) + "\n")
return file

View File

@ -29,7 +29,7 @@ from jubeatools.song import (
from ..command import is_command, parse_command
from ..files import load_files
from ..parser import (
from ..load_tools import (
CIRCLE_FREE_TO_DECIMAL_TIME,
LONG_ARROWS,
LONG_DIRECTION,

View File

@ -6,10 +6,24 @@ from jubeatools.song import BeatsTime, BPMEvent, Chart, Metadata, SecondsTime, T
from jubeatools.testutils.strategies import NoteOption
from jubeatools.testutils.strategies import notes as notes_strat
from ..mono_column.dump import _dump_mono_column_chart
from ..mono_column.load import MonoColumnParser
from ..memo.dump import _dump_memo_chart
from ..memo.load import MemoParser
@given(notes_strat(NoteOption.LONGS))
def test_many_notes(notes):
...
timing = Timing(
events=[BPMEvent(BeatsTime(0), Decimal(120))], beat_zero_offset=SecondsTime(0)
)
chart = Chart(
level=0, timing=timing, notes=sorted(notes, key=lambda n: (n.time, n.position))
)
metadata = Metadata("", "", "", "")
string_io = _dump_memo_chart("", chart, metadata, timing)
chart = string_io.getvalue()
parser = MemoParser()
for line in chart.split("\n"):
parser.load_line(line)
parser.finish_last_few_notes()
actual = set(parser.notes())
assert notes == actual

View File

@ -167,7 +167,7 @@ class Song:
charts: Mapping[str, Chart] = field(default_factory=MultiDict)
global_timing: Optional[Timing] = None
def merge(self, other: Song) -> Song:
def merge(self, other: "Song") -> "Song":
if self.metadata != other.metadata:
raise ValueError(
"Merge conflit in song metadata :\n"

View File

@ -94,7 +94,7 @@ def bad_notes(draw, longs: bool):
note_strat = tap_note()
if longs:
note_strat = st.one_of(note_strat, long_note())
return draw(st.sets(note_strat, max_size=50))
return draw(st.sets(note_strat, max_size=32))
@st.composite
@ -105,7 +105,7 @@ def notes(draw, options: NoteOption):
note_strat = tap_note()
if NoteOption.LONGS in options:
note_strat = st.one_of(note_strat, long_note())
raw_notes = draw(st.sets(note_strat, max_size=50))
raw_notes = draw(st.sets(note_strat, max_size=32))
if NoteOption.COLLISIONS in options:
return raw_notes