diff --git a/__init__.py b/__init__.py index 1850f04961279d9feb2ac7b56e3bc79e1ed9bdc8..62b5e275894e676449b4361c4a87859f7ecd38fc 100644 --- a/__init__.py +++ b/__init__.py @@ -6,6 +6,7 @@ import st3m.run import leds import bl00mbox from time import sleep +import sys_display UNSUPPORTED = False try: import media @@ -36,9 +37,13 @@ class PetalHero(Application): self.after_score = False #self.blm_extra = bl00mbox.Channel("Petal Hero Extra") #self.blm_extra.background_mute_override = True + + self.blm_timeout = 1 readme.install() + #def show_icons(self): return True + def load(self): if self.loaded: return @@ -60,8 +65,13 @@ class PetalHero(Application): self.loaded = True def load_fiba(self): + if not self.loaded: + return + if self.app.fiba_sound: return + + utils.blm_wake(self, 1) self.app.fiba_sound = [] for i in range(6): @@ -135,6 +145,7 @@ class PetalHero(Application): media.think(delta_ms) self.flower.think(delta_ms) + utils.blm_timeout(self, delta_ms) if self.time < 0: self.time = 0 @@ -167,15 +178,16 @@ class PetalHero(Application): self.load() media.load(self.path + '/sounds/menu.mp3') self.time = -1 - leds.set_brightness(69) + leds.set_slew_rate(255) + leds.set_auto_update(0) def on_exit(self): super().on_exit() if UNSUPPORTED: return media.stop() + leds.set_slew_rate(255) leds.set_all_rgb(0, 0, 0) - leds.set_brightness(69) leds.update() if self.vm.direction == ViewTransitionDirection.BACKWARD: utils.play_back(self.app) diff --git a/difficulty.py b/difficulty.py index 1b420fdf9e25f6277a9888fd586d96694748ec85..05e26ef20f2740fee21436183f52db4c61bb81a1 100644 --- a/difficulty.py +++ b/difficulty.py @@ -104,16 +104,17 @@ class DifficultyView(BaseView): self._sc.think(ins, delta_ms) media.think(delta_ms) self.flower.think(delta_ms) + utils.blm_timeout(self, delta_ms) self._scroll_pos += delta_ms / 1000 if not self.is_active(): return - if self.input.buttons.app.left.pressed: + if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated: self._sc.scroll_left() self._scroll_pos = 0.0 utils.play_crunch(self.app) - elif self.input.buttons.app.right.pressed: + elif self.input.buttons.app.right.pressed or self.input.buttons.app.right.repeated: self._sc.scroll_right() self._scroll_pos = 0.0 utils.play_crunch(self.app) diff --git a/loading.py b/loading.py index 043d105c0772633c75f2ad9e81df27b0713b2b35..05f0bfb0dac413e62fec6898c7f79c534befebbd 100644 --- a/loading.py +++ b/loading.py @@ -3,6 +3,7 @@ import gc import sys_display import song +import utils class LoadingView(BaseView): def __init__(self, app, song, difficulty): @@ -23,6 +24,7 @@ class LoadingView(BaseView): ctx.text("Loading...") def think(self, ins: InputState, delta_ms: int) -> None: + utils.blm_timeout(self, delta_ms) if self.vm.transitioning or not self.is_active(): return #gc.collect() self.vm.replace(song.SongView(self.app, self.song, self.difficulty), ViewTransitionBlend()) diff --git a/midi/EventDispatcher.py b/midi/EventDispatcher.py index 85982aaf9966dca6b52ccc114ae3ff26b5b287c9..1a617550f56b663323aa5f1d01ef7fdac331ac77 100644 --- a/midi/EventDispatcher.py +++ b/midi/EventDispatcher.py @@ -192,7 +192,6 @@ class EventDispatcher: # uses 3 bytes to represent time between quarter # notes in microseconds stream.tempo((b1<<16) + (b2<<8) + b3) - return # SEQUENCE_NUMBER = 0x00 (00 02 ss ss (seq-number)) if meta_type == SEQUENCE_NUMBER: diff --git a/midi/MidiFileParser.py b/midi/MidiFileParser.py index d0b6bd0290d587fb25d8dc7a86b9a1c1c2020f9b..fed6e375b6b89ccd002d6bb4735df57cdf9a754b 100644 --- a/midi/MidiFileParser.py +++ b/midi/MidiFileParser.py @@ -77,6 +77,7 @@ class MidiFileParser: "Parses a track chunk. This is the most important part of the parser." # set time to 0 at start of a track + self.ignored = False self.dispatch.reset_time() dispatch = self.dispatch @@ -126,20 +127,25 @@ class MidiFileParser: meta_type = raw_in.readBew() meta_length = raw_in.readVarLen() meta_data = raw_in.nextSlice(meta_length) + if self.ignored and meta_type != TEMPO: return if not meta_length: return - dispatch.meta_event(meta_type, meta_data) - if meta_type == END_OF_TRACK: return + res = dispatch.meta_event(meta_type, meta_data) + if meta_type == SEQUENCE_NAME: + self.ignored = res + if meta_type == END_OF_TRACK: + return # Oh! Then it must be a midi event (channel voice message) else: data_size = data_sizes.get(hi_nible, 0) channel_data = raw_in.nextSlice(data_size) + if self.ignored: return event_type, channel = hi_nible, lo_nible dispatch.channel_messages(event_type, channel, channel_data) def parseMTrkChunks(self): "Parses all track chunks." - for t in range(min(2, self.nTracks)): + for t in range(self.nTracks): self._current_track = t self.parseMTrkChunk() # this is where it's at! self.dispatch.eof() diff --git a/midireader.py b/midireader.py index dc631e8a1b4df70085212f424b19711d7b50bc89..b02c67264e80fbee6eedeef2392d8cf60e075c10 100644 --- a/midireader.py +++ b/midireader.py @@ -17,10 +17,10 @@ class Difficulty: return self.text difficulties = { - SUPAEASY_DIFFICULTY: Difficulty(SUPAEASY_DIFFICULTY, "Supaeasy"), - EASY_DIFFICULTY: Difficulty(EASY_DIFFICULTY, "Easy"), - MEDIUM_DIFFICULTY: Difficulty(MEDIUM_DIFFICULTY, "Medium"), - AMAZING_DIFFICULTY: Difficulty(AMAZING_DIFFICULTY, "Amazing"), + SUPAEASY_DIFFICULTY: Difficulty(SUPAEASY_DIFFICULTY, "Easy"), + EASY_DIFFICULTY: Difficulty(EASY_DIFFICULTY, "Medium"), + MEDIUM_DIFFICULTY: Difficulty(MEDIUM_DIFFICULTY, "Hard"), + AMAZING_DIFFICULTY: Difficulty(AMAZING_DIFFICULTY, "Expert"), } baseNotes = { @@ -37,6 +37,8 @@ for basenote, diff in baseNotes.items(): reverseNoteMap = dict([(v, k) for k, v in list(noteMap.items())]) +noteSet = set(noteMap.keys()) + class Event: __slots__ = ("length", "time") @@ -119,6 +121,8 @@ class MidiReader(midi.MidiOutStream): #self.tracks = [Track() for t in range(len(difficulties))] self.difficulty = difficulty self.track = Track() + self.nTracks = -1 + self.ignored = False def addEvent(self, track, event): time = event.time @@ -158,8 +162,10 @@ class MidiReader(midi.MidiOutStream): def header(self, format, nTracks, division): self.ticksPerBeat = division + self.nTracks = nTracks def tempo(self, value): + #if self.ignored: return bpm = 60.0 * 10.0**6 / value if not self.tempoMarkers or bpm != self.tempoMarkers[-1][1]: self.tempoMarkers.append((midi.MidiOutStream.abs_time(self), bpm)) @@ -170,24 +176,30 @@ class MidiReader(midi.MidiOutStream): #print('bpm', bpm) #self.addEvent(None, Tempo(bpm)) + def sequence_name(self, val): + name = ''.join(list(map(chr, val))) + #print(name) + self.ignored = name != "PART GUITAR" and self.nTracks > 2 + return self.ignored + def note_on(self, channel, note, velocity): - if self.get_current_track() > 1: return + if self.ignored: return #print("note_on", channel, note, velocity, self.abs_time()) + if not note in noteMap: + return self.velocity[note] = velocity self.heldNotes[(self.get_current_track(), channel, note)] = self.abs_time() def note_off(self, channel, note, velocity): - if self.get_current_track() > 1: return + if self.ignored: return #print("note_off", channel, note, velocity, self.abs_time()) + if not note in noteMap: + return try: startTime = self.heldNotes[(self.get_current_track(), channel, note)] endTime = self.abs_time() del self.heldNotes[(self.get_current_track(), channel, note)] - if note in noteMap: - track, number = noteMap[note] - self.addEvent(track, Note(startTime, number, endTime - startTime, special = self.velocity[note] == 127)) - else: - #print("MIDI note 0x%x at %d does not map to any game note." % (note, self.abs_time())) - pass + track, number = noteMap[note] + self.addEvent(track, Note(startTime, number, endTime - startTime, special = self.velocity[note] == 127)) except KeyError: print("MIDI note 0x%x on channel %d ending at %d was never started." % (note, channel, self.abs_time())) diff --git a/readme.py b/readme.py index 09e5e461f7d6f056366c80ee80a80d3819fddbad..1de3855ba4b5af60700aaae836ff10db4e29e7e6 100644 --- a/readme.py +++ b/readme.py @@ -7,7 +7,7 @@ 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. This should do: - sox -m *.ogg -c 1 -C 64 -r 48000 song.mp3 + sox -m *.ogg -c 1 -C 128 -r 48k song.mp3 norm -3 You need song.ini, song.mp3 and notes.mid in the song directory. """ diff --git a/score.py b/score.py index 3098f23cc663bfe6942925adba9de2be7a44d245..20a48ff232879583ff3e271c73126a3ab39c057d 100644 --- a/score.py +++ b/score.py @@ -92,6 +92,7 @@ class ScoreView(BaseView): def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) media.think(delta_ms) + utils.blm_timeout(self, delta_ms) self.time += delta_ms / 1000 if self.time > 1.5 and not self.played: diff --git a/select.py b/select.py index a9080ff8b96746c2a4f7aac698c0f88d21b4c39c..9a5317cb28fb1bcd157dbd80f869fba0fdbbb9fd 100644 --- a/select.py +++ b/select.py @@ -182,6 +182,7 @@ class SelectView(BaseView): def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) self._sc.think(ins, delta_ms) + utils.blm_timeout(self, delta_ms) if not self.to_process and not self.processing_now: media.think(delta_ms) self.flower.think(delta_ms) @@ -191,11 +192,14 @@ class SelectView(BaseView): if cur_target < 0: cur_target = 0 if cur_target > len(self.songs) - 1: cur_target = len(self.songs) - 1 - if self.input.buttons.app.left.pressed: + if not self.is_active(): + return + + if self.input.buttons.app.left.pressed or self.input.buttons.app.left.repeated: self._sc.scroll_left() self._scroll_pos = 0.0 utils.play_crunch(self.app) - elif self.input.buttons.app.right.pressed: + elif self.input.buttons.app.right.pressed or self.input.buttons.app.right.repeated: self._sc.scroll_right() self._scroll_pos = 0.0 utils.play_crunch(self.app) @@ -204,9 +208,6 @@ class SelectView(BaseView): if pos < 0: pos = 0 if pos > len(self.songs) - 1: pos = len(self.songs) - 1 - if not self.is_active(): - return - if pos != cur_target: media.load(self.songs[pos].dirName + "/song.mp3") diff --git a/song.py b/song.py index 16ddd123f1e02986974cbf78de055e46c30a1e73..3bbea0d28387cc2a60d8017cde6a29199c67b9fb 100644 --- a/song.py +++ b/song.py @@ -5,7 +5,9 @@ import st3m.run import math import leds import sys_display +import sys_scope from micropython import const +from time import ticks_ms try: import media from st3m.ui.view import ViewTransitionDirection @@ -63,16 +65,20 @@ class SongView(BaseView): self.notes = set() self.events_in_margin = set() self.petal_events = [set() for i in range(5)] + self.last_played = 0 self.good = 0.0 self.bad = 0.0 self.missed = [0.0] * 5 self.miss = 0.0 - self.oldmem = 0 + #self.oldmem = 0 + #self.redraw = True + #self.acc = 0 def draw(self, ctx: Context) -> None: #mem = gc.mem_alloc() + #self.redraw = True self.time += VIDEO_DELAY other = int(self.time / 2 / self.data.period) % 2 @@ -211,16 +217,26 @@ class SongView(BaseView): ctx.save() ctx.rgb(0.5 + self.bad * 0.4, 0.5, 0.5) + #ctx.rectangle(-8, -8, 15, 15) + #ctx.clip() + ctx.line_width = 12 + ctx.scale(0.0625, 0.125 * 0.3) #ctx.rotate(wiggle) + ctx.begin_path() if self.started: - ctx.rectangle(-8, -8, 15, 15) - ctx.clip() # for firmwares that stroke the scope... - ctx.scale(0.0625, 0.125 * 0.3) - ctx.begin_path() - ctx.scope() - ctx.line_to(120, 0) - ctx.line_to(-120, 0) + buf = sys_scope.get_buffer_x() + ctx.move_to(-120, 0) + for i in range(0, len(buf), 32): + val = buf[i] / 32 + ctx.line_to(-120 + i, max(6, val)) + for i in range(len(buf) - 1, 0, -32): + val = buf[i] / 32 + ctx.line_to(-120 + i, min(-6, val)) ctx.fill() + else: + ctx.move_to(-120, 0) + ctx.line_to(120, 0) + ctx.stroke() ctx.restore() ctx.gray(0.8) @@ -248,13 +264,25 @@ class SongView(BaseView): ctx.font_size = 64 ctx.text("PAUSED") #print("draw", gc.mem_alloc() - mem) + #t = ticks_ms() + #print(ticks_ms() - t) def think(self, ins: InputState, delta_ms: int) -> None: #mem = gc.mem_alloc() #print(mem - self.oldmem) #self.oldmem = mem + + #self.acc += delta_ms + #if self.redraw: + # delta_ms = self.acc + # self.acc = 0 + # self.redraw = False + #else: + # return + #if (delta_ms > 100): print(delta_ms) super().think(ins, delta_ms) + utils.blm_timeout(self, delta_ms) if self.first_think: self.first_think = False @@ -262,7 +290,7 @@ class SongView(BaseView): media.think(delta_ms) - if self.input.buttons.os.middle.pressed and not self.is_active(): + if self.input.buttons.os.middle.pressed and not self.is_active(): # TODO: will that work with pending transitions? self.vm.push(self) self.vm.pop(ViewTransitionSwipeRight(), depth=2) return @@ -273,6 +301,7 @@ class SongView(BaseView): if self.song and self.started and media.get_position() == media.get_duration() and not self.finished: self.finished = True media.stop() + gc.collect() self.vm.replace(score.ScoreView(self.app, self.data, self.longeststreak), ViewTransitionBlend()) return @@ -316,7 +345,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 + lateMargin = 60000.0 / self.data.bpm / 3.5 + delta_ms self.notes.clear() self.events_in_margin.clear() @@ -340,6 +369,8 @@ class SongView(BaseView): if not self.demo_mode: self.missed[event.number] = 1.0 self.miss = 1.0 + if event.time >= self.last_played: + 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 if not ins.captouch.petals[p*2].pressed and not event.missed: @@ -366,7 +397,7 @@ class SongView(BaseView): event = e #event = sorted(events, key = lambda x: x.time)[0] event.played = True - self.led_override[petal] = 120 + self.led_override[petal] = 50 if event.time > self.laststreak: self.streak += 1 self.laststreak = event.time @@ -374,6 +405,8 @@ class SongView(BaseView): print(self.time - event.time) self.petals[petal] = event self.good = 1.0 + self.last_played = event.time + media.set_volume(1.0) if not pressed: self.petals[petal] = None @@ -384,6 +417,7 @@ class SongView(BaseView): utils.petal_leds(petal, d) leds.update() + #gc.collect() #print("think", gc.mem_alloc() - mem) def on_enter(self, vm: Optional[ViewManager]) -> None: @@ -393,7 +427,8 @@ class SongView(BaseView): return if self.app: media.load(self.app.path + '/sounds/start.mp3') - self.app.blm.volume = 10000 + utils.volume(self.app, 10000) + leds.set_slew_rate(238) def on_enter_done(self): #sys_display.set_mode(sys_display.get_mode() | 512) @@ -410,7 +445,7 @@ class SongView(BaseView): #gc.enable() if self.app and not self.finished: - self.app.blm.volume = 14000 + utils.volume(self.app, 14000) utils.play_back(self.app) if self.song and not self.started: diff --git a/songinfo.py b/songinfo.py index 2a1ec67e536e78d5d94e39398497ec5ad9fb60c7..23d174a317dc5f03e9fca850ce8f3b46b46bab25 100644 --- a/songinfo.py +++ b/songinfo.py @@ -3,30 +3,7 @@ import os import midi import midireader -AMAZING_DIFFICULTY = 0 -MEDIUM_DIFFICULTY = 1 -EASY_DIFFICULTY = 2 -SUPAEASY_DIFFICULTY = 3 - -class Difficulty: - def __init__(self, id, text): - self.id = id - self.text = text - - def __str__(self): - return self.text - - def __repr__(self): - return self.text - -difficulties = { - SUPAEASY_DIFFICULTY: Difficulty(SUPAEASY_DIFFICULTY, "Supaeasy"), - EASY_DIFFICULTY: Difficulty(EASY_DIFFICULTY, "Easy"), - MEDIUM_DIFFICULTY: Difficulty(MEDIUM_DIFFICULTY, "Medium"), - AMAZING_DIFFICULTY: Difficulty(AMAZING_DIFFICULTY, "Amazing"), -} - -noteSet = set(midireader.noteMap.keys()) +from midireader import difficulties, noteSet class MidiInfoReader(midi.MidiOutStream): __slots__ = ("notes", ) diff --git a/utils.py b/utils.py index d7fb6ae312207187777ce5ee7a3c81cf8e3473ad..2ae4ec26522878544ee612e31a5b01972a275b02 100644 --- a/utils.py +++ b/utils.py @@ -3,6 +3,7 @@ import leds import random import os import time +import sys_bl00mbox def dim(color, val): res = [] @@ -45,25 +46,46 @@ def petal_leds(petal, val, color = None): led += 40 leds.set_rgb(led, *color) +def blm_wake(app, timeout): + if not app or not app.loaded: return + if app.blm_timeout == 0: + sys_bl00mbox.channel_enable(app.blm.channel_num) + if timeout > app.blm_timeout: + app.blm_timeout = timeout + +def blm_timeout(view, delta_ms): + app = view.app + if not app: return + if not view.is_active(): return + if app.blm_timeout == 0: return + app.blm_timeout -= delta_ms + if app.blm_timeout <= 0: + sys_bl00mbox.channel_disable(app.blm.channel_num) + app.blm_timeout = 0 + def play_crunch(app): if not app: return if not app.crunch_sound: return app.crunch_sound[random.randint(0, 2)].signals.trigger.start() + blm_wake(app, 600) def play_fiba(app): if not app: return if not app.fiba_sound: return app.fiba_sound[random.randint(0, 5)].signals.trigger.start() + blm_wake(app, 1400) def play_go(app): - if not app: return + if not app or not app.loaded: return app.in_sound.signals.trigger.start() + blm_wake(app, 1200) def play_back(app): - if not app: return + if not app or not app.loaded: return app.out_sound.signals.trigger.start() + blm_wake(app, 800) def circle(ctx, x, y, r): c = 0.55191502449 @@ -92,3 +114,7 @@ def timed_function(f, *args, **kwargs): print('Function {} Time = {:6.3f}ms'.format(myname, delta/1000)) return result return new_func + +def volume(app, vol): + if app and app.loaded: + app.blm.volume = vol