def compute_density_graph(events: List[konami.Event], end_time: int) -> List[int]: events_by_type = group_by(events, lambda e: e.command) buckets: DefaultDict[int, int] = defaultdict(int) for tap in events_by_type[konami.Command.PLAY]: bucket = int((tap.time / end_time) * 120) buckets[bucket] += 1 for long in events_by_type[konami.Command.LONG]: press_bucket = int((long.time / end_time) * 120) buckets[press_bucket] += 1 duration = konami.EveLong.from_value(long.value).duration release_time = long.time + duration release_bucket = int((release_time / end_time) * 120) buckets[release_bucket] += 1 res = [] for i in range(0, 120, 2): # The jbsq density graph in a array of nibbles, the twist is that for # some obscure reason each pair of nibbles is swapped in the byte ... # little-endianness is a hell of a drug, don't do drugs kids ... first_nibble = min(buckets[i], 15) second_nibble = min(buckets[i + 1], 15) density_byte = (second_nibble << 4) + first_nibble res.append(density_byte) return res
def naive_approach(beats: song.Timing, beat: song.BeatsTime) -> Fraction: if beat < 0: raise ValueError("Can't compute seconds at negative beat") if not beats.events: raise ValueError("No BPM defined") grouped_by_time = group_by(beats.events, key=lambda e: e.time) for time, events in grouped_by_time.items(): if len(events) > 1: raise ValueError( f"Multiple BPMs defined on beat {time} : {events}") sorted_events = sorted(beats.events, key=lambda e: e.time) first_event = sorted_events[0] if first_event.time != song.BeatsTime(0): raise ValueError("First BPM event is not on beat zero") if beat > sorted_events[-1].time: events_before = sorted_events else: last_index = next(i for i, e in enumerate(sorted_events) if e.time >= beat) events_before = sorted_events[:last_index] total_seconds = Fraction(0) current_beat = beat for event in reversed(events_before): beats_since_previous = current_beat - event.time seconds_since_previous = (60 * beats_since_previous) / Fraction( event.BPM) total_seconds += seconds_since_previous current_beat = event.time total_seconds = total_seconds + Fraction(beats.beat_zero_offset) return total_seconds
def from_seconds(cls, events: List[BPMAtSecond]) -> TimeMap: """Create a time map from a list of BPM changes with time positions given in seconds. The first BPM implicitely happens at beat zero""" if not events: raise ValueError("No BPM defined") grouped_by_time = group_by(events, key=lambda e: e.seconds) for time, events_at_time in grouped_by_time.items(): if len(events_at_time) > 1: raise ValueError(f"Multiple BPMs defined at {time} seconds : {events}") # take the first BPM change then compute from there sorted_events = sorted(events, key=lambda e: e.seconds) first_event = sorted_events[0] current_beat = Fraction(0) bpm_changes = [BPMChange(current_beat, first_event.seconds, first_event.BPM)] for previous, current in windowed(sorted_events, 2): if previous is None or current is None: continue seconds_since_last_event = current.seconds - previous.seconds beats_since_last_event = ( previous.BPM * seconds_since_last_event ) / Fraction(60) current_beat += beats_since_last_event bpm_change = BPMChange(current_beat, current.seconds, current.BPM) bpm_changes.append(bpm_change) return cls( events_by_beats=SortedKeyList(bpm_changes, key=lambda b: b.beats), events_by_seconds=SortedKeyList(bpm_changes, key=lambda b: b.seconds), )
def from_beats(cls, events: List[BPMAtBeat], offset: SecondsAtBeat) -> TimeMap: """Create a time map from a list of BPM changes with times given in beats, the offset parameter is more flexible than a "regular" beat zero offset as it accepts non-zero beats""" if not events: raise ValueError("No BPM defined") grouped_by_time = group_by(events, key=lambda e: e.beats) for time, events_at_time in grouped_by_time.items(): if len(events_at_time) > 1: raise ValueError(f"Multiple BPMs defined at beat {time} : {events}") # First compute everything as if the first BPM change happened at # zero seconds, then shift according to the offset sorted_events = sorted(events, key=lambda e: e.beats) first_event = sorted_events[0] current_second = Fraction(0) bpm_changes = [ BPMChange(first_event.beats, current_second, Fraction(first_event.BPM)) ] for previous, current in windowed(sorted_events, 2): if previous is None or current is None: continue beats_since_last_event = current.beats - previous.beats seconds_since_last_event = (60 * beats_since_last_event) / Fraction( previous.BPM ) current_second += seconds_since_last_event bpm_change = BPMChange(current.beats, current_second, Fraction(current.BPM)) bpm_changes.append(bpm_change) not_shifted = cls( events_by_beats=SortedKeyList(bpm_changes, key=lambda b: b.beats), events_by_seconds=SortedKeyList(bpm_changes, key=lambda b: b.seconds), ) unshifted_seconds_at_offset = not_shifted.fractional_seconds_at(offset.beats) shift = offset.seconds - unshifted_seconds_at_offset shifted_bpm_changes = [ replace(b, seconds=b.seconds + shift) for b in bpm_changes ] return cls( events_by_beats=SortedKeyList(shifted_bpm_changes, key=lambda b: b.beats), events_by_seconds=SortedKeyList( shifted_bpm_changes, key=lambda b: b.seconds ), )
def make_chart_from_events(events: Iterable[Event], beat_snap: int = 240) -> song.Chart: events_by_command = group_by(events, lambda e: e.command) bpms = [ BPMAtSecond(seconds=ticks_to_seconds(e.time), BPM=value_to_truncated_bpm(e.value)) for e in sorted(events_by_command[Command.TEMPO]) ] time_map = TimeMap.from_seconds(bpms) tap_notes: List[AnyNote] = [ make_tap_note(e.time, e.value, time_map, beat_snap) for e in events_by_command[Command.PLAY] ] long_notes: List[AnyNote] = [ make_long_note(e.time, e.value, time_map, beat_snap) for e in events_by_command[Command.LONG] ] 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) return song.Chart(level=Decimal(0), timing=timing, notes=all_notes)
def compute_max_combo(notes: List[AnyNote]) -> int: notes_by_type = group_by(notes, type) tap_notes = len(notes_by_type[song.TapNote]) long_notes = len(notes_by_type[song.LongNote]) return tap_notes + 2 * long_notes