diff --git a/python_payload/apps/wobbler/__init__.py b/python_payload/apps/wobbler/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f8b404203b11b1243a2c7b85fc599f837c384bde --- /dev/null +++ b/python_payload/apps/wobbler/__init__.py @@ -0,0 +1,187 @@ +from st3m.application import Application + +import math, cmath +import bl00mbox +import captouch +import leds +from st3m.ui import widgets, colours + + +class Wobbler(Application): + def __init__(self, app_ctx): + super().__init__(app_ctx) + self.blm = None + + self.any_playing = False + self.tilt_ref = None + self.tilt = None + self.pitch = -36 + + def on_enter(self, vm): + super().on_enter(vm) + if self.blm is not None: + try: + self.blm.foreground = True + except ReferenceError: + self.blm = None + + if self.blm is None: + self.build_synth() + + leds.set_slew_rate(min(160, leds.get_slew_rate())) + + def on_exit(self): + super().on_exit() + if self.any_playing: + self.blm.background_mute_override = True + else: + self.blm.delete() + self.blm = None + + def build_synth(self): + self.blm = bl00mbox.Channel("wobbler") + self.blm.gain_dB = 0 + self.mixer = self.blm.new(bl00mbox.plugins.mixer, 2) + self.mixer.signals.gain.mult = 8 + self.filter = self.blm.new(bl00mbox.plugins.filter) + self.filter.signals.gain.mult = 0.2 + self.filter.signals.reso.value = 22000 + self.env = self.blm.new(bl00mbox.plugins.env_adsr) + self.filter.signals.input << self.mixer.signals.output + self.env.signals.input << self.filter.signals.output + self.blm.signals.line_out << self.env.signals.output + self.oscs = [self.blm.new(bl00mbox.plugins.osc) for x in range(2)] + + for x, osc in enumerate(self.oscs): + osc.signals.output >> self.mixer.signals.input[x] + osc.signals.waveform.switch.SAW = True + + # background widget, doesn't do normal think/on_enter/on_exit + self.tilt_widget = widgets.Inclinometer(buffer_len=2) + self.tilt_widget.on_enter() + + def synth_callback(ins, delta_ms): + self.tilt_widget.think(ins, delta_ms) + roll = self.tilt_widget.roll + if roll is not None: + tilt = complex(roll, self.tilt_widget.pitch) + if self.tilt_ref is None: + self.tilt_ref = tilt + else: + tilt = tilt - self.tilt_ref + tilt *= 1.6 + abs_tilt = abs(tilt) + if abs_tilt > 1: + tilt /= abs_tilt + self.tilt = tilt + self.filter.signals.cutoff.tone = self.pitch + (2 - tilt.imag) * 20 + self.oscs[0].signals.pitch.tone = self.pitch + tilt.real * 2 + self.oscs[1].signals.pitch.tone = self.pitch - tilt.real * 2 + + self.blm.callback = synth_callback + + def think(self, ins, delta_ms): + super().think(ins, delta_ms) + + if self.input.buttons.app.middle.pressed: + roll = self.tilt_widget.roll + if roll is not None: + tilt = complex(roll, self.tilt_widget.pitch) + self.tilt_ref = tilt + + any_playing = False + pos = 0 + pos_div = 0 + for x in range(0, 10, 2): + if pressed := ins.captouch.petals[x].pressed: + any_playing = True + pos += ins.captouch.petals[x].pos.real + pos_div += 1 + """ + if x == 2: + for x, osc in enumerate(self.oscs): + if pressed: + osc.signals.waveform.switch.SAW = True + else: + osc.signals.waveform.switch.TRI = True + elif x == 8: + self.mixer.signals.gain.mult = 8 if pressed else 2 + """ + + if pos_div: + self.pitch = -44 + (pos / pos_div + 1) * 6 + + if self.any_playing != any_playing: + if any_playing: + self.env.signals.trigger.start() + else: + self.env.signals.trigger.stop() + self.any_playing = any_playing + + def get_help(self): + ret = ( + "Press any top petal to play the note. How far away from " + "the center you press controls pitch.\n\n" + "Tilt forward/backward to change filter cutoff and left/right " + "to detune the oscillators. Press app button down to zero the " + "tilt reference.\n\n" + "If you exit while playing the note it will continue playing " + "in the background while still responding to tilt." + ) + return ret + + def draw(self, ctx): + ctx.gray(0).rectangle(-120, -120, 240, 240).fill() + + col = (1, 1, 1) + eye_pos = complex(0, 0) + hue = 0 + + tilt = self.tilt_widget.pitch + if self.tilt is not None: + eye_pos = self.tilt * 10 + hue = (eye_pos.real + eye_pos.imag) % math.tau + eye_pos *= 5 + + ctx.radial_gradient( + eye_pos.real, eye_pos.imag, 0, eye_pos.real, eye_pos.imag, 100 + ) + for x in range(5): + rel = x / 4 + nval = rel + if self.any_playing: + nval /= 3 + col = colours.hsv_to_rgb(hue + math.tau * rel, 1 - rel / 2, 1 - nval) + ctx.add_stop(rel, col, 1) + for y in range(8): + leds.set_rgb(y * 5 + x, *col) + leds.update() + + length = 100 + openness = 60 + ctx.line_width = 1 + ctx.move_to(length, 0).quad_to(0, openness, -length, 0).stroke() + ctx.move_to(length, 0).quad_to(0, -openness, -length, 0).stroke() + for x in range(3): + lash_length = 0.3 if x == 1 else 0.2 + t = (x + 1) / 4 + nt = 1 - t + # nt^2 * p0 + 2 * nt * t * p1 + t^2 * P2 + lash_start_x = (nt * nt - t * t) * length + lash_start_y = t * nt * openness * 2 + # 2 * (nt - t) * p1 - 2 * nt * p0 + 2 * t * p2 + lash_angle_x = 2 * length + lash_angle_y = 2 * (nt - t) * openness + ctx.move_to(lash_start_x, lash_start_y) + ctx.rel_line_to( + lash_angle_y * lash_length, lash_angle_x * lash_length + ).stroke() + + ctx.arc(eye_pos.real, eye_pos.imag, 10, 0, math.tau, 0).stroke() + ctx.arc(eye_pos.real, eye_pos.imag, 20, 0, math.tau, 0).stroke() + + +if __name__ == "__main__": + import st3m.run + + st3m.run.run_app(Wobbler) diff --git a/python_payload/apps/wobbler/flow3r.toml b/python_payload/apps/wobbler/flow3r.toml new file mode 100644 index 0000000000000000000000000000000000000000..926c9988ceaa264eaba4c62995faf470abfc538a --- /dev/null +++ b/python_payload/apps/wobbler/flow3r.toml @@ -0,0 +1,11 @@ +[app] +name = "wobbler" +category = "Music" + +[entry] +class = "Wobbler" + +[metadata] +author = "Flow3r Badge Authors" +license = "LGPL-3.0-only" +url = "https://git.flow3r.garden/flow3r/flow3r-firmware"