diff --git a/README.md b/README.md index 735c33982..fe343bffe 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ You can [buy a Tulip for $59 US](https://tulip.computer) or [build your own Tuli Tulip CC supports: - 8.5MB of RAM - 2MB is available to MicroPython, and 1.5MB is available for OS memory. The rest is used for the graphics framebuffers (which you can use as storage) and the firmware cache. - 32MB flash storage, as a filesystem accesible in Python (24MB left over after OS in ROM) -- An [AMY](https://github.com/shorepine/amy) stereo 120-voice synthesizer engine running locally, or as a wireless controller for an [Alles](https://github.com/shorepine/alles) mesh. Tulip's synth supports additive and subtractive oscillators, an excellent FM synthesis engine, samplers, karplus-strong, high quality analog style filters, and much more. We ship Tulip with a drum machine, voices / patch app, and Juno-6 editor. +- An [AMY](https://github.com/shorepine/amy) stereo 120-voice synthesizer engine running locally, or as a wireless controller for an [Alles](https://github.com/shorepine/alles) mesh. Tulip's synth supports additive and subtractive oscillators, an excellent FM synthesis engine, samplers, karplus-strong, high quality analog style filters, a sequencer, and much more. We ship Tulip with a drum machine, voices / patch app, and Juno-6 editor. - Text frame buffer layer, 128 x 50, with ANSI support for 256 colors, inverse, bold, underline, background color - Up to 32 sprites on screen, drawn per scanline, with collision detection, from a total of 32KB of bitmap memory (1 byte per pixel) - A 1024 (+128 overscan) by 600 (+100 overscan) background frame buffer to draw arbitrary bitmaps to, or use as RAM, and which can scroll horizontally / vertically diff --git a/amy b/amy index 5262c3955..66d31441e 160000 --- a/amy +++ b/amy @@ -1 +1 @@ -Subproject commit 5262c395596729cd19ab4fd03604273ec14dd473 +Subproject commit 66d31441ed50f61a0fc4873c6c27c8de937606e0 diff --git a/docs/music.md b/docs/music.md index 96f09a2a3..d20a3068b 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: @@ -130,7 +132,7 @@ synth1.note_on(50, 1) synth2.note_on(50, 0.5) ``` -You can also "schedule" notes in the near future (up to 20 seconds ahead). This is useful for sequencing fast parameter changes or keeping in time with the sequencer. `Synth`s accept a `time` parameter, and it's in milliseconds. For example: +You can also "schedule" notes. This is useful for sequencing fast parameter changes. `Synth`s accept a `time` parameter, and it's in milliseconds. For example: ```python # play a chord all at once @@ -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/docs/tulip_api.md b/docs/tulip_api.md index dadfd5d05..12dc14bbc 100644 --- a/docs/tulip_api.md +++ b/docs/tulip_api.md @@ -490,19 +490,21 @@ for i,note in enumerate(chord.midinotes()): ## Music sequencer -Tulip is always running a live sequencer, meant for music programs you write to share a common clock. This allows you to have multiple music programs running that respond to a callback to play notes. +Tulip is always running AMY's live sequencer, meant for music programs you write to share a common clock. This allows you to have multiple music programs running that respond to a callback to play notes. -To use the clock in your code, you should first register on the music callback with `slot = tulip.seq_add_callback(my_callback)`. You can remove your callback with `tulip.seq_remove_callback(slot)`. You can remove all callbacks with `tulip.seq_remove_callbacks()`. We support up to 8 callbacks running at once. +**There are two types of sequencer callbacks in Tulip**. One is the AMY sequencer, where you set up an AMY synthesizer event to run at a certain time (or periodically.) This is done using the `amy.send(sequence=)` command. See [AMY's documentation](https://github.com/shorepine/amy/blob/main/README.md#the-sequencer) for more details. -When adding a callback, there's an optional second parameter to denote a divider on the system level parts-per-quarter timer (currently at 48). If you run `slot = tulip.seq_add_callback(my_callback, 6)`, it would call your function `my_callback` every 6th "tick", so 8 times a quarter note at a PPQ of 48. The default divider is 48, so if you don't set a divider, your callback will activate once a quarter note. +Tulip also receives these same sequencer messages, for use in updating the screen or doing other periodic events. Due the way Tulip works, depending on the activity, there can sometimes be a noticeable delay between the sequencer firing and Tulip finishing drawing (some 10s-100 milliseconds.) The audio synthesizer will run far more accurately using the AMY native sequencer. So make sure you use AMY's event sequencing to schedule audio events, and use these Tulip callbacks for less important events like updating the screen. For exanple, a drum machine should use AMY's `sequence` command to schedule the notes to play, but using the `tulip.seq_add_callback` API to update the "beat ticker" display in Tulip. See how we do this in the [`drums`](https://github.com/shorepine/tulipcc/blob/main/tulip/shared/py/drums.py) app. + +To use the lower-precision Python Tulip sequencer callback in your code, you should first register with `slot = tulip.seq_add_callback(my_callback)`. You can remove your callback with `tulip.seq_remove_callback(slot)`. You can remove all callbacks with `tulip.seq_remove_callbacks()`. We support up to 8 callbacks running at once. -By default, your callback will receive a message 50 milliseconds ahead of the time of the intended tick, with the parameters `my_callback(intended_time_ms)`. This is so that you can take extra CPU time to prepare to send messages at the precise time, using AMY scheduling commands, to keep in perfect sync. You can set this "lookahead" globally for all callbacks if you want more or less latency with `tulip.seq_latency(X)` or get it with `tulip.seq_latency()`. +When adding a callback, there's an optional second parameter to denote a divider on the system level parts-per-quarter timer (currently at 48). If you run `slot = tulip.seq_add_callback(my_callback, 6)`, it would call your function `my_callback` every 6th "tick", so 8 times a quarter note at a PPQ of 48. The default divider is 48, so if you don't set a divider, your callback will activate once a quarter note. -You can set the system-wide BPM (beats, or quarters per minute) with `tulip.seq_bpm(120)` or retrieve it with `tulip.seq_bpm()`. +You can set the system-wide BPM (beats, or quarters per minute) with AMY's `amy.send(tempo=120)` or using wrapper `tulip.seq_bpm(bpm)`. You can retrieve the BPM with `tulip.seq_bpm()`. You can see what tick you are on with `tulip.seq_ticks()`. -See the example `seq.py` on Tulip World for an example of using the music clock, or the [`drums`](https://github.com/shorepine/tulipcc/blob/main/tulip/shared/py/drums.py) included app. +See the example `world.download('seq.py','bwhitman')` on Tulip World for an example of using the music clock, or the [`drums`](https://github.com/shorepine/tulipcc/blob/main/tulip/shared/py/drums.py) included app. **See the [music tutorial](music.md) for a LOT more information on music in Tulip.** diff --git a/tulip/esp32s3/boards/TDECK/sdkconfig.board b/tulip/esp32s3/boards/TDECK/sdkconfig.board index 5e9f8e25b..d2a3dc806 100644 --- a/tulip/esp32s3/boards/TDECK/sdkconfig.board +++ b/tulip/esp32s3/boards/TDECK/sdkconfig.board @@ -1 +1,2 @@ CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=4608 diff --git a/tulip/esp32s3/boards/sdkconfig.tulip b/tulip/esp32s3/boards/sdkconfig.tulip index 0c3871e09..b6170b264 100644 --- a/tulip/esp32s3/boards/sdkconfig.tulip +++ b/tulip/esp32s3/boards/sdkconfig.tulip @@ -28,4 +28,6 @@ CONFIG_SPIRAM_RODATA=y CONFIG_LCD_RGB_ISR_IRAM_SAFE=n CONFIG_LCD_RGB_RESTART_IN_VSYNC=y -CONFIG_LWIP_PPP_SUPPORT=n \ No newline at end of file +CONFIG_LWIP_PPP_SUPPORT=n + +CONFIG_ESP_TIMER_TASK_STACK_SIZE=8192 diff --git a/tulip/esp32s3/esp32_common.cmake b/tulip/esp32s3/esp32_common.cmake index 3bdce0b3c..868bba462 100644 --- a/tulip/esp32s3/esp32_common.cmake +++ b/tulip/esp32s3/esp32_common.cmake @@ -178,7 +178,7 @@ list(APPEND MICROPY_SOURCE_EXTMOD ${TULIP_SHARED_DIR}/ui.c ${TULIP_SHARED_DIR}/midi.c ${TULIP_SHARED_DIR}/sounds.c - ${TULIP_SHARED_DIR}/sequencer.c + ${TULIP_SHARED_DIR}/tsequencer.c ${TULIP_SHARED_DIR}/lodepng.c ${TULIP_SHARED_DIR}/lvgl_u8g2.c ${TULIP_SHARED_DIR}/u8fontdata.c @@ -194,6 +194,7 @@ list(APPEND MICROPY_SOURCE_EXTMOD ${AMY_DIR}/src/filters.c ${AMY_DIR}/src/oscillators.c ${AMY_DIR}/src/transfer.c + ${AMY_DIR}/src/sequencer.c ${AMY_DIR}/src/partials.c ${AMY_DIR}/src/pcm.c ${AMY_DIR}/src/log2_exp2.c diff --git a/tulip/esp32s3/main.c b/tulip/esp32s3/main.c index db09338ba..4bc624f7a 100644 --- a/tulip/esp32s3/main.c +++ b/tulip/esp32s3/main.c @@ -53,7 +53,6 @@ #include "usb.h" -#include "sequencer.h" #include "usb_serial_jtag.h" #include "modmachine.h" #include "modnetwork.h" @@ -63,6 +62,8 @@ #include "tdeck_keyboard.h" #endif +#include "tsequencer.h" + #if MICROPY_BLUETOOTH_NIMBLE #include "extmod/modbluetooth.h" @@ -440,10 +441,6 @@ void app_main(void) { fflush(stderr); delay_ms(100); - fprintf(stderr,"Starting Sequencer (timer)\n"); - sequencer_init(); - run_sequencer(); - #ifdef TDECK delay_ms(3000); // wait for touchscreen fprintf(stderr,"Starting T-Deck keyboard on core %d\n", USB_TASK_COREID); @@ -452,6 +449,7 @@ void app_main(void) { delay_ms(10); #endif + tsequencer_init(); } diff --git a/tulip/esp32s3/tdeck_keyboard.c b/tulip/esp32s3/tdeck_keyboard.c index 4a1f4d002..d7abfaf38 100644 --- a/tulip/esp32s3/tdeck_keyboard.c +++ b/tulip/esp32s3/tdeck_keyboard.c @@ -205,12 +205,29 @@ void run_tdeck_keyboard() { } else { char_to_send[0] = rx_data[0]; } + + uint8_t c = char_to_send[0]; + if(keycode_to_ctrl_key(c) != '\0') { + const size_t len = strlen(lvgl_kb_buf); + if (len < KEYBOARD_BUFFER_SIZE - 1) { + lvgl_kb_buf[len] = keycode_to_ctrl_key(c); + lvgl_kb_buf[len + 1] = '\0'; + } + } else { + // put it in lvgl_kb_buf for lvgl + const size_t len = strlen(lvgl_kb_buf); + if (len < KEYBOARD_BUFFER_SIZE - 1) { + lvgl_kb_buf[len] = c; + lvgl_kb_buf[len+1] = '\0'; + } + } + // Send as is, combining with ctrl if toggled if (ctrl_toggle) { - send_key_to_micropython(get_alternative_char(ctrlMappings, ctrlMappingsSize, char_to_send[0])); + send_key_to_micropython(get_alternative_char(ctrlMappings, ctrlMappingsSize, c)); ctrl_toggle = false; // Reset toggle after sending } else { - send_key_to_micropython(char_to_send[0]); + send_key_to_micropython(c); } } } diff --git a/tulip/linux/main.c b/tulip/linux/main.c index 0b2f6c5d6..b684c3267 100644 --- a/tulip/linux/main.c +++ b/tulip/linux/main.c @@ -57,7 +57,6 @@ #include "display.h" #include "alles.h" #include "midi.h" -#include "sequencer.h" #include "shared/runtime/pyexec.h" @@ -863,6 +862,7 @@ MP_NOINLINE void * main_(void *vargs) { //int argc, char **argv) { extern int8_t unix_display_flag; #include "lvgl.h" +#include "tsequencer.h" int main(int argc, char **argv) { // Get the resources folder loc @@ -912,10 +912,7 @@ int main(int argc, char **argv) { pthread_t mp_thread_id; pthread_create(&mp_thread_id, NULL, main_, NULL); - sequencer_init(); - pthread_t sequencer_thread_id; - pthread_create(&sequencer_thread_id, NULL, run_sequencer, NULL); - + tsequencer_init(); delay_ms(100); // Schedule a "turning on" sound diff --git a/tulip/macos/main.c b/tulip/macos/main.c index 40e00ad74..6fd8e300a 100644 --- a/tulip/macos/main.c +++ b/tulip/macos/main.c @@ -57,9 +57,9 @@ #include "display.h" #include "alles.h" #include "midi.h" -#include "sequencer.h" #include "shared/runtime/pyexec.h" +#include "tsequencer.h" // Command line options, with their defaults @@ -945,9 +945,7 @@ int main(int argc, char **argv) { pthread_t mp_thread_id; pthread_create(&mp_thread_id, NULL, main_, NULL); - sequencer_init(); - pthread_t sequencer_thread_id; - pthread_create(&sequencer_thread_id, NULL, run_sequencer, NULL); + tsequencer_init(); delay_ms(100); // Schedule a "turning on" sound diff --git a/tulip/shared/modtulip.c b/tulip/shared/modtulip.c index 8c974e554..ba60a9538 100644 --- a/tulip/shared/modtulip.c +++ b/tulip/shared/modtulip.c @@ -13,7 +13,7 @@ #include "alles.h" #include "midi.h" #include "ui.h" -#include "sequencer.h" +#include "tsequencer.h" #include "keyscan.h" #include "genhdr/mpversion.h" @@ -483,7 +483,7 @@ STATIC mp_obj_t tulip_seq_add_callback(size_t n_args, const mp_obj_t *args) { if(n_args == 2) { sequencer_dividers[index] = mp_obj_get_int(args[1]); } else { - sequencer_dividers[index] = sequencer_ppq; + sequencer_dividers[index] = AMY_SEQUENCER_PPQ; } } else { index = -1; @@ -513,42 +513,6 @@ STATIC mp_obj_t tulip_seq_remove_callbacks(size_t n_args, const mp_obj_t *args) STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_seq_remove_callbacks_obj, 0, 0, tulip_seq_remove_callbacks); -STATIC mp_obj_t tulip_seq_bpm(size_t n_args, const mp_obj_t *args) { - if(n_args == 1) { - sequencer_bpm = mp_obj_get_float(args[0]); - sequencer_recompute(); - } else { - return mp_obj_new_float(sequencer_bpm); - } - return mp_const_none; -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_seq_bpm_obj, 0, 1, tulip_seq_bpm); - - -STATIC mp_obj_t tulip_seq_ppq(size_t n_args, const mp_obj_t *args) { - if(n_args == 1) { - sequencer_ppq = mp_obj_get_int(args[0]); - sequencer_recompute(); - } else { - return mp_obj_new_int(sequencer_ppq); - } - return mp_const_none; -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_seq_ppq_obj, 0, 1, tulip_seq_ppq); - -STATIC mp_obj_t tulip_seq_latency(size_t n_args, const mp_obj_t *args) { - if(n_args == 1) { - sequencer_latency_ms = mp_obj_get_int(args[0]); - } else { - return mp_obj_new_int(sequencer_latency_ms); - } - return mp_const_none; -} - -STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(tulip_seq_latency_obj, 0, 1, tulip_seq_latency); - STATIC mp_obj_t tulip_seq_ticks(size_t n_args, const mp_obj_t *args) { return mp_obj_new_int(sequencer_tick_count); @@ -1320,9 +1284,6 @@ STATIC const mp_rom_map_elem_t tulip_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_seq_remove_callback), MP_ROM_PTR(&tulip_seq_remove_callback_obj) }, { MP_ROM_QSTR(MP_QSTR_seq_remove_callbacks), MP_ROM_PTR(&tulip_seq_remove_callbacks_obj) }, { MP_ROM_QSTR(MP_QSTR_midi_callback), MP_ROM_PTR(&tulip_midi_callback_obj) }, - { MP_ROM_QSTR(MP_QSTR_seq_bpm), MP_ROM_PTR(&tulip_seq_bpm_obj) }, - { MP_ROM_QSTR(MP_QSTR_seq_ppq), MP_ROM_PTR(&tulip_seq_ppq_obj) }, - { MP_ROM_QSTR(MP_QSTR_seq_latency), MP_ROM_PTR(&tulip_seq_latency_obj) }, { MP_ROM_QSTR(MP_QSTR_seq_ticks), MP_ROM_PTR(&tulip_seq_ticks_obj) }, { MP_ROM_QSTR(MP_QSTR_midi_in), MP_ROM_PTR(&tulip_midi_in_obj) }, { MP_ROM_QSTR(MP_QSTR_midi_out), MP_ROM_PTR(&tulip_midi_out_obj) }, diff --git a/tulip/shared/py/arpegg.py b/tulip/shared/py/arpegg.py index 52102f714..58121076e 100644 --- a/tulip/shared/py/arpegg.py +++ b/tulip/shared/py/arpegg.py @@ -3,9 +3,7 @@ import time import random import tulip - - -#tulip.seq_add_callback(midi_step, int(tulip.seq_ppq()/2)) +import amy class ArpeggiatorSynth: @@ -69,7 +67,7 @@ def run(self): self.current_step = -1 # Semaphore to the run loop to start going. self.running = True - self.slot = tulip.seq_add_callback(self.step_callback, int(tulip.seq_ppq()/2)) + self.slot = tulip.seq_add_callback(self.step_callback, int(amy.SEQUENCER_PPQ/2)) def stop(self): self.running = False diff --git a/tulip/shared/py/drums.py b/tulip/shared/py/drums.py index a639c74fe..172d24df7 100644 --- a/tulip/shared/py/drums.py +++ b/tulip/shared/py/drums.py @@ -1,11 +1,11 @@ # drums.py # lvgl drum machine for Tulip -from tulip import UIScreen, UIElement, pal_to_lv, lv_depad, lv, frame_callback, ticks_ms, seq_add_callback, seq_remove_callback, seq_ppq, seq_ticks +from tulip import UIScreen, UIElement, pal_to_lv, lv_depad, lv, frame_callback, ticks_ms, seq_add_callback, seq_remove_callback, seq_ticks import amy import midi from patches import drumkit - +AMY_TAG_OFFSET = 4385 # random offset to allow other apps to share # A single drum machine switch with LED class DrumSwitch(UIElement): @@ -16,9 +16,11 @@ class DrumSwitch(UIElement): button_width = 30 button_height = 40 - def __init__(self, switch_color_idx=0): + def __init__(self, row, col, switch_color_idx=0): super().__init__() - + self.row = row + self.col = col + self.tag = (self.row * 16) + self.col + AMY_TAG_OFFSET self.group.set_size(DrumSwitch.button_width+10,DrumSwitch.button_height+25) lv_depad(self.group) @@ -44,25 +46,33 @@ def __init__(self, switch_color_idx=0): self.led.off() self.on = False + def update_amy(self): + row = app.rows[self.row] + if(self.on): + length = int((amy.SEQUENCER_PPQ/2) * 16) + offset = int(amy.SEQUENCER_PPQ/2) * self.col + app.synth.note_on(note=row.midi_note, sequence = "%d,%d,%d" % (offset, length, self.tag)) + else: + # Turn off note, the rest of stuff doesn't matter + amy.send(sequence = ",,%d" % (self.tag)) def set(self, val): if(val): self.on = True self.led.on() else: - self.off = False + self.on = False self.led.off() + self.update_amy() def get(self): return self.on def cb(self, e): if(self.on): - self.on = False - self.led.off() + self.set(False) else: - self.on = True - self.led.on() + self.set(True) # A row of LEDs to keep time with and some labels class LEDStrip(UIElement): @@ -101,7 +111,7 @@ def set(self, idx, val): # and some knobs on the right for pitch / vol / pan class DrumRow(UIElement): knob_color = 209 - def __init__(self, items, midi_note=0): + def __init__(self, items, row, midi_note=0): super().__init__() self.midi_note = midi_note self.preset = 0 @@ -121,7 +131,7 @@ def __init__(self, items, midi_note=0): lv_depad(self.dropdown) for i in range(4): for j in range(4): - d = DrumSwitch(switch_color_idx=i) + d = DrumSwitch(switch_color_idx=i, row=row, col = j + i*4) self.objs.append(d) d.group.set_parent(self.group) d.group.set_style_bg_color(pal_to_lv(UIScreen.default_bg_color),lv.PART.MAIN) @@ -161,6 +171,8 @@ def get(self, idx): def set_vel(self, val): self.knobs[0].set_value(int(val*100.0)) self.vel = val + self.update_note() + def get_vel(self): return self.knobs[0].get_value()/100.0 @@ -194,6 +206,7 @@ def update_note(self): base_note = drumkit[self.preset][0] note_for_pitch = int(base_note + (self.pitch - 0.5)*24.0) params_dict={ + 'amp': self.vel*2.0, 'wave': amy.PCM, 'patch': self.preset, 'freq': 0, @@ -202,8 +215,14 @@ def update_note(self): } app.synth.setup_midi_note(midi_note=self.midi_note, params_dict=params_dict) + # For each on switch in the row, we have to update the sequence with the new params + for switch in self.objs: + switch.update_amy() + + def vel_cb(self, e): self.vel = e.get_target_obj().get_value() / 100.0 + self.update_note() def pitch_cb(self, e): self.pitch = e.get_target_obj().get_value() / 100.0 @@ -218,35 +237,31 @@ def dropdown_cb(self, e): self.update_note() -# Called from tulip's sequencer +# Called from AMY's sequencer, just updates the LEDs def beat_callback(t): global app - # Hm, how should we deal with visual latency + app.current_beat = int((seq_ticks() / 24) % 16) + app.leds.set((app.current_beat-1)% 16, 0) app.leds.set(app.current_beat, 1) - for i, row in enumerate(app.rows): - if(row.get(app.current_beat)): - base_note = drumkit[row.get_preset()][0] - note_for_pitch = None - if row.get_pitch() != 0.5: - note_for_pitch = int(base_note + (row.get_pitch() - 0.5)*24.0) - app.synth.note_on(note=row.midi_note, velocity=row.get_vel()*2, time=t) - app.current_beat = (app.current_beat+1) % 16 def quit(screen): seq_remove_callback(screen.slot) + # Clear all the sequenced tags -- even if we never set them, it's a no-op if so + for i in range(16*7): + amy.send(sequence=",,%d" % (i+AMY_TAG_OFFSET)) + def run(screen): global app app = screen # we can use the screen obj passed in as a general "store stuff here" class, as well as inspect the UI app.synth = midi.config.synth_per_channel[10] - app.current_beat = 0 app.offset_y = 10 app.set_bg_color(0) app.quit_callback = quit app.leds = LEDStrip() app.add(app.leds, direction=lv.ALIGN.OUT_BOTTOM_LEFT, pad_y=0) - app.rows = [DrumRow([x[1] for x in drumkit], midi_note=i) for i in range(7)] + app.rows = [DrumRow([x[1] for x in drumkit], i, midi_note=i) for i in range(7)] app.add(app.rows, direction=lv.ALIGN.OUT_BOTTOM_LEFT) for i,row in enumerate(app.rows): @@ -257,7 +272,8 @@ def run(screen): for i in range(16): app.rows[2].objs[i].set(True) - app.slot = seq_add_callback(beat_callback, int(seq_ppq()/2)) + app.current_beat = int((seq_ticks() / 24) % 16) + app.slot = seq_add_callback(beat_callback, int(amy.SEQUENCER_PPQ/2)) app.present() diff --git a/tulip/shared/py/midi.py b/tulip/shared/py/midi.py index be694e1bf..d8865c241 100644 --- a/tulip/shared/py/midi.py +++ b/tulip/shared/py/midi.py @@ -174,11 +174,11 @@ class VoiceObject: def __init__(self, amy_voice): self.amy_voice = amy_voice - def note_on(self, note, vel, time=None): - amy.send(time=time, voices=self.amy_voice, note=note, vel=vel) + def note_on(self, note, vel, time=None, sequence=None): + amy.send(time=time, voices=self.amy_voice, note=note, vel=vel, sequence=sequence) - def note_off(self, time=None): - amy.send(time=time, voices=self.amy_voice, vel=0) + def note_off(self, time=None, sequence=None): + amy.send(time=time, voices=self.amy_voice, vel=0, sequence=sequence) @@ -186,8 +186,8 @@ class Synth: """Manage a polyphonic synthesizer by rotating among a fixed pool of voices. Provides methods: - synth.note_on(midi_note, velocity, time=None) - synth.note_off(midi_note, time=None) + synth.note_on(midi_note, velocity, time=None, sequence=None) + synth.note_off(midi_note, time=None, sequence=None) synth.all_notes_off() synth.program_change(patch_num) changes preset for all voices. synth.control_change(control, value) modifies a parameter for all voices. @@ -276,21 +276,21 @@ def _get_next_voice(self): self._voice_off(stolen_voice) return stolen_voice - def _voice_off(self, voice, time=None): + def _voice_off(self, voice, time=None, sequence=None): """Terminate voice, update note_of_voice, but don't alter the queues.""" - self.voice_objs[voice].note_off(time) + self.voice_objs[voice].note_off(time=time, sequence=sequence) # We no longer have a voice playing this note. del self.voice_of_note[self.note_of_voice[voice]] self.note_of_voice[voice] = None - def note_off(self, note, time=None): + def note_off(self, note, time=None, sequence=None): if self.sustaining: self.sustained_notes.add(note) return if note not in self.voice_of_note: return old_voice = self.voice_of_note[note] - self._voice_off(old_voice, time) + self._voice_off(old_voice, time=time, sequence=sequence) # Return to released. self.active_voices.remove(old_voice) self.released_voices.put(old_voice) @@ -302,12 +302,12 @@ def all_notes_off(self): self._voice_off(voice) - def note_on(self, note, velocity=1, time=None): + def note_on(self, note, velocity=1, time=None, sequence=None): if not self.amy_voice_nums: # Note on after synth.release()? raise ValueError('Synth note on with no voices - synth has been released?') if velocity == 0: - self.note_off(note, time) + self.note_off(note, time=time, sequence=sequence) else: # Velocity > 0, note on. if note in self.voice_of_note: @@ -318,7 +318,7 @@ def note_on(self, note, velocity=1, time=None): self.active_voices.put(new_voice) self.voice_of_note[note] = new_voice self.note_of_voice[new_voice] = note - self.voice_objs[new_voice].note_on(note, velocity, time) + self.voice_objs[new_voice].note_on(note, velocity, time=time, sequence=sequence) def sustain(self, state): """Turn sustain on/off.""" @@ -376,20 +376,20 @@ def __init__(self, num_voices=6, first_osc=None): self.patch_number = None # This method must be overridden by the derived class to actually send the note. - def _note_on_with_osc(self, osc, note, velocity, time): + def _note_on_with_osc(self, osc, note, velocity, time=None, sequence=None): raise NotImplementedError - def note_on(self, note, velocity=1, time=None): + def note_on(self, note, velocity=1, time=None, sequence=None): osc = self.oscs[self.next_osc] self.next_osc = (self.next_osc + 1) % len(self.oscs) # Update mapping of note to osc. If notes are repeated, this will lose track. self.osc_of_note[note] = osc # Actually issue the note-on via derived class function - self._note_on_with_osc(osc, note, velocity, time) + self._note_on_with_osc(osc, note, velocity, time=time, sequence=sequence) - def note_off(self, note, time=None): + def note_off(self, note, time=None, sequence=None): if note in self.osc_of_note: - amy.send(time=time, osc=self.osc_of_note[note], vel=0) + amy.send(time=time, sequence=sequence, osc=self.osc_of_note[note], vel=0) del self.osc_of_note[note] def all_notes_off(self): @@ -434,8 +434,8 @@ def update_oscs(self, **kwargs): for osc in self.oscs: amy.send(osc=osc, **kwargs) - def _note_on_with_osc(self, osc, note, velocity, time=None): - amy.send(time=time, osc=osc, note=note, vel=velocity) + def _note_on_with_osc(self, osc, note, velocity, time=None, sequence=None): + amy.send(time=time, sequence=sequence, osc=osc, note=note, vel=velocity) class DrumSynth(SingleOscSynthBase): @@ -458,7 +458,7 @@ def setup_midi_note(self, midi_note, params_dict): """Configure a midi note with a dict of osc params.""" self.midi_note_params[midi_note] = params_dict - def _note_on_with_osc(self, osc, note, velocity, time): + def _note_on_with_osc(self, osc, note, velocity=None, time=None, sequence=None): if note not in self.midi_note_params: if config.show_warnings and note not in DrumSynth.missing_note_warned: print("DrumSynth note_on for note %d but only %s set up." % ( @@ -466,7 +466,11 @@ def _note_on_with_osc(self, osc, note, velocity, time): )) DrumSynth.missing_note_warned.append(note) return - send_args = {'time': time, 'osc': osc, 'vel': velocity} + + # If velocity not set, attempt to use amp from the stored settings. + if(velocity is None): velocity = self.midi_note_params[note].get('amp',1) + + send_args = {'time': time, 'sequence': sequence, 'osc': osc, 'vel': velocity} # Add the args for this note send_args |= self.midi_note_params[note] amy.send(**send_args) diff --git a/tulip/shared/py/tulip.py b/tulip/shared/py/tulip.py index 632eb8065..1265da403 100644 --- a/tulip/shared/py/tulip.py +++ b/tulip/shared/py/tulip.py @@ -250,6 +250,20 @@ def add_to_bootpy(s): w.write(bootpy) w.close() +# Wrapper around AMY tempo to store it +amy_bpm = 108 +def seq_bpm(bpm=None): + global amy_bpm + if bpm is None: + return amy_bpm + else: + amy.send(tempo=bpm) + amy_bpm = bpm + +def seq_ppq(ppq=None): + if(ppq is not None): + print("You can no longer set PPQ in Tulip. It's fixed at %d" % (amy.SEQUENCER_PPQ)) + return amy.SEQUENCER_PPQ def remap(): print("Type key or key combo you wish to remap: ",end='') diff --git a/tulip/shared/py/ui.py b/tulip/shared/py/ui.py index a06bd2781..6f11ed314 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) @@ -258,7 +242,7 @@ def present(self): tulip.tfb_stop() if(self.activate_callback is not None): - tulip.defer(self.activate_callback, self, wait_time) + self.activate_callback(self) tulip.ui_quit_callback(self.screen_quit_callback) tulip.ui_switch_callback(self.alttab_callback) @@ -278,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 @@ -288,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): diff --git a/tulip/shared/sequencer.c b/tulip/shared/sequencer.c deleted file mode 100644 index 96be427e3..000000000 --- a/tulip/shared/sequencer.c +++ /dev/null @@ -1,154 +0,0 @@ -// timer.c -// handle timer-like events for music -// runs in a thread / task - - -//get_ticks_ms() gets amy ms -// mp_hal_ticks_us() gets us from MP - -/* -To use the clock in your code, you should first register on the music callback with tulip.seq_add_callback(my_callback). -We support up to 4 music callbacks running at once. -You can remove your callback with tulip.seq_remove_callback(my_callback) - -When adding a callback, the optional 2nd parameter is a divider on the system level parts-per-quarter timer. -If you set it to 6, we would call your function my_callback every 6th "tick", so 8 times a quarter note. Set this to 48 (or, tulip.seq_ppq()) to receive a message once a beat. -The default divider is set to ppq. So if you don't set a divider in music_callback, your callback will be called once a beat. - -By default, your callback will receive a message 50 milliseconds ahead of the time of the intended tick, with -the parameters my_callback(intended_time_ms). This is so that you can take extra CPU time to prepare to send messages at the precise time, -using AMY scheduling commands, to keep in perfect sync. - -You can set this "lookahead" globally if you want more or less latency with tulip.seq_latency(X) or get it with tulip.seq_latency(). - -You can set the system-wide BPM (quarters per minute) with tulip.seq_bpm(120) or retrieve it with tulip.seq_bpm(). - -You can change the PPQ with tulip.seq_ppq(new_value) or retrieve it with tulip.seq_ppq(). - -You can pause the sequencer with tulip.seq_pause() and start it with tulip.seq_start(). - - -So.. - -music_callback -- MP / tulip.py -adds your function to a list of FPs for the music callback, saves off the dividers - -in C, we have a timer/thread that calls .. mp_sched_schedule(tick_guy(time)) and tick_guy calls all the callbacks - - - -*/ - -#include "sequencer.h" - -// Things that MP can change -float sequencer_bpm = 108; // verified optimal BPM -uint8_t sequencer_ppq = 48; -uint16_t sequencer_latency_ms = 100; -mp_obj_t sequencer_callbacks[SEQUENCER_SLOTS]; -uint8_t sequencer_dividers[SEQUENCER_SLOTS]; - -mp_obj_t defer_callbacks[DEFER_SLOTS]; -mp_obj_t defer_args[DEFER_SLOTS]; -uint32_t defer_sysclock[DEFER_SLOTS]; - -uint8_t sequencer_running = 1; - -// Our internal accounting -uint32_t sequencer_tick_count = 0; -uint64_t next_amy_tick_us = 0; -uint32_t us_per_tick = 0; - -#ifdef ESP_PLATFORM -#include "esp_timer.h" -esp_timer_handle_t periodic_timer; -#endif - -void sequencer_recompute() { - us_per_tick = (uint32_t) (1000000.0 / ((sequencer_bpm/60.0) * (float)sequencer_ppq)); - next_amy_tick_us = amy_sysclock()*1000 + us_per_tick; -} - -void sequencer_start() { - sequencer_recompute(); - sequencer_running = 1; - #ifdef ESP_PLATFORM - // Restart the timer - run_sequencer(); - #endif -} - - -void sequencer_stop() { - sequencer_running = 0; - #ifdef ESP_PLATFORM - // Kill the timer - ESP_ERROR_CHECK(esp_timer_stop(periodic_timer)); - ESP_ERROR_CHECK(esp_timer_delete(periodic_timer)); - #endif -} - - -void sequencer_init() { - for(uint8_t i=0;i=0) { - //fprintf(stderr, "check and fill %" PRIu32"\n", amy_sysclock()); - while(amy_sysclock() >= (next_amy_tick_us/1000)) { - sequencer_tick_count++; - // Check defers - for(uint8_t i=0;i defer_sysclock[i]) { - //fprintf(stderr, "calling defer with sysclock %" PRIu32 " and actual %" PRIu32"\n", defer_sysclock[i], amy_sysclock() ); - mp_sched_schedule(defer_callbacks[i], defer_args[i]); - defer_callbacks[i] = NULL; defer_sysclock[i] = 0; defer_args[i] = NULL; - } - } - - for(uint8_t i=0;i -#include "polyfills.h" - -// Things that MP can change -extern float sequencer_bpm ; -extern uint8_t sequencer_ppq ; -extern uint16_t sequencer_latency_ms ; -extern uint8_t sequencer_running; - -extern mp_obj_t sequencer_callbacks[SEQUENCER_SLOTS]; -extern uint8_t sequencer_dividers[SEQUENCER_SLOTS]; - -extern mp_obj_t defer_callbacks[DEFER_SLOTS]; -extern uint32_t defer_sysclock[DEFER_SLOTS]; -extern mp_obj_t defer_args[DEFER_SLOTS]; - - -// Our internal accounting -extern uint32_t sequencer_tick_count ; -extern uint64_t next_amy_tick_us ; -extern uint32_t us_per_tick ; - - -#ifndef ESP_PLATFORM -void * run_sequencer(void *vargs); -#else -void run_sequencer(); -#endif - -void sequencer_init(); -void sequencer_start(); -void sequencer_stop(); -void sequencer_recompute(); - -#endif diff --git a/tulip/shared/tsequencer.c b/tulip/shared/tsequencer.c new file mode 100644 index 000000000..7acc35b53 --- /dev/null +++ b/tulip/shared/tsequencer.c @@ -0,0 +1,35 @@ + +#include "tsequencer.h" +mp_obj_t sequencer_callbacks[SEQUENCER_SLOTS]; +uint8_t sequencer_dividers[SEQUENCER_SLOTS]; + +mp_obj_t defer_callbacks[DEFER_SLOTS]; +mp_obj_t defer_args[DEFER_SLOTS]; +uint32_t defer_sysclock[DEFER_SLOTS]; + +void tulip_amy_sequencer_hook(uint32_t tick_count) { + for(uint8_t i=0;i defer_sysclock[i]) { + //fprintf(stderr, "calling defer with sysclock %" PRIu32 " and actual %" PRIu32"\n", defer_sysclock[i], amy_sysclock() ); + mp_sched_schedule(defer_callbacks[i], defer_args[i]); + defer_callbacks[i] = NULL; defer_sysclock[i] = 0; defer_args[i] = NULL; + } + } + + for(uint8_t i=0;i +#include "polyfills.h" +#include "sequencer.h" +extern mp_obj_t sequencer_callbacks[SEQUENCER_SLOTS]; +extern uint8_t sequencer_dividers[SEQUENCER_SLOTS]; + +extern mp_obj_t defer_callbacks[DEFER_SLOTS]; +extern uint32_t defer_sysclock[DEFER_SLOTS]; +extern mp_obj_t defer_args[DEFER_SLOTS]; + + +void tsequencer_init(); + +#endif \ No newline at end of file diff --git a/tulip/shared/tulip.mk b/tulip/shared/tulip.mk index 3c63e167e..5727538a8 100644 --- a/tulip/shared/tulip.mk +++ b/tulip/shared/tulip.mk @@ -12,6 +12,7 @@ EXTMOD_SRC_C += $(addprefix ../amy/src/, \ filters.c \ oscillators.c \ transfer.c \ + sequencer.c \ partials.c \ pcm.c \ log2_exp2.c \ @@ -34,7 +35,7 @@ EXTMOD_SRC_C += $(addprefix $(TULIP_EXTMOD_DIR)/, \ alles.c \ sounds.c \ lodepng.c \ - sequencer.c \ + tsequencer.c \ lvgl_u8g2.c \ )