| @@ -15,6 +15,16 @@ | |||||
| * For a full copy of the GNU General Public License see the LICENSE file. | * 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" | #include "plugincontext.hpp" | ||||
| #ifndef HEADLESS | #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; | using namespace CarlaBackend; | ||||
| static uint32_t host_get_buffer_size(NativeHostHandle); | static uint32_t host_get_buffer_size(NativeHostHandle); | ||||
| @@ -85,6 +213,12 @@ struct IldaeilModule : Module { | |||||
| enum InputIds { | enum InputIds { | ||||
| INPUT1, | INPUT1, | ||||
| INPUT2, | INPUT2, | ||||
| PITCH_INPUT, | |||||
| GATE_INPUT, | |||||
| VEL_INPUT, | |||||
| AFT_INPUT, | |||||
| PW_INPUT, | |||||
| MW_INPUT, | |||||
| NUM_INPUTS | NUM_INPUTS | ||||
| }; | }; | ||||
| enum OutputIds { | enum OutputIds { | ||||
| @@ -105,7 +239,6 @@ struct IldaeilModule : Module { | |||||
| CarlaHostHandle fCarlaHostHandle = nullptr; | CarlaHostHandle fCarlaHostHandle = nullptr; | ||||
| mutable NativeTimeInfo fCarlaTimeInfo; | mutable NativeTimeInfo fCarlaTimeInfo; | ||||
| // mutable water::MemoryOutputStream fLastProjectState; | |||||
| void* fUI = nullptr; | void* fUI = nullptr; | ||||
| @@ -115,10 +248,24 @@ struct IldaeilModule : Module { | |||||
| float audioDataOut2[BUFFER_SIZE]; | float audioDataOut2[BUFFER_SIZE]; | ||||
| unsigned audioDataFill = 0; | unsigned audioDataFill = 0; | ||||
| IldaeilMidiGenerator midiGenerator; | |||||
| IldaeilModule() | IldaeilModule() | ||||
| : pcontext(reinterpret_cast<CardinalPluginContext*>(APP)) | : pcontext(reinterpret_cast<CardinalPluginContext*>(APP)) | ||||
| { | { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | 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(audioDataOut1, 0, sizeof(audioDataOut1)); | ||||
| std::memset(audioDataOut2, 0, sizeof(audioDataOut2)); | std::memset(audioDataOut2, 0, sizeof(audioDataOut2)); | ||||
| @@ -270,7 +417,7 @@ struct IldaeilModule : Module { | |||||
| engine->loadProjectInternal(xml, true); | engine->loadProjectInternal(xml, true); | ||||
| } | } | ||||
| void process(const ProcessArgs&) override | |||||
| void process(const ProcessArgs& args) override | |||||
| { | { | ||||
| if (fCarlaPluginHandle == nullptr) | if (fCarlaPluginHandle == nullptr) | ||||
| return; | return; | ||||
| @@ -282,15 +429,46 @@ struct IldaeilModule : Module { | |||||
| outputs[OUTPUT1].setVoltage(audioDataOut1[i] * 10.0f); | outputs[OUTPUT1].setVoltage(audioDataOut1[i] * 10.0f); | ||||
| outputs[OUTPUT2].setVoltage(audioDataOut2[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) | if (audioDataFill == BUFFER_SIZE) | ||||
| { | { | ||||
| audioDataFill = 0; | audioDataFill = 0; | ||||
| float* ins[2] = { audioDataIn1, audioDataIn2 }; | float* ins[2] = { audioDataIn1, audioDataIn2 }; | ||||
| float* outs[2] = { audioDataOut1, audioDataOut2 }; | 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 | void onSampleRateChange(const SampleRateChangeEvent& e) override | ||||
| { | { | ||||
| if (fCarlaPluginHandle == nullptr) | if (fCarlaPluginHandle == nullptr) | ||||
| @@ -493,10 +671,14 @@ struct IldaeilWidget : ImGuiWidget, IdleCallback, Thread { | |||||
| if (module->fCarlaHostHandle != nullptr) | if (module->fCarlaHostHandle != nullptr) | ||||
| { | { | ||||
| module->fUI = 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_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()) | if (isThreadRunning()) | ||||
| @@ -1235,6 +1417,13 @@ struct IldaeilModuleWidget : ModuleWidget { | |||||
| addInput(createInput<PJ301MPort>(Vec(3, 54 + 30), module, IldaeilModule::INPUT2)); | 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 + 60), module, IldaeilModule::OUTPUT1)); | ||||
| addOutput(createOutput<PJ301MPort>(Vec(3, 54 + 90), module, IldaeilModule::OUTPUT2)); | 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 | #else | ||||