diff --git a/src/core/MIDI_CV.cpp b/src/core/MIDI_CV.cpp index 4c1fdee3..f4def99b 100644 --- a/src/core/MIDI_CV.cpp +++ b/src/core/MIDI_CV.cpp @@ -34,49 +34,7 @@ struct MIDI_CV : Module { }; midi::InputQueue midiInput; - - /** Number of semitones to bend up/down by pitch wheel */ - float pwRange; - bool smooth; - int clockDivision; - int channels; - enum PolyMode { - ROTATE_MODE, - REUSE_MODE, - RESET_MODE, - MPE_MODE, - NUM_POLY_MODES - }; - PolyMode polyMode; - - uint32_t clock = 0; - - bool pedal; - // Indexed by channel - uint8_t notes[16]; - bool gates[16]; - uint8_t velocities[16]; - uint8_t aftertouches[16]; - std::vector heldNotes; - - int rotateIndex; - - /** Pitch wheel. - When MPE is disabled, only the first channel is used. - [channel] - */ - uint16_t pws[16]; - /** [channel] */ - uint8_t mods[16]; - dsp::ExponentialFilter pwFilters[16]; - dsp::ExponentialFilter modFilters[16]; - - dsp::PulseGenerator clockPulse; - dsp::PulseGenerator clockDividerPulse; - dsp::PulseGenerator retriggerPulses[16]; - dsp::PulseGenerator startPulse; - dsp::PulseGenerator stopPulse; - dsp::PulseGenerator continuePulse; + dsp::MidiParser<16> midiParser; MIDI_CV() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); @@ -92,401 +50,65 @@ struct MIDI_CV : Module { configOutput(START_OUTPUT, "Start trigger"); configOutput(STOP_OUTPUT, "Stop trigger"); configOutput(CONTINUE_OUTPUT, "Continue trigger"); - heldNotes.reserve(128); - for (int c = 0; c < 16; c++) { - pwFilters[c].setTau(1 / 30.f); - modFilters[c].setTau(1 / 30.f); - } - onReset(); } void onReset() override { - smooth = true; - channels = 1; - polyMode = ROTATE_MODE; - pwRange = 2; - clockDivision = 24; - panic(); + midiParser.reset(); midiInput.reset(); } - /** Resets performance state */ - void panic() { - for (int c = 0; c < 16; c++) { - notes[c] = 60; - gates[c] = false; - velocities[c] = 0; - aftertouches[c] = 0; - pws[c] = 8192; - mods[c] = 0; - pwFilters[c].reset(); - modFilters[c].reset(); - } - pedal = false; - rotateIndex = -1; - heldNotes.clear(); - } - void process(const ProcessArgs& args) override { midi::Message msg; while (midiInput.tryPop(&msg, args.frame)) { - processMessage(msg); + midiParser.processMessage(msg); } - // Set pitch wheel and mod wheel - int wheelChannels = (polyMode == MPE_MODE) ? 16 : 1; - float pwValues[16] = {}; - outputs[PW_OUTPUT].setChannels(wheelChannels); - outputs[MOD_OUTPUT].setChannels(wheelChannels); - for (int c = 0; c < wheelChannels; c++) { - float pw = (int16_t(pws[c]) - 8192) / 8191.f; - pw = clamp(pw, -1.f, 1.f); - if (smooth) - pw = pwFilters[c].process(args.sampleTime, pw); - else - pwFilters[c].out = pw; - pwValues[c] = pw; - outputs[PW_OUTPUT].setVoltage(pw * 5.f, c); - - float mod = mods[c] / 127.f; - mod = clamp(mod, 0.f, 1.f); - if (smooth) - mod = modFilters[c].process(args.sampleTime, mod); - else - modFilters[c].out = mod; - outputs[MOD_OUTPUT].setVoltage(mod * 10.f, c); - } + midiParser.processFilters(args.sampleTime); // Set note outputs - outputs[PITCH_OUTPUT].setChannels(channels); - outputs[GATE_OUTPUT].setChannels(channels); - outputs[VELOCITY_OUTPUT].setChannels(channels); - outputs[AFTERTOUCH_OUTPUT].setChannels(channels); - outputs[RETRIGGER_OUTPUT].setChannels(channels); - for (int c = 0; c < channels; c++) { - float pw = pwValues[(polyMode == MPE_MODE) ? c : 0]; - float pitch = (notes[c] - 60.f + pw * pwRange) / 12.f; - outputs[PITCH_OUTPUT].setVoltage(pitch, c); - outputs[GATE_OUTPUT].setVoltage(gates[c] ? 10.f : 0.f, c); - outputs[VELOCITY_OUTPUT].setVoltage(rescale(velocities[c], 0, 127, 0.f, 10.f), c); - outputs[AFTERTOUCH_OUTPUT].setVoltage(rescale(aftertouches[c], 0, 127, 0.f, 10.f), c); - outputs[RETRIGGER_OUTPUT].setVoltage(retriggerPulses[c].process(args.sampleTime) ? 10.f : 0.f, c); + outputs[PITCH_OUTPUT].setChannels(midiParser.channels); + outputs[GATE_OUTPUT].setChannels(midiParser.channels); + outputs[VELOCITY_OUTPUT].setChannels(midiParser.channels); + outputs[AFTERTOUCH_OUTPUT].setChannels(midiParser.channels); + outputs[RETRIGGER_OUTPUT].setChannels(midiParser.channels); + for (uint8_t c = 0; c < midiParser.channels; c++) { + outputs[PITCH_OUTPUT].setVoltage(midiParser.getPitchVoltage(c), c); + outputs[GATE_OUTPUT].setVoltage(midiParser.gates[c] ? 10.f : 0.f, c); + outputs[VELOCITY_OUTPUT].setVoltage(midiParser.velocities[c] / 127.f * 10.f, c); + outputs[AFTERTOUCH_OUTPUT].setVoltage(midiParser.aftertouches[c] / 127.f * 10.f, c); + outputs[RETRIGGER_OUTPUT].setVoltage(midiParser.retriggerPulses[c].isHigh() ? 10.f : 0.f, c); + } + + // Pitch and mod wheel outputs + uint8_t wheelChannels = midiParser.getWheelChannels(); + outputs[PW_OUTPUT].setChannels(wheelChannels); + outputs[MOD_OUTPUT].setChannels(wheelChannels); + for (uint8_t c = 0; c < wheelChannels; c++) { + outputs[PW_OUTPUT].setVoltage(midiParser.getPw(c) * 5.f, c); + outputs[MOD_OUTPUT].setVoltage(midiParser.getMod(c) * 10.f, c); } // Set clock and transport outputs - outputs[CLOCK_OUTPUT].setVoltage(clockPulse.process(args.sampleTime) ? 10.f : 0.f); - outputs[CLOCK_DIV_OUTPUT].setVoltage(clockDividerPulse.process(args.sampleTime) ? 10.f : 0.f); - outputs[START_OUTPUT].setVoltage(startPulse.process(args.sampleTime) ? 10.f : 0.f); - outputs[STOP_OUTPUT].setVoltage(stopPulse.process(args.sampleTime) ? 10.f : 0.f); - outputs[CONTINUE_OUTPUT].setVoltage(continuePulse.process(args.sampleTime) ? 10.f : 0.f); - } + outputs[CLOCK_OUTPUT].setVoltage(midiParser.clockPulse.isHigh() ? 10.f : 0.f); + outputs[CLOCK_DIV_OUTPUT].setVoltage(midiParser.clockDividerPulse.isHigh() ? 10.f : 0.f); + outputs[START_OUTPUT].setVoltage(midiParser.startPulse.isHigh() ? 10.f : 0.f); + outputs[STOP_OUTPUT].setVoltage(midiParser.stopPulse.isHigh() ? 10.f : 0.f); + outputs[CONTINUE_OUTPUT].setVoltage(midiParser.continuePulse.isHigh() ? 10.f : 0.f); - void processMessage(const midi::Message& msg) { - // DEBUG("MIDI: %ld %s", msg.getFrame(), msg.toString().c_str()); - - switch (msg.getStatus()) { - // note off - case 0x8: { - releaseNote(msg.getNote()); - } break; - // note on - case 0x9: { - if (msg.getValue() > 0) { - int c = msg.getChannel(); - pressNote(msg.getNote(), &c); - velocities[c] = msg.getValue(); - } - else { - // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. - releaseNote(msg.getNote()); - } - } break; - // key pressure - case 0xa: { - // Set the aftertouches with the same note - // TODO Should we handle the MPE case differently? - for (int c = 0; c < 16; c++) { - if (notes[c] == msg.getNote()) - aftertouches[c] = msg.getValue(); - } - } break; - // cc - case 0xb: { - processCC(msg); - } break; - // channel pressure - case 0xd: { - if (polyMode == MPE_MODE) { - // Set the channel aftertouch - aftertouches[msg.getChannel()] = msg.getNote(); - } - else { - // Set all aftertouches - for (int c = 0; c < 16; c++) { - aftertouches[c] = msg.getNote(); - } - } - } break; - // pitch wheel - case 0xe: { - int c = (polyMode == MPE_MODE) ? msg.getChannel() : 0; - pws[c] = ((uint16_t) msg.getValue() << 7) | msg.getNote(); - } break; - case 0xf: { - processSystem(msg); - } break; - default: break; - } - } - - void processCC(const midi::Message &msg) { - switch (msg.getNote()) { - // mod - case 0x01: { - int c = (polyMode == MPE_MODE) ? msg.getChannel() : 0; - mods[c] = msg.getValue(); - } break; - // sustain - case 0x40: { - if (msg.getValue() >= 64) - pressPedal(); - else - releasePedal(); - } break; - // all notes off (panic) - case 0x7b: { - if (msg.getValue() == 0) { - panic(); - } - } break; - default: break; - } - } - - void processSystem(const midi::Message &msg) { - switch (msg.getChannel()) { - // Song Position Pointer - case 0x2: { - int32_t pos = int32_t(msg.getNote()) | (int32_t(msg.getValue()) << 7); - clock = pos * 6; - } break; - // Timing - case 0x8: { - clockPulse.trigger(1e-3); - if (clock % clockDivision == 0) { - clockDividerPulse.trigger(1e-3); - } - clock++; - } break; - // Start - case 0xa: { - startPulse.trigger(1e-3); - clock = 0; - } break; - // Continue - case 0xb: { - continuePulse.trigger(1e-3); - } break; - // Stop - case 0xc: { - stopPulse.trigger(1e-3); - } break; - default: break; - } - } - - int assignChannel(uint8_t note) { - if (channels == 1) - return 0; - - switch (polyMode) { - case REUSE_MODE: { - // Find channel with the same note - for (int c = 0; c < channels; c++) { - if (notes[c] == note) - return c; - } - } // fallthrough - - case ROTATE_MODE: { - // Find next available channel - for (int i = 0; i < channels; i++) { - rotateIndex++; - if (rotateIndex >= channels) - rotateIndex = 0; - if (!gates[rotateIndex]) - return rotateIndex; - } - // No notes are available. Advance rotateIndex once more. - rotateIndex++; - if (rotateIndex >= channels) - rotateIndex = 0; - return rotateIndex; - } break; - - case RESET_MODE: { - for (int c = 0; c < channels; c++) { - if (!gates[c]) - return c; - } - return channels - 1; - } break; - - case MPE_MODE: { - // This case is handled by querying the MIDI message channel. - return 0; - } break; - - default: return 0; - } - } - - void pressNote(uint8_t note, int* channel) { - // Remove existing similar note - auto it = std::find(heldNotes.begin(), heldNotes.end(), note); - if (it != heldNotes.end()) - heldNotes.erase(it); - // Push note - heldNotes.push_back(note); - // Determine actual channel - if (polyMode == MPE_MODE) { - // Channel is already decided for us - } - else { - *channel = assignChannel(note); - } - // Set note - notes[*channel] = note; - gates[*channel] = true; - retriggerPulses[*channel].trigger(1e-3); - } - - void releaseNote(uint8_t note) { - // Remove the note - auto it = std::find(heldNotes.begin(), heldNotes.end(), note); - if (it != heldNotes.end()) - heldNotes.erase(it); - // Hold note if pedal is pressed - if (pedal) - return; - // Turn off gate of all channels with note - for (int c = 0; c < channels; c++) { - if (notes[c] == note) { - gates[c] = false; - } - } - // Set last note if monophonic - if (channels == 1) { - if (note == notes[0] && !heldNotes.empty()) { - uint8_t lastNote = heldNotes.back(); - notes[0] = lastNote; - gates[0] = true; - return; - } - } - } - - void pressPedal() { - if (pedal) - return; - pedal = true; - } - - void releasePedal() { - if (!pedal) - return; - pedal = false; - // Set last note if monophonic - if (channels == 1) { - if (!heldNotes.empty()) { - // Replace note with last held note - uint8_t lastNote = heldNotes.back(); - notes[0] = lastNote; - } - else { - // Disable gate - gates[0] = false; - } - } - // Clear notes that are not held if polyphonic - else { - for (int c = 0; c < channels; c++) { - if (!gates[c]) - continue; - // Disable all gates - gates[c] = false; - // Re-enable gate if channel's note is still held - for (uint8_t note : heldNotes) { - if (notes[c] == note) { - gates[c] = true; - break; - } - } - } - } - } - - void setChannels(int channels) { - if (channels == this->channels) - return; - this->channels = channels; - panic(); - } - - void setPolyMode(PolyMode polyMode) { - if (polyMode == this->polyMode) - return; - this->polyMode = polyMode; - panic(); + midiParser.processPulses(args.sampleTime); } json_t* dataToJson() override { - json_t* rootJ = json_object(); - json_object_set_new(rootJ, "pwRange", json_real(pwRange)); - json_object_set_new(rootJ, "smooth", json_boolean(smooth)); - json_object_set_new(rootJ, "channels", json_integer(channels)); - json_object_set_new(rootJ, "polyMode", json_integer(polyMode)); - json_object_set_new(rootJ, "clockDivision", json_integer(clockDivision)); - // Saving/restoring pitch and mod doesn't make much sense for MPE. - if (polyMode != MPE_MODE) { - json_object_set_new(rootJ, "lastPitch", json_integer(pws[0])); - json_object_set_new(rootJ, "lastMod", json_integer(mods[0])); - } + json_t* rootJ = midiParser.toJson(); json_object_set_new(rootJ, "midi", midiInput.toJson()); return rootJ; } void dataFromJson(json_t* rootJ) override { - json_t* pwRangeJ = json_object_get(rootJ, "pwRange"); - if (pwRangeJ) - pwRange = json_number_value(pwRangeJ); // For backwards compatibility, set to 0 if undefined in JSON. - else - pwRange = 0; - - json_t* smoothJ = json_object_get(rootJ, "smooth"); - if (smoothJ) - smooth = json_boolean_value(smoothJ); - - json_t* channelsJ = json_object_get(rootJ, "channels"); - if (channelsJ) - setChannels(json_integer_value(channelsJ)); - - json_t* polyModeJ = json_object_get(rootJ, "polyMode"); - if (polyModeJ) - polyMode = (PolyMode) json_integer_value(polyModeJ); - - json_t* clockDivisionJ = json_object_get(rootJ, "clockDivision"); - if (clockDivisionJ) - clockDivision = json_integer_value(clockDivisionJ); - - json_t* lastPitchJ = json_object_get(rootJ, "lastPitch"); - if (lastPitchJ) - pws[0] = json_integer_value(lastPitchJ); + midiParser.pwRange = 0; - json_t* lastModJ = json_object_get(rootJ, "lastMod"); - if (lastModJ) - mods[0] = json_integer_value(lastModJ); + midiParser.fromJson(rootJ); json_t* midiJ = json_object_get(rootJ, "midi"); if (midiJ) @@ -543,35 +165,36 @@ struct MIDI_CVWidget : ModuleWidget { else return string::f("%g octave", pwRange / 12) + (pwRange / 12 == 1 ? "" : "s"); }; - menu->addChild(createSubmenuItem("Pitch bend range", getPwRangeLabel(module->pwRange), [=](Menu* menu) { + menu->addChild(createSubmenuItem("Pitch bend range", getPwRangeLabel(module->midiParser.pwRange), [=](Menu* menu) { for (size_t i = 0; i < pwRanges.size(); i++) { menu->addChild(createCheckMenuItem(getPwRangeLabel(pwRanges[i]), "", - [=]() {return module->pwRange == pwRanges[i];}, - [=]() {module->pwRange = pwRanges[i];} + [=]() {return module->midiParser.pwRange == pwRanges[i];}, + [=]() {module->midiParser.pwRange = pwRanges[i];} )); } })); - menu->addChild(createBoolPtrMenuItem("Smooth pitch/mod wheel", "", &module->smooth)); + menu->addChild(createBoolPtrMenuItem("Smooth pitch/mod wheel", "", &module->midiParser.smooth)); - static const std::vector clockDivisions = {24 * 4, 24 * 2, 24, 24 / 2, 24 / 4, 24 / 8, 2, 1}; + static const std::vector clockDivisions = {24 * 4, 24 * 2, 24, 24 / 2, 24 / 4, 24 / 8, 2, 1}; static const std::vector clockDivisionLabels = {"Whole", "Half", "Quarter", "8th", "16th", "32nd", "12 PPQN", "24 PPQN"}; - size_t clockDivisionIndex = std::find(clockDivisions.begin(), clockDivisions.end(), module->clockDivision) - clockDivisions.begin(); + size_t clockDivisionIndex = std::find(clockDivisions.begin(), clockDivisions.end(), module->midiParser.clockDivision) - clockDivisions.begin(); std::string clockDivisionLabel = (clockDivisionIndex < clockDivisionLabels.size()) ? clockDivisionLabels[clockDivisionIndex] : ""; menu->addChild(createSubmenuItem("CLK/N divider", clockDivisionLabel, [=](Menu* menu) { for (size_t i = 0; i < clockDivisions.size(); i++) { menu->addChild(createCheckMenuItem(clockDivisionLabels[i], "", - [=]() {return module->clockDivision == clockDivisions[i];}, - [=]() {module->clockDivision = clockDivisions[i];} + [=]() {return module->midiParser.clockDivision == clockDivisions[i];}, + [=]() {module->midiParser.clockDivision = clockDivisions[i];} )); } })); - menu->addChild(createSubmenuItem("Polyphony channels", string::f("%d", module->channels), [=](Menu* menu) { + menu->addChild(createSubmenuItem("Polyphony channels", string::f("%d", module->midiParser.channels), [=](Menu* menu) { for (int c = 1; c <= 16; c++) { - menu->addChild(createCheckMenuItem((c == 1) ? "Monophonic" : string::f("%d", c), "", - [=]() {return module->channels == c;}, - [=]() {module->setChannels(c);} + std::string channelsLabel = (c == 1) ? "Monophonic" : string::f("%d", c); + menu->addChild(createCheckMenuItem(channelsLabel, "", + [=]() {return module->midiParser.channels == c;}, + [=]() {module->midiParser.setChannels(c);} )); } })); @@ -581,10 +204,10 @@ struct MIDI_CVWidget : ModuleWidget { "Reuse", "Reset", "MPE", - }, &module->polyMode)); + }, &module->midiParser.polyMode)); menu->addChild(createMenuItem("Panic", "", - [=]() {module->panic();} + [=]() {module->midiParser.panic();} )); // Example of using appendMidiMenu()