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 fa33b8d8..a4f097e5 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 84e0afa7..3457fcac 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 @@ -907,10 +923,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 a1ddcb6c..63cfabe4 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 e8578a11..d7e51ddd 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); resetDefaultCCValues(); resetAllControllers(0); @@ -231,6 +231,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(); @@ -255,16 +276,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_.clearNonState(); rootPath_.clear(); numGroups_ = 0; numMasters_ = 0; + numOutputs_ = 1; noteOffset_ = 0; octaveOffset_ = 0; currentSwitch_ = absl::nullopt; @@ -288,6 +304,8 @@ void Synth::Impl::clear() unknownOpcodes_.clear(); modificationTime_ = absl::nullopt; playheadMoved_ = false; + + initEffectBuses(); } void Synth::Impl::handleMasterOpcodes(const std::vector& members) @@ -451,14 +469,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_); @@ -468,11 +499,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"): @@ -486,15 +512,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; } } @@ -510,11 +542,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)); } void Synth::Impl::resetDefaultCCValues() noexcept @@ -801,6 +832,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; } @@ -868,7 +900,7 @@ void Synth::Impl::finalizeSfzLoad() settingsPerVoice_.haveFilterLFO = haveFilterLFO; applySettingsPerVoice(); - + addEffectBusesIfNecessary(numOutputs_); setupModMatrix(); // cache the set of used CCs for future access @@ -969,9 +1001,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); + } } } @@ -991,9 +1025,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); + } } } @@ -1054,9 +1090,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); + } } } @@ -1072,10 +1110,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); } @@ -1097,20 +1136,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_)); @@ -1672,10 +1717,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 @@ -2058,8 +2103,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 a4094092..d57a70fc 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", "") { @@ -362,6 +366,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 988110ee..2fd072d9 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -249,6 +249,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 @@ -293,7 +294,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 d2c63870..4974be3d 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;