from __future__ import annotations import displayio import json from adafruit_ticks import ticks_ms from adafruit_midi.system_exclusive import SystemExclusive from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff try: import storage dev_mode = storage.getmount("/").readonly except: dev_mode = False try: import audiomixer import synthio except: synthio = None from .core import Mode, Note from .matrix import Matrix from .scale import Scale from .layout import Layout, LAYOUTS, WickiHaydenLayout from .rgb import RGB_EFFECTS from .i2c import I2CLeader, I2CFollower from .menu import ( Settings, SliderSetting, Slider16Setting, ChoiceSetting, MenuMode, _eq, _thresh_offor, _color_offor, _fmt_offorpct, ) from .base import BaseMode, BaseShiftMode class Keyboard: scale: Scale layout: Layout flip: bool offset: tuple[int, int] mode: Mode modes: dict[str, Mode] sticky_modes: dict[int, Mode] matrix: Matrix pixels: NeoPixel display: Display midi_usb: MIDI midi_trs: MIDI audio_out: PWMAudioOut def __init__(self, board): self.board = board self.matrix = board.create_matrix(self) self.board.dev_mode = dev_mode self.pixels = board.create_pixels(self, auto_write=False) displayio.release_displays() self.display = board.create_display( self, width=128, height=32, auto_refresh=False ) self.display.root_group = displayio.Group() self._display_rotation = self.display.rotation self.display.refresh() self.midi_usb = board.create_midi_usb(self) self.midi_trs = board.create_midi_trs(self) self.i2c = None self.audio_out = board.create_audio_out(self) if synthio: self.synth = synthio.Synthesizer(sample_rate=41100) self.mixer = audiomixer.Mixer( sample_rate=41100, buffer_size=5120, channel_count=1 ) self.filter = self.synth.low_pass_filter(2000, 0.8) else: self.synth = None self.settings = Settings( { # MIDI "midi_ch_usb": Slider16Setting( "USB MIDI CHANNEL", fmt="CH{}", thresh=_eq ), "midi_ch_trs": Slider16Setting( "TRS MIDI CHANNEL", fmt="CH{}", thresh=_eq ), "midi_vel": SliderSetting("MIDI VELOCITY", 127, default=64), # I2C "i2c_mode": ChoiceSetting( "I2C MODE", [False, "leader", "follower"], default=False, color=_color_offor, ), "i2c_addr": SliderSetting( "I2C ADDRESS", 127, default=0x33, fmt="0x{0:02x} ({0:d})" ), "i2c_xo": SliderSetting("I2C X OFFSET", 12 * 6, default=0), "i2c_yo": SliderSetting("I2C Y OFFSET", 16, default=0), # SYNTH "synth_vol": SliderSetting( "SYNTH VOLUME", 100, default=0, fmt=_fmt_offorpct, color=_color_offor, thresh=_thresh_offor, ), "synth_a": SliderSetting( "SYNTH ATTACK", 100, default=3, fmt=lambda v: f"{(v*v/1000):.2f}s", ), "synth_d": SliderSetting( "SYNTH DECAY", 100, default=25, fmt=lambda v: f"{(v*v/1000):.2f}s", ), "synth_s": SliderSetting( "SYNTH SUSTAIN", 100, default=75, fmt=lambda v: f"{(v*v/100):.0f}%", ), "synth_r": SliderSetting( "SYNTH RELEASE", 100, default=30, fmt=lambda v: f"{(v*v/1000):.2f}s", ), # DISPLAY "rgb_eff": ChoiceSetting( "RGB EFFECT", ["scale", "rainbow", "rainbow scale", "rainbow scale alt"], default="scale", ), "rgb_bright": SliderSetting( "RGB BRIGHTNESS", 100, default=100, fmt=_fmt_offorpct, color=_color_offor, thresh=_thresh_offor, ), "jam_timeout": ChoiceSetting( "JAM MODE FADE TIME", [0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 12, 20], default=6, fmt="{:.2f}s", color=_color_offor, ), "flip": ChoiceSetting( "DISPLAY FLIP", [False, True], default=False, color=_color_offor ), # LAYOUT "layout_name": ChoiceSetting( "KEYBOARD LAYOUT", [ "wicki/hayden", "harmonic table", "gerhard", "jankó", "trad piano", ], default="wicki/hayden", ), "layout_offset": SliderSetting( "LAYOUT START NOTE", 127, default=24, fmt=lambda p: "{} ({})".format(p, self.scale.label(p, octave=True)), ), # SCALE (hidden) "scale_name": ChoiceSetting( "HIGHLIGHT SCALE", [ "major", "min nat", "min harm", "min mel", "min hung", "whole", "penta", ], default="major", ), "scale_root": SliderSetting("SCALE ROOT NOTE", 127, default=43), }, readonly=self.board.dev_mode, ) self.layout = WickiHaydenLayout() self.scale = Scale(0, Scale.STEPS["major"], "major") self.flip = False self.offset = (0, 0) self.settings.on("*", self.on_change) self.settings.on("midi_ch_usb", self.on_midi_ch_usb) self.settings.on("midi_ch_trs", self.on_midi_ch_trs) self.settings.on("midi_vel", self.on_midi_vel) self.settings.on("rgb_eff", self.on_rgb_eff) self.settings.on("rgb_bright", self.on_rgb_bright) self.settings.on("jam_timeout", self.on_jam_timeout) self.settings.on("layout_name", self.on_layout) self.settings.on("layout_offset", self.on_layout) self.settings.on("scale_name", self.on_scale) self.settings.on("scale_root", self.on_scale) self.settings.on("flip", self.on_flip) self.settings.on("i2c_mode", self.on_i2c) self.settings.on("i2c_addr", self.on_i2c) self.settings.on("i2c_xo", self.on_i2c) self.settings.on("i2c_yo", self.on_i2c) self.settings.on("synth_vol", self.on_synth_vol) self.settings.on("synth_a", self.on_synth_adsr) self.settings.on("synth_d", self.on_synth_adsr) self.settings.on("synth_s", self.on_synth_adsr) self.settings.on("synth_r", self.on_synth_adsr) self.sticky_modes = {} self.modes = { "base": BaseMode(self), "base_shift": BaseShiftMode(self), "menu": MenuMode( self, settings=self.settings, groups=[ ["midi_ch_usb", "midi_ch_trs", "midi_vel"], ["i2c_mode", "i2c_addr", "i2c_xo", "i2c_yo"], ["synth_vol", "synth_a", "synth_d", "synth_s", "synth_r"], ["rgb_eff", "rgb_bright", "jam_timeout", "flip"], ["layout_name", "layout_offset"], ], ), } self.mode = self.modes["base"] self.settings.load() self.mode.update_scale() def on_midi_ch_usb(self, ch, last): self.midi_usb.out_channel = ch def on_midi_ch_trs(self, ch, last): self.midi_trs.out_channel = ch def on_midi_vel(self, vel, last): self.velocity = vel def on_rgb_eff(self, effect, last): self.modes["base"].rgb = RGB_EFFECTS[effect](self) self.modes["base"].rgb.prepare() def on_rgb_bright(self, b, last): self.pixels.brightness = b / 100 def on_jam_timeout(self, t, last): self.jam_timeout = t * 1000 def on_layout(self, v, last): name = self.settings.get("layout_name").value offset = self.settings.get("layout_offset").value self.layout = LAYOUTS[name](offset) self.modes["base"].update_scale() def on_flip(self, flip, last): self.display.rotation = self._display_rotation + (180 if flip else 0) self.flip = flip for key in self.modes["base"].keys: key.update_pos() self.on_layout(None, None) def on_i2c(self, v, last): if self.i2c: self.i2c.deinit() mode = self.settings.get("i2c_mode").value if not mode: self.i2c = None self.offset = (0, 0) elif mode == "leader": self.i2c = I2CLeader(self, self.board) self.offset = (0, 0) elif mode == "follower": addr = self.settings.get("i2c_addr").value self.i2c = I2CFollower(self, self.board, addr) xo = self.settings.get("i2c_xo").value yo = -self.settings.get("i2c_yo").value self.offset = (xo, yo) else: raise ValueError("unknown i2c mode") for key in self.modes["base"].keys: key.update_pos() self.on_layout(None, None) def on_change(self, id, val, last): if not ( id.startswith("rgb") or id.startswith("layout") or id.startswith("scale") ): return if isinstance(self.i2c, I2CLeader): self.i2c.broadcast_chunked("S", json.dumps({id: val}).encode()) def on_scale(self, v, last): name = self.settings.get("scale_name").value root = self.settings.get("scale_root").value self.scale = Scale(root, Scale.STEPS[name], name) self.modes["base"].update_scale() self.modes["base_shift"].update_display() def on_synth_vol(self, v, last): if not self.synth or not self.audio_out: return if v == 0: self.audio_out.stop() else: self.audio_out.play(self.mixer) self.mixer.voice[0].play(self.synth) self.mixer.voice[0].level = v * v / 10000 def on_synth_adsr(self, v, last): if not self.synth: return a = self.settings.get("synth_a").value d = self.settings.get("synth_d").value s = self.settings.get("synth_s").value r = self.settings.get("synth_r").value self.synth.envelope = synthio.Envelope( attack_time=a * a / 1000, decay_time=d * d / 1000, sustain_level=s * s / 10000, release_time=r * r / 1000, ) @property def mode(self) -> Mode: return self._mode @mode.setter def mode(self, mode: Mode): if hasattr(self, "_mode"): if self._mode == mode: return self._mode.exit() self._mode = mode self._mode.enter() def broadcast(self, msg: MIDIMessage): self.midi_usb.send(msg) self.midi_trs.send(msg) def tick(self): ticks = ticks_ms() self.mode.tick(ticks) for i, pressed in self.matrix.scan_for_changes(): if self.board.sysex_key_sync: msg = SystemExclusive(b"\0s-", [i, pressed]) self.broadcast(msg) mode = self.sticky_modes.pop(i, self.mode) should_stick = mode.key_event(i, pressed) if should_stick: self.sticky_modes[i] = mode if self.i2c: self.i2c.tick() msg = self.midi_usb.receive() while msg: if isinstance(msg, NoteOn): note = Note(self, msg.note) self.modes["base"].extra_notes[note.pitch] = note elif isinstance(msg, NoteOff) and self.modes["base"].extra_notes: del self.modes["base"].extra_notes[msg.note] msg = self.midi_usb.receive() self.mode.update_pixels(self.pixels) self.pixels.show() self.display.refresh() def run(self): while True: self.tick()