/* * Acid Line: a 303-style bass line for OpenGroove. * SPDX-License-Identifier: Apache-2.0 * * One band-limited saw/square oscillator into a resonant 4-pole ladder with * a saturating feedback path, accent "wow", slide, a 16-step sequencer and a * ping-pong delay. Press PLAY: the default line starts squelching. * * Controls * knobs CUTOFF, RESONANCE, ENV MOD, DECAY * SHIFT+knobs TEMPO, ACCENT, DELAY, DRIVE * PLAY / STOP transport. REC: toggle EDIT mode. SHIFT+REC or SELECT: saw/square * 4x4, PLAY pads are a keyboard in A minor (bottom-left = root). Hold two * pads for a slide. UP/DOWN transpose (SHIFT: octave). * 4x4, EDIT pads are the 16 steps. Tap = select + hear, tap again = rest. * UP/DOWN (or jog) note, SHIFT+UP/DOWN octave, LEFT accent, RIGHT slide. * 4x16 row 1 steps (on/off + select), row 2 accents, row 3 slides, * row 4 keyboard. In EDIT, a key writes the note into the * selected step and moves to the next one. * MIDI in notes play (legato = slide), CC74 cutoff, CC71 resonance. * * Params (ids 1-8): Cutoff, Resonance, Env Mod, Decay, Tempo, Accent, Delay, Drive. * 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_fw.h" #include "og/arena.h" /* toolkit: init-time memory, so the framebuffer fits the real display */ #define MAX_BLOCK 512 #define NSTEPS 16 #define MAX_PADS 64 #define ROOT 33 /* A1 */ #define DLY_LEN 144000 /* 1.5 s at 96 kHz */ typedef struct { int8_t note; /* semitones above ROOT, -12..36 */ uint8_t gate, accent, slide; } step_t; typedef struct { step_t steps[NSTEPS]; float cutoff, reso, envmod, decay; /* 0..1 */ float accent, delay, drive; /* 0..1 */ float tempo; /* 60..180 */ int8_t transpose; /* -12..12 */ uint8_t wave; /* 0 saw, 1 square */ 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[512]; static float dly_l[DLY_LEN], dly_r[DLY_LEN]; static og_gfx gfx; /* transport + UI */ static int playing, step = -1, steps_played; static double to_next; static int shift_held, edit_mode, cursor; static int dirty = 1, dirty_curve; static uint64_t clock_samples, last_curve_draw; static int knob_shown = -1; static uint64_t knob_time; /* voice */ static float phase, cur_note = ROOT, target_note = ROOT, glide_coef; static int gate, gate_timer = -1; /* samples until gate off, -1 = hold */ static float amp, fenv, fdec, acc_now, acc_cv; static float lad[4]; static og_onepole dc_l, dc_r, damp_l, damp_r; static int dly_w; static int sounding = -1; /* note shown on screen */ /* host transport */ static og_fw_sync clk; static int sync_mode; /* ------------------------------------------------------------------ */ /* Parameters: the knobs (and SHIFT layer), in user units */ /* ------------------------------------------------------------------ */ enum { P_CUTOFF = 1, P_RESO, P_ENVMOD, P_DECAY, P_TEMPO, P_ACCENT, P_DELAY, P_DRIVE }; #define AUTO OG_PARAM_AUTOMATABLE static const og_fw_param params[] = { OG_FW_PARAM(P_CUTOFF, "Cutoff", "%", 0.0f, 100.0f, 30.0f, AUTO, &song.cutoff, 0.01f), OG_FW_PARAM(P_RESO, "Resonance", "%", 0.0f, 100.0f, 78.0f, AUTO, &song.reso, 0.01f), OG_FW_PARAM(P_ENVMOD, "Env Mod", "%", 0.0f, 100.0f, 55.0f, AUTO, &song.envmod, 0.01f), OG_FW_PARAM(P_DECAY, "Decay", "%", 0.0f, 100.0f, 35.0f, AUTO, &song.decay, 0.01f), OG_FW_PARAM(P_TEMPO, "Tempo", "BPM", 60.0f, 180.0f, 126.0f, AUTO, &song.tempo, 1.0f), OG_FW_PARAM(P_ACCENT, "Accent", "%", 0.0f, 100.0f, 75.0f, AUTO, &song.accent, 0.01f), OG_FW_PARAM(P_DELAY, "Delay", "%", 0.0f, 100.0f, 28.0f, AUTO, &song.delay, 0.01f), OG_FW_PARAM(P_DRIVE, "Drive", "%", 0.0f, 100.0f, 35.0f, AUTO, &song.drive, 0.01f), }; #define NPARAMS (sizeof params / sizeof params[0]) static float cur_tempo(void) { return sync_mode ? (float)clk.bpm : song.tempo; } /* live keyboard: last-note priority */ static int held[8], nheld; static const int scale[7] = { 0, 2, 3, 5, 7, 8, 10 }; /* natural minor */ static const char *const note_names[12] = { "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#" }; /* ------------------------------------------------------------------ */ /* Voice */ /* ------------------------------------------------------------------ */ static void note_on(int note, int accent, int slide) { target_note = (float)og_clampi(note, 12, 96); if (!(slide && gate)) { cur_note = target_note; /* jump and retrigger */ fenv = 1.0f; acc_now = accent ? song.accent : 0.0f; fdec = og_decay_coef(accent ? 0.2f : 0.08f * og_exp2(song.decay * 4.7f), sr); if (accent) acc_cv += (1.0f - acc_cv) * 0.55f; } gate = 1; gate_timer = -1; sounding = note; dirty = 1; } static void note_off(void) { gate = 0; gate_timer = -1; } /* 4-pole ladder, zero-delay feedback solved linearly, saturated input stage. */ static float ladder(float x, float fc, float k) { float g = og_tanf(OG_PI * fc / sr); float b = 1.0f / (1.0f + g), G = g * b, G2 = G * G, G4 = G2 * G2; float S = G2 * G * b * lad[0] + G2 * b * lad[1] + G * b * lad[2] + b * lad[3]; float y4 = (G4 * x + S) / (1.0f + k * G4); float u = og_sat(x - k * y4); for (int i = 0; i < 4; i++) { float v = (u - lad[i]) * G; float y = v + lad[i]; lad[i] = y + v + OG_DENORM; u = y; } return u; } static float voice_tick(void) { cur_note += (target_note - cur_note) * glide_coef; float f = og_mtof(cur_note), dt = f / sr; phase += dt; if (phase >= 1.0f) phase -= 1.0f; float osc; if (song.wave) { float p2 = phase + 0.5f; if (p2 >= 1.0f) p2 -= 1.0f; osc = (phase < 0.5f ? 1.0f : -1.0f) + og_polyblep(phase, dt) - og_polyblep(p2, dt); osc *= 0.8f; } else { osc = 2.0f * phase - 1.0f - og_polyblep(phase, dt); } float env_oct = song.envmod * 5.0f * fenv + acc_cv * song.accent * 2.2f * fenv; float base = 40.0f * og_exp2(song.cutoff * 7.6f); float fc = og_clampf(base * og_exp2(env_oct), 25.0f, sr * 0.45f); float k = song.reso * 3.95f; float y = ladder(osc * 0.55f, fc, k) * (1.0f + k * 0.4f); fenv *= fdec; acc_cv *= 0.99995f; float target = gate ? 1.0f : 0.0f; amp += (target - amp) * (target > amp ? 0.02f : 0.006f) * (48000.0f / sr); if (gate_timer > 0 && --gate_timer == 0) note_off(); return y * amp * (1.0f + 0.6f * acc_now); } /* ------------------------------------------------------------------ */ /* Sequencer */ /* ------------------------------------------------------------------ */ static float samples_per_step(void) { return sr * 60.0f / og_clampf(cur_tempo(), 20.0f, 400.0f) / 4.0f; } static int step_note(int s) { return ROOT + song.transpose + song.steps[s].note; } static void seq_step(void) { step = (step + 1) % NSTEPS; float sps = samples_per_step(); to_next += sps; dirty = 1; if (nheld > 0) { steps_played++; return; } /* live keys win */ const step_t *s = &song.steps[step], *prev = &song.steps[(step + NSTEPS - 1) % NSTEPS]; const step_t *next = &song.steps[(step + 1) % NSTEPS]; if (s->gate) { int slide_in = steps_played > 0 && prev->gate && prev->slide; note_on(step_note(step), s->accent, slide_in); gate_timer = (s->slide && next->gate) ? -1 : (int)(sps * 0.55f); } else { note_off(); } steps_played++; } static void set_playing(int on) { if (on && !playing) { playing = 1; step = -1; to_next = 0.0; steps_played = 0; } else if (!on && playing) { playing = 0; step = -1; if (nheld == 0) note_off(); } dirty = 1; } /* ------------------------------------------------------------------ */ /* 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 >= NSTEPS && dev.pad_rows >= 4; } /* scale degree -> semitones above ROOT */ static int degree_semis(int d) { return scale[d % 7] + 12 * (d / 7); } /* Keyboard degree for a pad on a square-ish grid: bottom-left = root. */ static int key_degree(int id) { int cols = dev.pad_cols ? dev.pad_cols : 1; int row = id / cols, col = id % cols; return (dev.pad_rows - 1 - row) * cols + col; } static void live_on(int note, float vel) { for (int i = 0; i < nheld; i++) if (held[i] == note) return; if (nheld == 8) { for (int i = 1; i < 8; i++) held[i - 1] = held[i]; nheld--; } held[nheld++] = note; note_on(note, vel > 0.95f, nheld > 1); } static void live_off(int note) { int j = 0; for (int i = 0; i < nheld; i++) if (held[i] != note) held[j++] = held[i]; if (j == nheld) return; nheld = j; if (nheld > 0) note_on(held[nheld - 1], 0, 1); /* glide back */ else note_off(); } static void audition(int s) { if (playing || nheld) return; note_on(step_note(s), song.steps[s].accent, 0); gate_timer = (int)(sr * 0.18f); } static void select_step(int s) { s = og_clampi(s, 0, NSTEPS - 1); if (s == cursor && edit_mode) song.steps[s].gate ^= 1; cursor = s; if (song.steps[s].gate || !playing) audition(s); dirty = 1; } static int pad_note(int id) { if (lanes_layout()) return ROOT + song.transpose + degree_semis(id % dev.pad_cols) + 12; return ROOT + song.transpose + degree_semis(key_degree(id)) + 12; } static void pad(int id, int down, float vel) { if (id < 0 || id >= n_pads()) return; if (lanes_layout()) { int row = id / dev.pad_cols, col = id % dev.pad_cols; if (row == 3) { int note = pad_note(id); if (down) { live_on(note, vel); if (edit_mode) { song.steps[cursor].note = (int8_t)(note - ROOT - song.transpose); song.steps[cursor].gate = 1; cursor = (cursor + 1) % NSTEPS; } } else { live_off(note); } dirty = 1; return; } if (!down || col >= NSTEPS) return; if (row == 0) { if (col == cursor || !edit_mode) song.steps[col].gate ^= 1; cursor = col; if (song.steps[col].gate) audition(col); } else if (row == 1) { song.steps[col].accent ^= 1; cursor = col; audition(col); } else if (row == 2) { song.steps[col].slide ^= 1; cursor = col; audition(col); } else { audition(col); } dirty = 1; return; } if (edit_mode) { if (down) select_step(id); return; } int note = pad_note(id); if (down) live_on(note, vel); else live_off(note); } static void edit_note(int semis) { step_t *s = &song.steps[cursor]; s->note = (int8_t)og_clampi(s->note + semis, -12, 36); s->gate = 1; audition(cursor); dirty = 1; } static void transpose(int semis) { song.transpose = (int8_t)og_clampi(song.transpose + semis, -12, 12); 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) * 0.01f; switch (k) { case 0: song.cutoff = og_clampf(song.cutoff + d, 0.0f, 1.0f); break; case 1: song.reso = og_clampf(song.reso + d, 0.0f, 1.0f); break; case 2: song.envmod = og_clampf(song.envmod + d, 0.0f, 1.0f); break; case 3: song.decay = og_clampf(song.decay + d, 0.0f, 1.0f); break; case 4: song.tempo = og_clampf(song.tempo + d * 100.0f, 60.0f, 180.0f); break; case 5: song.accent = og_clampf(song.accent + d, 0.0f, 1.0f); break; case 6: song.delay = og_clampf(song.delay + d, 0.0f, 1.0f); break; case 7: song.drive = og_clampf(song.drive + d, 0.0f, 1.0f); break; default: return; } knob_shown = k; 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_REC: if (shift_held) song.wave ^= 1; else edit_mode = !edit_mode; dirty = 1; break; case OG_BTN_SELECT: case OG_BTN_MODE_1: song.wave ^= 1; dirty = 1; break; case OG_BTN_UP: case OG_BTN_DOWN: { int dir = id == OG_BTN_UP ? 1 : -1; if (edit_mode) edit_note(dir * (shift_held ? 12 : 1)); else transpose(dir * (shift_held ? 12 : 1)); break; } case OG_BTN_LEFT: if (edit_mode) { song.steps[cursor].accent ^= 1; audition(cursor); dirty = 1; } break; case OG_BTN_RIGHT: if (edit_mode) { song.steps[cursor].slide ^= 1; dirty = 1; } break; } } static void jog(float v) { int dir = v > 0 ? 1 : (v < 0 ? -1 : 0); if (!dir) return; if (edit_mode) edit_note(dir * (shift_held ? 12 : 1)); else transpose(dir); } /* Large-screen layout, shared by drawing and touch. */ typedef struct { int x0, pitch, top, height; } lane_geom; static lane_geom big_lane(void) { lane_geom g; int w = dev.display_w, h = dev.display_h; g.x0 = 24; g.pitch = (w - 48) / NSTEPS; g.top = h * 17 / 100; g.height = h * 40 / 100; return g; } static void touch(float x, float y) { if (dev.display_w < 320) return; lane_geom g = big_lane(); int ix = (int)x, iy = (int)y; if (iy < g.top - 10) { if (ix > dev.display_w / 2) { if (!sync_mode) set_playing(!playing); } else { edit_mode = !edit_mode; dirty = 1; } return; } if (iy < g.top + g.height + 30) { int s = (ix - g.x0) / (g.pitch ? g.pitch : 1); if (ix >= g.x0 && s >= 0 && s < NSTEPS) { if (!edit_mode) { edit_mode = 1; } select_step(s); } } } static void midi(uint8_t st, uint8_t d1, uint8_t d2) { uint8_t type = st & 0xF0; if (st == 0xFA) { if (!sync_mode) set_playing(1); } else if (st == 0xFC) { if (!sync_mode) set_playing(0); } else if (type == 0x90 && d2) live_on(d1 & 127, (float)d2 / 127.0f); else if (type == 0x80 || (type == 0x90 && !d2)) live_off(d1 & 127); else if (type == 0xB0 && d1 == 74) { song.cutoff = (float)(d2 & 127) / 127.0f; dirty = 1; } else if (type == 0xB0 && d1 == 71) { song.reso = (float)(d2 & 127) / 127.0f; dirty = 1; } } /* ------------------------------------------------------------------ */ /* 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((int)e->id, 1, e->value); break; case OG_PAD_UP: pad((int)e->id, 0, 0.0f); 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: jog(e->value); 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) { /* note (semitones above A1), gate, accent, slide */ static const int8_t pat[NSTEPS][4] = { { 12, 1, 1, 0 }, { 12, 1, 0, 0 }, { 24, 1, 0, 1 }, { 12, 1, 0, 0 }, { 15, 1, 1, 0 }, { 12, 0, 0, 0 }, { 22, 1, 0, 1 }, { 19, 1, 0, 0 }, { 12, 1, 1, 0 }, { 12, 1, 0, 1 }, { 24, 1, 0, 0 }, { 22, 1, 1, 1 }, { 19, 1, 0, 0 }, { 15, 0, 0, 0 }, { 10, 1, 0, 1 }, { 12, 1, 1, 0 }, }; for (int i = 0; i < NSTEPS; i++) { song.steps[i].note = pat[i][0]; song.steps[i].gate = (uint8_t)pat[i][1]; song.steps[i].accent = (uint8_t)pat[i][2]; song.steps[i].slide = (uint8_t)pat[i][3]; } og_fw_params_reset(params, NPARAMS); /* the knobs: defaults from the table */ song.transpose = 0; song.wave = 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(); return 0; } void og_reset(void) { evn = 0; /* queues clear on reset */ gate = 0; gate_timer = -1; amp = 0.0f; fenv = 0.0f; acc_now = 0.0f; acc_cv = 0.0f; for (int i = 0; i < 4; i++) lad[i] = 0.0f; dc_l.y = dc_r.y = damp_l.y = damp_r.y = 0.0f; __builtin_memset(dly_l, 0, sizeof dly_l); __builtin_memset(dly_r, 0, sizeof dly_r); nheld = 0; } 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; glide_coef = 1.0f - og_expf(-1.0f / (0.045f * sr)); fdec = og_decay_coef(0.3f, 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; 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); } /* Coefficients derived from params. Recomputed at the block start and after * any event, so OG_PARAM_CHANGE takes effect exactly at its offset. */ static int dlen; static float fbk, wet, damp, dcc, pre, post; static void derive(void) { dlen = (int)(samples_per_step() * 3.0f); /* dotted eighth */ if (dlen > DLY_LEN - 1) dlen = DLY_LEN - 1; if (dlen < 1) dlen = 1; fbk = 0.38f + 0.2f * song.delay; wet = song.delay * 0.55f; damp = og_onepole_coef(3200.0f, sr); dcc = og_onepole_coef(12.0f, sr); pre = 1.0f + song.drive * 4.0f; post = 1.0f / (1.0f + song.drive * 1.5f); } 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 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 && clk.playing != playing) set_playing(clk.playing); derive(); 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(); } if (sync) { long n = og_fw_sync_tick(&clk); if (n >= 0) { int s = (int)(((n % NSTEPS) + NSTEPS) % NSTEPS); if (s != (step + 1) % NSTEPS) steps_played = 0; /* jumped: no slide into it */ step = (s + NSTEPS - 1) % NSTEPS; seq_step(); } } else if (playing) { if (to_next <= 0.0) seq_step(); to_next -= 1.0; } float x = voice_tick(); x = og_sat(x * pre) * post; int r = dly_w - dlen; if (r < 0) r += DLY_LEN; float dl = og_onepole_lp(&damp_l, dly_l[r], damp); float dr = og_onepole_lp(&damp_r, dly_r[r], damp); dly_l[dly_w] = x * 0.8f + dr * fbk; dly_r[dly_w] = dl * fbk; if (++dly_w >= DLY_LEN) dly_w = 0; float l = x + dl * wet, rr = x + dr * wet; l = og_onepole_hp(&dc_l, l, dcc); rr = og_onepole_hp(&dc_r, rr, dcc); outl[i] = og_sat(l * 0.95f) * 0.88f; outr[i] = og_sat(rr * 0.95f) * 0.88f; } while (e < evn) apply_ev(&evq[e++]); evn = 0; clock_samples += frames; if (gate || amp > 0.01f) dirty_curve = 1; } /* ------------------------------------------------------------------ */ /* Drawing */ /* ------------------------------------------------------------------ */ static void note_name(int midi_note, char *b, int cap) { int n = midi_note - ROOT; int pc = ((n % 12) + 12) % 12; og_strcpy(b, note_names[pc], cap); og_strcat_int(b, midi_note / 12 - 1, cap); /* A1 = 33 */ } static const char *const knob_names[8] = { "CUTOFF", "RESO", "ENV MOD", "DECAY", "TEMPO", "ACCENT", "DELAY", "DRIVE" }; static float knob_val(int k) { switch (k) { case 0: return song.cutoff; case 1: return song.reso; case 2: return song.envmod; case 3: return song.decay; case 4: return og_clampf((cur_tempo() - 60.0f) / 120.0f, 0.0f, 1.0f); case 5: return song.accent; case 6: return song.delay; default: return song.drive; } } static void knob_text(int k, char *b, int cap) { og_strcpy(b, "", cap); 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); } } /* Ladder magnitude in dB at frequency f for cutoff fc and feedback k * (analog prototype: H = p^4 / (1 + k p^4), p = 1 / (1 + j f/fc)). */ static float ladder_db(float f, float fc, float k) { float w = f / fc; /* p = (1 - j w) / (1 + w^2); compute p^4 via complex squaring */ float d = 1.0f / (1.0f + w * w); float re = d, im = -w * d; float r2 = re * re - im * im, i2 = 2.0f * re * im; float r4 = r2 * r2 - i2 * i2, i4 = 2.0f * r2 * i2; float dr = 1.0f + k * r4, di = k * i4; float num = r4 * r4 + i4 * i4, den = dr * dr + di * di; float mag2 = den > 1e-12f ? num / den : 1e12f; return 10.0f * og_log2(mag2 + 1e-12f) * 0.30103f + 20.0f * og_log2(1.0f + k * 0.4f) * 0.30103f; } static void draw_curve(int x, int y, int w, int h, og_color line, og_color fill) { float base = 40.0f * og_exp2(song.cutoff * 7.6f); float env_oct = song.envmod * 5.0f * fenv + acc_cv * song.accent * 2.2f * fenv; float fc = og_clampf(base * og_exp2(gate || amp > 0.01f ? env_oct : 0.0f), 25.0f, 20000.0f); float k = song.reso * 3.95f; og_fill_round_rect(&gfx, x, y, w, h, 10, og_hex(0x131A14)); for (int i = 1; i < 4; i++) og_vline(&gfx, x + w * i / 4, y + 6, h - 12, og_hex(0x1E2A20)); og_hline(&gfx, x + 6, y + h / 3, w - 12, og_hex(0x1E2A20)); int prev_y = -1; for (int px = 0; px < w - 8; px++) { float f = 20.0f * og_exp2((float)px / (float)(w - 8) * 10.0f); /* 20 Hz .. 20 kHz */ float db = og_clampf(ladder_db(f, fc, k), -48.0f, 24.0f); int yy = y + h / 3 - (int)(db * (float)h / 72.0f); yy = og_clampi(yy, y + 4, y + h - 5); og_vline(&gfx, x + 4 + px, yy, y + h - 4 - yy, fill); if (prev_y >= 0) og_line_thick(&gfx, x + 3 + px, prev_y, x + 4 + px, yy, 3, line); prev_y = yy; } } static void draw_big(void) { int w = dev.display_w, h = dev.display_h; og_color bg = og_hex(0x0C0F0D), panel = og_hex(0x151A16), text = og_hex(0xE8F0E6), muted = og_hex(0x6E7D70); og_color acid = og_hex(0xB8FF3C), accent_c = og_hex(0xFF8A2A), slide_c = og_hex(0x4ED8FF); og_clear(&gfx, bg); char b[32]; /* header */ 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, "ACID", ts, text); x = og_text(&gfx, x + 3 * ts, ty, "LINE", ts, acid); /* chips */ x += 24; const char *mode = edit_mode ? "EDIT" : "PLAY"; og_fill_round_rect(&gfx, x, ty - 4, og_text_width(mode, 2) + 20, 7 * 2 + 8, 6, edit_mode ? og_hex(0xFF3D5A) : og_hex(0x26302A)); og_text(&gfx, x + 10, ty, mode, 2, edit_mode ? og_rgb(255, 255, 255) : acid); x += og_text_width(mode, 2) + 32; const char *wv = song.wave ? "SQUARE" : "SAW"; og_fill_round_rect(&gfx, x, ty - 4, og_text_width(wv, 2) + 20, 7 * 2 + 8, 6, og_hex(0x26302A)); og_text(&gfx, x + 10, ty, wv, 2, text); if (sync_mode) { x += og_text_width(wv, 2) + 32; og_fill_round_rect(&gfx, x, ty - 4, og_text_width("SYNC", 2) + 20, 7 * 2 + 8, 6, og_hex(0x1D3A4A)); og_text(&gfx, x + 10, ty, "SYNC", 2, slide_c); } 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, acid); else og_fill_rect(&gfx, px, ty, 7 * ts, 7 * ts, muted); /* step lane: a tiny piano roll */ lane_geom g = big_lane(); int lo = 0, hi = 36; /* displayed semitone range */ int nh = g.height / 14; /* note block height */ for (int s = 0; s < NSTEPS; s++) { int cx = g.x0 + s * g.pitch; og_color col_bg = (s / 4) % 2 ? og_hex(0x121713) : og_hex(0x161C17); if (playing && s == step) col_bg = og_hex(0x24301F); og_fill_rect(&gfx, cx, g.top, g.pitch - 2, g.height, col_bg); } for (int s = 0; s < NSTEPS; s++) { const step_t *st = &song.steps[s]; int cx = g.x0 + s * g.pitch; int n = og_clampi(st->note, lo, hi); int y = g.top + g.height - nh - (n - lo) * (g.height - nh) / (hi - lo); if (st->gate) { og_color c = st->accent ? accent_c : acid; if (playing && s == step) c = og_rgb(255, 255, 255); int bw = st->slide ? g.pitch : g.pitch - 8; og_fill_round_rect(&gfx, cx + 3, y, bw, nh, 4, c); if (st->slide) { /* slide: a ramp toward the next note */ const step_t *nx = &song.steps[(s + 1) % NSTEPS]; int n2 = og_clampi(nx->note, lo, hi); int y2 = g.top + g.height - nh - (n2 - lo) * (g.height - nh) / (hi - lo); if (s < NSTEPS - 1 && nx->gate) og_line_thick(&gfx, cx + g.pitch - 4, y + nh / 2, cx + g.pitch + 6, y2 + nh / 2, 4, slide_c); og_fill_rect(&gfx, cx + 3, y + nh - 3, bw, 3, slide_c); } } else { og_fill_rect(&gfx, cx + 8, g.top + g.height - 6, g.pitch - 18, 3, og_hex(0x2C352E)); } /* labels under the lane */ int ly = g.top + g.height + 8; if (st->gate) { const char *nn = note_names[((st->note + song.transpose) % 12 + 24) % 12]; og_text_center(&gfx, cx + g.pitch / 2 - 1, ly, nn, 2, st->accent ? accent_c : text); } else { og_text_center(&gfx, cx + g.pitch / 2 - 1, ly, "-", 2, muted); } if (edit_mode && s == cursor) og_round_rect(&gfx, cx - 1, g.top - 3, g.pitch + 1, g.height + 30, 6, og_rgb(255, 255, 255)); } /* filter curve + knobs */ int by = h * 68 / 100, bh = h * 23 / 100; int cw = w * 33 / 100; draw_curve(24, by, cw, bh, acid, og_hex(0x1D2A17)); int kx0 = 24 + cw + 24, kw = (w - kx0 - 24) / 4; for (int i = 0; i < 4; i++) { int k = i + (shift_held ? 4 : 0); int kx = kx0 + i * kw, kcx = kx + kw / 2, kcy = by + bh / 2 - 10, kr = bh / 2 - 22; if (kr < 6) kr = 6; float v = knob_val(k); for (int a = 0; a <= 270; a += 3) { float ang = (135.0f + (float)a) / 360.0f; int lit = (float)a <= v * 270.0f; og_fill_circle(&gfx, kcx + (int)(og_cos_cycles(ang) * (float)kr), kcy + (int)(og_sin_cycles(ang) * (float)kr), 3, lit ? (k >= 4 ? slide_c : acid) : og_hex(0x26302A)); } knob_text(k, b, sizeof b); og_text_center(&gfx, kcx, kcy - 10, b, 3, text); og_text_center(&gfx, kcx, by + bh - 12, knob_names[k], 2, muted); } /* hint */ const char *hint; if (lanes_layout()) hint = edit_mode ? "ROWS: STEP ACCENT SLIDE KEYS KEYS WRITE THE SELECTED STEP" : "ROWS: STEP ACCENT SLIDE KEYS REC: EDIT SHIFT: MORE KNOBS"; else hint = edit_mode ? "TAP STEP UP/DN NOTE LEFT ACCENT RIGHT SLIDE REC: DONE" : "PADS PLAY HOLD TWO = SLIDE REC: EDIT SHIFT: MORE KNOBS"; og_text_center(&gfx, w / 2, h - 24, hint, 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, edit_mode ? "EDIT" : "ACID", 1, on); og_text(&gfx, 30, 0, song.wave ? "SQR" : "SAW", 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); if (sounding >= 0 && (gate || amp > 0.05f)) { note_name(sounding, b, sizeof b); og_text(&gfx, 56, 0, b, 1, on); } int top = 10, lh = h - 30, pitch = w / NSTEPS; if (lh < 8) lh = 8; if (pitch < 2) pitch = 2; for (int s = 0; s < NSTEPS; s++) { const step_t *st = &song.steps[s]; int x = s * pitch; int inv = playing && s == step; if (inv) og_fill_rect(&gfx, x, top, pitch - 1, lh, on); og_color c = inv ? off : on; if (st->gate) { int n = og_clampi(st->note, 0, 36); int y = top + lh - 3 - n * (lh - 3) / 36; og_fill_rect(&gfx, x + 1, y, st->slide ? pitch : pitch - 2, st->accent ? 3 : 2, c); } if (st->accent) og_pixel(&gfx, x + pitch / 2 - 1, top + lh + 2, on); if (st->slide) og_hline(&gfx, x + 1, top + lh + 4, pitch - 1, on); if (edit_mode && s == cursor) og_stroke_rect(&gfx, x, top - 1, pitch, lh + 2, inv ? off : on); } if (knob_shown >= 0) { og_strcpy(b, knob_names[knob_shown], sizeof b); og_strcat(b, " ", sizeof b); char v[12]; knob_text(knob_shown, v, sizeof v); og_strcat(b, v, sizeof b); og_text(&gfx, 0, h - 7, b, 1, on); og_fill_rect(&gfx, 80, h - 6, (int)(knob_val(knob_shown) * 46.0f), 5, on); og_stroke_rect(&gfx, 80, h - 7, 48, 7, on); } else { og_strcpy(b, "CUT", sizeof b); og_strcat_int(b, (int)(song.cutoff * 100.0f + 0.5f), sizeof b); og_strcat(b, " RES", sizeof b); og_strcat_int(b, (int)(song.reso * 100.0f + 0.5f), sizeof b); og_strcat(b, " ENV", sizeof b); og_strcat_int(b, (int)(song.envmod * 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 set_led(int i, int r, int g, int b) { want_pad[i * 3] = (uint8_t)og_clampi(r, 0, 255); want_pad[i * 3 + 1] = (uint8_t)og_clampi(g, 0, 255); want_pad[i * 3 + 2] = (uint8_t)og_clampi(b, 0, 255); } static int is_held(int note) { for (int i = 0; i < nheld; i++) if (held[i] == note) return 1; return 0; } static void key_led(int i, int note, int degree) { int root = degree % 7 == 0; if (is_held(note)) set_led(i, 255, 255, 255); else if (sounding == note && (gate || amp > 0.05f)) set_led(i, 150, 255, 60); else if (root) set_led(i, 70, 110, 20); else set_led(i, 18, 30, 10); } static void compute_leds(void) { int n = n_pads(); for (int i = 0; i < n; i++) set_led(i, 0, 0, 0); if (lanes_layout()) { for (int i = 0; i < n; i++) { int row = i / dev.pad_cols, col = i % dev.pad_cols; if (row == 3) { key_led(i, pad_note(i), col); continue; } if (row > 3 || col >= NSTEPS) continue; const step_t *st = &song.steps[col]; int ph = playing && col == step, cur = edit_mode && col == cursor; if (row == 0) { if (ph) set_led(i, 255, 255, 255); else if (cur) set_led(i, 255, 60, 90); else if (st->gate) set_led(i, 140, 230, 40); else set_led(i, 12, 16, 10); } else if (row == 1) { if (st->accent) set_led(i, 255, 120, 30); else set_led(i, ph ? 40 : 10, ph ? 30 : 6, 4); } else { if (st->slide) set_led(i, 60, 200, 255); else set_led(i, 4, ph ? 30 : 8, ph ? 40 : 12); } } } else if (edit_mode) { for (int i = 0; i < n && i < NSTEPS; i++) { const step_t *st = &song.steps[i]; if (playing && i == step) set_led(i, 255, 255, 255); else if (i == cursor) set_led(i, 255, 60, 90); else if (!st->gate) set_led(i, 12, 14, 12); else if (st->accent) set_led(i, 255, 120, 30); else if (st->slide) set_led(i, 60, 200, 255); else set_led(i, 120, 220, 40); } } else { for (int i = 0; i < n; i++) key_led(i, pad_note(i), key_degree(i)); } 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_REC] = edit_mode ? 255 : 40; 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] = edit_mode && song.steps[cursor].accent ? 255 : 40; want_btn[OG_BTN_RIGHT] = edit_mode && song.steps[cursor].slide ? 255 : 40; want_btn[OG_BTN_MODE_1] = want_btn[OG_BTN_SELECT] = song.wave ? 255 : 60; } 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; } /* the filter curve follows the envelope, at most ~30 fps */ if (dirty_curve && clock_samples - last_curve_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_curve_draw = clock_samples; changed = 1; } dirty = 0; dirty_curve = 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 and the sound */ /* ------------------------------------------------------------------ */ #define STATE_TAG OG_FOURCC('A', 'C', 'I', 'D') #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 < NSTEPS; i++) { s->steps[i].note = (int8_t)og_clampi(s->steps[i].note, -12, 36); s->steps[i].gate &= 1; s->steps[i].accent &= 1; s->steps[i].slide &= 1; } s->cutoff = og_clampf(s->cutoff, 0, 1); s->reso = og_clampf(s->reso, 0, 1); s->envmod = og_clampf(s->envmod, 0, 1); s->decay = og_clampf(s->decay, 0, 1); s->accent = og_clampf(s->accent, 0, 1); s->delay = og_clampf(s->delay, 0, 1); s->drive = og_clampf(s->drive, 0, 1); s->tempo = og_clampf(s->tempo, 60, 180); s->transpose = (int8_t)og_clampi(s->transpose, -12, 12); s->wave &= 1; song = b; dirty = 1; return OG_OK; }