diff --git a/plugins/common/plugin/InstrumentDescription.cpp b/plugins/common/plugin/InstrumentDescription.cpp index 071655b1..8e396c0d 100644 --- a/plugins/common/plugin/InstrumentDescription.cpp +++ b/plugins/common/plugin/InstrumentDescription.cpp @@ -88,6 +88,8 @@ std::string getDescriptionBlob(sfizz_synth_t* handle) cdata.synth->sendMessage(*cdata.client, 0, cdata.pathbuf->c_str(), "", nullptr); bufferedStrCat(cdata.pathbuf, "/cc", cc, "/default"); cdata.synth->sendMessage(*cdata.client, 0, cdata.pathbuf->c_str(), "", nullptr); + bufferedStrCat(cdata.pathbuf, "/cc", cc, "/value"); + cdata.synth->sendMessage(*cdata.client, 0, cdata.pathbuf->c_str(), "", nullptr); } } } @@ -163,6 +165,8 @@ InstrumentDescription parseDescriptionBlob(absl::string_view blob) desc.ccLabel[indices[0]] = args[0].s; else if (Messages::matchOSC("/cc&/default", path, indices) && !strcmp(sig, "f")) desc.ccDefault[indices[0]] = args[0].f; + else if (Messages::matchOSC("/cc&/value", path, indices) && !strcmp(sig, "f")) + desc.ccValue[indices[0]] = args[0].f; src += byteCount; srcSize -= byteCount; diff --git a/plugins/common/plugin/InstrumentDescription.h b/plugins/common/plugin/InstrumentDescription.h index 094d06b3..a78988cd 100644 --- a/plugins/common/plugin/InstrumentDescription.h +++ b/plugins/common/plugin/InstrumentDescription.h @@ -32,6 +32,7 @@ struct InstrumentDescription { std::array keyswitchLabel {}; std::array ccLabel {}; std::array ccDefault {}; + std::array ccValue {}; }; /** 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..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 { @@ -1244,10 +1388,10 @@ sfizz_lv2_update_sfz_info(sfizz_plugin_t *self) for (unsigned cc = 0; cc < sfz::config::numCCs; ++cc) { if (desc.ccUsed.test(cc) && !desc.sustainOrSostenuto.test(cc)) { // Mark all the used CCs for automation with default values - self->ccauto[cc] = desc.ccDefault[cc]; + self->ccauto[cc] = desc.ccValue[cc]; self->have_ccauto = true; // Update the current CCs - self->cc_current[cc] = desc.ccDefault[cc]; + self->cc_current[cc] = desc.ccValue[cc]; } } @@ -1332,7 +1476,7 @@ restore(LV2_Handle instance, if (path) { - strncpy(self->sfz_file_path, path, MAX_PATH_SIZE); + strncpy(self->sfz_file_path, path, MAX_PATH_SIZE - 1); self->sfz_file_path[MAX_PATH_SIZE - 1] = '\0'; if (map_path) @@ -1353,7 +1497,7 @@ restore(LV2_Handle instance, if (path) { - strncpy(self->scala_file_path, path, MAX_PATH_SIZE); + strncpy(self->scala_file_path, path, MAX_PATH_SIZE - 1); self->scala_file_path[MAX_PATH_SIZE - 1] = '\0'; if (map_path) @@ -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..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(); @@ -113,7 +113,7 @@ tresult PLUGIN_API SfizzVstProcessor::initialize(FUnknown* context) _synth->setBroadcastCallback(onMessage, this); _currentStretchedTuning = 0.0; - loadSfzFileOrDefault({}, false); + loadSfzFileOrDefault({}); _synth->bpmTempo(0, 120); _timeSigNumerator = 4; @@ -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); @@ -218,7 +220,7 @@ void SfizzVstProcessor::syncStateToSynth() if (!synth) return; - loadSfzFileOrDefault(_state.sfzFile, true); + loadSfzFileOrDefault(_state.sfzFile); synth->setVolume(_state.volume); synth->setNumVoices(_state.numVoices); synth->setOversamplingFactor(1 << _state.oversamplingLog2); @@ -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 @@ -574,7 +590,7 @@ tresult PLUGIN_API SfizzVstProcessor::notify(Vst::IMessage* message) std::unique_lock lock(_processMutex); _state.sfzFile.assign(static_cast(data), size); - loadSfzFileOrDefault(_state.sfzFile, false); + loadSfzFileOrDefault(_state.sfzFile); lock.unlock(); } else if (!std::strcmp(id, "LoadScala")) { @@ -689,7 +705,7 @@ void SfizzVstProcessor::receiveOSC(int delay, const char* path, const char* sig, } } -void SfizzVstProcessor::loadSfzFileOrDefault(const std::string& filePath, bool initParametersFromState) +void SfizzVstProcessor::loadSfzFileOrDefault(const std::string& filePath) { sfz::Sfizz& synth = *_synth; @@ -709,16 +725,7 @@ void SfizzVstProcessor::loadSfzFileOrDefault(const std::string& filePath, bool i const InstrumentDescription desc = parseDescriptionBlob(descBlob); for (uint32 cc = 0; cc < sfz::config::numCCs; ++cc) { if (desc.ccUsed.test(cc)) - newControllers[cc] = desc.ccDefault[cc]; - } - // set CC from existing state - if (initParametersFromState) { - for (uint32 cc = 0; cc < sfz::config::numCCs; ++cc) { - if (absl::optional value = oldControllers[cc]) { - newControllers[cc] = *value; - synth.automateHdcc(0, int(cc), *value); - } - } + newControllers[cc] = desc.ccValue[cc]; } _state.controllers = std::move(newControllers); } @@ -822,7 +829,7 @@ void SfizzVstProcessor::doBackgroundIdle(size_t idleCounter) if (_synth->shouldReloadFile()) { fprintf(stderr, "[Sfizz] sfz file has changed, reloading\n"); std::lock_guard lock(_processMutex); - loadSfzFileOrDefault(_state.sfzFile, false); + loadSfzFileOrDefault(_state.sfzFile); } if (_synth->shouldReloadScala()) { fprintf(stderr, "[Sfizz] scala file has changed, reloading\n"); @@ -916,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 a41016e8..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 @@ -84,7 +86,7 @@ private: void receiveOSC(int delay, const char* path, const char* sig, const sfizz_arg_t* args); // misc - void loadSfzFileOrDefault(const std::string& filePath, bool initParametersFromState); + void loadSfzFileOrDefault(const std::string& filePath); // note event tracking std::array _noteEventsCurrentCycle; // 0: off, >0: on, <0: no change @@ -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 599c70ba..9bfc2425 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -46,7 +46,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/FilePool.cpp b/src/sfizz/FilePool.cpp index cce2941f..35dd61b9 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -320,6 +320,7 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce preloadedFiles[fileId].information.maxOffset = maxOffset; preloadedFiles[fileId].preloadedData = readFromFile(*reader, framesToLoad); } + existingFile->second.preloadCallCount++; } else { fileInformation->sampleRate = static_cast(reader->sampleRate()); auto insertedPair = preloadedFiles.insert_or_assign(fileId, { @@ -327,14 +328,30 @@ bool sfz::FilePool::preloadFile(const FileId& fileId, uint32_t maxOffset) noexce *fileInformation }); - if (!insertedPair.second) - return false; - insertedPair.first->second.status = FileData::Status::Preloaded; + insertedPair.first->second.preloadCallCount++; } + return true; } +void sfz::FilePool::resetPreloadCallCounts() noexcept +{ + for (auto& preloadedFile: preloadedFiles) + preloadedFile.second.preloadCallCount = 0; +} + +void sfz::FilePool::removeUnusedPreloadedData() noexcept +{ + for (auto it = preloadedFiles.begin(), end = preloadedFiles.end(); it != end; ) { + auto copyIt = it++; + if (copyIt->second.preloadCallCount == 0) { + DBG("[sfizz] Removing unused preloaded data: " << copyIt->first.filename()); + preloadedFiles.erase(copyIt); + } + } +} + sfz::FileDataHolder sfz::FilePool::loadFile(const FileId& fileId) noexcept { auto fileInformation = getFileInformation(fileId); @@ -363,7 +380,7 @@ sfz::FileDataHolder sfz::FilePool::getFilePromise(const std::shared_ptr& { const auto preloaded = preloadedFiles.find(*fileId); if (preloaded == preloadedFiles.end()) { - DBG("[sfizz] File not found in the preloaded files: " << fileId); + DBG("[sfizz] File not found in the preloaded files: " << fileId->filename()); return {}; } QueuedFileData queuedData { fileId, &preloaded->second, std::chrono::high_resolution_clock::now() }; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index a219b46c..48a75f97 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -109,6 +109,7 @@ struct FileData FileAudioBuffer preloadedData; FileInformation information; FileAudioBuffer fileData {}; + int preloadCallCount { 0 }; std::atomic status { Status::Invalid }; std::atomic availableFrames { 0 }; std::atomic readerCount { 0 }; @@ -258,6 +259,17 @@ public: * */ void clear(); + + /** + * @brief Reset the number of preloadFile counts for each sample. + */ + void resetPreloadCallCounts() noexcept; + + /** + * @brief Clear all files with a preload call count of 0. + */ + void removeUnusedPreloadedData() noexcept; + /** * @brief Get a handle on a file, which triggers background loading * diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index a7e5f7ac..1e31d156 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -10,7 +10,8 @@ sfz::MidiState::MidiState() { - reset(); + resetEventStates(); + resetNoteStates(); } void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noexcept @@ -221,11 +222,36 @@ float sfz::MidiState::getCCValueAt(int ccNumber, int delay) const noexcept return ccEvents[ccNumber].back().value; } -void sfz::MidiState::reset() noexcept +void sfz::MidiState::resetNoteStates() noexcept { for (auto& velocity: lastNoteVelocities) velocity = 0.0f; + velocityOverride = 0.0f; + activeNotes = 0; + internalClock = 0; + lastNotePlayed = -1; + alternate = 0.0f; + + auto setEvents = [] (EventVector& events, float value) { + events.clear(); + events.push_back({ 0, value }); + }; + + setEvents(ccEvents[ExtendedCCs::noteOnVelocity], 0.0f); + setEvents(ccEvents[ExtendedCCs::keyboardNoteNumber], 0.0f); + setEvents(ccEvents[ExtendedCCs::unipolarRandom], 0.0f); + setEvents(ccEvents[ExtendedCCs::bipolarRandom], 0.0f); + setEvents(ccEvents[ExtendedCCs::keyboardNoteGate], 0.0f); + setEvents(ccEvents[ExtendedCCs::alternate], 0.0f); + + noteStates.reset(); + absl::c_fill(noteOnTimes, 0); + absl::c_fill(noteOffTimes, 0); +} + +void sfz::MidiState::resetEventStates() noexcept +{ auto clearEvents = [] (EventVector& events) { events.clear(); events.push_back({ 0, 0.0f }); @@ -239,14 +265,6 @@ void sfz::MidiState::reset() noexcept clearEvents(pitchEvents); clearEvents(channelAftertouchEvents); - - velocityOverride = 0.0f; - activeNotes = 0; - internalClock = 0; - lastNotePlayed = -1; - noteStates.reset(); - absl::c_fill(noteOnTimes, 0); - absl::c_fill(noteOffTimes, 0); } const sfz::EventVector& sfz::MidiState::getCCEvents(int ccIdx) const noexcept diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index d6dd0db1..290c60d4 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -184,15 +184,20 @@ public: float getCCValueAt(int ccNumber, int delay) const noexcept; /** - * @brief Reset the midi state (does not impact the last note on time) + * @brief Reset the midi note states * */ - void reset() noexcept; + void resetNoteStates() noexcept; const EventVector& getCCEvents(int ccIdx) const noexcept; const EventVector& getPolyAftertouchEvents(int noteNumber) const noexcept; const EventVector& getPitchEvents() const noexcept; const EventVector& getChannelAftertouchEvents() const noexcept; + /** + * @brief Reset the midi event states (CC, AT, and pitch bend) + * + */ + void resetEventStates() noexcept; private: diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index f78a2346..23b83dcf 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/Resources.cpp b/src/sfizz/Resources.cpp index 8770e60f..1e52ccc7 100644 --- a/src/sfizz/Resources.cpp +++ b/src/sfizz/Resources.cpp @@ -61,19 +61,25 @@ void Resources::setSamplesPerBlock(int samplesPerBlock) impl.beatClock.setSamplesPerBlock(samplesPerBlock); } -void Resources::clear() +void Resources::clearNonState() { Impl& impl = *impl_; impl.curves = CurveSet::createPredefined(); impl.filePool.clear(); impl.wavePool.clearFileWaves(); impl.logger.clear(); - impl.midiState.reset(); impl.modMatrix.clear(); - impl.beatClock.clear(); impl.metronome.clear(); } +void Resources::clearState() +{ + Impl& impl = *impl_; + impl.midiState.resetNoteStates(); + impl.midiState.resetEventStates(); + impl.beatClock.clear(); +} + const SynthConfig& Resources::getSynthConfig() const noexcept { return impl_->synthConfig; diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index b1928a8c..86cfe8ee 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -31,7 +31,17 @@ public: void setSampleRate(float samplerate); void setSamplesPerBlock(int samplesPerBlock); - void clear(); + /** + * @brief Clear resources that are related to a currently loaded SFZ file + * + */ + void clearNonState(); + /** + * @brief Clear resources that are unrelated to the currently loaded SFZ file, + * i.e. midi state and beat clock. + * + */ + void clearState(); #define ACCESSOR_RW(Accessor, RetTy) \ RetTy const& Accessor() const noexcept; \ diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 8aa118f5..810f0fc2 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -63,8 +63,10 @@ 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); // modulation sources MidiState& midiState = resources_.getMidiState(); @@ -229,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(); @@ -253,23 +276,18 @@ 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(); + resources_.clearNonState(); rootPath_.clear(); numGroups_ = 0; numMasters_ = 0; + numOutputs_ = 1; noteOffset_ = 0; octaveOffset_ = 0; currentSwitch_ = absl::nullopt; defaultPath_ = ""; image_ = ""; - midiState.reset(); - filePool.clear(); + midiState.resetNoteStates(); + midiState.flushEvents(); filePool.setRamLoading(config::loadInRam); clearCCLabels(); currentUsedCCs_.clear(); @@ -287,17 +305,7 @@ void Synth::Impl::clear() modificationTime_ = absl::nullopt; playheadMoved_ = false; - // set default controllers - // midistate is reset above - fill(absl::MakeSpan(defaultCCValues_), 0.0f); - setDefaultHdcc(7, normalizeCC(100)); - setDefaultHdcc(10, 0.5f); - setDefaultHdcc(11, 1.0f); - - // set default controller labels - setCCLabel(7, "Volume"); - setCCLabel(10, "Pan"); - setCCLabel(11, "Expression"); + initEffectBuses(); } void Synth::Impl::handleMasterOpcodes(const std::vector& members) @@ -383,12 +391,20 @@ void Synth::Impl::handleControlOpcodes(const std::vector& members) switch (member.lettersOnlyHash) { case hash("set_cc&"): if (Default::ccNumber.bounds.containsWithEnd(member.parameters.back())) { - setDefaultHdcc(member.parameters.back(), member.read(Default::loCC)); + const auto ccNumber = member.parameters.back(); + const auto value = member.read(Default::loCC); + setDefaultHdcc(ccNumber, value); + if (!reloading) + resources_.getMidiState().ccEvent(0, ccNumber, value); } break; case hash("set_hdcc&"): if (Default::ccNumber.bounds.containsWithEnd(member.parameters.back())) { - setDefaultHdcc(member.parameters.back(), member.read(Default::loNormalized)); + const auto ccNumber = member.parameters.back(); + const auto value = member.read(Default::loNormalized); + setDefaultHdcc(ccNumber, value); + if (!reloading) + resources_.getMidiState().ccEvent(0, ccNumber, value); } break; case hash("label_cc&"): @@ -453,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_); @@ -470,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"): @@ -488,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; } } @@ -512,22 +542,59 @@ 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 +{ + fill(absl::MakeSpan(defaultCCValues_), 0.0f); + setDefaultHdcc(7, normalizeCC(100)); + setDefaultHdcc(10, 0.5f); + setDefaultHdcc(11, 1.0f); + + setCCLabel(7, "Volume"); + setCCLabel(10, "Pan"); + setCCLabel(11, "Expression"); +} + +void Synth::Impl::prepareSfzLoad(const fs::path& path) +{ + auto newPath_ = path.string(); + reloading = (lastPath_ == newPath_); + + clear(); + +#ifndef NDEBUG + if (reloading) { + DBG("[sfizz] Reloading the current file"); + } +#endif + + if (!reloading) { + + // Clear the background queues and clear the filePool + auto& filePool = resources_.getFilePool(); + filePool.waitForBackgroundLoading(); + filePool.clear(); + + // Set the default hdcc to their default + resetDefaultCCValues(); + + // Store the new path + lastPath_ = std::move(newPath_); + } } bool Synth::loadSfzFile(const fs::path& file) { Impl& impl = *impl_; - - impl.clear(); + impl.prepareSfzLoad(file); std::error_code ec; fs::path realFile = fs::canonical(file, ec); - bool success = true; Parser& parser = impl.parser_; parser.parseFile(ec ? file : realFile); @@ -539,7 +606,10 @@ bool Synth::loadSfzFile(const fs::path& file) success = success && !impl.layers_.empty(); if (!success) { + DBG("[sfizz] Loading failed"); + auto& filePool = impl.resources_.getFilePool(); parser.clear(); + filePool.clear(); return false; } @@ -550,8 +620,7 @@ bool Synth::loadSfzFile(const fs::path& file) bool Synth::loadSfzString(const fs::path& path, absl::string_view text) { Impl& impl = *impl_; - - impl.clear(); + impl.prepareSfzLoad(path); bool success = true; Parser& parser = impl.parser_; @@ -564,7 +633,10 @@ bool Synth::loadSfzString(const fs::path& path, absl::string_view text) success = success && !impl.layers_.empty(); if (!success) { + auto& filePool = impl.resources_.getFilePool(); + DBG("[sfizz] Loading failed"); parser.clear(); + filePool.clear(); return false; } @@ -574,8 +646,10 @@ bool Synth::loadSfzString(const fs::path& path, absl::string_view text) void Synth::Impl::finalizeSfzLoad() { - const fs::path& rootDirectory = parser_.originalDirectory(); FilePool& filePool = resources_.getFilePool(); + WavetablePool& wavePool = resources_.getWavePool(); + + const fs::path& rootDirectory = parser_.originalDirectory(); filePool.setRootDirectory(rootDirectory); // a string representation used for OSC purposes @@ -611,9 +685,6 @@ void Synth::Impl::finalizeSfzLoad() absl::optional fileInformation; - FilePool& filePool = resources_.getFilePool(); - WavetablePool& wavePool = resources_.getWavePool(); - if (!region.isGenerator()) { if (!filePool.checkSampleId(*region.sampleId)) { removeCurrentRegion(); @@ -761,14 +832,24 @@ 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; } - for (const auto& toLoad: filesToLoad) { - filePool.preloadFile(toLoad.first, toLoad.second); - } + // Reset the preload call count to check for unused preloaded samples + // when reloading + if (reloading) + filePool.resetPreloadCallCounts(); + for (const auto& toLoad: filesToLoad) + filePool.preloadFile(toLoad.first, toLoad.second); + + // Remove preloaded data with no linked regions + if (reloading) + filePool.removeUnusedPreloadedData(); + + // Remove bad regions with unknown files if (currentRegionCount < layers_.size()) { DBG("Removing " << (layers_.size() - currentRegionCount) << " out of " << layers_.size() << " regions"); @@ -819,7 +900,7 @@ void Synth::Impl::finalizeSfzLoad() settingsPerVoice_.haveFilterLFO = haveFilterLFO; applySettingsPerVoice(); - + addEffectBusesIfNecessary(numOutputs_); setupModMatrix(); // cache the set of used CCs for future access @@ -920,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); + } } } @@ -942,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); + } } } @@ -1005,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); + } } } @@ -1023,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); } @@ -1048,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_)); @@ -1348,7 +1442,6 @@ void Synth::Impl::setDefaultHdcc(int ccNumber, float value) ASSERT(ccNumber >= 0); ASSERT(ccNumber < config::numCCs); defaultCCValues_[ccNumber] = value; - resources_.getMidiState().ccEvent(0, ccNumber, value); } float Synth::getHdcc(int ccNumber) @@ -1624,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 @@ -2010,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 e2ff06f9..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", "") { @@ -195,6 +199,22 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'b'>(delay, path, &blob); } break; + MATCH("/aftertouch", "") { + client.receive<'f'>(delay, path, impl.resources_.getMidiState().getChannelAftertouch()); + } break; + + MATCH("/poly_aftertouch/&", "") { + if (indices[0] > 127) + break; + // Note: result value is not frame-exact + client.receive<'f'>(delay, path, impl.resources_.getMidiState().getPolyAftertouch(indices[0])); + } break; + + MATCH("/pitch_bend", "") { + // Note: result value is not frame-exact + client.receive<'f'>(delay, path, impl.resources_.getMidiState().getPitchBend()); + } break; + //---------------------------------------------------------------------- MATCH("/mem/buffers", "") { @@ -346,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 44fd3b54..2fd072d9 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -176,9 +176,24 @@ struct Synth::Impl final: public Parser::Listener { */ void startDelayedSostenutoReleases(Layer* layer, int delay, SisterVoiceRingBuilder& ring) noexcept; + /** + * @brief Reset the default CCs + * + */ + void resetDefaultCCValues() noexcept; + + /** + * @brief Prepare before loading a new SFZ file. The behavior of this function + * is changed by the reloading state. + * + * @param path + */ + void prepareSfzLoad(const fs::path& path); + /** * @brief Finalize SFZ loading, following a successful execution of the - * parsing step. + * parsing step. The behavior of this function is changed by the reloading + * state. */ void finalizeSfzLoad(); @@ -234,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 @@ -278,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 }; @@ -326,7 +345,9 @@ struct Synth::Impl final: public Parser::Listener { std::chrono::time_point lastGarbageCollection_; Parser parser_; + std::string lastPath_; absl::optional modificationTime_ { }; + bool reloading { false }; std::array defaultCCValues_ { }; BitArray currentUsedCCs_; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index e6b245ef..23964557 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -766,7 +766,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/FilesT.cpp b/tests/FilesT.cpp index b65b9d8b..d66649f9 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -699,3 +699,19 @@ TEST_CASE("[Files] Key center from audio file") REQUIRE(synth.getRegionView(4)->pitchKeycenter == 10); REQUIRE(synth.getRegionView(5)->pitchKeycenter == 62); } + +TEST_CASE("[Files] Unused samples are cleared on reloading") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/unused_samples.sfz", R"( + sample=*sine + sample=kick.wav + )"); + REQUIRE(synth.getNumPreloadedSamples() == 1); + + // Same file path to reload + synth.loadSfzString(fs::current_path() / "tests/TestFiles/unused_samples.sfz", R"( + sample=*sine + )"); + REQUIRE(synth.getNumPreloadedSamples() == 0); +} diff --git a/tests/MidiStateT.cpp b/tests/MidiStateT.cpp index a470c0fe..57080c14 100644 --- a/tests/MidiStateT.cpp +++ b/tests/MidiStateT.cpp @@ -45,16 +45,67 @@ TEST_CASE("[MidiState] Set and get pitch bends") REQUIRE(state.getPitchBend() == 0.0f); } -TEST_CASE("[MidiState] Reset") +TEST_CASE("[MidiState] Resetting things") { sfz::MidiState state; state.pitchBendEvent(0, 0.7f); state.noteOnEvent(0, 64, 24_norm); state.ccEvent(0, 123, 124_norm); - state.reset(); - REQUIRE(state.getPitchBend() == 0.0f); + state.channelAftertouchEvent(0, 56_norm); + state.polyAftertouchEvent(0, 64, 43_norm); + state.advanceTime(1024); + + // Only reset note stuff + state.resetNoteStates(); REQUIRE(state.getNoteVelocity(64) == 0_norm); + REQUIRE(state.getNoteDuration(64) == 0_norm); + REQUIRE(state.getActiveNotes() == 0); + + // Extended CCs too + REQUIRE(state.getCCValue(131) == 0.0f); + REQUIRE(state.getCCValue(132) == 0.0f); + REQUIRE(state.getCCValue(133) == 0.0f); + REQUIRE(state.getCCValue(134) == 0.0f); + REQUIRE(state.getCCValue(135) == 0.0f); + REQUIRE(state.getCCValue(136) == 0.0f); + REQUIRE(state.getCCValue(137) == 0.0f); + + // State isn't reset + REQUIRE(state.getPitchBend() != 0.0f); + REQUIRE(state.getCCValue(123) != 0_norm); + REQUIRE(state.getChannelAftertouch() != 0_norm); + REQUIRE(state.getPolyAftertouch(64) != 0_norm); + + state.resetEventStates(); // But now it is + REQUIRE(state.getPitchBend() == 0.0f); REQUIRE(state.getCCValue(123) == 0_norm); + REQUIRE(state.getChannelAftertouch() == 0_norm); + REQUIRE(state.getPolyAftertouch(64) == 0_norm); +} + +TEST_CASE("[MidiState] Flushing state") +{ + sfz::MidiState state; + state.pitchBendEvent(40, 0.7f); + state.ccEvent(100, 123, 124_norm); + state.channelAftertouchEvent(20, 56_norm); + state.polyAftertouchEvent(80, 64, 43_norm); + + REQUIRE(state.getCCEvents(123).size() > 1); + REQUIRE(state.getChannelAftertouchEvents().size() > 1); + REQUIRE(state.getPolyAftertouchEvents(64).size() > 1); + REQUIRE(state.getPitchEvents().size() > 1); + + state.flushEvents(); + REQUIRE(state.getCCEvents(123).size() == 1); + REQUIRE(state.getChannelAftertouchEvents().size() == 1); + REQUIRE(state.getPolyAftertouchEvents(64).size() == 1); + REQUIRE(state.getPitchEvents().size() == 1); + + REQUIRE(state.getCCValue(123) == 124_norm); + REQUIRE(state.getChannelAftertouch() == 56_norm); + REQUIRE(state.getPolyAftertouch(64) == 43_norm); + REQUIRE(state.getPitchBend() == 0.7f); } TEST_CASE("[MidiState] Set and get note velocities") 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..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; @@ -1705,7 +1758,7 @@ TEST_CASE("[Synth] Initial values of CC") REQUIRE(synth.getHdcc(10) == 0.7f); REQUIRE(synth.getDefaultHdcc(10) == 0.5f); - synth.loadSfzString(fs::current_path() / "init_cc.sfz", R"( + synth.loadSfzString(fs::current_path() / "init_cc_new_file.sfz", R"( set_hdcc111=0.1234 set_cc112=77 sample=*sine )"); @@ -1964,3 +2017,126 @@ TEST_CASE("[Synth] Loading resets note and octave offsets") }; REQUIRE(messageList == expected); } + + +TEST_CASE("[Synth] Default CC values") +{ + sfz::Synth synth; + std::vector messageList; + sfz::Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/default.sfz", R"( + sample=*sine + )"); + synth.renderBlock(buffer); + synth.dispatchMessage(client, 0, "/cc7/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc10/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc11/value", "", nullptr); + + std::vector expected { + "/cc7/value,f : { 0.787402 }", + "/cc10/value,f : { 0.5 }", + "/cc11/value,f : { 1 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Synth] Loading a new file doesn't reset the midi state") +{ + sfz::Synth synth; + std::vector messageList; + sfz::Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sine.sfz", R"( + sample=*sine + )"); + + synth.hdcc(10, 63, 0.4f); + synth.hdcc(10, 7, 0.41f); + synth.hdcc(10, 10, 0.42f); + synth.hdcc(10, 11, 0.43f); + synth.hdChannelAftertouch(20, 0.1f); + synth.hdPolyAftertouch(30, 64, 0.2f); + synth.hdPitchWheel(40, 0.3f); + synth.renderBlock(buffer); + synth.dispatchMessage(client, 0, "/cc63/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc7/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc10/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc11/value", "", nullptr); + synth.dispatchMessage(client, 0, "/aftertouch", "", nullptr); + synth.dispatchMessage(client, 0, "/poly_aftertouch/64", "", nullptr); + synth.dispatchMessage(client, 0, "/pitch_bend", "", nullptr); + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/saw.sfz", R"( + sample=*saw + )"); + synth.renderBlock(buffer); + synth.dispatchMessage(client, 0, "/cc63/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc7/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc10/value", "", nullptr); + synth.dispatchMessage(client, 0, "/cc11/value", "", nullptr); + synth.dispatchMessage(client, 0, "/aftertouch", "", nullptr); + synth.dispatchMessage(client, 0, "/poly_aftertouch/64", "", nullptr); + synth.dispatchMessage(client, 0, "/pitch_bend", "", nullptr); + + std::vector expected { + "/cc63/value,f : { 0.4 }", + "/cc7/value,f : { 0.41 }", + "/cc10/value,f : { 0.42 }", + "/cc11/value,f : { 0.43 }", + "/aftertouch,f : { 0.1 }", + "/poly_aftertouch/64,f : { 0.2 }", + "/pitch_bend,f : { 0.3 }", + "/cc63/value,f : { 0.4 }", + "/cc7/value,f : { 0.41 }", + "/cc10/value,f : { 0.42 }", + "/cc11/value,f : { 0.43 }", + "/aftertouch,f : { 0.1 }", + "/poly_aftertouch/64,f : { 0.2 }", + "/pitch_bend,f : { 0.3 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Synth] Reloading a file ignores the `set_ccN` opcodes") +{ + sfz::Synth synth; + std::vector messageList; + sfz::Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sine.sfz", R"( + set_cc1=0 + sample=*sine + )"); + + synth.hdcc(10, 1, 0.4f); + synth.renderBlock(buffer); + synth.dispatchMessage(client, 0, "/cc1/value", "", nullptr); + + // Same file + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sine.sfz", R"( + set_cc1=0 + sample=*sine + )"); + synth.renderBlock(buffer); + synth.dispatchMessage(client, 0, "/cc1/value", "", nullptr); + + // Different file + synth.loadSfzString(fs::current_path() / "tests/TestFiles/saw.sfz", R"( + set_cc1=0 + sample=*saw + )"); + synth.renderBlock(buffer); + synth.dispatchMessage(client, 0, "/cc1/value", "", nullptr); + + std::vector expected { + "/cc1/value,f : { 0.4 }", + "/cc1/value,f : { 0.4 }", + "/cc1/value,f : { 0 }", + }; + + REQUIRE(messageList == expected); +}