/* * Pocket Drums: a 16-step drum machine for OpenGroove. * SPDX-License-Identifier: Apache-2.0 * * Five synthesized voices (kick, snare, closed hat, open hat, clap), a swung * 16-step sequencer with three velocity levels, a DJ filter and drive on the * master bus. Press PLAY: it starts with a groove. * * Controls * 4x4 pads 16 steps of the selected voice (reading order). Tap toggles, * SHIFT+tap cycles accent/ghost. Stopped: a tap also auditions. * 4x16 pads rows = voices, columns = steps. UP/DOWN (or jog) scroll to * reach the 5th voice. * knobs TEMPO, SWING, FILTER (SHIFT: DRIVE), VOLUME * UP/DOWN select voice (4x4) / scroll rows (4x16); jog does the same * LEFT/RIGHT rotate the selected voice's pattern * MODE 1-4, BANK select kick, snare, closed hat, open hat, clap * touch tap a cell to toggle it, a name to select it, the header to play * MIDI in GM drum notes 36 38 42 46 39; start/stop * * Params: Tempo, Swing, Filter, Drive, Volume (the knobs, ids 1-5). * Sync: when the host is transport master (OG_CTX_SYNC) the sequencer follows * its tempo and position; PLAY/STOP then belong to the host. */ #include "opengroove.h" #include "og_gfx.h" #include "og_dsp.h" #include "og/arena.h" #include "og_fw.h" #define MAX_BLOCK 512 #define NSTEPS 16 #define NVOICES 5 #define MAX_PADS 64 enum { V_KICK, V_SNARE, V_CHAT, V_OHAT, V_CLAP }; static const char *const voice_name[NVOICES] = { "KICK", "SNARE", "CL HAT", "OP HAT", "CLAP" }; static const char *const voice_short[NVOICES] = { "BD", "SD", "CH", "OH", "CP" }; static const uint8_t voice_rgb[NVOICES][3] = { { 255, 112, 48 }, { 255, 206, 64 }, { 64, 222, 255 }, { 96, 140, 255 }, { 255, 84, 168 }, }; static const float level_vel[4] = { 0.0f, 0.42f, 0.75f, 1.0f }; /* off, ghost, normal, accent */ /* ------------------------------------------------------------------ */ /* State */ /* ------------------------------------------------------------------ */ typedef struct { uint8_t steps[NVOICES][NSTEPS]; /* 0 off, 1 ghost, 2 normal, 3 accent */ float tempo; /* BPM 60..180 */ float swing; /* 0.50..0.75 */ float filter; /* -1 (lowpass) .. 0 (open) .. +1 (highpass) */ float drive; /* 0..1 */ float volume; /* 0..1 */ uint8_t voice; /* selected voice */ uint8_t row_offset; /* first voice shown on a multi-row grid */ uint8_t page; /* step page when there are < 16 pads */ uint8_t _pad; } song_t; static og_device dev_in = { .struct_size = sizeof(og_device) }; /* the host writes this */ static og_device dev; /* our copy (readers never write) */ static og_process_ctx ctx = { .struct_size = sizeof(og_process_ctx) }; /* and this, before each block */ static float sr = 48000.0f; static uint32_t block = MAX_BLOCK; /* og_activate's max_block */ static int active; static song_t song; static float audio_out[2 * MAX_BLOCK]; static uint8_t *framebuffer; /* exactly display_w * display_h * 4 bytes, from og_arena in og_init */ static og_arena arena; static uint8_t pad_leds[MAX_PADS * 3]; static uint8_t button_leds[OG_MAX_BUTTONS]; static uint8_t state_buf[512]; static og_gfx gfx; static og_rng rng = { 0x2545F491u }; static int playing, step = -1; static double to_next; /* samples until the next step fires */ static int shift_held; static int dirty = 1; static uint64_t clock_samples; static int knob_shown = -1; /* last knob touched, for the small screen */ static uint64_t knob_time; static og_fw_sync clk; /* host transport, when it is master */ static int sync_mode; static int pending_step = -1; /* swung 16th waiting to fire (sync mode) */ static double pending_left; /* ------------------------------------------------------------------ */ /* Parameters: the knobs, in user units */ /* ------------------------------------------------------------------ */ enum { P_TEMPO = 1, P_SWING, P_FILTER, P_DRIVE, P_VOLUME }; #define AUTO OG_PARAM_AUTOMATABLE static const og_fw_param params[] = { OG_FW_PARAM(P_TEMPO, "Tempo", "BPM", 60.0f, 180.0f, 122.0f, AUTO, &song.tempo, 1.0f), OG_FW_PARAM(P_SWING, "Swing", "%", 50.0f, 75.0f, 56.0f, AUTO, &song.swing, 0.01f), OG_FW_PARAM(P_FILTER, "Filter", "%", -100.0f, 100.0f, 0.0f, AUTO, &song.filter, 0.01f), OG_FW_PARAM(P_DRIVE, "Drive", "%", 0.0f, 100.0f, 20.0f, AUTO, &song.drive, 0.01f), OG_FW_PARAM(P_VOLUME, "Volume", "%", 0.0f, 100.0f, 80.0f, AUTO, &song.volume, 0.01f), }; #define NPARAMS (sizeof params / sizeof params[0]) static float cur_tempo(void) { return sync_mode ? (float)clk.bpm : song.tempo; } /* ------------------------------------------------------------------ */ /* Voices */ /* ------------------------------------------------------------------ */ static struct { float amp, penv, phase, click, vel; og_onepole clk; } kick; static struct { float tone, noise, p1, p2, vel; og_svf hp; } snare; static struct { float camp, oamp, ocoef, vel_c, vel_o; float ph[6]; og_svf bp; og_onepole hp; } hat; static struct { float burst, tail, vel; int t; og_svf bp; } clap; static float k_pdec, k_adec, k_cdec, s_tdec, s_ndec, h_cdec, h_odec, h_choke, c_bdec, c_tdec; static og_svf master_f[2]; static float filt_s, drive_s, vol_s; static float smooth_k = 0.0017f; /* per-sample de-zipper coefficient, set in og_activate */ static void trigger(int v, float vel) { vel = og_clampf(vel, 0.0f, 1.0f); switch (v) { case V_KICK: kick.amp = 1.0f; kick.penv = 1.0f; kick.phase = 0.0f; kick.click = 1.0f; kick.vel = vel; break; case V_SNARE: snare.tone = 1.0f; snare.noise = 1.0f; snare.vel = vel; break; case V_CHAT: hat.camp = 1.0f; hat.vel_c = vel; hat.ocoef = h_choke; break; /* chokes the open hat */ case V_OHAT: hat.oamp = 1.0f; hat.vel_o = vel; hat.ocoef = h_odec; break; case V_CLAP: clap.burst = 1.0f; clap.tail = 0.0f; clap.t = 0; clap.vel = vel; break; } } static void voices_init(void) { k_pdec = og_decay_coef(0.09f, sr); k_adec = og_decay_coef(0.75f, sr); k_cdec = og_decay_coef(0.004f, sr); s_tdec = og_decay_coef(0.16f, sr); s_ndec = og_decay_coef(0.30f, sr); h_cdec = og_decay_coef(0.09f, sr); h_odec = og_decay_coef(0.55f, sr); h_choke = og_decay_coef(0.025f, sr); c_bdec = og_decay_coef(0.012f, sr); c_tdec = og_decay_coef(0.24f, sr); hat.ocoef = h_odec; og_svf_set(&snare.hp, 1700.0f, 0.8f, sr); og_svf_set(&hat.bp, 9500.0f, 0.9f, sr); og_svf_set(&clap.bp, 1150.0f, 1.6f, sr); } /* One sample of the drum mix, stereo. */ static void voices_tick(float *l, float *r) { float inv = 1.0f / sr; /* Kick: sine with a fast pitch sweep, plus a short noise click. */ float k = 0.0f; if (kick.amp > 1e-5f) { float f = 44.0f + 230.0f * kick.penv; kick.penv *= k_pdec; kick.phase += f * inv; if (kick.phase >= 1.0f) kick.phase -= 1.0f; float body = og_sin_cycles(kick.phase) * kick.amp; kick.amp *= k_adec; float clk = og_onepole_lp(&kick.clk, og_noise(&rng) * kick.click, 0.45f); kick.click *= k_cdec; k = og_sat(1.5f * body + 0.6f * clk) * kick.vel; } /* Snare: two damped tones and high-passed noise. */ float s = 0.0f; if (snare.noise > 1e-5f) { float bend = 1.0f + 0.35f * snare.tone * snare.tone; snare.p1 += 182.0f * bend * inv; if (snare.p1 >= 1.0f) snare.p1 -= 1.0f; snare.p2 += 331.0f * bend * inv; if (snare.p2 >= 1.0f) snare.p2 -= 1.0f; float tone = (og_sin_cycles(snare.p1) * 0.6f + og_sin_cycles(snare.p2) * 0.4f) * snare.tone; og_svf_tick(&snare.hp, og_noise(&rng)); float nz = (snare.hp.hp * 0.7f + snare.hp.bp * 0.5f) * snare.noise; snare.tone *= s_tdec; snare.noise *= s_ndec; s = (tone * 0.75f + nz * 0.8f) * snare.vel; } /* Hats: six detuned square waves (808 style) plus noise, band-passed. */ float ch = 0.0f, oh = 0.0f; if (hat.camp > 1e-5f || hat.oamp > 1e-5f) { static const float hz[6] = { 205.3f, 304.4f, 369.6f, 522.7f, 540.0f, 800.0f }; float metal = 0.0f; for (int i = 0; i < 6; i++) { hat.ph[i] += hz[i] * 1.6f * inv; if (hat.ph[i] >= 1.0f) hat.ph[i] -= 1.0f; metal += hat.ph[i] < 0.5f ? 1.0f : -1.0f; } og_svf_tick(&hat.bp, metal * (1.0f / 6.0f) * 0.7f + og_noise(&rng) * 0.45f); float src = og_onepole_hp(&hat.hp, hat.bp.bp, 0.55f); ch = src * hat.camp * hat.vel_c; oh = src * hat.oamp * hat.vel_o; hat.camp *= h_cdec; hat.oamp *= hat.ocoef; } /* Clap: four quick noise bursts and a tail, through a band-pass. */ float c = 0.0f; if (clap.burst > 1e-5f || clap.tail > 1e-5f) { int t = clap.t++; int ms = (int)(sr * 0.001f); if (t == 10 * ms || t == 21 * ms || t == 31 * ms) clap.burst = 1.0f; if (t == 31 * ms) clap.tail = 0.75f; float env = og_maxf(clap.burst, clap.tail); og_svf_tick(&clap.bp, og_noise(&rng) * env); clap.burst *= c_bdec; clap.tail *= c_tdec; c = clap.bp.bp * 1.6f * clap.vel; } float mono = k * 0.95f + s * 0.55f; *l = mono + ch * 0.42f + oh * 0.32f + c * 0.58f; *r = mono + ch * 0.55f + oh * 0.44f + c * 0.44f; } /* ------------------------------------------------------------------ */ /* Sequencer */ /* ------------------------------------------------------------------ */ static float samples_per_step(void) { return sr * 60.0f / og_clampf(cur_tempo(), 20.0f, 400.0f) / 4.0f; } static void play_step(int s) { step = s; for (int v = 0; v < NVOICES; v++) { uint8_t lv = song.steps[v][step]; if (lv) trigger(v, level_vel[lv & 3]); } dirty = 1; } static void fire_step(void) { play_step((step + 1) % NSTEPS); /* Swing: even steps land on the beat grid, odd steps are delayed. */ float pair = 2.0f * samples_per_step(); float sw = og_clampf(song.swing, 0.5f, 0.75f); to_next += (step & 1) ? pair * (1.0f - sw) : pair * sw; dirty = 1; } static void set_playing(int on) { if (on && !playing) { playing = 1; step = -1; to_next = 0.0; } else if (!on) { playing = 0; step = -1; } dirty = 1; } /* ------------------------------------------------------------------ */ /* Input */ /* ------------------------------------------------------------------ */ static int n_pads(void) { int n = dev.pad_rows * dev.pad_cols; return n > MAX_PADS ? MAX_PADS : n; } /* Grid layout: at least 16 columns, each row one voice. */ static int grid_layout(void) { return dev.pad_cols >= NSTEPS && dev.pad_rows >= 1; } static int steps_per_page(void) { int n = n_pads(); return n < NSTEPS ? (n > 0 ? n : 1) : NSTEPS; } static int max_row_offset(void) { int m = NVOICES - dev.pad_rows; return m > 0 ? m : 0; } static void edit_step(int v, int s, int shift) { if (v < 0 || v >= NVOICES || s < 0 || s >= NSTEPS) return; uint8_t *lv = &song.steps[v][s]; if (shift) { static const uint8_t next[4] = { 3, 2, 3, 1 }; /* off->accent, ghost->normal, normal->accent, accent->ghost */ *lv = next[*lv & 3]; } else { *lv = *lv ? 0 : 2; } /* Audition when stopped so every tap is heard (and so you can hear what * you are about to program). */ if (!playing) trigger(v, *lv ? level_vel[*lv] : 0.75f); song.voice = (uint8_t)v; dirty = 1; } static void select_voice(int v) { song.voice = (uint8_t)og_clampi(v, 0, NVOICES - 1); /* keep it visible on a multi-row grid */ if (grid_layout()) { if (song.voice < song.row_offset) song.row_offset = song.voice; if (song.voice >= song.row_offset + dev.pad_rows) song.row_offset = (uint8_t)og_clampi(song.voice - dev.pad_rows + 1, 0, max_row_offset()); } dirty = 1; } static void voice_or_scroll(int dir) { if (grid_layout()) { song.row_offset = (uint8_t)og_clampi(song.row_offset + dir, 0, max_row_offset()); dirty = 1; } else { select_voice((song.voice + NVOICES + dir) % NVOICES); } } static void rotate_voice(int dir) { uint8_t *p = song.steps[song.voice], tmp[NSTEPS]; for (int i = 0; i < NSTEPS; i++) tmp[(i + dir + NSTEPS) % NSTEPS] = p[i]; for (int i = 0; i < NSTEPS; i++) p[i] = tmp[i]; dirty = 1; } static void pad_down(int id, float vel) { (void)vel; if (id < 0 || id >= n_pads()) return; if (grid_layout()) { int row = id / dev.pad_cols, col = id % dev.pad_cols; if (col < NSTEPS) edit_step(song.row_offset + row, col, shift_held); else if (!playing) trigger(song.row_offset + row < NVOICES ? song.row_offset + row : V_KICK, 0.75f); } else { edit_step(song.voice, song.page * steps_per_page() + id, shift_held); } } /* Which param a knob controls right now (SHIFT changes knob 3). */ static const og_fw_param *knob_param(int id) { static const uint32_t map[4] = { P_TEMPO, P_SWING, P_FILTER, P_VOLUME }; if (id < 0 || id > 3) return 0; return og_fw_param_find(params, NPARAMS, (id == 2 && shift_held) ? P_DRIVE : map[id]); } static void knob(int id, float d) { if (dev.knob_kind == OG_KNOB_ABSOLUTE) { /* a pot: d is the position 0..1 */ const og_fw_param *p = knob_param(id); if (!p) return; og_fw_param_set_norm(p, d); knob_shown = (id == 2 && shift_held) ? 4 : id; knob_time = clock_samples; dirty = 1; return; } d = og_clampf(d, -64.0f, 64.0f); switch (id) { case 0: song.tempo = og_clampf(song.tempo + d, 60.0f, 180.0f); break; case 1: song.swing = og_clampf(song.swing + d * 0.005f, 0.5f, 0.75f); break; case 2: if (shift_held) song.drive = og_clampf(song.drive + d * 0.02f, 0.0f, 1.0f); else song.filter = og_clampf(song.filter + d * 0.02f, -1.0f, 1.0f); break; case 3: song.volume = og_clampf(song.volume + d * 0.02f, 0.0f, 1.0f); break; default: return; } knob_shown = (id == 2 && shift_held) ? 4 : id; knob_time = clock_samples; dirty = 1; } static void button(int id, int down) { if (id == OG_BTN_SHIFT) { shift_held = down; dirty = 1; return; } if (!down) return; switch (id) { case OG_BTN_PLAY: if (!sync_mode) set_playing(!playing); break; /* in sync the host owns the transport */ case OG_BTN_STOP: if (!sync_mode) set_playing(0); break; case OG_BTN_UP: voice_or_scroll(-1); break; case OG_BTN_DOWN: voice_or_scroll(1); break; case OG_BTN_LEFT: case OG_BTN_RIGHT: { int dir = id == OG_BTN_LEFT ? -1 : 1; if (n_pads() < NSTEPS) { int pages = (NSTEPS + steps_per_page() - 1) / steps_per_page(); song.page = (uint8_t)((song.page + pages + dir) % pages); dirty = 1; } else { rotate_voice(dir); } break; } case OG_BTN_MODE_1: case OG_BTN_MODE_2: case OG_BTN_MODE_3: case OG_BTN_MODE_4: select_voice(id - OG_BTN_MODE_1); break; case OG_BTN_BANK: select_voice(V_CLAP); break; case OG_BTN_SELECT: select_voice((song.voice + 1) % NVOICES); break; } } /* Screen geometry for the large layout, shared by drawing and touch. */ typedef struct { int gx, gy, cw, ch, pitch, row_h, label_x; } grid_geom; static grid_geom big_geom(void) { grid_geom g; int w = dev.display_w, h = dev.display_h; g.label_x = w * 3 / 100; g.gx = w * 20 / 100; int gw = w - g.gx - g.label_x - 30; /* 3 group gaps of 10 */ g.pitch = gw / NSTEPS; g.cw = g.pitch - 4; g.gy = h * 18 / 100; g.row_h = h * 105 / 1000; g.ch = g.row_h - 8; return g; } static int big_cell_x(const grid_geom *g, int s) { return g->gx + s * g->pitch + (s / 4) * 10; } static void touch(float x, float y) { if (dev.display_w < 320) return; grid_geom g = big_geom(); int ix = (int)x, iy = (int)y; if (iy < g.gy - 8) { if (!sync_mode) set_playing(!playing); return; } int row = (iy - g.gy) / g.row_h; if (iy < g.gy || row < 0 || row >= NVOICES) return; if (ix < g.gx - 8) { select_voice(row); if (!playing) trigger(row, 0.75f); return; } for (int s = 0; s < NSTEPS; s++) { int cx = big_cell_x(&g, s); if (ix >= cx && ix < cx + g.pitch) { edit_step(row, s, shift_held); return; } } } static void midi(uint8_t st, uint8_t d1, uint8_t d2) { if (st == 0xFA) { if (!sync_mode) set_playing(1); return; } if (st == 0xFC) { if (!sync_mode) set_playing(0); return; } if ((st & 0xF0) != 0x90 || d2 == 0) return; int v = -1; switch (d1) { case 35: case 36: v = V_KICK; break; case 37: case 38: case 40: v = V_SNARE; break; case 42: case 44: v = V_CHAT; break; case 46: v = V_OHAT; break; case 39: v = V_CLAP; break; } if (v >= 0) trigger(v, (float)(d2 & 127) / 127.0f); } /* ------------------------------------------------------------------ */ /* Event queue: events are applied at their exact frame offset */ /* ------------------------------------------------------------------ */ typedef struct { uint32_t type, id, offset; float value, x, y; } ev_t; #define EVQ 256 static ev_t evq[EVQ]; static int evn; static void push_ev(uint32_t type, uint32_t id, float value, uint32_t offset, float x, float y) { if (evn >= EVQ) return; int i = evn++; /* insertion keeps the queue sorted by offset (stable) */ while (i > 0 && evq[i - 1].offset > offset) { evq[i] = evq[i - 1]; i--; } evq[i].type = type; evq[i].id = id; evq[i].value = value; evq[i].offset = offset; evq[i].x = x; evq[i].y = y; } #define EV_MIDI 1000u static void apply_ev(const ev_t *e) { switch (e->type) { case OG_PAD_DOWN: pad_down((int)e->id, e->value); break; case OG_KNOB: knob((int)e->id, e->value); break; case OG_BUTTON_DOWN: button((int)e->id, 1); break; case OG_BUTTON_UP: button((int)e->id, 0); break; case OG_JOG: voice_or_scroll(e->value > 0 ? 1 : (e->value < 0 ? -1 : 0)); break; case OG_TOUCH_DOWN: if (dev.caps & OG_CAP_TOUCH) touch(e->x, e->y); break; case OG_PARAM_CHANGE: if (og_fw_param_set(params, NPARAMS, e->id, e->value)) dirty = 1; break; case EV_MIDI: midi((uint8_t)(e->id >> 16), (uint8_t)(e->id >> 8), (uint8_t)e->id); break; } } /* ------------------------------------------------------------------ */ /* Exports */ /* ------------------------------------------------------------------ */ static void default_song(void) { static const char *const pat[NVOICES] = { /* 0 off, 1 ghost, 2 normal, 3 accent; one char per 16th */ "3000200020002010", /* kick: four on the floor, a ghost push */ "0001000000100023", /* snare: ghosts and a fill into the one */ "2102120221021202", /* closed hat: 16ths with movement */ "0020002000200020", /* open hat: offbeats, choked by the closed hat */ "0000300000002000", /* clap: 2 and 4 */ }; for (int v = 0; v < NVOICES; v++) for (int s = 0; s < NSTEPS; s++) song.steps[v][s] = (uint8_t)(pat[v][s] - '0'); og_fw_params_reset(params, NPARAMS); /* tempo, swing, filter, drive, volume */ song.voice = V_KICK; song.row_offset = 0; song.page = 0; } uint32_t og_abi_version(void) { return OG_ABI_VERSION; } uint32_t og_caps(void) { return OG_CAP_PARAMS | OG_CAP_STATE | OG_CAP_TRANSPORT | OG_CAP_RACK; } /* may run as one instance of a rack */ og_device *og_device_ptr(void) { return &dev_in; } og_process_ctx *og_process_ctx_ptr(void) { return &ctx; } static og_instance_info inst = OG_INSTANCE_INIT; /* the host writes it when we run inside a rack */ og_instance_info *og_instance_ptr(void) { return &inst; } int32_t og_init(void) { og_fw_device_read(&dev, &dev_in); if (dev.max_block == 0 || dev.max_block > MAX_BLOCK) dev.max_block = MAX_BLOCK; if (dev.display_w * dev.display_h > 2048 * 2048) { dev.display_w = 0; dev.display_h = 0; } og_arena_init(&arena); framebuffer = dev.display_w ? (uint8_t *)og_arena_alloc(&arena, (uint32_t)dev.display_w * dev.display_h * 4u, 4) : 0; if (dev.display_w && !framebuffer) return OG_ERR; og_gfx_init(&gfx, framebuffer, framebuffer ? dev.display_w : 0, framebuffer ? dev.display_h : 0); default_song(); filt_s = song.filter; drive_s = song.drive; vol_s = song.volume; dirty = 1; return 0; } void og_reset(void) { evn = 0; /* queues clear on reset */ __builtin_memset(&kick, 0, sizeof kick); __builtin_memset(&snare, 0, sizeof snare); __builtin_memset(&hat, 0, sizeof hat); __builtin_memset(&clap, 0, sizeof clap); __builtin_memset(master_f, 0, sizeof master_f); voices_init(); /* filter coefficients live in the cleared structs */ pending_step = -1; } int32_t og_activate(uint32_t sample_rate, uint32_t max_block) { sr = (sample_rate >= 8000 && sample_rate <= 384000) ? (float)sample_rate : 48000.0f; block = (max_block >= 1 && max_block <= dev.max_block) ? max_block : dev.max_block; smooth_k = 1.0f - og_expf(-1.0f / (0.012f * sr)); og_reset(); active = 1; return 0; } void og_deactivate(void) { active = 0; } void og_event(uint32_t type, uint32_t id, float value, uint32_t offset, float x, float y) { if (!(value == value)) value = 0.0f; /* NaN guard */ push_ev(type, id, value, offset, x, y); } void og_midi_in(uint8_t status, uint8_t d1, uint8_t d2, uint32_t offset) { push_ev(EV_MIDI, ((uint32_t)status << 16) | ((uint32_t)d1 << 8) | d2, 0.0f, offset, 0.0f, 0.0f); } /* Master bus coefficients from the smoothed params. Recomputed every 16 frames * and right after any event, so OG_PARAM_CHANGE starts acting at its offset. */ static float m_pre, m_post, m_gain; static int m_hp; static void derive_master(void) { float fc; m_hp = filt_s > 0.0f; if (m_hp) fc = 20.0f * og_exp2(filt_s * 9.0f); /* 20 Hz .. 10 kHz */ else fc = 20000.0f * og_exp2(filt_s * 6.6f); /* 20 kHz .. 200 Hz */ float q = 0.75f + 1.6f * og_fabsf(filt_s); og_svf_set(&master_f[0], fc, q, sr); og_svf_set(&master_f[1], fc, q, sr); m_pre = 1.0f + drive_s * 5.0f; m_post = 1.0f / (1.0f + drive_s * 1.6f); m_gain = vol_s * vol_s * 1.55f; } void og_process(uint32_t frames) { if (!active) og_activate(dev.sample_rate, dev.max_block); if (frames > block) frames = block; float *outl = audio_out, *outr = audio_out + dev.max_block; /* stride = og_device.max_block */ const float sm = smooth_k; /* Host transport: in sync mode it decides playing, tempo and position. */ int sync = og_fw_sync_begin(&clk, &ctx, sr, song.tempo); if (sync != sync_mode) { sync_mode = sync; pending_step = -1; if (!sync) set_playing(0); dirty = 1; } if (sync && clk.playing != playing) { playing = clk.playing; step = -1; pending_step = -1; dirty = 1; } int e = 0; for (uint32_t i = 0; i < frames; i++) { /* sorted by offset; out-of-range offsets land on the last frame */ int applied = 0; while (e < evn && (evq[e].offset <= i || i + 1 == frames)) { apply_ev(&evq[e++]); applied = 1; } /* de-zipper: per-sample smoothing (~12 ms) toward the params */ filt_s += (song.filter - filt_s) * sm; drive_s += (song.drive - drive_s) * sm; vol_s += (song.volume - vol_s) * sm; if (applied || (i & 15) == 0) derive_master(); if (sync) { long n = og_fw_sync_tick(&clk); if (pending_step >= 0 && (n >= 0 || --pending_left <= 0.0)) { play_step(pending_step); pending_step = -1; } if (n >= 0) { int s = (int)(((n % NSTEPS) + NSTEPS) % NSTEPS); /* swing delays the odd 16ths, as in free-running mode */ double delay = (s & 1) ? samples_per_step() * (2.0f * og_clampf(song.swing, 0.5f, 0.75f) - 1.0f) : 0.0; if (delay >= 1.0) { pending_step = s; pending_left = delay; } else play_step(s); } } else if (playing) { if (to_next <= 0.0) fire_step(); to_next -= 1.0; } float l, r; voices_tick(&l, &r); og_svf_tick(&master_f[0], l); og_svf_tick(&master_f[1], r); l = m_hp ? master_f[0].hp : master_f[0].lp; r = m_hp ? master_f[1].hp : master_f[1].lp; l = og_sat(l * m_pre) * m_post * m_gain; r = og_sat(r * m_pre) * m_post * m_gain; outl[i] = og_sat(l) * 0.88f; outr[i] = og_sat(r) * 0.88f; } while (e < evn) apply_ev(&evq[e++]); evn = 0; clock_samples += frames; } /* ------------------------------------------------------------------ */ /* Drawing */ /* ------------------------------------------------------------------ */ static og_color vcol(int v, int f) { return og_dim(og_rgb(voice_rgb[v][0], voice_rgb[v][1], voice_rgb[v][2]), f); } static void fmt_pct(char *b, int cap, float v) { og_strcpy(b, "", cap); og_strcat_int(b, (int)(v * 100.0f + 0.5f), cap); og_strcat(b, "%", cap); } static void fmt_knob(int k, char *name, char *val, int cap) { switch (k) { case 0: og_strcpy(name, sync_mode ? "SYNC" : "TEMPO", cap); og_itoa((int)(cur_tempo() + 0.5f), val); break; case 1: og_strcpy(name, "SWING", cap); og_itoa((int)(song.swing * 100.0f + 0.5f), val); og_strcat(val, "%", cap); break; case 2: og_strcpy(name, "FILTER", cap); if (og_fabsf(song.filter) < 0.02f) og_strcpy(val, "OPEN", cap); else { og_strcpy(val, song.filter < 0 ? "LP " : "HP ", cap); og_strcat_int(val, (int)(og_fabsf(song.filter) * 100.0f + 0.5f), cap); } break; case 3: og_strcpy(name, "VOLUME", cap); fmt_pct(val, cap, song.volume); break; default: og_strcpy(name, "DRIVE", cap); fmt_pct(val, cap, song.drive); break; } } /* Arc gauge: 270 degrees, from bottom-left clockwise. bipolar = fill from top. */ static void gauge(int cx, int cy, int r, float v, int bipolar, og_color on, og_color off) { for (int a = 0; a <= 270; a += 3) { float t = (float)a / 270.0f; float ang = (135.0f + (float)a) / 360.0f; /* cycles, 0 = +x axis, y down */ int x = cx + (int)(og_cos_cycles(ang) * (float)r); int y = cy + (int)(og_sin_cycles(ang) * (float)r); int lit = bipolar ? ((v >= 0.0f) ? (t >= 0.5f && t <= 0.5f + v * 0.5f) : (t <= 0.5f && t >= 0.5f + v * 0.5f)) : (t <= v); og_fill_circle(&gfx, x, y, 3, lit ? on : off); } float ang = (135.0f + 270.0f * (bipolar ? 0.5f + 0.5f * v : v)) / 360.0f; og_line_thick(&gfx, cx, cy, cx + (int)(og_cos_cycles(ang) * (float)(r - 9)), cy + (int)(og_sin_cycles(ang) * (float)(r - 9)), 3, og_rgb(235, 238, 245)); } static void draw_big(void) { int w = dev.display_w, h = dev.display_h; og_color bg = og_hex(0x0E1016), panel = og_hex(0x171A23), text = og_hex(0xE8EAF0), muted = og_hex(0x6B7186); og_clear(&gfx, bg); /* Header */ int hh = h * 13 / 100; og_fill_rect(&gfx, 0, 0, w, hh, panel); og_fill_rect(&gfx, 0, hh, w, 2, og_hex(0x232838)); int ts = h >= 400 ? 4 : 2; int ty = (hh - 7 * ts) / 2; int x = og_text(&gfx, 24, ty, "POCKET", ts, text); og_text(&gfx, x + 3 * ts, ty, "DRUMS", ts, og_hex(0xFF7030)); char b[24]; og_itoa((int)(cur_tempo() + 0.5f), b); og_strcat(b, " BPM", sizeof b); og_text_right(&gfx, w - 24, ty, b, ts, text); int px = w - 24 - og_text_width(b, ts) - 14 * ts; if (sync_mode) { int sx = px - og_text_width("SYNC", 2) - 30; og_fill_round_rect(&gfx, sx, ty - 2, og_text_width("SYNC", 2) + 16, 7 * 2 + 8, 6, og_hex(0x1D3A4A)); og_text(&gfx, sx + 8, ty + 2, "SYNC", 2, og_hex(0x40DEFF)); } if (playing) { for (int i = 0; i < 7 * ts; i++) og_vline(&gfx, px + i, ty + i / 2, 7 * ts - i, og_hex(0x5BE38A)); } else { og_fill_rect(&gfx, px, ty, 7 * ts, 7 * ts, muted); } /* Step grid */ grid_geom g = big_geom(); for (int v = 0; v < NVOICES; v++) { int y = g.gy + v * g.row_h; int sel = v == song.voice; if (sel) og_fill_round_rect(&gfx, g.label_x - 10, y - 4, w - 2 * g.label_x + 20, g.row_h, 8, og_hex(0x1C2030)); og_fill_round_rect(&gfx, g.label_x, y + g.ch / 2 - 6, 6, 12, 2, vcol(v, sel ? 255 : 120)); og_text(&gfx, g.label_x + 16, y + (g.ch - 14) / 2, voice_name[v], 2, sel ? text : muted); for (int s = 0; s < NSTEPS; s++) { int cx = big_cell_x(&g, s); uint8_t lv = song.steps[v][s]; og_color c = lv ? vcol(v, lv == 1 ? 110 : (lv == 2 ? 200 : 255)) : (s % 4 == 0 ? og_hex(0x2A2F40) : og_hex(0x20242F)); og_fill_round_rect(&gfx, cx, y, g.cw, g.ch, 6, c); if (lv == 3) og_fill_round_rect(&gfx, cx + g.cw / 2 - 4, y + 5, 8, 4, 2, og_rgb(255, 255, 255)); if (playing && s == step) { og_round_rect(&gfx, cx - 2, y - 2, g.cw + 4, g.ch + 4, 7, og_rgb(255, 255, 255)); if (lv) og_blend_rect(&gfx, cx + 2, y + 2, g.cw - 4, g.ch - 4, og_rgb(255, 255, 255), 90); } } } /* step numbers */ int ny = g.gy - 22; for (int s = 0; s < NSTEPS; s += 4) { og_itoa(s + 1, b); og_text(&gfx, big_cell_x(&g, s) + 2, ny, b, 2, muted); } /* which rows the pads show (multi-row grid) or which page */ if (grid_layout() && dev.pad_rows < NVOICES) { int y0 = g.gy + song.row_offset * g.row_h - 4; og_fill_rect(&gfx, g.label_x - 16, y0, 3, dev.pad_rows * g.row_h, og_hex(0xFF7030)); } /* Knobs */ int ky = h * 79 / 100, kr = h * 6 / 100; for (int k = 0; k < 4; k++) { int kx = 24 + k * (w - 48) / 4; int which = (k == 2 && shift_held) ? 4 : k; float v; switch (which) { case 0: v = og_clampf((cur_tempo() - 60.0f) / 120.0f, 0.0f, 1.0f); break; case 1: v = (song.swing - 0.5f) / 0.25f; break; case 2: v = song.filter; break; case 3: v = song.volume; break; default: v = song.drive; break; } og_color acc = which == 2 ? og_hex(0x40DEFF) : og_hex(0xFF7030); gauge(kx + kr, ky, kr, v, which == 2, acc, og_hex(0x2A2F40)); char name[16], val[16]; fmt_knob(which, name, val, 16); og_text(&gfx, kx + 2 * kr + 16, ky - 22, name, 2, muted); og_text(&gfx, kx + 2 * kr + 16, ky - 2, val, 3, text); } /* Footer hint */ int arrows = (dev.buttons_present >> OG_BTN_UP) & 1; /* KICK-class: no arrows, the jog picks the voice */ const char *hint = grid_layout() ? (arrows ? "SHIFT+PAD ACCENT UP/DN ROWS SHIFT+FILTER DRIVE" : "SHIFT+PAD ACCENT JOG ROWS SHIFT+FILTER DRIVE") : (arrows ? "SHIFT+PAD ACCENT UP/DN VOICE L/R NUDGE" : "SHIFT+PAD ACCENT JOG VOICE -/+ NUDGE"); og_text_center(&gfx, w / 2, h - 24, hint, 2, muted); if (!grid_layout() && n_pads() < NSTEPS) { og_strcpy(b, "PAGE ", sizeof b); og_strcat_int(b, song.page + 1, sizeof b); og_text_right(&gfx, w - 24, h - 24, b, 2, text); } } static void draw_small(void) { int w = dev.display_w, h = dev.display_h; og_color on = og_rgb(255, 255, 255), off = og_rgb(0, 0, 0); og_clear(&gfx, off); char b[24]; og_text(&gfx, 0, 0, voice_name[song.voice], 1, on); og_itoa((int)(cur_tempo() + 0.5f), b); og_strcat(b, sync_mode ? (playing ? " S>" : " S.") : (playing ? " >" : " ."), sizeof b); og_text_right(&gfx, w - 1, 0, b, 1, on); og_strcpy(b, "SW", sizeof b); og_strcat_int(b, (int)(song.swing * 100.0f + 0.5f), sizeof b); og_text_center(&gfx, w / 2 + 4, 0, b, 1, on); int top = 10, rh = (h - top - 10) / NVOICES; if (rh < 3) rh = 3; int lx = 15, pitch = (w - lx) / NSTEPS; if (pitch < 2) pitch = 2; if (playing && step >= 0) og_fill_rect(&gfx, lx + step * pitch - 1, top - 1, pitch, rh * NVOICES + 1, on); for (int v = 0; v < NVOICES; v++) { int y = top + v * rh; int vis = grid_layout() ? (v >= song.row_offset && v < song.row_offset + dev.pad_rows) : (v == song.voice); if (vis) og_vline(&gfx, 0, y, rh - 1, on); og_text(&gfx, 3, y + (rh - 7) / 2, voice_short[v], 1, on); for (int s = 0; s < NSTEPS; s++) { int x = lx + s * pitch; og_color c = (playing && s == step) ? off : on; uint8_t lv = song.steps[v][s]; int cw = pitch - 2, ch = rh - 2; if (lv == 3) og_fill_rect(&gfx, x, y, cw, ch, c); else if (lv == 2) og_fill_rect(&gfx, x + 1, y + 1, cw - 2 > 1 ? cw - 2 : 1, ch - 2 > 1 ? ch - 2 : 1, c); else if (lv == 1) og_stroke_rect(&gfx, x + 1, y + 1, cw - 2 > 1 ? cw - 2 : 1, ch - 2 > 1 ? ch - 2 : 1, c); else og_pixel(&gfx, x + cw / 2, y + ch / 2, c); } } /* bottom line: the knob you are turning, else filter + volume */ char name[16], val[16]; if (knob_shown >= 0) { fmt_knob(knob_shown, name, val, 16); og_strcpy(b, name, sizeof b); og_strcat(b, " ", sizeof b); og_strcat(b, val, sizeof b); } else { fmt_knob(2, name, val, 16); og_strcpy(b, "FLT ", sizeof b); og_strcat(b, val, sizeof b); og_strcat(b, " VOL ", sizeof b); og_strcat_int(b, (int)(song.volume * 100.0f + 0.5f), sizeof b); } og_text(&gfx, 0, h - 7, b, 1, on); } static uint8_t want_pad[MAX_PADS * 3], want_btn[OG_MAX_BUTTONS]; static void compute_leds(void) { int n = n_pads(); for (int i = 0; i < n; i++) { int v, s; if (grid_layout()) { v = song.row_offset + i / dev.pad_cols; s = i % dev.pad_cols; } else { v = song.voice; s = song.page * steps_per_page() + i; } int r = 0, g = 0, bl = 0; if (v < NVOICES && s < NSTEPS) { uint8_t lv = song.steps[v][s]; int f = lv == 0 ? 0 : (lv == 1 ? 60 : (lv == 2 ? 150 : 255)); r = voice_rgb[v][0] * f / 255; g = voice_rgb[v][1] * f / 255; bl = voice_rgb[v][2] * f / 255; if (!lv) { r = g = bl = (s % 4 == 0) ? 22 : 10; } if (playing && s == step) { r = lv ? 255 : 90; g = lv ? 255 : 90; bl = lv ? 255 : 90; } } want_pad[i * 3] = (uint8_t)r; want_pad[i * 3 + 1] = (uint8_t)g; want_pad[i * 3 + 2] = (uint8_t)bl; } for (int i = 0; i < OG_MAX_BUTTONS; i++) want_btn[i] = 0; want_btn[OG_BTN_PLAY] = playing ? 255 : 40; want_btn[OG_BTN_STOP] = playing ? 40 : 120; want_btn[OG_BTN_SHIFT] = shift_held ? 255 : 30; want_btn[OG_BTN_UP] = want_btn[OG_BTN_DOWN] = 60; want_btn[OG_BTN_LEFT] = want_btn[OG_BTN_RIGHT] = 40; for (int v = 0; v < 4; v++) want_btn[OG_BTN_MODE_1 + v] = song.voice == v ? 255 : 30; want_btn[OG_BTN_BANK] = song.voice == V_CLAP ? 255 : 30; } int32_t og_render(void) { int changed = 0; if (knob_shown >= 0 && clock_samples - knob_time > (uint64_t)(sr * 1.5f)) { knob_shown = -1; dirty = 1; } compute_leds(); int n = n_pads() * 3, nb = OG_MAX_BUTTONS; for (int i = 0; i < n; i++) if (pad_leds[i] != want_pad[i]) { pad_leds[i] = want_pad[i]; changed = 1; } for (int i = 0; i < nb; i++) if (button_leds[i] != want_btn[i]) { button_leds[i] = want_btn[i]; changed = 1; } if (dirty && gfx.w > 0) { if (gfx.w >= 320) draw_big(); else draw_small(); changed = 1; } dirty = 0; return changed; } float *og_audio_out_ptr(void) { return audio_out; } float *og_audio_in_ptr(void) { return 0; } uint8_t *og_framebuffer_ptr(void) { return gfx.w > 0 ? framebuffer : 0; } uint8_t *og_pad_leds_ptr(void) { return pad_leds; } uint8_t *og_button_leds_ptr(void) { return button_leds; } /* ------------------------------------------------------------------ */ /* Parameters */ /* ------------------------------------------------------------------ */ uint32_t og_param_count(void) { return NPARAMS; } const og_param_info *og_param_info_ptr(uint32_t index) { return og_fw_param_info_at(params, NPARAMS, index); } void og_param_set(uint32_t id, float value) { if (og_fw_param_set(params, NPARAMS, id, value)) dirty = 1; } float og_param_get(uint32_t id) { return og_fw_param_get(params, NPARAMS, id); } /* ------------------------------------------------------------------ */ /* State: the pattern, the knobs and the selected voice/page */ /* ------------------------------------------------------------------ */ #define STATE_TAG OG_FOURCC('P', 'D', 'R', 'M') #define STATE_SCHEMA 1u uint32_t og_state_size(void) { return sizeof state_buf; } uint8_t *og_state_ptr(void) { return state_buf; } int32_t og_state_save(void) { return og_fw_state_write(state_buf, sizeof state_buf, STATE_TAG, STATE_SCHEMA, &song, sizeof song); } int32_t og_state_load(uint32_t len) { song_t b; int32_t rc = og_fw_state_read(state_buf, len > sizeof state_buf ? 0 : len, STATE_TAG, STATE_SCHEMA, &b, sizeof b); if (rc != OG_OK) return rc; for (int v = 0; v < NVOICES; v++) for (int s = 0; s < NSTEPS; s++) b.steps[v][s] &= 3; b.tempo = og_clampf(b.tempo, 60.0f, 180.0f); b.swing = og_clampf(b.swing, 0.5f, 0.75f); b.filter = og_clampf(b.filter, -1.0f, 1.0f); b.drive = og_clampf(b.drive, 0.0f, 1.0f); b.volume = og_clampf(b.volume, 0.0f, 1.0f); if (b.voice >= NVOICES) b.voice = 0; if (b.row_offset > max_row_offset()) b.row_offset = 0; if (b.page * steps_per_page() >= NSTEPS) b.page = 0; song = b; dirty = 1; return OG_OK; }