home

offset oscillator

#code

🇬🇧: digital scale-aware oscillator for my korg minilogue xd and other logue-sdk instruments
🇮🇹: oscillatore digitale scale-aware per il mio korg minilogue xd e altri strumenti del logue-sdk

questa versione è datata e ne ho fatte altre che a un certo punto metterò su questo sito. se vuoi averla scrivimi per mail

v0.2-35
download: link

note

manifest.json

TODO: rinominare “root” -> “root (c = 0)”

{
    "header" : 
    {
        "platform" : "minilogue-xd",
        "module" : "osc",
        "api" : "1.1-0",
        "dev_id" : 0,
        "prg_id" : 0,
        "version" : "0.2-35",
        "name" : "offset",
        "num_param" : 6,
        "params" : [
            ["root",        0,  11,  ""],
            ["major/minor", 0,   1,  ""],
            ["offset/wave", 0,   1,  ""],
            ["main note",   0, 100, "%"],
            ["x/-5/-4/+5/+7/+9", 0,   5,  ""],
            ["lgb.26 :)",   0,   1,  ""]
          ]
    }
}

offset.cpp

#include "userosc.h"

static const uint8_t s_scales[8][7] = {
  {0, 2, 4, 5, 7, 9, 11},
  {0, 2, 3, 5, 7, 8, 10},
};

#define MODE_OFFSET 0
#define MODE_WVFORM 1

#define DEFAULT_GAIN 0.3

#define EXTRA_NONE 0
#define EXTRA_FIFTH 1
#define EXTRA_SEVENTH 2
#define EXTRA_NINTH 3

typedef struct State {
  float phase[4];
  uint8_t scale_idx;
  uint8_t root;
  uint8_t mode;      // 0 = Offset, 1 = Waveform

  uint8_t degree_offset;
  float shape;

  float main_note;   // how much of the tonic
  float wave_morph;  // current shape
  uint8_t extra;
} State;

static State s;

static inline q31_t q31_add(q31_t a, q31_t b);
uint8_t offset_note(const uint8_t* scale, const uint8_t scale_size, const uint8_t degree_offset, const uint8_t note, const uint8_t root);
void oscillate(
  const uint8_t note,
  const uint8_t voice,
  const uint8_t mod,
  const bool replace,
  const float wave_morph,
  const float gain,
  q31_t* y,
  const uint32_t frames
);

void OSC_INIT(uint32_t platform, uint32_t api) {
  s.phase[0] = 0.0f;
  s.phase[1] = 0.0f;
  s.phase[2] = 0.0f;
  s.phase[3] = 0.0f;
  s.scale_idx = 0;
  s.root = 0;
  s.mode = MODE_OFFSET;
  s.main_note = 1.0f;
  s.wave_morph = 0.0f;
}

void OSC_CYCLE(const user_osc_param_t * const params, int32_t *yn, const uint32_t frames) {
  switch (s.mode) {
  case MODE_OFFSET:
    s.degree_offset = (uint8_t)(s.shape * 10);
    break;
  case MODE_WVFORM:
    s.wave_morph = param_val_to_f32(s.shape);
    break;
  }

  // pitch logic
  #define current_scale (s_scales[s.scale_idx])

  uint8_t note = (params->pitch >> 8);
  uint8_t moved_note = offset_note(current_scale, 7, s.degree_offset, note, s.root);

  // audio generation
  const float w0 = osc_w0f_for_note(moved_note, params->pitch & 0xFF);

  q31_t * __restrict y = (q31_t *)yn;
  for (int i = 0; i < frames; i++) {
    yn[i] = f32_to_q31(0.0);
  }

  oscillate(note,       0, params->pitch & 0xFF, false, s.wave_morph, DEFAULT_GAIN * s.main_note, yn, frames);
  oscillate(moved_note, 1, params->pitch & 0xFF, false, s.wave_morph, DEFAULT_GAIN, yn, frames);


  uint8_t top_note = 0;
  switch (s.extra) {
    case EXTRA_NONE:
      break;
    case EXTRA_FIFTH:
      top_note = offset_note(current_scale, 7, 4, note, s.root);
      break;
    case EXTRA_SEVENTH:
      top_note = offset_note(current_scale, 7, 6, note, s.root);
      break;
    case EXTRA_NINTH:
      top_note = offset_note(current_scale, 7, 8, note, s.root);
      break;
  }

  if (top_note != 0) {
    oscillate(top_note, 2, params->pitch & 0xFF, false, s.wave_morph, DEFAULT_GAIN, yn, frames);
  }
}

uint8_t offset_note(
  const uint8_t* scale,
  const uint8_t scale_size,
  const uint8_t degree_offset,
  const uint8_t note,
  const uint8_t root
) {
  int8_t relative_note = (int8_t)note - s.root;
  uint8_t closest_root = (relative_note - (relative_note % 12)) + s.root;
  uint8_t semitone_offset = (note - closest_root) % 12;

  uint8_t closest_scale_degree = 0;
  for (int i = 0; i < scale_size; i++) {
    if (current_scale[i] <= semitone_offset) {
        closest_scale_degree = i;
      }
  }

  uint8_t offset_scale_degree = (closest_scale_degree + degree_offset);
  uint8_t octaves_above = offset_scale_degree / scale_size;
  uint8_t final_note = closest_root + current_scale[offset_scale_degree % scale_size] + (12 * octaves_above);

  return final_note;
}

void oscillate(
  const uint8_t note,
  const uint8_t voice,
  const uint8_t mod,
  const bool replace,
  const float wave_morph,
  const float gain,
  q31_t* y,
  const uint32_t frames
) {
  float* phase = &s.phase[voice];

  const float w0 = osc_w0f_for_note(note, mod);

  for (uint32_t i = 0; i < frames; i++) {
    float sin = osc_sinf(s.phase[voice]);
    float saw = (s.phase[voice] * 2.0) - 1.0;
    
    float sig = ((sin * s.wave_morph) + (saw * (1 - s.wave_morph))) * gain;

    if (replace) {
      // y[i] = 0;
      y[i] = f32_to_q31(sig * gain);
    } else {
      y[i] = q31_add(y[i], f32_to_q31(sig));
    }

    *phase += w0;
    if (*phase >= 1.0f) {
      *phase -= 1.0f;
    }
  }
}

static inline q31_t q31_add(q31_t a, q31_t b) {
  int64_t sum = (int64_t)a + (int64_t)b;
  if (sum > INT32_MAX) return INT32_MAX;
  if (sum < INT32_MIN) return INT32_MIN;
  return (q31_t)sum;
}

void OSC_PARAM(uint16_t index, uint16_t value) {
  switch (index) {
    case k_user_osc_param_id1:    s.root = value; break;
    case k_user_osc_param_id2:    s.scale_idx = value; break;
    case k_user_osc_param_id3:    s.mode = value; break; // 0: Offset, 1: Wave
    case k_user_osc_param_id4:    s.main_note = (float)value / 100.0; break;
    case k_user_osc_param_id5:    s.extra = value; break;
    case k_user_osc_param_shape:  s.shape = param_val_to_f32(value); break;
  }
}

void OSC_NOTEON(const user_osc_param_t * const params) {
  s.phase[0] = 0.0f;
  s.phase[1] = 0.0f;
  s.phase[2] = 0.0f;
  s.phase[3] = 0.0f;
}

void OSC_NOTEOFF(const user_osc_param_t * const params) { }