/* Core 0: panel, pages, LEARN, screen, storage. Port of the UI half of sim/app.js; the sequencer is on core 1. Hardware differences from the simulator: ENERGY/DRIFT are real pots, RUN is a toggle (its position is the state), and the bench settings (synth channel/octave, clock) keep the simulator's defaults. */ #include #include #include "pico/stdlib.h" #include "pico/multicore.h" #include "pico/rand.h" #include "app.h" #include "storage.h" #include "midi_uart.h" #include "pcd8544.h" #include "inputs.h" #include "encoder.h" #include "pots.h" live_t g_live; critical_section_t g_live_lock; queue_t g_cmdq; volatile int g_running; volatile int g_cur_bar = -1; volatile uint32_t g_hit_until_ms[SB_NT]; #define HOLD_MS 250 #define LONG_PUSH_MS 600 #define SAVE_AFTER_MS 1500 static const char *const TRACK_NAME[SB_NT] = { "KICK","KICK 2","SNARE","GHOST","HAT","HAT 16","PERC","BASS","MEL A","MEL B","PAD" }; static const char *const NOTE_NAME[12] = { "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" }; static const char *const GEN_NAME[SB_NGEN] = { "EUCLID","SKELETON","SUBDIV","AUTOMATA","AMENIZER" }; static const int8_t PANEL[12] = { 0, 1, 2, -1, 4, 5, 6, 3, 7, 8, 9, 10 }; /* screen map = button grid; -1 = RUN */ enum { PG_SEED, PG_GEN, PG_KEYS, PG_ROOT, PG_SCALE, PG_BPM, PG_SLOT, PG_ROLL, PG_LEARN, NPAGES }; static slot_t slots[NSLOTS]; static sb_state history[16]; static int nhist; static int page, editing, hist_sel, slot_sel, roll_sel, learn_mode, learn_tr = -1; static struct { int active, longp; uint32_t t0; } held[SB_NT]; static char msg_text[24]; static uint32_t msg_until, now_ms; static int dirty; static uint32_t dirty_ms; static void lock(void){ critical_section_enter_blocking(&g_live_lock); } static void unlock(void){ critical_section_exit(&g_live_lock); } static void send_cmd(int c, int arg){ cmd_t m = { (uint8_t)c, (int16_t)arg }; queue_add_blocking(&g_cmdq, &m); } static void msg(const char *t, uint32_t ms){ snprintf(msg_text, sizeof msg_text, "%s", t); msg_until = now_ms + ms; } static void mark_dirty(void){ dirty = 1; dirty_ms = now_ms; } static int clampi(int x, int a, int b){ return x < a ? a : x > b ? b : x; } static int sounds(int tr){ return g_live.on[tr] ^ g_live.inv[tr]; } static void set_state(sb_state s){ memmove(&history[1], &history[0], sizeof history[0] * 15); history[0] = g_live.state; if(nhist < 16) nhist++; s.salts[0] &= 0xFFFF; s.salts[1] &= 0xFFFF; lock(); g_live.state = s; unlock(); hist_sel = 0; mark_dirty(); } static void new_seed(void){ sb_state s = { get_rand_32(), {0, 0} }; set_state(s); } static void reroll(int l){ sb_state s = g_live.state; s.salts[l] = (s.salts[l] + 1) & 0xFFFF; set_state(s); msg(l ? "NEW KEYS" : "NEW KIT", 700); } static void apply_on(int tr, int v){ lock(); g_live.on[tr] = (uint8_t)v; unlock(); mark_dirty(); if(!sounds(tr)) send_cmd(CMD_RELEASE_TRACK, tr); } static void set_inv(int tr, int v){ lock(); g_live.inv[tr] = (uint8_t)v; unlock(); if(!sounds(tr)) send_cmd(CMD_RELEASE_TRACK, tr); } static void set_bpm(int b){ b = clampi(b, 60, 240); if(b == g_live.bpm) return; lock(); g_live.bpm = b; unlock(); send_cmd(CMD_BPM, b); mark_dirty(); } static void enc_turn(int d){ if(!editing){ page = (page + d + NPAGES) % NPAGES; learn_mode = 0; learn_tr = -1; return; } switch(page){ case PG_SEED: hist_sel = clampi(hist_sel + d, 0, nhist); break; case PG_GEN: lock(); g_live.gen = (g_live.gen + d + SB_NGEN) % SB_NGEN; unlock(); mark_dirty(); break; case PG_KEYS: lock(); g_live.keys = (g_live.keys + d + SB_NKEYS) % SB_NKEYS; unlock(); mark_dirty(); break; case PG_ROOT: lock(); g_live.root = (g_live.root + 1 + d + 13) % 13 - 1; unlock(); mark_dirty(); break; /* AUTO, C .. B */ case PG_SCALE: lock(); g_live.scale = (g_live.scale + 1 + d + SB_NSCALES + 1) % (SB_NSCALES + 1) - 1; unlock(); mark_dirty(); break; /* AUTO, MAJOR .. LOCRIAN */ case PG_BPM: set_bpm(g_live.bpm + d); break; case PG_SLOT: slot_sel = (slot_sel + d + NSLOTS) % NSLOTS; break; case PG_ROLL: roll_sel = (roll_sel + d + 2) % 2; break; } } static void enc_push(int hold){ if(page == PG_LEARN){ if(learn_mode == 0){ learn_mode = 1; editing = 1; } else { learn_mode = 0; learn_tr = -1; editing = 0; msg("CANCELLED", 700); } return; } if(!editing){ editing = 1; return; } if(page == PG_SEED && hist_sel > 0) set_state(history[hist_sel - 1]); if(page == PG_SLOT){ if(hold){ char t[24]; slots[slot_sel].used = 1; slots[slot_sel].seed = g_live.state.seed; slots[slot_sel].salts[0] = g_live.state.salts[0]; slots[slot_sel].salts[1] = g_live.state.salts[1]; slots[slot_sel].gen = (uint8_t)g_live.gen; slots[slot_sel].keys = (uint8_t)g_live.keys; mark_dirty(); snprintf(t, sizeof t, "SAVED S%d", slot_sel + 1); msg(t, 900); return; } if(slots[slot_sel].used){ sb_state s = { slots[slot_sel].seed, { slots[slot_sel].salts[0], slots[slot_sel].salts[1] } }; lock(); g_live.gen = slots[slot_sel].gen; g_live.keys = slots[slot_sel].keys % SB_NKEYS; unlock(); set_state(s); } } if(page == PG_ROLL) reroll(roll_sel); /* then back to page select */ editing = 0; } static void track_down(int t){ if(learn_mode == 1 || learn_mode == 2){ if(t >= NKIT){ msg("KIT ONLY", 700); return; } learn_tr = t; learn_mode = 2; return; } held[t].active = 1; held[t].longp = 0; held[t].t0 = now_ms; } static void track_up(int t){ if(!held[t].active) return; held[t].active = 0; if(held[t].longp) set_inv(t, 0); else apply_on(t, !g_live.on[t]); } /* MIDI IN: only note-ons matter (LEARN). Handles running status, skips realtime and system messages. */ static void midi_in_byte(int b){ static int status, nd, d[2]; if(b >= 0xF8) return; if(b & 0x80){ status = b < 0xF0 ? b : 0; nd = 0; return; } if(!status) return; d[nd++] = b; if(nd < (((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0) ? 1 : 2)) return; nd = 0; if((status & 0xF0) == 0x90 && d[1] > 0 && learn_mode == 2 && learn_tr >= 0){ char t[24]; lock(); g_live.learn[learn_tr].used = 1; g_live.learn[learn_tr].ch = (uint8_t)(status & 15); g_live.learn[learn_tr].note = (uint8_t)d[0]; g_live.on[learn_tr] = 1; /* a freshly learned track starts on */ unlock(); mark_dirty(); snprintf(t, sizeof t, "N%d CH%d OK", d[0], (status & 15) + 1); msg(t, 1400); learn_mode = 0; learn_tr = -1; editing = 0; } } /* ---------- screen: the Nokia branch of drawOled() in app.js ---------- */ static void hex8(char *out, uint32_t v){ snprintf(out, 9, "%08lX", (unsigned long)v); } static void screen_text(char *label, char *value){ char l[24] = "", v[24] = ""; switch(page){ case PG_SEED: { const sb_state *s = hist_sel > 0 ? &history[hist_sel - 1] : &g_live.state; char n[16] = ""; if(hist_sel > 0) snprintf(n, sizeof n, " -%d", hist_sel); snprintf(l, sizeof l, "SEED%s%s", n, (s->salts[0] || s->salts[1]) ? " *" : ""); hex8(v, s->seed); break; } case PG_GEN: strcpy(l, "KIT GEN"); strcpy(v, GEN_NAME[g_live.gen]); break; case PG_KEYS: strcpy(l, "KEYS GEN"); strcpy(v, sb_keys_name(g_live.keys)); break; case PG_ROOT: case PG_SCALE: { /* on AUTO, show the seed's own pick */ sb_genome g; int sc = page == PG_SCALE, x = sc ? g_live.scale : g_live.root; if(x < 0){ sb_genome_make(&g_live.state, &g); x = sc ? g.scale : g.root; } snprintf(l, sizeof l, "%s%s", sc ? "SCALE" : "ROOT", (sc ? g_live.scale : g_live.root) < 0 ? " AUTO" : ""); strcpy(v, sc ? sb_scale_name(x) : NOTE_NAME[x]); break; } case PG_BPM: strcpy(l, "TEMPO"); snprintf(v, sizeof v, "%d BPM", g_live.bpm); break; case PG_SLOT: snprintf(l, sizeof l, "SLOT %d", slot_sel + 1); if(slots[slot_sel].used) hex8(v, slots[slot_sel].seed); else strcpy(v, "EMPTY"); break; case PG_ROLL: strcpy(l, "RE-ROLL"); strcpy(v, roll_sel ? "KEYS" : "KIT"); break; case PG_LEARN: { if(learn_mode == 2){ const char *n = TRACK_NAME[learn_tr]; char *o; strcpy(l, "LEARN "); o = l + 6; for(; *n; n++) if(*n != ' ') *o++ = *n; *o = 0; } else strcpy(l, "LEARN"); strcpy(v, learn_mode == 0 ? "PUSH" : learn_mode == 1 ? "PICK TRACK" : "HIT PAD"); break; } } if(now_ms < msg_until) snprintf(v, sizeof v, "%s", msg_text); l[10] = 0; v[10] = 0; strcpy(label, l); strcpy(value, v); } static void draw_cell(int x, int y, int w, int h, int state){ /* 0 off, 1 on, 2 on+hit, 3 selectable (dot), 4 selected */ if(state == 1 || state == 2 || state == 4) lcd_fill(x, y, w, h, 1); else lcd_box(x, y, w, h); if(state == 2) lcd_fill(x + 3, y + 3, w - 6, h - 6, 0); if(state == 3) lcd_fill(x + (w >> 1) - 1, y + (h >> 1) - 1, 2, 2, 1); } static int cell_state(int t, int blink){ if(learn_mode > 0) return t >= NKIT ? 0 : (learn_mode == 2 && learn_tr == t) ? (blink ? 4 : 3) : 3; if(t < NKIT && !g_live.learn[t].used) return 3; /* a dot: this kit track still needs learning */ return sounds(t) ? (now_ms < g_hit_until_ms[t] ? 2 : 1) : 0; } static void draw_screen(void){ char label[24], value[24]; int blink = (now_ms / 250) % 2 == 0, cur = g_cur_bar, i, k; screen_text(label, value); lcd_clear(); lcd_text(0, 0, label); if(g_running && cur >= 0) for(i = 0; i < 4; i++){ int nx = 64 + i * 5; if(i == cur % 4) lcd_fill(nx, 1, 4, 5, 1); else lcd_box(nx, 1, 4, 5); } if(editing){ lcd_fill(0, 8, 84, 10, 1); lcd_text_v(1, 9, value, 0); } else lcd_text(1, 9, value); for(k = 0; k < 12; k++){ int t = PANEL[k], x = 1 + (k % 4) * 21, y = 20 + (k / 4) * 10; draw_cell(x, y, 19, 8, t < 0 ? (g_running ? 1 : 0) : cell_state(t, blink)); } lcd_flush(); } int main(void){ uint32_t prev_in = 0, push_t0 = 0, ms = 0; int32_t prev_enc; int prev_pot[2] = {-1, -1}, i; absolute_time_t next; stdio_init_all(); /* USB: keeps the reset interface for picotool reboot -f -u */ critical_section_init(&g_live_lock); queue_init(&g_cmdq, sizeof(cmd_t), 32); storage_load(&g_live, slots); midi_init(); inputs_init(); encoder_init(); pots_init(); lcd_init(LCD_VOP_DEFAULT); g_live.energy = pot_value(POT_ENERGY); g_live.drift = pot_value(POT_DRIFT); multicore_launch_core1(seq_core1_main); prev_enc = encoder_count(); next = get_absolute_time(); for(;;){ uint32_t in, ch; int32_t enc; int b; next = delayed_by_us(next, 1000); sleep_until(next); now_ms = to_ms_since_boot(get_absolute_time()); ms++; /* buttons and switches */ in = inputs_scan(); ch = in ^ prev_in; prev_in = in; for(i = 0; i < IN_MATRIX; i++) if(ch & (1u << i)){ int t = INPUT_KEY_TRACK[i]; if(t < 0) continue; if(in & (1u << i)) track_down(t); else track_up(t); } if((ch >> IN_SEED) & 1 && !((in >> IN_SEED) & 1)) new_seed(); /* on release, like the simulator */ if((ch >> IN_RUN) & 1) send_cmd((in >> IN_RUN) & 1 ? CMD_START : CMD_STOP, 0); if((ch >> IN_PUSH) & 1){ if((in >> IN_PUSH) & 1) push_t0 = now_ms; else enc_push(now_ms - push_t0 > LONG_PUSH_MS); } for(i = 0; i < SB_NT; i++) if(held[i].active && !held[i].longp && now_ms - held[i].t0 >= HOLD_MS){ held[i].longp = 1; set_inv(i, 1); } /* encoder */ enc = encoder_count(); while(enc != prev_enc){ int d = enc > prev_enc ? 1 : -1; prev_enc += d; enc_turn(d); } /* MIDI IN */ while((b = midi_in_read()) >= 0) midi_in_byte(b); /* pots */ if(ms % 10 == 0){ pots_update(); if(pot_value(POT_ENERGY) != prev_pot[0] || pot_value(POT_DRIFT) != prev_pot[1]){ prev_pot[0] = pot_value(POT_ENERGY); prev_pot[1] = pot_value(POT_DRIFT); lock(); g_live.energy = prev_pot[0]; g_live.drift = prev_pot[1]; unlock(); } } /* storage: only while stopped (a flash erase pauses both cores) */ if(dirty && !g_running && now_ms - dirty_ms > SAVE_AFTER_MS){ live_t L; lock(); L = g_live; unlock(); storage_save(&L, slots); dirty = 0; } if(ms % 40 == 0) draw_screen(); } }