/* * Drift Arp: a generative arpeggiator over a four-chord progression. * SPDX-License-Identifier: Apache-2.0 * * A random-walk arpeggio of soft FM bells plays over a gliding saw pad, * through a small stereo reverb. The progression has four slots, one bar * each; pads choose diatonic 7th chords in D minor. * * Controls * pads choose a chord for the current slot (stopped: also strums it). * 4x4: bottom-left = i, rising by scale degree. * 4x16: columns = scale degree, rows = voicing * (top: open 5ths, then add9, triad+octave, bottom: 7th). * knobs RATE, DENSITY, SPREAD, SPACE * SHIFT+knobs TEMPO, TONE, PAD LEVEL, VOLUME * PLAY / STOP transport. LEFT/RIGHT or jog: select slot. UP/DOWN: key. * MIDI in a note sets the chord root (nearest scale degree). * * Params (ids 1-8): Rate, Density, Spread, Space, Tempo, Tone, Pad, Volume. * Sync: when the host is transport master (OG_CTX_SYNC) the arp and the * progression follow its tempo and beat position; PLAY/STOP belong to the host. */ #include "opengroove.h" #include "og_gfx.h" #include "og_dsp.h" #include "og_fw.h" #include "og/arena.h" /* toolkit: init-time memory, so the framebuffer fits the real display */ #define MAX_BLOCK 512 #define MAX_PADS 64 #define NSLOTS 4 #define NARP 6 #define NPAD 4 #define KEY_ROOT 50 /* D3 */ static const int scale[7] = { 0, 2, 3, 5, 7, 8, 10 }; static const char *const pc_names[12] = { "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B" }; static const float rate_beats[6] = { 1.0f, 0.5f, 1.0f / 3.0f, 0.25f, 1.0f / 6.0f, 0.125f }; static const char *const rate_names[6] = { "1/4", "1/8", "1/8T", "1/16", "1/16T", "1/32" }; typedef struct { uint8_t degree, voicing; } chord_t; /* degree 0..20, voicing 0..3 */ typedef struct { chord_t slots[NSLOTS]; float rate; /* 0..5 (index into rate_beats) */ float density, spread, space, tone, pad, volume; /* 0..1 */ float tempo; int8_t key; /* transpose -6..6 */ uint8_t slot; /* selected slot */ uint8_t _pad[2]; } 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, each block */ static float sr = 48000.0f; static uint32_t block = MAX_BLOCK; static int active; static song_t song; static float audio_out[2 * MAX_BLOCK]; static uint8_t *framebuffer; /* display_w * display_h * 4 bytes from og_arena in og_init */ static uint8_t pad_leds[MAX_PADS * 3]; static uint8_t button_leds[OG_MAX_BUTTONS]; static uint8_t state_buf[256]; static og_gfx gfx; static og_rng rng = { 0xC0FFEE11u }; static int playing, cur_slot, shift_held, dirty = 1, anim; static double beat_pos; static long last_arp_step = -1, last_bar = -1; static uint64_t clock_samples, last_anim_draw, knob_time; static int knob_shown = -1; static int pressed_pad = -1; /* host transport */ static og_fw_sync clk; static int sync_mode; /* ------------------------------------------------------------------ */ /* Parameters: the knobs (and SHIFT layer), in user units */ /* ------------------------------------------------------------------ */ enum { P_RATE = 1, P_DENSITY, P_SPREAD, P_SPACE, P_TEMPO, P_TONE, P_PAD, P_VOLUME }; #define AUTO OG_PARAM_AUTOMATABLE static const char rate_labels[6][OG_PARAM_LABEL_LEN] = { "1/4", "1/8", "1/8T", "1/16", "1/16T", "1/32" }; static const og_fw_param params[] = { /* Rate is an index into 1/4, 1/8, 1/8T, 1/16, 1/16T, 1/32 */ OG_FW_PARAM_EX(P_RATE, "Rate", "", 0.0f, 5.0f, 3.0f, AUTO | OG_PARAM_STEPPED, &song.rate, 1.0f, 1.0f, OG_CURVE_LINEAR, rate_labels, 6), OG_FW_PARAM(P_DENSITY, "Density", "%", 0.0f, 100.0f, 72.0f, AUTO, &song.density, 0.01f), OG_FW_PARAM(P_SPREAD, "Spread", "%", 0.0f, 100.0f, 50.0f, AUTO, &song.spread, 0.01f), OG_FW_PARAM(P_SPACE, "Space", "%", 0.0f, 100.0f, 45.0f, AUTO, &song.space, 0.01f), OG_FW_PARAM(P_TEMPO, "Tempo", "BPM", 60.0f, 180.0f, 96.0f, AUTO, &song.tempo, 1.0f), OG_FW_PARAM(P_TONE, "Tone", "%", 0.0f, 100.0f, 45.0f, AUTO, &song.tone, 0.01f), OG_FW_PARAM(P_PAD, "Pad", "%", 0.0f, 100.0f, 50.0f, AUTO, &song.pad, 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; } /* chord currently sounding */ static int chord_notes[4], chord_n; static int walk = 0; static float pc_glow[12]; /* per pitch class: recent-note glow, for the screen */ static int last_note = -1; /* strum queue (stopped auditions) */ static int strum_left, strum_timer, strum_idx; /* ------------------------------------------------------------------ */ /* Harmony */ /* ------------------------------------------------------------------ */ static int degree_semis(int d) { return scale[d % 7] + 12 * (d / 7); } /* Build a chord: MIDI notes in ascending order. */ static int build_chord(chord_t c, int *out) { int d = c.degree, base = KEY_ROOT + song.key; static const int stacks[4][4] = { { 0, 4, 7, -1 }, { 0, 1, 2, 4 }, { 0, 2, 4, 7 }, { 0, 2, 4, 6 } }; int n = 0; for (int i = 0; i < 4; i++) { int s = stacks[c.voicing & 3][i]; if (s < 0) break; out[n++] = base + degree_semis(d + s) - (d >= 7 ? 12 : 0); } /* order */ for (int i = 1; i < n; i++) for (int j = i; j > 0 && out[j] < out[j - 1]; j--) { int t = out[j]; out[j] = out[j - 1]; out[j - 1] = t; } return n; } static void chord_name(chord_t c, char *b, int cap) { int notes[4], n = build_chord(c, notes); (void)n; int root = notes[0]; /* every voicing stacks upward from the root */ og_strcpy(b, pc_names[((root % 12) + 12) % 12], cap); int fifth = KEY_ROOT + song.key + degree_semis(c.degree + 4) - (c.degree >= 7 ? 12 : 0) - root; fifth = ((fifth % 12) + 12) % 12; int t3 = ((KEY_ROOT + song.key + degree_semis(c.degree + 2) - (c.degree >= 7 ? 12 : 0) - root) % 12 + 12) % 12; int t7 = ((KEY_ROOT + song.key + degree_semis(c.degree + 6) - (c.degree >= 7 ? 12 : 0) - root) % 12 + 12) % 12; int minor = t3 == 3, dim = fifth == 6; switch (c.voicing & 3) { case 0: og_strcat(b, dim ? "(b5)" : "5", cap); break; case 1: og_strcat(b, minor ? (dim ? "dim" : "m") : "", cap); og_strcat(b, "add9", cap); break; case 2: og_strcat(b, dim ? "dim" : (minor ? "m" : ""), cap); break; default: if (dim) og_strcat(b, "m7b5", cap); else if (minor) og_strcat(b, "m7", cap); else og_strcat(b, t7 == 11 ? "maj7" : "7", cap); } } /* ------------------------------------------------------------------ */ /* Voices */ /* ------------------------------------------------------------------ */ typedef struct { float pc, pm, inc, amp, idx, adec, idec, pan; int att; } bell_t; static bell_t bells[NARP]; static int bell_next; typedef struct { float ph[2], cur, target; } padv_t; static padv_t padv[NPAD]; static float pad_amp, pad_gate; static og_svf pad_f[2]; static void bell(int note, float vel) { bell_t *b = &bells[bell_next]; bell_next = (bell_next + 1) % NARP; float f = og_mtof((float)note); b->inc = f / sr; b->pc = 0.0f; b->pm = 0.0f; b->amp = og_clampf(vel, 0.0f, 1.0f); b->idx = 1.2f + 1.6f * vel; float len = 0.35f + 1.4f * song.tone; b->adec = og_decay_coef(len, sr); b->idec = og_decay_coef(len * 0.35f, sr); b->pan = 0.5f + (og_randf(&rng) - 0.5f) * 0.7f; b->att = (int)(sr * 0.002f) + 1; last_note = note; pc_glow[((note % 12) + 12) % 12] = 1.0f; anim = 1; } static void set_chord(chord_t c) { chord_n = build_chord(c, chord_notes); for (int i = 0; i < NPAD; i++) { int n = chord_notes[i < chord_n ? i : chord_n - 1] - 12; padv[i].target = og_mtof((float)n); if (padv[i].cur <= 0.0f) padv[i].cur = padv[i].target; } dirty = 1; } /* ------------------------------------------------------------------ */ /* Reverb (Freeverb-style: 4 combs + 2 allpasses per channel) */ /* ------------------------------------------------------------------ */ #define CB 4096 #define AB 2048 static float comb_buf[8][CB], comb_lp[8]; static int comb_len[8], comb_pos[8]; static float ap_buf[4][AB]; static int ap_len[4], ap_pos[4]; static void reverb_init(void) { static const int cl[4] = { 1116, 1188, 1277, 1356 }; static const int al[2] = { 556, 441 }; float k = sr / 44100.0f; for (int i = 0; i < 8; i++) { int l = (int)((float)(cl[i & 3] + (i >= 4 ? 23 : 0)) * k * 1.3f); comb_len[i] = og_clampi(l, 16, CB - 1); } for (int i = 0; i < 4; i++) ap_len[i] = og_clampi((int)((float)(al[i & 1] + (i >= 2 ? 23 : 0)) * k), 16, AB - 1); } static void reverb_tick(float in, float *l, float *r) { float out[2] = { 0, 0 }; for (int c = 0; c < 8; c++) { float y = comb_buf[c][comb_pos[c]]; comb_lp[c] = y * 0.6f + comb_lp[c] * 0.4f + OG_DENORM; comb_buf[c][comb_pos[c]] = in + comb_lp[c] * 0.86f; if (++comb_pos[c] >= comb_len[c]) comb_pos[c] = 0; out[c >> 2] += y; } for (int ch = 0; ch < 2; ch++) { float x = out[ch] * 0.25f; for (int a = 0; a < 2; a++) { int i = ch * 2 + a; float bv = ap_buf[i][ap_pos[i]]; ap_buf[i][ap_pos[i]] = x + bv * 0.5f; x = bv - x; if (++ap_pos[i] >= ap_len[i]) ap_pos[i] = 0; } out[ch] = x; } *l = out[0]; *r = out[1]; } /* ------------------------------------------------------------------ */ /* Generative arp */ /* ------------------------------------------------------------------ */ static void arp_step(long n) { if (chord_n <= 0) return; float r = og_randf(&rng); int downbeat = (n % 4) == 0; if (!downbeat && r > song.density) return; int octs = 1 + (int)(song.spread * 2.99f); int range = chord_n * octs; /* random walk: mostly steps, sometimes leaps */ float m = og_randf(&rng); int move = m < 0.4f ? 1 : (m < 0.7f ? -1 : (m < 0.85f ? 2 : -2)); walk += move; if (walk < 0) walk = -walk; if (walk >= range) walk = 2 * (range - 1) - walk; walk = og_clampi(walk, 0, range - 1); int note = chord_notes[walk % chord_n] + 12 * (walk / chord_n); float vel = downbeat ? 0.9f : 0.45f + 0.4f * og_randf(&rng); bell(note, vel); } static void begin_strum(void) { strum_left = chord_n; strum_idx = 0; strum_timer = 0; } /* ------------------------------------------------------------------ */ /* Input */ /* ------------------------------------------------------------------ */ static int n_pads(void) { int n = dev.pad_rows * dev.pad_cols; return n > MAX_PADS ? MAX_PADS : n; } static int lanes_layout(void) { return dev.pad_cols >= 16 && dev.pad_rows >= 2; } static chord_t pad_chord(int id) { chord_t c; int cols = dev.pad_cols ? dev.pad_cols : 1; int row = id / cols, col = id % cols; if (lanes_layout()) { c.degree = (uint8_t)og_clampi(col, 0, 20); c.voicing = (uint8_t)(3 - og_clampi(dev.pad_rows - 1 - row, 0, 3)); /* bottom row = 7ths */ } else { c.degree = (uint8_t)og_clampi((dev.pad_rows - 1 - row) * cols + col, 0, 20); c.voicing = 3; } return c; } static void pad_down(int id) { if (id < 0 || id >= n_pads()) return; chord_t c = pad_chord(id); int slot = playing ? cur_slot : song.slot; song.slots[slot] = c; set_chord(c); pad_gate = 1.0f; pressed_pad = id; if (!playing) begin_strum(); dirty = 1; } static void knob(int id, float d) { int k = id + (shift_held ? 4 : 0); if (dev.knob_kind == OG_KNOB_ABSOLUTE) { /* a pot: d is the position 0..1 */ if (id < 0 || id > 3) return; og_fw_param_set_norm(og_fw_param_find(params, NPARAMS, (uint32_t)(k + 1)), d); knob_shown = k; knob_time = clock_samples; dirty = 1; return; } d = og_clampf(d, -64.0f, 64.0f); switch (k) { case 0: song.rate = og_clampf(song.rate + d * 0.1f, 0.0f, 5.0f); break; case 1: song.density = og_clampf(song.density + d * 0.01f, 0.0f, 1.0f); break; case 2: song.spread = og_clampf(song.spread + d * 0.01f, 0.0f, 1.0f); break; case 3: song.space = og_clampf(song.space + d * 0.01f, 0.0f, 1.0f); break; case 4: song.tempo = og_clampf(song.tempo + d, 60.0f, 180.0f); break; case 5: song.tone = og_clampf(song.tone + d * 0.01f, 0.0f, 1.0f); break; case 6: song.pad = og_clampf(song.pad + d * 0.01f, 0.0f, 1.0f); break; case 7: song.volume = og_clampf(song.volume + d * 0.01f, 0.0f, 1.0f); break; default: return; } knob_shown = k; knob_time = clock_samples; dirty = 1; } static void select_slot(int dir) { song.slot = (uint8_t)((song.slot + NSLOTS + dir) % NSLOTS); if (!playing) set_chord(song.slots[song.slot]); dirty = 1; } static void set_playing(int on) { if (on && !playing) { playing = 1; beat_pos = 0.0; last_arp_step = -1; last_bar = -1; walk = 0; pad_gate = 1.0f; } else if (!on && playing) { playing = 0; pad_gate = 0.0f; } 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); pad_gate = 0.0f; break; case OG_BTN_LEFT: select_slot(-1); break; case OG_BTN_RIGHT: select_slot(1); break; case OG_BTN_UP: song.key = (int8_t)og_clampi(song.key + 1, -6, 6); set_chord(song.slots[playing ? cur_slot : song.slot]); break; case OG_BTN_DOWN: song.key = (int8_t)og_clampi(song.key - 1, -6, 6); set_chord(song.slots[playing ? cur_slot : song.slot]); break; } } static void touch(float x, float y) { if (dev.display_w < 320) return; int w = dev.display_w, h = dev.display_h; if (y < h * 14 / 100) { if (!sync_mode) set_playing(!playing); return; } /* progression slots */ int sx0 = w * 47 / 100, sy = h * 42 / 100, sw = (w - sx0 - 24) / NSLOTS, sh = h * 15 / 100; if (y >= sy && y < sy + sh && x >= sx0) { int s = ((int)x - sx0) / (sw ? sw : 1); if (s >= 0 && s < NSLOTS) { song.slot = (uint8_t)s; if (!playing) { set_chord(song.slots[s]); begin_strum(); } dirty = 1; } } } static void midi(uint8_t st, uint8_t d1, uint8_t d2) { uint8_t t = st & 0xF0; if (st == 0xFA) { if (!sync_mode) set_playing(1); return; } if (st == 0xFC) { if (!sync_mode) set_playing(0); return; } if (t != 0x90 || !d2) return; int rel = ((int)d1 - (KEY_ROOT + song.key)) % 12; if (rel < 0) rel += 12; int deg = 0; for (int i = 0; i < 7; i++) if (scale[i] <= rel) deg = i; chord_t c = { (uint8_t)deg, 3 }; int slot = playing ? cur_slot : song.slot; song.slots[slot] = c; set_chord(c); pad_gate = 1.0f; if (!playing) begin_strum(); } /* ------------------------------------------------------------------ */ /* Event queue */ /* ------------------------------------------------------------------ */ typedef struct { uint32_t type, id, offset; float value, x, y; } ev_t; #define EVQ 256 #define EV_MIDI 1000u 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++; 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; } static void apply_ev(const ev_t *e) { switch (e->type) { case OG_PAD_DOWN: pad_down((int)e->id); break; case OG_PAD_UP: if ((int)e->id == pressed_pad) { pressed_pad = -1; dirty = 1; } 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: if (e->value != 0.0f) select_slot(e->value > 0 ? 1 : -1); 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) { /* i7 - VImaj7 - IIImaj7 - VII7 : Dm7, Bbmaj7, Fmaj7, C7 */ static const uint8_t prog[NSLOTS] = { 0, 5, 2, 6 }; for (int i = 0; i < NSLOTS; i++) { song.slots[i].degree = prog[i]; song.slots[i].voicing = 3; } og_fw_params_reset(params, NPARAMS); /* the knobs: defaults from the table */ song.key = 0; song.slot = 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; } if (dev.display_w && dev.display_h && !framebuffer) { og_arena arena; og_arena_init(&arena); framebuffer = og_arena_alloc(&arena, (uint32_t)dev.display_w * dev.display_h * 4, 4); if (!framebuffer) { dev.display_w = 0; dev.display_h = 0; } } og_gfx_init(&gfx, dev.display_w ? framebuffer : 0, dev.display_w, dev.display_h); default_song(); set_chord(song.slots[0]); return 0; } void og_reset(void) { evn = 0; /* queues clear on reset */ __builtin_memset(bells, 0, sizeof bells); __builtin_memset(padv, 0, sizeof padv); __builtin_memset(pad_f, 0, sizeof pad_f); __builtin_memset(comb_buf, 0, sizeof comb_buf); __builtin_memset(comb_lp, 0, sizeof comb_lp); __builtin_memset(ap_buf, 0, sizeof ap_buf); pad_amp = 0.0f; pad_gate = playing ? 1.0f : 0.0f; strum_left = 0; set_chord(song.slots[playing ? cur_slot : song.slot]); } 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; reverb_init(); 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; 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); } /* Values derived from params. Recomputed at the block start and after any * event, so OG_PARAM_CHANGE takes effect exactly at its offset. */ static float beat_inc, div, glide, pad_coef, gain, wet; static void derive(int sync) { beat_inc = sync ? (float)clk.inc : og_clampf(song.tempo, 60.0f, 180.0f) / 60.0f / sr; div = rate_beats[og_clampi((int)(song.rate + 0.5f), 0, 5)]; float pad_fc = 300.0f * og_exp2(song.tone * 3.5f); og_svf_set(&pad_f[0], pad_fc, 0.8f, sr); og_svf_set(&pad_f[1], pad_fc * 1.1f, 0.8f, sr); glide = 1.0f - og_expf(-1.0f / (0.12f * sr)); pad_coef = (pad_gate > pad_amp ? 1.0f / (0.6f * sr) : 1.0f / (1.5f * sr)); gain = song.volume * song.volume * 1.9f; wet = song.space * 0.9f; } 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 */ /* Host transport: in sync mode it decides playing, tempo and beat position. */ int sync = og_fw_sync_begin(&clk, &ctx, sr, song.tempo); if (sync != sync_mode) { sync_mode = sync; if (!sync) set_playing(0); dirty = 1; } if (sync) { if (clk.playing != playing) set_playing(clk.playing); if (playing) beat_pos = clk.beat; } derive(sync); int e = 0; for (uint32_t i = 0; i < frames; i++) { if (e < evn && (evq[e].offset <= i || i + 1 == frames)) { while (e < evn && (evq[e].offset <= i || i + 1 == frames)) apply_ev(&evq[e++]); derive(sync); } if (playing) { long bar = (long)(beat_pos / 4.0); if (bar != last_bar) { last_bar = bar; cur_slot = (int)(bar % NSLOTS); set_chord(song.slots[cur_slot]); } long n = (long)(beat_pos / div); if (n != last_arp_step) { last_arp_step = n; arp_step(n); } beat_pos += beat_inc; } if (strum_left > 0 && --strum_timer <= 0) { bell(chord_notes[strum_idx % (chord_n ? chord_n : 1)] + 12, 0.75f); strum_idx++; strum_left--; strum_timer = (int)(sr * 0.045f); if (strum_left == 0 && !playing) pad_gate = 0.0f; /* pad swells briefly */ } /* bells: 2-op FM */ float bl = 0.0f, br = 0.0f; for (int v = 0; v < NARP; v++) { bell_t *b = &bells[v]; if (b->amp < 1e-4f) continue; float a = b->amp; if (b->att > 0) { a *= 1.0f - (float)b->att / (sr * 0.002f + 1.0f); b->att--; } b->pm += b->inc * 2.0f; if (b->pm >= 1.0f) b->pm -= 1.0f; b->pc += b->inc; if (b->pc >= 1.0f) b->pc -= 1.0f; float s = og_sin_cycles(b->pc + b->idx * 0.16f * og_sin_cycles(b->pm)) * a; b->amp *= b->adec; b->idx *= b->idec; bl += s * (1.0f - b->pan); br += s * b->pan; } /* pad: two detuned saws per chord tone, gliding between chords */ pad_amp += (pad_gate - pad_amp) * pad_coef * 4.0f; float pl = 0.0f, pr = 0.0f; if (pad_amp > 1e-4f) { for (int v = 0; v < NPAD; v++) { padv_t *p = &padv[v]; p->cur += (p->target - p->cur) * glide; for (int k = 0; k < 2; k++) { float dt = p->cur * (k ? 1.0035f : 0.9965f) / sr; p->ph[k] += dt; if (p->ph[k] >= 1.0f) p->ph[k] -= 1.0f; float saw = 2.0f * p->ph[k] - 1.0f - og_polyblep(p->ph[k], dt); if (k) pr += saw; else pl += saw; } } og_svf_tick(&pad_f[0], pl * 0.12f); og_svf_tick(&pad_f[1], pr * 0.12f); pl = pad_f[0].lp * pad_amp * song.pad; pr = pad_f[1].lp * pad_amp * song.pad; } float dl = bl * 0.5f + pl * 1.3f, dr = br * 0.5f + pr * 1.3f; float rl, rr; reverb_tick((dl + dr) * 0.12f, &rl, &rr); float l = (dl + rl * wet) * gain, r = (dr + rr * wet) * 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; if (sync) clk.beat = beat_pos; /* so the next block can tell a jump from continuity */ /* glow decay for the screen */ float g = og_exp2(-(float)frames / (sr * 0.12f)); for (int i = 0; i < 12; i++) { if (pc_glow[i] > 0.01f) { pc_glow[i] *= g; anim = 1; } else pc_glow[i] = 0.0f; } } /* ------------------------------------------------------------------ */ /* Drawing */ /* ------------------------------------------------------------------ */ static const char *const knob_names[8] = { "RATE", "DENSITY", "SPREAD", "SPACE", "TEMPO", "TONE", "PAD", "VOLUME" }; static float knob_val(int k) { switch (k) { case 0: return song.rate / 5.0f; case 1: return song.density; case 2: return song.spread; case 3: return song.space; case 4: return og_clampf((cur_tempo() - 60.0f) / 120.0f, 0.0f, 1.0f); case 5: return song.tone; case 6: return song.pad; default: return song.volume; } } static void knob_text(int k, char *b, int cap) { og_strcpy(b, "", cap); if (k == 0) og_strcpy(b, rate_names[og_clampi((int)(song.rate + 0.5f), 0, 5)], cap); else if (k == 2) { og_strcat_int(b, 1 + (int)(song.spread * 2.99f), cap); og_strcat(b, " OCT", cap); } else if (k == 4) og_strcat_int(b, (int)(cur_tempo() + 0.5f), cap); else og_strcat_int(b, (int)(knob_val(k) * 100.0f + 0.5f), cap); } static int chord_has_pc(int pc) { for (int i = 0; i < chord_n; i++) if (((chord_notes[i] % 12) + 12) % 12 == pc) return 1; return 0; } static void draw_big(void) { int w = dev.display_w, h = dev.display_h; og_color bg = og_hex(0x0F0D16), panel = og_hex(0x181522), text = og_hex(0xEEEAF8), muted = og_hex(0x77708C); og_color lav = og_hex(0xB49CFF), teal = og_hex(0x5EE6C8); og_clear(&gfx, bg); char b[32]; int hh = h * 12 / 100, ts = h >= 400 ? 4 : 2, ty = (hh - 7 * ts) / 2; og_fill_rect(&gfx, 0, 0, w, hh, panel); int x = og_text(&gfx, 24, ty, "DRIFT", ts, text); og_text(&gfx, x + 3 * ts, ty, "ARP", ts, lav); 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) - 12 * ts; if (playing) for (int i = 0; i < 7 * ts; i++) og_vline(&gfx, px + i, ty + i / 2, 7 * ts - i, lav); else og_fill_rect(&gfx, px, ty, 7 * ts, 7 * ts, muted); if (sync_mode) { int sx = px - og_text_width("SYNC", 2) - 36; og_fill_round_rect(&gfx, sx, ty - 2, og_text_width("SYNC", 2) + 20, 7 * 2 + 8, 6, og_hex(0x1E3A36)); og_text(&gfx, sx + 10, ty + 2, "SYNC", 2, teal); } /* pitch-class orbit */ int cx = w * 22 / 100, cy = h * 42 / 100, R = h * 19 / 100; og_circle(&gfx, cx, cy, R, og_hex(0x2A2540)); og_circle(&gfx, cx, cy, R - 1, og_hex(0x2A2540)); int lx = 0, ly = 0, first = 1, fx = 0, fy = 0; for (int k = 0; k < chord_n; k++) { /* connect chord tones as a polygon */ int pc = ((chord_notes[k] % 12) + 12) % 12; float a = (float)pc / 12.0f - 0.25f; int px2 = cx + (int)(og_cos_cycles(a) * (float)R), py2 = cy + (int)(og_sin_cycles(a) * (float)R); if (!first) og_line_thick(&gfx, lx, ly, px2, py2, 2, og_hex(0x4A3F7A)); else { fx = px2; fy = py2; first = 0; } lx = px2; ly = py2; } if (chord_n > 2) og_line_thick(&gfx, lx, ly, fx, fy, 2, og_hex(0x4A3F7A)); for (int pc = 0; pc < 12; pc++) { float a = (float)pc / 12.0f - 0.25f; int px2 = cx + (int)(og_cos_cycles(a) * (float)R), py2 = cy + (int)(og_sin_cycles(a) * (float)R); int in = chord_has_pc(pc); int glow = (int)(pc_glow[pc] * 255.0f); if (glow > 8) og_fill_circle(&gfx, px2, py2, 12 + glow / 18, og_mix(bg, teal, glow / 2)); og_fill_circle(&gfx, px2, py2, in ? 12 : 5, in ? og_mix(lav, og_rgb(255, 255, 255), glow) : og_hex(0x3A3450)); int tx = cx + (int)(og_cos_cycles(a) * (float)(R + 28)), tyy = cy + (int)(og_sin_cycles(a) * (float)(R + 26)); og_text_center(&gfx, tx, tyy - 7, pc_names[pc], 2, in ? text : muted); } /* chord name and progression */ int rx = w * 47 / 100; chord_t now = song.slots[playing ? cur_slot : song.slot]; chord_name(now, b, sizeof b); og_text(&gfx, rx, h * 19 / 100, b, h >= 400 ? 7 : 3, text); og_text(&gfx, rx, h * 33 / 100, playing ? "NOW PLAYING" : "PADS SET THE SELECTED SLOT", 2, muted); int sy = h * 42 / 100, sw = (w - rx - 24) / NSLOTS, sh = h * 15 / 100; for (int s = 0; s < NSLOTS; s++) { int sx = rx + s * sw; int active = playing ? s == cur_slot : s == song.slot; og_fill_round_rect(&gfx, sx, sy, sw - 10, sh, 10, active ? og_hex(0x2E2650) : og_hex(0x1B1828)); if (s == song.slot) og_round_rect(&gfx, sx, sy, sw - 10, sh, 10, lav); chord_name(song.slots[s], b, sizeof b); og_text_center(&gfx, sx + (sw - 10) / 2, sy + sh / 2 - 7, b, 2, active ? text : muted); if (playing && s == cur_slot) { float bar_frac = (float)(beat_pos / 4.0) - og_floorf((float)(beat_pos / 4.0)); og_fill_rect(&gfx, sx + 8, sy + sh - 8, (int)((float)(sw - 26) * bar_frac), 3, teal); } } /* knobs */ int ky = h * 78 / 100, kr = h * 6 / 100; for (int i = 0; i < 4; i++) { int k = i + (shift_held ? 4 : 0); int kx = 24 + i * (w - 48) / 4; float v = knob_val(k); for (int a = 0; a <= 270; a += 3) { float ang = (135.0f + (float)a) / 360.0f; og_fill_circle(&gfx, kx + kr + (int)(og_cos_cycles(ang) * (float)kr), ky + (int)(og_sin_cycles(ang) * (float)kr), 3, (float)a <= v * 270.0f ? (k >= 4 ? teal : lav) : og_hex(0x2A2540)); } og_text(&gfx, kx + 2 * kr + 16, ky - 22, knob_names[k], 2, muted); knob_text(k, b, sizeof b); og_text(&gfx, kx + 2 * kr + 16, ky - 2, b, 3, text); } og_text_center(&gfx, w / 2, h - 24, "PADS CHORDS L/R SLOT UP/DN KEY SHIFT: MORE KNOBS", 2, muted); } 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, "DRIFT", 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_text_center(&gfx, w / 2, 0, rate_names[og_clampi((int)(song.rate + 0.5f), 0, 5)], 1, on); chord_name(song.slots[playing ? cur_slot : song.slot], b, sizeof b); og_text(&gfx, 0, 12, b, 2, on); /* last note glow as a bar of 12 pitch classes */ for (int pc = 0; pc < 12; pc++) { int x = 72 + pc * 5; if (chord_has_pc(pc)) og_fill_rect(&gfx, x, 12, 4, 4, on); else og_pixel(&gfx, x + 1, 14, on); int gh = (int)(pc_glow[pc] * 10.0f); if (gh > 0) og_fill_rect(&gfx, x, 26 - gh, 4, gh, on); } int sw = w / NSLOTS; for (int s = 0; s < NSLOTS; s++) { int active = playing ? s == cur_slot : s == song.slot; chord_name(song.slots[s], b, sizeof b); if (og_strlen(b) > 5) b[5] = 0; if (active) { og_fill_rect(&gfx, s * sw, 31, sw - 2, 11, on); og_text(&gfx, s * sw + 1, 33, b, 1, off); } else { og_stroke_rect(&gfx, s * sw, 31, sw - 2, 11, s == song.slot ? on : og_rgb(90, 90, 90)); og_text(&gfx, s * sw + 1, 33, b, 1, on); } } int k = knob_shown >= 0 ? knob_shown : 1; og_strcpy(b, knob_names[k], sizeof b); og_strcat(b, " ", sizeof b); char v[12]; knob_text(k, v, sizeof v); og_strcat(b, v, sizeof b); og_text(&gfx, 0, h - 9, b, 1, on); og_stroke_rect(&gfx, 82, h - 9, 46, 7, on); og_fill_rect(&gfx, 83, h - 8, (int)(knob_val(k) * 44.0f), 5, on); } static uint8_t want_pad[MAX_PADS * 3], want_btn[OG_MAX_BUTTONS]; static void compute_leds(void) { int n = n_pads(); chord_t now = song.slots[playing ? cur_slot : song.slot]; for (int i = 0; i < n; i++) { chord_t c = pad_chord(i); int r, g, b; if (i == pressed_pad) { r = 255; g = 255; b = 255; } else if (c.degree == now.degree && c.voicing == now.voicing) { r = 180; g = 156; b = 255; } else if (c.degree % 7 == 0) { r = 40; g = 30; b = 80; } else { r = 14; g = 10; b = 30; } /* flash the pad whose chord root just rang */ int pc = ((KEY_ROOT + song.key + degree_semis(c.degree)) % 12 + 12) % 12; int gl = (int)(pc_glow[pc] * 120.0f); if (c.voicing == now.voicing || !lanes_layout()) { g = og_clampi(g + gl, 0, 255); b = og_clampi(b + gl / 2, 0, 255); } want_pad[i * 3] = (uint8_t)r; want_pad[i * 3 + 1] = (uint8_t)g; want_pad[i * 3 + 2] = (uint8_t)b; } 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] = 60; want_btn[OG_BTN_SHIFT] = shift_held ? 255 : 30; want_btn[OG_BTN_LEFT] = want_btn[OG_BTN_RIGHT] = 80; want_btn[OG_BTN_UP] = want_btn[OG_BTN_DOWN] = 50; } 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; } if ((anim || playing) && clock_samples - last_anim_draw > (uint64_t)(sr / 30.0f)) 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(); last_anim_draw = clock_samples; anim = 0; 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 progression and the sound */ /* ------------------------------------------------------------------ */ #define STATE_TAG OG_FOURCC('D', 'A', 'R', 'P') #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, *s = &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 i = 0; i < NSLOTS; i++) { s->slots[i].degree = (uint8_t)og_clampi(s->slots[i].degree, 0, 20); s->slots[i].voicing &= 3; } s->rate = og_clampf(s->rate, 0, 5); s->density = og_clampf(s->density, 0, 1); s->spread = og_clampf(s->spread, 0, 1); s->space = og_clampf(s->space, 0, 1); s->tone = og_clampf(s->tone, 0, 1); s->pad = og_clampf(s->pad, 0, 1); s->volume = og_clampf(s->volume, 0, 1); s->tempo = og_clampf(s->tempo, 60, 180); s->key = (int8_t)og_clampi(s->key, -6, 6); s->slot %= NSLOTS; song = b; set_chord(song.slots[playing ? cur_slot : song.slot]); dirty = 1; return OG_OK; }