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)