from __future__ import annotations import displayio from micropython import const from adafruit_ticks import ticks_ms, ticks_diff from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff from .util import hsv_to_rgb try: import synthio except: synthio = None def clamp_norm(v): return max(0, min(v, 1)) class Note: keyboard: Keyboard base: BaseMode pitch: int end_tick: None | int expiry: float def __init__(self, keyboard: Keyboard, pitch: int): self.keyboard = keyboard self.base = keyboard.modes["base"] self.pitch = pitch self.end_tick = None self.expiry = 1.0 if self.keyboard.synth: self.note = synthio.Note( synthio.midi_to_hz(self.pitch), filter=self.keyboard.filter ) def on(self, local=True): self.base.notes[self.pitch] = self self.base.notes_dirty = True if self.keyboard.synth: self.keyboard.synth.press(self.note) msg = NoteOn(self.pitch, self.keyboard.velocity) self.keyboard.broadcast(msg) if local and self.keyboard.i2c: self.keyboard.i2c.send(self, True) if self.pitch in self.base.notes_expiring: del self.base.notes_expiring[self.pitch] def off(self, local=True): if self.base.notes.get(self.pitch) is self: self.base.notes_expiring[self.pitch] = self del self.base.notes[self.pitch] self.base.notes_dirty = True if self.keyboard.synth: self.keyboard.synth.release(self.note) msg = NoteOff(self.pitch, self.keyboard.velocity) self.keyboard.broadcast(msg) if local and self.keyboard.i2c: self.keyboard.i2c.send(self, False) self.end_tick = ticks_ms() def update_expiry(self, ticks_ms_now): delta = ticks_diff(ticks_ms_now, self.end_tick) if delta > self.keyboard.jam_timeout: del self.base.notes_expiring[self.pitch] self.expiry = 0 return self.expiry = clamp_norm(1 - delta / self.keyboard.jam_timeout) class Key: MENU_I = const(48 + 0) PREV_I = const(48 + 4) NEXT_I = const(48 + 5) keyboard: Keyboard i: int """matrix index for this key.""" pos: tuple[float, int] """logical coordinates for this key.""" note: None | Note pitch: int def __init__(self, keyboard: Keyboard, i: int): self.keyboard = keyboard self.i = i self.note = None self.update_pos() def update_pos(self): x = self.i % 12 y = self.i // 12 if y % 2 == 0: x += 0.5 if self.keyboard.flip: x = 11.5 - x else: y = 3 - y x += self.keyboard.offset[0] y += self.keyboard.offset[1] self.pos = (x, y) def update_scale(self): self.pitch = self.keyboard.layout.get_pitch(self) def on_press(self): if self.note: self.note.off() if self.pitch < 0 or self.pitch > 127: return self.note = Note(self.keyboard, self.pitch) self.note.on() def on_release(self): if not self.note: return self.note.off() self.note = None class Mode: keyboard: Keyboard group: displayio.Group color: tuple[float, float, float] = (0.5, 1.0, 0.5) def __init__(self, keyboard: Keyboard): self.keyboard = keyboard self.group = displayio.Group() def enter(self): """ Called when this mode becomes active. """ self.keyboard.display.root_group = self.group def exit(self): """ Called when this mode becomes inactive. """ pass def tick(self, ticks_ms: int): """ Called every 'tick' while this mode is active. """ pass def key_event(self, i: int, pressed: bool) -> bool: """ Process a key event. Return whether the next key event with this `i` should be delivered to this mode, even if this mode is not active then (make it "sticky"). """ return False def update_pixels(self, pixels: NeoPixel): """ Callback to update RGB LEDs. """ pixels[0] = hsv_to_rgb(*self.color)