From 67c97da3d0034c3d167d2b71237344ce5187a447 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Fri, 8 Nov 2024 10:27:14 -0500 Subject: [PATCH] music.md updates on sequencer --- docs/music.md | 18 ++++++++++++++++++ tulip/shared/py/ui.py | 19 ------------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/music.md b/docs/music.md index a61b1350..d20a3068 100644 --- a/docs/music.md +++ b/docs/music.md @@ -113,6 +113,8 @@ You can also easily change the BPM of the sequencer -- this will impact everythi tulip.seq_bpm(120) ``` +(Make sure to read below about the higher-accuracy sequencer API, `amy.send(sequence)`. The Tulip `seq_X` commands are simple and easy to use, but if you're making a music app that requires rock-solid timing, you'll want to use the AMY sequencer directly.) + ## Making new Synths We're using `midi.config.get_synth(channel=1)` to "borrow" the synth booted with Tulip. But if you're going to want to share your ideas with others, you should make your own `Synth` that doesn't conflict with anything already running on Tulip. That's easy, you can just run: @@ -477,6 +479,22 @@ s.note_on(55, 1) Try saving these setup commands (including the `store_patch`, which gets cleared on power / boot) to a python file, like `woodpiano.py`, and `execfile("woodpiano.py")` on reboot to set it up for you! +## Direct AMY sequencer + +Tulip can use the AMY sequencer directly. The `tulip.seq_X` commands are written in Python, and may end up being delayed some small amount of milliseconds if Python is busy doing other things (like drawing a screen.) For this reason, we recommend using the AMY sequencer directly for music, and using the Tulip sequencer for graphical updates. The AMY sequencer runs in a separate "thread" on Tulip and cannot be interrupted. It will maintain rock-solid timing using the audio clock on your device. + +A great longer example of how to do this is in our [`drums` app](https://github.com/shorepine/tulipcc/blob/main/tulip/shared/py/drums.py). You can see that the drum pattern itself is updated in AMY any time a parameter is changed, and that we use `tulip.seq_X` callbacks only to update the "time LED" ticker across the top. + +You can schedule events to happen in a sequence in AMY using `amy.send(sequence=` commands. For the drum machine example, you set the `period` of the sequence and then update events using AMY commands at the right `tick` offset to that length. For example, a drum machine that has 16 steps, each an eighth note, would have a `period` of 24 * 16 = 384. (24 is half of the sequencer's PPQ. If you wanted 16 quarter notes, you would use 48 * 16. Sixteenth notes would be 12 * 16.) And then, each event you expect to play in that pattern is sequenced with an "offset" `tick` into that pattern. The first event in the pattern is at `tick` 0, and the 9th event would be at `tick` 24 * 9 = 216. + +```python +amy.send(reset=amy.RESET_SEQUENCER) # clears the sequence + +amy.send(osc=0, vel=1, wave=amy.PCM, patch=0, sequence="0,384,1") # first slot of a 16 1/8th note drum machine +amy.send(osc=1, vel=1, wave=amy.PCM, patch=1, sequence="216,384,2") # ninth slot of a 16 1/8th note drum machine +``` + +The three parameters in `sequence` are `tick`, `period` and then `tag`. `tag` is used to keep track of which events are scheduled, so you can overwrite their parameters or delete them later. diff --git a/tulip/shared/py/ui.py b/tulip/shared/py/ui.py index 20b22174..6f11ed31 100644 --- a/tulip/shared/py/ui.py +++ b/tulip/shared/py/ui.py @@ -60,7 +60,6 @@ def current_lv_group(): def hide(i): g = tulip.current_uiscreen().group - #g = lv.screen_active().get_child(0) to_hide = g.get_child(i) try: to_hide.add_flag(1) # hide @@ -69,7 +68,6 @@ def hide(i): def unhide(i): - #g = lv.screen_active().get_child(0) g = tulip.current_uiscreen().group to_unhide = g.get_child(i) try: @@ -84,7 +82,6 @@ class UIScreen(): # Start drawing at this position, a little to the right of the edge and 100px down default_offset_x = 10 default_offset_y = 100 - #load_delay = 200 # milliseconds between section loads def __init__(self, name, keep_tfb = False, bg_color=default_bg_color, offset_x=default_offset_x, offset_y=default_offset_y, activate_callback=None, quit_callback=None, deactivate_callback=None, handle_keyboard=False): @@ -166,9 +163,6 @@ def alttab_callback(self, e): if(len(running_apps)>1): self.active = False - #for i in range(self.group.get_child_count()): - # hide(i) - if(self.deactivate_callback is not None): self.deactivate_callback(self) @@ -206,7 +200,6 @@ def add(self, obj, first_align=lv.ALIGN.TOP_LEFT, direction=lv.ALIGN.OUT_RIGHT_M if(type(obj) != list): obj = [obj] for o in obj: - #o.update_callbacks(self.change_callback) o.group.set_parent(self.group) o.group.set_style_bg_color(pal_to_lv(self.bg_color), lv.PART.MAIN) o.group.set_height(lv.SIZE_CONTENT) @@ -220,7 +213,6 @@ def add(self, obj, first_align=lv.ALIGN.TOP_LEFT, direction=lv.ALIGN.OUT_RIGHT_M o.group.align_to(self.group,first_align,self.offset_x,self.offset_y) o.group.set_width(o.group.get_width()+pad_x) o.group.set_height(o.group.get_height()+pad_y) - #o.group.add_flag(1) # Hide by default if(x is not None and y is not None): o.group.set_pos(x,y) self.last_obj_added = o.group @@ -234,14 +226,6 @@ def present(self): lv.screen_load(self.screen) - - # We stagger the loading of LVGL elements in presenting a screen. - # Tulip can draw the screen faster, but the bandwidth it uses on SPIRAM to draw to the screen BG kills audio if done too fast. - #wait_time = UIScreen.load_delay - #for i in range(self.group.get_child_count()): - # tulip.defer(unhide, i, UIScreen.load_delay + i*UIScreen.load_delay) - # wait_time = wait_time + UIScreen.load_delay - if(self.handle_keyboard): get_keypad_indev().set_group(self.kb_group) @@ -259,7 +243,6 @@ def present(self): if(self.activate_callback is not None): self.activate_callback(self) - #tulip.defer(self.activate_callback, self, wait_time) tulip.ui_quit_callback(self.screen_quit_callback) tulip.ui_switch_callback(self.alttab_callback) @@ -279,7 +262,6 @@ class UIElement(): temp_screen = lv.obj() def __init__(self, debug=False): - #self.change_callback = None self.group = lv.obj(UIElement.temp_screen) # Hot tip - set this to 1 if you're debugging why elements are not aligning like you think they should bw = 0 @@ -289,7 +271,6 @@ def __init__(self, debug=False): def update_callbacks(self, cb): pass - #self.change_callback = cb # Remove the elements you created (including the group) def remove_items(self):