Skip to content

Commit e0fc088

Browse files
committed
Add RandomNoteCV module
1 parent 979464e commit e0fc088

9 files changed

Lines changed: 504 additions & 12 deletions

File tree

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,18 @@ If the chord is a triad (3 notes), the 4th 1v/oct output will be the bass note +
2727
## ScaleCV
2828
![ChordCV](https://i.imgur.com/GHhsEgZ.jpg "ScaleCV")
2929

30-
Generates a scale and outputs a polyphonic 1v/oct signal with 8 notes.
30+
Generates a scale and outputs a polyphonic 1v/oct signal with 7 notes.
3131

3232
* **Root**: Chooses the root note (1v/oct, input range -4v to 4v)
3333
* **Mode**: Chooses the scale mode (input range -4v to 4v)
34+
35+
## RandomNoteCV
36+
![RandomNoteCV](https://i.imgur.com/5rybov5.jpg "RandomNoteCV")
37+
38+
Generates a random note when triggered within the selected range and outputs a 1v/oct signal. If a polyphonic input is connected the note is selected from the provided notes (they are quantized).
39+
40+
This module is best used when combined with the above modules.
41+
42+
* **Trigger**: Triggers a new note to be selected
43+
* **Root Bias**: How often should the root (first) note be forced (0-100%, input range -4v to 4v, only valid if polyphonic input is connected)
44+
* **Range**: Range in octaves (Minimum=1, input range -4v to 4v)

plugin.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
"name": "ScaleCV",
2525
"description": "Generates a scale",
2626
"tags": ["Polyphonic","Tuner"]
27+
},
28+
{
29+
"slug": "RandomNoteCV",
30+
"name": "RandomNoteCV",
31+
"description": "Generates a random note when triggered",
32+
"tags": ["Polyphonic","Tuner"]
2733
}
2834
]
2935
}

res/RandomNoteCV.svg

Lines changed: 236 additions & 0 deletions
Loading

src/RandomNoteCV.cpp

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#include "plugin.hpp"
2+
#include "musiclib.hpp"
3+
4+
struct RandomNoteCV : Module {
5+
enum ParamIds {
6+
TRIGGER_PARAM,
7+
BIAS_PARAM,
8+
RANGE_PARAM,
9+
NUM_PARAMS
10+
};
11+
enum InputIds {
12+
POLY_INPUT,
13+
TRIGGER_INPUT,
14+
BIAS_INPUT,
15+
RANGE_INPUT,
16+
NUM_INPUTS
17+
};
18+
enum OutputIds {
19+
NOTE_OUTPUT,
20+
NUM_OUTPUTS
21+
};
22+
enum LightIds {
23+
TRIGGER_LIGHT,
24+
NUM_LIGHTS
25+
};
26+
27+
Trigger randomTrigger;
28+
int playing_note = 48;
29+
float playing_voltage = note_to_voltage(playing_note);
30+
float triggerLight = 0.0f;
31+
float range = 1.0f;
32+
float bias = 0.5f;
33+
double sampleTime;
34+
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};
35+
int polyNotes[16] = {48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48};
36+
int polyChannels = 0;
37+
38+
bool hasPoly = false;
39+
40+
RefreshCounter refresh;
41+
42+
RandomNoteCV() {
43+
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
44+
configButton(TRIGGER_PARAM);
45+
configParam(BIAS_PARAM, -4.0, 4.0, 0.0, "Root Bias");
46+
configParam(RANGE_PARAM, -4.0, 4.0, -3.0, "Range");
47+
48+
configInput(POLY_INPUT, "1V/oct Poly");
49+
configInput(TRIGGER_INPUT, "Trigger");
50+
configInput(BIAS_INPUT, "Root Bias");
51+
configInput(RANGE_INPUT, "Range");
52+
53+
configOutput(NOTE_OUTPUT, "1v/oct");
54+
}
55+
56+
void process(const ProcessArgs& args) override;
57+
};
58+
59+
void RandomNoteCV::process(const ProcessArgs &args){
60+
sampleTime = 1.0 / (double)(APP->engine->getSampleRate());
61+
62+
if (refresh.processInputs()) {
63+
if(inputs[POLY_INPUT].isConnected()){
64+
hasPoly = true;
65+
polyChannels = inputs[POLY_INPUT].getChannels();
66+
for (int c = 0; c < 16; c++) {
67+
float v = inputs[POLY_INPUT].getVoltage(c);
68+
polyNotes_v[c] = v;
69+
polyNotes[c] = voltage_to_note_int(v);
70+
}
71+
}else{
72+
hasPoly = false;
73+
}
74+
75+
float range_v = params[RANGE_PARAM].getValue();
76+
if(inputs[RANGE_INPUT].isConnected()){
77+
range_v = inputs[RANGE_INPUT].getVoltage();
78+
}
79+
range = 1.0f + (range_v + 4.0f);
80+
81+
float bias_v = params[BIAS_PARAM].getValue();
82+
if(inputs[BIAS_INPUT].isConnected()){
83+
bias_v = inputs[BIAS_INPUT].getVoltage();
84+
}
85+
bias = (bias_v + 4.0f) / 8.0f;
86+
}
87+
88+
if (randomTrigger.process(inputs[TRIGGER_INPUT].getVoltage() + params[TRIGGER_PARAM].getValue())) {
89+
triggerLight = 1.0f;
90+
//Select a new note
91+
if(!hasPoly){
92+
playing_note = 36 + (int)round(random::uniform() * (range * 12.0f));
93+
playing_voltage = note_to_voltage(playing_note);
94+
}else{
95+
float root_octave = round(polyNotes_v[0]) + 4.0f;
96+
int index = (int)round(random::uniform() * (float)polyChannels);
97+
if(bias > 0.0f && random::uniform() < bias){
98+
index = 0;
99+
}
100+
int octave = (int)root_octave + (int)round(random::uniform() * range);
101+
playing_note = polyNotes[index] + (12 * octave);
102+
playing_voltage = note_to_voltage(playing_note);
103+
}
104+
}
105+
106+
// lights
107+
if (refresh.processLights()) {
108+
// Trigger light
109+
lights[TRIGGER_LIGHT].setSmoothBrightness(triggerLight, (float)sampleTime * (RefreshCounter::displayRefreshStepSkips >> 2));
110+
triggerLight = 0.0f;
111+
}
112+
113+
outputs[NOTE_OUTPUT].setVoltage(playing_voltage);
114+
}
115+
116+
117+
struct RandomNoteCVWidget : ModuleWidget {
118+
struct NoteDisplayWidget : TransparentWidget {
119+
RandomNoteCV* module;
120+
std::shared_ptr<Font> font;
121+
char text[2];
122+
123+
NoteDisplayWidget(Vec _pos, Vec _size, RandomNoteCV* _module) {
124+
box.size = _size;
125+
box.pos = _pos.minus(_size.div(2));
126+
module = _module;
127+
font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/PixelOperator.ttf"));
128+
}
129+
130+
void draw(const DrawArgs &args) override {
131+
NVGcolor textColor = prepareDisplay(args.vg, &box, 22);
132+
nvgFontFaceId(args.vg, font->handle);
133+
nvgTextLetterSpacing(args.vg, -1.5);
134+
nvgTextAlign(args.vg, NVG_ALIGN_CENTER);
135+
136+
Vec textPos = Vec(box.size.x/2, 21.0f);
137+
nvgFillColor(args.vg, textColor);
138+
139+
if (module != NULL){
140+
get_note_name(module->playing_note,text);
141+
}else{
142+
snprintf(text, 1, " ");
143+
}
144+
145+
nvgText(args.vg, textPos.x, textPos.y, text, NULL);
146+
}
147+
148+
};
149+
150+
RandomNoteCVWidget(RandomNoteCV* module) {
151+
setModule(module);
152+
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/RandomNoteCV.svg")));
153+
154+
addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
155+
addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
156+
addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
157+
addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
158+
159+
const int centerX = box.size.x / 2;
160+
161+
NoteDisplayWidget* display = new NoteDisplayWidget(Vec(centerX, 55), Vec(76, 29), module);
162+
addChild(display);
163+
164+
const int offsetXL = 40;
165+
166+
addInput(createInputCentered<PJ301MPort>(Vec(centerX, 95), module, RandomNoteCV::POLY_INPUT));
167+
168+
addParam(createParamCentered<LEDBezel>(Vec(centerX,155), module, RandomNoteCV::TRIGGER_PARAM));
169+
addChild(createLightCentered<LEDBezelLight<GreenLight>>(Vec(centerX,155), module, RandomNoteCV::TRIGGER_LIGHT));
170+
addInput(createInputCentered<PJ301MPort>(Vec(centerX - offsetXL, 155), module, RandomNoteCV::TRIGGER_INPUT));
171+
172+
addParam(createParamCentered<Rogan2PWhite>(Vec(centerX,200), module, RandomNoteCV::BIAS_PARAM));
173+
addInput(createInputCentered<PJ301MPort>(Vec(centerX - offsetXL, 200), module, RandomNoteCV::BIAS_INPUT));
174+
175+
addParam(createParamCentered<Rogan2PWhite>(Vec(centerX,245), module, RandomNoteCV::RANGE_PARAM));
176+
addInput(createInputCentered<PJ301MPort>(Vec(centerX - offsetXL, 245), module, RandomNoteCV::RANGE_INPUT));
177+
178+
addOutput(createOutputCentered<PJ301MPort>(Vec(centerX, 330), module, RandomNoteCV::NOTE_OUTPUT));
179+
}
180+
};
181+
182+
183+
Model* modelRandomNoteCV = createModel<RandomNoteCV, RandomNoteCVWidget>("RandomNoteCV");

src/ScaleCV.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ void ScaleCV::process(const ProcessArgs &args){
5858
//Make the scale
5959
struct scale s = get_scale(root_note, mode);
6060

61-
outputs[POLY_OUTPUT].setChannels(8);
62-
for(int t=0; t<8; t++){
61+
outputs[POLY_OUTPUT].setChannels(7);
62+
for(int t=0; t<7; t++){
6363
outputs[POLY_OUTPUT].setVoltage(note_to_voltage(s.notes[t]),t);
6464
}
6565
}

src/musiclib.cpp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ void get_chord_name(int root_semi, int chord_type, bool inverted, int bass_note,
162162
sprintf(text, "%s%s%s", NOTE_NAMES[root_semi], CHORD_TYPE_NAMES[chord_type], inv);
163163
}
164164

165+
void get_note_name(int note, char* text) {
166+
sprintf(text, "%s", NOTE_NAMES[note % 12]);
167+
}
168+
165169
//Scales
166170
static const char * MODE_NAMES[] = {
167171
"",
@@ -178,13 +182,13 @@ void get_scale_name(int root_semi, int mode, char* text) {
178182
}
179183

180184
static const int MODE_DEGREES[7][7] = {
181-
{2,2,1,2,2,2,1}, //Major (Ionian)
182-
{2,1,2,2,2,1,2}, //Dorian
183-
{1,2,2,2,1,2,2}, //Phrygian
184-
{2,2,2,1,2,2,1}, //Lydian
185-
{2,2,1,2,2,1,2}, //Mixolydian
186-
{2,1,2,2,1,2,2}, //Minor (Aeolian)
187-
{1,2,2,1,2,2,2}, //Locrian
185+
{2,2,1,2,2,2}, //Major (Ionian)
186+
{2,1,2,2,2,1}, //Dorian
187+
{1,2,2,2,1,2}, //Phrygian
188+
{2,2,2,1,2,2}, //Lydian
189+
{2,2,1,2,2,1}, //Mixolydian
190+
{2,1,2,2,1,2}, //Minor (Aeolian)
191+
{1,2,2,1,2,2}, //Locrian
188192
};
189193

190194
struct scale get_scale(int root_note, int mode){
@@ -196,7 +200,7 @@ struct scale get_scale(int root_note, int mode){
196200
int current_note = root_note;
197201

198202
int t;
199-
for(t=1; t<8; t++){
203+
for(t=1; t<7; t++){
200204
current_note += degrees[t-1];
201205
return_scale.notes[t] = current_note;
202206
}

src/musiclib.hpp

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,57 @@
11
#pragma once
2+
#include "rack.hpp"
3+
4+
using namespace rack;
25

36
//Note <-> Voltage conversion
47
float note_to_voltage(int v);
58
float voltage_to_note(float value);
69
int voltage_to_note_int(float value);
710

11+
//DSP Stuff (credit Marc Boule / Impromptu Modular)
12+
struct Trigger : dsp::SchmittTrigger {
13+
// implements a 0.1V - 1.0V SchmittTrigger (see include/dsp/digital.hpp) instead of
14+
// calling SchmittTriggerInstance.process(math::rescale(in, 0.1f, 1.f, 0.f, 1.f))
15+
bool process(float in) {
16+
if (state) {
17+
// HIGH to LOW
18+
if (in <= 0.1f) {
19+
state = false;
20+
}
21+
}
22+
else {
23+
// LOW to HIGH
24+
if (in >= 1.0f) {
25+
state = true;
26+
return true;
27+
}
28+
}
29+
return false;
30+
}
31+
};
32+
33+
struct RefreshCounter {
34+
// Note: because of stagger, and asyncronous dataFromJson, should not assume this processInputs() will return true on first run
35+
// of module::process()
36+
static const unsigned int displayRefreshStepSkips = 256;
37+
static const unsigned int userInputsStepSkipMask = 0xF;// sub interval of displayRefreshStepSkips, since inputs should be more responsive than lights
38+
// above value should make it such that inputs are sampled > 1kHz so as to not miss 1ms triggers
39+
40+
unsigned int refreshCounter = (random::u32() % displayRefreshStepSkips);// stagger start values to avoid processing peaks when many Geo and Impromptu modules in the patch
41+
42+
bool processInputs() {
43+
return ((refreshCounter & userInputsStepSkipMask) == 0);
44+
}
45+
bool processLights() {// this must be called even if module has no lights, since counter is decremented here
46+
refreshCounter++;
47+
bool process = refreshCounter >= displayRefreshStepSkips;
48+
if (process) {
49+
refreshCounter = 0;
50+
}
51+
return process;
52+
}
53+
};
54+
855
//Chords
956
struct chord {
1057
int num_notes;
@@ -16,7 +63,10 @@ void get_chord_name(int root_semi, int chord_type, bool inverted, int bass_note,
1663

1764
//Scales
1865
struct scale {
19-
int notes[8];
66+
int notes[7];
2067
};
2168
void get_scale_name(int root_semi, int mode, char* text);
2269
struct scale get_scale(int root_note, int mode);
70+
71+
//Notes
72+
void get_note_name(int note, char* text);

src/plugin.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ void init(Plugin* p) {
1111
// p->addModel(modelMyModule);
1212
p->addModel(modelChordCV);
1313
p->addModel(modelScaleCV);
14+
p->addModel(modelRandomNoteCV);
1415

1516
// Any other plugin initialization may go here.
1617
// As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack.

src/plugin.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extern Plugin* pluginInstance;
1111
// extern Model* modelMyModule;
1212
extern Model* modelChordCV;
1313
extern Model* modelScaleCV;
14+
extern Model* modelRandomNoteCV;
1415

1516
static const int displayAlpha = 23;
1617
NVGcolor prepareDisplay(NVGcontext *vg, Rect *box, int fontSize);

0 commit comments

Comments
 (0)