diff --git a/src/AudibleInstruments.cpp b/src/AudibleInstruments.cpp index 6df5685..071a47f 100644 --- a/src/AudibleInstruments.cpp +++ b/src/AudibleInstruments.cpp @@ -19,6 +19,7 @@ struct AudibleInstrumentsPlugin : Plugin { createModel(this, "Braids", "Macro Oscillator"); createModel(this, "Elements", "Modal Synthesizer"); createModel(this, "Tides", "Tidal Modulator"); + createModel(this, "Sheep", "Wavetable Oscillator"); // createModel(this, "Streams", "Dual Dynamics Gate"); createModel(this, "Clouds", "Texture Synthesizer"); createModel(this, "Warps", "Meta Modulator"); diff --git a/src/AudibleInstruments.hpp b/src/AudibleInstruments.hpp index f2a75dd..39f5ff4 100644 --- a/src/AudibleInstruments.hpp +++ b/src/AudibleInstruments.hpp @@ -21,6 +21,10 @@ struct TidesWidget : ModuleWidget { TidesWidget(); }; +struct SheepWidget : ModuleWidget { + SheepWidget(); +}; + struct StreamsWidget : ModuleWidget { StreamsWidget(); }; diff --git a/src/Sheep.cpp b/src/Sheep.cpp new file mode 100644 index 0000000..31f18e7 --- /dev/null +++ b/src/Sheep.cpp @@ -0,0 +1,236 @@ +#include "AudibleInstruments.hpp" +#include + +#define WAVETABLE_HACK +#include "tides/generator.h" + + +struct Sheep : Module { + enum ParamIds { + MODE_PARAM, + RANGE_PARAM, + + FREQUENCY_PARAM, + FM_PARAM, + + SHAPE_PARAM, + SLOPE_PARAM, + SMOOTHNESS_PARAM, + NUM_PARAMS + }; + enum InputIds { + SHAPE_INPUT, + SLOPE_INPUT, + SMOOTHNESS_INPUT, + + TRIG_INPUT, + FREEZE_INPUT, + PITCH_INPUT, + FM_INPUT, + LEVEL_INPUT, + + CLOCK_INPUT, + NUM_INPUTS + }; + enum OutputIds { + HIGH_OUTPUT, + LOW_OUTPUT, + UNI_OUTPUT, + BI_OUTPUT, + NUM_OUTPUTS + }; + + tides::Generator generator; + float lights[3] = {}; + int frame = 0; + uint8_t lastGate; + SchmittTrigger modeTrigger; + SchmittTrigger rangeTrigger; + + Sheep(); + void step(); + + json_t *toJson() { + json_t *rootJ = json_object(); + + json_object_set_new(rootJ, "mode", json_integer((int) generator.mode())); + json_object_set_new(rootJ, "range", json_integer((int) generator.range())); + + return rootJ; + } + + void fromJson(json_t *rootJ) { + json_t *modeJ = json_object_get(rootJ, "mode"); + if (modeJ) { + generator.set_mode((tides::GeneratorMode) json_integer_value(modeJ)); + } + + json_t *rangeJ = json_object_get(rootJ, "range"); + if (rangeJ) { + generator.set_range((tides::GeneratorRange) json_integer_value(rangeJ)); + } + } + + void initialize() { + generator.set_range(tides::GENERATOR_RANGE_MEDIUM); + generator.set_mode(tides::GENERATOR_MODE_LOOPING); + } + + void randomize() { + generator.set_range((tides::GeneratorRange) (randomu32() % 3)); + generator.set_mode((tides::GeneratorMode) (randomu32() % 3)); + } +}; + + +Sheep::Sheep() { + params.resize(NUM_PARAMS); + inputs.resize(NUM_INPUTS); + outputs.resize(NUM_OUTPUTS); + + memset(&generator, 0, sizeof(generator)); + generator.Init(); + generator.set_sync(false); + initialize(); +} + +void Sheep::step() { + // TODO Save / load the state of MODE and RANGE to JSON + tides::GeneratorMode mode = generator.mode(); + if (modeTrigger.process(params[MODE_PARAM])) { + mode = (tides::GeneratorMode) (((int)mode - 1 + 3) % 3); + generator.set_mode(mode); + } + lights[0] = (float)mode; + + tides::GeneratorRange range = generator.range(); + if (rangeTrigger.process(params[RANGE_PARAM])) { + range = (tides::GeneratorRange) (((int)range - 1 + 3) % 3); + generator.set_range(range); + } + lights[2] = (float)range; + + // Buffer loop + if (++frame >= 16) { + frame = 0; + + // Pitch + float pitch = params[FREQUENCY_PARAM]; + pitch += 12.0 * getf(inputs[PITCH_INPUT]); + pitch += params[FM_PARAM] * getf(inputs[FM_INPUT], 0.1) / 5.0; + pitch += 60.0; + // Scale to the global sample rate + pitch += log2f(48000.0 / gSampleRate) * 12.0; + generator.set_pitch(clampf(pitch * 0x80, -0x8000, 0x7fff)); + + // Slope, smoothness, pitch + int16_t shape = clampf(params[SHAPE_PARAM] + getf(inputs[SHAPE_INPUT]) / 5.0, -1.0, 1.0) * 0x7fff; + int16_t slope = clampf(params[SLOPE_PARAM] + getf(inputs[SLOPE_INPUT]) / 5.0, -1.0, 1.0) * 0x7fff; + int16_t smoothness = clampf(params[SMOOTHNESS_PARAM] + getf(inputs[SMOOTHNESS_INPUT]) / 5.0, -1.0, 1.0) * 0x7fff; + generator.set_shape(shape); + generator.set_slope(slope); + generator.set_smoothness(smoothness); + + // Sync + // Slight deviation from spec here. + // Instead of toggling sync by holding the range button, just enable it if the clock port is plugged in. + generator.set_sync(inputs[CLOCK_INPUT]); + + // Generator + generator.Process(); + } + + // Level + uint16_t level = clampf(getf(inputs[LEVEL_INPUT], 8.0) / 8.0, 0.0, 1.0) * 0xffff; + if (level < 32) + level = 0; + + uint8_t gate = 0; + if (getf(inputs[FREEZE_INPUT]) >= 0.7) + gate |= tides::CONTROL_FREEZE; + if (getf(inputs[TRIG_INPUT]) >= 0.7) + gate |= tides::CONTROL_GATE; + if (getf(inputs[CLOCK_INPUT]) >= 0.7) + gate |= tides::CONTROL_CLOCK; + if (!(lastGate & tides::CONTROL_CLOCK) && (gate & tides::CONTROL_CLOCK)) + gate |= tides::CONTROL_GATE_RISING; + if (!(lastGate & tides::CONTROL_GATE) && (gate & tides::CONTROL_GATE)) + gate |= tides::CONTROL_GATE_RISING; + if ((lastGate & tides::CONTROL_GATE) && !(gate & tides::CONTROL_GATE)) + gate |= tides::CONTROL_GATE_FALLING; + lastGate = gate; + + const tides::GeneratorSample& sample = generator.Process(gate); + uint32_t uni = sample.unipolar; + int32_t bi = sample.bipolar; + + uni = uni * level >> 16; + bi = -bi * level >> 16; + float unif = rescalef(uni, 0, 0xffff, 0.0, 8.0); + float bif = rescalef(bi, -0x8000, 0x7fff, -5.0, 5.0); + + setf(outputs[HIGH_OUTPUT], sample.flags & tides::FLAG_END_OF_ATTACK ? 0.0 : 5.0); + setf(outputs[LOW_OUTPUT], sample.flags & tides::FLAG_END_OF_RELEASE ? 0.0 : 5.0); + setf(outputs[UNI_OUTPUT], unif); + setf(outputs[BI_OUTPUT], bif); + + lights[1] = (sample.flags & tides::FLAG_END_OF_ATTACK ? -unif : unif) / 8.0; +} + + +struct SheepModeLight : ModeValueLight { + SheepModeLight() { + addColor(COLOR_RED); + addColor(COLOR_BLACK_TRANSPARENT); + addColor(COLOR_CYAN); + } +}; + + +SheepWidget::SheepWidget() { + Sheep *module = new Sheep(); + setModule(module); + box.size = Vec(15 * 14, 380); + + { + Panel *panel = new LightPanel(); + panel->backgroundImage = Image::load("plugins/AudibleInstruments/res/Tides.png"); + panel->box.size = box.size; + addChild(panel); + } + + addChild(createScrew(Vec(15, 0))); + addChild(createScrew(Vec(180, 0))); + addChild(createScrew(Vec(15, 365))); + addChild(createScrew(Vec(180, 365))); + + addParam(createParam(Vec(19, 52), module, Sheep::MODE_PARAM, 0.0, 1.0, 0.0)); + addParam(createParam(Vec(19, 93), module, Sheep::RANGE_PARAM, 0.0, 1.0, 0.0)); + + addParam(createParam(Vec(79, 60), module, Sheep::FREQUENCY_PARAM, -48.0, 48.0, 0.0)); + addParam(createParam(Vec(156, 66), module, Sheep::FM_PARAM, -12.0, 12.0, 0.0)); + + addParam(createParam(Vec(13, 155), module, Sheep::SHAPE_PARAM, -1.0, 1.0, 0.0)); + addParam(createParam(Vec(85, 155), module, Sheep::SLOPE_PARAM, -1.0, 1.0, 0.0)); + addParam(createParam(Vec(156, 155), module, Sheep::SMOOTHNESS_PARAM, -1.0, 1.0, 0.0)); + + addInput(createInput(Vec(18, 216), module, Sheep::SHAPE_INPUT)); + addInput(createInput(Vec(90, 216), module, Sheep::SLOPE_INPUT)); + addInput(createInput(Vec(161, 216), module, Sheep::SMOOTHNESS_INPUT)); + + addInput(createInput(Vec(18, 271), module, Sheep::TRIG_INPUT)); + addInput(createInput(Vec(54, 271), module, Sheep::FREEZE_INPUT)); + addInput(createInput(Vec(90, 271), module, Sheep::PITCH_INPUT)); + addInput(createInput(Vec(125, 271), module, Sheep::FM_INPUT)); + addInput(createInput(Vec(161, 271), module, Sheep::LEVEL_INPUT)); + + addInput(createInput(Vec(18, 313), module, Sheep::CLOCK_INPUT)); + addOutput(createOutput(Vec(54, 313), module, Sheep::HIGH_OUTPUT)); + addOutput(createOutput(Vec(90, 313), module, Sheep::LOW_OUTPUT)); + addOutput(createOutput(Vec(125, 313), module, Sheep::UNI_OUTPUT)); + addOutput(createOutput(Vec(161, 313), module, Sheep::BI_OUTPUT)); + + addChild(createValueLight>(Vec(57, 62), &module->lights[0])); + addChild(createValueLight>(Vec(57, 83), &module->lights[1])); + addChild(createValueLight>(Vec(57, 103), &module->lights[2])); +}