add various RGB effects
s-ol
1 year, 2 months ago
15 | 15 | from .core import Mode |
16 | 16 | from .matrix import Matrix |
17 | 17 | 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 | |
19 | 20 | from .menu import ( |
20 | 21 | Settings, |
21 | 22 | SliderSetting, |
80 | 81 | "TRS MIDI CHANNEL", 15, fmt="CH{}", thresh=_eq |
81 | 82 | ), |
82 | 83 | "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 | ), | |
83 | 89 | "rgb_bright": SliderSetting( |
84 | 90 | "LED BRIGHTNESS", |
85 | 91 | 100, |
124 | 130 | self.settings.on('midi_ch_usb', self.on_midi_ch_usb) |
125 | 131 | self.settings.on('midi_ch_trs', self.on_midi_ch_trs) |
126 | 132 | self.settings.on('midi_vel', self.on_midi_vel) |
133 | self.settings.on('rgb_eff', self.on_rgb_eff) | |
127 | 134 | self.settings.on('rgb_bright', self.on_rgb_bright) |
128 | 135 | self.settings.on('jam_timeout', self.on_jam_timeout) |
129 | 136 | self.settings.on('layout_name', self.on_layout) |
144 | 151 | "midi_ch_usb", |
145 | 152 | "midi_ch_trs", |
146 | 153 | "midi_vel", |
154 | "rgb_eff", | |
147 | 155 | "rgb_bright", |
148 | 156 | "jam_timeout", |
149 | 157 | "layout_name", |
164 | 172 | def on_midi_vel(self, vel): |
165 | 173 | self.velocity = vel |
166 | 174 | |
175 | def on_rgb_eff(self, effect): | |
176 | self.modes["base"].rgb = RGB_EFFECTS[effect](self) | |
177 | self.modes["base"].rgb.prepare() | |
178 | ||
167 | 179 | def on_rgb_bright(self, b): |
168 | 180 | self.pixels.brightness = b / 100 |
169 | 181 | |
173 | 185 | def on_layout(self, v): |
174 | 186 | name = self.settings.get('layout_name').value |
175 | 187 | 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) | |
182 | 189 | self.modes["base"].update_scale() |
183 | 190 | |
184 | 191 | def on_scale(self, v): |
6 | 6 | |
7 | 7 | from .util import ticks_diff, FONT_10, led_map |
8 | 8 | from .core import Key, Note, Mode |
9 | from .rgb import ColorScale | |
9 | 10 | |
10 | 11 | |
11 | 12 | class BaseMode(Mode): |
12 | 13 | keys: list[Key] |
14 | rgb: RGBEffect | |
15 | ||
13 | 16 | notes: dict[int, Note] |
14 | 17 | notes_expiring: dict[int, Note] |
15 | 18 | |
22 | 25 | super().__init__(*args) |
23 | 26 | |
24 | 27 | self.keys = [Key(self.keyboard, i) for i in range(48)] |
28 | self.rgb = ColorScale(self.keyboard) | |
29 | ||
25 | 30 | self.notes = {} |
26 | 31 | self.notes_expiring = {} |
27 | 32 | |
68 | 73 | for key in self.keys: |
69 | 74 | key.update_scale() |
70 | 75 | |
76 | self.rgb.prepare() | |
77 | ||
71 | 78 | self.scale_label.text = self.keyboard.scale.format() |
72 | 79 | |
73 | 80 | def tick(self, ticks_ms: int): |
74 | 81 | for note in self.notes_expiring.values(): |
75 | 82 | note.update_expiry(ticks_ms) |
76 | 83 | |
84 | self.rgb.tick(ticks_ms) | |
85 | ||
77 | 86 | def update_pixels(self, pixels: NeoPixel): |
78 | 87 | pixels.fill(0) |
79 | 88 | super().update_pixels(pixels) |
80 | 89 | |
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) | |
83 | 92 | |
84 | 93 | if self._notes_dirty: |
85 | 94 | active = [pitch for pitch in self.notes] |
205 | 214 | |
206 | 215 | if i < len(base.keys): |
207 | 216 | key = base.keys[i] |
208 | self.keyboard.settings.get("scale_root").value = key._pitch | |
217 | self.keyboard.settings.get("scale_root").value = key.pitch | |
209 | 218 | self.keyboard.settings.dispatch("scale_root") |
210 | 219 | self.pressed_something = True |
211 | 220 | return False |
68 | 68 | '''logical coordinates for this key.''' |
69 | 69 | |
70 | 70 | note: None | Note |
71 | ||
72 | _pitch: int | |
73 | _color: tuple[float, float, float] | |
71 | pitch: int | |
74 | 72 | |
75 | 73 | def __init__(self, keyboard: Keyboard, i: int): |
76 | 74 | x = i % 12 |
86 | 84 | self.note = None |
87 | 85 | |
88 | 86 | 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) | |
112 | 88 | |
113 | 89 | def on_press(self): |
114 | 90 | if self.note: |
115 | 91 | self.note.off() |
116 | 92 | |
117 | if self._pitch < 0 or self._pitch > 127: | |
93 | if self.pitch < 0 or self.pitch > 127: | |
118 | 94 | return |
119 | 95 | |
120 | self.note = Note(self.keyboard, self._pitch) | |
96 | self.note = Note(self.keyboard, self.pitch) | |
121 | 97 | self.note.on() |
122 | 98 | |
123 | 99 | def on_release(self): |
28 | 28 | def get_pitch(self, key: Key) -> int: |
29 | 29 | x, y = key.pos |
30 | 30 | 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 | } |
29 | 29 | self.notes = [sum(steps[:i]) for i in range(len(steps))] |
30 | 30 | self.size = sum(steps) |
31 | 31 | |
32 | def get_hsv(self, pitch: int) -> tuple[float, float, float]: | |
32 | def is_in_scale(self, pitch: int) -> Union[bool, Literal['core']]: | |
33 | 33 | relative_pitch = (pitch - self.root) % self.size |
34 | 34 | |
35 | 35 | if relative_pitch not in self.notes: |
36 | # out of scale | |
37 | return (0.3, 0.8, 0.0) | |
36 | return False | |
38 | 37 | |
39 | 38 | if self.root <= pitch < self.root + self.size: |
40 | # in core scale | |
41 | return (0.1, 1.0, 0.2) | |
39 | return 'core' | |
42 | 40 | |
43 | # in scale | |
44 | return (0.7, 0.9, 0.15) | |
41 | return True | |
45 | 42 | |
46 | 43 | def label(self, pitch): |
47 | 44 | return self.LABELS[pitch % self.size] |