From f40555cddf0f1aff5382fc4065d10ba70fb0fde0 Mon Sep 17 00:00:00 2001 From: Aaron Static Date: Sat, 6 Nov 2021 19:07:35 +1100 Subject: [PATCH] Adds DiatonicCV module --- README.md | 11 ++ plugin.json | 6 + res/DiatonicCV.svg | 338 +++++++++++++++++++++++++++++++++++++++++++ src/DiatonicCV.cpp | 219 ++++++++++++++++++++++++++++ src/RandomNoteCV.cpp | 2 + src/musiclib.cpp | 250 +++++++++++++++++++++++++++++--- src/musiclib.hpp | 10 +- src/plugin.cpp | 1 + src/plugin.hpp | 1 + 9 files changed, 817 insertions(+), 21 deletions(-) create mode 100644 res/DiatonicCV.svg create mode 100644 src/DiatonicCV.cpp diff --git a/README.md b/README.md index b5dc078..4671472 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,17 @@ Generates a scale and outputs a polyphonic 1v/oct signal with 7 notes + quantize * **Mode**: Chooses the scale mode (input range -4v to 4v) * **Quantizers**: 4 Quantizers that will quantize a monophonic input to the selected scale +## DiatonicCV +![DiatonicCV](https://i.imgur.com/aJNhwkL.jpg "DiatonicCV") + +Generates a diatonic chord from the provided scale, if one is provided via the poly input (otherwise C Major is used). Best used in combination with ScaleCV above. + +* **Octave**: Which octave to transpose the chord to (1v/oct, input range -4v to 4v) +* **Chord**: Which chord degree in the scale (I - VII, input range 0v to 6v) +* **Type**: Which chord type (triad, seventh, ninth, input range 0v to 3v) +* **Inversion**: Chooses chord inversion (input range 0v to 4v) +* **Voicing**: Chooses the chord voicing (input range 0v to 4v, refer to ChordCV for more info) + ## RandomNoteCV ![RandomNoteCV](https://i.imgur.com/xK91S79.jpg "RandomNoteCV") diff --git a/plugin.json b/plugin.json index 9b65a67..b2dd68c 100644 --- a/plugin.json +++ b/plugin.json @@ -25,6 +25,12 @@ "description": "Generates a scale", "tags": ["Polyphonic","Tuner","Quantizer"] }, + { + "slug": "DiatonicCV", + "name": "DiatonicCV", + "description": "Generates diatonic chords from the provided scale", + "tags": ["Polyphonic","Tuner"] + }, { "slug": "RandomNoteCV", "name": "RandomNoteCV", diff --git a/res/DiatonicCV.svg b/res/DiatonicCV.svg new file mode 100644 index 0000000..09b221d --- /dev/null +++ b/res/DiatonicCV.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DiatonicCV.cpp b/src/DiatonicCV.cpp new file mode 100644 index 0000000..34f67ce --- /dev/null +++ b/src/DiatonicCV.cpp @@ -0,0 +1,219 @@ +#include "plugin.hpp" +#include "musiclib.hpp" + +struct DiatonicCV : Module { + enum ParamIds { + OCTAVE_PARAM, + CHORD_PARAM, + TYPE_PARAM, + INVERSION_PARAM, + VOICING_PARAM, + NUM_PARAMS + }; + enum InputIds { + POLY_INPUT, + OCTAVE_INPUT, + CHORD_INPUT, + TYPE_INPUT, + INVERSION_INPUT, + VOICING_INPUT, + NUM_INPUTS + }; + enum OutputIds { + POLY_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + NUM_LIGHTS + }; + + int octave = 4; + int chord = 0; + int chord_type = 0; + bool inverted = false; + bool hasPoly = true; + int bass_note = 0; + float polyNotes_v[16] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f}; + int polyNotes[16] = {48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48}; + int polyChannels = 0; + int inversion = 0; + int voicing = 0; + struct chord playing_chord; + struct scale cmajor; + + RefreshCounter refresh; + + DiatonicCV() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + configParam(OCTAVE_PARAM, -4.0, 4.0, 0.0, "Octave"); + configParam(CHORD_PARAM, 0, 6.0, 0.0, "Chord (I - VII)"); + configParam(TYPE_PARAM, 0, 2.0, 0.0, "Chord Type"); + configParam(INVERSION_PARAM, 0.0, 4.0, 0.0, "Inversion"); + configParam(VOICING_PARAM, 0.0, 4.0, 0.0, "Voicing"); + + configInput(POLY_INPUT, "Polyphonic"); + configInput(OCTAVE_INPUT, "Octave"); + configInput(CHORD_INPUT, "Chord"); + configInput(TYPE_INPUT, "Chord Type"); + configInput(INVERSION_INPUT, "Inversion"); + configInput(VOICING_INPUT, "Voicing"); + + configOutput(POLY_OUTPUT, "Polyphonic"); + + cmajor = get_scale(0,0); + } + + void process(const ProcessArgs& args) override; +}; + +void DiatonicCV::process(const ProcessArgs &args){ + if (refresh.processInputs()) { + if(inputs[POLY_INPUT].isConnected()){ + polyChannels = inputs[POLY_INPUT].getChannels(); + for (int c = 0; c < 16; c++) { + float v = inputs[POLY_INPUT].getVoltage(c); + polyNotes_v[c] = v; + polyNotes[c] = voltage_to_note_int(v); + } + //sort the notes in ascending order + std::sort(std::begin(polyNotes), polyNotes + polyChannels); + }else{ + //Just make it C Major + polyChannels = 7; + for(int t=0; t<7; t++){ + polyNotes[t] = cmajor.notes[t]; + } + } + + float octave_v = params[OCTAVE_PARAM].getValue(); + if(inputs[OCTAVE_INPUT].isConnected()){ + octave_v = inputs[OCTAVE_INPUT].getVoltage(); + } + octave = (int)round(octave_v) + 4; + + float chord_v = params[CHORD_PARAM].getValue(); + if(inputs[CHORD_INPUT].isConnected()){ + chord_v = inputs[CHORD_INPUT].getVoltage(); + if(chord_v < 0.0f) chord_v = 0.0f; + if(chord_v > 7.0f) chord_v = 7.0f; + } + chord = (int)round(chord_v); + + float type_v = params[TYPE_PARAM].getValue(); + if(inputs[TYPE_INPUT].isConnected()){ + type_v = inputs[TYPE_INPUT].getVoltage(); + if(type_v < 0.0f) type_v = 0.0f; + if(type_v > 3.0f) type_v = 3.0f; + } + chord_type = (int)round(type_v); + + //inversion + inversion = (int)round(params[INVERSION_PARAM].getValue()); + if(inputs[INVERSION_PARAM].isConnected()){ + inversion = (int)clamp(round(inputs[INVERSION_PARAM].getVoltage()),0.0f,3.0f); + } + + //voicing + voicing = (int)round(params[VOICING_PARAM].getValue()); + if(inputs[VOICING_PARAM].isConnected()){ + voicing = (int)clamp(round(inputs[VOICING_PARAM].getVoltage()),0.0f,4.0f); + } + + //Make the chord + playing_chord = get_diatonic_chord(polyNotes, polyChannels, octave, chord, chord_type, inversion, voicing); + } + + if (refresh.processLights()) { + + } + + outputs[POLY_OUTPUT].setChannels(playing_chord.num_notes); + for(int t=0; t font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/PixelOperator.ttf")); + if(font){ + NVGcolor textColor = prepareDisplay(args.vg, &box, 22); + nvgFontFaceId(args.vg, font->handle); + nvgTextLetterSpacing(args.vg, -1.5); + nvgTextAlign(args.vg, NVG_ALIGN_CENTER); + + Vec textPos = Vec(box.size.x/2, 21.0f); + nvgFillColor(args.vg, textColor); + + if (module != NULL && module->playing_chord.num_notes > 2){ + detect_chord_name_simple(module->playing_chord,text); + }else{ + snprintf(text, 9, " "); + } + + nvgText(args.vg, textPos.x, textPos.y, text, NULL); + } + } + + }; + + DiatonicCVWidget(DiatonicCV* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/DiatonicCV.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + const int centerX = box.size.x / 2; + + ChordDisplayWidget* display = new ChordDisplayWidget(Vec(centerX, 55), Vec(76, 29), module); + addChild(display); + + addInput(createInputCentered(Vec(centerX, 95), module, DiatonicCV::POLY_INPUT)); + + const int offsetXL = 40; + const int spacingY = 45; + const int y1 = 134; + const int y2 = y1 + spacingY; + const int y3 = y2 + spacingY; + + + addParam(createParamCentered(Vec(centerX,y1), module, DiatonicCV::OCTAVE_PARAM)); + addInput(createInputCentered(Vec(centerX - offsetXL, y1), module, DiatonicCV::OCTAVE_INPUT)); + + addParam(createParamCentered(Vec(centerX,y2), module, DiatonicCV::CHORD_PARAM)); + addInput(createInputCentered(Vec(centerX - offsetXL, y2), module, DiatonicCV::CHORD_INPUT)); + + addParam(createParamCentered(Vec(centerX,y3), module, DiatonicCV::TYPE_PARAM)); + addInput(createInputCentered(Vec(centerX - offsetXL, y3), module, DiatonicCV::TYPE_INPUT)); + + static const int offsetX2 = 14; + static const int posY = 269; + + const int offsetXL2 = 42; + + addParam(createParamCentered(Vec(centerX - offsetX2,posY), module, DiatonicCV::INVERSION_PARAM)); + addInput(createInputCentered(Vec(centerX - offsetXL2, posY), module, DiatonicCV::INVERSION_INPUT)); + + addParam(createParamCentered(Vec(centerX + offsetX2,posY), module, DiatonicCV::VOICING_PARAM)); + addInput(createInputCentered(Vec(centerX + offsetXL2, posY), module, DiatonicCV::VOICING_INPUT)); + + addOutput(createOutputCentered(Vec(centerX, 332), module, DiatonicCV::POLY_OUTPUT)); + } +}; + + +Model* modelDiatonicCV = createModel("DiatonicCV"); diff --git a/src/RandomNoteCV.cpp b/src/RandomNoteCV.cpp index 156e691..554522b 100644 --- a/src/RandomNoteCV.cpp +++ b/src/RandomNoteCV.cpp @@ -68,6 +68,8 @@ void RandomNoteCV::process(const ProcessArgs &args){ polyNotes_v[c] = v; polyNotes[c] = voltage_to_note_int(v); } + //sort the notes in ascending order + std::sort(std::begin(polyNotes), polyNotes + polyChannels); }else{ hasPoly = false; } diff --git a/src/musiclib.cpp b/src/musiclib.cpp index b20f783..a2d679d 100644 --- a/src/musiclib.cpp +++ b/src/musiclib.cpp @@ -25,19 +25,30 @@ int voltage_to_note_int(float value) { return semi; } - +//Intervals (for readability) +const int MINOR_SECOND = 1; +const int MAJOR_SECOND = 2; +const int MINOR_THIRD = 3; +const int MAJOR_THIRD = 4; +const int PERFECT_FOURTH = 5; +const int DIMINISHED_FIFTH = 6; +const int PERFECT_FIFTH = 7; +const int AUGMENTED_FIFTH = 8; +const int MAJOR_SIXTH = 9; +const int MINOR_SEVENTH = 10; +const int MAJOR_SEVENTH = 11; //Chords static const int CHORD_DEGREES[9][3] = { - {4,7,0}, //Major - {3,7,0}, //Minor - {4,7,10}, //Dominant 7 - {3,7,10}, //Minor 7 - {4,7,11}, //Major 7 - {2,7,0}, //sus2 - {5,7,0}, //sus4 - {3,6,0}, //dim - {4,8,0} //aug + {MAJOR_THIRD,PERFECT_FIFTH,0}, //Major + {MINOR_THIRD,PERFECT_FIFTH,0}, //Minor + {MAJOR_THIRD,PERFECT_FIFTH,MINOR_SEVENTH}, //Dominant 7 + {MINOR_THIRD,PERFECT_FIFTH,MINOR_SEVENTH}, //Minor 7 + {MAJOR_THIRD,PERFECT_FIFTH,MAJOR_SEVENTH}, //Major 7 + {MAJOR_SECOND,PERFECT_FIFTH,0}, //sus2 + {PERFECT_FOURTH,PERFECT_FIFTH,0}, //sus4 + {MINOR_THIRD,DIMINISHED_FIFTH,0}, //dim + {MAJOR_THIRD,AUGMENTED_FIFTH,0} //aug }; struct chord get_chord(int root_note, int type, int inversion, int voicing){ @@ -50,6 +61,10 @@ struct chord get_chord(int root_note, int type, int inversion, int voicing){ int seventh_note = root_note + degrees[2]; if(degrees[2] == 0){ //Has 3 notes + return_chord.notes_pre[0] = root_note; + return_chord.notes_pre[1] = third_note; + return_chord.notes_pre[2] = fifth_note; + if(inversion == 3){ inversion = 2; } @@ -87,6 +102,11 @@ struct chord get_chord(int root_note, int type, int inversion, int voicing){ return_chord.num_notes = 3; }else{ //Has 4 notes + return_chord.notes_pre[0] = root_note; + return_chord.notes_pre[1] = third_note; + return_chord.notes_pre[2] = fifth_note; + return_chord.notes_pre[3] = seventh_note; + if(inversion == 1){ root_note += 12; int tmp = root_note; @@ -138,20 +158,126 @@ struct chord get_chord(int root_note, int type, int inversion, int voicing){ return_chord.notes[3] = seventh_note; return_chord.num_notes = 4; } + return_chord.inversion = inversion; return return_chord; } +struct chord get_diatonic_chord(int* notes, int num_notes, int octave, int chord, int type, int inversion, int voicing){ + struct chord return_chord; + + int chord_length = 3 + type; + int actual_length = 0; + if(inversion > chord_length - 1) inversion = chord_length - 1; + return_chord.inversion = inversion; + + //Build the chord from the scale + + for(int t=0; t < chord_length; t++){ + int index = chord + (t * 2); + int transpose = 0; + + //Wrap if needed + if(index >= num_notes){ + index -= num_notes; + transpose++; + } + + //Wrap again if needed + if(index >= num_notes){ + index -= num_notes; + transpose++; + } + + if(index < num_notes){ + return_chord.notes_pre[t] = (transpose * 12) + notes[index]; + actual_length++; + } + } + + return_chord.num_notes = actual_length; + + //Transpose the chord + for(int t=0; t < actual_length; t++){ + return_chord.notes[t] = (octave * 12) + return_chord.notes_pre[t]; + } + + //Perform inversion + if(inversion > 0 && actual_length > 1){ + int new_notes[6]; + int index = inversion; + for(int t=0; t < actual_length; t++){ + int i = index; + if(i >= actual_length) { + i -= actual_length; + new_notes[t] = return_chord.notes[i] + 12; + }else{ + new_notes[t] = return_chord.notes[i]; + } + index++; + } + for(int t=0; t < actual_length; t++){ + return_chord.notes[t] = new_notes[t]; + } + } + + //Voice the chord + if(voicing > 0 && actual_length > 2){ + int root_note = return_chord.notes[0]; + if(root_note > 11){ + if(voicing == 1){ + return_chord.notes[0] -= 12; + } + if(voicing == 2){ + return_chord.notes[0] -= 12; + return_chord.notes[2] -= 12; + } + if(voicing == 3){ + return_chord.notes[0] -= 12; + return_chord.notes[1] += 12; + } + if(voicing == 4){ + return_chord.notes[0] -= 12; + if(actual_length > 3){ + return_chord.notes[3] -= 12; + } + } + } + } + + return return_chord; +} + static const char * NOTE_NAMES[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; static const char * CHORD_TYPE_NAMES[] = { - "", - "m", - "7", - "m7", - "maj7", - "sus2", - "sus4", - "dim", - "+" + "", //0 + "m", //1 + "7", //2 + "m7", //3 + "maj7", //4 + "sus2", //5 + "sus4", //6 + "dim", //7 + "+", //8 + "9", //9 + "m9", //10 + "maj9", //11 + "+M7", //12 + "+M9", //13 + "mM7", //14 + "mM9", //15 + "dim7", //16 + "dim9", //17 + "m7b5", //18 + "+7", //19 + "mM7b5", //20 + "7b5", //21 + "M7b5", //22 + "7b9", //23 + "6/9", //24 + "mb9", //25 + "maj79", //26 + "dimb9", //27 + "dim9" //28 }; void get_chord_name(int root_semi, int chord_type, bool inverted, int bass_note, char* text) { @@ -162,6 +288,92 @@ void get_chord_name(int root_semi, int chord_type, bool inverted, int bass_note, sprintf(text, "%s%s%s", NOTE_NAMES[root_semi], CHORD_TYPE_NAMES[chord_type], inv); } +void detect_chord_name_simple(struct chord chord, char* text){ + //Detect the chord type (if we can) + int chord_type = 0; //default to major + + if(chord.num_notes > 2){ + //don't bother unless theres at least 3 notes + + //build an array of intervals + int intervals[5] = {0,0,0,0,0}; + for(int t=1; t < chord.num_notes; t++){ + intervals[t-1] = chord.notes_pre[t] - chord.notes_pre[t-1]; + } + + + + //Is there a better way to do this? if you know please submit a pull request xD + if(chord.num_notes > 3){ + //Its a 7 or 9 chord + if(intervals[0] == MAJOR_THIRD){ + if(intervals[1] == MINOR_THIRD){ + //major triad + if(intervals[2] == MINOR_THIRD) { + chord_type = 2; //dominant seventh + if(chord.num_notes == 5 && intervals[3] == MINOR_THIRD) chord_type = 23; //dominant minor ninth + if(chord.num_notes == 5 && intervals[3] == MAJOR_THIRD) chord_type = 9; //dominant ninth + } + if(intervals[2] == MAJOR_THIRD) { + chord_type = 4; //major seventh + if(chord.num_notes == 5 && intervals[3] == MAJOR_THIRD) chord_type = 11; //major ninth + if(chord.num_notes == 5 && intervals[3] == MINOR_THIRD) chord_type = 26; //major seventh ninth + } + if(intervals[2] == MAJOR_SECOND) { + if(chord.num_notes == 5 && intervals[3] == PERFECT_FOURTH) chord_type = 24; //6/9 + } + } + if(intervals[1] == MAJOR_THIRD){ + //augmented triad + if(intervals[2] == MINOR_THIRD) chord_type = 12; //augmented major seventh + if(intervals[2] == MAJOR_SECOND) chord_type = 19; //augmented seventh + } + if(intervals[1] == MAJOR_SECOND){ + if(intervals[2] == MAJOR_THIRD) chord_type = 21; //dominant seventh flat 5 + if(intervals[2] == PERFECT_FOURTH) chord_type = 22; //major seventh flat 5 + } + } + if(intervals[0] == MINOR_THIRD){ + if(intervals[1] == MAJOR_THIRD){ + //minor triad + if(intervals[2] == MINOR_THIRD) { + chord_type = 3; //minor seventh + if(chord.num_notes == 5 && intervals[3] == MAJOR_THIRD) chord_type = 10; //minor ninth + if(chord.num_notes == 5 && intervals[3] == MINOR_THIRD) chord_type = 25; //minor flat ninth + } + if(intervals[2] == MAJOR_THIRD) chord_type = 14; //minmaj seventh + } + if(intervals[1] == MINOR_THIRD){ + //diminished triad + if(intervals[2] == MINOR_THIRD) chord_type = 16; //diminished seventh + if(intervals[2] == MAJOR_THIRD) { + chord_type = 18; //half diminished seventh + if(chord.num_notes == 5 && intervals[3] == MINOR_THIRD) chord_type = 27; //diminished flat ninth + if(chord.num_notes == 5 && intervals[3] == MAJOR_THIRD) chord_type = 28; //diminished ninth + } + if(intervals[2] == PERFECT_FOURTH) chord_type = 20; //diminished major seventh + } + } + }else{ + //Its just a triad + if(intervals[0] == MINOR_THIRD){ + if(intervals[1] == MAJOR_THIRD) chord_type = 1; //minor + if(intervals[1] == MINOR_THIRD) chord_type = 7; //diminished + } + if(intervals[0] == MAJOR_THIRD && intervals[1] == MAJOR_THIRD) chord_type = 8; //augmented + if(intervals[0] == MAJOR_SECOND && intervals[1] == PERFECT_FOURTH) chord_type = 5; //sus2 + if(intervals[0] == PERFECT_FOURTH && intervals[1] == MAJOR_SECOND) chord_type = 6; //sus4 + } + } + + if(chord.inversion > 0){ + int bass_note = chord.notes[0] % 12; + sprintf(text, "%s%s/%s", NOTE_NAMES[chord.notes_pre[0]], CHORD_TYPE_NAMES[chord_type], NOTE_NAMES[bass_note]); + }else{ + sprintf(text, "%s%s", NOTE_NAMES[chord.notes_pre[0]], CHORD_TYPE_NAMES[chord_type]); + } +} + void get_note_name(int note, char* text) { sprintf(text, "%s", NOTE_NAMES[note % 12]); } diff --git a/src/musiclib.hpp b/src/musiclib.hpp index 3387760..ee6dcd1 100644 --- a/src/musiclib.hpp +++ b/src/musiclib.hpp @@ -54,13 +54,19 @@ struct RefreshCounter { //Chords struct chord { - int num_notes; - int notes[4]; + int num_notes; //How many notes in this chord + int notes[6]; //The notes (post-inversion and transpose) + int notes_pre[6]; //The notes (before inversion and transpose) + int inversion; //The inversion of this chord + int octave; //Octave of the root note }; struct chord get_chord(int root_note, int type, int inversion, int voicing); void get_chord_name(int root_semi, int chord_type, bool inverted, int bass_note, char* text); +struct chord get_diatonic_chord(int* notes, int num_notes, int octave, int chord, int type, int inversion, int voicing); +void detect_chord_name_simple(struct chord chord, char* text); + //Scales struct scale { int notes[7]; diff --git a/src/plugin.cpp b/src/plugin.cpp index 0b2efdf..57b19d9 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -12,6 +12,7 @@ void init(Plugin* p) { p->addModel(modelChordCV); p->addModel(modelScaleCV); p->addModel(modelRandomNoteCV); + p->addModel(modelDiatonicCV); // Any other plugin initialization may go here. // As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack. diff --git a/src/plugin.hpp b/src/plugin.hpp index 83afd90..a8a3f8a 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -12,6 +12,7 @@ extern Plugin* pluginInstance; extern Model* modelChordCV; extern Model* modelScaleCV; extern Model* modelRandomNoteCV; +extern Model* modelDiatonicCV; static const int displayAlpha = 23; NVGcolor prepareDisplay(NVGcontext *vg, Rect *box, int fontSize);