| @@ -15,6 +15,16 @@ | |||
| * For a full copy of the GNU General Public License see the LICENSE file. | |||
| */ | |||
| /** | |||
| * This file uses code adapted from VCVRack's CV_MIDI.cpp and midi.hpp | |||
| * Copyright (C) 2016-2021 VCV. | |||
| * | |||
| * This program is free software: you can redistribute it and/or | |||
| * modify it under the terms of the GNU General Public License as | |||
| * published by the Free Software Foundation; either version 3 of | |||
| * the License, or (at your option) any later version. | |||
| */ | |||
| #include "plugincontext.hpp" | |||
| #ifndef HEADLESS | |||
| @@ -61,6 +71,124 @@ namespace ildaeil { | |||
| // -------------------------------------------------------------------------------------------------------------------- | |||
| /** Converts gates and CV to MIDI messages. */ | |||
| struct IldaeilMidiGenerator { | |||
| static const constexpr uint CHANNELS = 16; | |||
| static const constexpr uint MAX_MIDI_EVENTS = 128; | |||
| int8_t vels[CHANNELS]; | |||
| int8_t notes[CHANNELS]; | |||
| bool gates[CHANNELS]; | |||
| int8_t keyPressures[CHANNELS]; | |||
| int8_t mw; | |||
| int16_t pw; | |||
| uint32_t frame; | |||
| // generated output | |||
| uint midiEventCount; | |||
| NativeMidiEvent midiEvents[MAX_MIDI_EVENTS]; | |||
| IldaeilMidiGenerator() { | |||
| reset(); | |||
| } | |||
| void reset() { | |||
| for (uint c = 0; c < CHANNELS; c++) { | |||
| vels[c] = 100; | |||
| notes[c] = 60; | |||
| gates[c] = false; | |||
| keyPressures[c] = -1; | |||
| } | |||
| mw = -1; | |||
| pw = 0x2000; | |||
| frame = 0; | |||
| midiEventCount = 0; | |||
| } | |||
| void setFrame(uint32_t frame) { | |||
| this->frame = frame; | |||
| if (frame == 0) | |||
| midiEventCount = 0; | |||
| } | |||
| /** Must be called before setNoteGate(). */ | |||
| void setVelocity(int8_t vel, int c) { | |||
| vels[c] = vel; | |||
| } | |||
| void setNoteGate(int8_t note, bool gate, int c) { | |||
| if (midiEventCount == MAX_MIDI_EVENTS) | |||
| return; | |||
| bool changedNote = gate && gates[c] && (note != notes[c]); | |||
| bool enabledGate = gate && !gates[c]; | |||
| bool disabledGate = !gate && gates[c]; | |||
| if (changedNote || disabledGate) { | |||
| // Note off | |||
| NativeMidiEvent& m(midiEvents[midiEventCount++]); | |||
| m.time = frame; | |||
| m.port = 0; | |||
| m.size = 3; | |||
| m.data[0] = 0x80; | |||
| m.data[1] = notes[c]; | |||
| m.data[2] = vels[c]; | |||
| } | |||
| if (changedNote || enabledGate) { | |||
| // Note on | |||
| NativeMidiEvent& m(midiEvents[midiEventCount++]); | |||
| m.time = frame; | |||
| m.port = 0; | |||
| m.size = 3; | |||
| m.data[0] = 0x90; | |||
| m.data[1] = note; | |||
| m.data[2] = vels[c]; | |||
| } | |||
| notes[c] = note; | |||
| gates[c] = gate; | |||
| } | |||
| void setKeyPressure(int8_t val, int c) { | |||
| if (keyPressures[c] == val || midiEventCount == MAX_MIDI_EVENTS) | |||
| return; | |||
| keyPressures[c] = val; | |||
| // Polyphonic key pressure | |||
| NativeMidiEvent& m(midiEvents[midiEventCount++]); | |||
| m.time = frame; | |||
| m.port = 0; | |||
| m.size = 3; | |||
| m.data[0] = 0xa0; | |||
| m.data[1] = notes[c]; | |||
| m.data[2] = val; | |||
| } | |||
| void setModWheel(int8_t mw) { | |||
| if (this->mw == mw || midiEventCount == MAX_MIDI_EVENTS) | |||
| return; | |||
| this->mw = mw; | |||
| // Modulation Wheel (CC1) | |||
| NativeMidiEvent& m(midiEvents[midiEventCount++]); | |||
| m.time = frame; | |||
| m.port = 0; | |||
| m.size = 3; | |||
| m.data[0] = 0xb0; | |||
| m.data[1] = 1; | |||
| m.data[2] = mw; | |||
| } | |||
| void setPitchWheel(int16_t pw) { | |||
| if (this->pw == pw || midiEventCount == MAX_MIDI_EVENTS) | |||
| return; | |||
| this->pw = pw; | |||
| // Pitch Wheel | |||
| NativeMidiEvent& m(midiEvents[midiEventCount++]); | |||
| m.time = frame; | |||
| m.port = 0; | |||
| m.size = 3; | |||
| m.data[0] = 0xe0; | |||
| m.data[1] = pw & 0x7f; | |||
| m.data[2] = (pw >> 7) & 0x7f; | |||
| } | |||
| }; | |||
| // -------------------------------------------------------------------------------------------------------------------- | |||
| using namespace CarlaBackend; | |||
| static uint32_t host_get_buffer_size(NativeHostHandle); | |||
| @@ -85,6 +213,12 @@ struct IldaeilModule : Module { | |||
| enum InputIds { | |||
| INPUT1, | |||
| INPUT2, | |||
| PITCH_INPUT, | |||
| GATE_INPUT, | |||
| VEL_INPUT, | |||
| AFT_INPUT, | |||
| PW_INPUT, | |||
| MW_INPUT, | |||
| NUM_INPUTS | |||
| }; | |||
| enum OutputIds { | |||
| @@ -105,7 +239,6 @@ struct IldaeilModule : Module { | |||
| CarlaHostHandle fCarlaHostHandle = nullptr; | |||
| mutable NativeTimeInfo fCarlaTimeInfo; | |||
| // mutable water::MemoryOutputStream fLastProjectState; | |||
| void* fUI = nullptr; | |||
| @@ -115,10 +248,24 @@ struct IldaeilModule : Module { | |||
| float audioDataOut2[BUFFER_SIZE]; | |||
| unsigned audioDataFill = 0; | |||
| IldaeilMidiGenerator midiGenerator; | |||
| IldaeilModule() | |||
| : pcontext(reinterpret_cast<CardinalPluginContext*>(APP)) | |||
| { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| for (uint i=0; i<2; ++i) | |||
| { | |||
| const char name[] = { 'A','u','d','i','o',' ','#',static_cast<char>('0'+i+1),'\0' }; | |||
| configInput(i, name); | |||
| configOutput(i, name); | |||
| } | |||
| configInput(PITCH_INPUT, "Pitch (1V/oct)"); | |||
| configInput(GATE_INPUT, "Gate"); | |||
| configInput(VEL_INPUT, "Velocity"); | |||
| configInput(AFT_INPUT, "Aftertouch"); | |||
| configInput(PW_INPUT, "Pitch wheel"); | |||
| configInput(MW_INPUT, "Mod wheel"); | |||
| std::memset(audioDataOut1, 0, sizeof(audioDataOut1)); | |||
| std::memset(audioDataOut2, 0, sizeof(audioDataOut2)); | |||
| @@ -270,7 +417,7 @@ struct IldaeilModule : Module { | |||
| engine->loadProjectInternal(xml, true); | |||
| } | |||
| void process(const ProcessArgs&) override | |||
| void process(const ProcessArgs& args) override | |||
| { | |||
| if (fCarlaPluginHandle == nullptr) | |||
| return; | |||
| @@ -282,15 +429,46 @@ struct IldaeilModule : Module { | |||
| outputs[OUTPUT1].setVoltage(audioDataOut1[i] * 10.0f); | |||
| outputs[OUTPUT2].setVoltage(audioDataOut2[i] * 10.0f); | |||
| midiGenerator.setFrame(i); | |||
| for (int c = 0; c < inputs[PITCH_INPUT].getChannels(); c++) { | |||
| int vel = (int) std::round(inputs[VEL_INPUT].getNormalPolyVoltage(10.f * 100 / 127, c) / 10.f * 127); | |||
| vel = clamp(vel, 0, 127); | |||
| midiGenerator.setVelocity(vel, c); | |||
| int note = (int) std::round(inputs[PITCH_INPUT].getVoltage(c) * 12.f + 60.f); | |||
| note = clamp(note, 0, 127); | |||
| bool gate = inputs[GATE_INPUT].getPolyVoltage(c) >= 1.f; | |||
| midiGenerator.setNoteGate(note, gate, c); | |||
| int aft = (int) std::round(inputs[AFT_INPUT].getPolyVoltage(c) / 10.f * 127); | |||
| aft = clamp(aft, 0, 127); | |||
| midiGenerator.setKeyPressure(aft, c); | |||
| } | |||
| int pw = (int) std::round((inputs[PW_INPUT].getVoltage() + 5.f) / 10.f * 0x4000); | |||
| pw = clamp(pw, 0, 0x3fff); | |||
| midiGenerator.setPitchWheel(pw); | |||
| int mw = (int) std::round(inputs[MW_INPUT].getVoltage() / 10.f * 127); | |||
| mw = clamp(mw, 0, 127); | |||
| midiGenerator.setModWheel(mw); | |||
| if (audioDataFill == BUFFER_SIZE) | |||
| { | |||
| audioDataFill = 0; | |||
| float* ins[2] = { audioDataIn1, audioDataIn2 }; | |||
| float* outs[2] = { audioDataOut1, audioDataOut2 }; | |||
| fCarlaPluginDescriptor->process(fCarlaPluginHandle, ins, outs, BUFFER_SIZE, nullptr, 0); | |||
| fCarlaPluginDescriptor->process(fCarlaPluginHandle, ins, outs, BUFFER_SIZE, | |||
| midiGenerator.midiEvents, midiGenerator.midiEventCount); | |||
| } | |||
| } | |||
| void onReset() override | |||
| { | |||
| midiGenerator.reset(); | |||
| } | |||
| void onSampleRateChange(const SampleRateChangeEvent& e) override | |||
| { | |||
| if (fCarlaPluginHandle == nullptr) | |||
| @@ -493,10 +671,14 @@ struct IldaeilWidget : ImGuiWidget, IdleCallback, Thread { | |||
| if (module->fCarlaHostHandle != nullptr) | |||
| { | |||
| module->fUI = nullptr; | |||
| if (fPluginRunning) | |||
| carla_show_custom_ui(module->fCarlaHostHandle, 0, false); | |||
| carla_set_engine_option(module->fCarlaHostHandle, ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0"); | |||
| carla_show_custom_ui(module->fCarlaHostHandle, 0, false); | |||
| module->pcontext->removeIdleCallback(this); | |||
| if (idleCallbackActive) | |||
| module->pcontext->removeIdleCallback(this); | |||
| } | |||
| if (isThreadRunning()) | |||
| @@ -1235,6 +1417,13 @@ struct IldaeilModuleWidget : ModuleWidget { | |||
| addInput(createInput<PJ301MPort>(Vec(3, 54 + 30), module, IldaeilModule::INPUT2)); | |||
| addOutput(createOutput<PJ301MPort>(Vec(3, 54 + 60), module, IldaeilModule::OUTPUT1)); | |||
| addOutput(createOutput<PJ301MPort>(Vec(3, 54 + 90), module, IldaeilModule::OUTPUT2)); | |||
| addOutput(createInput<PJ301MPort>(Vec(3, 54 + 135), module, IldaeilModule::PITCH_INPUT)); | |||
| addOutput(createInput<PJ301MPort>(Vec(3, 54 + 165), module, IldaeilModule::GATE_INPUT)); | |||
| addOutput(createInput<PJ301MPort>(Vec(3, 54 + 195), module, IldaeilModule::VEL_INPUT)); | |||
| addOutput(createInput<PJ301MPort>(Vec(3, 54 + 225), module, IldaeilModule::AFT_INPUT)); | |||
| addOutput(createInput<PJ301MPort>(Vec(3, 54 + 255), module, IldaeilModule::PW_INPUT)); | |||
| addOutput(createInput<PJ301MPort>(Vec(3, 54 + 285), module, IldaeilModule::MW_INPUT)); | |||
| } | |||
| }; | |||
| #else | |||