2019-12-08 22:43:49 +01:00
|
|
|
import struct
|
|
|
|
from typing import List, Optional, Tuple
|
2021-09-07 19:57:18 +02:00
|
|
|
from typing_extensions import Final
|
2019-12-08 22:43:49 +01:00
|
|
|
|
|
|
|
|
|
|
|
class IIDXChart:
|
|
|
|
"""
|
|
|
|
Class representing a IIDX chart. This is known to be iffy with charge notes
|
|
|
|
and hell charge notes, but I never investigated enough to fix it. If somebody
|
|
|
|
wants to dig in and make a patch that would be excellent. This currently only
|
|
|
|
allows fetching notecounts and bpm since this is necessary for calculating
|
|
|
|
clear ranks for IIDX.
|
|
|
|
"""
|
|
|
|
|
2021-09-07 19:57:18 +02:00
|
|
|
CHART_POSITIONS: Final[List[int]] = [1, 0, 2, 7, 6, 8]
|
2019-12-08 22:43:49 +01:00
|
|
|
|
|
|
|
def __init__(self, data: bytes) -> None:
|
|
|
|
self.__bpm_min: Optional[int] = None
|
|
|
|
self.__bpm_max: Optional[int] = None
|
|
|
|
self.__note_counts = [0, 0, 0, 0, 0, 0]
|
|
|
|
self.__parse_charts(data)
|
|
|
|
|
|
|
|
def __parse_header(self, data: bytes) -> List[Tuple[int, int]]:
|
|
|
|
header: List[Tuple[int, int]] = []
|
|
|
|
for i in range(12):
|
|
|
|
offset, length = struct.unpack('<II', data[(i * 8):((i + 1) * 8)])
|
|
|
|
header.append((offset, length))
|
|
|
|
return header
|
|
|
|
|
|
|
|
def __parse_charts(self, data: bytes) -> None:
|
|
|
|
header = self.__parse_header(data)
|
|
|
|
|
|
|
|
for chart in [0, 1, 2, 3, 4, 5]:
|
|
|
|
offset, length = header[self.CHART_POSITIONS[chart]]
|
|
|
|
chartdata = data[offset:(offset + length)]
|
|
|
|
position = 0
|
|
|
|
|
|
|
|
if length == 0:
|
|
|
|
# Some songs don't have all charts :(
|
|
|
|
continue
|
|
|
|
|
|
|
|
while True:
|
|
|
|
time, event, side, value = struct.unpack('<iBBH', chartdata[(position * 8):((position + 1) * 8)])
|
|
|
|
position += 1
|
|
|
|
|
|
|
|
if time == 0x7FFFFFFF:
|
|
|
|
break
|
|
|
|
|
|
|
|
if event == 0 or event == 1:
|
|
|
|
# Note!
|
|
|
|
self.__note_counts[chart] += 1
|
|
|
|
if value != 0:
|
|
|
|
# Add one more for charge note lift
|
|
|
|
self.__note_counts[chart] += 1
|
|
|
|
|
|
|
|
if event == 4:
|
|
|
|
# BPM change
|
|
|
|
if value > 1000:
|
|
|
|
value = int(value / 100)
|
|
|
|
|
|
|
|
if self.__bpm_min is None:
|
|
|
|
self.__bpm_min = value
|
|
|
|
else:
|
|
|
|
self.__bpm_min = min(self.__bpm_min, value)
|
|
|
|
if self.__bpm_max is None:
|
|
|
|
self.__bpm_max = value
|
|
|
|
else:
|
|
|
|
self.__bpm_max = max(self.__bpm_max, value)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def bpm(self) -> Tuple[int, int]:
|
2021-05-31 20:13:43 +02:00
|
|
|
if self.__bpm_min is None or self.__bpm_max is None:
|
|
|
|
raise Exception("BPM change was not found in the chart!")
|
2019-12-08 22:43:49 +01:00
|
|
|
return (self.__bpm_min, self.__bpm_max)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def notecounts(self) -> List[int]:
|
|
|
|
return self.__note_counts
|