diff --git a/python_payload/apps/violin/__init__.py b/python_payload/apps/violin/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..841286344a3869a67717726e10cf8b4129cdb86d
--- /dev/null
+++ b/python_payload/apps/violin/__init__.py
@@ -0,0 +1,606 @@
+import captouch
+import bl00mbox
+import leds
+
+import cmath
+import math
+
+from st3m.application import Application
+from st3m.ui import led_patterns, widgets
+
+# i stole a waveform from wikipedia and all i got was this lousy waveform
+# fmt: off
+wavetable_G = [ -31908, -31214, -29327, -25617, -19835, -13173, -5860, 430, 6178, 9928, 10517, 8145, 5175, 5247, 9082,
+                14845, 21967, 27964, 31532, 32686, 31010, 27298, 23469, 19145, 16123, 12363, 7738, 4198, 1774, 470,
+                -1782, -5034, -8392, -9682, -7834, -4587, 384, 5137, 8867, 9100, 5054, -1239, -7294, -11015, -11725,
+                -11018, -10495, -10393, -11481, -13339, -15281, -17068, -18206, -18616, -18261, -17259, -16580, -14969,
+                -13589, -11777, -9421, -6406, -3500, -622, 190, 275, 1082, 3731, 6917, 10332, 13113, 15365, 15551, 13645,
+                9494, 4473, 822, -571, -225, 3121, 6186, 8317, 8741, 8079, 5348, 2070, -961, -2887, -3492, -1762, 154,
+                2427, 3639, 3447, 1661, 836, 1407, 2701, 4776, 5851, 6284, 5217, 3111, 1050, -478, -2179, -3504, -5057,
+                -7776, -10438, -9483, -2039, 7867, 14062, 14379, 11906, 9318, 7723, 6459, 5114, 5220, 5800, 5678, 2184,
+                -5499, -14483, -22161, -27325, -29727]
+
+wavetable_A = [ -32767, -31475, -30284, -28603, -26239, -24215, -23129, -21511, -19390, -17493, -16263, -15358, -14344,
+                -12856, -10677, -8105, -4704, -682, 3135, 7347, 12576, 17683, 21531, 24140, 26388, 27561, 27630, 27327,
+                25666, 22949, 19891, 17256, 13853, 10037, 6571, 3688, 843, -355, -869, -792, -50, 3607, 7571, 11713,
+                15976, 20482, 24073, 27454, 30329, 31943, 31778, 30676, 28887, 26673, 24447, 21763, 19467, 17814, 16826,
+                16464, 16516, 17115, 18404, 20744, 23196, 25173, 26214, 25629, 23905, 20921, 16930, 12316, 7937, 4004,
+                385, -2880, -5435, -6997, -7843, -7838, -6809, -5498, -4290, -3957, -5058, -8246, -11763, -14893, -17610,
+                -20118, -22048, -22408, -21980, -21133, -18463, -14705, -11003, -7341, -3085, -1132, 173, 569, -268,
+                -3059, -5441, -8344, -11743, -14770, -16139, -16539, -16075, -14852, -13130, -11212, -9252, -7450, -6252,
+                -5424, -5269, -5695, -6513, -7898, -10547, -13393, -16203, -20419, -25175, -28446, -30443]
+# fmt: on
+
+background_color = (0, 0, 0)
+default_color = (0.7, 0.7, 0.7)
+active_string_color = (0, 0.5, 1)
+target_string_color = (0, 1, 0)
+
+
+class WavetableOsc(bl00mbox.Patch):
+    def __init__(self, chan, curve):
+        super().__init__(chan)
+
+        osc = self.new(bl00mbox.plugins.osc)
+        osc.antialiasing = False
+        osc.signals.waveform.switch.SAW = True
+        wave = self.new(bl00mbox.plugins.distortion)
+        wave.signals.input << osc.signals.output
+
+        self._wave = wave
+
+        self.signals.pitch = osc.signals.pitch
+        self.signals.output = wave.signals.output
+        self.set_curve_normalized(curve)
+
+    def set_curve_normalized(
+        self, curve, autobias=True, autoscale=True, suggest_curve=True
+    ):
+        ref_curve = curve
+
+        # for audio we want the average of the waveform to be 0. if fed by a saw or triangle wave,
+        # this directly transfers to the average of the wavetable, so we remove it.
+        # if you switch up the waveform of the oscillator this doesn't hold true and you
+        # must remove DC by other means.
+        if autobias:
+            avg = sum(curve) / len(curve)
+            curve = [x - avg for x in curve]
+
+        # scale it to the max. must be done after zeroing average.
+        # maybe RMS would be smarter here but then we'd have to set an expected crest ratio
+        # and all that and we're not gonna do that :3
+        if autoscale:
+            max_abs = max([abs(x) for x in curve])
+            curve = [int(32767 * x / max_abs) for x in curve]
+
+        self._wave.curve = curve
+
+        # if you save your wavetable somewhere (as we do at the top of the file), you might as well
+        # save it as it is actually applied. if there's a difference this option prints it out
+        # on the REPL so that you can paste it in your code file.
+        # we already did that here, so we could call this with all optional arguemnts False to make
+        # loading a teeny tiny bit faster, but since we expect people to copy-paste this code and
+        # change up the wavetable we are not doing this here. but we could.
+        if suggest_curve:
+            curve = self._wave.curve
+            already_good = False
+            if len(ref_curve) == len(curve):
+                already_good = any(
+                    [curve[x] != ref_curve[x] for x in range(len(curve))]
+                )
+            if not already_good:
+                print("curve was resampled/-scaled/-biased, try this:")
+                print(self._wave.curve)
+
+
+class ViolinPatch(bl00mbox.Patch):
+    def __init__(self, chan):
+        super().__init__(chan)
+        mp = self.new(bl00mbox.plugins.multipitch, 1)
+        lo_osc = self.new(WavetableOsc, wavetable_G)
+        lo_osc.signals.pitch << mp.signals.thru
+        hi_osc = self.new(WavetableOsc, wavetable_A)
+        hi_osc.signals.pitch << mp.signals.thru
+
+        mixer = self.new(bl00mbox.plugins.mixer, 2)
+        mixer.signals.input[0] << hi_osc.signals.output
+        mixer.signals.input[1] << lo_osc.signals.output
+
+        prs = [self.new(bl00mbox.plugins.range_shifter) for x in range(2)]
+        prs[1].signals.input << prs[0].signals.output
+        for x in range(2):
+            pr = prs[x]
+            mixer.signals.input_gain[x] << pr.signals.output
+            if x:
+                pr.signals.input_range[0] = 0
+                pr.signals.input_range[1] = 4096
+            pr.signals.output_range[0] = 4096
+            pr.signals.output_range[1] = 0
+
+        mixer.signals.gain.mult = 0
+
+        lp = self.new(bl00mbox.plugins.filter)
+        lp.signals.cutoff.freq = 8000
+        mixer.signals.output >> lp.signals.input
+
+        # we were thinking about this for a while: wouldn't it be cool to just
+        # directly drive pitch bend from from user input instead of having this
+        # pesky osc in-between? it sounds good on paper, but turns out u can't
+        # switch up ur curl that quickly. how about modulo on the phase of position?
+        # ...interesting, we'd have to chase the average tilt of the wiggle, but also
+        # to control low wiggle sensitivity we'd probs need to adjust field on the fly
+        # or smth... it an amount of math in there. and this rigging works alright
+        # we think.
+        # about waveform: our widget vibrato output is unstable enough that we can get
+        # free random AM/FM from it, so that's cool
+
+        vib_osc = self.new(bl00mbox.plugins.osc)
+        vib_osc.speed = "lfo"
+        vib_osc.signals.waveform.switch.SINE = True
+        vib_osc.signals.pitch.freq = 5
+        mp.signals.mod_in << vib_osc.signals.output
+        mp.signals.mod_sens = 0
+
+        env = self.new(bl00mbox.plugins.env_adsr)
+        env.signals.input << lp.signals.output
+        env.signals.attack = 150
+        env.signals.release = 200
+        env.signals.sustain = 32767
+
+        self.signals.vibrato_speed = vib_osc.signals.pitch
+        self.signals.vibrato_depth = mp.signals.mod_sens
+        self.signals.volume = mixer.signals.gain
+        self.signals.timbre = prs[0].signals.input
+        self.signals.pitch = mp.signals.input
+        self.signals.output = env.signals.output
+        self.signals.trigger = env.signals.trigger
+        self.signals.env_gain = env.signals.env_output
+
+
+class ViolinReverb(bl00mbox.Patch):
+    def __init__(self, chan):
+        super().__init__(chan)
+        num_delays = 4
+        delays = [self.new(bl00mbox.plugins.delay_static) for x in range(num_delays)]
+        mixers = [
+            self.new(bl00mbox.plugins.mixer, num_delays) for x in range(num_delays)
+        ]
+        buffer = self.new(bl00mbox.plugins.mixer, 1)
+        feedback_buffer = self.new(bl00mbox.plugins.mixer, 1)
+
+        buffer.signals.output >> delays[0].signals.input
+
+        self.signals.input = buffer.signals.input[0]
+        self.signals.output = mixers[0].signals.output
+
+        delays[0].signals.feedback = 0
+        delays[0].signals.dry_vol = 0
+
+        for x in range(1, num_delays):
+            delays[x].signals.input << mixers[x].signals.output
+            delays[x].signals.output >> mixers[0].signals.input[x]
+            mixers[x].signals.input[0] << delays[0].signals.output
+            for j in range(1, num_delays):
+                delays[j].signals.output >> mixers[x].signals.input[j]
+            delays[x].signals.feedback = 0
+            delays[x].signals.dry_vol = 0
+            delays[x].signals.time = min(30 * (x ** (3 / 2)), 500)
+            mixers[x].signals.gain << feedback_buffer.signals.output
+
+        mixers[0].signals.input[0] << buffer.signals.output
+        mixers[0].signals.gain.mult = 1
+        delays[0].signals.time = 100
+        delays[0].signals.level = 0
+        self.signals.volume = delays[0].signals.level
+
+        # not great, be careful of oscillations
+        self.signals.feedback = feedback_buffer.signals.input[0]
+        feedback_buffer.signals.input[0] = 4096 * 0.4
+        feedback_buffer.signals.gain.mult = -1
+
+
+def sqabs(val):
+    return val.real * val.real + val.imag * val.imag
+
+
+class ViolinWidget(widgets.PetalWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.ring_len = 16
+        self.min_len = 3
+        self.min_log_len = 2
+        self.max_log_len = 6
+        self.avg_pos = None
+        self.curls = [0.0] * self.ring_len
+        self.sqvels = [0.0] * self.ring_len
+        self._autoclear()
+
+    def think(self, ins, delta_ms):
+        log = ins.captouch.petals[self.petal].log
+        if not log:
+            return
+        for frame in log:
+            if not self._append_and_validate(frame):
+                continue
+            if self._log.length() < self.min_log_len:
+                continue
+            self._log.crop(-self.max_log_len)
+            pos = self._log.frames[-1].pos
+            prevpos = self._log.frames[-2].pos
+            # we're not using this timestamp as a negative cpu load
+            # random noise modulation load :> units are whatever
+            vel = pos - prevpos
+            pos = (pos + prevpos) / 2
+            if self.avg_pos is None:
+                self.avg_pos = pos
+            else:
+                self.avg_pos += (pos - self.avg_pos) * 0.1
+            pos -= self.avg_pos
+            sqvel = sqabs(vel)
+            self.sqvels[self.ring_index] = sqvel
+            curl = 0
+            if pos:
+                # cross product to measure angle between movement
+                # and
+                curl = pos.real * vel.imag - pos.imag * vel.real
+                # it kinda helps for low amplitude circles
+                curl /= abs(pos)
+            # getting rid of sign here but also squaring it.
+            # curls need a lot of squaring kinda.
+            # but if u wanna not square do put an abs in there
+            self.curls[self.ring_index] = curl * curl
+            if self.num_entries < self.ring_len:
+                self.num_entries += 1
+            self.ring_index += 1
+            self.ring_index %= self.ring_len
+        self.active = self.num_entries >= self.min_len
+        if self.active:
+            curl = 0
+            sqvel = 0
+            for i in range(self.num_entries):
+                curl += self.curls[i]
+                sqvel += self.sqvels[i]
+            sqvel = sqvel / self.ring_len
+            curl /= self.num_entries
+            if sqvel:
+                # random thoughtless curve mangling, it feels alright ig?
+                # it is questionable whether this should be before the filter instead.
+                # but it's here, for now, and that's good enough ^w^
+                curl /= sqvel * 4 + 0.2
+                curl /= sqvel * 2 + 0.6
+            else:
+                curl = 0
+            # eyeballin it
+            # you'll probably have to change gain here if u make adjustments to
+            # the math above, just print values while interacting
+            sqvel *= 4
+            curl *= 7
+            curl *= curl
+            self.volume = min(sqvel, 1)
+            self.vibrato = min(curl, 1)
+
+    def on_exit(self):
+        super().on_exit()
+        self._autoclear()
+
+    def _autoclear(self):
+        self.avg_pos = None
+        self.num_entries = 0
+        self.ring_index = 0
+        self.volume = 0
+        self.vibrato = 0
+
+
+class App(Application):
+    def __init__(self, app_ctx) -> None:
+        super().__init__(app_ctx)
+        self.captouch_conf = captouch.Config.default()
+        # we wamt equal spin times on top and bottom so that we don't have to deal
+        # with behavioral differences based on sample rate
+        self.captouch_conf.petals[1].mode = 1
+        self.violin_widgets = [
+            ViolinWidget(self.captouch_conf, petal) for petal in range(0, 10, 2)
+        ]
+        self.reverb_widget = widgets.Slider(self.captouch_conf, 5)
+        self.tilt_widget = widgets.Inclinometer(buffer_len=8)
+        self.widgets = self.violin_widgets + [self.reverb_widget, self.tilt_widget]
+
+        # index of the widget we consider active. [0..4] or None.
+        self.active_widget_index = None
+        # which string we are targeting
+        self.target_string = 1
+        # same but continuous
+        self.target_string_unfloored = 1
+
+        # fret corresponding to active_widget_index
+        self.fret = 4
+        self.string = None
+        self.pitch = None
+
+        # string that is targeted when setting tilt reference.
+        # lowest string is 0, highest is 3
+        self.ref_string = 2
+        # stores tilt reference
+        self.ref_tilt = 0
+        # angular tilt distance between strings, in radians (here 12 degrees)
+        self.tilt_spacing = 12 * math.tau / 360
+        # tilt hysteresis in units of tilt_spacing, i.e. 0.2*12deg = 2.4deg
+        self.tilt_hysteresis = 0.2
+        self.bass = 1
+        self.app_is_left = None
+
+        # some helper storage for drawing
+        self.draw_sin = 0
+        self.full_redraw = True
+        self.always_full_redraw = False
+        self.reverb_volume = 0.1
+        self.show_reverb_bar = False
+        self.volume = 0
+        self.vibrato = 0
+
+    def _build_synth(self):
+        self.blm = bl00mbox.Channel("violin")
+        self.synth = self.blm.new(ViolinPatch)
+        self.reverb = self.blm.new(ViolinReverb)
+        self.synth.signals.output >> self.reverb.signals.input
+        self.blm.signals.line_out << self.reverb.signals.output
+
+    def draw(self, ctx) -> None:
+        ctx.text_align = ctx.CENTER
+        ctx.text_baseline = ctx.IDEOGRAPHIC
+        ctx.font = "Comic Mono"
+        if self.always_full_redraw:
+            self.full_redraw = True
+        if self.full_redraw:
+            ctx.rgb(*background_color).rectangle(-120, -120, 240, 240).fill()
+            ctx.rgb(*default_color)
+            num_holes = max(self.bass + 1, 1)
+            f_hole = "f" * num_holes
+            width = 90 - 3 * (3 - num_holes)
+            for x in range(2):
+                ctx.save()
+                if x:
+                    ctx.apply_transform(-1, 0, 0, 0, 1, 0, 0, 0, 1)
+                ctx.translate(-width, 20)
+                ctx.move_to(1, 2)
+                ctx.rotate(-0.2)
+                if self.bass == -1:
+                    ctx.rel_move_to(1, 6)
+                    ctx.scale(0.8, 1)
+                ctx.font_size = 32
+                ctx.text(f_hole)
+                ctx.move_to(0, 0)
+                ctx.rotate(math.pi)
+                if self.bass == -1:
+                    ctx.rel_move_to(1, 6)
+                ctx.text(f_hole)
+                ctx.restore()
+        else:
+            ctx.rgb(*background_color).rectangle(-120, 75, 240, 10).fill()
+            ctx.rgb(*background_color).rectangle(-120, -105, 240, 10).fill()
+            pos = 63
+            ctx.rgb(*background_color).rectangle(-pos, -120, pos * 2, 195).fill()
+
+        ctx.save()
+        ctx.translate(0, 100)
+        size = 30
+        if self.full_redraw:
+            ctx.line_width = 2
+            ctx.rgb(*default_color)
+            if self.show_reverb_bar:
+                for x in range(2):
+                    angle = 0.045
+                    rad = 20
+                    pos = size + 10 - rad  # + x * 5
+                    ctx.arc(
+                        -pos,
+                        0,
+                        rad + x * 5,
+                        (0.5 - angle) * math.tau,
+                        (0.5 + angle) * math.tau,
+                        0,
+                    ).stroke()
+                    ctx.arc(
+                        pos,
+                        0,
+                        rad + x * 5,
+                        (1 - angle) * math.tau,
+                        (1 + angle) * math.tau,
+                        0,
+                    ).stroke()
+            else:
+                ctx.move_to(-size - 10, -6)
+                ctx.rel_line_to(0, 11)
+                ctx.rel_curve_to(-5, -2, -5, -7, 0, -5)
+                ctx.stroke()
+                ctx.save()
+                ctx.translate(size + 10 - 1, 0)
+                ctx.move_to(3, -6)
+                ctx.rel_line_to(0, 11).stroke()
+                ctx.move_to(0, -2).rel_line_to(6, -1).stroke()
+                ctx.move_to(0, 2).rel_line_to(6, -1).stroke()
+                ctx.restore()
+            padding = 4
+            ctx.round_rectangle(
+                -size - padding,
+                -5 - padding,
+                (size + padding) * 2,
+                (5 + padding) * 2,
+                3 + padding - 1,
+            ).fill()
+        ctx.rgb(*background_color)
+        padding = 2
+        ctx.round_rectangle(
+            -size - padding,
+            -5 - padding,
+            (size + padding) * 2,
+            2 * (5 + padding),
+            3 + padding + 1,
+        ).fill()
+        ctx.rgb(*default_color)
+        if self.show_reverb_bar:
+            ctx.round_rectangle(-size, -5, self.reverb_volume * 2 * size, 10, 3).fill()
+        else:
+            ctx.round_rectangle(
+                -self.vibrato * size, -5, self.vibrato * size * 2, 10, 3
+            ).fill()
+        ctx.restore()
+
+        if self.app_is_left is not None:
+            string_spacing = 30
+            if not self.app_is_left:
+                string_spacing = -string_spacing
+            target_pos = (self.target_string_unfloored - 2) * string_spacing
+            ctx.rgb(*target_string_color)
+            ctx.arc(target_pos, 80, 3, 0, math.tau, 0).fill()
+            ctx.arc(target_pos, -100, 3, 0, math.tau, 0).fill()
+            for x in range(4):
+                ctx.move_to((x - 1.5) * string_spacing, -90)
+                ctx.line_width = 5 - x
+                if x == self.string:
+                    ctx.rgb(*active_string_color)
+                    # hey we could just take the output of the synthesizer here, wouldn't
+                    # that be much better than this fake sine?
+                    # ... yyyeah it doesn't really look all that good. sad. :'(
+                    self.draw_sin %= math.tau
+                    amplitude = 25 * self.volume * math.sin(self.draw_sin)
+                    amplitude *= self.synth.signals.env_gain.mult
+                    number = 3 + self.fret
+                    quad_len = 160 / number
+                    for _ in range(number):
+                        amplitude *= -1
+                        ctx.rel_quad_to(amplitude, quad_len / 2, 0, quad_len)
+                else:
+                    ctx.rgb(*default_color)
+                    ctx.rel_line_to(0, 160)
+                if x == self.target_string:
+                    ctx.rgb(*target_string_color)
+                ctx.stroke()
+        self.full_redraw = False
+
+    def think(self, ins, delta_ms) -> None:
+        super().think(ins, delta_ms)
+        self.app_is_left = ins.buttons.app_is_left
+        for widget in self.widgets:
+            widget.think(ins, delta_ms)
+
+        self.draw_sin += delta_ms / 20
+
+        bass = self.bass
+        bass += self.input.captouch.petals[1].whole.pressed
+        bass += self.input.captouch.petals[9].whole.pressed
+        bass -= self.input.captouch.petals[3].whole.pressed
+        bass -= self.input.captouch.petals[7].whole.pressed
+        bass = max(-1, min(2, bass))
+        if bass != self.bass:
+            self.bass = bass
+            self.full_redraw = True
+
+        tilt = self.tilt_widget.roll
+        if tilt is not None:
+            if ins.buttons.app == 2 or self.ref_tilt is None:
+                self.ref_tilt = tilt
+                tilt = None
+            else:
+                tilt -= self.ref_tilt
+                tilt_spacing = self.tilt_spacing
+                if not self.app_is_left:
+                    tilt_spacing = -tilt_spacing
+                string = tilt / tilt_spacing + 0.5 + self.ref_string
+                predicted_string = math.floor(string)
+                if predicted_string > self.target_string:
+                    string -= self.tilt_hysteresis
+                elif predicted_string < self.target_string:
+                    string += self.tilt_hysteresis
+                self.target_string_unfloored = string
+                self.target_string = max(0, min(3, math.floor(string)))
+
+        active_widget_index = None
+        volume = 0
+        self.vibrato = 0
+        for x, widget in enumerate(self.violin_widgets):
+            if widget.volume > volume:
+                volume = widget.volume
+                self.volume = volume
+                self.vibrato = widget.vibrato
+                active_widget_index = x
+
+        new_note_happened = False
+        if self.active_widget_index != active_widget_index:
+            self.active_widget_index = active_widget_index
+            if active_widget_index is None:
+                self.string = None
+                self.synth.signals.trigger.stop()
+            else:
+                self.string = self.target_string
+                self.fret = 4 - self.active_widget_index
+                self.pitch = self.fret + self.string * 5 - self.bass * 12 - 14
+                self.synth.signals.pitch.tone = self.pitch
+                self.synth.signals.trigger.start()
+                new_note_happened = True
+
+        self.synth.signals.volume.value = 1000 * self.volume
+
+        if self.string is not None:
+            self.synth.signals.vibrato_speed.freq = self.vibrato * 4 + 3
+            self.synth.signals.vibrato_depth.value = 4096 * self.vibrato / 24
+            # TODO: this isn't very nice or cover much range
+            timbre = (19 - self.pitch) / 19
+            timbre = max(-1, min(1, timbre * 2 - 1))
+            self.synth.signals.timbre.value = 32767 * timbre
+
+        if self.reverb_widget.active != self.show_reverb_bar:
+            self.full_redraw = True
+            self.show_reverb_bar = self.reverb_widget.active
+        if self.reverb_widget.active:
+            self.reverb_volume = (1 - self.reverb_widget.pos.real) / 2
+        self.reverb.signals.volume.value = self.reverb_volume * 30000
+
+        if new_note_happened:
+            led_patterns.pretty_pattern()
+            leds.update()
+
+    def on_enter(self, vm):
+        super().on_enter(vm)
+        for widget in self.widgets:
+            widget.on_enter()
+        self.ref_tilt = None
+        self.string = None
+        self._build_synth()
+        self.captouch_conf.apply()
+        led_patterns.pretty_pattern()
+        leds.update()
+        self.always_full_redraw = True
+        self.full_redraw = True
+
+    def on_exit(self):
+        super().on_exit()
+        for widget in self.widgets:
+            widget.on_exit()
+        self.blm = None
+        self.always_full_redraw = True
+
+    def on_enter_done(self):
+        self.always_full_redraw = False
+
+    def on_exit_done(self):
+        self.always_full_redraw = False
+
+    def get_help(self):
+        ret = (
+            "Violin Emulator\n\n"
+            'Rub the top petals to play different "frets".\n\n'
+            "Tilt left/right to switch string. String switching occurs when the active petal changes. "
+            "Pressing the app button down zeros the tilt reference to the current orientation.\n\n"
+            "You can add vibrato by rubbing in circles.\n\n"
+            "Tapping petals 1 or 9 shifts the global pitch an octave lower, tapping petals 3 or 7 shifts it "
+            "an octave higher.\n\n"
+            "Petal 5 is a slider to control the amount of reverb."
+        )
+        return ret
+
+
+if __name__ == "__main__":
+    from st3m.run import run_app
+
+    run_app(App, "/flash/sys/apps/violin")
diff --git a/python_payload/apps/violin/flow3r.toml b/python_payload/apps/violin/flow3r.toml
new file mode 100644
index 0000000000000000000000000000000000000000..5d9df0d1e98a4c7111b205852ab6e8e3b546f2d8
--- /dev/null
+++ b/python_payload/apps/violin/flow3r.toml
@@ -0,0 +1,8 @@
+[app]
+name = "violin"
+category = "Music"
+
+[metadata]
+author = "Flow3r Badge Authors"
+license = "LGPL-3.0-only"
+url = "https://git.flow3r.garden/flow3r/flow3r-firmware"