diff --git a/doc/design.md b/doc/design.md index f5ebc32..77ca2bf 100644 --- a/doc/design.md +++ b/doc/design.md @@ -7,7 +7,7 @@ PATCH SELECTOR: - Jog Buttons for next/prev MAIN/PLAY MODE: -- Global Transpose and Global in-semitone-tuning with LFO routable to tuning +- Global Transpose CLAP - An output per OP wher each output is just the solo OP * Main ADSR (and zero if the OP is off) @@ -25,4 +25,6 @@ THINGS I DIDNT GET TO FOR 1.0 - 90%-100% of internal nyquist mute fades (maybe ship without this tho) - Main DAHDSR at DAHD=0 SR=1 means voice lifetime is longest active op mixer DAHDSR and main DAHDSR bypassed - Note Expressions -- CLAP PolyMod \ No newline at end of file +- CLAP PolyMod +- Global tuning other than octave transpose +- A main LFO \ No newline at end of file diff --git a/src/dsp/matrix_node.h b/src/dsp/matrix_node.h index 678e564..d9be81c 100644 --- a/src/dsp/matrix_node.h +++ b/src/dsp/matrix_node.h @@ -421,12 +421,14 @@ struct MixerNode : EnvelopeSupport, } }; -struct OutputNode : EnvelopeSupport +struct OutputNode : EnvelopeSupport, ModulationSupport { float output alignas(16)[2][blockSize]; std::array &fromArr; SRProvider sr; + const Patch::OutputNode &outputNode; + const MonoValues &monoValues; const VoiceValues &voiceValues; @@ -436,9 +438,9 @@ struct OutputNode : EnvelopeSupport OutputNode(const Patch::OutputNode &on, std::array &f, MonoValues &mv, const VoiceValues &vv) - : monoValues(mv), voiceValues(vv), sr(mv), fromArr(f), level(on.level), bendUp(on.bendUp), - bendDown(on.bendDown), velSen(on.velSensitivity), EnvelopeSupport(on, mv, vv), - defTrigV(on.defaultTrigger) + : outputNode(on), ModulationSupport(on, mv, vv), monoValues(mv), voiceValues(vv), sr(mv), + fromArr(f), level(on.level), bendUp(on.bendUp), bendDown(on.bendDown), + velSen(on.velSensitivity), EnvelopeSupport(on, mv, vv), defTrigV(on.defaultTrigger) { memset(output, 0, sizeof(output)); allowVoiceTrigger = false; @@ -447,11 +449,15 @@ struct OutputNode : EnvelopeSupport void attack() { defaultTrigger = (TriggerMode)std::round(defTrigV); + bindModulation(); + calculateModulation(); envAttack(); } + float finalEnvLevel alignas(16)[blockSize]; void renderBlock() { + calculateModulation(); for (const auto &from : fromArr) { mech::accumulate_from_to(from.output[0], output[0]); @@ -459,13 +465,33 @@ struct OutputNode : EnvelopeSupport } envProcess(false); - mech::scale_by(env.outputCache, output[0], output[1]); + mech::copy_from_to(env.outputCache, finalEnvLevel); + mech::scale_by(depthAtten, finalEnvLevel); + mech::scale_by(finalEnvLevel, output[0], output[1]); // Apply main output auto lv = level; - auto v = 1.0 - velSen * (1.0 - voiceValues.velocity); - lv = 0.15 * v * lv * lv * lv; + auto v = 1.f - velSen * (1.f - voiceValues.velocity); + lv = 0.15 * std::clamp(v * lv * lv * lv, 0.f, 1.f); mech::scale_by(lv, output[0], output[1]); + + auto pn = panMod; + if (pn != 0.f) + { + pn = (pn + 1) * 0.5; + sdsp::pan_laws::panmatrix_t pmat; + sdsp::pan_laws::stereoEqualPower(pn, pmat); + + for (int i = 0; i < blockSize; ++i) + { + auto oL = output[0][i]; + auto oR = output[1][i]; + + output[0][i] = pmat[0] * oL + pmat[2] * oR; + output[1][i] = pmat[3] * oL + pmat[1] * oR; + } + } + #if DEBUG_LEVELS for (int i = 0; i < blockSize; ++i) { @@ -476,6 +502,46 @@ struct OutputNode : EnvelopeSupport } #endif } + + float panMod{0.f}; + float depthAtten{1.0}; + + void calculateModulation() + { + depthAtten = 1.f; + attackMod = 0.f; + panMod = 0.f; + + if (!anySources) + return; + + for (int i = 0; i < numModsPer; ++i) + { + if (sourcePointers[i] && + (int)outputNode.modtarget[i].value != Patch::MixerNode::TargetID::NONE) + { + // targets: env depth atten, lfo dept atten, direct adjust, env attack, lfo rate + auto dp = depthPointers[i]; + if (!dp) + continue; + auto d = *dp; + switch ((Patch::OutputNode::TargetID)outputNode.modtarget[i].value) + { + case Patch::OutputNode::PAN: + panMod += d * *sourcePointers[i]; + break; + case Patch::OutputNode::DEPTH_ATTEN: + depthAtten *= 1.0 - d * (1.0 - std::clamp(*sourcePointers[i], 0.f, 1.f)); + break; + case Patch::OutputNode::ENV_ATTACK: + attackMod += d * *sourcePointers[i]; + break; + default: + break; + } + } + } + } }; } // namespace baconpaul::six_sines diff --git a/src/synth/patch.h b/src/synth/patch.h index d2362a6..553faed 100644 --- a/src/synth/patch.h +++ b/src/synth/patch.h @@ -331,6 +331,7 @@ struct Patch // These stream enum TargetID { + SKIP = -1, NONE = 0, DIRECT = 10, ENV_DEPTH_ATTEN = 20, @@ -342,9 +343,11 @@ struct Patch std::vector> targetList{ {TargetID::NONE, "Off"}, + {TargetID::SKIP, ""}, {TargetID::DIRECT, "Ratio"}, - {TargetID::ENV_DEPTH_ATTEN, "Env Depth"}, - {TargetID::LFO_DEPTH_ATTEN, "LFO Depth"}, + {TargetID::ENV_DEPTH_ATTEN, "Env Sens"}, + {TargetID::LFO_DEPTH_ATTEN, "LFO Sens"}, + {TargetID::SKIP, ""}, {TargetID::ENV_ATTACK, "Env Attack"}, {TargetID::LFO_RATE, "LFO Rate"}, }; @@ -466,6 +469,7 @@ struct Patch // These stream enum TargetID { + SKIP = -1, NONE = 0, DIRECT = 10, DEPTH_ATTEN = 20, @@ -477,9 +481,11 @@ struct Patch std::vector> targetList{ {TargetID::NONE, "Off"}, + {TargetID::SKIP, ""}, {TargetID::DIRECT, "Feedback Level"}, - {TargetID::DEPTH_ATTEN, "Mod Depth"}, - {TargetID::LFO_DEPTH_ATTEN, "LFO Depth"}, + {TargetID::DEPTH_ATTEN, "Mod Sens"}, + {TargetID::LFO_DEPTH_ATTEN, "LFO Sens"}, + {TargetID::SKIP, ""}, {TargetID::ENV_ATTACK, "Env Attack"}, {TargetID::LFO_RATE, "LFO Rate"}, }; @@ -554,6 +560,7 @@ struct Patch enum TargetID { + SKIP = -1, NONE = 0, DIRECT = 10, DEPTH_ATTEN = 20, @@ -565,9 +572,11 @@ struct Patch std::vector> targetList{ {TargetID::NONE, "Off"}, + {TargetID::SKIP, ""}, {TargetID::DIRECT, "Modulation Level"}, - {TargetID::DEPTH_ATTEN, "Mod Depth"}, - {TargetID::LFO_DEPTH_ATTEN, "LFO Depth"}, + {TargetID::DEPTH_ATTEN, "Mod Sens"}, + {TargetID::LFO_DEPTH_ATTEN, "LFO Sens"}, + {TargetID::SKIP, ""}, {TargetID::ENV_ATTACK, "Env Attack"}, {TargetID::LFO_RATE, "LFO Rate"}, }; @@ -656,6 +665,7 @@ struct Patch enum TargetID { + SKIP = -1, NONE = 0, DIRECT = 10, PAN = 15, @@ -669,11 +679,14 @@ struct Patch std::vector> targetList{ {TargetID::NONE, "Off"}, + {TargetID::SKIP, ""}, {TargetID::DIRECT, "Amplitude"}, {TargetID::PAN, "Pan"}, - {TargetID::DEPTH_ATTEN, "Env Depth"}, - {TargetID::LFO_DEPTH_ATTEN, "LFOENV Depth"}, - {TargetID::LFO_DEPTH_ATTEN, "LFOPan Depth"}, + {TargetID::SKIP, ""}, + {TargetID::DEPTH_ATTEN, "Env Sens"}, + {TargetID::LFO_DEPTH_ATTEN, "LFOENV Sens"}, + {TargetID::LFO_DEPTH_ATTEN, "LFOPan Sens"}, + {TargetID::SKIP, ""}, {TargetID::ENV_ATTACK, "Env Attack"}, {TargetID::LFO_RATE, "LFO Rate"}, }; @@ -779,9 +792,25 @@ struct Patch } }; - struct OutputNode : public DAHDSRMixin + struct OutputNode : public DAHDSRMixin, public ModulationMixin { static constexpr uint32_t idBase{500}; + + enum TargetID + { + SKIP = -1, + NONE = 0, + PAN = 15, + DEPTH_ATTEN = 20, + + ENV_ATTACK = 40, + }; + + std::vector> targetList{ + {TargetID::NONE, "Off"}, {TargetID::SKIP, ""}, {TargetID::DEPTH_ATTEN, "Env Sens"}, + {TargetID::PAN, "Pan"}, {TargetID::SKIP, ""}, {TargetID::ENV_ATTACK, "Env Attack"}, + }; + OutputNode() : DAHDSRMixin(name(), id(2), true), level(floatMd() .asPercent() @@ -879,7 +908,19 @@ struct Patch .withGroupName(name()) .withRange(1, 96) .withDefault(24) - .withID(id(34))) + .withID(id(34))), + + ModulationMixin(name(), id(120)), + modtarget(scpu::make_array_lambda( + [this](int i) + { + return md_t() + .withName(name() + " Mod Target " + std::to_string(i)) + .withGroupName(name()) + .withRange(0, 2000) + .withDefault(TargetID::NONE) + .withID(id(150 + i)); + })) { defaultTrigger.adhocFeatures = Param::AdHocFeatureValues::TRIGGERMODE; } @@ -892,6 +933,8 @@ struct Patch Param unisonCount, unisonSpread, uniPhaseRand; Param mpeActive, mpeBendRange; + std::array modtarget; + std::vector params() { std::vector res{ @@ -899,6 +942,11 @@ struct Patch &polyLimit, &defaultTrigger, &portaTime, &pianoModeActive, &unisonCount, &unisonSpread, &uniPhaseRand, &mpeActive, &mpeBendRange}; appendDAHDSRParams(res); + + for (int i = 0; i < numModsPer; ++i) + res.push_back(&modtarget[i]); + appendModulationParams(res); + return res; } }; diff --git a/src/ui/main-sub-panel.cpp b/src/ui/main-sub-panel.cpp index e6bcc82..cc23b7b 100644 --- a/src/ui/main-sub-panel.cpp +++ b/src/ui/main-sub-panel.cpp @@ -18,11 +18,13 @@ namespace baconpaul::six_sines::ui { -MainSubPanel::MainSubPanel(SixSinesEditor &e) : HasEditor(e), DAHDSRComponents() +MainSubPanel::MainSubPanel(SixSinesEditor &e) + : HasEditor(e), DAHDSRComponents(), ModulationComponents() { auto &on = editor.patchCopy.output; voiceTrigerAllowed = false; setupDAHDSR(e, on); + setupModulation(e, on); createComponent(editor, *this, on.velSensitivity, velSen, velSenD); addAndMakeVisible(*velSen); @@ -201,6 +203,8 @@ void MainSubPanel::resized() mpeRange->setBounds(bbx.withHeight(uicLabelHeight)); bbx = bbx.translated(0, uicMargin + uicLabelHeight); mpeRangeL->setBounds(bbx.withHeight(uicLabelHeight)); + + layoutModulation(p); } void MainSubPanel::setTriggerButtonLabel() @@ -225,7 +229,7 @@ void MainSubPanel::showTriggerButtonMenu() { auto tmv = (int)std::round(editor.patchCopy.output.defaultTrigger.value); - auto genSet = [w = juce::Component::SafePointer(asComp())](int nv) + auto genSet = [w = juce::Component::SafePointer(this)](int nv) { auto that = w; return [nv, that]() @@ -246,7 +250,7 @@ void MainSubPanel::showTriggerButtonMenu() p.addItem(TriggerModeName[g], true, tmv == g, genSet(g)); } - p.showMenuAsync(juce::PopupMenu::Options().withParentComponent(&asComp()->editor)); + p.showMenuAsync(juce::PopupMenu::Options().withParentComponent(&this->editor)); } void MainSubPanel::setEnabledState() diff --git a/src/ui/main-sub-panel.h b/src/ui/main-sub-panel.h index 32f7338..60a0d43 100644 --- a/src/ui/main-sub-panel.h +++ b/src/ui/main-sub-panel.h @@ -25,11 +25,15 @@ #include "patch-data-bindings.h" #include "six-sines-editor.h" #include "dahdsr-components.h" +#include "modulation-components.h" #include "ruled-label.h" namespace baconpaul::six_sines::ui { -struct MainSubPanel : juce::Component, HasEditor, DAHDSRComponents +struct MainSubPanel : juce::Component, + HasEditor, + DAHDSRComponents, + ModulationComponents { MainSubPanel(SixSinesEditor &); ~MainSubPanel(); diff --git a/src/ui/modulation-components.h b/src/ui/modulation-components.h index f870ab1..e636192 100644 --- a/src/ui/modulation-components.h +++ b/src/ui/modulation-components.h @@ -155,19 +155,26 @@ template struct ModulationComponents p.addSeparator(); for (const auto &[id, nm] : patchPtr->targetList) { - p.addItem(nm, - [si = id, w = juce::Component::SafePointer(asComp()), index]() - { - if (!w) - return; - if (!w->patchPtr) - return; - w->patchPtr->modtarget[index].value = si; - w->editor.uiToAudio.push({Synth::UIToAudioMsg::SET_PARAM, - w->patchPtr->modtarget[index].meta.id, - (float)si}); - w->resetTargetLabel(index); - }); + if (id == -1) + { + p.addSeparator(); + } + else + { + p.addItem(nm, + [si = id, w = juce::Component::SafePointer(asComp()), index]() + { + if (!w) + return; + if (!w->patchPtr) + return; + w->patchPtr->modtarget[index].value = si; + w->editor.uiToAudio.push({Synth::UIToAudioMsg::SET_PARAM, + w->patchPtr->modtarget[index].meta.id, + (float)si}); + w->resetTargetLabel(index); + }); + } } p.showMenuAsync(juce::PopupMenu::Options().withParentComponent(&asComp()->editor)); }