diff --git a/__init__.py b/__init__.py index 62b5e275894e676449b4361c4a87859f7ecd38fc..8d0ef459a6ebe62a5a6851257b928fc0a5b93251 100644 --- a/__init__.py +++ b/__init__.py @@ -33,8 +33,8 @@ class PetalHero(Application): self.loaded = False self.blm = None self.fiba_sound = None - self.select = select.SelectView(self.app) self.after_score = False + self.select = None #self.blm_extra = bl00mbox.Channel("Petal Hero Extra") #self.blm_extra.background_mute_override = True @@ -168,6 +168,8 @@ class PetalHero(Application): if self.input.buttons.app.middle.pressed: utils.play_go(self.app) + if not self.select: + self.select = select.SelectView(self.app) self.vm.push(self.select, ViewTransitionSwipeLeft()) def on_enter(self, vm) -> None: diff --git a/difficulty.py b/difficulty.py index 05e26ef20f2740fee21436183f52db4c61bb81a1..1fb18f7bfa84028c17ec3825e5ca17a59f811513 100644 --- a/difficulty.py +++ b/difficulty.py @@ -52,6 +52,11 @@ class DifficultyView(BaseView): ctx.text_baseline = ctx.MIDDLE ctx.move_to(0, 0) + if not self.song.difficulties: + ctx.gray(0.0) + ctx.font_size = 24 + ctx.text("No guitar track found!") + for idx, diff in enumerate(self.song.difficulties): target = idx == self._sc.target_position() if target: @@ -121,8 +126,9 @@ class DifficultyView(BaseView): if self.input.buttons.app.middle.pressed: utils.play_go(self.app) - media.stop() - self.vm.replace(loading.LoadingView(self.app, self.song, self.song.difficulties[self._sc.target_position()]), ViewTransitionBlend()) + if self.song.difficulties: + media.stop() + self.vm.replace(loading.LoadingView(self.app, self.song, self.song.difficulties[self._sc.target_position()]), ViewTransitionBlend()) def on_exit(self): super().on_exit() diff --git a/readme.py b/readme.py index 1de3855ba4b5af60700aaae836ff10db4e29e7e6..63998f3e9e1e8aaacf4d2038df160b3c87202fe9 100644 --- a/readme.py +++ b/readme.py @@ -3,8 +3,10 @@ import os README = """Put your Petal Hero songs here. -Petal Hero is compatible with songs for Frets on Fire, but with -one caveat: you need to mix audio tracks and save them as MP3. +Petal Hero is compatible with songs for Frets on Fire, FoFiX, Performous, +Phase Shift and Clone Hero (MIDI) that contain a guitar track, but with one +caveat: you need to mix audio tracks and save them as MP3. + This should do: sox -m *.ogg -c 1 -C 128 -r 48k song.mp3 norm -3 diff --git a/select.py b/select.py index 9a5317cb28fb1bcd157dbd80f869fba0fdbbb9fd..6c597dea6df651da14a3cd28a5543d327f3ab820 100644 --- a/select.py +++ b/select.py @@ -13,6 +13,21 @@ import difficulty import songinfo import utils +class LazySong(songinfo.SongInfo): + def __init__(self, dirpath): + self.dirpath = dirpath + self.loaded = False + + def load(self): + if not self.loaded: + try: + super().__init__(self.dirpath) + except Exception as e: + print(f"Failed to read the song from {inipath}: {e}") + self.loaded = True + return self + + def discover_songs(path: str): path = path.rstrip("/") try: @@ -37,13 +52,10 @@ def discover_songs(path: str): except Exception: continue - try: - s = songinfo.SongInfo(dirpath) - except Exception as e: - print(f"Failed to read the song from {inipath}: {e}") - continue - - inipath = dirpath + "/diffs.pet" + s = LazySong(dirpath) + songs.append(s) + + inipath = dirpath + "/.diff.pet" try: st = os.stat(inipath) if not stat.S_ISREG(st[0]): @@ -51,8 +63,6 @@ def discover_songs(path: str): except Exception: to_process.append(s) - songs.append(s) - return songs, to_process @@ -81,6 +91,7 @@ class SelectView(BaseView): ctx.restore() if self.processing_now: + self.processing_now.load() self.processing_now.getDifficulties() self.processing_now.saveDifficulties() self.processing_now = None @@ -144,6 +155,7 @@ class SelectView(BaseView): distance = self._sc.current_position() - idx if abs(distance) <= 3: + song.load() xpos = 0.0 ctx.font_size = 24 - abs(distance) * 3 if target and (width := ctx.text_width(song.name)) > 220: @@ -209,7 +221,8 @@ class SelectView(BaseView): if pos > len(self.songs) - 1: pos = len(self.songs) - 1 if pos != cur_target: - media.load(self.songs[pos].dirName + "/song.mp3") + song = self.songs[pos].load() + media.load(song.dirName + "/song.mp3") if media.get_position() == media.get_duration(): media.seek(0) @@ -229,7 +242,8 @@ class SelectView(BaseView): def play(self): if self.songs: - media.load(self.songs[self._sc.target_position()].dirName + "/song.mp3") + song = self.songs[self._sc.target_position()].load() + media.load(song.dirName + "/song.mp3") else: media.stop() diff --git a/song.py b/song.py index 3bbea0d28387cc2a60d8017cde6a29199c67b9fb..a5fdb9cb9556b781570f1c7aa4551040fb2a2b8d 100644 --- a/song.py +++ b/song.py @@ -66,6 +66,7 @@ class SongView(BaseView): self.events_in_margin = set() self.petal_events = [set() for i in range(5)] self.last_played = 0 + self.last_time = 0 self.good = 0.0 self.bad = 0.0 @@ -318,6 +319,7 @@ class SongView(BaseView): media.load(self.song.dirName + '/song.mp3') if self.song and self.started: + # TODO: handle delay specified in song.ini self.time = media.get_time() * 1000 + AUDIO_DELAY if self.input.buttons.app.middle.pressed: @@ -333,9 +335,13 @@ class SongView(BaseView): if self.input.buttons.app.right.pressed: self.demo_mode = not self.demo_mode + if self.demo_mode: + media.set_volume(1.0) if self.paused: return + + delta_time = self.time - self.last_time self.good = max(0, self.good - delta_ms / self.data.period) self.bad = max(0, self.bad - delta_ms / 500) @@ -345,7 +351,7 @@ class SongView(BaseView): self.petal_events[i].clear() earlyMargin = 60000.0 / self.data.bpm / 3.5 - lateMargin = 60000.0 / self.data.bpm / 3.5 + delta_ms + lateMargin = 60000.0 / self.data.bpm / 3.5 + delta_time self.notes.clear() self.events_in_margin.clear() @@ -359,7 +365,7 @@ class SongView(BaseView): if isinstance(event, midireader.Note): if event.time <= self.time <= event.time + event.length: self.notes.add(event.number) - if event.time - earlyMargin <= self.time <= event.time + lateMargin: + if self.time - lateMargin <= event.time <= self.time + earlyMargin: self.events_in_margin.add(event) if not event.played: self.petal_events[event.number].add(event) @@ -369,7 +375,7 @@ class SongView(BaseView): if not self.demo_mode: self.missed[event.number] = 1.0 self.miss = 1.0 - if event.time >= self.last_played: + if event.time >= self.last_played and not self.demo_mode: media.set_volume(0.25) if event.played and event.time + event.length - lateMargin > self.time: p = 4 if event.number == 0 else event.number - 1 @@ -391,21 +397,28 @@ class SongView(BaseView): self.bad = 1.0 self.streak = 0 else: - event = events.pop() - for e in events: - if e.time < event.time: - event = e - #event = sorted(events, key = lambda x: x.time)[0] - event.played = True - self.led_override[petal] = 50 - if event.time > self.laststreak: + main_event = min(events, key=lambda x: abs(self.time - x.time)) + # mark event as played, and also previous events since the last think + main_event.played = True + for event in events: + if event.played: continue + if event.time <= main_event.time - delta_ms: + event.played = True + if event.time > self.laststreak: + # TODO: correctly handle chords + self.streak += 1 + + if main_event.time > self.laststreak: self.streak += 1 self.laststreak = event.time if self.debug: print(self.time - event.time) - self.petals[petal] = event + + self.laststreak = main_event.time + self.led_override[petal] = 50 + self.petals[petal] = main_event self.good = 1.0 - self.last_played = event.time + self.last_played = main_event.time media.set_volume(1.0) if not pressed: @@ -419,6 +432,8 @@ class SongView(BaseView): leds.update() #gc.collect() #print("think", gc.mem_alloc() - mem) + + self.last_time = self.time def on_enter(self, vm: Optional[ViewManager]) -> None: super().on_enter(vm) @@ -427,7 +442,7 @@ class SongView(BaseView): return if self.app: media.load(self.app.path + '/sounds/start.mp3') - utils.volume(self.app, 10000) + utils.volume(self.app, 8000) leds.set_slew_rate(238) def on_enter_done(self): diff --git a/songinfo.py b/songinfo.py index 23d174a317dc5f03e9fca850ce8f3b46b46bab25..53f484ab2fa16c3ac448c00366eb8dabe664c412 100644 --- a/songinfo.py +++ b/songinfo.py @@ -3,20 +3,41 @@ import os import midi import midireader -from midireader import difficulties, noteSet +from midireader import difficulties, noteSet, noteMap class MidiInfoReader(midi.MidiOutStream): - __slots__ = ("notes", ) - + __slots__ = ("difficulties", "nTracks", "ignored", "trackNo") + # We exit via this exception so that we don't need to read the whole file in class Done(Exception): pass def __init__(self): super().__init__() - self.notes = set() + self.difficulties = set() + self.ignored = False + self.nTracks = 0 + self.trackNo = -1 + + def start_of_track(self, track): + self.trackNo = track + + def header(self, format, nTracks, division): + self.nTracks = nTracks + + def sequence_name(self, val): + name = ''.join(list(map(chr, val))) + self.ignored = name != "PART GUITAR" and self.nTracks > 2 + if self.difficulties: + raise MidiInfoReader.Done() + return self.ignored def note_on(self, channel, note, velocity): - self.notes.add(note) + if not self.ignored: + if not note in noteMap: + return + self.difficulties.add(difficulties[noteMap[note][0]]) + if len(self.difficulties) == len(difficulties): + raise MidiInfoReader.Done() class SongInfo(object): def __init__(self, dirName): @@ -54,7 +75,7 @@ class SongInfo(object): if self._difficulties is not None: return self._difficulties - diffFileName = os.path.join(os.path.dirname(self.fileName), "diffs.pet") + diffFileName = os.path.join(os.path.dirname(self.fileName), ".diff.pet") try: with open(diffFileName, "rb") as f: self._difficulties = [] @@ -78,17 +99,7 @@ class SongInfo(object): except MidiInfoReader.Done: pass - diffset = set() - for note in info.notes: - if not note in noteSet: - continue - track, number = midireader.noteMap[note] - diff = difficulties[track] - if not diff in diffset: - diffset.add(diff) - if len(diffset) == len(difficulties): - break - self._difficulties = list(diffset) + self._difficulties = list(info.difficulties) self._difficulties.sort(key = lambda a: a.id, reverse=True) #except Exception as e: # print(e) @@ -96,10 +107,10 @@ class SongInfo(object): return self._difficulties def saveDifficulties(self): - if self._difficulties is None: + if not self._difficulties: return try: - diffFileName = os.path.join(os.path.dirname(self.fileName), "diffs.pet") + diffFileName = os.path.join(os.path.dirname(self.fileName), ".diff.pet") with open(diffFileName, "wb") as f: for diff in self._difficulties: f.write(bytes([diff.id]))