git.s-ol.nu hw/0x33.board/firmware / 0f6f218
add various RGB effects s-ol a month ago
6 changed file(s) with 148 addition(s) and 45 deletion(s). Raw diff Collapse all Expand all
1515 from .core import Mode
1616 from .matrix import Matrix
1717 from .scale import Scale
18 from .layout import Layout, WickiHaydenLayout, HarmonicLayout, GerhardLayout
18 from .layout import Layout, LAYOUTS, WickiHaydenLayout
19 from .rgb import RGB_EFFECTS
1920 from .menu import (
2021 Settings,
2122 SliderSetting,
8081 "TRS MIDI CHANNEL", 15, fmt="CH{}", thresh=_eq
8182 ),
8283 "midi_vel": SliderSetting("MIDI VELOCITY", 127, default=64),
84 "rgb_eff": ChoiceSetting(
85 "RGB EFFECT",
86 ['scale', 'rainbow', 'rainbow scale', 'rainbow scale alt'],
87 default='scale',
88 ),
8389 "rgb_bright": SliderSetting(
8490 "LED BRIGHTNESS",
8591 100,
124130 self.settings.on('midi_ch_usb', self.on_midi_ch_usb)
125131 self.settings.on('midi_ch_trs', self.on_midi_ch_trs)
126132 self.settings.on('midi_vel', self.on_midi_vel)
133 self.settings.on('rgb_eff', self.on_rgb_eff)
127134 self.settings.on('rgb_bright', self.on_rgb_bright)
128135 self.settings.on('jam_timeout', self.on_jam_timeout)
129136 self.settings.on('layout_name', self.on_layout)
144151 "midi_ch_usb",
145152 "midi_ch_trs",
146153 "midi_vel",
154 "rgb_eff",
147155 "rgb_bright",
148156 "jam_timeout",
149157 "layout_name",
164172 def on_midi_vel(self, vel):
165173 self.velocity = vel
166174
175 def on_rgb_eff(self, effect):
176 self.modes["base"].rgb = RGB_EFFECTS[effect](self)
177 self.modes["base"].rgb.prepare()
178
167179 def on_rgb_bright(self, b):
168180 self.pixels.brightness = b / 100
169181
173185 def on_layout(self, v):
174186 name = self.settings.get('layout_name').value
175187 offset = self.settings.get('layout_offset').value
176 if name == 'wicki/hayden':
177 self.layout = WickiHaydenLayout(offset)
178 elif name == 'harmonic table':
179 self.layout = HarmonicLayout(offset)
180 elif name == 'gerhard':
181 self.layout = GerhardLayout(offset)
188 self.layout = LAYOUTS[name](offset)
182189 self.modes["base"].update_scale()
183190
184191 def on_scale(self, v):
66
77 from .util import ticks_diff, FONT_10, led_map
88 from .core import Key, Note, Mode
9 from .rgb import ColorScale
910
1011
1112 class BaseMode(Mode):
1213 keys: list[Key]
14 rgb: RGBEffect
15
1316 notes: dict[int, Note]
1417 notes_expiring: dict[int, Note]
1518
2225 super().__init__(*args)
2326
2427 self.keys = [Key(self.keyboard, i) for i in range(48)]
28 self.rgb = ColorScale(self.keyboard)
29
2530 self.notes = {}
2631 self.notes_expiring = {}
2732
6873 for key in self.keys:
6974 key.update_scale()
7075
76 self.rgb.prepare()
77
7178 self.scale_label.text = self.keyboard.scale.format()
7279
7380 def tick(self, ticks_ms: int):
7481 for note in self.notes_expiring.values():
7582 note.update_expiry(ticks_ms)
7683
84 self.rgb.tick(ticks_ms)
85
7786 def update_pixels(self, pixels: NeoPixel):
7887 pixels.fill(0)
7988 super().update_pixels(pixels)
8089
81 for key in self.keys:
82 pixels[led_map[key.i]] = key.update_color()
90 for i, key in enumerate(self.keys):
91 pixels[led_map[key.i]] = self.rgb.get_color(i, key)
8392
8493 if self._notes_dirty:
8594 active = [pitch for pitch in self.notes]
205214
206215 if i < len(base.keys):
207216 key = base.keys[i]
208 self.keyboard.settings.get("scale_root").value = key._pitch
217 self.keyboard.settings.get("scale_root").value = key.pitch
209218 self.keyboard.settings.dispatch("scale_root")
210219 self.pressed_something = True
211220 return False
6868 '''logical coordinates for this key.'''
6969
7070 note: None | Note
71
72 _pitch: int
73 _color: tuple[float, float, float]
71 pitch: int
7472
7573 def __init__(self, keyboard: Keyboard, i: int):
7674 x = i % 12
8684 self.note = None
8785
8886 def update_scale(self):
89 self._pitch = self.keyboard.layout.get_pitch(self)
90 self._hsv = self.keyboard.scale.get_hsv(self._pitch)
91
92 def update_color(self):
93 if self._pitch < 0 or self._pitch > 127:
94 return 0
95
96 hue, sat, key_val = self._hsv
97
98 base = self.keyboard.modes["base"]
99 note_for_pitch = base.notes.get(self._pitch)
100 note_for_pitch = note_for_pitch or base.notes_expiring.get(self._pitch)
101
102 if self.note:
103 value = 1.0
104 elif note_for_pitch:
105 value = note_for_pitch.expiry * 0.8
106 else:
107 value = 0.0
108
109 rest = 1.0 - key_val
110 value = key_val + value * rest
111 return hsv_to_rgb(hue, sat, value)
87 self.pitch = self.keyboard.layout.get_pitch(self)
11288
11389 def on_press(self):
11490 if self.note:
11591 self.note.off()
11692
117 if self._pitch < 0 or self._pitch > 127:
93 if self.pitch < 0 or self.pitch > 127:
11894 return
11995
120 self.note = Note(self.keyboard, self._pitch)
96 self.note = Note(self.keyboard, self.pitch)
12197 self.note.on()
12298
12399 def on_release(self):
2828 def get_pitch(self, key: Key) -> int:
2929 x, y = key.pos
3030 return int(self.offset + 3 * x + 2.5 * y)
31
32
33 LAYOUTS = {
34 "wicki/hayden": WickiHaydenLayout,
35 "harmonic table": HarmonicLayout,
36 "gerhard": GerhardLayout,
37 }
0 from __future__ import annotations
1
2 from colorsys import hsv_to_rgb
3 from .util import ticks_diff
4
5
6 class RGBEffect:
7 keyboard: Keyboard
8 prepared: list[Any]
9
10 def __init__(self, keyboard: Keyboard):
11 self.keyboard = keyboard
12
13 def prepare(self):
14 base = self.keyboard.modes["base"]
15 self.prepared = [self.prepare_key(key) for key in base.keys]
16
17 def prepare_key(self, key: Key) -> Any:
18 pass
19
20 def tick(self, ticks_ms: int):
21 pass
22
23 def get_color(self, i: int, key: Key):
24 pass
25
26
27 class ColorScale(RGBEffect):
28 def prepare_key(self, key: Key):
29 in_scale = self.keyboard.scale.is_in_scale(key.pitch)
30
31 if in_scale == "core":
32 return (0.1, 1.0, 0.2)
33 elif in_scale:
34 return (0.7, 0.9, 0.15)
35 else:
36 return (0.3, 0.8, 0.0)
37
38 def get_color(self, i: int, key: Key):
39 if key.pitch < 0 or key.pitch > 127:
40 return 0
41
42 hue, sat, key_val = self.prepared[i]
43
44 base = self.keyboard.modes["base"]
45 note_for_pitch = base.notes.get(key.pitch)
46 note_for_pitch = note_for_pitch or base.notes_expiring.get(key.pitch)
47
48 if key.note:
49 value = 1.0
50 elif note_for_pitch:
51 value = note_for_pitch.expiry * 0.8
52 else:
53 value = 0.0
54
55 rest = 1.0 - key_val
56 value = key_val + value * rest
57 return hsv_to_rgb(hue, sat, value)
58
59
60 class Rainbow(RGBEffect):
61 hue_shift: float = 0.0
62 last_ticks_ms: int
63
64 def tick(self, ticks_ms: int):
65 if hasattr(self, "last_ticks_ms"):
66 delta = ticks_diff(ticks_ms, self.last_ticks_ms)
67 self.hue_shift = (self.hue_shift + delta / 1000 / 6) % 1
68
69 self.last_ticks_ms = ticks_ms
70
71 def prepare_key(self, key: Key):
72 return (0, 0.9, 1.0)
73
74 def get_color(self, i: int, key: Key):
75 if key.pitch < 0 or key.pitch > 127:
76 return 0
77
78 x, y = key.pos
79 hue, sat, val = self.prepared[i]
80 hue += x / 50 + y / 40 + self.hue_shift
81
82 return hsv_to_rgb(hue, sat, val)
83
84
85 class RainbowScale(Rainbow):
86 def prepare_key(self, key: Key):
87 if self.keyboard.scale.is_in_scale(key.pitch):
88 return (0, 0.9, 1)
89 else:
90 return (0, 0.75, 0.1)
91
92
93 class RainbowScaleAlt(Rainbow):
94 def prepare_key(self, key: Key):
95 if self.keyboard.scale.is_in_scale(key.pitch):
96 return (0, 0.9, 1)
97 else:
98 return (0.2, 0.9, 0.5)
99
100
101 RGB_EFFECTS = {
102 "scale": ColorScale,
103 "rainbow": Rainbow,
104 "rainbow scale": RainbowScale,
105 "rainbow scale alt": RainbowScaleAlt,
106 }
2929 self.notes = [sum(steps[:i]) for i in range(len(steps))]
3030 self.size = sum(steps)
3131
32 def get_hsv(self, pitch: int) -> tuple[float, float, float]:
32 def is_in_scale(self, pitch: int) -> Union[bool, Literal['core']]:
3333 relative_pitch = (pitch - self.root) % self.size
3434
3535 if relative_pitch not in self.notes:
36 # out of scale
37 return (0.3, 0.8, 0.0)
36 return False
3837
3938 if self.root <= pitch < self.root + self.size:
40 # in core scale
41 return (0.1, 1.0, 0.2)
39 return 'core'
4240
43 # in scale
44 return (0.7, 0.9, 0.15)
41 return True
4542
4643 def label(self, pitch):
4744 return self.LABELS[pitch % self.size]