Browse Source

implement Edges

pull/94/head
Kautenja 5 years ago
parent
commit
1edb3ea1a8
8 changed files with 2241 additions and 1 deletions
  1. +12
    -1
      plugin.json
  2. +247
    -0
      res/Edges.svg
  3. +413
    -0
      src/Edges.cpp
  4. +378
    -0
      src/Edges/digital_oscillator.hpp
  5. +169
    -0
      src/Edges/square_oscillator.hpp
  6. +1020
    -0
      src/Edges/wavetables.hpp
  7. +1
    -0
      src/plugin.cpp
  8. +1
    -0
      src/plugin.hpp

+ 12
- 1
plugin.json View File

@@ -230,6 +230,17 @@
"Hardware clone",
"Polyphonic"
]
},
{
"slug": "Edges",
"name": "Quad Chiptune Audio Generator",
"description": "Based on Mutable Instruments Edges",
"manualUrl": "https://mutable-instruments.net/modules/edges/manual/",
"modularGridUrl": "https://www.modulargrid.net/e/mutable-instruments-edges",
"tags": [
"Oscillator",
"Hardware clone"
]
}
]
}
}

+ 247
- 0
res/Edges.svg
File diff suppressed because it is too large
View File


+ 413
- 0
src/Edges.cpp View File

@@ -0,0 +1,413 @@
#include "plugin.hpp"
#include "Edges/digital_oscillator.hpp"
#include "Edges/square_oscillator.hpp"

using simd::float_4;

// ---------------------------------------------------------------------------
// MARK: Module
// ---------------------------------------------------------------------------

/// the PWM values to cycle between
static const float SQUARE_PWM_VALUES[] = {0.5, 0.66, 0.75, 0.87, 0.95, 1.0};

struct Edges : Module {
enum ParamIds {
FREQ1_PARAM,
XMOD1_PARAM,
LEVEL1_PARAM,
FREQ2_PARAM,
XMOD2_PARAM,
LEVEL2_PARAM,
FREQ3_PARAM,
XMOD3_PARAM,
LEVEL3_PARAM,
FREQ4_PARAM,
LEVEL4_PARAM,
WAVEFORM1_PARAM,
WAVEFORM2_PARAM,
WAVEFORM3_PARAM,
WAVEFORM4_PARAM,
NUM_PARAMS
};
enum InputIds {
GATE1_INPUT,
FREQ1_INPUT,
MOD1_INPUT,
GATE2_INPUT,
FREQ2_INPUT,
MOD2_INPUT,
GATE3_INPUT,
FREQ3_INPUT,
MOD3_INPUT,
GATE4_INPUT,
FREQ4_INPUT,
MOD4_INPUT,
NUM_INPUTS
};
enum OutputIds {
WAVEFORM1_OUTPUT,
WAVEFORM2_OUTPUT,
WAVEFORM3_OUTPUT,
WAVEFORM4_OUTPUT,
MIX_OUTPUT,
NUM_OUTPUTS
};
enum LightIds {
GATE1_LIGHT_GREEN, GATE1_LIGHT_RED,
GATE2_LIGHT_GREEN, GATE2_LIGHT_RED,
GATE3_LIGHT_GREEN, GATE3_LIGHT_RED,
GATE4_LIGHT_GREEN, GATE4_LIGHT_RED,
NUM_LIGHTS
};

Edges() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
// setup waveform 1 parameters
configParam(FREQ1_PARAM, -36.0, 36.0, 0.0, "Channel 1 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
configParam(XMOD1_PARAM, 0.0, 1.0, 0.0, "Channel 1 to Channel 2 hard sync");
configParam(LEVEL1_PARAM, 0.0, 1.0, 0.0, "Channel 1 level", "%", 0, 100);
// setup waveform 2 parameters
configParam(FREQ2_PARAM, -36.0, 36.0, 0.0, "Channel 2 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
configParam(XMOD2_PARAM, 0.0, 1.0, 0.0, "Channel 1 x Channel 2 ring modulation");
configParam(LEVEL2_PARAM, 0.0, 1.0, 0.0, "Channel 2 level", "%", 0, 100);
// setup waveform 3 parameters
configParam(FREQ3_PARAM, -36.0, 36.0, 0.0, "Channel 3 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
configParam(XMOD3_PARAM, 0.0, 1.0, 0.0, "Channel 1 x Channel 3 ring modulation");
configParam(LEVEL3_PARAM, 0.0, 1.0, 0.0, "Channel 3 level", "%", 0, 100);
// setup waveform 4 parameters
configParam(FREQ4_PARAM, -36.0, 36.0, 0.0, "Channel 4 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
configParam(LEVEL4_PARAM, 0.0, 1.0, 0.0, "Channel 4 level", "%", 0, 100);
// setup waveform selection parameters
configParam(WAVEFORM1_PARAM, 0.0, 1.0, 0.0, "Channel 1 select");
configParam(WAVEFORM2_PARAM, 0.0, 1.0, 0.0, "Channel 2 select");
configParam(WAVEFORM3_PARAM, 0.0, 1.0, 0.0, "Channel 3 select");
configParam(WAVEFORM4_PARAM, 0.0, 1.0, 0.0, "Channel 4 select");
}

/// a type for the square wave oscillator
typedef SquareWaveOscillator<16, 16, float_4> Square;
/// a type for the digital oscillator
typedef DigitalOscillator<float_4> Digital;

/// channel 1 (square wave)
Square oscillator1;
/// channel 2 (square wave)
Square oscillator2;
/// channel 3 (square wave)
Square oscillator3;
/// channel 4 (digital selectable wave)
Digital oscillator4;

/// a flag determining whether waveform 1's gate is open
bool is_gate1_open = true;
/// a flag determining whether waveform 2's gate is open
bool is_gate2_open = true;
/// a flag determining whether waveform 3's gate is open
bool is_gate3_open = true;
/// a flag determining whether waveform 4's gate is open
bool is_gate4_open = true;

/// the voltage at 1V/oct port 1
float_4 voct1_voltage = 0.f;
/// the voltage at 1V/oct port 2
float_4 voct2_voltage = 0.f;
/// the voltage at 1V/oct port 3
float_4 voct3_voltage = 0.f;
/// the voltage at 1V/oct port 4
float voct4_voltage = 0.f;

/// the mod voltage for mod port 1
float_4 mod1_voltage = 0.f;
/// the mod voltage for mod port 2
float_4 mod2_voltage = 0.f;
/// the mod voltage for mod port 3
float_4 mod3_voltage = 0.f;
/// the mod voltage for mod port 4
float mod4_voltage = 0.f;

/// the state of oscillator 1 determined by the button
int osc1_state = 0;
/// the state of oscillator 2 determined by the button
int osc2_state = 0;
/// the state of oscillator 3 determined by the button
int osc3_state = 0;

/// a Schmitt trigger for handling inputs to the waveform select 1 button
dsp::SchmittTrigger oscillator1Trigger;
/// a Schmitt trigger for handling inputs to the waveform select 2 button
dsp::SchmittTrigger oscillator2Trigger;
/// a Schmitt trigger for handling inputs to the waveform select 3 button
dsp::SchmittTrigger oscillator3Trigger;
/// a Schmitt trigger for handling inputs to the waveform select 4 button
dsp::SchmittTrigger oscillator4Trigger;

/// Process the gate inputs to determine which waveforms are active.
/// Note that Gates are normalled, meaning that when a connection is active
/// at a higher level gate, the signal propagates to lower level gates that
/// are not connected
/// TODO: determine whether the 0.7V threshold from hardware specification
/// fits the software model
void process_gate_inputs() {
// the 0.7V threshold is from the hardware specification for Edges
is_gate1_open = inputs[GATE1_INPUT].getNormalVoltage(0.7f) >= 0.7f;
is_gate2_open = inputs[GATE2_INPUT].getNormalVoltage(is_gate1_open * 0.7f) >= 0.7f;
is_gate3_open = inputs[GATE3_INPUT].getNormalVoltage(is_gate2_open * 0.7f) >= 0.7f;
is_gate4_open = inputs[GATE4_INPUT].getNormalVoltage(is_gate3_open * 0.7f) >= 0.7f;
// set LEDs for each of the gates
lights[GATE1_LIGHT_GREEN].setBrightness(is_gate1_open);
lights[GATE2_LIGHT_GREEN].setBrightness(is_gate2_open);
lights[GATE3_LIGHT_GREEN].setBrightness(is_gate3_open);
lights[GATE4_LIGHT_GREEN].setBrightness(is_gate4_open);
}

/// Process the 1V/oct inputs
void process_voct_inputs() {
voct1_voltage = inputs[FREQ1_INPUT].getNormalVoltageSimd<float_4>(0, 0);
voct2_voltage = inputs[FREQ2_INPUT].getNormalVoltageSimd<float_4>(voct1_voltage, 0);
voct3_voltage = inputs[FREQ3_INPUT].getNormalVoltageSimd<float_4>(voct2_voltage, 0);
// get the normalled channel 4 input as a float
auto voct1_voltage_ = inputs[FREQ1_INPUT].getNormalVoltage(0, 0);
auto voct2_voltage_ = inputs[FREQ2_INPUT].getNormalVoltage(voct1_voltage_, 0);
auto voct3_voltage_ = inputs[FREQ3_INPUT].getNormalVoltage(voct2_voltage_, 0);
voct4_voltage = inputs[FREQ4_INPUT].getNormalVoltage(voct3_voltage_, 0);
// TODO: refactor to use the float_4 structure
// voct4_voltage = inputs[FREQ4_INPUT].getNormalVoltageSimd<float_4>(voct3_voltage, 0);
}

inline void process_mod_inputs() {
mod1_voltage = inputs[MOD1_INPUT].getNormalVoltageSimd<float_4>(0, 0);
mod2_voltage = inputs[MOD2_INPUT].getNormalVoltageSimd<float_4>(0, 0);
mod3_voltage = inputs[MOD3_INPUT].getNormalVoltageSimd<float_4>(0, 0);
mod4_voltage = inputs[MOD4_INPUT].getNormalVoltage(0, 0);
}

void process_buttons() {
// process a button press for oscillator 1
if (oscillator1Trigger.process(params[WAVEFORM1_PARAM].getValue())) {
osc1_state = (osc1_state + 1) % 6;
if (osc1_state < 5) {
oscillator1.setPulseWidth(SQUARE_PWM_VALUES[osc1_state]);
}
}
// set PWM for oscillator 1 based on oscillator 4 frequency control
if (osc1_state == 5) {
oscillator1.setPulseWidth(inputs[FREQ4_INPUT].getNormalVoltage(10.f) / 10.f);
}

// process a button press for oscillator 2
if (oscillator2Trigger.process(params[WAVEFORM2_PARAM].getValue())) {
osc2_state = (osc2_state + 1) % 6;
if (osc2_state < 5) {
oscillator2.setPulseWidth(SQUARE_PWM_VALUES[osc2_state]);
}
}
// set PWM for oscillator 2 based on oscillator 4 frequency control
if (osc2_state == 5) {
oscillator2.setPulseWidth(inputs[FREQ4_INPUT].getNormalVoltage(10.f) / 10.f);
}

// process a button press for oscillator 3
if (oscillator3Trigger.process(params[WAVEFORM3_PARAM].getValue())) {
osc3_state = (osc3_state + 1) % 6;
if (osc3_state < 5) {
oscillator3.setPulseWidth(SQUARE_PWM_VALUES[osc3_state]);
}
}
// set PWM for oscillator 3 based on oscillator 4 frequency control
if (osc3_state == 5) {
oscillator3.setPulseWidth(inputs[FREQ4_INPUT].getNormalVoltage(10.f) / 10.f);
}

// process a button press for oscillator 4
if (oscillator4Trigger.process(params[WAVEFORM4_PARAM].getValue())) {
oscillator4.nextShape();
}
}

void process_xmod_switches() {
oscillator2.syncEnabled = params[XMOD1_PARAM].getValue();
oscillator2.ringModulation = params[XMOD2_PARAM].getValue();
oscillator3.ringModulation = params[XMOD3_PARAM].getValue();
}

void process_frequency(float sampleTime, int sampleRate) {
// set the pitch of oscillator 1
float_4 pitch1 = params[FREQ1_PARAM].getValue() / 12.f;
pitch1 += voct1_voltage;
pitch1 += mod1_voltage;
oscillator1.setPitch(pitch1);
// set the pitch of oscillator 2
float_4 pitch2 = params[FREQ2_PARAM].getValue() / 12.f;
pitch2 += voct2_voltage;
pitch2 += mod2_voltage;
oscillator2.setPitch(pitch2);
// set the pitch of oscillator 3
float_4 pitch3 = params[FREQ3_PARAM].getValue() / 12.f;
pitch3 += voct3_voltage;
pitch3 += mod3_voltage;
oscillator3.setPitch(pitch3);
// set the pitch of oscillator 4
// get the pitch for the 4th oscillator (12 notes/octave * 1V/octave)
auto twelve_volt_octave = 12 * (voct4_voltage + mod4_voltage);
// 61 is the base for C4
uint16_t pitch4_int = 61 + params[FREQ4_PARAM].getValue() + twelve_volt_octave;
// get the microtone for the 4th oscillator as 7-bit integer
uint16_t pitch4_frac = 128 * (params[FREQ4_PARAM].getValue() - static_cast<int>(params[FREQ4_PARAM].getValue()));
pitch4_frac += 128 * (twelve_volt_octave - static_cast<int>(twelve_volt_octave));
// set the pitch of oscillator 4
oscillator4.setPitch((pitch4_int << 7) + pitch4_frac + 64);

// Process the output voltage of each oscillator
oscillator1.process(sampleTime);
// sync oscillator 2 to oscillator 1, ring mod with oscillator 1
// (hard sync is only applied if oscillator2.syncEnabled is true)
// (ring mod is only applied if oscillator2.ringModulation is true)
oscillator2.process(sampleTime, oscillator1.sqr(), oscillator1.sqr());
// don't sync oscillator 3, ring mod with oscillator 1
// (ring mod is only applied if oscillator3.ringModulation is true)
oscillator3.process(sampleTime, 0, oscillator1.sqr());
oscillator4.process(sampleRate);
}

void process_output() {
// set outputs for channel 1 if connected
if (outputs[WAVEFORM1_OUTPUT].isConnected())
outputs[WAVEFORM1_OUTPUT].setVoltageSimd(is_gate1_open * 5.f * oscillator1.sqr(), 0);
// set outputs for channel 2 if connected
if (outputs[WAVEFORM2_OUTPUT].isConnected())
outputs[WAVEFORM2_OUTPUT].setVoltageSimd(is_gate2_open * 5.f * oscillator2.sqr(), 0);
// set outputs for channel 3 if connected
if (outputs[WAVEFORM3_OUTPUT].isConnected())
outputs[WAVEFORM3_OUTPUT].setVoltageSimd(is_gate3_open * 5.f * oscillator3.sqr(), 0);
// set outputs for channel 4 if connected
if (outputs[WAVEFORM4_OUTPUT].isConnected())
outputs[WAVEFORM4_OUTPUT].setVoltageSimd(is_gate4_open * 5.f * oscillator4.getValue(), 0);
// create the mixed output if connected
if (outputs[MIX_OUTPUT].isConnected()) {
auto the_mix = is_gate1_open * oscillator1.sqr() * !outputs[WAVEFORM1_OUTPUT].isConnected() * params[LEVEL1_PARAM].getValue();
the_mix += is_gate2_open * oscillator2.sqr() * !outputs[WAVEFORM2_OUTPUT].isConnected() * params[LEVEL2_PARAM].getValue();
the_mix += is_gate3_open * oscillator3.sqr() * !outputs[WAVEFORM3_OUTPUT].isConnected() * params[LEVEL3_PARAM].getValue();
the_mix += is_gate4_open * oscillator4.getValue() * !outputs[WAVEFORM4_OUTPUT].isConnected() * params[LEVEL4_PARAM].getValue();
outputs[MIX_OUTPUT].setVoltageSimd(5.f * the_mix, 0);
}
}

void process(const ProcessArgs &args) override {
process_gate_inputs();
process_voct_inputs();
process_mod_inputs();
process_buttons();
process_xmod_switches();
process_frequency(args.sampleTime, args.sampleRate);
process_output();
}
};

// ---------------------------------------------------------------------------
// MARK: Widget
// ---------------------------------------------------------------------------

/// A menu item for controlling the quantization feature
template<typename T>
struct EdgesQuantizerItem : MenuItem {
/// the oscillator to control the quantization of
T *oscillator;

/// Respond to a menu action.
inline void onAction(const event::Action &e) override {
oscillator->isQuantized = not oscillator->isQuantized;
}

/// Perform a step on the menu.
inline void step() override {
rightText = oscillator->isQuantized ? "âś”" : "";
MenuItem::step();
}
};

/// The widget structure that lays out the panel of the module and the UI menus.
struct EdgesWidget : ModuleWidget {
EdgesWidget(Edges *module) {
setModule(module);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Edges.svg")));
// add vanity screws
addChild(createWidget<ScrewSilver>(Vec(15, 0)));
addChild(createWidget<ScrewSilver>(Vec(270, 0)));
addChild(createWidget<ScrewSilver>(Vec(15, 365)));
addChild(createWidget<ScrewSilver>(Vec(270, 365)));
// add frequency knobs
addParam(createParam<Rogan1PSWhite>(Vec(122, 54), module, Edges::FREQ1_PARAM));
addParam(createParam<Rogan1PSWhite>(Vec(122, 114), module, Edges::FREQ2_PARAM));
addParam(createParam<Rogan1PSWhite>(Vec(122, 174), module, Edges::FREQ3_PARAM));
addParam(createParam<Rogan1PSWhite>(Vec(122, 234), module, Edges::FREQ4_PARAM));
// add XMOD switches
addParam(createParam<CKSS>(Vec(178, 64), module, Edges::XMOD1_PARAM));
addParam(createParam<CKSS>(Vec(178, 124), module, Edges::XMOD2_PARAM));
addParam(createParam<CKSS>(Vec(178, 184), module, Edges::XMOD3_PARAM));
// add level knobs
addParam(createParam<Rogan1PSGreen>(Vec(245, 54), module, Edges::LEVEL1_PARAM));
addParam(createParam<Rogan1PSGreen>(Vec(245, 114), module, Edges::LEVEL2_PARAM));
addParam(createParam<Rogan1PSGreen>(Vec(245, 174), module, Edges::LEVEL3_PARAM));
addParam(createParam<Rogan1PSGreen>(Vec(245, 234), module, Edges::LEVEL4_PARAM));
// add waveform selection buttons
addParam(createParam<TL1105>(Vec(43, 320), module, Edges::WAVEFORM1_PARAM));
addParam(createParam<TL1105>(Vec(67, 320), module, Edges::WAVEFORM2_PARAM));
addParam(createParam<TL1105>(Vec(91, 320), module, Edges::WAVEFORM3_PARAM));
addParam(createParam<TL1105>(Vec(115, 320), module, Edges::WAVEFORM4_PARAM));
// add inputs for Gate
addInput(createInput<PJ301MPort>(Vec(20, 62), module, Edges::GATE1_INPUT));
addInput(createInput<PJ301MPort>(Vec(20, 122), module, Edges::GATE2_INPUT));
addInput(createInput<PJ301MPort>(Vec(20, 182), module, Edges::GATE3_INPUT));
addInput(createInput<PJ301MPort>(Vec(20, 242), module, Edges::GATE4_INPUT));
// add inputs for Frequency (1V/Oct)
addInput(createInput<PJ301MPort>(Vec(58, 62), module, Edges::FREQ1_INPUT));
addInput(createInput<PJ301MPort>(Vec(58, 122), module, Edges::FREQ2_INPUT));
addInput(createInput<PJ301MPort>(Vec(58, 182), module, Edges::FREQ3_INPUT));
addInput(createInput<PJ301MPort>(Vec(58, 242), module, Edges::FREQ4_INPUT));
// add inputs for Mod
addInput(createInput<PJ301MPort>(Vec(90, 62), module, Edges::MOD1_INPUT));
addInput(createInput<PJ301MPort>(Vec(90, 122), module, Edges::MOD2_INPUT));
addInput(createInput<PJ301MPort>(Vec(90, 182), module, Edges::MOD3_INPUT));
addInput(createInput<PJ301MPort>(Vec(90, 242), module, Edges::MOD4_INPUT));
// add outputs for each waveform
addOutput(createOutput<PJ301MPort>(Vec(215, 62), module, Edges::WAVEFORM1_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(215, 122), module, Edges::WAVEFORM2_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(215, 182), module, Edges::WAVEFORM3_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(215, 242), module, Edges::WAVEFORM4_OUTPUT));
// add output for mix (all waveforms mixed)
addOutput(createOutput<PJ301MPort>(Vec(253, 315), module, Edges::MIX_OUTPUT));
// add an LED for each waveform selection button
addChild(createLight<MediumLight<GreenRedLight>>(Vec(46, 300), module, Edges::GATE1_LIGHT_GREEN));
addChild(createLight<MediumLight<GreenRedLight>>(Vec(70, 300), module, Edges::GATE2_LIGHT_GREEN));
addChild(createLight<MediumLight<GreenRedLight>>(Vec(94, 300), module, Edges::GATE3_LIGHT_GREEN));
addChild(createLight<MediumLight<GreenRedLight>>(Vec(118, 300), module, Edges::GATE4_LIGHT_GREEN));
}

void appendContextMenu(Menu *menu) override {
Edges *module = dynamic_cast<Edges*>(this->module);
assert(module);
// add the menu for the Quantizer feature
menu->addChild(construct<MenuLabel>());
menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Quantizer"));
menu->addChild(construct<EdgesQuantizerItem<Edges::Square>>(
&MenuItem::text, "Channel 1",
&EdgesQuantizerItem<Edges::Square>::oscillator, &module->oscillator1
));
menu->addChild(construct<EdgesQuantizerItem<Edges::Square>>(
&MenuItem::text, "Channel 2",
&EdgesQuantizerItem<Edges::Square>::oscillator, &module->oscillator2
));
menu->addChild(construct<EdgesQuantizerItem<Edges::Square>>(
&MenuItem::text, "Channel 3",
&EdgesQuantizerItem<Edges::Square>::oscillator, &module->oscillator3
));
menu->addChild(construct<EdgesQuantizerItem<Edges::Digital>>(
&MenuItem::text, "Channel 4",
&EdgesQuantizerItem<Edges::Digital>::oscillator, &module->oscillator4
));
}
};


Model *modelEdges = createModel<Edges, EdgesWidget>("Edges");

+ 378
- 0
src/Edges/digital_oscillator.hpp View File

@@ -0,0 +1,378 @@

#include "wavetables.hpp"
#include <algorithm>

#ifndef DIGITAL_OSCILLATOR
#define DIGITAL_OSCILLATOR

// ---------------------------------------------------------------------------
// MARK: 24-bit floating point
// ---------------------------------------------------------------------------

/// A 24-bit floating point number
struct uint24_t {
/// the integral piece of the number
uint16_t integral;
/// the fractional piece of the number
uint8_t fractional;
};

/// Return the sum of two uint24_t values.
///
/// @param left the left operand
/// @param right the right operand
/// @returns the sum of the left and right operands
///
static inline uint24_t operator+(uint24_t left, uint24_t right) {
uint24_t result;
uint32_t leftv = (static_cast<uint32_t>(left.integral) << 8) + left.fractional;
uint32_t rightv = (static_cast<uint32_t>(right.integral) << 8) + right.fractional;
uint32_t sum = leftv + rightv;
result.integral = sum >> 8;
result.fractional = sum & 0xff;
return result;
}

/// Perform a right shift operation in place.
///
/// @param value a reference to the uint24_t to shift right in place
/// @returns a reference to value
///
static inline uint24_t& operator>>=(uint24_t& value, uint8_t num_shifts) {
while (num_shifts--) {
uint32_t av = static_cast<uint32_t>(value.integral) << 8;
av += value.fractional;
av >>= 1;
value.integral = av >> 8;
value.fractional = av & 0xff;
}
return value;
}

// ---------------------------------------------------------------------------
// MARK: Interpolation
// ---------------------------------------------------------------------------

/// Mix two 8-bit values.
///
/// @param a the first value to mix
/// @param b the second value to mix
/// @param balance the mix between a (0) and b (255)
///
static inline uint8_t mix(uint16_t a, uint16_t b, uint8_t balance) {
return (a * (255 - balance) + b * balance) >> 8;
}

/// Interpolate between a sample from a wave table.
///
/// @param table a pointer to the table to lookup samples in
/// @param phase the current phase in the wave table
///
static inline uint8_t interpolate(const uint8_t* table, uint16_t phase) {
auto index = phase >> 7;
return mix(table[index], table[index + 1], phase & 0xff);
}

/// Interpolate between two wave tables.
///
/// @param table_a the first wave table
/// @param table_b the second wave table
/// @param phase the phase in the wave table
/// @param gain_a the gain for the first wave
/// @param gain_b the gain for the second wave
/// @returns a value interpolated between the two wave tables at the given phase
///
static inline uint16_t interpolate(
const uint8_t* table_a,
const uint8_t* table_b,
uint16_t phase,
uint8_t gain_a,
uint8_t gain_b
) {
uint16_t result = 0;
result += interpolate(table_a, phase) * gain_a;
result += interpolate(table_b, phase) * gain_b;
return result;
}

// ---------------------------------------------------------------------------
// MARK: Digital Oscillator
// ---------------------------------------------------------------------------

/// The wave shapes for the digital oscillator
enum OscillatorShape {
OSC_SINE = 0,
OSC_TRIANGLE,
OSC_NES_TRIANGLE,
OSC_PITCHED_NOISE,
OSC_NES_NOISE_LONG,
OSC_NES_NOISE_SHORT,
NUM_DIGITAL_OSC // the total number of oscillators in the enumeration
};

/// TODO: document
static const uint8_t kMaxZone = 7;
/// TODO: document
static const int16_t kOctave = 12 * 128;
/// TODO: document
static const int16_t kPitchTableStart = 116 * 128;

/// the native sample rate of the digital oscillator
static const float SAMPLE_RATE = 48000.f;

/// A digital oscillator with triangle, NES triangle, NES noise, hold & sample
/// noise, and sine wave shapes.
template<typename T>
class DigitalOscillator {
public:
/// whether note quantization is enabled
bool isQuantized = false;
/// Initialize a new digital oscillator.
DigitalOscillator() { intialize(); }

/// Destroy an existing digital oscillator.
~DigitalOscillator() { }

/// Initialize the digital oscillator
void intialize() {
// set the pitch to the default value
setPitch(60 << 7);
// reset the wave shape to the first value
shape_ = OSC_SINE;
// set the gate to true (i.e., open)
setGate(true);
// reset the random number seed to 1
rng_state_ = 1;
}

/// Set the wave shape to a new value.
///
/// @param shape_ the wave shape to select
///
inline void setShape(OscillatorShape shape) { shape_ = shape; }

/// Select the next wave shape.
inline void nextShape() {
auto next_shape = (static_cast<int>(shape_) + 1) % NUM_DIGITAL_OSC;
shape_ = static_cast<OscillatorShape>(next_shape);
}

/// Set the pitch to a new value.
///
/// @param pitch_ the pitch of the oscillator
///
inline void setPitch(int16_t pitch) {
if (isQuantized) pitch = pitch & 0b1111111110000000;
pitch_ = pitch;
}

/// Set the gate to a new value.
///
/// @param gate_ the gate of the oscillator
///
inline void setGate(bool gate) { gate_ = gate; }

/// Set the CV Pulse Width to a new value.
///
/// @param cv_pw_ the CV parameter for the pulse width
///
inline void set_cv_pw(uint8_t cv_pw) { cv_pw_ = cv_pw; }

/// Render a sample from the oscillator using current parameters.
void process(int sampleRate) {
if (gate_) {
computePhaseIncrement(sampleRate);
(this->*getRenderFunction(shape_))();
} else {
renderSilence();
}
}

/// Get the value from the oscillator in the range [-1.0, 1.0]
inline T getValue() const {
// divide the 12-bit value by 4096.0 to normalize in [0.0, 1.0]
// multiply by 2 and subtract 1 to get the value in [-1.0, 1.0]
return 2 * (value / 4096.0) - 1;
}

private:
/// a type for calling and storing render functions for the wave shapes
typedef void (DigitalOscillator::*RenderFunction)();

/// Get the render function for the given shape.
///
/// @param shape the wave shape to return the render function for
/// @returns the render function for the given wave shape
///
static inline RenderFunction getRenderFunction(OscillatorShape shape) {
// create a static constant array of the functions
static const RenderFunction function_table[] = {
&DigitalOscillator::renderSine,
&DigitalOscillator::renderBandlimitedTriangle,
&DigitalOscillator::renderBandlimitedTriangle,
&DigitalOscillator::renderNoise,
&DigitalOscillator::renderNoiseNES,
&DigitalOscillator::renderNoiseNES
};
// lookup the wave using the shape enumeration
return function_table[shape];
}

/// the current shape of the wave produced by the oscillator
OscillatorShape shape_ = OSC_TRIANGLE;
/// the current pitch of the oscillator
int16_t pitch_ = 60 << 7;
/// the 12TET quantized note based on the oscillator pitch
uint8_t note_ = 0;
/// whether the gate is open
bool gate_ = true;

/// the phase the oscillator is currently at
uint24_t phase_;
/// the change inn phase at the current processing step
uint24_t phase_increment_;
/// The random number generator state for generating random noise
uint16_t rng_state_ = 1;
/// A sample from the sine wave to use for the random noise generators
uint16_t sample_ = 0;
/// The auxiliary phase for the sine wave bit crusher
uint16_t aux_phase_ = 0;
/// The CV PW value for the sine wave bit crusher
uint8_t cv_pw_ = 0;

/// The output value from the oscillator
uint16_t value = 0;

/// Compute the increment in phased for the current step.
///
/// @param sampleRate the sample rate to output audio at
///
void computePhaseIncrement(int sampleRate) {
int16_t ref_pitch = pitch_ - kPitchTableStart;
uint8_t num_shifts = shape_ >= OSC_PITCHED_NOISE ? 0 : 1;
while (ref_pitch < 0) {
ref_pitch += kOctave;
++num_shifts;
}

uint24_t increment;
uint16_t pitch_lookup_index_integral = ref_pitch >> 4;
uint8_t pitch_lookup_index_fractional = ref_pitch << 4;

uint16_t increment16 = lut_res_oscillator_increments[pitch_lookup_index_integral];
uint16_t increment16_next = lut_res_oscillator_increments[pitch_lookup_index_integral + 1];
// set the integral and fractional of the increment
uint32_t increment16_diff = increment16_next - increment16;
uint32_t pitch32 = pitch_lookup_index_fractional;
uint16_t integral_diff = (increment16_diff * pitch32) >> 8;
// set the integral based o the current sample rate
increment.integral = (SAMPLE_RATE / sampleRate) * increment16 + integral_diff;
increment.fractional = 0;
increment >>= num_shifts;

// shift the 15-bit pitch over 7 bits to produce a byte, 12 is the min value
note_ = std::max(pitch_ >> 7, 12);
phase_increment_ = increment;
}

/// Run the sample loop with given callback for the loop body.
///
/// @param render_fn a callback function that accepts the phase and phase
/// increment as parameters
///
template<typename Callable>
inline void renderWrapper(Callable render_fn) {
uint24_t phase;
uint24_t phase_increment;
phase_increment.integral = phase_increment_.integral;
phase_increment.fractional = phase_increment_.fractional;
phase.integral = phase_.integral;
phase.fractional = phase_.fractional;
render_fn(phase, phase_increment);
phase_.integral = phase.integral;
phase_.fractional = phase.fractional;
}

/// Render silence from the oscillator.
inline void renderSilence() { value = 0; }

/// Render a sine wave from the oscillator.
void renderSine() {
uint16_t aux_phase_increment = lut_res_bitcrusher_increments[cv_pw_];
renderWrapper([&, this](uint24_t& phase, uint24_t& phase_increment) {
phase = phase + phase_increment;
aux_phase_ += aux_phase_increment;
if (aux_phase_ < aux_phase_increment || !aux_phase_increment) {
sample_ = interpolate(wav_res_bandlimited_triangle_6, phase.integral) << 8;
}
value = sample_ >> 4;
});
}

/// Render a triangle wave from the oscillator.
void renderBandlimitedTriangle() {
uint8_t balance_index = ((note_ - 12) << 4) | ((note_ - 12) >> 4);
uint8_t gain_2 = balance_index & 0xf0;
uint8_t gain_1 = ~gain_2;

uint8_t wave_index = balance_index & 0xf;
uint8_t base_resource_id = (shape_ == OSC_NES_TRIANGLE)
? WAV_RES_BANDLIMITED_NES_TRIANGLE_0
: WAV_RES_BANDLIMITED_TRIANGLE_0;

const uint8_t* wave_1 = waveform_table[base_resource_id + wave_index];
wave_index = std::min<uint8_t>(wave_index + 1, kMaxZone);
const uint8_t* wave_2 = waveform_table[base_resource_id + wave_index];

renderWrapper([&](uint24_t& phase, uint24_t& phase_increment) {
phase = phase + phase_increment;
uint16_t sample = interpolate(wave_1, wave_2, phase.integral, gain_1, gain_2);
value = sample >> 4;
});
}

/// Render NES noise from the oscillator.
void renderNoiseNES() {
uint16_t rng_state = rng_state_;
uint16_t sample = sample_;
renderWrapper([&, this](uint24_t& phase, uint24_t& phase_increment) {
phase = phase + phase_increment;
if (phase.integral < phase_increment.integral) {
uint8_t tap = rng_state >> 1;
if (shape_ == OSC_NES_NOISE_SHORT) {
tap >>= 5;
}
uint8_t random_bit = (rng_state ^ tap) & 1;
rng_state >>= 1;
if (random_bit) {
rng_state |= 0x4000;
sample = 0x0300;
} else {
sample = 0x0cff;
}
}
value = sample;
});
rng_state_ = rng_state;
sample_ = sample;
}

/// Render sample and hold noise from the oscillator.
void renderNoise() {
uint16_t rng_state = rng_state_;
uint16_t sample = sample_;
renderWrapper([&](uint24_t& phase, uint24_t& phase_increment) {
phase = phase + phase_increment;
if (phase.integral < phase_increment.integral) {
rng_state = (rng_state >> 1) ^ (-(rng_state & 1) & 0xb400);
sample = rng_state & 0x0fff;
sample = 512 + ((sample * 3) >> 2);
}
value = sample;
});
rng_state_ = rng_state;
sample_ = sample;
}
};

#endif // DIGITAL_OSCILLATOR

+ 169
- 0
src/Edges/square_oscillator.hpp View File

@@ -0,0 +1,169 @@

#ifndef SQUARE_OSCILLATOR_HPP
#define SQUARE_OSCILLATOR_HPP

// ---------------------------------------------------------------------------
// MARK: Square Oscillator
// ---------------------------------------------------------------------------

/// An oscillator that generates a square wave
template <int OVERSAMPLE, int QUALITY, typename T>
struct SquareWaveOscillator {
/// whether the oscillator is emulating an analog oscillator
bool analog = false;
/// TODO: document
bool soft = false;
/// whether the oscillator is synced to another oscillator
bool syncEnabled = false;
/// Whether ring modulation is enabled
bool ringModulation = false;
/// whether note quantization is enabled
bool isQuantized = false;
// For optimizing in serial code
int channels = 0;

/// the value from the last oscillator synchronization
T lastSyncValue = 0.f;
/// the current phase in [0, 1] (2 * pi * phase)
T phase = 0.f;
/// the current frequency
T freq;
/// the current pulse width in [0, 1]
T pulseWidth = 0.5f;
/// the direction of the synchronization
T syncDirection = 1.f;

/// a filter for producing an analog effect on the square wave
dsp::TRCFilter<T> sqrFilter;

/// the minimum-phase band-limited step generator for preventing aliasing
dsp::MinBlepGenerator<QUALITY, OVERSAMPLE, T> sqrMinBlep;

/// The current value of the square wave
T sqrValue = 0.f;

/// Set the pitch of the oscillator to a new value.
///
/// @param pitch the new pitch to set the oscillator to
///
inline void setPitch(T pitch) {
// quantized the pitch to semitone if enabled
if (isQuantized) pitch = floor(pitch * 12) / 12.f;
// set the frequency based on the pitch
freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30) / 1073741824;
}

/// Set the pulse width of the square wave to a new value.
///
/// @param pulseWidth the new pulse width to set the square wave to
///
inline void setPulseWidth(T pulseWidth) {
const float pwMin = 0.01f;
this->pulseWidth = simd::clamp(pulseWidth, pwMin, 1.f - pwMin);
}

/// Process a sample for given change in time and sync value.
///
/// @param deltaTime the change in time between samples
/// @param syncValue the value of the oscillator to sync to
/// @param modulator the value of the oscillator applying ring modulation
///
void process(float deltaTime, T syncValue = 0, T modulator = 1) {
// Advance phase
T deltaPhase = simd::clamp(freq * deltaTime, 1e-6f, 0.35f);
if (soft) // Reverse direction
deltaPhase *= syncDirection;
else // Reset back to forward
syncDirection = 1.f;
phase += deltaPhase;
// Wrap phase
phase -= simd::floor(phase);

// Jump sqr when crossing 0, or 1 if backwards
T wrapPhase = (syncDirection == -1.f) & 1.f;
T wrapCrossing = (wrapPhase - (phase - deltaPhase)) / deltaPhase;
int wrapMask = simd::movemask((0 < wrapCrossing) & (wrapCrossing <= 1.f));
if (wrapMask) {
for (int i = 0; i < channels; i++) {
if (wrapMask & (1 << i)) {
T mask = simd::movemaskInverse<T>(1 << i);
float p = wrapCrossing[i] - 1.f;
T x = mask & (2.f * syncDirection);
sqrMinBlep.insertDiscontinuity(p, x);
}
}
}

// Jump sqr when crossing `pulseWidth`
T pulseCrossing = (pulseWidth - (phase - deltaPhase)) / deltaPhase;
int pulseMask = simd::movemask((0 < pulseCrossing) & (pulseCrossing <= 1.f));
if (pulseMask) {
for (int i = 0; i < channels; i++) {
if (pulseMask & (1 << i)) {
T mask = simd::movemaskInverse<T>(1 << i);
float p = pulseCrossing[i] - 1.f;
T x = mask & (-2.f * syncDirection);
sqrMinBlep.insertDiscontinuity(p, x);
}
}
}

// Detect sync
// Might be NAN or outside of [0, 1) range
if (syncEnabled) {
T deltaSync = syncValue - lastSyncValue;
T syncCrossing = -lastSyncValue / deltaSync;
lastSyncValue = syncValue;
T sync = (0.f < syncCrossing) & (syncCrossing <= 1.f) & (syncValue >= 0.f);
int syncMask = simd::movemask(sync);
if (syncMask) {
if (soft) {
syncDirection = simd::ifelse(sync, -syncDirection, syncDirection);
}
else {
T newPhase = simd::ifelse(sync, (1.f - syncCrossing) * deltaPhase, phase);
// Insert minBLEP for sync
for (int i = 0; i < channels; i++) {
if (syncMask & (1 << i)) {
T mask = simd::movemaskInverse<T>(1 << i);
float p = syncCrossing[i] - 1.f;
T x;
x = mask & (sqr(newPhase) - sqr(phase));
sqrMinBlep.insertDiscontinuity(p, x);
}
}
phase = newPhase;
}
}
}

// process the square wave value
sqrValue = sqr(phase);
sqrValue += sqrMinBlep.process();

if (analog) { // apply an analog filter
sqrFilter.setCutoffFreq(20.f * deltaTime);
sqrFilter.process(sqrValue);
sqrValue = sqrFilter.highpass() * 0.95f;
}

if (ringModulation) { // apply ring modulation
sqrValue *= modulator;
}
}

/// Calculate and return the value of the square wave for given phase.
///
/// @param phase the phase of the wave in [0, 1]
/// @returns the value of the wave in [0, 1]
///
inline T sqr(T phase) { return simd::ifelse(phase < pulseWidth, 1.f, -1.f); }

/// Return the value of the square wave.
///
/// @returns the value of the wave in [0, 1]
///
inline T sqr() const { return sqrValue; }
};

#endif // SQUARE_OSCILLATOR_HPP

+ 1020
- 0
src/Edges/wavetables.hpp
File diff suppressed because it is too large
View File


+ 1
- 0
src/plugin.cpp View File

@@ -24,4 +24,5 @@ void init(rack::Plugin* p) {
p->addModel(modelMarbles);
p->addModel(modelStages);
p->addModel(modelRipples);
p->addModel(modelEdges);
}

+ 1
- 0
src/plugin.hpp View File

@@ -24,3 +24,4 @@ extern Model* modelFrames;
extern Model* modelStages;
extern Model* modelMarbles;
extern Model* modelRipples;
extern Model *modelEdges;

Loading…
Cancel
Save