diff --git a/python_payload/apps/demo_melodic/__init__.py b/python_payload/apps/demo_melodic/__init__.py
index 881edc62deb89e382ecdc8c6bb85fe60bb0c3ee4..0d619dca50f50ab704ab85c117c06ece32d4c270 100644
--- a/python_payload/apps/demo_melodic/__init__.py
+++ b/python_payload/apps/demo_melodic/__init__.py
@@ -76,10 +76,6 @@ class MelodicApp(Application):
         if new_page is self._fg_page:
             return
         new_page.full_redraw = True
-        if new_page.use_bottom_petals and not self._fg_page.use_bottom_petals:
-            if not self.drone_toggle.value:
-                for i in range(1, 10, 2):
-                    self.poly_squeeze.signals.trigger_in[i].stop()
         self.petal_block = [True for _ in self.petal_block]
         self._fg_page = new_page
 
@@ -197,16 +193,20 @@ class MelodicApp(Application):
         self.blm = bl00mbox.Channel("mono synth")
         self.blm.volume = 13000
         self.poly_squeeze = self.blm.new(bl00mbox.plugins.poly_squeeze, 1, 10)
+        self.arp = self.blm.new(arp)
 
         self.synth = self.blm.new(mix_env)
+
+        self.arp.signals.trigger_in = self.poly_squeeze.signals.trigger_out[0]
+        self.arp.signals.input = self.poly_squeeze.signals.pitch_out[0]
+        self.synth.signals.trigger = self.arp.signals.trigger_out
+        self.synth.signals.pitch = self.arp.signals.output
         self.synth.signals.output = self.blm.mixer
-        self.synth.signals.trigger = self.poly_squeeze.signals.trigger_out[0]
-        self.synth.signals.pitch = self.poly_squeeze.signals.pitch_out[0]
 
         mod_envs = [self.blm.new(env) for x in range(2)]
         for mod_env in mod_envs:
             mod_env.always_render = True
-            mod_env.signals.trigger = self.poly_squeeze.signals.trigger_out[0]
+            mod_env.signals.trigger = self.arp.signals.trigger_out
 
         self.mixer_page = self.synth.make_mixer_page()
 
@@ -237,6 +237,7 @@ class MelodicApp(Application):
 
         self.scale_page = ScaleSetupPage("scale")
         self.steps_page = StepsPage("steps")
+        self.arp_page = ArpPage("arp")
 
         self.notes_page = SubMenuPage("notes")
         self.sound_page = SubMenuPage("sounds")
@@ -265,7 +266,7 @@ class MelodicApp(Application):
         self.sound_page.savepage = SoundSavePage("sound", 5)
         self.notes_page.menupages = [
             self.scale_page,
-            DummyPage("arp"),
+            self.arp_page,
             DummyPage("bend"),
             DummyPage("steps"),
             # self.steps_page,
@@ -348,17 +349,10 @@ class MelodicApp(Application):
             if self.input.captouch.petals[i].whole.pressed:
                 self.poly_squeeze.signals.pitch_in[i].tone = self.scale[i]
                 self.poly_squeeze.signals.trigger_in[i].start()
-            elif (
-                self.input.captouch.petals[i].whole.released
-                and not self.drone_toggle.value
-            ):
+            elif self.input.captouch.petals[i].whole.released:
                 self.poly_squeeze.signals.trigger_in[i].stop()
 
-        if self.drone_toggle.changed and not self.drone_toggle.value:
-            for i in range(10):
-                if not ins.captouch.petals[i].pressed:
-                    self.poly_squeeze.signals.trigger_in[i].stop()
-            self.drone_toggle.changed = False
+        self.arp.block_stop = self.drone_toggle.value
 
         if self.fg_page.use_bottom_petals:
             for petal in [7, 9, 1, 3]:
diff --git a/python_payload/apps/demo_melodic/colors.py b/python_payload/apps/demo_melodic/colors.py
index afdee7b0916a4d5356b5a0d73f0786b42b1b9c85..3165c942e6a61fd9138cd3ac7b4c2cb0185e9450 100644
--- a/python_payload/apps/demo_melodic/colors.py
+++ b/python_payload/apps/demo_melodic/colors.py
@@ -81,18 +81,23 @@ class StyleClassic(StyleTheme):
         ctx.arc(0, 150, rad, 0, math.tau, 1).fill()
         ctx.rgb(*app.cols.bg)
         ctx.text_align = ctx.CENTER
-        ctx.font_size = 19
         if arrow:
-            ctx.move_to(-10, pos + 9)
-            ctx.rel_line_to(10, 5)
-            ctx.rel_line_to(10, -5)
-            ctx.stroke()
+            if isinstance(arrow, str):
+                ctx.move_to(0, pos + 9 + 9)
+                ctx.font_size = 16
+                ctx.text(arrow)
+            else:
+                ctx.move_to(-10, pos + 9)
+                ctx.rel_line_to(10, 5)
+                ctx.rel_line_to(10, -5)
+                ctx.stroke()
         if lr_arrows:
             for i in [-1, 1]:
                 ctx.move_to(63 * i, 86)
                 ctx.rel_line_to(5 * i, 4)
                 ctx.rel_line_to(-5 * i, 3)
                 ctx.stroke()
+        ctx.font_size = 19
         ctx.move_to(0, pos)
         ctx.text(text)
         ctx.restore()
diff --git a/python_payload/apps/demo_melodic/modules/synth.py b/python_payload/apps/demo_melodic/modules/synth.py
index 89a0585826a6d6341ce623b2301e5d867c698891..677adf54d338c2939f00444c46cc76e159c71d78 100644
--- a/python_payload/apps/demo_melodic/modules/synth.py
+++ b/python_payload/apps/demo_melodic/modules/synth.py
@@ -256,3 +256,104 @@ class mix_env(bl00mbox.Patch):
         page.params += [param]
         page.params += [mix_params[1]]
         return page
+
+
+class arp(bl00mbox.Patch):
+    def __init__(self, chan):
+        super().__init__(chan)
+        self.num_steps = 12
+        self.plugins.seq = self._channel.new(
+            bl00mbox.plugins.sequencer, 2, self.num_steps
+        )
+        self.plugins.mp = self._channel.new(bl00mbox.plugins.multipitch, 1)
+        self.plugins.mp.signals.shift[0] = self.plugins.seq.signals.track[0]
+
+        self.plugins.tm = self._channel.new(bl00mbox.plugins.trigger_merge, 2)
+        self.plugins.tm.signals.input[0] = self.plugins.mp.signals.trigger_thru
+        self.plugins.tm.signals.input[1] = self.plugins.seq.signals.track[1]
+
+        self.plugins.block = self._channel.new(bl00mbox.plugins.trigger_merge)
+        self.plugins.mp.signals.trigger_in = self.plugins.block.signals.output
+
+        self.signals.input = self.plugins.mp.signals.input
+        self.signals.output = self.plugins.mp.signals.output[0]
+        self.signals.trigger_in = self.plugins.block.signals.input[0]
+        self.signals.trigger_out = self.plugins.tm.signals.output
+        self.signals.bpm = self.plugins.seq.signals.bpm
+        self.signals.bpm = 80
+
+        default_pattern = [0, -12, 0, 12, 0, -12, 0, -12]
+        self.max_step = len(default_pattern)
+        default_pattern += [0] * (self.num_steps - self.max_step)
+        self.max_step -= 1
+        table = self.plugins.seq.table_int16_array
+        table[0] = 32767
+        for i in range(self.num_steps):
+            self.tone_set(i, default_pattern[i])
+            self.plugins.seq.trigger_start(1, i)
+
+        self._bypass = False
+        self.bypass = True
+
+    @property
+    def bypass(self):
+        return self._bypass
+
+    @bypass.setter
+    def bypass(self, val):
+        val = bool(val)
+        if val is self._bypass:
+            return
+        elif val:
+            self.plugins.seq.signals.sync_in.stop()
+        else:
+            self.plugins.seq.signals.sync_in = self.plugins.mp.signals.trigger_thru
+        self._bypass = val
+
+    def tone_set(self, step_index, tone):
+        if step_index >= self.num_steps:
+            return
+        table = self.plugins.seq.table_int16_array
+        table[step_index + 1] = 18367 + int(200 * tone)
+
+    def tone_get(self, step_index):
+        if step_index >= self.num_steps:
+            return
+        table = self.plugins.seq.table_int16_array
+        return (table[step_index + 1] - 18367) / 200
+
+    def trigger_set(self, step_index, val):
+        if val > 0:
+            self.plugins.seq.trigger_start(1, step_index)
+        elif val < 0:
+            self.plugins.seq.trigger_stop(1, step_index)
+        else:
+            self.plugins.seq.trigger_clear(1, step_index)
+
+    def trigger_get(self, step_index):
+        ret = self.plugins.seq.trigger_state(1, step_index)
+        if ret > 0:
+            return 1
+        elif ret < 0:
+            return -1
+        return 0
+
+    @property
+    def max_step(self):
+        return self.plugins.seq.signals.step_end.value
+
+    @max_step.setter
+    def max_step(self, val):
+        self.plugins.seq.signals.step_end = val
+
+    @property
+    def step(self):
+        return self.plugins.seq.signals.step.value
+
+    @property
+    def block_stop(self):
+        return self.plugins.block.block_stop
+
+    @block_stop.setter
+    def block_stop(self, val):
+        self.plugins.block.block_stop = val
diff --git a/python_payload/apps/demo_melodic/pages.py b/python_payload/apps/demo_melodic/pages.py
index acc8f9b8fe237501c79538cf0bfe23e74e2a9f47..11fe2b9bc2fe43d393bac48c0bc9fb6b8d6f8037 100644
--- a/python_payload/apps/demo_melodic/pages.py
+++ b/python_payload/apps/demo_melodic/pages.py
@@ -930,6 +930,197 @@ class StepsPage(LonerPage):
         ctx.restore()
 
 
+class ArpPage(LonerPage):
+    def get_help(self):
+        return (
+            "You can configure the arpeggiator here. An arp applies "
+            "a sequence of pitch shift values to whatever you are "
+            "playing.\n\n"
+            "Petal 5 allows you to tap a tempo for the arp. You can also "
+            "turn it off entirely by holding the petal for a second.\n\n"
+            "To edit the sequence, use the app button l+r to select a stage "
+            "in the sequence. You can then peform the following actions:\n\n"
+            "Petal 1+3 set the pitch shift value of the stage. The pitch "
+            "of the first stage is fixed to 0 and cannot be edited.\n\n"
+            "Petal 9 sets the currently selected stage as the last one "
+            "before the sequence overflows.\n\n"
+            "Petal 7 sets the event type of the selected stage: A filled "
+            "square means the envelope generators of the synthesizer are "
+            "retriggered when this stage is reached, an empty square means "
+            "they aren't.\n\n"
+            # "retriggered when this stage is reached, a cross means they "
+            # 'receive a "stop" event, and an empty square means they remain '
+            # "in whatever state they were.\n\n"
+            "\n\n\n\n\n\n\n"
+            "Hold petal 7 while adjusting pitch shift with petal 1+3 to do "
+            "so in 1/4 semitone resolution for microtonal experiments."
+        )
+
+    def __init__(self, name):
+        super().__init__(name)
+        self.pos = 0
+        self.microtonal_latch = False
+        self.tap_acc = 3000
+        self.press_counter = 0
+
+    def lr_press_event(self, app, lr):
+        self.full_redraw = True
+        self.pos += lr
+        self.pos %= app.arp.num_steps
+
+    def think(self, ins, delta_ms, app):
+        lr_dir = app.input.captouch.petals[1].whole.pressed
+        lr_dir -= app.input.captouch.petals[3].whole.pressed
+        if lr_dir and self.pos:
+            if ins.captouch.petals[7].pressed:
+                self.microtonal_latch = True
+                lr_dir /= 4
+            val = app.arp.tone_get(self.pos) + lr_dir
+            val = min(15, max(-12, val))
+            app.arp.tone_set(self.pos, val)
+            self.full_redraw = True
+        if app.input.captouch.petals[9].whole.pressed:
+            app.arp.max_step = self.pos
+            self.full_redraw = True
+        if app.input.captouch.petals[7].whole.released:
+            if not self.microtonal_latch:
+                val = app.arp.trigger_get(self.pos) + 1
+                # we wondered when it'd come bite us! the radspa signal
+                # event encoding can't send two stops in a row, which
+                # isn't very good. anyways, it causes problems here in
+                # the edge case where u have zero retriggers but stops
+                # in the sequencer.
+                # trivial solutions likely drag performance down so we
+                # switch when we have a mature fix. gonna hide our shame
+                # here until then:
+                val = val % 2
+                # val = ((val + 1) % 3) - 1
+                app.arp.trigger_set(self.pos, val)
+                self.full_redraw = True
+            else:
+                self.microtonal_latch = False
+        if app.input.captouch.petals[5].whole.pressed:
+            bpm = 60000 / self.tap_acc
+            self.tap_acc = 0
+            self.press_counter = 0
+            if app.arp.bypass:
+                app.arp.bypass = False
+                self.full_redraw = True
+            if bpm > 30 and bpm < 300:
+                app.arp.signals.bpm = bpm
+                self.full_redraw = True
+        if ins.captouch.petals[5].pressed and self.press_counter is not None:
+            if self.press_counter > 1000:
+                app.arp.bypass = True
+                self.full_redraw = True
+                self.press_counter = None
+            else:
+                self.press_counter += delta_ms
+        self.tap_acc += delta_ms
+
+    def draw(self, ctx, app):
+        dot_size = 12
+        dot_dist = 16
+        ctx.font = "Arimo Bold"
+        if self.full_redraw:
+            self.full_redraw = False
+            ctx.rgb(*app.cols.bg).rectangle(-120, -120, 240, 240).fill()
+            app.draw_title(ctx, self.display_name)
+            if app.arp.bypass:
+                app.draw_modulator_indicator(ctx, "start")
+            else:
+                app.draw_modulator_indicator(
+                    ctx,
+                    "tap bpm: " + str(app.arp.signals.bpm.value),
+                    arrow="(hold: stop)",
+                )
+            for i in range(app.arp.num_steps):
+                tone = app.arp.tone_get(i)
+                trig = app.arp.trigger_get(i)
+                size = dot_size
+                if trig != 1:
+                    size -= 2
+                xpos = (i - 5.5) * dot_dist - size / 2
+                ypos = -tone * 3 - size / 2
+                if self.pos == i:
+                    col = app.cols.alt
+                    round_tone = round(tone * 4)
+                    if round_tone:
+                        ctx.save()
+                        ctx.rgb(*col)
+                        ctx.font_size = 16
+                        ctx.move_to(0, 0)
+                        if tone < 0:
+                            ctx.translate(xpos + size, ypos - 4)
+                            align = ctx.LEFT
+                        else:
+                            ctx.translate(xpos + size, ypos + size + 4)
+                            align = ctx.RIGHT
+                        ctx.rotate(-math.tau / 4)
+                        ctx.text_align = align
+                        if round_tone % 4:
+                            ctx.text(f"{tone:.2f}")
+                        else:
+                            ctx.text(f"{int(tone)}")
+                        ctx.restore()
+                else:
+                    col = app.cols.fg
+                if i > app.arp.max_step:
+                    ctx.rgb(*[x * 0.5 for x in col])
+                else:
+                    ctx.rgb(*col)
+                if trig == 1:
+                    ctx.rectangle(xpos, ypos, size, size).fill()
+                elif trig == 0:
+                    ctx.rectangle(xpos, ypos, size, size).stroke()
+                elif trig == -1:
+                    ctx.move_to(xpos, ypos)
+                    ctx.rel_line_to(size, size)
+                    ctx.rel_move_to(0, -size)
+                    ctx.rel_line_to(-size, size)
+                    ctx.stroke()
+            # arrows
+            ctx.rgb(*app.cols.hi)
+            ctx.move_to(100, 50)
+            ctx.rel_line_to(-4, -6)
+            ctx.rel_line_to(8, 0)
+            ctx.rel_line_to(-4, 6)
+            ctx.stroke()
+
+            ctx.move_to(70, -93)
+            ctx.rel_line_to(-4, 6)
+            ctx.rel_line_to(8, 0)
+            ctx.rel_line_to(-4, -6)
+            ctx.stroke()
+
+            # :|
+            ctx.move_to(-70, -93).rel_line_to(0, 10).stroke()
+            ctx.rectangle(-75, -91, 2, 2).fill()
+            ctx.rectangle(-75, -87, 2, 2).fill()
+
+            # event selector
+            size = 8
+            xpos, ypos = -100, 50 - 3
+            ctx.rectangle(xpos, ypos, size, size).fill()
+            size = 6
+            xpos, ypos = -100 + 7, 50 + 3
+            ctx.rectangle(xpos, ypos, size, size).stroke()
+            xpos, ypos = -100 - 2, 50 + 6
+            # ctx.move_to(xpos, ypos)
+            # ctx.rel_line_to(size, size)
+            # ctx.rel_move_to(0, -size)
+            # ctx.rel_line_to(-size, size)
+            # ctx.stroke()
+
+        i = app.arp.step
+        ctx.rgb(*app.cols.bg)
+        ctx.rectangle(-120, -60, 240, 4).fill()
+        ctx.rgb(*app.cols.hi)
+        size = 4
+        xpos = (i - 5.5) * dot_dist - size / 2
+        ctx.rectangle(xpos, -60, size, size).fill()
+
+
 class ScaleSetupPage(LonerPage):
     def get_help(self):
         return (
@@ -1083,6 +1274,7 @@ class ParameterPage(Page):
     def back_press_event(self, app):
         if self.subwindow:
             self.subwindow = 0
+            self.full_redraw = True
         else:
             self.scroll_to_parent(app)