From bdc9522662b5ed3ca1d200724c780ab2d6c5ef64 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Fri, 29 Oct 2021 08:42:43 +0200 Subject: [PATCH] Add multiple output capabilities to the library and plugins Add a fast path to effect busses Get the number of outputs in the Synth Duplicate effect buses and process outputs separately Update the API wrappers to handle multichannel processing Accomodate multiple outputs in the VST plugin Publish a 8 stereo version of the LV2 plugin Publish a 8 stereo version of the VST3 plugin WIP Remove the duplication of effects Cleanups Debugs Tweaks Windows free error Port numbers change between plugin versions Fix warning Proper meter backgrounds --- plugins/common/plugin/RMSFollower.h | 149 +++++++- plugins/editor/layout/main.fl | 52 +-- plugins/editor/src/editor/EditIds.h | 7 +- plugins/editor/src/editor/Editor.cpp | 95 +++-- plugins/editor/src/editor/layout/main.hpp | 32 +- plugins/lv2/manifest.ttl.in | 5 + plugins/lv2/sfizz.cpp | 176 ++++++++- plugins/lv2/sfizz.ttl.in | 439 ++++++++++++++++++++++ plugins/lv2/sfizz_lv2.h | 48 +++ plugins/lv2/sfizz_lv2_common.cpp | 8 + plugins/lv2/sfizz_lv2_plugin.h | 3 +- plugins/lv2/sfizz_ui.cpp | 177 ++++++--- plugins/vst/SfizzVstController.cpp | 17 +- plugins/vst/SfizzVstEditor.cpp | 12 +- plugins/vst/SfizzVstIDs.h | 2 + plugins/vst/SfizzVstParameters.h | 13 +- plugins/vst/SfizzVstProcessor.cpp | 81 +++- plugins/vst/SfizzVstProcessor.h | 10 +- plugins/vst/VstPluginFactory.cpp | 11 + src/sfizz.h | 3 +- src/sfizz/AudioBuffer.h | 2 +- src/sfizz/AudioSpan.h | 27 +- src/sfizz/Config.h | 2 +- src/sfizz/Defaults.cpp | 1 + src/sfizz/Defaults.h | 1 + src/sfizz/Effects.cpp | 16 +- src/sfizz/Effects.h | 11 +- src/sfizz/Region.cpp | 3 + src/sfizz/Region.h | 1 + src/sfizz/Synth.cpp | 150 +++++--- src/sfizz/Synth.h | 3 +- src/sfizz/SynthMessaging.cpp | 9 + src/sfizz/SynthPrivate.h | 6 +- src/sfizz/Voice.cpp | 2 +- src/sfizz/effects/Disto.cpp | 1 - src/sfizz/effects/Lofi.cpp | 1 - src/sfizz/effects/Rectify.cpp | 1 - src/sfizz/sfizz.cpp | 5 +- src/sfizz/sfizz_wrapper.cpp | 6 +- tests/RegionValuesT.cpp | 58 +++ tests/SynthT.cpp | 55 ++- 41 files changed, 1439 insertions(+), 262 deletions(-) diff --git a/plugins/common/plugin/RMSFollower.h b/plugins/common/plugin/RMSFollower.h index 43cc7ac2..77274b92 100644 --- a/plugins/common/plugin/RMSFollower.h +++ b/plugins/common/plugin/RMSFollower.h @@ -8,24 +8,48 @@ #include #include #include +#ifdef _WIN32 +#include +#endif class RMSFollower { public: RMSFollower() { + setNumOutputs(numOutputs_); updatePole(); } + ~RMSFollower() + { + freeAlignedMemory(); + } + void clear() { - mem_ = simde_mm_setzero_ps(); + for (int c = 0; c < numOutputs_; c += 4) { + simde__m128* mem = (simde__m128*) (mem_ + c); + *mem = simde_mm_setzero_ps(); + } + } + + void setNumOutputs(int numOutputs) + { + freeAlignedMemory(); + numOutputs_ = numOutputs; + memSize_ = numOutputs % 4 == 0 ? numOutputs * sizeof(float) : (numOutputs / 4 + 1) * 4 * sizeof(float); +#ifdef _WIN32 + mem_ = (float *)_aligned_malloc(memSize_, 4 * sizeof(float)); +#else + mem_ = (float *)aligned_alloc(4 * sizeof(float), memSize_); +#endif + clear(); } void init(float sampleRate) { sampleRate_ = sampleRate; updatePole(); - clear(); } void setT60(float t60) @@ -34,40 +58,129 @@ public: updatePole(); } - void process(const float* leftBlock, const float* rightBlock, size_t numFrames) + void process(const float** blocks, size_t numFrames, size_t numChannels) { - simde__m128 mem = mem_; - const simde__m128 pole = simde_mm_load1_ps(&pole_); - for (size_t i = 0; i < numFrames; ++i) { - float left = leftBlock[i]; - float right = rightBlock[i]; - simde__m128 input = simde_mm_setr_ps(left, right, 0.0f, 0.0f); - input = simde_mm_mul_ps(input, input); - simde__m128 output = simde_mm_add_ps(input, simde_mm_mul_ps(pole, simde_mm_sub_ps(mem, input))); - mem = output; + assert(numChannels <= static_cast(numOutputs_)); + const auto numSafeChannels = (numChannels / 4) * 4; + for (size_t c = 0; c < numSafeChannels; c += 4) { + simde__m128* mem = (simde__m128*) (mem_ + c); + process4(mem, &blocks[c], numFrames); } - mem_ = mem; + + simde__m128* mem = (simde__m128*) (mem_ + numSafeChannels); + const auto remainingChannels = numChannels - numSafeChannels; + if (remainingChannels >= 4) + process4(mem, &blocks[numSafeChannels], numFrames); + else if (remainingChannels == 3) + process3(mem, &blocks[numSafeChannels], numFrames); + else if (remainingChannels == 2) + process2(mem, &blocks[numSafeChannels], numFrames); + else if (remainingChannels == 1) + process1(mem, &blocks[numSafeChannels], numFrames); } - simde__m128 getMS() const + void getMS(float* ms, size_t numChannels) const { - return mem_; + assert(numChannels <= static_cast(numOutputs_)); + const auto numSafeChannels = (numChannels / 4) * 4; + for (size_t c = 0; c < numSafeChannels; c += 4) { + simde__m128* mem = (simde__m128*) (mem_ + c); + simde_mm_store_ps(ms + c, *mem); + } + + simde__m128* mem = (simde__m128*) (mem_ + numSafeChannels); + float* temp = (float*)&temp; + simde_mm_store_ps(temp, *mem); + for (size_t c = numSafeChannels, t = 0; c < numChannels; c++, t++) + ms[c] = temp[t]; } - simde__m128 getRMS() const + void getRMS(float* rms, size_t numChannels) const { - return simde_mm_sqrt_ps(mem_); + assert(numChannels <= static_cast(numOutputs_)); + const auto numSafeChannels = (numChannels / 4) * 4; + for (size_t c = 0; c < numSafeChannels; c += 4) { + simde__m128* mem = (simde__m128*) (mem_ + c); + simde_mm_store_ps(rms + c, simde_mm_sqrt_ps(*mem)); + } + + simde__m128* mem = (simde__m128*) (mem_ + numSafeChannels); + float* temp = (float*)&temp_; + simde_mm_store_ps(temp, simde_mm_sqrt_ps(*mem)); + for (size_t c = numSafeChannels, t = 0; c < numChannels; c++, t++) + rms[c] = temp[t]; } private: + void freeAlignedMemory() + { + if (mem_) +#ifdef _WIN32 + _aligned_free(mem_); +#else + free(mem_); +#endif + } + void updatePole() { pole_ = std::exp(float(-2.0 * M_PI) / (t60_ * sampleRate_)); } + void process1(simde__m128* mem, const float** blocks, size_t numFrames) + { + simde__m128 input; + const simde__m128 pole = simde_mm_load1_ps(&pole_); + for (size_t i = 0; i < numFrames; ++i) { + input = simde_mm_setr_ps( + blocks[0][i], 0.0f, 0.0f, 0.0f); + input = simde_mm_mul_ps(input, input); + *mem = simde_mm_add_ps(input, simde_mm_mul_ps(pole, simde_mm_sub_ps(*mem, input))); + } + } + + void process2(simde__m128* mem, const float** blocks, size_t numFrames) + { + simde__m128 input; + const simde__m128 pole = simde_mm_load1_ps(&pole_); + for (size_t i = 0; i < numFrames; ++i) { + input = simde_mm_setr_ps( + blocks[0][i], blocks[1][i], 0.0f, 0.0f); + input = simde_mm_mul_ps(input, input); + *mem = simde_mm_add_ps(input, simde_mm_mul_ps(pole, simde_mm_sub_ps(*mem, input))); + } + } + + void process3(simde__m128* mem, const float** blocks, size_t numFrames) + { + simde__m128 input; + const simde__m128 pole = simde_mm_load1_ps(&pole_); + for (size_t i = 0; i < numFrames; ++i) { + input = simde_mm_setr_ps( + blocks[0][i], blocks[1][i], blocks[2][i], 0.0f); + input = simde_mm_mul_ps(input, input); + *mem = simde_mm_add_ps(input, simde_mm_mul_ps(pole, simde_mm_sub_ps(*mem, input))); + } + } + + void process4(simde__m128* mem, const float** blocks, size_t numFrames) + { + simde__m128 input; + const simde__m128 pole = simde_mm_load1_ps(&pole_); + for (size_t i = 0; i < numFrames; ++i) { + input = simde_mm_setr_ps( + blocks[0][i], blocks[1][i], blocks[2][i], blocks[3][i]); + input = simde_mm_mul_ps(input, input); + *mem = simde_mm_add_ps(input, simde_mm_mul_ps(pole, simde_mm_sub_ps(*mem, input))); + } + } + private: - simde__m128 mem_ {}; + float* mem_ { nullptr }; + simde__m128 temp_; float pole_ {}; float t60_ = 300e-3; float sampleRate_ = 44100; + int numOutputs_ = 2; + int memSize_ = 4; }; diff --git a/plugins/editor/layout/main.fl b/plugins/editor/layout/main.fl index e862d432..63efffff 100644 --- a/plugins/editor/layout/main.fl +++ b/plugins/editor/layout/main.fl @@ -1,5 +1,5 @@ # data file for the Fltk User Interface Designer (fluid) -version 1.0306 +version 1.0304 header_name {.h} code_name {.cxx} widget_class mainView {open @@ -12,10 +12,10 @@ widget_class mainView {open } Fl_Group {} { comment {palette=invertedPalette} open - xywh {0 0 800 110} + xywh {0 0 814 110} class LogicalGroup } { - Fl_Group {} { + Fl_Group {} {open xywh {5 4 175 101} box ROUNDED_BOX align 0 class RoundedGroup } { @@ -45,18 +45,18 @@ widget_class mainView {open class HomeButton } } - Fl_Group {} { - xywh {185 5 418 100} box ROUNDED_BOX + Fl_Group {} {open + xywh {185 5 390 100} box ROUNDED_BOX class RoundedGroup } { Fl_Box {} { label {Separator 1} - xywh {195 41 395 5} box BORDER_BOX labeltype NO_LABEL + xywh {195 41 370 5} box BORDER_BOX labeltype NO_LABEL class HLine } Fl_Box {} { label {Separator 2} - xywh {195 73 395 5} box BORDER_BOX labeltype NO_LABEL + xywh {195 73 370 5} box BORDER_BOX labeltype NO_LABEL class HLine } Fl_Box sfzFileLabel_ { @@ -66,7 +66,7 @@ widget_class mainView {open class ClickableLabel } Fl_Box keyswitchLabel_ { - xywh {265 45 325 35} labelsize 20 align 20 + xywh {265 45 310 35} labelsize 20 align 20 class Label } Fl_Box keyswitchBadge_ { @@ -85,17 +85,17 @@ widget_class mainView {open } Fl_Button {} { comment {tag=kTagPreviousSfzFile} - xywh {515 18 25 25} labelsize 24 + xywh {488 18 25 25} labelsize 24 class PreviousFileButton } Fl_Button {} { comment {tag=kTagNextSfzFile} - xywh {540 18 25 25} labelsize 24 + xywh {513 18 25 25} labelsize 24 class NextFileButton } Fl_Button fileOperationsMenu_ { comment {tag=kTagFileOperations} - xywh {565 18 25 25} labelsize 24 + xywh {538 18 25 25} labelsize 24 class ChevronDropDown } Fl_Box infoVoicesLabel_ { @@ -104,7 +104,7 @@ widget_class mainView {open } Fl_Box {} { label {Max:} - xywh {332 78 40 25} labelsize 12 align 24 + xywh {322 78 40 25} labelsize 12 align 24 class Label } Fl_Box numVoicesLabel_ { @@ -113,11 +113,11 @@ widget_class mainView {open } Fl_Box {} { label {Memory:} - xywh {459 78 60 25} labelsize 12 align 24 + xywh {456 78 60 25} labelsize 12 align 24 class Label } Fl_Box memoryLabel_ { - xywh {525 78 60 25} labelsize 12 align 16 + xywh {522 78 50 25} labelsize 12 align 16 class Label } Fl_Button numVoicesSlider_ { @@ -126,42 +126,42 @@ widget_class mainView {open class ChevronValueDropDown } } - Fl_Group {} { - xywh {608 5 187 100} box ROUNDED_BOX + Fl_Group {} {open + xywh {580 5 215 100} box ROUNDED_BOX class RoundedGroup } { Fl_Dial {} { - xywh {615 20 48 48} value 0.5 hide + xywh {587 20 48 48} value 0.5 hide class Knob48 } Fl_Box {} { label Center - xywh {610 70 60 5} labelsize 12 hide + xywh {582 70 60 5} labelsize 12 hide class ValueLabel } Fl_Box volumeCCKnob_ { label Volume comment {tag=kTagSetCCVolume} - xywh {614 10 70 90} box BORDER_BOX labelsize 12 align 17 + xywh {586 10 70 90} box BORDER_BOX labelsize 12 align 17 class KnobCCBox } Fl_Box panCCKnob_ { label Pan comment {tag=kTagSetCCPan} - xywh {691 10 70 90} box BORDER_BOX labelsize 12 align 17 + xywh {663 10 70 90} box BORDER_BOX labelsize 12 align 17 class KnobCCBox } - Fl_Box leftMeter_ { - xywh {767 10 9 90} box BORDER_BOX + Fl_Box {meters_[0]} { + xywh {739 10 23 90} box BORDER_BOX class VMeter } - Fl_Box rightMeter_ { - xywh {779 10 9 90} box BORDER_BOX + Fl_Box {meters_[1]} {selected + xywh {763 10 23 90} box BORDER_BOX class VMeter } } } - Fl_Group {subPanels_[kPanelInfo]} {open + Fl_Group {subPanels_[kPanelInfo]} { xywh {5 110 790 285} hide class LogicalGroup } { @@ -386,7 +386,7 @@ widget_class mainView {open xywh {5 400 790 70} labelsize 12 class Piano } - Fl_Group {subPanels_[kPanelGeneral]} {open selected + Fl_Group {subPanels_[kPanelGeneral]} { xywh {5 110 790 285} hide class LogicalGroup } {} diff --git a/plugins/editor/src/editor/EditIds.h b/plugins/editor/src/editor/EditIds.h index c0070ed6..e2ac0d89 100644 --- a/plugins/editor/src/editor/EditIds.h +++ b/plugins/editor/src/editor/EditIds.h @@ -26,6 +26,7 @@ enum class EditId : int { // #define KEY_RANGE(Name) Name##0, Name##Last = Name##0 + 128 - 1 #define CC_RANGE(Name) Name##0, Name##Last = Name##0 + sfz::config::numCCs - 1 + #define METER_RANGE(Name) Name##0, Name##Last = Name##0 + 16 - 1 // KEY_RANGE(Key), CC_RANGE(Controller), @@ -38,8 +39,7 @@ enum class EditId : int { CC_RANGE(ControllerDefault), CC_RANGE(ControllerLabel), // - LeftLevel, - RightLevel, + METER_RANGE(Level), // UINumCurves, UINumMasters, @@ -53,9 +53,11 @@ enum class EditId : int { // PluginFormat, PluginHost, + PluginOutputs, // #undef KEY_RANGE #undef CC_RANGE + #undef METER_RANGE }; struct EditRange { @@ -89,6 +91,7 @@ struct EditRange { // defines editIdForCC, ccForEditId, editIdIsCC, etc.. DEFINE_EDIT_ID_RANGE_HELPERS(cc, CC, Controller) +DEFINE_EDIT_ID_RANGE_HELPERS(level, Level, Level) DEFINE_EDIT_ID_RANGE_HELPERS(key, Key, Key) DEFINE_EDIT_ID_RANGE_HELPERS(keyUsed, KeyUsed, KeyUsed) DEFINE_EDIT_ID_RANGE_HELPERS(keyLabel, KeyLabel, KeyLabel) diff --git a/plugins/editor/src/editor/Editor.cpp b/plugins/editor/src/editor/Editor.cpp index bc7b4d71..a9e02003 100644 --- a/plugins/editor/src/editor/Editor.cpp +++ b/plugins/editor/src/editor/Editor.cpp @@ -58,6 +58,7 @@ struct Editor::Impl : EditorController::Receiver, std::string currentThemeName_; std::string userFilesDir_; std::string fallbackFilesDir_; + bool multi_ { false }; int currentKeyswitch_ = -1; std::unordered_map keyswitchNames_; @@ -154,9 +155,7 @@ struct Editor::Impl : EditorController::Receiver, SKnobCCBox* volumeCCKnob_ = nullptr; SKnobCCBox* panCCKnob_ = nullptr; - SLevelMeter* leftMeter_ = nullptr; - SLevelMeter* rightMeter_ = nullptr; - + SLevelMeter* meters_[16] {}; SAboutDialog* aboutDialog_ = nullptr; SharedPointer backgroundBitmap_; @@ -183,6 +182,7 @@ struct Editor::Impl : EditorController::Receiver, SharedPointer oscSendQueueTimer_; void createFrameContents(); + SLevelMeter* createVMeter(const CRect& bounds, int, const char*, CHoriTxtAlign, int); template void adjustMinMaxToEditRange(Control* c, EditId id) @@ -333,6 +333,16 @@ void Editor::close() } } +SLevelMeter* Editor::Impl::createVMeter(const CRect& bounds, int, const char*, CHoriTxtAlign, int) { + SLevelMeter* meter = new SLevelMeter(bounds); + Palette* palette = &theme_->invertedPalette; + meter->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); + meter->setNormalFillColor(CColor(0x00, 0xaa, 0x11)); + meter->setDangerFillColor(CColor(0xaa, 0x00, 0x00)); + meter->setBackColor(palette->knobInactiveTrack); + return meter; +}; + void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) { switch (id) { @@ -508,20 +518,51 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) updateBackgroundImage(value.c_str()); } break; - case EditId::LeftLevel: + case EditId::PluginOutputs: { - const float value = v.to_float(); - if (SLevelMeter* meter = leftMeter_) - meter->setValue(value); + const int value = static_cast(v.to_float()); + if (!meters_[0]) + return; + + const auto firstMeterRect = meters_[0]->getViewSize(); + const auto minLeft = firstMeterRect.left; + auto maxRight = firstMeterRect.right; + auto numMeters = 1; + for (; numMeters < 16; ++numMeters) { + if (!meters_[numMeters]) + break; + + maxRight = meters_[numMeters]->getViewSize().right; + } + const auto meterWidth = std::max(1.0, (maxRight - minLeft - value + 1) / value); + + auto meterParent = meters_[0]->getParentView()->asViewContainer(); + const auto roundRectRadius = [value] { + if (value < 4) + return 5.0; + else if (value < 8) + return 3.0; + else if (value < 16) + return 1.0; + else + return 0.0; + }(); + + for (int i = 0; i < 16; ++i) { + if (meters_[i]) { + meterParent->removeView(meters_[i]); + } + + if (i < value) { + double left { minLeft + i * (meterWidth + 1) }; + CRect rect { left, firstMeterRect.top, left + meterWidth, firstMeterRect.bottom }; + meters_[i] = createVMeter(rect, -1, "", kCenterText, 14); + meters_[i]->setRoundRectRadius(roundRectRadius); + meterParent->addView(meters_[i]); + } + } + break; } - break; - case EditId::RightLevel: - { - const float value = v.to_float(); - if (SLevelMeter* meter = rightMeter_) - meter->setValue(value); - } - break; default: if (editIdIsKey(id)) { const int key = keyForEditId(id); @@ -554,6 +595,11 @@ void Editor::Impl::uiReceiveValue(EditId id, const EditValue& v) else if (editIdIsCCLabel(id)) { updateCCLabel(ccLabelForEditId(id), v.to_string().c_str()); } + else if (editIdIsLevel(id)) { + const float value = v.to_float(); + if (SLevelMeter* meter = meters_[levelForEditId(id)]) + meter->setValue(value); + } break; } } @@ -721,16 +767,6 @@ void Editor::Impl::createFrameContents() lbl->setFont(font); return lbl; }; - auto createVMeter = [this, &palette](const CRect& bounds, int, const char*, CHoriTxtAlign, int) { - SLevelMeter* meter = new SLevelMeter(bounds); - meter->setFrameColor(CColor(0x00, 0x00, 0x00, 0x00)); - meter->setNormalFillColor(CColor(0x00, 0xaa, 0x11)); - meter->setDangerFillColor(CColor(0xaa, 0x00, 0x00)); - OnThemeChanged.push_back([meter, palette]() { - meter->setBackColor(palette->knobInactiveTrack); - }); - return meter; - }; #if 0 auto createButton = [this](const CRect& bounds, int tag, const char* label, CHoriTxtAlign align, int fontsize) { CTextButton* button = new CTextButton(bounds, this, tag, label); @@ -952,6 +988,15 @@ void Editor::Impl::createFrameContents() mainView->setBackgroundColor(theme->frameBackground); }); + OnThemeChanged.push_back([this, theme]() { + for (unsigned i = 0, n = sizeof(meters_)/sizeof(meters_[0]); i < n; ++i ) { + Palette& palette = theme->invertedPalette; + const auto meter = meters_[i]; + if (meter) + meter->setBackColor(palette.knobInactiveTrack); + } + }); + #if LINUX if (!isZenityAvailable()) { CRect bounds = mainView->getViewSize(); diff --git a/plugins/editor/src/editor/layout/main.hpp b/plugins/editor/src/editor/layout/main.hpp index a14cb951..f640d447 100644 --- a/plugins/editor/src/editor/layout/main.hpp +++ b/plugins/editor/src/editor/layout/main.hpp @@ -5,7 +5,7 @@ auto* const view__1 = createBackground(CRect(5, 110, 795, 395), -1, "", kCenterT imageContainer_ = view__1; view__0->addView(view__1); enterPalette(invertedPalette); -auto* const view__2 = createLogicalGroup(CRect(0, 0, 800, 110), -1, "", kCenterText, 14); +auto* const view__2 = createLogicalGroup(CRect(0, 0, 814, 110), -1, "", kCenterText, 14); view__0->addView(view__2); auto* const view__3 = createRoundedGroup(CRect(5, 4, 180, 105), -1, "", kCenterText, 14); view__2->addView(view__3); @@ -23,16 +23,16 @@ view__3->addView(view__7); auto* const view__8 = createHomeButton(CRect(11, 69, 43, 101), kTagFirstChangePanel+kPanelGeneral, "", kCenterText, 30); panelButtons_[kPanelGeneral] = view__8; view__3->addView(view__8); -auto* const view__9 = createRoundedGroup(CRect(185, 5, 603, 105), -1, "", kCenterText, 14); +auto* const view__9 = createRoundedGroup(CRect(185, 5, 575, 105), -1, "", kCenterText, 14); view__2->addView(view__9); -auto* const view__10 = createHLine(CRect(10, 36, 405, 41), -1, "", kCenterText, 14); +auto* const view__10 = createHLine(CRect(10, 36, 380, 41), -1, "", kCenterText, 14); view__9->addView(view__10); -auto* const view__11 = createHLine(CRect(10, 68, 405, 73), -1, "", kCenterText, 14); +auto* const view__11 = createHLine(CRect(10, 68, 380, 73), -1, "", kCenterText, 14); view__9->addView(view__11); auto* const view__12 = createClickableLabel(CRect(10, 8, 330, 38), kTagLoadSfzFile, "DefaultInstrument.sfz", kLeftText, 20); sfzFileLabel_ = view__12; view__9->addView(view__12); -auto* const view__13 = createLabel(CRect(80, 40, 405, 75), -1, "", kLeftText, 20); +auto* const view__13 = createLabel(CRect(80, 40, 390, 75), -1, "", kLeftText, 20); keyswitchLabel_ = view__13; view__9->addView(view__13); auto* const view__14 = createBadge(CRect(10, 42, 70, 68), -1, "", kCenterText, 20); @@ -44,30 +44,30 @@ view__9->addView(view__15); view__15->setVisible(false); auto* const view__16 = createLabel(CRect(10, 73, 70, 98), -1, "Voices:", kRightText, 12); view__9->addView(view__16); -auto* const view__17 = createPreviousFileButton(CRect(330, 13, 355, 38), kTagPreviousSfzFile, "", kCenterText, 24); +auto* const view__17 = createPreviousFileButton(CRect(303, 13, 328, 38), kTagPreviousSfzFile, "", kCenterText, 24); view__9->addView(view__17); -auto* const view__18 = createNextFileButton(CRect(355, 13, 380, 38), kTagNextSfzFile, "", kCenterText, 24); +auto* const view__18 = createNextFileButton(CRect(328, 13, 353, 38), kTagNextSfzFile, "", kCenterText, 24); view__9->addView(view__18); -auto* const view__19 = createChevronDropDown(CRect(380, 13, 405, 38), kTagFileOperations, "", kCenterText, 24); +auto* const view__19 = createChevronDropDown(CRect(353, 13, 378, 38), kTagFileOperations, "", kCenterText, 24); fileOperationsMenu_ = view__19; view__9->addView(view__19); auto* const view__20 = createLabel(CRect(75, 73, 115, 98), -1, "", kCenterText, 12); infoVoicesLabel_ = view__20; view__9->addView(view__20); -auto* const view__21 = createLabel(CRect(147, 73, 187, 98), -1, "Max:", kRightText, 12); +auto* const view__21 = createLabel(CRect(137, 73, 177, 98), -1, "Max:", kRightText, 12); view__9->addView(view__21); auto* const view__22 = createLabel(CRect(193, 73, 228, 98), -1, "", kCenterText, 12); numVoicesLabel_ = view__22; view__9->addView(view__22); -auto* const view__23 = createLabel(CRect(274, 73, 334, 98), -1, "Memory:", kRightText, 12); +auto* const view__23 = createLabel(CRect(271, 73, 331, 98), -1, "Memory:", kRightText, 12); view__9->addView(view__23); -auto* const view__24 = createLabel(CRect(340, 73, 400, 98), -1, "", kCenterText, 12); +auto* const view__24 = createLabel(CRect(337, 73, 387, 98), -1, "", kCenterText, 12); memoryLabel_ = view__24; view__9->addView(view__24); auto* const view__25 = createChevronValueDropDown(CRect(235, 77, 255, 97), kTagSetNumVoices, "", kCenterText, 16); numVoicesSlider_ = view__25; view__9->addView(view__25); -auto* const view__26 = createRoundedGroup(CRect(608, 5, 795, 105), -1, "", kCenterText, 14); +auto* const view__26 = createRoundedGroup(CRect(580, 5, 795, 105), -1, "", kCenterText, 14); view__2->addView(view__26); auto* const view__27 = createKnob48(CRect(7, 15, 55, 63), -1, "", kCenterText, 14); view__26->addView(view__27); @@ -81,11 +81,11 @@ view__26->addView(view__29); auto* const view__30 = createKnobCCBox(CRect(83, 5, 153, 95), kTagSetCCPan, "Pan", kCenterText, 12); panCCKnob_ = view__30; view__26->addView(view__30); -auto* const view__31 = createVMeter(CRect(159, 5, 168, 95), -1, "", kCenterText, 14); -leftMeter_ = view__31; +auto* const view__31 = createVMeter(CRect(159, 5, 182, 95), -1, "", kCenterText, 14); +meters_[0] = view__31; view__26->addView(view__31); -auto* const view__32 = createVMeter(CRect(171, 5, 180, 95), -1, "", kCenterText, 14); -rightMeter_ = view__32; +auto* const view__32 = createVMeter(CRect(183, 5, 206, 95), -1, "", kCenterText, 14); +meters_[1] = view__32; view__26->addView(view__32); enterPalette(defaultPalette); auto* const view__33 = createLogicalGroup(CRect(5, 110, 795, 395), -1, "", kCenterText, 14); diff --git a/plugins/lv2/manifest.ttl.in b/plugins/lv2/manifest.ttl.in index 3c5a130c..37305afd 100644 --- a/plugins/lv2/manifest.ttl.in +++ b/plugins/lv2/manifest.ttl.in @@ -7,6 +7,11 @@ lv2:binary ; rdfs:seeAlso <@PROJECT_NAME@.ttl>, . +<@LV2PLUGIN_URI@-multi> + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso <@PROJECT_NAME@.ttl>, . + @LV2PLUGIN_IF_ENABLE_UI@<@LV2PLUGIN_URI@#ui> @LV2PLUGIN_IF_ENABLE_UI@ a ui:@LV2_UI_TYPE@ ; @LV2PLUGIN_IF_ENABLE_UI@ ui:binary ; diff --git a/plugins/lv2/sfizz.cpp b/plugins/lv2/sfizz.cpp index 407ed804..8b8a60d8 100644 --- a/plugins/lv2/sfizz.cpp +++ b/plugins/lv2/sfizz.cpp @@ -192,7 +192,7 @@ sfizz_atom_extract_integer(sfizz_plugin_t *self, const LV2_Atom *atom, int64_t * } static void -connect_port(LV2_Handle instance, +connect_port_stereo(LV2_Handle instance, uint32_t port, void *data) { @@ -264,6 +264,133 @@ connect_port(LV2_Handle instance, } } +static void +connect_port_multi(LV2_Handle instance, + uint32_t port, + void *data) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + switch (port) + { + case SFIZZ_MULTI_CONTROL: + self->control_port = (const LV2_Atom_Sequence *)data; + break; + case SFIZZ_MULTI_AUTOMATE: + self->automate_port = (LV2_Atom_Sequence *)data; + break; + case SFIZZ_MULTI_OUT1L: + self->output_buffers[0] = (float *)data; + break; + case SFIZZ_MULTI_OUT1R: + self->output_buffers[1] = (float *)data; + break; + case SFIZZ_MULTI_OUT2L: + self->output_buffers[2] = (float *)data; + break; + case SFIZZ_MULTI_OUT2R: + self->output_buffers[3] = (float *)data; + break; + case SFIZZ_MULTI_OUT3L: + self->output_buffers[4] = (float *)data; + break; + case SFIZZ_MULTI_OUT3R: + self->output_buffers[5] = (float *)data; + break; + case SFIZZ_MULTI_OUT4L: + self->output_buffers[6] = (float *)data; + break; + case SFIZZ_MULTI_OUT4R: + self->output_buffers[7] = (float *)data; + break; + case SFIZZ_MULTI_OUT5L: + self->output_buffers[8] = (float *)data; + break; + case SFIZZ_MULTI_OUT5R: + self->output_buffers[9] = (float *)data; + break; + case SFIZZ_MULTI_OUT6L: + self->output_buffers[10] = (float *)data; + break; + case SFIZZ_MULTI_OUT6R: + self->output_buffers[11] = (float *)data; + break; + case SFIZZ_MULTI_OUT7L: + self->output_buffers[12] = (float *)data; + break; + case SFIZZ_MULTI_OUT7R: + self->output_buffers[13] = (float *)data; + break; + case SFIZZ_MULTI_OUT8L: + self->output_buffers[14] = (float *)data; + break; + case SFIZZ_MULTI_OUT8R: + self->output_buffers[15] = (float *)data; + break; + case SFIZZ_MULTI_VOLUME: + self->volume_port = (const float *)data; + break; + case SFIZZ_MULTI_POLYPHONY: + self->polyphony_port = (const float *)data; + break; + case SFIZZ_MULTI_OVERSAMPLING: + self->oversampling_port = (const float *)data; + break; + case SFIZZ_MULTI_PRELOAD: + self->preload_port = (const float *)data; + break; + case SFIZZ_MULTI_FREEWHEELING: + self->freewheel_port = (const float *)data; + break; + case SFIZZ_MULTI_SCALA_ROOT_KEY: + self->scala_root_key_port = (const float *)data; + break; + case SFIZZ_MULTI_TUNING_FREQUENCY: + self->tuning_frequency_port = (const float *)data; + break; + case SFIZZ_MULTI_STRETCH_TUNING: + self->stretch_tuning_port = (const float *)data; + break; + case SFIZZ_MULTI_SAMPLE_QUALITY: + self->sample_quality_port = (const float *)data; + break; + case SFIZZ_MULTI_OSCILLATOR_QUALITY: + self->oscillator_quality_port = (const float *)data; + break; + case SFIZZ_MULTI_ACTIVE_VOICES: + self->active_voices_port = (float *)data; + break; + case SFIZZ_MULTI_NUM_CURVES: + self->num_curves_port = (float *)data; + break; + case SFIZZ_MULTI_NUM_MASTERS: + self->num_masters_port = (float *)data; + break; + case SFIZZ_MULTI_NUM_GROUPS: + self->num_groups_port = (float *)data; + break; + case SFIZZ_MULTI_NUM_REGIONS: + self->num_regions_port = (float *)data; + break; + case SFIZZ_MULTI_NUM_SAMPLES: + self->num_samples_port = (float *)data; + break; + default: + break; + } +} + +static void +connect_port(LV2_Handle instance, + uint32_t port, + void *data) +{ + sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + if (self->multi_out) + connect_port_multi(instance, port, data); + else + connect_port_stereo(instance, port, data); +} + // This function sets the sample rate in the self parameter but does not update it. static void sfizz_lv2_parse_sample_rate(sfizz_plugin_t* self, const LV2_Options_Option* opt) @@ -370,6 +497,7 @@ instantiate(const LV2_Descriptor *descriptor, self->bundle_path[MAX_BUNDLE_PATH_SIZE - 1] = '\0'; // Set defaults + self->multi_out = !strcmp(descriptor->URI, SFIZZ_MULTI_URI); self->max_block_size = MAX_BLOCK_SIZE; self->sample_rate = (float)rate; self->expect_nominal_block_length = false; @@ -513,6 +641,13 @@ instantiate(const LV2_Descriptor *descriptor, sfizz_lv2_update_timeinfo(self, 0, ~0); +#if defined(SFIZZ_LV2_UI) + if (self->multi_out) + self->rms_follower.setNumOutputs(MULTI_OUTPUT_COUNT); + else + self->rms_follower.setNumOutputs(2); +#endif + return (LV2_Handle)self; } @@ -589,11 +724,8 @@ sfizz_lv2_send_controller(sfizz_plugin_t *self, LV2_URID verb_uri, unsigned cc, #if defined(SFIZZ_LV2_UI) static void -sfizz_lv2_send_levels(sfizz_plugin_t *self, float left, float right) +sfizz_lv2_send_levels(sfizz_plugin_t *self, const float* levels, uint32_t num_levels) { - const float levels[] = {left, right}; - uint32_t num_levels = sizeof(levels) / sizeof(levels[0]); - LV2_Atom_Forge* forge = &self->forge_automate; bool write_ok = lv2_atom_forge_frame_time(forge, 0); @@ -1083,7 +1215,10 @@ run(LV2_Handle instance, uint32_t sample_count) } // Render the block - sfizz_render_block(self->synth, self->output_buffers, 2, (int)sample_count); + if (self->multi_out) + sfizz_render_block(self->synth, self->output_buffers, MULTI_OUTPUT_COUNT, (int)sample_count); + else + sfizz_render_block(self->synth, self->output_buffers, 2, (int)sample_count); // Request OSC updates sfizz_send_message(self->synth, self->client, 0, "/sw/last/current", "", nullptr); @@ -1110,10 +1245,19 @@ run(LV2_Handle instance, uint32_t sample_count) #if defined(SFIZZ_LV2_UI) if (self->ui_active) { - self->rms_follower.process(self->output_buffers[0], self->output_buffers[1], sample_count); - const simde__m128 rms = self->rms_follower.getRMS(); - const float *levels = (const float *)&rms; - sfizz_lv2_send_levels(self, levels[0], levels[1]); + if (self->multi_out) { + self->rms_follower.process((const float**)self->output_buffers, + sample_count, MULTI_OUTPUT_COUNT); + float levels[MULTI_OUTPUT_COUNT]; + self->rms_follower.getRMS(levels, MULTI_OUTPUT_COUNT); + sfizz_lv2_send_levels(self, levels, MULTI_OUTPUT_COUNT); + } else { + self->rms_follower.process((const float**)self->output_buffers, + sample_count, 2); + float levels[2]; + self->rms_follower.getRMS(levels, 2); + sfizz_lv2_send_levels(self, levels, 2); + } } else { @@ -1769,6 +1913,16 @@ static const LV2_Descriptor descriptor = { cleanup, extension_data}; +static const LV2_Descriptor descriptor_multi = { + SFIZZ_MULTI_URI, + instantiate, + connect_port, + activate, + run, + deactivate, + cleanup, + extension_data}; + LV2_SYMBOL_EXPORT const LV2_Descriptor * lv2_descriptor(uint32_t index) @@ -1777,6 +1931,8 @@ lv2_descriptor(uint32_t index) { case 0: return &descriptor; + case 1: + return &descriptor_multi; default: return NULL; } diff --git a/plugins/lv2/sfizz.ttl.in b/plugins/lv2/sfizz.ttl.in index fd860990..e8eae3a8 100644 --- a/plugins/lv2/sfizz.ttl.in +++ b/plugins/lv2/sfizz.ttl.in @@ -409,3 +409,442 @@ midnam:update a lv2:Feature . ] ; rdfs:seeAlso . + +<@LV2PLUGIN_URI@-multi> + a doap:Project, lv2:Plugin, lv2:InstrumentPlugin ; + + doap:name "@LV2PLUGIN_NAME@" ; + doap:license ; + doap:maintainer [ + foaf:name "@LV2PLUGIN_AUTHOR@" ; + foaf:homepage <@LV2PLUGIN_URI@> ; + foaf:mbox ; + ] ; + rdfs:comment "@LV2PLUGIN_COMMENT@", + "Échantillonneur SFZ"@fr , + "Campionatore SFZ"@it ; + + lv2:minorVersion @LV2PLUGIN_VERSION_MINOR@ ; + lv2:microVersion @LV2PLUGIN_VERSION_MICRO@ ; + + lv2:requiredFeature urid:map, bufsize:boundedBlockLength, work:schedule ; + lv2:optionalFeature lv2:hardRTCapable, opts:options, state:mapPath, state:freePath ; + lv2:extensionData opts:interface, state:interface, work:interface ; + + lv2:optionalFeature midnam:update ; + lv2:extensionData midnam:interface ; + + opts:supportedOption param:sampleRate ; + opts:supportedOption bufsize:maxBlockLength, bufsize:nominalBlockLength ; + + @LV2PLUGIN_IF_ENABLE_UI@ui:ui <@LV2PLUGIN_URI@#ui> ; + + patch:writable <@LV2PLUGIN_URI@:sfzfile> , + <@LV2PLUGIN_URI@:tuningfile> ; + + pg:mainOutput <@LV2PLUGIN_URI@#stereo_output> ; + + lv2:port [ + a lv2:InputPort, atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message, midi:MidiEvent, time:Position, <@LV2PLUGIN_URI@:OSCBlob>, <@LV2PLUGIN_URI@:Notify> ; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:symbol "control" ; + lv2:name "Control", + "Contrôle"@fr ; + rsz:minimumSize 524288 ; + ] , [ + a lv2:OutputPort, atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message, <@LV2PLUGIN_URI@:OSCBlob> ; + lv2:designation lv2:control ; + lv2:index 1 ; + lv2:symbol "automate" ; + lv2:name "Automate", + "Automatisation"@fr ; + rsz:minimumSize 524288 ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 2 ; + lv2:symbol "out1L" ; + lv2:name "Output 1 Left", + "Sortie gauche 1"@fr , + "Uscita Sinistra 1"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 3 ; + lv2:symbol "out1R" ; + lv2:name "Output 1 Right", + "Sortie droite 1"@fr , + "Uscita Destra 1"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 4 ; + lv2:symbol "out2L" ; + lv2:name "Output 2 Left", + "Sortie gauche 2"@fr , + "Uscita Sinistra 2"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 5 ; + lv2:symbol "out2R" ; + lv2:name "Output 2 Right", + "Sortie droite 2"@fr , + "Uscita Destra 2"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 6 ; + lv2:symbol "out3L" ; + lv2:name "Output 3 Left", + "Sortie gauche 3"@fr , + "Uscita Sinistra 3"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 7 ; + lv2:symbol "out3R" ; + lv2:name "Output 3 Right", + "Sortie droite 3"@fr , + "Uscita Destra 3"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 8 ; + lv2:symbol "out4L" ; + lv2:name "Output 4 Left", + "Sortie gauche 4"@fr , + "Uscita Sinistra 4"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 9 ; + lv2:symbol "out4R" ; + lv2:name "Output 4 Right", + "Sortie droite 4"@fr , + "Uscita Destra 4"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 10 ; + lv2:symbol "out5L" ; + lv2:name "Output 5 Left", + "Sortie gauche 5"@fr , + "Uscita Sinistra 5"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 11 ; + lv2:symbol "out5R" ; + lv2:name "Output 5 Right", + "Sortie droite 5"@fr , + "Uscita Destra 5"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 12 ; + lv2:symbol "out6L" ; + lv2:name "Output 6 Left", + "Sortie gauche 6"@fr , + "Uscita Sinistra 6"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 13 ; + lv2:symbol "out6R" ; + lv2:name "Output 6 Right", + "Sortie droite 6"@fr , + "Uscita Destra 6"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 14 ; + lv2:symbol "out7L" ; + lv2:name "Output 7 Left", + "Sortie gauche 7"@fr , + "Uscita Sinistra 7"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 15 ; + lv2:symbol "out7R" ; + lv2:name "Output 7 Right", + "Sortie droite 7"@fr , + "Uscita Destra 7"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 16 ; + lv2:symbol "out8L" ; + lv2:name "Output 8 Left", + "Sortie gauche 8"@fr , + "Uscita Sinistra 8"@it ; + ] , [ + a lv2:AudioPort, lv2:OutputPort ; + lv2:index 17 ; + lv2:symbol "out8R" ; + lv2:name "Output 8 Right", + "Sortie droite 8"@fr , + "Uscita Destra 8"@it ; + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 18 ; + lv2:symbol "volume" ; + lv2:name "Volume" ; + lv2:default 0.0 ; + lv2:minimum -80.0 ; + lv2:maximum 6.0 ; + units:unit units:db + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 19 ; + lv2:symbol "num_voices" ; + lv2:name "Polyphony", + "Polyphonie"@fr , + "Polifonia"@it ; + pg:group <@LV2PLUGIN_URI@#config> ; + lv2:portProperty pprop:notAutomatic ; + lv2:portProperty pprop:expensive ; + lv2:portProperty lv2:integer ; + lv2:portProperty lv2:enumeration ; + lv2:default 64 ; + lv2:minimum 8 ; + lv2:maximum 256 ; + lv2:scalePoint [ rdfs:label "8 voices", + "8 voix"@fr , + "8 Voci"@it; + rdf:value 8 + ] ; + lv2:scalePoint [ rdfs:label "16 voices", + "16 voix"@fr , + "16 Voci"@it; + rdf:value 16 + ] ; + lv2:scalePoint [ rdfs:label "32 voices", + "32 voix"@fr , + "32 Voci"@it; + rdf:value 32 + ] ; + lv2:scalePoint [ rdfs:label "64 voices", + "64 voix"@fr , + "64 Voci"@it; + rdf:value 64 + ] ; + lv2:scalePoint [ rdfs:label "128 voices", + "128 voix"@fr , + "128 Voci"@it; + rdf:value 128 + ] ; + lv2:scalePoint [ rdfs:label "256 voices", + "256 voix"@fr , + "256 Voci"@it; + rdf:value 256 + ] ; + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 20 ; + lv2:symbol "oversampling" ; + lv2:name "Oversampling factor", + "Facteur de suréchantillonnage"@fr , + "Fattore Sovracampionamento"@it ; + pg:group <@LV2PLUGIN_URI@#config> ; + lv2:portProperty pprop:notAutomatic ; + lv2:portProperty pprop:expensive ; + lv2:portProperty lv2:integer ; + lv2:portProperty lv2:enumeration ; + lv2:default 1 ; + lv2:minimum 1 ; + lv2:maximum 8 ; + lv2:scalePoint [ rdfs:label "x1 oversampling", + "suréchantillonnage x1"@fr , + "x1 Sovracampionamento"@it; + rdf:value 1 + ] ; + lv2:scalePoint [ rdfs:label "x2 oversampling", + "suréchantillonnage x2"@fr , + "x2 Sovracampionamento"@it; + rdf:value 2 + ] ; + lv2:scalePoint [ rdfs:label "x4 oversampling", + "suréchantillonnage x4"@fr , + "x4 Sovracampionamento"@it; + rdf:value 4 + ] ; + lv2:scalePoint [ rdfs:label "x8 oversampling", + "suréchantillonnage x8"@fr , + "x8 Sovracampionamento"@it; + rdf:value 8 + ] ; + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 21 ; + lv2:symbol "preload_size" ; + lv2:name "Preload size", + "Taille préchargée"@fr , + "Grandezza Precaricamento"@it ; + pg:group <@LV2PLUGIN_URI@#config> ; + lv2:portProperty pprop:notAutomatic ; + lv2:portProperty pprop:expensive ; + lv2:portProperty lv2:integer ; + lv2:portProperty lv2:enumeration ; + lv2:default 8192 ; + lv2:minimum 1024 ; + lv2:maximum 65536 ; + lv2:scalePoint [ rdfs:label "4 KB", + "4 Ko"@fr; + rdf:value 1024 + ] ; + lv2:scalePoint [ rdfs:label "8 KB", + "8 Ko"@fr; + rdf:value 2048 + ] ; + lv2:scalePoint [ rdfs:label "16 KB", + "16 Ko"@fr; + rdf:value 4096 + ] ; + lv2:scalePoint [ rdfs:label "32 KB", + "32 Ko"@fr; + rdf:value 8192 + ] ; + lv2:scalePoint [ rdfs:label "64 KB", + "64 Ko"@fr; + rdf:value 16384 + ] ; + lv2:scalePoint [ rdfs:label "128 KB", + "128 Ko"@fr; + rdf:value 32768 + ] ; + lv2:scalePoint [ rdfs:label "256 KB", + "256 Ko"@fr; + rdf:value 65536 + ] ; + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 22 ; + lv2:symbol "freewheeling" ; + lv2:name "Freewheeling", + "En roue libre (freewheeling)"@fr , + "A Ruota Libera"@it ; + lv2:designation lv2:freeWheeling ; + lv2:portProperty lv2:toggled ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 1 ; + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 23 ; + lv2:symbol "scala_root_key" ; + lv2:name "Scala root key", + "Tonalité de base Scala"@fr , + "Tonalità di base Scala"@it ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:portProperty lv2:integer ; + lv2:default 60 ; + lv2:minimum 0 ; + lv2:maximum 127 ; + units:unit units:midiNote + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 24 ; + lv2:symbol "tuning_frequency" ; + lv2:name "Tuning frequency", + "Fréquence d'accordage"@fr , + "Frequenza di accordatura"@it ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:default 440.0 ; + lv2:minimum 300.0 ; + lv2:maximum 500.0 ; + units:unit units:hz + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 25 ; + lv2:symbol "stretched_tuning" ; + lv2:name "Stretched tuning", + "Accordage étiré"@fr , + "Tensione di accordatura"@it ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + units:unit units:coef + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 26 ; + lv2:symbol "sample_quality" ; + lv2:name "Sample quality", + "Qualité des échantillons"@fr , + "Qualità del campione"@it ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:default 2.0 ; + lv2:minimum 0.0 ; + lv2:maximum 10.0 + ] , [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 27 ; + lv2:symbol "oscillator_quality" ; + lv2:name "Oscillator quality", + "Qualité des oscillateurs"@fr , + "Qualità dell'oscillatore"@it ; + pg:group <@LV2PLUGIN_URI@#tuning> ; + lv2:default 1.0 ; + lv2:minimum 0.0 ; + lv2:maximum 3.0 + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 28 ; + lv2:symbol "active_voices" ; + lv2:name "Active voices", + "Voix utilisées"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 256 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 29 ; + lv2:symbol "num_curves" ; + lv2:name "Number of curves", + "Nombre de courbes"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 30 ; + lv2:symbol "num_masters" ; + lv2:name "Number of masters", + "Nombre de maîtres"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 31 ; + lv2:symbol "num_groups" ; + lv2:name "Number of groups", + "Nombre de groupes"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 32 ; + lv2:symbol "num_regions" ; + lv2:name "Number of regions", + "Nombre de régions"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 33 ; + lv2:symbol "num_samples" ; + lv2:name "Number of samples", + "Nombre d'échantillons"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] ; + + rdfs:seeAlso . diff --git a/plugins/lv2/sfizz_lv2.h b/plugins/lv2/sfizz_lv2.h index c9935ec0..c99f8bbb 100644 --- a/plugins/lv2/sfizz_lv2.h +++ b/plugins/lv2/sfizz_lv2.h @@ -30,8 +30,10 @@ #define MAX_PATH_SIZE 1024 #define ATOM_TEMP_SIZE 8192 #define OSC_TEMP_SIZE 8192 +#define MULTI_OUTPUT_COUNT 16 #define SFIZZ_URI "http://sfztools.github.io/sfizz" +#define SFIZZ_MULTI_URI SFIZZ_URI "-multi" #define SFIZZ_UI_URI "http://sfztools.github.io/sfizz#ui" #define SFIZZ_PREFIX SFIZZ_URI "#" #define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" @@ -73,6 +75,44 @@ enum SFIZZ_NUM_SAMPLES = 19, }; +enum +{ + SFIZZ_MULTI_CONTROL = 0, + SFIZZ_MULTI_AUTOMATE = 1, + SFIZZ_MULTI_OUT1L = 2, + SFIZZ_MULTI_OUT1R = 3, + SFIZZ_MULTI_OUT2L = 4, + SFIZZ_MULTI_OUT2R = 5, + SFIZZ_MULTI_OUT3L = 6, + SFIZZ_MULTI_OUT3R = 7, + SFIZZ_MULTI_OUT4L = 8, + SFIZZ_MULTI_OUT4R = 9, + SFIZZ_MULTI_OUT5L = 10, + SFIZZ_MULTI_OUT5R = 11, + SFIZZ_MULTI_OUT6L = 12, + SFIZZ_MULTI_OUT6R = 13, + SFIZZ_MULTI_OUT7L = 14, + SFIZZ_MULTI_OUT7R = 15, + SFIZZ_MULTI_OUT8L = 16, + SFIZZ_MULTI_OUT8R = 17, + SFIZZ_MULTI_VOLUME = 18, + SFIZZ_MULTI_POLYPHONY = 19, + SFIZZ_MULTI_OVERSAMPLING = 20, + SFIZZ_MULTI_PRELOAD = 21, + SFIZZ_MULTI_FREEWHEELING = 22, + SFIZZ_MULTI_SCALA_ROOT_KEY = 23, + SFIZZ_MULTI_TUNING_FREQUENCY = 24, + SFIZZ_MULTI_STRETCH_TUNING = 25, + SFIZZ_MULTI_SAMPLE_QUALITY = 26, + SFIZZ_MULTI_OSCILLATOR_QUALITY = 27, + SFIZZ_MULTI_ACTIVE_VOICES = 28, + SFIZZ_MULTI_NUM_CURVES = 29, + SFIZZ_MULTI_NUM_MASTERS = 30, + SFIZZ_MULTI_NUM_GROUPS = 31, + SFIZZ_MULTI_NUM_REGIONS = 32, + SFIZZ_MULTI_NUM_SAMPLES = 33, +}; + // For use with instance-access struct sfizz_plugin_t; @@ -92,6 +132,14 @@ bool sfizz_lv2_fetch_description( sfizz_plugin_t *self, const int *serial, uint8_t **descp, uint32_t *sizep, int *serialp); +/** + * @brief Returns the number of outputs of the plugin + * + * @param self + * @return int + */ +int sfizz_lv2_get_num_outputs(sfizz_plugin_t *self); + #if defined(SFIZZ_LV2_UI) void sfizz_lv2_set_ui_active(sfizz_plugin_t *self, bool ui_active); #endif diff --git a/plugins/lv2/sfizz_lv2_common.cpp b/plugins/lv2/sfizz_lv2_common.cpp index 30bf4b63..bd6a38ee 100644 --- a/plugins/lv2/sfizz_lv2_common.cpp +++ b/plugins/lv2/sfizz_lv2_common.cpp @@ -31,6 +31,14 @@ bool sfizz_lv2_fetch_description( return true; } +int sfizz_lv2_get_num_outputs(sfizz_plugin_t *self) +{ + if (self->multi_out) + return MULTI_OUTPUT_COUNT; + + return 2; +} + #if defined(SFIZZ_LV2_UI) void sfizz_lv2_set_ui_active(sfizz_plugin_t *self, bool ui_active) { diff --git a/plugins/lv2/sfizz_lv2_plugin.h b/plugins/lv2/sfizz_lv2_plugin.h index fad8342a..e7a41c19 100644 --- a/plugins/lv2/sfizz_lv2_plugin.h +++ b/plugins/lv2/sfizz_lv2_plugin.h @@ -33,9 +33,10 @@ struct sfizz_plugin_t LV2_Midnam *midnam {}; // Ports + bool multi_out { false }; const LV2_Atom_Sequence *control_port {}; LV2_Atom_Sequence *automate_port {}; - float *output_buffers[2] {}; + float *output_buffers[MULTI_OUTPUT_COUNT] {}; const float *volume_port {}; const float *polyphony_port {}; const float *oversampling_port {}; diff --git a/plugins/lv2/sfizz_ui.cpp b/plugins/lv2/sfizz_ui.cpp index eaf5219d..af98100e 100644 --- a/plugins/lv2/sfizz_ui.cpp +++ b/plugins/lv2/sfizz_ui.cpp @@ -125,6 +125,7 @@ struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { int sfz_serial = 0; bool valid_sfz_serial = false; + bool multi_out = false; protected: void uiSendValue(EditId id, const EditValue& v) override; @@ -271,6 +272,13 @@ instantiate(const LV2UI_Descriptor *descriptor, // LV2 has its own path management mechanism self->uiReceiveValue(EditId::CanEditUserFilesDir, 0); + // Send the number of outputs if necessary + int numOutputs = sfizz_lv2_get_num_outputs(self->plugin); + if (numOutputs > 2) { + self->multi_out = true; + self->uiReceiveValue(EditId::PluginOutputs, numOutputs); + } + *widget = reinterpret_cast(uiFrame->getPlatformFrame()->getPlatformRepresentation()); if (self->resize) @@ -295,6 +303,114 @@ cleanup(LV2UI_Handle ui) delete self; } +static void +port_event_stereo(sfizz_ui_t *self, uint32_t port_index, const void *buffer) +{ + const float v = *reinterpret_cast(buffer); + + switch (port_index) { + case SFIZZ_VOLUME: + self->uiReceiveValue(EditId::Volume, v); + break; + case SFIZZ_POLYPHONY: + self->uiReceiveValue(EditId::Polyphony, v); + break; + case SFIZZ_OVERSAMPLING: + self->uiReceiveValue(EditId::Oversampling, v); + break; + case SFIZZ_PRELOAD: + self->uiReceiveValue(EditId::PreloadSize, v); + break; + case SFIZZ_SCALA_ROOT_KEY: + self->uiReceiveValue(EditId::ScalaRootKey, v); + break; + case SFIZZ_TUNING_FREQUENCY: + self->uiReceiveValue(EditId::TuningFrequency, v); + break; + case SFIZZ_STRETCH_TUNING: + self->uiReceiveValue(EditId::StretchTuning, v); + break; + case SFIZZ_SAMPLE_QUALITY: + self->uiReceiveValue(EditId::SampleQuality, v); + break; + case SFIZZ_OSCILLATOR_QUALITY: + self->uiReceiveValue(EditId::OscillatorQuality, v); + break; + case SFIZZ_ACTIVE_VOICES: + self->uiReceiveValue(EditId::UINumActiveVoices, v); + break; + case SFIZZ_NUM_CURVES: + self->uiReceiveValue(EditId::UINumCurves, v); + break; + case SFIZZ_NUM_MASTERS: + self->uiReceiveValue(EditId::UINumMasters, v); + break; + case SFIZZ_NUM_GROUPS: + self->uiReceiveValue(EditId::UINumGroups, v); + break; + case SFIZZ_NUM_REGIONS: + self->uiReceiveValue(EditId::UINumRegions, v); + break; + case SFIZZ_NUM_SAMPLES: + self->uiReceiveValue(EditId::UINumPreloadedSamples, v); + break; + } +} + +static void +port_event_multi(sfizz_ui_t *self, uint32_t port_index, const void *buffer) +{ + const float v = *reinterpret_cast(buffer); + + switch (port_index) { + case SFIZZ_MULTI_VOLUME: + self->uiReceiveValue(EditId::Volume, v); + break; + case SFIZZ_MULTI_POLYPHONY: + self->uiReceiveValue(EditId::Polyphony, v); + break; + case SFIZZ_MULTI_OVERSAMPLING: + self->uiReceiveValue(EditId::Oversampling, v); + break; + case SFIZZ_MULTI_PRELOAD: + self->uiReceiveValue(EditId::PreloadSize, v); + break; + case SFIZZ_MULTI_SCALA_ROOT_KEY: + self->uiReceiveValue(EditId::ScalaRootKey, v); + break; + case SFIZZ_MULTI_TUNING_FREQUENCY: + self->uiReceiveValue(EditId::TuningFrequency, v); + break; + case SFIZZ_MULTI_STRETCH_TUNING: + self->uiReceiveValue(EditId::StretchTuning, v); + break; + case SFIZZ_MULTI_SAMPLE_QUALITY: + self->uiReceiveValue(EditId::SampleQuality, v); + break; + case SFIZZ_MULTI_OSCILLATOR_QUALITY: + self->uiReceiveValue(EditId::OscillatorQuality, v); + break; + case SFIZZ_MULTI_ACTIVE_VOICES: + self->uiReceiveValue(EditId::UINumActiveVoices, v); + break; + case SFIZZ_MULTI_NUM_CURVES: + self->uiReceiveValue(EditId::UINumCurves, v); + break; + case SFIZZ_MULTI_NUM_MASTERS: + self->uiReceiveValue(EditId::UINumMasters, v); + break; + case SFIZZ_MULTI_NUM_GROUPS: + self->uiReceiveValue(EditId::UINumGroups, v); + break; + case SFIZZ_MULTI_NUM_REGIONS: + self->uiReceiveValue(EditId::UINumRegions, v); + break; + case SFIZZ_MULTI_NUM_SAMPLES: + self->uiReceiveValue(EditId::UINumPreloadedSamples, v); + break; + } +} + static void port_event(LV2UI_Handle ui, uint32_t port_index, @@ -303,57 +419,11 @@ port_event(LV2UI_Handle ui, const void *buffer) { sfizz_ui_t *self = (sfizz_ui_t *)ui; - if (format == 0) { - const float v = *reinterpret_cast(buffer); - - switch (port_index) { - case SFIZZ_VOLUME: - self->uiReceiveValue(EditId::Volume, v); - break; - case SFIZZ_POLYPHONY: - self->uiReceiveValue(EditId::Polyphony, v); - break; - case SFIZZ_OVERSAMPLING: - self->uiReceiveValue(EditId::Oversampling, v); - break; - case SFIZZ_PRELOAD: - self->uiReceiveValue(EditId::PreloadSize, v); - break; - case SFIZZ_SCALA_ROOT_KEY: - self->uiReceiveValue(EditId::ScalaRootKey, v); - break; - case SFIZZ_TUNING_FREQUENCY: - self->uiReceiveValue(EditId::TuningFrequency, v); - break; - case SFIZZ_STRETCH_TUNING: - self->uiReceiveValue(EditId::StretchTuning, v); - break; - case SFIZZ_SAMPLE_QUALITY: - self->uiReceiveValue(EditId::SampleQuality, v); - break; - case SFIZZ_OSCILLATOR_QUALITY: - self->uiReceiveValue(EditId::OscillatorQuality, v); - break; - case SFIZZ_ACTIVE_VOICES: - self->uiReceiveValue(EditId::UINumActiveVoices, v); - break; - case SFIZZ_NUM_CURVES: - self->uiReceiveValue(EditId::UINumCurves, v); - break; - case SFIZZ_NUM_MASTERS: - self->uiReceiveValue(EditId::UINumMasters, v); - break; - case SFIZZ_NUM_GROUPS: - self->uiReceiveValue(EditId::UINumGroups, v); - break; - case SFIZZ_NUM_REGIONS: - self->uiReceiveValue(EditId::UINumRegions, v); - break; - case SFIZZ_NUM_SAMPLES: - self->uiReceiveValue(EditId::UINumPreloadedSamples, v); - break; - } + if (self->multi_out) + port_event_multi(self, port_index, buffer); + else + port_event_stereo(self, port_index, buffer); } else if (format == self->atom_event_transfer_uri) { auto *atom = reinterpret_cast(buffer); @@ -410,11 +480,8 @@ port_event(LV2UI_Handle ui, const float *levels = reinterpret_cast(vec_body); uint32_t count = static_cast((vec_end - vec_body) / sizeof(float)); - float left = (count > 0) ? levels[0] : 0.0f; - float right = (count > 1) ? levels[1] : 0.0f; - - self->uiReceiveValue(EditId::LeftLevel, left); - self->uiReceiveValue(EditId::RightLevel, right); + for (uint32_t i = 0; i < count; ++i) + self->uiReceiveValue(editIdForLevel(i), levels[i]); } } } diff --git a/plugins/vst/SfizzVstController.cpp b/plugins/vst/SfizzVstController.cpp index 19cac7cc..500acf95 100644 --- a/plugins/vst/SfizzVstController.cpp +++ b/plugins/vst/SfizzVstController.cpp @@ -105,14 +105,15 @@ tresult PLUGIN_API SfizzVstControllerNoUi::initialize(FUnknown* context) } // Volume levels - parameters.addParameter( - SfizzRange::getForParameter(kPidLeftLevel).createParameter( - Steinberg::String("Left level"), pid++, nullptr, - 0, Vst::ParameterInfo::kIsReadOnly|Vst::ParameterInfo::kIsHidden, Vst::kRootUnitId)); - parameters.addParameter( - SfizzRange::getForParameter(kPidRightLevel).createParameter( - Steinberg::String("Right level"), pid++, nullptr, - 0, Vst::ParameterInfo::kIsReadOnly|Vst::ParameterInfo::kIsHidden, Vst::kRootUnitId)); + for (unsigned i = 0; i < 16; ++i) { + Steinberg::String title; + title.printf("Level %u", i); + + parameters.addParameter( + SfizzRange::getForParameter(kPidLevel0 + i).createParameter( + title, pid++, nullptr, 0, + Vst::ParameterInfo::kIsReadOnly|Vst::ParameterInfo::kIsHidden, Vst::kRootUnitId)); + } // Editor status parameters.addParameter( diff --git a/plugins/vst/SfizzVstEditor.cpp b/plugins/vst/SfizzVstEditor.cpp index 2e1506ca..ea9cf26c 100644 --- a/plugins/vst/SfizzVstEditor.cpp +++ b/plugins/vst/SfizzVstEditor.cpp @@ -343,16 +343,16 @@ void SfizzVstEditor::updateParameter(Vst::Parameter* parameterToUpdate) case kPidOscillatorQuality: uiReceiveValue(EditId::OscillatorQuality, range.denormalize(value)); break; - case kPidLeftLevel: - uiReceiveValue(EditId::LeftLevel, range.denormalize(value)); - break; - case kPidRightLevel: - uiReceiveValue(EditId::RightLevel, range.denormalize(value)); + case kPidNumOutputs: + uiReceiveValue(EditId::PluginOutputs, (int32)range.denormalize(value)); break; default: if (id >= kPidCC0 && id <= kPidCCLast) { int cc = int(id - kPidCC0); uiReceiveValue(editIdForCC(cc), range.denormalize(value)); + } else if (id >= kPidLevel0 && id <= kPidLevelLast) { + int levelId = int(id - kPidLevel0); + uiReceiveValue(editIdForLevel(levelId), range.denormalize(value)); } break; } @@ -517,6 +517,8 @@ Vst::ParamID SfizzVstEditor::parameterOfEditId(EditId id) default: if (editIdIsCC(id)) return kPidCC0 + ccForEditId(id); + else if (editIdIsLevel(id)) + return kPidLevel0 + levelForEditId(id); return Vst::kNoParamId; } } diff --git a/plugins/vst/SfizzVstIDs.h b/plugins/vst/SfizzVstIDs.h index bdd629a7..eb4e7386 100644 --- a/plugins/vst/SfizzVstIDs.h +++ b/plugins/vst/SfizzVstIDs.h @@ -13,6 +13,8 @@ */ #define SfizzVstProcessor_cid \ Steinberg::FUID(0xe8fab718, 0x15ed46e3, 0x8b598310, 0x1e12993f) +#define SfizzVstProcessorMulti_cid \ + Steinberg::FUID(0xc9da9274, 0x43794873, 0xa900ed81, 0xd1946115) #define SfizzVstController_cid \ Steinberg::FUID(0x7129736c, 0xbc784134, 0xbb899d56, 0x2ebafe4f) diff --git a/plugins/vst/SfizzVstParameters.h b/plugins/vst/SfizzVstParameters.h index cfe1c473..6f9c4e9a 100644 --- a/plugins/vst/SfizzVstParameters.h +++ b/plugins/vst/SfizzVstParameters.h @@ -26,8 +26,9 @@ enum { kPidPitchBend, kPidCC0, kPidCCLast = kPidCC0 + sfz::config::numCCs - 1, - kPidLeftLevel, - kPidRightLevel, + kPidNumOutputs, + kPidLevel0, + kPidLevelLast = kPidLevel0 + 16, kPidEditorOpen, /* Reserved */ kNumParameters, @@ -81,15 +82,15 @@ struct SfizzRange { return {0.0, 0.0, 1.0}; case kPidPitchBend: return {0.0, -1.0, 1.0}; - case kPidLeftLevel: - return {0.0, 0.0, 1.0}; - case kPidRightLevel: - return {0.0, 0.0, 1.0}; case kPidEditorOpen: return {0.0, 0.0, 1.0}; + case kPidNumOutputs: + return {2.0, 2.0, 16.0}; default: if (id >= kPidCC0 && id <= kPidCCLast) return {0.0, 0.0, 1.0}; + else if (id >= kPidLevel0 && id <= kPidLevelLast) + return {0.0, 0.0, 1.0}; throw std::runtime_error("Bad parameter ID"); } } diff --git a/plugins/vst/SfizzVstProcessor.cpp b/plugins/vst/SfizzVstProcessor.cpp index e50801fa..4a78bc46 100644 --- a/plugins/vst/SfizzVstProcessor.cpp +++ b/plugins/vst/SfizzVstProcessor.cpp @@ -92,7 +92,7 @@ tresult PLUGIN_API SfizzVstProcessor::initialize(FUnknown* context) _scalaUpdate->addDependent(this); _automationUpdate->addDependent(this); - addAudioOutput(STR16("Audio Output"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 1"), Vst::SpeakerArr::kStereo); addEventInput(STR16("Event Input"), 1); _state = SfizzVstState(); @@ -143,9 +143,11 @@ tresult PLUGIN_API SfizzVstProcessor::terminate() tresult PLUGIN_API SfizzVstProcessor::setBusArrangements(Vst::SpeakerArrangement* inputs, int32 numIns, Vst::SpeakerArrangement* outputs, int32 numOuts) { - bool isStereo = numIns == 0 && numOuts == 1 && outputs[0] == Vst::SpeakerArr::kStereo; + bool allStereo { true }; + for (unsigned o = 0; o < numOuts; ++o) + allStereo &= (outputs[o] == Vst::SpeakerArr::kStereo); - if (!isStereo) + if (!allStereo) return kResultFalse; return AudioEffect::setBusArrangements(inputs, numIns, outputs, numOuts); @@ -274,30 +276,45 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) if (data.processMode == Vst::kOffline) lock.lock(); else - lock.try_lock(); + (void)lock.try_lock(); if (data.processContext) updateTimeInfo(*data.processContext); const uint32 numFrames = data.numSamples; _canPerformEventsAndParameters = lock.owns_lock(); + bool editorWasOpen = _editorIsOpen; processUnorderedEvents(numFrames, data.inputParameterChanges, data.inputEvents); if (data.numOutputs < 1) // flush mode return kResultTrue; constexpr uint32 numChannels = 2; - float* outputs[numChannels]; + constexpr uint32 maxChannels = 16; + float* outputs[maxChannels]; + const auto numMonoChannels = data.numOutputs * numChannels; - assert(numChannels == data.outputs[0].numChannels); + for (unsigned o = 0; o < data.numOutputs; ++o) { + assert(data.outputs[o].numChannels == numChannels); + for (unsigned c = 0; c < numChannels; ++c) + outputs[numChannels * o + c] = data.outputs[o].channelBuffers32[c]; + } - for (unsigned c = 0; c < numChannels; ++c) - outputs[c] = data.outputs[0].channelBuffers32[c]; + if (!editorWasOpen && _editorIsOpen) { + if (Vst::IParameterChanges* pcs = data.outputParameterChanges) { + int32 index; + if (Vst::IParamValueQueue* vq = pcs->addParameterData(kPidNumOutputs, index)) + vq->addPoint(0, SfizzRange::getForParameter(kPidNumOutputs).normalize(numMonoChannels), index); + } + } if (!lock.owns_lock()) { for (unsigned c = 0; c < numChannels; ++c) std::memset(outputs[c], 0, numFrames * sizeof(float)); - data.outputs[0].silenceFlags = 3; + + for (unsigned o = 0; o < data.numOutputs; ++o) + data.outputs[o].silenceFlags = 3; + return kResultTrue; } @@ -318,21 +335,20 @@ tresult PLUGIN_API SfizzVstProcessor::process(Vst::ProcessData& data) synth.setSampleQuality(sfz::Sfizz::ProcessLive, _state.sampleQuality); synth.setOscillatorQuality(sfz::Sfizz::ProcessLive, _state.oscillatorQuality); - synth.renderBlock(outputs, numFrames, numChannels); + synth.renderBlock(outputs, numFrames, data.numOutputs); // Update levels, if editor is open, otherwise skip RMSFollower& rmsFollower = _rmsFollower; if (_editorIsOpen) { - rmsFollower.process(outputs[0], outputs[1], numFrames); - simde__m128 rms = _rmsFollower.getRMS(); - float left = reinterpret_cast(&rms)[0]; - float right = reinterpret_cast(&rms)[1]; + rmsFollower.process((const float**)outputs, numFrames, numMonoChannels); + float levels[maxChannels]; + rmsFollower.getRMS(levels, numMonoChannels); if (Vst::IParameterChanges* pcs = data.outputParameterChanges) { int32 index; - if (Vst::IParamValueQueue* vq = pcs->addParameterData(kPidLeftLevel, index)) - vq->addPoint(0, left, index); - if (Vst::IParamValueQueue* vq = pcs->addParameterData(kPidRightLevel, index)) - vq->addPoint(0, right, index); + for (int c = 0; c < numMonoChannels; ++c) { + if (Vst::IParamValueQueue* vq = pcs->addParameterData(kPidLevel0 + c, index)) + vq->addPoint(0, levels[c], index); + } } } else @@ -916,10 +932,39 @@ FUnknown* SfizzVstProcessor::createInstance(void*) return static_cast(new SfizzVstProcessor); } +FUnknown* SfizzVstProcessorMulti::createInstance(void*) +{ + return static_cast(new SfizzVstProcessorMulti); +} + template <> FUnknown* createInstance(void* context) { return SfizzVstProcessor::createInstance(context); } +tresult PLUGIN_API SfizzVstProcessorMulti::initialize(FUnknown* context) +{ + tresult res = SfizzVstProcessor::initialize(context); + if (res != kResultFalse) { + addAudioOutput(STR16("Audio Output 2"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 3"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 4"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 5"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 6"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 7"), Vst::SpeakerArr::kStereo); + addAudioOutput(STR16("Audio Output 8"), Vst::SpeakerArr::kStereo); + } + _multi = true; + _rmsFollower.setNumOutputs(16); + return res; +} + +template <> +FUnknown* createInstance(void* context) +{ + return SfizzVstProcessorMulti::createInstance(context); +} + FUID SfizzVstProcessor::cid = SfizzVstProcessor_cid; +FUID SfizzVstProcessorMulti::cid = SfizzVstProcessor_cid; diff --git a/plugins/vst/SfizzVstProcessor.h b/plugins/vst/SfizzVstProcessor.h index a41016e8..0f983036 100644 --- a/plugins/vst/SfizzVstProcessor.h +++ b/plugins/vst/SfizzVstProcessor.h @@ -55,6 +55,9 @@ public: static FUID cid; // --- Sfizz stuff here below --- +protected: + RMSFollower _rmsFollower {}; + bool _multi { false }; private: // synth state. acquire processMutex before accessing std::unique_ptr _synth; @@ -66,7 +69,6 @@ private: bool _canPerformEventsAndParameters {}; // level meters - RMSFollower _rmsFollower; bool _editorIsOpen = false; // updates @@ -129,6 +131,12 @@ private: static bool writeMessage(Ring_Buffer& fifo, const char* type, const void* data, uintptr_t size); }; +class SfizzVstProcessorMulti : public SfizzVstProcessor { +public: + tresult PLUGIN_API initialize(FUnknown* context) override; + static FUnknown* createInstance(void*); + static FUID cid; +}; //------------------------------------------------------------------------------ template const T* SfizzVstProcessor::RTMessage::payload() const diff --git a/plugins/vst/VstPluginFactory.cpp b/plugins/vst/VstPluginFactory.cpp index 462caff4..53146738 100644 --- a/plugins/vst/VstPluginFactory.cpp +++ b/plugins/vst/VstPluginFactory.cpp @@ -12,6 +12,7 @@ #include "pluginterfaces/vst/ivsteditcontroller.h" class SfizzVstProcessor; +class SfizzVstProcessorMulti; class SfizzVstController; BEGIN_FACTORY_DEF(VSTPLUGIN_VENDOR, @@ -28,6 +29,16 @@ DEF_CLASS2 (INLINE_UID_FROM_FUID(SfizzVstProcessor_cid), kVstVersionString, createInstance) +DEF_CLASS2 (INLINE_UID_FROM_FUID(SfizzVstProcessorMulti_cid), + PClassInfo::kManyInstances, + kVstAudioEffectClass, + VSTPLUGIN_NAME, + Vst::kDistributable, + Vst::PlugType::kInstrumentSynth, + VSTPLUGIN_VERSION, + kVstVersionString, + createInstance) + DEF_CLASS2 (INLINE_UID_FROM_FUID(SfizzVstController_cid), PClassInfo::kManyInstances, kVstComponentControllerClass, diff --git a/src/sfizz.h b/src/sfizz.h index bc578a91..a709a6b4 100644 --- a/src/sfizz.h +++ b/src/sfizz.h @@ -699,7 +699,8 @@ SFIZZ_EXPORTED_API void sfizz_send_playback_state(sfizz_synth_t* synth, int dela * * @param synth The synth. * @param channels Pointers to the left and right channel of the output. - * @param num_channels Should be equal to 2 for the time being. + * @param num_channels Number of output channels; should be a multiple of 2 as + * sfizz only handles stereo outputs. * @param num_frames Number of frames to fill. This should be less than * or equal to the expected samples_per_block. * diff --git a/src/sfizz/AudioBuffer.h b/src/sfizz/AudioBuffer.h index 3d802e3d..847fb8b1 100644 --- a/src/sfizz/AudioBuffer.h +++ b/src/sfizz/AudioBuffer.h @@ -26,7 +26,7 @@ namespace sfz * @tparam MaxChannels the maximum number of channels in the buffer * @tparam Alignment the alignment for the buffers */ -template class AudioBuffer { diff --git a/src/sfizz/AudioSpan.h b/src/sfizz/AudioSpan.h index dd74d9c5..a4a8eab8 100644 --- a/src/sfizz/AudioSpan.h +++ b/src/sfizz/AudioSpan.h @@ -70,7 +70,7 @@ namespace sfz * @tparam Type the underlying buffer type * @tparam MaxChannels the maximum number of channels. Defaults to sfz::config::numChannels */ -template +template class AudioSpan { public: using size_type = size_t; @@ -95,6 +95,23 @@ public: this->spans[i] = spans[i] + offset; } + /** + * @brief Construct a new Audio Span object + * + * @param spans an array of pointers to buffers. + * @param numChannels the number of spans to take in from the array + * @param offset starting offset for the AudioSpan + * @param numFrames size of the AudioSpan + */ + AudioSpan(Type** spans, size_type numChannels, size_type offset, size_type numFrames) + : numFrames(numFrames) + , numChannels(numChannels) + { + ASSERT(numChannels <= MaxChannels); + for (size_t i = 0; i < numChannels; ++i) + this->spans[i] = spans[i] + offset; + } + /** * @brief Construct a new Audio Span object from initializer lists * @@ -440,12 +457,18 @@ public: * * @param length the number of elements to take on each channel */ - AudioSpan subspan(size_type offset) const + AudioSpan subspan(size_type offset) const { ASSERT(offset <= numFrames); return { spans, numChannels, offset, numFrames - offset }; } + AudioSpan getStereoSpan(size_type start) const + { + ASSERT(start + 1 < numChannels); + return { { spans[start], spans[start+1] }, 2, 0, numFrames }; + } + private: static_assert(MaxChannels > 0, "Need a positive number of channels"); std::array spans; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index a458a837..d0337196 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -44,7 +44,7 @@ namespace config { constexpr int loggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 }; constexpr bool loggingEnabled { false }; - constexpr size_t numChannels { 2 }; + constexpr size_t maxChannels { 32 }; constexpr int numBackgroundThreads { 4 }; constexpr unsigned fileClearingPeriod { 5 }; // in seconds constexpr int numVoices { 64 }; diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index bb17b1bf..70bc3d01 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -42,6 +42,7 @@ FloatSpec oscillatorModDepth { 0.0f, {0.0f, 10000.0f}, kNormalizePercent|kPermis FloatSpec oscillatorModDepthMod { 0.0f, {0.0f, 10000.0f}, kNormalizePercent|kPermissiveBounds }; Int32Spec oscillatorQuality { 1, {0, 3}, 0 }; Int64Spec group { 0, {-int32_t_max, uint32_t_max}, 0 }; +UInt16Spec output { 0, {0, config::maxChannels / 2 - 1}, kEnforceBounds }; FloatSpec offTime { 6e-3f, {0.0f, 100.0f}, kPermissiveBounds }; UInt32Spec polyphony { config::maxVoices, {0, config::maxVoices}, kEnforceBounds }; UInt32Spec notePolyphony { config::maxVoices, {1, config::maxVoices}, kEnforceBounds }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index dccba31c..9387369e 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -154,6 +154,7 @@ namespace Default extern const OpcodeSpec oscillatorModDepthMod; extern const OpcodeSpec oscillatorQuality; extern const OpcodeSpec group; + extern const OpcodeSpec output; extern const OpcodeSpec offTime; extern const OpcodeSpec polyphony; extern const OpcodeSpec notePolyphony; diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index da99d98c..81cc3d00 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -100,6 +100,14 @@ void EffectBus::addEffect(std::unique_ptr fx) _effects.emplace_back(std::move(fx)); } +const Effect* EffectBus::effectView(unsigned index) const +{ + if (index > _effects.size()) + return {}; + + return _effects[index].get(); +} + void EffectBus::clearInputs(unsigned nframes) { AudioSpan(_inputs).first(nframes).fill(0.0f); @@ -111,6 +119,8 @@ void EffectBus::addToInputs(const float* const addInput[], float addGain, unsign if (addGain == 0) return; + _hasSignal = true; + for (unsigned c = 0; c < EffectChannels; ++c) { absl::Span addIn { addInput[c], nframes }; sfz::multiplyAdd1(addGain, addIn, _inputs.getSpan(c).first(nframes)); @@ -144,15 +154,19 @@ void EffectBus::process(unsigned nframes) { size_t numEffects = _effects.size(); + // TODO: Can we have only one buffer and pass stuff without copies? if (numEffects > 0 && hasNonZeroOutput()) { _effects[0]->process( AudioSpan(_inputs), AudioSpan(_outputs), nframes); for (size_t i = 1; i < numEffects; ++i) _effects[i]->process( AudioSpan(_outputs), AudioSpan(_outputs), nframes); - } else + } else { fx::Nothing().process( AudioSpan(_inputs), AudioSpan(_outputs), nframes); + } + + _hasSignal = false; } void EffectBus::mixOutputsTo(float* const mainOutput[], float* const mixOutput[], unsigned nframes) diff --git a/src/sfizz/Effects.h b/src/sfizz/Effects.h index 8cc5d63f..25ce96e5 100644 --- a/src/sfizz/Effects.h +++ b/src/sfizz/Effects.h @@ -98,10 +98,18 @@ public: */ void addEffect(std::unique_ptr fx); + /** + * @brief Get a view into an effect in the chain + * + * @param index + * @return const Effect* + */ + const Effect* effectView(unsigned index) const; + /** @brief Checks whether this bus can produce output. */ - bool hasNonZeroOutput() const { return _gainToMain != 0 || _gainToMix != 0; } + bool hasNonZeroOutput() const { return _hasSignal && (_gainToMain != 0 || _gainToMix != 0); } /** @brief Sets the amount of effect output going to the main. @@ -181,6 +189,7 @@ private: AudioBuffer _outputs { EffectChannels, config::defaultSamplesPerBlock }; float _gainToMain { Default::effect }; float _gainToMix { Default::effect }; + bool _hasSignal { false }; }; } // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e7481101..a9d7544e 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -182,6 +182,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool cleanOpcode) case hash("group"): // also polyphony_group group = opcode.read(Default::group); break; + case hash("output"): + output = opcode.read(Default::output); + break; case hash("off_by"): // also offby offBy = opcode.readOptional(Default::group); break; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index f30a4c12..1ebb1818 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -263,6 +263,7 @@ struct Region { // Instrument settings: voice lifecycle int64_t group { Default::group }; // group + uint16_t output { Default::output }; // output absl::optional offBy {}; // off_by OffMode offMode { Default::offMode }; // off_mode float offTime { Default::offTime }; // off_mode diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index fbf5f158..86fb4093 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -63,7 +63,7 @@ Synth::Impl::Impl() parser_.setListener(this); effectFactory_.registerStandardEffectTypes(); - effectBuses_.reserve(5); // sufficient room for main and fx1-4 + initEffectBuses(); resetVoices(config::numVoices); // modulation sources @@ -229,6 +229,27 @@ void Synth::Impl::buildRegion(const std::vector& regionOpcodes) lastLayer->initializeActivations(); } +void Synth::Impl::addEffectBusesIfNecessary(uint16_t output) +{ + while (effectBuses_.size() <= output) { + // Add output + effectBuses_.emplace_back(); + auto& buses = effectBuses_.back(); + // Add an empty main bus on output + buses.emplace_back(new EffectBus); + buses[0]->setGainToMain(1.0); + buses[0]->setSamplesPerBlock(samplesPerBlock_); + buses[0]->setSampleRate(sampleRate_); + buses[0]->clearInputs(samplesPerBlock_); + } +} + +void Synth::Impl::initEffectBuses() +{ + effectBuses_.clear(); + addEffectBusesIfNecessary(0); +} + void Synth::Impl::clear() { FilePool& filePool = resources_.getFilePool(); @@ -253,16 +274,11 @@ void Synth::Impl::clear() currentSet_ = nullptr; sets_.clear(); layers_.clear(); - effectBuses_.clear(); - effectBuses_.emplace_back(new EffectBus); - effectBuses_[0]->setGainToMain(1.0); - effectBuses_[0]->setSamplesPerBlock(samplesPerBlock_); - effectBuses_[0]->setSampleRate(sampleRate_); - effectBuses_[0]->clearInputs(samplesPerBlock_); resources_.clear(); rootPath_.clear(); numGroups_ = 0; numMasters_ = 0; + numOutputs_ = 1; noteOffset_ = 0; octaveOffset_ = 0; currentSwitch_ = absl::nullopt; @@ -298,6 +314,8 @@ void Synth::Impl::clear() setCCLabel(7, "Volume"); setCCLabel(10, "Pan"); setCCLabel(11, "Expression"); + + initEffectBuses(); } void Synth::Impl::handleMasterOpcodes(const std::vector& members) @@ -453,14 +471,27 @@ void Synth::Impl::handleControlOpcodes(const std::vector& members) } } + void Synth::Impl::handleEffectOpcodes(const std::vector& rawMembers) { - absl::string_view busName = "main"; + absl::string_view busName { "main" }; + uint16_t output { Default::output }; - auto getOrCreateBus = [this](unsigned index) -> EffectBus& { - if (index + 1 > effectBuses_.size()) - effectBuses_.resize(index + 1); - EffectBusPtr& bus = effectBuses_[index]; + std::vector members; + members.reserve(rawMembers.size()); + for (const Opcode& opcode : rawMembers) { + if (opcode.lettersOnlyHash == hash("output")) + output = opcode.read(Default::output); + + members.push_back(opcode.cleanUp(kOpcodeScopeEffect)); + } + + addEffectBusesIfNecessary(output); + + auto getOrCreateBus = [this, output](unsigned index) -> EffectBus& { + if (index + 1 > effectBuses_[output].size()) + effectBuses_[output].resize(index + 1); + EffectBusPtr& bus = effectBuses_[output][index]; if (!bus) { bus.reset(new EffectBus); bus->setSampleRate(sampleRate_); @@ -470,11 +501,6 @@ void Synth::Impl::handleEffectOpcodes(const std::vector& rawMembers) return *bus; }; - std::vector members; - members.reserve(rawMembers.size()); - for (const Opcode& opcode : rawMembers) - members.push_back(opcode.cleanUp(kOpcodeScopeEffect)); - for (const Opcode& opcode : members) { switch (opcode.lettersOnlyHash) { case hash("bus"): @@ -488,15 +514,21 @@ void Synth::Impl::handleEffectOpcodes(const std::vector& rawMembers) break; case hash("fx&tomain"): // fx&tomain - if (opcode.parameters.front() < 1 || opcode.parameters.front() > config::maxEffectBuses) - break; - getOrCreateBus(opcode.parameters.front()).setGainToMain(opcode.read(Default::effect)); + { + const auto busIndex = opcode.parameters.front(); + if (busIndex < 1 || busIndex > config::maxEffectBuses) + break; + getOrCreateBus(busIndex).setGainToMain(opcode.read(Default::effect)); + } break; case hash("fx&tomix"): // fx&tomix - if (opcode.parameters.front() < 1 || opcode.parameters.front() > config::maxEffectBuses) - break; - getOrCreateBus(opcode.parameters.front()).setGainToMix(opcode.read(Default::effect)); + { + const auto busIndex = opcode.parameters.front(); + if (busIndex < 1 || busIndex > config::maxEffectBuses) + break; + getOrCreateBus(busIndex).setGainToMix(opcode.read(Default::effect)); + } break; } } @@ -512,11 +544,10 @@ void Synth::Impl::handleEffectOpcodes(const std::vector& rawMembers) } // create the effect and add it - EffectBus& bus = getOrCreateBus(busIndex); auto fx = effectFactory_.makeEffect(members); fx->setSampleRate(sampleRate_); fx->setSamplesPerBlock(samplesPerBlock_); - bus.addEffect(std::move(fx)); + getOrCreateBus(busIndex).addEffect(std::move(fx)); } bool Synth::loadSfzFile(const fs::path& file) @@ -761,6 +792,7 @@ void Synth::Impl::finalizeSfzLoad() haveAmplitudeLFO = haveAmplitudeLFO || region.amplitudeLFO != absl::nullopt; havePitchLFO = havePitchLFO || region.pitchLFO != absl::nullopt; haveFilterLFO = haveFilterLFO || region.filterLFO != absl::nullopt; + numOutputs_ = max(region.output + 1, numOutputs_); ++currentRegionIndex; } @@ -819,7 +851,7 @@ void Synth::Impl::finalizeSfzLoad() settingsPerVoice_.haveFilterLFO = haveFilterLFO; applySettingsPerVoice(); - + addEffectBusesIfNecessary(numOutputs_); setupModMatrix(); // cache the set of used CCs for future access @@ -920,9 +952,11 @@ void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept impl.resources_.setSamplesPerBlock(samplesPerBlock); - for (auto& bus : impl.effectBuses_) { - if (bus) - bus->setSamplesPerBlock(samplesPerBlock); + for (int i = 0; i < impl.numOutputs_; ++i) { + for (auto& bus : impl.getEffectBusesForOutput(i)) { + if (bus) + bus->setSamplesPerBlock(samplesPerBlock); + } } } @@ -942,9 +976,11 @@ void Synth::setSampleRate(float sampleRate) noexcept impl.resources_.setSampleRate(sampleRate); - for (auto& bus : impl.effectBuses_) { - if (bus) - bus->setSampleRate(sampleRate); + for (int i = 0; i < impl.numOutputs_; ++i) { + for (auto& bus : impl.getEffectBusesForOutput(i)) { + if (bus) + bus->setSampleRate(sampleRate); + } } } @@ -1005,9 +1041,11 @@ void Synth::renderBlock(AudioSpan buffer) noexcept { // Clear effect busses ScopedTiming logger { callbackBreakdown.effects }; - for (auto& bus : impl.effectBuses_) { - if (bus) - bus->clearInputs(numFrames); + for (int i = 0; i < impl.numOutputs_; ++i) { + for (auto& bus : impl.getEffectBusesForOutput(i)) { + if (bus) + bus->clearInputs(numFrames); + } } } @@ -1023,10 +1061,11 @@ void Synth::renderBlock(AudioSpan buffer) noexcept const Region* region = voice.getRegion(); ASSERT(region != nullptr); + const auto& effectBuses = impl.getEffectBusesForOutput(region->output); voice.renderBlock(*tempSpan); - for (size_t i = 0, n = impl.effectBuses_.size(); i < n; ++i) { - if (auto& bus = impl.effectBuses_[i]) { + for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { + if (auto& bus = effectBuses[i]) { float addGain = region->getGainToEffectBus(i); bus->addToInputs(*tempSpan, addGain, numFrames); } @@ -1048,20 +1087,26 @@ void Synth::renderBlock(AudioSpan buffer) noexcept // without any , the signal is just going to flow through it. ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration }; - for (auto& bus : impl.effectBuses_) { - if (bus) { - bus->process(numFrames); - bus->mixOutputsTo(buffer, *tempMixSpan, numFrames); + const int numChannels = static_cast(buffer.getNumChannels()); + for (int i = 0; i < impl.numOutputs_; ++i) { + const auto outputStart = numChannels == 0 ? 0 : (2 * i) % numChannels; + auto outputSpan = buffer.getStereoSpan(outputStart); + const auto& effectBuses = impl.getEffectBusesForOutput(i); + for (auto& bus : effectBuses) { + if (bus) { + bus->process(numFrames); + bus->mixOutputsTo(outputSpan, *tempMixSpan, numFrames); + } } + + // Add the Mix output (fxNtomix opcodes) + // -- note(jpc) the purpose of the Mix output is not known. + // perhaps it's designed as extension point for custom processing? + // as default behavior, it adds itself to the Main signal. + outputSpan.add(*tempMixSpan); } } - // Add the Mix output (fxNtomix opcodes) - // -- note(jpc) the purpose of the Mix output is not known. - // perhaps it's designed as extension point for custom processing? - // as default behavior, it adds itself to the Main signal. - buffer.add(*tempMixSpan); - // Apply the master volume buffer.applyGain(db2mag(impl.volume_)); @@ -1624,10 +1669,10 @@ const Region* Synth::getRegionView(int idx) const noexcept return layer ? &layer->getRegion() : nullptr; } -const EffectBus* Synth::getEffectBusView(int idx) const noexcept +const EffectBus* Synth::getEffectBusView(int idx, int output) const noexcept { Impl& impl = *impl_; - return (size_t)idx < impl.effectBuses_.size() ? impl.effectBuses_[idx].get() : nullptr; + return (size_t)idx < impl.effectBuses_[output].size() ? impl.effectBuses_[output][idx].get() : nullptr; } const RegionSet* Synth::getRegionSetView(int idx) const noexcept @@ -2010,8 +2055,11 @@ void Synth::allSoundOff() noexcept Impl& impl = *impl_; for (auto& voice : impl.voiceManager_) voice.reset(); - for (auto& effectBus : impl.effectBuses_) - effectBus->clear(); + for (int i = 0; i < impl.numOutputs_; ++i) { + for (auto& effectBus : impl.getEffectBusesForOutput(i)) + if (effectBus) + effectBus->clear(); + } } void Synth::addExternalDefinition(const std::string& id, const std::string& value) diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3ffaedab..036500dd 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -237,9 +237,10 @@ public: * You'll need to include "Effects.h" to resolve the forward declaration. * * @param idx + * @param output * @return const EffectBus* */ - const EffectBus* getEffectBusView(int idx) const noexcept; + const EffectBus* getEffectBusView(int idx, int output = 0) const noexcept; /** * @brief Get a raw view into a specific set of regions. This is mostly used * for testing. diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index e2ff06f9..67a03c02 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -95,6 +95,10 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'i'>(delay, path, impl.noteOffset_); } break; + MATCH("/num_outputs", "") { + client.receive<'i'>(delay, path, impl.numOutputs_); + } break; + //---------------------------------------------------------------------- MATCH("/key/slots", "") { @@ -346,6 +350,11 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'N'>(delay, path, {}); } break; + MATCH("/region&/output", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'i'>(delay, path, region.output); + } break; + MATCH("/region&/group", "") { GET_REGION_OR_BREAK(indices[0]) client.receive<'h'>(delay, path, region.group); diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 44fd3b54..b1e1e9ea 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -234,6 +234,7 @@ struct Synth::Impl final: public Parser::Listener { int numGroups_ { 0 }; int numMasters_ { 0 }; + int numOutputs_ { 1 }; // Opcode memory; these are used to build regions, as a new region // will integrate opcodes from the group, master and global block @@ -278,7 +279,10 @@ struct Synth::Impl final: public Parser::Listener { // Effect factory and buses EffectFactory effectFactory_; typedef std::unique_ptr EffectBusPtr; - std::vector effectBuses_; // 0 is "main", 1-N are "fx1"-"fxN" + std::vector> effectBuses_; // first index is the output, then 0 is "main", 1-N are "fx1"-"fxN" + const std::vector& getEffectBusesForOutput(uint16_t numOutput) { return effectBuses_[numOutput]; } + void initEffectBuses(); + void addEffectBusesIfNecessary(uint16_t output); int samplesPerBlock_ { config::defaultSamplesPerBlock }; float sampleRate_ { config::defaultSampleRate }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index f08def4e..41c98307 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -765,7 +765,7 @@ void Voice::setSamplesPerBlock(int samplesPerBlock) noexcept impl.powerFollower_.setSamplesPerBlock(samplesPerBlock); } -void Voice::renderBlock(AudioSpan buffer) noexcept +void Voice::renderBlock(AudioSpan buffer) noexcept { Impl& impl = *impl_; ASSERT(static_cast(buffer.getNumFrames()) <= impl.samplesPerBlock_); diff --git a/src/sfizz/effects/Disto.cpp b/src/sfizz/effects/Disto.cpp index 13eeaa52..2102a420 100644 --- a/src/sfizz/effects/Disto.cpp +++ b/src/sfizz/effects/Disto.cpp @@ -191,6 +191,5 @@ std::unique_ptr Disto::makeInstance(absl::Span members) return fx; } - } // namespace sfz } // namespace fx diff --git a/src/sfizz/effects/Lofi.cpp b/src/sfizz/effects/Lofi.cpp index 591be3a9..0fbea47c 100644 --- a/src/sfizz/effects/Lofi.cpp +++ b/src/sfizz/effects/Lofi.cpp @@ -206,6 +206,5 @@ namespace fx { fPhase = phase; fLastValue = lastValue; } - } // namespace fx } // namespace sfz diff --git a/src/sfizz/effects/Rectify.cpp b/src/sfizz/effects/Rectify.cpp index dbc92997..f651286a 100644 --- a/src/sfizz/effects/Rectify.cpp +++ b/src/sfizz/effects/Rectify.cpp @@ -100,6 +100,5 @@ namespace fx { return fx; } - } // namespace fx } // namespace sfz diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 2d7d58c0..4c96a69a 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -255,9 +255,10 @@ void sfz::Sfizz::playbackState(int delay, int playbackState) synth->synth.playbackState(delay, playbackState); } -void sfz::Sfizz::renderBlock(float** buffers, size_t numSamples, int /*numOutputs*/) noexcept +void sfz::Sfizz::renderBlock(float** buffers, size_t numSamples, int numOutputs) noexcept { - synth->synth.renderBlock({{buffers[0], buffers[1]}, numSamples}); + sfz::AudioSpan bufferSpan { buffers, static_cast(numOutputs * 2), 0, numSamples }; + synth->synth.renderBlock(bufferSpan); } int sfz::Sfizz::getNumActiveVoices() const noexcept diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index 4b191ab5..0c9a3b35 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -193,10 +193,8 @@ void sfizz_send_playback_state(sfizz_synth_t* synth, int delay, int playback_sta void sfizz_render_block(sfizz_synth_t* synth, float** channels, int num_channels, int num_frames) { - // Only stereo output is supported for now - ASSERT(num_channels == 2); - UNUSED(num_channels); - synth->synth.renderBlock({{channels[0], channels[1]}, static_cast(num_frames)}); + sfz::AudioSpan channelSpan { channels, static_cast(num_channels), 0, static_cast(num_frames) }; + synth->synth.renderBlock(channelSpan); } unsigned int sfizz_get_preload_size(sfizz_synth_t* synth) diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 28e5e0a9..592ecee5 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -427,6 +427,64 @@ TEST_CASE("[Values] Loop count") REQUIRE(messageList == expected); } +TEST_CASE("[Values] Output") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + + SECTION("No special outputs") { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + )"); + synth.dispatchMessage(client, 0, "/region0/output", "", nullptr); + synth.dispatchMessage(client, 0, "/num_outputs", "", nullptr); + std::vector expected { + "/region0/output,i : { 0 }", + "/num_outputs,i : { 1 }", + }; + REQUIRE(messageList == expected); + } + + SECTION("1 output") { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav output=1 + sample=kick.wav output=-1 + )"); + synth.dispatchMessage(client, 0, "/region0/output", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/output", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/output", "", nullptr); + synth.dispatchMessage(client, 0, "/num_outputs", "", nullptr); + std::vector expected { + "/region0/output,i : { 0 }", + "/region1/output,i : { 1 }", + "/region2/output,i : { 0 }", + "/num_outputs,i : { 2 }", + }; + REQUIRE(messageList == expected); + } + + SECTION("More than 1 output") { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav output=1 + sample=kick.wav output=3 + )"); + synth.dispatchMessage(client, 0, "/region0/output", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/output", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/output", "", nullptr); + synth.dispatchMessage(client, 0, "/num_outputs", "", nullptr); + std::vector expected { + "/region0/output,i : { 0 }", + "/region1/output,i : { 1 }", + "/region2/output,i : { 3 }", + "/num_outputs,i : { 4 }", + }; + REQUIRE(messageList == expected); + } +} TEST_CASE("[Values] Group") { diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index b0f3feda..e92fd343 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -271,7 +271,6 @@ TEST_CASE("[Synth] Number of effect buses and resetting behavior") sfz::Synth synth; sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; - REQUIRE( synth.getEffectBusView(0) == nullptr); // No effects at first synth.loadSfzString(fs::current_path() / "tests/TestFiles/Effects/base.sfz", R"( lokey=0 hikey=127 sample=*sine )"); @@ -398,6 +397,60 @@ TEST_CASE("[Synth] Gain to mix") REQUIRE( bus->gainToMix() == 0.5 ); } +TEST_CASE("[Synth] Effect with two outputs") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_2.sfz", R"( + lokey=0 hikey=127 sample=*sine effect1=100 + lokey=0 hikey=127 sample=*sine effect1=100 output=1 + directtomain=50 fx1tomain=50 type=lofi bus=fx1 bitred=90 decim=10 + )"); + auto bus = synth.getEffectBusView(0); + REQUIRE( bus != nullptr); + REQUIRE( bus->numEffects() == 0 ); + REQUIRE( bus->gainToMain() == 0.5 ); + REQUIRE( bus->gainToMix() == 0 ); + bus = synth.getEffectBusView(1); + REQUIRE( bus != nullptr); + REQUIRE( bus->numEffects() == 1 ); + REQUIRE( bus->gainToMain() == 0.5 ); + REQUIRE( bus->gainToMix() == 0 ); + bus = synth.getEffectBusView(0, 1); + REQUIRE( bus != nullptr); + REQUIRE( bus->numEffects() == 0 ); + REQUIRE( bus->gainToMain() == 1 ); + REQUIRE( bus->gainToMix() == 0 ); + bus = synth.getEffectBusView(1, 1); + REQUIRE( bus == nullptr); +} + +TEST_CASE("[Synth] Effect on the second output, with two outputs") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_2.sfz", R"( + lokey=0 hikey=127 sample=*sine effect1=100 + lokey=0 hikey=127 sample=*sine effect1=100 output=1 + directtomain=50 fx1tomain=50 type=lofi bus=fx1 bitred=90 decim=10 output=1 + )"); + auto bus = synth.getEffectBusView(0); + REQUIRE( bus != nullptr); + REQUIRE( bus->numEffects() == 0 ); + REQUIRE( bus->gainToMain() == 1 ); + REQUIRE( bus->gainToMix() == 0 ); + bus = synth.getEffectBusView(1); + REQUIRE( bus == nullptr); + bus = synth.getEffectBusView(0, 1); + REQUIRE( bus != nullptr); + REQUIRE( bus->numEffects() == 0 ); + REQUIRE( bus->gainToMain() == 0.5 ); + REQUIRE( bus->gainToMix() == 0 ); + bus = synth.getEffectBusView(1, 1); + REQUIRE( bus != nullptr); + REQUIRE( bus->numEffects() == 1 ); + REQUIRE( bus->gainToMain() == 0.5 ); + REQUIRE( bus->gainToMix() == 0 ); +} + TEST_CASE("[Synth] Basic curves") { sfz::Synth synth;