From adc7b407297b28848babfd2c64204cd4eeff40b1 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:47:22 -0500 Subject: [PATCH 001/131] Initial commit --- cmake/modules/PluginList.cmake | 1 + plugins/SfzSampler/CMakeLists.txt | 10 +++ plugins/SfzSampler/SfzSampler.cpp | 105 ++++++++++++++++++++++++++ plugins/SfzSampler/SfzSampler.h | 64 ++++++++++++++++ plugins/SfzSampler/SfzSamplerView.cpp | 81 ++++++++++++++++++++ plugins/SfzSampler/SfzSamplerView.h | 65 ++++++++++++++++ plugins/SfzSampler/full_logo.png | Bin 0 -> 2535 bytes plugins/SfzSampler/logo.png | Bin 0 -> 759 bytes 8 files changed, 326 insertions(+) create mode 100644 plugins/SfzSampler/CMakeLists.txt create mode 100644 plugins/SfzSampler/SfzSampler.cpp create mode 100644 plugins/SfzSampler/SfzSampler.h create mode 100644 plugins/SfzSampler/SfzSamplerView.cpp create mode 100644 plugins/SfzSampler/SfzSamplerView.h create mode 100644 plugins/SfzSampler/full_logo.png create mode 100644 plugins/SfzSampler/logo.png diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 7a6b266cf34..9069a6b421b 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -61,6 +61,7 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SfzSampler SlewDistortion SlicerT SpectrumAnalyzer diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt new file mode 100644 index 00000000000..01e93b3d241 --- /dev/null +++ b/plugins/SfzSampler/CMakeLists.txt @@ -0,0 +1,10 @@ +include(BuildPlugin) + +build_plugin(sfzsampler + SfzSampler.cpp + SfzSampler.h + SfzSamplerView.cpp + SfzSamplerView.h + MOCFILES SfzSampler.h SfzSamplerView.h + EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" +) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp new file mode 100644 index 00000000000..c5f6bcd9c58 --- /dev/null +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -0,0 +1,105 @@ +/* + * SfzSampler.cpp - Simple SFZ instrument loader/editor + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SfzSampler.h" + +#include +#include +#include + +#include "Engine.h" +#include "InstrumentTrack.h" +#include "PathUtil.h" +#include "SampleLoader.h" +#include "SfzSamplerView.h" +#include "Song.h" +#include "embed.h" +#include "interpolation.h" +#include "plugin_export.h" + +namespace lmms { + +extern "C" { +Plugin::Descriptor PLUGIN_EXPORT sfzsampler_plugin_descriptor = { + LMMS_STRINGIFY(PLUGIN_NAME), + "SfzSampler", + QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), + "Daniel Kauss Serna ", + 0x0100, + Plugin::Type::Instrument, + new PluginPixmapLoader("logo"), + nullptr, + nullptr, +}; +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) +{ + return new SfzSampler(static_cast(m)); +} +} // end extern + + +SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) + : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor) + , m_originalSample() + , m_parentTrack(instrumentTrack) +{ +} + +void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) +{ + if (m_originalSample.sampleSize() <= 1) { return; } + + int noteIndex = handle->key() - m_parentTrack->baseNote(); + const fpp_t frames = handle->framesLeftForCurrentPeriod(); + const f_cnt_t offset = handle->noteOffset(); +} + +void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) +{ +} + + +void SfzSampler::loadFile(const QString& file) +{ +} + +void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) +{ +} + +void SfzSampler::loadSettings(const QDomElement& element) +{ +} + +QString SfzSampler::nodeName() const +{ + return sfzsampler_plugin_descriptor.name; +} + +gui::PluginView* SfzSampler::instantiateView(QWidget* parent) +{ + return new gui::SfzSamplerView(this, parent); +} + +} // namespace lmms diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h new file mode 100644 index 00000000000..a5a77ac9363 --- /dev/null +++ b/plugins/SfzSampler/SfzSampler.h @@ -0,0 +1,64 @@ +/* + * SfzSampler.h - Declaration of SfzSampler class, Simple SFZ instrument loader/editor + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SFZSAMPLER_H +#define LMMS_SFZSAMPLER_H + +#include "AutomatableModel.h" +#include "ComboBoxModel.h" +#include "Instrument.h" +#include "Note.h" +#include "Sample.h" +#include "SfzSamplerView.h" + +namespace lmms { + +class SfzSampler : public Instrument +{ + Q_OBJECT + +public: + SfzSampler(InstrumentTrack* instrumentTrack); + + void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; + void deleteNotePluginData(NotePlayHandle* handle) override; + + void saveSettings(QDomDocument& document, QDomElement& element) override; + void loadSettings(const QDomElement& element) override; + + void loadFile(const QString& file) override; + + QString nodeName() const override; + gui::PluginView* instantiateView(QWidget* parent) override; + +private: + + Sample m_originalSample; + + InstrumentTrack* m_parentTrack; + + friend class gui::SfzSamplerView; +}; +} // namespace lmms +#endif // LMMS_SFZSAMPLER_H diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp new file mode 100644 index 00000000000..40256a14d85 --- /dev/null +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -0,0 +1,81 @@ +/* + * SfzSamplerView.cpp - Controls View for SfzSampler + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SfzSamplerView.h" + +#include +#include +#include + +#include "Clipboard.h" +#include "ComboBox.h" +#include "DataFile.h" +#include "InstrumentView.h" +#include "Knob.h" +#include "LcdSpinBox.h" +#include "PixmapButton.h" +#include "SampleLoader.h" +#include "SfzSampler.h" +#include "StringPairDrag.h" +#include "Track.h" +#include "embed.h" + +namespace lmms { + +namespace gui { + +SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) + : InstrumentView(instrument, parent) + , m_instrument(instrument) +{ + // window settings + setAcceptDrops(true); + setAutoFillBackground(true); + + setMaximumSize(QSize(10000, 10000)); + setMinimumSize(QSize(516, 400)); + + update(); +} + + +void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) +{ +} + +void SfzSamplerView::dropEvent(QDropEvent* de) +{ +} + +void SfzSamplerView::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); +} + +void SfzSamplerView::resizeEvent(QResizeEvent* re) +{ +} + +} // namespace gui +} // namespace lmms diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h new file mode 100644 index 00000000000..cc39efb2efd --- /dev/null +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -0,0 +1,65 @@ +/* + * SfzSamplerView.h - Declaration of class SfzSamplerView + * + * Copyright (c) 2023 Daniel Kauss Serna + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_SFZSAMPLER_VIEW_H +#define LMMS_GUI_SFZSAMPLER_VIEW_H + + +#include "InstrumentView.h" + +class QPushButton; + +namespace lmms { + +class SfzSampler; + +namespace gui { + +class ComboBox; +class Knob; +class LcdSpinBox; +class PixmapButton; + +class SfzSamplerView : public InstrumentView +{ + Q_OBJECT + +public: + SfzSamplerView(SfzSampler* instrument, QWidget* parent); + +protected: + void dragEnterEvent(QDragEnterEvent* dee) override; + void dropEvent(QDropEvent* de) override; + + void paintEvent(QPaintEvent* pe) override; + void resizeEvent(QResizeEvent* event) override; + +private: + bool isResizable() const override { return true; } + + SfzSampler* m_instrument; +}; +} // namespace gui +} // namespace lmms +#endif // LMMS_GUI_SFZSAMPLER_VIEW_H diff --git a/plugins/SfzSampler/full_logo.png b/plugins/SfzSampler/full_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aa8c1d26a0073971e9b502b17ab0667f76d0659a GIT binary patch literal 2535 zcmVEX>4Tx04R}tkv&MmKpe$iQ^g{!4t5Z6$WX<>f>;qptwIqhlv<%x2a`*`ph-iL z;^HW{799LotU9+0Yt2!bCV&JIqBE>hzEl0u6Z503ls?%w0>9UwF+Of|bE09CV$ zbRsThbE{&{D+1_42xEvz%+%*nsU$qd*FAiEy^HcJ?{j~SkdikU;1h{wnQmCb8^qI_ zmd<&fILu0tLVQjQ9~IOScuZ9kzyiE`*9EdkmFC0OD0zt zj2sK7LWSh`!T;cQw`L(W=_Uo^K=+Gne~bV$WEE0hc?#;FB&Hk|X(P3WWmjen#Jv0|st^-Zi(k);>-jfDCn&ya5gl zfzcvmuY0^Z(AnF+XIlOJ0Iu0`zXUl&T>t<824YJ`L;wH)0002_L%V+f000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j~b64k#Nt%0=-2000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000NRNklD_%+ajpL z6&MKI1Ux`nzcB8bbI+N<4>SSLR-MVJgAuOEuPHziPzTiNYXi{Q zZw$QQ=^A4U{PdliBsAdxNmUp!6_h++B5*&Dp`ZH|mM?&_dej1K>T&!ox6>qz+?-128VP)7){4V~oiMRsoZNe7$%qk}D!L z+E)p%TJ`HhzxU+dPW^U1(ByJEeUaLy#Br0B z0vpw>^$9V?2=FUC()y?)841h*<``p^f3UCgOv#G%^?(6}G_+ovIBs}e+?`u3DIS;G z*%)p6lo)S74)Di52+elj2mK}$7O~8o_pOI2ow8VD%esN+QP)>&2b}2I+Y_ z@FH+PQ?yv5HGy+{r3*AI!0&*=`b@tWiM=rT--n!MMmd(2u8M2dns$|}T2t-w`EA$R zJ+r)>-U)uce?-@I*P#T033I@oi|-CI9a;vnMuG5ZJ!UENVH*N1?LQ~P|p__ zW7dm^uNQ+eKlFzuBGofZG^zDAa9-d3JsQ)ByWM!2B zUc~AlP+|2dWQhp&hXPv&RDS`E-0hgH$G}l|sx|P8X0ld=YrV!CX#}dKOA;`@D?uZd zmsNbvVJ}LFGZtw8snygRD1LU6cd3rBY4lrA<+1NH||yNc-M8O^l;6uiBn%v5%(#$j;A&6SMJ`{e?LGzE zw3`J_eaAOsNQTvFwXo{fE3UjXYU|to*s=ee-Uf+?{F^X~&c>L>fOqwN*?`r?n8o+9 z6cX!N)c|5&r%KTqy*@-&*S2b8JBl401BGTTCIyJ;D zwkU0JT|`VAP`$6&>=som1hyGto)r;4y7OuotZ@{ovrv5cS?}#=YVWhI=jC{6qB7%GhrDSrdc(S9j@evo7M^RyGtLtKCq1 z%;=HR6D(K~4PSX##pQW37j4zp;*I+HdPlr{Enc$dmoB&S7jwQndv}Ci>^jZj8tB6! zBCCO|A*yA-hJG327-MK|ZlU371N;9~%G;aY%?|w-#3TY>5!qpk`6sXxSU_M$T-Lq# z@!X-j2xRl+a>`4_m>cnIfV(4m#+lVvfdRk>HU1P}mZsU+1hyHZ5!f9ctU34tpkRP{ z0+rkk)eC$CRJq*FPLk3$yk8WMhu)uboW;lZs6j4Yz39O)>0j?2P zh-=pO|A}UMVp94TI9>iSkp+BBGoyTkB}=mk4}r}|O$4g6pKGSlgzk=axt;!`nfp07 zT9O|Qj8#Cg^>LIzU{iGPG4r{4kMmkQs|VV;?u1WDKLha@GO{13~A;2@8szjXir002ovPDHLkV1iv7uQ>n! literal 0 HcmV?d00001 diff --git a/plugins/SfzSampler/logo.png b/plugins/SfzSampler/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c4fabf14facdba53038995a70700bd9a14ad06 GIT binary patch literal 759 zcmVe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Li0L_t(&-tC%AXcJKsM$g1( zO9)OEDF{*!@h3D0y3#C^P!MVnbfa{kM07J11ee)}h%SO|G*S&!Qlp?kMT=Teiedz@ zRTo975~bCGDYOPFQwfc2(sA<|CJka2NjtC1x4iS1`3`sPeRqbZghMe_f}8-M@bQwM zs;awyAP@k&7V(+_l0aP7^#SvRa43dj;c8V?uLJ#nW+4cK7Y*na9!um{0Fe-jtu9S? zEe8>l%@UPGvpF*kQ%z_R*0?6T7c&x+xM2m&AOVR%0%j1e#30_Xt|hZ<5KE3I%eoo( zPU7Es00pHaUYN^suDhkOAgwJLr-ElH3h?dw7uv72vvc$QG6-p2t)IVs5b3x`lkW(t z98NiFWIC7SO6LW3Z#+uPYL}eFUMS@0?253(wHMXpk+;|%Kf6!giv;ePH~(F07;}{e z>3j8v(Xru0-LP@f4o)0CT@uUvnPzzO6CdBE$z(^!WJi{#{Z39zFz|k2Q8!%YSzi|W z{B?-Cce>cqc!;13D;vZHu|aGQ8^i{&L2M8k#G0zwf33lE4} Date: Wed, 10 Dec 2025 18:08:19 -0500 Subject: [PATCH 002/131] Temporary Commit --- plugins/SfzSampler/SfzSampler.cpp | 38 +++++++++++++++++++++++++++---- plugins/SfzSampler/SfzSampler.h | 3 ++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index c5f6bcd9c58..ca5ae049c67 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -25,12 +25,12 @@ #include "SfzSampler.h" #include -#include -#include +#include #include "Engine.h" #include "InstrumentTrack.h" #include "PathUtil.h" +#include "ConfigManager.h" #include "SampleLoader.h" #include "SfzSamplerView.h" #include "Song.h" @@ -61,22 +61,50 @@ PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor) - , m_originalSample() + , m_originalSample1() + , m_originalSample2() , m_parentTrack(instrumentTrack) { + QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; + if (auto buffer = gui::SampleLoader::createBufferFromFile(path + "As_029__F1_279-stereo.flac")) + { + m_originalSample1 = Sample(std::move(buffer)); + } + if (auto buffer = gui::SampleLoader::createBufferFromFile(path + "As_035__B1_281-stereo.flac")) + { + m_originalSample2 = Sample(std::move(buffer)); + } + + emit dataChanged(); } void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { - if (m_originalSample.sampleSize() <= 1) { return; } + //if (m_originalSample.sampleSize() <= 1) { return; } - int noteIndex = handle->key() - m_parentTrack->baseNote(); + int noteIndex = handle->key(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); + + const f_cnt_t startFrame = 0; + + if (!handle->m_pluginData) { handle->m_pluginData = new Sample::PlaybackState(AudioResampler::Mode::Linear, startFrame); } + + auto playbackState = static_cast(handle->m_pluginData); + + if (noteIndex % 2 == 0) + { + m_originalSample1.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off); + } + else + { + m_originalSample2.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off); + } } void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) { + delete static_cast(handle->m_pluginData); } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index a5a77ac9363..8d1e06f2de0 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -54,7 +54,8 @@ class SfzSampler : public Instrument private: - Sample m_originalSample; + Sample m_originalSample1; + Sample m_originalSample2; InstrumentTrack* m_parentTrack; From 1620912128f60dfda8368e53125212de1e8fb45d Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:57:08 -0500 Subject: [PATCH 003/131] Kinda sorta working --- plugins/SfzSampler/CMakeLists.txt | 4 +- plugins/SfzSampler/SfzFormat.cpp | 12 ++ plugins/SfzSampler/SfzFormat.h | 62 ++++++ plugins/SfzSampler/SfzSampler.cpp | 302 +++++++++++++++++++++++++++++- plugins/SfzSampler/SfzSampler.h | 33 +++- 5 files changed, 407 insertions(+), 6 deletions(-) create mode 100644 plugins/SfzSampler/SfzFormat.cpp create mode 100644 plugins/SfzSampler/SfzFormat.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 01e93b3d241..6b1345fe39b 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -5,6 +5,8 @@ build_plugin(sfzsampler SfzSampler.h SfzSamplerView.cpp SfzSamplerView.h - MOCFILES SfzSampler.h SfzSamplerView.h + SfzFormat.cpp + SfzFormat.h + MOCFILES SfzSampler.h SfzSamplerView.h SfzFormat.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" ) diff --git a/plugins/SfzSampler/SfzFormat.cpp b/plugins/SfzSampler/SfzFormat.cpp new file mode 100644 index 00000000000..4985b85f172 --- /dev/null +++ b/plugins/SfzSampler/SfzFormat.cpp @@ -0,0 +1,12 @@ + + + + + + +namespace lmms { + + + + +} \ No newline at end of file diff --git a/plugins/SfzSampler/SfzFormat.h b/plugins/SfzSampler/SfzFormat.h new file mode 100644 index 00000000000..b796452c672 --- /dev/null +++ b/plugins/SfzSampler/SfzFormat.h @@ -0,0 +1,62 @@ + +#ifndef LMMS_SFZFORMAT_H +#define LMMS_SFZFORMAT_H + +#include + +#include + + +namespace lmms { + + +enum LoopMode +{ + LoopContinuous, +}; + +struct SfzSettingState +{ + std::optional sampleFile; + int sampleIndex = -1; + std::optional lokey; + std::optional hikey; + std::optional lovel; + std::optional hivel; + std::optional pitch_keycenter; + std::optional loop_mode; + + std::optional ampeg_release; + std::optional ampeg_hold; + std::optional ampeg_decay; + std::optional ampeg_sustain; +}; + +class SfzRegion +{ +public: + SfzSettingState m_settings; +}; + +class SfzGroup +{ +public: + SfzSettingState m_globalSettings; + std::vector m_regions; +}; + +class SfzSettings +{ +public: + SfzSettingState m_globalSettings; + std::vector m_groups; +}; + + + +} + + + + +#endif // LMMS_SFZFORMAT_H diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index ca5ae049c67..09f0436a404 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -28,6 +28,7 @@ #include #include "Engine.h" +#include "InstrumentPlayHandle.h" #include "InstrumentTrack.h" #include "PathUtil.h" #include "ConfigManager.h" @@ -59,10 +60,44 @@ PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) } // end extern + + +int keyStringToInt(QString keyString) +{ + bool ok; + int asInt = keyString.toInt(&ok); + if (ok) { return asInt; } + qDebug() << "yyayyyy"; + QString octaveString = keyString.right(1); + QString keyName = keyString.chopped(1).toLower(); + qDebug() << octaveString << keyName; + bool ok2; + int octave = octaveString.toInt(&ok2); + if (!ok2) { qDebug() << "AAAA bad octave"; return 0; } + int keyOffset = 0; + if (keyName == "a") { keyOffset = 0; } + else if (keyName == "a#" || keyName == "bb") { keyOffset = 1; } + else if (keyName == "b") { keyOffset = 2; } + else if (keyName == "c") { keyOffset = 3; } + else if (keyName == "c#" || keyName == "db") { keyOffset = 4; } + else if (keyName == "d") { keyOffset = 5; } + else if (keyName == "d#" || keyName == "eb") { keyOffset = 6; } + else if (keyName == "e") { keyOffset = 7; } + else if (keyName == "f") { keyOffset = 8; } + else if (keyName == "f#" || keyName == "gb") { keyOffset = 9; } + else if (keyName == "g") { keyOffset = 10; } + else if (keyName == "g#" || keyName == "ab") { keyOffset = 11; } + else { qDebug() << "AAAA bad key"; return 0; } + qDebug() << "returning" << 21 + octave * 12 + keyOffset; + return 21 + octave * 12 + keyOffset; +} + + SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) - : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor) + : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased) , m_originalSample1() , m_originalSample2() + , m_tempBuffer(new SampleFrame[Engine::audioEngine()->framesPerPeriod()]) , m_parentTrack(instrumentTrack) { QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; @@ -75,10 +110,270 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) m_originalSample2 = Sample(std::move(buffer)); } + QFile file(path + "_jRhodes-stereo-looped.sfz"); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } + + bool inGlobal = false; + bool inGroup = false; + bool inRegion = false; + + while (!file.atEnd()) + { + QString line = file.readLine(); + // Trim comments off end + line = line.split("//")[0]; + line = line.trimmed(); + qDebug() << line; + // Split by "=" TODO make better + QStringList segments = line.split("="); + if (segments.length() == 0) { continue; } + if (segments.length() == 1) + { + if (line == "") + { + qDebug() << "Global!"; + inGlobal = true; + inGroup = false; + inRegion = false; + } + if (line == "") + { + qDebug() << "New Group!"; + inGlobal = false; + inGroup = true; + inRegion = false; + m_sfz.m_groups.push_back(SfzGroup()); + } + if (line == "") + { + qDebug() << "New Region!"; + inGlobal = false; + inGroup = false; + inRegion = true; + m_sfz.m_groups.back().m_regions.push_back(SfzRegion()); + } + } + if (segments.length() == 2) + { + QString opcode = segments[0]; + QString value = segments[1]; + SfzSettingState& currentSettingsState = inGlobal + ? m_sfz.m_globalSettings + : inGroup + ? m_sfz.m_groups.back().m_globalSettings + : m_sfz.m_groups.back().m_regions.back().m_settings; + if (opcode == "sample") + { + currentSettingsState.sampleFile = value; + qDebug() << "LOADING SAMPLE!!!" << value; + if (auto buffer = gui::SampleLoader::createBufferFromFile(path + value)) + { + m_samples.push_back(Sample(std::move(buffer))); + currentSettingsState.sampleIndex = m_samples.size() - 1; + } + } + else if (opcode == "lokey") + { + currentSettingsState.lokey = keyStringToInt(value); + } + else if (opcode == "hikey") + { + currentSettingsState.hikey = keyStringToInt(value); + } + else if (opcode == "pitch_keycenter") + { + currentSettingsState.pitch_keycenter = keyStringToInt(value); + } + else if (opcode == "lovel") + { + currentSettingsState.lovel = value.toInt(); + } + else if (opcode == "hivel") + { + currentSettingsState.hivel = value.toInt(); + } + else if (opcode == "loop_mode") + { + if (value == "loop_continuous") { currentSettingsState.loop_mode = LoopMode::LoopContinuous; } + else { qDebug() << "Oops loop val"; } + } + else if (opcode == "ampeg_hold") + { + currentSettingsState.ampeg_hold = value.toFloat(); + } + else if (opcode == "ampeg_sustain") + { + currentSettingsState.ampeg_sustain = value.toFloat(); + } + else if (opcode == "ampeg_decay") + { + currentSettingsState.ampeg_decay = value.toFloat(); + } + else if (opcode == "ampeg_release") + { + currentSettingsState.ampeg_release = value.toFloat(); + } + else + { + qDebug() << "AAAAAAAA uknown opcode" << opcode << value; + } + } + + } + + qDebug() << "Yay!" << m_sfz.m_groups.size(); + for (auto& group : m_sfz.m_groups) + { + qDebug() << "wooo" << group.m_regions.size(); + } + + auto iph = new InstrumentPlayHandle(this, instrumentTrack); + Engine::audioEngine()->addPlayHandle( iph ); + emit dataChanged(); } -void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) + +SfzSampler::~SfzSampler() +{ + delete[] m_tempBuffer; +} + + + +bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) +{ + if (event.type() == MidiNoteOn) + { + int key = event.key(); + int velocity = event.velocity(); + m_noteStates[key].pressed = true; + m_noteStates[key].velocity = velocity; + m_noteStates[key].frameCounter = -offset; + m_noteStates[key].playbackState = Sample::PlaybackState(AudioResampler::Mode::Linear); + qDebug() << "Note on!" << key << velocity; + } + if (event.type() == MidiNoteOff) + { + int key = event.key(); + int velocity = event.velocity(); + m_noteStates[key].pressed = false; + m_noteStates[key].velocity = velocity; + m_noteStates[key].frameReleased = m_noteStates[key].frameCounter; + qDebug() << "Note off!" << key << velocity; + } + + float sampleRate = Engine::audioEngine()->outputSampleRate(); + + // Loop through all regions, and find ones which match the current state + // If they match, play them + if (event.type() == MidiNoteOn || event.type() == MidiNoteOff) + { + int key = event.key(); + int velocity = event.velocity(); + for (auto& group: m_sfz.m_groups) + { + for (auto& region: group.m_regions) + { + int lokey = region.m_settings.lokey.value_or(group.m_globalSettings.lokey.value_or(m_sfz.m_globalSettings.lokey.value_or(0))); + int hikey = region.m_settings.hikey.value_or(group.m_globalSettings.hikey.value_or(m_sfz.m_globalSettings.hikey.value_or(0))); + int pitch_keycenter = region.m_settings.pitch_keycenter.value_or(group.m_globalSettings.pitch_keycenter.value_or(m_sfz.m_globalSettings.pitch_keycenter.value_or(0))); + int lovel = region.m_settings.lovel.value_or(group.m_globalSettings.lovel.value_or(m_sfz.m_globalSettings.lovel.value_or(255))); + int hivel = region.m_settings.hivel.value_or(group.m_globalSettings.hivel.value_or(m_sfz.m_globalSettings.hivel.value_or(0))); + float ampeg_hold = region.m_settings.ampeg_hold.value_or(group.m_globalSettings.ampeg_hold.value_or(m_sfz.m_globalSettings.ampeg_hold.value_or(0))); + float ampeg_decay = region.m_settings.ampeg_decay.value_or(group.m_globalSettings.ampeg_decay.value_or(m_sfz.m_globalSettings.ampeg_decay.value_or(0))); + float ampeg_sustain = region.m_settings.ampeg_sustain.value_or(group.m_globalSettings.ampeg_sustain.value_or(m_sfz.m_globalSettings.ampeg_sustain.value_or(0))); + float ampeg_release = region.m_settings.ampeg_release.value_or(group.m_globalSettings.ampeg_release.value_or(m_sfz.m_globalSettings.ampeg_release.value_or(0))); + //qDebug() << "Checking!" << lokey << hikey << pitch_keycenter << lovel << hivel; + if (key >= lokey && key <= hikey && velocity >= lovel && velocity <= hivel) + { + qDebug() << "Found a match!!!" << region.m_settings.lokey.value_or(-1) << region.m_settings.hikey.value_or(-1) << region.m_settings.lovel.value_or(-1) << region.m_settings.hivel.value_or(-1) << region.m_settings.sampleFile.value_or("idk"); + m_noteStates[key].sampleIndex = region.m_settings.sampleIndex; + int keydiff = key - pitch_keycenter; + double pitchRatio = std::exp2(-keydiff / 12.0); + m_noteStates[key].pitchRatio = pitchRatio; + qDebug() << "Pitch ratio:" << pitchRatio; + m_noteStates[key].holdFrames = ampeg_hold * sampleRate; + m_noteStates[key].decayFrames = ampeg_decay * sampleRate; + m_noteStates[key].sustainLevel = ampeg_sustain / 100.0f; + m_noteStates[key].releaseFrames = ampeg_release * sampleRate; + } + } + } + } + + return true; +} + + +void SfzSampler::play(SampleFrame* workingBuffer) +{ + const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); + for (int key = 0; key < 128; ++key) + { + //m_noteStates[key].playbackState + f_cnt_t offsetFrames = 0; + if (m_noteStates[key].frameCounter < 0 && frames - m_noteStates[key].frameCounter > 0) + { + offsetFrames = frames - m_noteStates[key].frameCounter; + } + if (m_noteStates[key].sampleIndex != -1) + { + // don't play if past release + if (!m_noteStates[key].pressed && m_noteStates[key].frameCounter - m_noteStates[key].frameReleased > m_noteStates[key].releaseFrames) { continue; } + + m_samples.at(m_noteStates[key].sampleIndex).play(m_tempBuffer + offsetFrames, &m_noteStates[key].playbackState, frames - offsetFrames, Sample::Loop::Off, m_noteStates[key].pitchRatio); + + for (f_cnt_t f = 0; f < frames; ++f) + { + float ampeg_multiplier = 1.0f; + if (m_noteStates[key].pressed) + { + if (m_noteStates[key].frameCounter > 0 && m_noteStates[key].frameCounter < 0) // TODO attack + { + ampeg_multiplier = 1.0f; // TODO + //qDebug() << "Attack" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; + } + else if (m_noteStates[key].frameCounter < m_noteStates[key].holdFrames) + { + ampeg_multiplier = 1.0f; + //qDebug() << "Hold" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; + } + else if (m_noteStates[key].frameCounter < m_noteStates[key].holdFrames + m_noteStates[key].decayFrames) + { + ampeg_multiplier = 1.0f - (1.0f - m_noteStates[key].sustainLevel) * 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames; + //qDebug() << 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames << ampeg_multiplier; + //qDebug() << "Decay" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; + } + else + { + ampeg_multiplier = m_noteStates[key].sustainLevel; + //qDebug() << "Sustain" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; + } + } + else + { + if (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased < m_noteStates[key].releaseFrames) + { + ampeg_multiplier = 1.0f - 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased) / m_noteStates[key].releaseFrames; + //qDebug() << "Release" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; + } + else + { + ampeg_multiplier = 0.0f; + } + } + workingBuffer[f] += m_tempBuffer[f] * ampeg_multiplier; + m_noteStates[key].frameCounter += 1; + } + } + } +} + + + +/*void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { //if (m_originalSample.sampleSize() <= 1) { return; } @@ -92,6 +387,7 @@ void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) auto playbackState = static_cast(handle->m_pluginData); + if (noteIndex % 2 == 0) { m_originalSample1.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off); @@ -105,7 +401,7 @@ void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) { delete static_cast(handle->m_pluginData); -} +}*/ void SfzSampler::loadFile(const QString& file) diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 8d1e06f2de0..7b6fe969bf1 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -31,6 +31,7 @@ #include "Note.h" #include "Sample.h" #include "SfzSamplerView.h" +#include "SfzFormat.h" namespace lmms { @@ -40,9 +41,13 @@ class SfzSampler : public Instrument public: SfzSampler(InstrumentTrack* instrumentTrack); + ~SfzSampler(); - void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; - void deleteNotePluginData(NotePlayHandle* handle) override; + //void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; + //void deleteNotePluginData(NotePlayHandle* handle) override; + + void play(SampleFrame* workingBuffer) override; + bool handleMidiEvent(const MidiEvent& event, const TimePos& time = TimePos(), f_cnt_t offset = 0) override; void saveSettings(QDomDocument& document, QDomElement& element) override; void loadSettings(const QDomElement& element) override; @@ -57,8 +62,32 @@ class SfzSampler : public Instrument Sample m_originalSample1; Sample m_originalSample2; + SampleFrame* m_tempBuffer; + + std::vector m_samples; + + SfzSettings m_sfz; + InstrumentTrack* m_parentTrack; + struct NoteState + { + bool pressed = false; + int velocity = 0; + int frameCounter = 0; + int frameReleased = 0; + int sampleIndex = -1; + double pitchRatio = 1.0; + int attackFrames = 0; + int holdFrames = 0; + int decayFrames = 0; + float sustainLevel = 1.0f; + int releaseFrames = 0; + Sample::PlaybackState playbackState; + }; + + std::array m_noteStates; + friend class gui::SfzSamplerView; }; } // namespace lmms From 2469ea1dca890fb89a45b979faef8204d68b0520 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:14:29 -0500 Subject: [PATCH 004/131] Temp commit --- plugins/SfzSampler/SfzSampler.cpp | 266 +++++++++++++------------- plugins/SfzSampler/SfzSamplerView.cpp | 18 ++ 2 files changed, 151 insertions(+), 133 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 09f0436a404..c0337fd5ebe 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -101,132 +101,8 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) , m_parentTrack(instrumentTrack) { QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; - if (auto buffer = gui::SampleLoader::createBufferFromFile(path + "As_029__F1_279-stereo.flac")) - { - m_originalSample1 = Sample(std::move(buffer)); - } - if (auto buffer = gui::SampleLoader::createBufferFromFile(path + "As_035__B1_281-stereo.flac")) - { - m_originalSample2 = Sample(std::move(buffer)); - } - - QFile file(path + "_jRhodes-stereo-looped.sfz"); - - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } - - bool inGlobal = false; - bool inGroup = false; - bool inRegion = false; - - while (!file.atEnd()) - { - QString line = file.readLine(); - // Trim comments off end - line = line.split("//")[0]; - line = line.trimmed(); - qDebug() << line; - // Split by "=" TODO make better - QStringList segments = line.split("="); - if (segments.length() == 0) { continue; } - if (segments.length() == 1) - { - if (line == "") - { - qDebug() << "Global!"; - inGlobal = true; - inGroup = false; - inRegion = false; - } - if (line == "") - { - qDebug() << "New Group!"; - inGlobal = false; - inGroup = true; - inRegion = false; - m_sfz.m_groups.push_back(SfzGroup()); - } - if (line == "") - { - qDebug() << "New Region!"; - inGlobal = false; - inGroup = false; - inRegion = true; - m_sfz.m_groups.back().m_regions.push_back(SfzRegion()); - } - } - if (segments.length() == 2) - { - QString opcode = segments[0]; - QString value = segments[1]; - SfzSettingState& currentSettingsState = inGlobal - ? m_sfz.m_globalSettings - : inGroup - ? m_sfz.m_groups.back().m_globalSettings - : m_sfz.m_groups.back().m_regions.back().m_settings; - if (opcode == "sample") - { - currentSettingsState.sampleFile = value; - qDebug() << "LOADING SAMPLE!!!" << value; - if (auto buffer = gui::SampleLoader::createBufferFromFile(path + value)) - { - m_samples.push_back(Sample(std::move(buffer))); - currentSettingsState.sampleIndex = m_samples.size() - 1; - } - } - else if (opcode == "lokey") - { - currentSettingsState.lokey = keyStringToInt(value); - } - else if (opcode == "hikey") - { - currentSettingsState.hikey = keyStringToInt(value); - } - else if (opcode == "pitch_keycenter") - { - currentSettingsState.pitch_keycenter = keyStringToInt(value); - } - else if (opcode == "lovel") - { - currentSettingsState.lovel = value.toInt(); - } - else if (opcode == "hivel") - { - currentSettingsState.hivel = value.toInt(); - } - else if (opcode == "loop_mode") - { - if (value == "loop_continuous") { currentSettingsState.loop_mode = LoopMode::LoopContinuous; } - else { qDebug() << "Oops loop val"; } - } - else if (opcode == "ampeg_hold") - { - currentSettingsState.ampeg_hold = value.toFloat(); - } - else if (opcode == "ampeg_sustain") - { - currentSettingsState.ampeg_sustain = value.toFloat(); - } - else if (opcode == "ampeg_decay") - { - currentSettingsState.ampeg_decay = value.toFloat(); - } - else if (opcode == "ampeg_release") - { - currentSettingsState.ampeg_release = value.toFloat(); - } - else - { - qDebug() << "AAAAAAAA uknown opcode" << opcode << value; - } - } - } - - qDebug() << "Yay!" << m_sfz.m_groups.size(); - for (auto& group : m_sfz.m_groups) - { - qDebug() << "wooo" << group.m_regions.size(); - } + //loadFile(path + "_jRhodes-stereo-looped.sfz"); auto iph = new InstrumentPlayHandle(this, instrumentTrack); Engine::audioEngine()->addPlayHandle( iph ); @@ -259,7 +135,7 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ int key = event.key(); int velocity = event.velocity(); m_noteStates[key].pressed = false; - m_noteStates[key].velocity = velocity; + //m_noteStates[key].velocity = velocity; m_noteStates[key].frameReleased = m_noteStates[key].frameCounter; qDebug() << "Note off!" << key << velocity; } @@ -328,27 +204,28 @@ void SfzSampler::play(SampleFrame* workingBuffer) for (f_cnt_t f = 0; f < frames; ++f) { float ampeg_multiplier = 1.0f; + ampeg_multiplier *= (m_noteStates[key].velocity / 127.0f); if (m_noteStates[key].pressed) { if (m_noteStates[key].frameCounter > 0 && m_noteStates[key].frameCounter < 0) // TODO attack { - ampeg_multiplier = 1.0f; // TODO + ampeg_multiplier *= 1.0f; // TODO //qDebug() << "Attack" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; } else if (m_noteStates[key].frameCounter < m_noteStates[key].holdFrames) { - ampeg_multiplier = 1.0f; + ampeg_multiplier *= 1.0f; //qDebug() << "Hold" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; } else if (m_noteStates[key].frameCounter < m_noteStates[key].holdFrames + m_noteStates[key].decayFrames) { - ampeg_multiplier = 1.0f - (1.0f - m_noteStates[key].sustainLevel) * 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames; + ampeg_multiplier *= 1.0f - (1.0f - m_noteStates[key].sustainLevel) * 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames; //qDebug() << 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames << ampeg_multiplier; //qDebug() << "Decay" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; } else { - ampeg_multiplier = m_noteStates[key].sustainLevel; + ampeg_multiplier *= m_noteStates[key].sustainLevel; //qDebug() << "Sustain" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; } } @@ -356,12 +233,12 @@ void SfzSampler::play(SampleFrame* workingBuffer) { if (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased < m_noteStates[key].releaseFrames) { - ampeg_multiplier = 1.0f - 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased) / m_noteStates[key].releaseFrames; + ampeg_multiplier *= 1.0f - 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased) / m_noteStates[key].releaseFrames; //qDebug() << "Release" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; } else { - ampeg_multiplier = 0.0f; + ampeg_multiplier *= 0.0f; } } workingBuffer[f] += m_tempBuffer[f] * ampeg_multiplier; @@ -404,8 +281,131 @@ void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) }*/ -void SfzSampler::loadFile(const QString& file) +void SfzSampler::loadFile(const QString& filepath) { + + m_samples.clear(); + + QString parentDirectory = QFileInfo(filepath).absoluteDir().path() + "/"; + qDebug() << "File!" << filepath; + qDebug() << "Dir!" << parentDirectory; + QFile file(filepath); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Could not read!!"; return; } + + bool inGlobal = false; + bool inGroup = false; + bool inRegion = false; + + while (!file.atEnd()) + { + QString line = file.readLine(); + // Trim comments off end + line = line.split("//")[0]; + line = line.trimmed(); + qDebug() << line; + // Split by "=" TODO make better + QStringList segments = line.split("="); + if (segments.length() == 0) { continue; } + if (segments.length() == 1) + { + if (line == "") + { + qDebug() << "Global!"; + inGlobal = true; + inGroup = false; + inRegion = false; + } + if (line == "") + { + qDebug() << "New Group!"; + inGlobal = false; + inGroup = true; + inRegion = false; + m_sfz.m_groups.push_back(SfzGroup()); + } + if (line == "") + { + qDebug() << "New Region!"; + inGlobal = false; + inGroup = false; + inRegion = true; + m_sfz.m_groups.back().m_regions.push_back(SfzRegion()); + } + } + if (segments.length() == 2) + { + QString opcode = segments[0]; + QString value = segments[1]; + SfzSettingState& currentSettingsState = inGlobal + ? m_sfz.m_globalSettings + : inGroup + ? m_sfz.m_groups.back().m_globalSettings + : m_sfz.m_groups.back().m_regions.back().m_settings; + if (opcode == "sample") + { + currentSettingsState.sampleFile = value; + qDebug() << "LOADING SAMPLE!!!" << value; + if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory + value)) + { + m_samples.push_back(Sample(std::move(buffer))); + currentSettingsState.sampleIndex = m_samples.size() - 1; + } + } + else if (opcode == "lokey") + { + currentSettingsState.lokey = keyStringToInt(value); + } + else if (opcode == "hikey") + { + currentSettingsState.hikey = keyStringToInt(value); + } + else if (opcode == "pitch_keycenter") + { + currentSettingsState.pitch_keycenter = keyStringToInt(value); + } + else if (opcode == "lovel") + { + currentSettingsState.lovel = value.toInt(); + } + else if (opcode == "hivel") + { + currentSettingsState.hivel = value.toInt(); + } + else if (opcode == "loop_mode") + { + if (value == "loop_continuous") { currentSettingsState.loop_mode = LoopMode::LoopContinuous; } + else { qDebug() << "Oops loop val"; } + } + else if (opcode == "ampeg_hold") + { + currentSettingsState.ampeg_hold = value.toFloat(); + } + else if (opcode == "ampeg_sustain") + { + currentSettingsState.ampeg_sustain = value.toFloat(); + } + else if (opcode == "ampeg_decay") + { + currentSettingsState.ampeg_decay = value.toFloat(); + } + else if (opcode == "ampeg_release") + { + currentSettingsState.ampeg_release = value.toFloat(); + } + else + { + qDebug() << "AAAAAAAA uknown opcode" << opcode << value; + } + } + + } + + qDebug() << "Yay!" << m_sfz.m_groups.size(); + for (auto& group : m_sfz.m_groups) + { + qDebug() << "wooo" << group.m_regions.size(); + } } void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 40256a14d85..3c32d5f2bdc 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -40,6 +40,10 @@ #include "StringPairDrag.h" #include "Track.h" #include "embed.h" +#include "ConfigManager.h" +#include "FileDialog.h" +#include "PathUtil.h" +#include "embed.h" namespace lmms { @@ -56,6 +60,20 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) setMaximumSize(QSize(10000, 10000)); setMinimumSize(QSize(516, 400)); + auto openfilebutton = new QPushButton("Open SFZ File", this); + openfilebutton->setIcon(embed::getIconPixmap("folder")); + openfilebutton->setToolTip(tr("Open SFZ File")); + connect(openfilebutton, &PixmapButton::clicked, [this](){ + auto openFileDialog = FileDialog(nullptr, QObject::tr("Open SFZ File")); + auto dir = ConfigManager::inst()->userSamplesDir(); + openFileDialog.setDirectory(dir); + if (openFileDialog.exec() == QDialog::Accepted) + { + if (openFileDialog.selectedFiles().isEmpty()) { return; } + m_instrument->loadFile(openFileDialog.selectedFiles()[0]); + } + }); + update(); } From 7762bac3cfee1d3022dfacbba51745e044e9adeb Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:58:47 -0500 Subject: [PATCH 005/131] Fix segfault --- plugins/SfzSampler/SfzSampler.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index c0337fd5ebe..ba84a9a41a3 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -190,9 +190,13 @@ void SfzSampler::play(SampleFrame* workingBuffer) { //m_noteStates[key].playbackState f_cnt_t offsetFrames = 0; - if (m_noteStates[key].frameCounter < 0 && frames - m_noteStates[key].frameCounter > 0) + if (m_noteStates[key].frameCounter < 0 && m_noteStates[key].frameCounter + frames > 0) { - offsetFrames = frames - m_noteStates[key].frameCounter; + offsetFrames = m_noteStates[key].frameCounter + frames; + } + else if (m_noteStates[key].frameCounter && m_noteStates[key].frameCounter + frames < 0) + { + continue; } if (m_noteStates[key].sampleIndex != -1) { From ed38cb02c3ebb574f6c4186bf7542db062609098 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 17 Dec 2025 07:07:59 -0500 Subject: [PATCH 006/131] Temporary commit --- plugins/SfzSampler/SfzFormat.cpp | 12 -- plugins/SfzSampler/SfzFormat.h | 62 ------ plugins/SfzSampler/SfzNotePlayState.cpp | 0 plugins/SfzSampler/SfzNotePlayState.h | 19 ++ plugins/SfzSampler/SfzOpcodeState.h | 17 ++ plugins/SfzSampler/SfzOpcodes.h | 15 ++ plugins/SfzSampler/SfzParser.cpp | 0 plugins/SfzSampler/SfzParser.h | 19 ++ plugins/SfzSampler/SfzRegion.cpp | 0 plugins/SfzSampler/SfzRegion.h | 18 ++ plugins/SfzSampler/SfzSampler.cpp | 259 +----------------------- plugins/SfzSampler/SfzSampler.h | 30 +-- 12 files changed, 98 insertions(+), 353 deletions(-) delete mode 100644 plugins/SfzSampler/SfzFormat.cpp delete mode 100644 plugins/SfzSampler/SfzFormat.h create mode 100644 plugins/SfzSampler/SfzNotePlayState.cpp create mode 100644 plugins/SfzSampler/SfzNotePlayState.h create mode 100644 plugins/SfzSampler/SfzOpcodeState.h create mode 100644 plugins/SfzSampler/SfzOpcodes.h create mode 100644 plugins/SfzSampler/SfzParser.cpp create mode 100644 plugins/SfzSampler/SfzParser.h create mode 100644 plugins/SfzSampler/SfzRegion.cpp create mode 100644 plugins/SfzSampler/SfzRegion.h diff --git a/plugins/SfzSampler/SfzFormat.cpp b/plugins/SfzSampler/SfzFormat.cpp deleted file mode 100644 index 4985b85f172..00000000000 --- a/plugins/SfzSampler/SfzFormat.cpp +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - -namespace lmms { - - - - -} \ No newline at end of file diff --git a/plugins/SfzSampler/SfzFormat.h b/plugins/SfzSampler/SfzFormat.h deleted file mode 100644 index b796452c672..00000000000 --- a/plugins/SfzSampler/SfzFormat.h +++ /dev/null @@ -1,62 +0,0 @@ - -#ifndef LMMS_SFZFORMAT_H -#define LMMS_SFZFORMAT_H - -#include - -#include - - -namespace lmms { - - -enum LoopMode -{ - LoopContinuous, -}; - -struct SfzSettingState -{ - std::optional sampleFile; - int sampleIndex = -1; - std::optional lokey; - std::optional hikey; - std::optional lovel; - std::optional hivel; - std::optional pitch_keycenter; - std::optional loop_mode; - - std::optional ampeg_release; - std::optional ampeg_hold; - std::optional ampeg_decay; - std::optional ampeg_sustain; -}; - -class SfzRegion -{ -public: - SfzSettingState m_settings; -}; - -class SfzGroup -{ -public: - SfzSettingState m_globalSettings; - std::vector m_regions; -}; - -class SfzSettings -{ -public: - SfzSettingState m_globalSettings; - std::vector m_groups; -}; - - - -} - - - - -#endif // LMMS_SFZFORMAT_H diff --git a/plugins/SfzSampler/SfzNotePlayState.cpp b/plugins/SfzSampler/SfzNotePlayState.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/SfzSampler/SfzNotePlayState.h b/plugins/SfzSampler/SfzNotePlayState.h new file mode 100644 index 00000000000..54ad5f5f03b --- /dev/null +++ b/plugins/SfzSampler/SfzNotePlayState.h @@ -0,0 +1,19 @@ + +#ifndef LMMS_SFZ_NOTE_PLAY_STATE_H +#define LMMS_SFZ_NOTE_PLAY_STATE_H + + +namespace lmms +{ + + +class SfzNotePlayState +{ + +} + + +} // namespace lmms + + +#endif // LMMS_SFZ_NOTE_PLAY_STATE_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h new file mode 100644 index 00000000000..c9af214f367 --- /dev/null +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -0,0 +1,17 @@ + +#ifndef LMMS_SFZ_OPCODE_STATE_H +#define LMMS_SFZ_OPCODE_STATE_H + + +namespace lmms +{ + +class SfzOpcodeState +{ +} + + +} // namespace lmms + + +#endif // LMMS_SFZ_OPCODE_STATE_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h new file mode 100644 index 00000000000..b4740b13c56 --- /dev/null +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -0,0 +1,15 @@ + +#ifndef LMMS_SFZ_OPCODES_H +#define LMMS_SFZ_OPCODES_H + + +namespace lmms +{ + + + + +} // namespace lmms + + +#endif // LMMS_SFZ_OPCODES_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h new file mode 100644 index 00000000000..e1a4740ea46 --- /dev/null +++ b/plugins/SfzSampler/SfzParser.h @@ -0,0 +1,19 @@ + +#ifndef LMMS_SFZ_PARSER_H +#define LMMS_SFZ_PARSER_H + + +namespace lmms +{ + +class SfzParser +{ + +} + + + +} // namespace lmms + + +#endif // LMMS_SFZ_PARSER_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h new file mode 100644 index 00000000000..a02c8350519 --- /dev/null +++ b/plugins/SfzSampler/SfzRegion.h @@ -0,0 +1,18 @@ + +#ifndef LMMS_SFZ_REGION_H +#define LMMS_SFZ_REGION_H + + +namespace lmms +{ + +class SfzRegion +{ + +} + + +} // namespace lmms + + +#endif // LMMS_SFZ_REGION_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index ba84a9a41a3..ec624514a06 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -94,7 +94,7 @@ int keyStringToInt(QString keyString) SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) - : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased) + : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) , m_originalSample1() , m_originalSample2() , m_tempBuffer(new SampleFrame[Engine::audioEngine()->framesPerPeriod()]) @@ -102,10 +102,10 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) { QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; - //loadFile(path + "_jRhodes-stereo-looped.sfz"); + loadFile(path + "_jRhodes-stereo-looped.sfz"); auto iph = new InstrumentPlayHandle(this, instrumentTrack); - Engine::audioEngine()->addPlayHandle( iph ); + Engine::audioEngine()->addPlayHandle(iph); emit dataChanged(); } @@ -124,172 +124,38 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ { int key = event.key(); int velocity = event.velocity(); - m_noteStates[key].pressed = true; - m_noteStates[key].velocity = velocity; - m_noteStates[key].frameCounter = -offset; - m_noteStates[key].playbackState = Sample::PlaybackState(AudioResampler::Mode::Linear); qDebug() << "Note on!" << key << velocity; } - if (event.type() == MidiNoteOff) - { - int key = event.key(); - int velocity = event.velocity(); - m_noteStates[key].pressed = false; - //m_noteStates[key].velocity = velocity; - m_noteStates[key].frameReleased = m_noteStates[key].frameCounter; - qDebug() << "Note off!" << key << velocity; - } - - float sampleRate = Engine::audioEngine()->outputSampleRate(); - - // Loop through all regions, and find ones which match the current state - // If they match, play them - if (event.type() == MidiNoteOn || event.type() == MidiNoteOff) - { - int key = event.key(); - int velocity = event.velocity(); - for (auto& group: m_sfz.m_groups) - { - for (auto& region: group.m_regions) - { - int lokey = region.m_settings.lokey.value_or(group.m_globalSettings.lokey.value_or(m_sfz.m_globalSettings.lokey.value_or(0))); - int hikey = region.m_settings.hikey.value_or(group.m_globalSettings.hikey.value_or(m_sfz.m_globalSettings.hikey.value_or(0))); - int pitch_keycenter = region.m_settings.pitch_keycenter.value_or(group.m_globalSettings.pitch_keycenter.value_or(m_sfz.m_globalSettings.pitch_keycenter.value_or(0))); - int lovel = region.m_settings.lovel.value_or(group.m_globalSettings.lovel.value_or(m_sfz.m_globalSettings.lovel.value_or(255))); - int hivel = region.m_settings.hivel.value_or(group.m_globalSettings.hivel.value_or(m_sfz.m_globalSettings.hivel.value_or(0))); - float ampeg_hold = region.m_settings.ampeg_hold.value_or(group.m_globalSettings.ampeg_hold.value_or(m_sfz.m_globalSettings.ampeg_hold.value_or(0))); - float ampeg_decay = region.m_settings.ampeg_decay.value_or(group.m_globalSettings.ampeg_decay.value_or(m_sfz.m_globalSettings.ampeg_decay.value_or(0))); - float ampeg_sustain = region.m_settings.ampeg_sustain.value_or(group.m_globalSettings.ampeg_sustain.value_or(m_sfz.m_globalSettings.ampeg_sustain.value_or(0))); - float ampeg_release = region.m_settings.ampeg_release.value_or(group.m_globalSettings.ampeg_release.value_or(m_sfz.m_globalSettings.ampeg_release.value_or(0))); - //qDebug() << "Checking!" << lokey << hikey << pitch_keycenter << lovel << hivel; - if (key >= lokey && key <= hikey && velocity >= lovel && velocity <= hivel) - { - qDebug() << "Found a match!!!" << region.m_settings.lokey.value_or(-1) << region.m_settings.hikey.value_or(-1) << region.m_settings.lovel.value_or(-1) << region.m_settings.hivel.value_or(-1) << region.m_settings.sampleFile.value_or("idk"); - m_noteStates[key].sampleIndex = region.m_settings.sampleIndex; - int keydiff = key - pitch_keycenter; - double pitchRatio = std::exp2(-keydiff / 12.0); - m_noteStates[key].pitchRatio = pitchRatio; - qDebug() << "Pitch ratio:" << pitchRatio; - m_noteStates[key].holdFrames = ampeg_hold * sampleRate; - m_noteStates[key].decayFrames = ampeg_decay * sampleRate; - m_noteStates[key].sustainLevel = ampeg_sustain / 100.0f; - m_noteStates[key].releaseFrames = ampeg_release * sampleRate; - } - } - } - } - - return true; } void SfzSampler::play(SampleFrame* workingBuffer) { const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - for (int key = 0; key < 128; ++key) - { - //m_noteStates[key].playbackState - f_cnt_t offsetFrames = 0; - if (m_noteStates[key].frameCounter < 0 && m_noteStates[key].frameCounter + frames > 0) - { - offsetFrames = m_noteStates[key].frameCounter + frames; - } - else if (m_noteStates[key].frameCounter && m_noteStates[key].frameCounter + frames < 0) - { - continue; - } - if (m_noteStates[key].sampleIndex != -1) - { - // don't play if past release - if (!m_noteStates[key].pressed && m_noteStates[key].frameCounter - m_noteStates[key].frameReleased > m_noteStates[key].releaseFrames) { continue; } - - m_samples.at(m_noteStates[key].sampleIndex).play(m_tempBuffer + offsetFrames, &m_noteStates[key].playbackState, frames - offsetFrames, Sample::Loop::Off, m_noteStates[key].pitchRatio); - - for (f_cnt_t f = 0; f < frames; ++f) - { - float ampeg_multiplier = 1.0f; - ampeg_multiplier *= (m_noteStates[key].velocity / 127.0f); - if (m_noteStates[key].pressed) - { - if (m_noteStates[key].frameCounter > 0 && m_noteStates[key].frameCounter < 0) // TODO attack - { - ampeg_multiplier *= 1.0f; // TODO - //qDebug() << "Attack" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; - } - else if (m_noteStates[key].frameCounter < m_noteStates[key].holdFrames) - { - ampeg_multiplier *= 1.0f; - //qDebug() << "Hold" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; - } - else if (m_noteStates[key].frameCounter < m_noteStates[key].holdFrames + m_noteStates[key].decayFrames) - { - ampeg_multiplier *= 1.0f - (1.0f - m_noteStates[key].sustainLevel) * 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames; - //qDebug() << 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].holdFrames) / m_noteStates[key].decayFrames << ampeg_multiplier; - //qDebug() << "Decay" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; - } - else - { - ampeg_multiplier *= m_noteStates[key].sustainLevel; - //qDebug() << "Sustain" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; - } - } - else - { - if (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased < m_noteStates[key].releaseFrames) - { - ampeg_multiplier *= 1.0f - 1.0f * (m_noteStates[key].frameCounter - m_noteStates[key].frameReleased) / m_noteStates[key].releaseFrames; - //qDebug() << "Release" << m_noteStates[key].frameCounter << m_noteStates[key].attackFrames << m_noteStates[key].holdFrames << m_noteStates[key].decayFrames << m_noteStates[key].sustainLevel << m_noteStates[key].releaseFrames; - } - else - { - ampeg_multiplier *= 0.0f; - } - } - workingBuffer[f] += m_tempBuffer[f] * ampeg_multiplier; - m_noteStates[key].frameCounter += 1; - } - } - } } -/*void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) +void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { - //if (m_originalSample.sampleSize() <= 1) { return; } - int noteIndex = handle->key(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); - const f_cnt_t startFrame = 0; - - if (!handle->m_pluginData) { handle->m_pluginData = new Sample::PlaybackState(AudioResampler::Mode::Linear, startFrame); } - - auto playbackState = static_cast(handle->m_pluginData); - - - if (noteIndex % 2 == 0) + if (!handle->m_pluginData) { - m_originalSample1.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off); - } - else - { - m_originalSample2.play(workingBuffer + offset, playbackState, frames, Sample::Loop::Off); + handle->m_pluginData = ; } } void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) { - delete static_cast(handle->m_pluginData); -}*/ + //delete static_cast(handle->m_pluginData); +} void SfzSampler::loadFile(const QString& filepath) { - - m_samples.clear(); - QString parentDirectory = QFileInfo(filepath).absoluteDir().path() + "/"; qDebug() << "File!" << filepath; qDebug() << "Dir!" << parentDirectory; @@ -297,118 +163,9 @@ void SfzSampler::loadFile(const QString& filepath) if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Could not read!!"; return; } - bool inGlobal = false; - bool inGroup = false; - bool inRegion = false; - while (!file.atEnd()) { QString line = file.readLine(); - // Trim comments off end - line = line.split("//")[0]; - line = line.trimmed(); - qDebug() << line; - // Split by "=" TODO make better - QStringList segments = line.split("="); - if (segments.length() == 0) { continue; } - if (segments.length() == 1) - { - if (line == "") - { - qDebug() << "Global!"; - inGlobal = true; - inGroup = false; - inRegion = false; - } - if (line == "") - { - qDebug() << "New Group!"; - inGlobal = false; - inGroup = true; - inRegion = false; - m_sfz.m_groups.push_back(SfzGroup()); - } - if (line == "") - { - qDebug() << "New Region!"; - inGlobal = false; - inGroup = false; - inRegion = true; - m_sfz.m_groups.back().m_regions.push_back(SfzRegion()); - } - } - if (segments.length() == 2) - { - QString opcode = segments[0]; - QString value = segments[1]; - SfzSettingState& currentSettingsState = inGlobal - ? m_sfz.m_globalSettings - : inGroup - ? m_sfz.m_groups.back().m_globalSettings - : m_sfz.m_groups.back().m_regions.back().m_settings; - if (opcode == "sample") - { - currentSettingsState.sampleFile = value; - qDebug() << "LOADING SAMPLE!!!" << value; - if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory + value)) - { - m_samples.push_back(Sample(std::move(buffer))); - currentSettingsState.sampleIndex = m_samples.size() - 1; - } - } - else if (opcode == "lokey") - { - currentSettingsState.lokey = keyStringToInt(value); - } - else if (opcode == "hikey") - { - currentSettingsState.hikey = keyStringToInt(value); - } - else if (opcode == "pitch_keycenter") - { - currentSettingsState.pitch_keycenter = keyStringToInt(value); - } - else if (opcode == "lovel") - { - currentSettingsState.lovel = value.toInt(); - } - else if (opcode == "hivel") - { - currentSettingsState.hivel = value.toInt(); - } - else if (opcode == "loop_mode") - { - if (value == "loop_continuous") { currentSettingsState.loop_mode = LoopMode::LoopContinuous; } - else { qDebug() << "Oops loop val"; } - } - else if (opcode == "ampeg_hold") - { - currentSettingsState.ampeg_hold = value.toFloat(); - } - else if (opcode == "ampeg_sustain") - { - currentSettingsState.ampeg_sustain = value.toFloat(); - } - else if (opcode == "ampeg_decay") - { - currentSettingsState.ampeg_decay = value.toFloat(); - } - else if (opcode == "ampeg_release") - { - currentSettingsState.ampeg_release = value.toFloat(); - } - else - { - qDebug() << "AAAAAAAA uknown opcode" << opcode << value; - } - } - - } - - qDebug() << "Yay!" << m_sfz.m_groups.size(); - for (auto& group : m_sfz.m_groups) - { - qDebug() << "wooo" << group.m_regions.size(); } } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 7b6fe969bf1..b8ed9dfc7e7 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -43,8 +43,8 @@ class SfzSampler : public Instrument SfzSampler(InstrumentTrack* instrumentTrack); ~SfzSampler(); - //void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; - //void deleteNotePluginData(NotePlayHandle* handle) override; + void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; + void deleteNotePluginData(NotePlayHandle* handle) override; void play(SampleFrame* workingBuffer) override; bool handleMidiEvent(const MidiEvent& event, const TimePos& time = TimePos(), f_cnt_t offset = 0) override; @@ -58,36 +58,10 @@ class SfzSampler : public Instrument gui::PluginView* instantiateView(QWidget* parent) override; private: - - Sample m_originalSample1; - Sample m_originalSample2; - SampleFrame* m_tempBuffer; - std::vector m_samples; - - SfzSettings m_sfz; - InstrumentTrack* m_parentTrack; - struct NoteState - { - bool pressed = false; - int velocity = 0; - int frameCounter = 0; - int frameReleased = 0; - int sampleIndex = -1; - double pitchRatio = 1.0; - int attackFrames = 0; - int holdFrames = 0; - int decayFrames = 0; - float sustainLevel = 1.0f; - int releaseFrames = 0; - Sample::PlaybackState playbackState; - }; - - std::array m_noteStates; - friend class gui::SfzSamplerView; }; } // namespace lmms From 9a75508c2139a02f63440b5062413354e8a1c43e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:48:02 -0500 Subject: [PATCH 007/131] Initial parser --- plugins/SfzSampler/SfzOpcodeState.cpp | 77 +++++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 26 ++++++ plugins/SfzSampler/SfzOpcodes.h | 34 +++++++ plugins/SfzSampler/SfzParser.cpp | 130 ++++++++++++++++++++++++++ plugins/SfzSampler/SfzParser.h | 4 + plugins/SfzSampler/SfzSampler.cpp | 3 +- 6 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 plugins/SfzSampler/SfzOpcodeState.cpp diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp new file mode 100644 index 00000000000..0f9752d55e1 --- /dev/null +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -0,0 +1,77 @@ + + + + +#include "SfzOpcodeState.h" + +namespace lmms +{ + + +bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& value) +{ + bool successful = true; + + if (name == "sample") + { + m_sample = value; + } + + else if (name == "key") + { + m_key = value.toInt(&successful); + } + else if (name == "lokey") + { + m_lokey = value.toInt(&successful); + } + else if (name == "hikey") + { + m_hikey = value.toInt(&successful); + } + + else if (name == "lovel") + { + m_lovel = value.toInt(&successful); + } + else if (name == "hivel") + { + m_hivel = value.toInt(&successful); + } + + else if (name == "pitch_keycenter") + { + m_pitch_keycenter = value.toInt(&successful); + } + + else if (name == "ampeg_delay") + { + m_ampeg_delay = value.toFloat(&successful); + } + else if (name == "ampeg_attack") + { + m_ampeg_attack = value.toFloat(&successful); + } + else if (name == "ampeg_hold") + { + m_ampeg_hold = value.toFloat(&successful); + } + else if (name == "ampeg_decay") + { + m_ampeg_decay = value.toFloat(&successful); + } + else if (name == "ampeg_sustain") + { + m_ampeg_sustain = value.toFloat(&successful); + } + else if (name == "ampeg_release") + { + m_ampeg_release = value.toFloat(&successful); + } + + return successful; +} + + + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index c9af214f367..f9c64ad750c 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -2,12 +2,38 @@ #ifndef LMMS_SFZ_OPCODE_STATE_H #define LMMS_SFZ_OPCODE_STATE_H +#include "SfzOpcodes.h" +#include +//#include namespace lmms { class SfzOpcodeState { +public: + void setOpcodeByStrings(const QString& name, const QString& value); + +private: + std::optional m_sample = std::nullopt; + + std::optional m_key = std::nullopt; + std::optional m_lokey = 0; + std::optional m_hikey = 127; + + std::optional m_lovel = 0; + std::optional m_hivel = 127; + + std::optional m_pitch_keycenter = 60; + + std::optional m_ampeg_delay = 0; + std::optional m_ampeg_attack = 0; + std::optional m_ampeg_hold = 0; + std::optional m_ampeg_decay = 0; + std::optional m_ampeg_sustain = 100; + std::optional m_ampeg_release = 0.001; + + friend class SfzRegion; } diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index b4740b13c56..c2ff10b5b64 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -2,11 +2,45 @@ #ifndef LMMS_SFZ_OPCODES_H #define LMMS_SFZ_OPCODES_H +#include namespace lmms { +enum class SfzOpcodes +{ + Sample, + + Key, + LoKey, + HiKey, + + LoVel, + HiVel, + + PitchKeyCenter, + + AmpEGDelay, + AmpEGAttack, + AmpEGHold, + AmpEGSustain, + AmpEGDecay, + AmpEGRelease, +} + + +std::map SfzOpcodeNames = { + {SfzOpcodes::Sample, "sample"}, + + {SfzOpcodes::Key, "key"}, + {SfzOpcodes::LoKey, "lokey"}, + {SfzOpcodes::HiKey, "hikey"}, + + {SfzOpcodes::LoVel, "lovel"}, + {SfzOpcodes::HiVel, "hivel"}, + {SfzOpcodes::PitchKeyCenter, "pitch_keycenter"}, +} } // namespace lmms diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index e69de29bb2d..3af6bb4aad0 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -0,0 +1,130 @@ + + +#include "SfzParser.h" +//#include + +namespace lmms +{ + + +static bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions) +{ + QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + QFile file(filePath); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Could not read!!"; return; } + + std::vector parsedSegments; + + while (!file.atEnd()) + { + QString line = file.readLine(); + // Remove comments from end of line + line = line.split("\\")[0]; + // Split the line on whitespace to extract header and opcodes keywords + // Fortunately, the SFZ format specifically states that opcode assignments cannot contain spaces, so there is no risk + // of accidentally splitting the opcode name from the value. + for (QString segment : line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts)) + { + parsedSegments.push_back(parsedSegments); + } + } + + // Now that all the segments are collected, we can go through them all and construct the SfzRegions + // First, the header(s) must be found. The SFZ format website does not guarantee that will + // be the first header, so we have to search through all the segments to find the globals before parsing any headers + + // Create a base opcode state list to keep track of which defaults are global + SfzOpcodeState globalState; + + bool withinGlobal = false + for (QString segment : parsedSegments) + { + // Track whether we are entering a header region + if (segment == "") + { + withinGlobal = true; + continue; + } + else if (segment == "" || segment == "") + { + withinGlobal = false; + continue; + } + // TODO handle more header types + + // If we are in , update the global opcode state + if (withinGlobal) + { + // Opcodes are stored in name=value format, with no spaces, so splitting on the "=" always works + auto opcodeNameAndValue = segment.split("="); // TODO add debug message if error + globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + } + } + + // Now that all the global opcode defaults have been parsed, we can start stepping through the file again and + // parsing all the s into SfzRegion objects + + // Regions can be within s, which can define default opcodes for all the regions inside them + // Keep track of the current group and region states. These will be initialized to whatever the global/group settings are, once we encounter a group ro region header + SfzOpcodeState currentGroupState; + SfzOpcodeState currentRegionState; + + // Track whether we are in global, a group, or a region header + withinGlobal = false; + bool withinGroup = false; + bool withinRegion = false; + for (segment : parsedSegments) + { + // Track whether we are entering a header region + if (segment == "") + { + // If we were previously in a , then wrap it up and add it to the output vector + if (withinRegion) { outputRegions.push_back(currentRegionState); } + withinGlobal = true; + withinGroup = false; + withinRegion = false; + // Don't do anything special; we handled the globals above + continue; + } + else if (segment == "") + { + if (withinRegion) { outputRegions.push_back(currentRegionState); } + withinGlobal = false; + withinGroup = true; + withinRegion = false; + // Reset the current group settings to the global defaults + currentGroupState = globalState; + continue; + } + else if (segment == "") + { + if (withinRegion) { outputRegions.push_back(currentRegionState); } + withinGlobal = false; + withinGroup = false; + withinRegion = true; + // Reset the current region settings to the group defaults + currentRegionState = currentGroupState; + continue; + } + // TODO handle more header types + + // If we are in a group, update the opcodes of the current group state + if (withinGroup) + { + auto opcodeNameAndValue = segment.split("="); // TODO add debug message if error + currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + } + // If we are within a region, update the opcodes of the current region state + if (withinRegion) + { + auto opcodeNameAndValue = segment.split("="); // TODO add debug message if error + currentRegionState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + } + } + + // Now that all the opcodes have been parsed into regions and added to the output vector, we are done! +} + + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index e1a4740ea46..2e2617af83d 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -2,12 +2,16 @@ #ifndef LMMS_SFZ_PARSER_H #define LMMS_SFZ_PARSER_H +#include "SfzRegion.h" +#include namespace lmms { class SfzParser { +public: + static bool parseSfzFile(const QString& filePath, std::vector& outputRegions); } diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index ec624514a06..4a9db5c41fd 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -39,7 +39,8 @@ #include "interpolation.h" #include "plugin_export.h" -namespace lmms { +namespace lmms +{ extern "C" { Plugin::Descriptor PLUGIN_EXPORT sfzsampler_plugin_descriptor = { From d9cc663a107c2fa3e5a5950551fa6bb93847ce61 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:14:16 -0500 Subject: [PATCH 008/131] Temp commit, Compiles --- plugins/SfzSampler/CMakeLists.txt | 20 +++- plugins/SfzSampler/SfzNotePlayState.cpp | 0 plugins/SfzSampler/SfzNotePlayState.h | 19 ---- plugins/SfzSampler/SfzOpcodeState.cpp | 6 ++ plugins/SfzSampler/SfzOpcodeState.h | 10 +- plugins/SfzSampler/SfzOpcodes.h | 5 +- plugins/SfzSampler/SfzParser.cpp | 114 +++++++++++++--------- plugins/SfzSampler/SfzParser.h | 4 +- plugins/SfzSampler/SfzRegion.cpp | 19 ++++ plugins/SfzSampler/SfzRegion.h | 8 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 10 ++ plugins/SfzSampler/SfzRegionPlayState.h | 19 ++++ plugins/SfzSampler/SfzSampler.cpp | 77 ++++++--------- plugins/SfzSampler/SfzSampler.h | 15 ++- 14 files changed, 198 insertions(+), 128 deletions(-) delete mode 100644 plugins/SfzSampler/SfzNotePlayState.cpp delete mode 100644 plugins/SfzSampler/SfzNotePlayState.h create mode 100644 plugins/SfzSampler/SfzRegionPlayState.cpp create mode 100644 plugins/SfzSampler/SfzRegionPlayState.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 6b1345fe39b..b02b6e28f7a 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -5,8 +5,22 @@ build_plugin(sfzsampler SfzSampler.h SfzSamplerView.cpp SfzSamplerView.h - SfzFormat.cpp - SfzFormat.h - MOCFILES SfzSampler.h SfzSamplerView.h SfzFormat.h + SfzOpcodes.h + SfzOpcodeState.cpp + SfzOpcodeState.h + SfzRegion.cpp + SfzRegion.h + SfzParser.cpp + SfzParser.h + SfzRegionPlayState.cpp + SfzRegionPlayState.h + MOCFILES + SfzSampler.h + SfzSamplerView.h + SfzOpcodes.h + SfzOpcodeState.h + SfzRegion.h + SfzParser.h + SfzRegionPlayState.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" ) diff --git a/plugins/SfzSampler/SfzNotePlayState.cpp b/plugins/SfzSampler/SfzNotePlayState.cpp deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/plugins/SfzSampler/SfzNotePlayState.h b/plugins/SfzSampler/SfzNotePlayState.h deleted file mode 100644 index 54ad5f5f03b..00000000000 --- a/plugins/SfzSampler/SfzNotePlayState.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef LMMS_SFZ_NOTE_PLAY_STATE_H -#define LMMS_SFZ_NOTE_PLAY_STATE_H - - -namespace lmms -{ - - -class SfzNotePlayState -{ - -} - - -} // namespace lmms - - -#endif // LMMS_SFZ_NOTE_PLAY_STATE_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 0f9752d55e1..c0354bb6b1e 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -3,6 +3,7 @@ #include "SfzOpcodeState.h" +#include namespace lmms { @@ -68,6 +69,11 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_ampeg_release = value.toFloat(&successful); } + else + { + qDebug() << "[SFZ Parser] Unknown opcode:" << name; + return false; + } return successful; } diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index f9c64ad750c..8af3ea74cdc 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -2,9 +2,9 @@ #ifndef LMMS_SFZ_OPCODE_STATE_H #define LMMS_SFZ_OPCODE_STATE_H -#include "SfzOpcodes.h" +//#include "SfzOpcodes.h" #include -//#include +#include namespace lmms { @@ -12,9 +12,9 @@ namespace lmms class SfzOpcodeState { public: - void setOpcodeByStrings(const QString& name, const QString& value); + bool setOpcodeByStrings(const QString& name, const QString& value); -private: +//private: std::optional m_sample = std::nullopt; std::optional m_key = std::nullopt; @@ -34,7 +34,7 @@ class SfzOpcodeState std::optional m_ampeg_release = 0.001; friend class SfzRegion; -} +}; } // namespace lmms diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index c2ff10b5b64..300991cd3e5 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -3,6 +3,7 @@ #define LMMS_SFZ_OPCODES_H #include +#include namespace lmms { @@ -26,7 +27,7 @@ enum class SfzOpcodes AmpEGSustain, AmpEGDecay, AmpEGRelease, -} +}; std::map SfzOpcodeNames = { @@ -40,7 +41,7 @@ std::map SfzOpcodeNames = { {SfzOpcodes::HiVel, "hivel"}, {SfzOpcodes::PitchKeyCenter, "pitch_keycenter"}, -} +}; } // namespace lmms diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 3af6bb4aad0..b8c13b89222 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -1,18 +1,27 @@ #include "SfzParser.h" -//#include +#include "SfzOpcodeState.h" +#include +//#include +#include +#include +#include namespace lmms { -static bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions) +bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions) { QDir parentDirectory = QFileInfo(filePath).absoluteDir(); QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Could not read!!"; return; } + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + qDebug() << "[SFZ Parser] Could not read file:" << filePath; + return false; + } std::vector parsedSegments; @@ -26,7 +35,7 @@ static bool SfzParser::parseSfzFile(const QString& filePath, std::vector header region - if (segment == "") + if (segment.front() == "<" && segment.back() == ">") { - withinGlobal = true; + if (segment == "") + { + withinGlobal = true; + } + else + { + withinGlobal = false; + } continue; } - else if (segment == "" || segment == "") - { - withinGlobal = false; - continue; - } - // TODO handle more header types // If we are in , update the global opcode state if (withinGlobal) { // Opcodes are stored in name=value format, with no spaces, so splitting on the "=" always works - auto opcodeNameAndValue = segment.split("="); // TODO add debug message if error + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); } } @@ -74,56 +85,67 @@ static bool SfzParser::parseSfzFile(const QString& filePath, std::vector header region - if (segment == "") - { - // If we were previously in a , then wrap it up and add it to the output vector - if (withinRegion) { outputRegions.push_back(currentRegionState); } - withinGlobal = true; - withinGroup = false; - withinRegion = false; - // Don't do anything special; we handled the globals above - continue; - } - else if (segment == "") - { - if (withinRegion) { outputRegions.push_back(currentRegionState); } - withinGlobal = false; - withinGroup = true; - withinRegion = false; - // Reset the current group settings to the global defaults - currentGroupState = globalState; - continue; - } - else if (segment == "") + // Track whether we are entering a new header region + if (segment.front() == "<" && segment.back() == ">") { - if (withinRegion) { outputRegions.push_back(currentRegionState); } - withinGlobal = false; - withinGroup = false; - withinRegion = true; - // Reset the current region settings to the group defaults - currentRegionState = currentGroupState; + if (segment == "") + { + // If we were previously in a , then wrap it up and add it to the output vector + if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + withinGlobal = true; + withinGroup = false; + withinRegion = false; + // Don't do anything special; we already handled the globals above + } + else if (segment == "") + { + if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + withinGlobal = false; + withinGroup = true; + withinRegion = false; + // Reset the current group settings to the global defaults + currentGroupState = globalState; + } + else if (segment == "") + { + if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + withinGlobal = false; + withinGroup = false; + withinRegion = true; + // Reset the current region settings to the group defaults + currentRegionState = currentGroupState; + } + else + { + qDebug() << "[SFZ Parser] Unknown header type:" << segment; + return false; + } continue; + // TODO handle more header types } - // TODO handle more header types // If we are in a group, update the opcodes of the current group state if (withinGroup) { - auto opcodeNameAndValue = segment.split("="); // TODO add debug message if error + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); } // If we are within a region, update the opcodes of the current region state if (withinRegion) { - auto opcodeNameAndValue = segment.split("="); // TODO add debug message if error + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } currentRegionState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); } } + // Check one last time in case the file ended with a region and didn't get added + if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } // Now that all the opcodes have been parsed into regions and added to the output vector, we are done! + return true; } diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index 2e2617af83d..bd6ef0fb2a2 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -4,6 +4,7 @@ #include "SfzRegion.h" #include +#include namespace lmms { @@ -12,8 +13,7 @@ class SfzParser { public: static bool parseSfzFile(const QString& filePath, std::vector& outputRegions); - -} +}; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index e69de29bb2d..e49a4c4c3f7 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -0,0 +1,19 @@ + + + + + + +#include "SfzRegion.h" + +namespace lmms +{ + + +SfzRegion::SfzRegion(SfzOpcodeState opcodeState) + : SfzOpcodeState(opcodeState) +{ +} + + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index a02c8350519..dc6ce24d767 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -2,14 +2,16 @@ #ifndef LMMS_SFZ_REGION_H #define LMMS_SFZ_REGION_H +#include "SfzOpcodeState.h" namespace lmms { -class SfzRegion +class SfzRegion : public SfzOpcodeState { - -} +public: + SfzRegion(SfzOpcodeState opcodeState); +}; } // namespace lmms diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp new file mode 100644 index 00000000000..2c637c32fe9 --- /dev/null +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -0,0 +1,10 @@ + + +#include "SfzRegionPlayState.h" + +namespace lmms +{ + + + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h new file mode 100644 index 00000000000..b02e15bd766 --- /dev/null +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -0,0 +1,19 @@ + +#ifndef LMMS_SFZ_REGION_PLAY_STATE_H +#define LMMS_SFZ_REGION_PLAY_STATE_H + + +namespace lmms +{ + + +class SfzRegionPlayState +{ + +}; + + +} // namespace lmms + + +#endif // LMMS_SFZ_REGION_PLAY_STATE_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 4a9db5c41fd..c75239bf899 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -23,6 +23,7 @@ */ #include "SfzSampler.h" +#include "SfzParser.h" #include #include @@ -61,43 +62,8 @@ PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) } // end extern - - -int keyStringToInt(QString keyString) -{ - bool ok; - int asInt = keyString.toInt(&ok); - if (ok) { return asInt; } - qDebug() << "yyayyyy"; - QString octaveString = keyString.right(1); - QString keyName = keyString.chopped(1).toLower(); - qDebug() << octaveString << keyName; - bool ok2; - int octave = octaveString.toInt(&ok2); - if (!ok2) { qDebug() << "AAAA bad octave"; return 0; } - int keyOffset = 0; - if (keyName == "a") { keyOffset = 0; } - else if (keyName == "a#" || keyName == "bb") { keyOffset = 1; } - else if (keyName == "b") { keyOffset = 2; } - else if (keyName == "c") { keyOffset = 3; } - else if (keyName == "c#" || keyName == "db") { keyOffset = 4; } - else if (keyName == "d") { keyOffset = 5; } - else if (keyName == "d#" || keyName == "eb") { keyOffset = 6; } - else if (keyName == "e") { keyOffset = 7; } - else if (keyName == "f") { keyOffset = 8; } - else if (keyName == "f#" || keyName == "gb") { keyOffset = 9; } - else if (keyName == "g") { keyOffset = 10; } - else if (keyName == "g#" || keyName == "ab") { keyOffset = 11; } - else { qDebug() << "AAAA bad key"; return 0; } - qDebug() << "returning" << 21 + octave * 12 + keyOffset; - return 21 + octave * 12 + keyOffset; -} - - SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) - , m_originalSample1() - , m_originalSample2() , m_tempBuffer(new SampleFrame[Engine::audioEngine()->framesPerPeriod()]) , m_parentTrack(instrumentTrack) { @@ -127,6 +93,7 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ int velocity = event.velocity(); qDebug() << "Note on!" << key << velocity; } + return true; } @@ -145,7 +112,26 @@ void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) if (!handle->m_pluginData) { - handle->m_pluginData = ; + qDebug() << "Note play handle no plugin data!"; + // Find an empty active note array + for (int i = 0; i < MAX_ACTIVE_NOTES; ++i) + { + if (m_activeNoteArrays[i] == std::nullopt) + { + handle->m_pluginData = &m_activeNoteArrays[i]; + } + } + // Did we find an open array? + if (!handle->m_pluginData) { qDebug() << "[SFZ Player] Could not find vacant note array in buffer!"; return; } + + auto* activeNoteArray = static_cast>*>(handle->m_pluginData); + + // Now loop through the regions are check which ones meet the conditions to spawn a new note + + for (auto region : m_sfzRegions) + { + // TODO + } } } @@ -157,17 +143,14 @@ void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) void SfzSampler::loadFile(const QString& filepath) { - QString parentDirectory = QFileInfo(filepath).absoluteDir().path() + "/"; - qDebug() << "File!" << filepath; - qDebug() << "Dir!" << parentDirectory; - QFile file(filepath); - - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Could not read!!"; return; } - - while (!file.atEnd()) - { - QString line = file.readLine(); - } + bool successful = SfzParser::parseSfzFile(filepath, m_sfzRegions); + qDebug() << "was okay?" << successful; + qDebug() << "num regions" << m_sfzRegions.size(); + qDebug() << "first region sample" << m_sfzRegions[0].m_sample.value_or("aaaa"); + qDebug() << "first region lokey" << m_sfzRegions[0].m_lokey.value_or(-1); + qDebug() << "first region hikey" << m_sfzRegions[0].m_hikey.value_or(-1); + qDebug() << "first region lovel" << m_sfzRegions[0].m_lovel.value_or(-1); + qDebug() << "first region hivel" << m_sfzRegions[0].m_hivel.value_or(-1); } void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index b8ed9dfc7e7..8cae9d9bdc4 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -31,7 +31,9 @@ #include "Note.h" #include "Sample.h" #include "SfzSamplerView.h" -#include "SfzFormat.h" +#include "SfzParser.h" +#include "SfzRegion.h" +#include "SfzRegionPlayState.h" namespace lmms { @@ -62,6 +64,17 @@ class SfzSampler : public Instrument InstrumentTrack* m_parentTrack; + std::vector m_sfzRegions; + + + // We need to keep track of which notes are active. Because a single NotePlayHandle can create multiple sounds, + // their m_pluginData need to point to an array of SfzRegionPlayStates. We don't want to perform any allocations at runtime, + // so we define a buffer to hold a bunch of them at once and give out pointers to the NotePlayHandles as they are created. + static constexpr int MAX_SOUNDS_PER_NOTE_PRESS = 16; + static constexpr int MAX_ACTIVE_NOTES = 128; + + std::array>, MAX_ACTIVE_NOTES> m_activeNoteArrays; + friend class gui::SfzSamplerView; }; } // namespace lmms From 5f7c15c98572a95c396c41a4e699ec7f746abe92 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:10:06 -0500 Subject: [PATCH 009/131] Lots of additions --- plugins/SfzSampler/CMakeLists.txt | 6 ++ plugins/SfzSampler/SfzGlobalState.cpp | 26 +++++++ plugins/SfzSampler/SfzGlobalState.h | 46 ++++++++++++ plugins/SfzSampler/SfzOpcodeState.cpp | 46 +++++++++++- plugins/SfzSampler/SfzOpcodeState.h | 1 + plugins/SfzSampler/SfzParser.cpp | 6 +- plugins/SfzSampler/SfzRegion.cpp | 4 + plugins/SfzSampler/SfzRegion.h | 8 ++ plugins/SfzSampler/SfzRegionPlayState.cpp | 14 ++++ plugins/SfzSampler/SfzRegionPlayState.h | 33 +++++++- plugins/SfzSampler/SfzSampler.cpp | 92 ++++++++++++++++------- plugins/SfzSampler/SfzSampler.h | 14 ++-- plugins/SfzSampler/SfzTrigger.cpp | 35 +++++++++ plugins/SfzSampler/SfzTrigger.h | 36 +++++++++ 14 files changed, 328 insertions(+), 39 deletions(-) create mode 100644 plugins/SfzSampler/SfzGlobalState.cpp create mode 100644 plugins/SfzSampler/SfzGlobalState.h create mode 100644 plugins/SfzSampler/SfzTrigger.cpp create mode 100644 plugins/SfzSampler/SfzTrigger.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index b02b6e28f7a..67500a74236 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -14,6 +14,10 @@ build_plugin(sfzsampler SfzParser.h SfzRegionPlayState.cpp SfzRegionPlayState.h + SfzGlobalState.cpp + SfzGlobalState.h + SfzTrigger.cpp + SfzTrigger.h MOCFILES SfzSampler.h SfzSamplerView.h @@ -22,5 +26,7 @@ build_plugin(sfzsampler SfzRegion.h SfzParser.h SfzRegionPlayState.h + SfzGlobalState.h + SfzTrigger.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" ) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp new file mode 100644 index 00000000000..f1d3f163e03 --- /dev/null +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -0,0 +1,26 @@ + + +#include "SfzGlobalState.h" + +namespace lmms +{ + +SfzGlobalState::SfzGlobalState(const int numRegions) +{ + m_regionNoteCounts.resize(numRegions); +} + +void SfzGlobalState::processTrigger(const SfzTrigger& trigger) +{ +} + +void SfzGlobalState::regionTriggered(const SfzRegion& region) +{ +} + +void SfzGlobalState::regionEnded(const SfzRegion& region) +{ +} + + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h new file mode 100644 index 00000000000..fd2486367d9 --- /dev/null +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -0,0 +1,46 @@ + +#ifndef LMMS_SFZ_GLOBAL_STATE_H +#define LMMS_SFZ_GLOBAL_STATE_H + +#include +#include + +#include "SfzTrigger.h" +#include "SfzRegion.h" + +namespace lmms +{ + + +class SfzGlobalState +{ +public: + SfzGlobalState(const int numRegions); + SfzGlobalState() = default; + + //! Handles updating the last played keys and keeps track of which keys are currently being pressed + void processTrigger(const SfzTrigger& trigger); + + //! Handles keeping track of how many sounds are actively being played per region + void regionTriggered(const int regionNumber); + void regionEnded(const int regionNumber); + +private: + //! Stores a number for every key, tracking which order the keys were played in + //! The first key played gets a 1, the second key gets a 2, etc, overwriting when keys are played multiple times/ + //! By finding the maximum number in a range, you can find the last played key + std::array, 128> m_lastPlayedKeys; + + //! Keeps track of which keys on the piano are currently being pressed + std::array m_activeKeys; + + //! Keeps track of the number of active sounds on each region + //! This is stored as a vector, but it is resized/initialized to the correct length when the SFZ file is parsed, so no runtime allocations are performed + std::vector m_regionNoteCounts; +}; + + +} // namespace lmms + + +#endif // LMMS_SFZ_GLOBAL_STATE_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index c0354bb6b1e..17365e11574 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -21,14 +21,17 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "key") { m_key = value.toInt(&successful); + if (!successful) { m_key = keyNumFromString(value, &successful); } } else if (name == "lokey") { m_lokey = value.toInt(&successful); + if (!successful) { m_lokey = keyNumFromString(value, &successful); } } else if (name == "hikey") { m_hikey = value.toInt(&successful); + if (!successful) { m_hikey = keyNumFromString(value, &successful); } } else if (name == "lovel") @@ -43,6 +46,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "pitch_keycenter") { m_pitch_keycenter = value.toInt(&successful); + if (!successful) { m_pitch_keycenter = keyNumFromString(value, &successful); } } else if (name == "ampeg_delay") @@ -75,7 +79,47 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu return false; } - return successful; + if (!successful) + { + qDebug() << "[SFZ Parser] Unable to convert value to number:" << name << "=" << value; + return false; + } + return true; +} + + + +int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) +{ + keyString = keyString.toLower(); + // The last character is the octave number + int octave = QString(keyString.back()).toInt(successful); + if (!*successful) {qDebug() << "[SFZ Parser] Unable to parse key string, Invalid octave number:" << keyString; return -1; } + // The remaining characters at the start define the key + QString key = keyString.chopped(1); + int keyOffset = 0; + + if (key == "a") { keyOffset = -3; } + else if (key == "a#" || key == "bb") { keyOffset = -2; } + else if (key == "b") { keyOffset = -1; } + else if (key == "c") { keyOffset = 0; } // C is 0 since that's where the octaves change + else if (key == "c#" || key == "db") { keyOffset = 1; } + else if (key == "d") { keyOffset = 2; } + else if (key == "d#" || key == "eb") { keyOffset = 3; } + else if (key == "e") { keyOffset = 4; } + else if (key == "f") { keyOffset = 5; } + else if (key == "f#" || key == "gb") { keyOffset = 6; } + else if (key == "g") { keyOffset = 7; } + else if (key == "g#" || key == "ab") { keyOffset = 8; } + else + { + *successful = false; + qDebug() << "[SFZ Parser] Unable to parse key string, Invalid key:" << keyString; + return -1; + } + *successful = true; + // For some reason, C1 is midi key 21, so eveything is offset by 24 + return 24 + keyOffset + 12 * (octave - 1); } diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 8af3ea74cdc..fa2d792734f 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -13,6 +13,7 @@ class SfzOpcodeState { public: bool setOpcodeByStrings(const QString& name, const QString& value); + static int keyNumFromString(QString keyString, bool* successful); //private: std::optional m_sample = std::nullopt; diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index b8c13b89222..a4c0994ff9b 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -3,7 +3,6 @@ #include "SfzParser.h" #include "SfzOpcodeState.h" #include -//#include #include #include #include @@ -14,6 +13,10 @@ namespace lmms bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions) { + // Clear the vector of regions just in case anything is inside it from before + outputRegions.clear(); + + // Open the .sfz file QDir parentDirectory = QFileInfo(filePath).absoluteDir(); QFile file(filePath); @@ -145,6 +148,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } // Now that all the opcodes have been parsed into regions and added to the output vector, we are done! + // The samples themselves still need to be loaded, but that's a job for later return true; } diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index e49a4c4c3f7..92b7033185e 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -15,5 +15,9 @@ SfzRegion::SfzRegion(SfzOpcodeState opcodeState) { } +bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) +{ + return false; +} } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index dc6ce24d767..33567c40e15 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -3,6 +3,8 @@ #define LMMS_SFZ_REGION_H #include "SfzOpcodeState.h" +#include "SfzGlobalState.h" +#include "SfzTrigger.h" namespace lmms { @@ -11,6 +13,12 @@ class SfzRegion : public SfzOpcodeState { public: SfzRegion(SfzOpcodeState opcodeState); + + //! Returns true if the trigger event matches all of the requirements defined by the opcodes of this region + //! For example, if lokey=24 and hikey=29, and the trigger is a NoteOn event on key 26, then it will return true + //! If the trigger does not fall in the key range or velocity range or any other conditions are not met (including global state conditions, + //! such as which switch key was last pressed) this will return false. + bool triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger); }; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 2c637c32fe9..5cbf070ab40 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -5,6 +5,20 @@ namespace lmms { +SfzRegionPlayState::SfzRegionPlayState(const SfzRegion& region, const SfzTrigger& trigger) + : m_trigger(trigger) + , m_region(®ion) +{ +} + + +void SfzRegionPlayState::play(SampleFrame* buffer, fpp_t frames) +{ +} + +void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) +{ +} } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index b02e15bd766..d560b1a3477 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -2,6 +2,9 @@ #ifndef LMMS_SFZ_REGION_PLAY_STATE_H #define LMMS_SFZ_REGION_PLAY_STATE_H +#include "SampleFrame.h" +#include "SfzTrigger.h" +#include "SfzRegion.h" namespace lmms { @@ -9,7 +12,35 @@ namespace lmms class SfzRegionPlayState { - +public: + SfzRegionPlayState(const SfzRegion& region, const SfzTrigger& trigger); + SfzRegionPlayState() = default; // Needed to initialize array + + //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. + void play(SampleFrame* buffer, fpp_t frames); + + //! Handle incoming event to decide whether to deactivate/release + void processTrigger(const& SfzTrigger); + + bool active() const { return m_active; } + +private: + //! Stores whether this object still represents a sound which exists. + //! This will be true when the sound is active, but will become false once the release has ended or it has been forcefully deactivated. + bool m_active = false; + //! Stores whether the note has been released yet + bool m_released = false; + + //! The number of frames since the start of the sound. This may be negative if a note starts partway through a buffer. + int frameCount = 0; + //! The frame at which the note was released, relative to the start of the note + int releaseFrame = 0; + + //! The trigger event which caused this sound + SfzTrigger m_trigger; + + //! The region this sound originated from + const SfzRegion* m_region = nullptr; }; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index c75239bf899..a76f39a36f9 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -89,61 +89,95 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ { if (event.type() == MidiNoteOn) { - int key = event.key(); - int velocity = event.velocity(); - qDebug() << "Note on!" << key << velocity; + processTrigger(SfzTrigger::noteOnEvent(event.key(), event.velocity())); + return true; } - return true; -} - - -void SfzSampler::play(SampleFrame* workingBuffer) -{ - const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); + else if (event.type() == MidiNoteOff) + { + processTrigger(SfzTrigger::noteOffEvent(event.key(), event.velocity())); + return true; + } + return false; } - - void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { int noteIndex = handle->key(); const fpp_t frames = handle->framesLeftForCurrentPeriod(); const f_cnt_t offset = handle->noteOffset(); +} - if (!handle->m_pluginData) +void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) +{ + //delete static_cast(handle->m_pluginData); +} + + +void SfzSampler::processTrigger(const SfzTrigger& trigger) +{ + // Notify the global state to update which keys are active + m_sfzGlobalState.processTrigger(trigger); + // Loop through all the regions to check if a new note should be played + for (int regionIndex = 0; regionIndex < m_sfzRegions.size(); ++regionIndex) { - qDebug() << "Note play handle no plugin data!"; - // Find an empty active note array - for (int i = 0; i < MAX_ACTIVE_NOTES; ++i) + auto& region = m_sfzRegions.at(regionIndex); + if (region.triggerConditionsMet(m_sfzGlobalState, trigger)) { - if (m_activeNoteArrays[i] == std::nullopt) + // Loop through array to find open position + bool foundOpenPosition = false; + for (int i = 0; i < MAX_ACTIVE_NOTES; ++i) { - handle->m_pluginData = &m_activeNoteArrays[i]; + if (!m_activeNotes[i].active()) + { + m_activeNotes[i] = SfzRegionPlayState(region, trigger); + // Notify the global state to update the count of active notes in the region + m_sfzGlobalState.regionTriggered(regionIndex); + foundOpenPosition = true; + break; + } } + if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in note state buffer!"; } } - // Did we find an open array? - if (!handle->m_pluginData) { qDebug() << "[SFZ Player] Could not find vacant note array in buffer!"; return; } - - auto* activeNoteArray = static_cast>*>(handle->m_pluginData); - - // Now loop through the regions are check which ones meet the conditions to spawn a new note - - for (auto region : m_sfzRegions) + } + // Loop through all the active notes to check if any need to be deactivated/released by the trigger + for (auto regionPlayState : m_activeNotes) + { + if (regionPlayState.active()) { - // TODO + regionPlayState.processTrigger(trigger); + // If the trigger deactivated the sound, notify the global state to update the number of active notes in that region + if (!regionPlayState.active()) { m_sfzGlobalState.regionEnded(regionPlayState); } } } } -void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) + + + + +void SfzSampler::play(SampleFrame* workingBuffer) { - //delete static_cast(handle->m_pluginData); + const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); + + for (auto regionPlayState : m_activeNotes) + { + // Render audio from each of the active notes + regionPlayState.play(m_tempBuffer, frames); + for (f_cnt_t f = 0; f < frames; ++f) + { + workingBuffer[f] += m_tempBuffer[f]; + } + } } + + void SfzSampler::loadFile(const QString& filepath) { + // Parse all the headers of the .sfz (accounting for and defaults) and populate m_sfzRegions with the new SfzRegion bool successful = SfzParser::parseSfzFile(filepath, m_sfzRegions); + // qDebug() << "was okay?" << successful; qDebug() << "num regions" << m_sfzRegions.size(); qDebug() << "first region sample" << m_sfzRegions[0].m_sample.value_or("aaaa"); diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 8cae9d9bdc4..623f4c0731c 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -34,6 +34,7 @@ #include "SfzParser.h" #include "SfzRegion.h" #include "SfzRegionPlayState.h" +#include "SfzGlobalState.h" namespace lmms { @@ -60,20 +61,19 @@ class SfzSampler : public Instrument gui::PluginView* instantiateView(QWidget* parent) override; private: + void processTrigger(const SfzTrigger&); + SampleFrame* m_tempBuffer; InstrumentTrack* m_parentTrack; + //! Holds information about the total number of notes active, last switch keys pressed, etc + SfzGlobalState m_sfzGlobalState; + std::vector m_sfzRegions; - - // We need to keep track of which notes are active. Because a single NotePlayHandle can create multiple sounds, - // their m_pluginData need to point to an array of SfzRegionPlayStates. We don't want to perform any allocations at runtime, - // so we define a buffer to hold a bunch of them at once and give out pointers to the NotePlayHandles as they are created. - static constexpr int MAX_SOUNDS_PER_NOTE_PRESS = 16; static constexpr int MAX_ACTIVE_NOTES = 128; - - std::array>, MAX_ACTIVE_NOTES> m_activeNoteArrays; + std::array m_activeNotes; friend class gui::SfzSamplerView; }; diff --git a/plugins/SfzSampler/SfzTrigger.cpp b/plugins/SfzSampler/SfzTrigger.cpp new file mode 100644 index 00000000000..73fcf493175 --- /dev/null +++ b/plugins/SfzSampler/SfzTrigger.cpp @@ -0,0 +1,35 @@ + + +#include "SfzTrigger.h" + +namespace lmms +{ + +const SfzTrigger SfzTrigger::noteOnEvent(const int key, const int vel) +{ + SfzTrigger trigger = SfzTrigger(); + trigger.m_type = Type::NoteOn; + trigger.m_key = key; + trigger.m_vel = vel; + return trigger; +} + +const SfzTrigger SfzTrigger::noteOffEvent(const int key, const int vel) +{ + SfzTrigger trigger = SfzTrigger(); + trigger.m_type = Type::NoteOff; + trigger.m_key = key; + trigger.m_vel = vel; + return trigger; +} + +const SfzTrigger SfzTrigger::controlChangeEvent(const int controlNumber, const int value) +{ + SfzTrigger trigger = SfzTrigger(); + trigger.m_type = Type::ControlChange; + trigger.m_controlChangeNumber = controlNumber; + trigger.m_controlChangeValue = value; + return trigger; +} + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzSampler/SfzTrigger.h new file mode 100644 index 00000000000..23702f05111 --- /dev/null +++ b/plugins/SfzSampler/SfzTrigger.h @@ -0,0 +1,36 @@ + +#ifndef LMMS_SFZ_TRIGGER_H +#define LMMS_SFZ_TRIGGER_H + +#include + +namespace lmms +{ + + +class SfzTrigger +{ +public: + static const SfzTrigger noteOnEvent(const int key, const int vel); + static const SfzTrigger noteOffEvent(const int key, const int vel); + static const SfzTrigger controlChangeEvent(const int controlNumber, const int value); + + enum class Type + { + NoteOn, + NoteOff, + ControlChange, + }; + + Type m_type; + std::optional m_key; + std::optional m_vel; + std::optional m_controlChangeNumber; + std::optional m_controlChangeValue; +}; + + +} // namespace lmms + + +#endif // LMMS_SFZ_TRIGGER_H \ No newline at end of file From 49d750831d7be3b26bbc66c2718422052dce1bed Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:56:26 -0500 Subject: [PATCH 010/131] Compiles now --- plugins/SfzSampler/SfzGlobalState.cpp | 12 +---- plugins/SfzSampler/SfzGlobalState.h | 13 +----- plugins/SfzSampler/SfzRegion.cpp | 54 +++++++++++++++++++++++ plugins/SfzSampler/SfzRegion.h | 17 +++++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 6 +-- plugins/SfzSampler/SfzRegionPlayState.h | 10 +++-- plugins/SfzSampler/SfzSampler.cpp | 39 +++------------- plugins/SfzSampler/SfzSampler.h | 5 +-- 8 files changed, 92 insertions(+), 64 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index f1d3f163e03..fab57734f2a 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -5,21 +5,13 @@ namespace lmms { -SfzGlobalState::SfzGlobalState(const int numRegions) -{ - m_regionNoteCounts.resize(numRegions); -} - void SfzGlobalState::processTrigger(const SfzTrigger& trigger) { } -void SfzGlobalState::regionTriggered(const SfzRegion& region) -{ -} - -void SfzGlobalState::regionEnded(const SfzRegion& region) +int SfzGlobalState::lastKeyPressedInRange(const int lowKey, const int highKey) { + return 0; } diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index fd2486367d9..edca80ffc3b 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -6,7 +6,6 @@ #include #include "SfzTrigger.h" -#include "SfzRegion.h" namespace lmms { @@ -15,15 +14,11 @@ namespace lmms class SfzGlobalState { public: - SfzGlobalState(const int numRegions); - SfzGlobalState() = default; - //! Handles updating the last played keys and keeps track of which keys are currently being pressed void processTrigger(const SfzTrigger& trigger); - //! Handles keeping track of how many sounds are actively being played per region - void regionTriggered(const int regionNumber); - void regionEnded(const int regionNumber); + // Returns the midi key number of the last pressed key in the range [lowKey, highKey] + int lastKeyPressedInRange(const int lowKey, const int highKey); private: //! Stores a number for every key, tracking which order the keys were played in @@ -33,10 +28,6 @@ class SfzGlobalState //! Keeps track of which keys on the piano are currently being pressed std::array m_activeKeys; - - //! Keeps track of the number of active sounds on each region - //! This is stored as a vector, but it is resized/initialized to the correct length when the SFZ file is parsed, so no runtime allocations are performed - std::vector m_regionNoteCounts; }; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 92b7033185e..cd8dacc6b48 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -6,18 +6,72 @@ #include "SfzRegion.h" +#include "Engine.h" +#include "AudioEngine.h" + +#include + namespace lmms { SfzRegion::SfzRegion(SfzOpcodeState opcodeState) : SfzOpcodeState(opcodeState) + , m_tempBuffer(new SampleFrame[Engine::audioEngine()->framesPerPeriod()]) { } +SfzRegion::~SfzRegion() +{ + delete[] m_tempBuffer; +} + bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) { return false; } +void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigger& trigger) +{ + // If the trigger conditions are met, spawn a new sound + if (triggerConditionsMet(globalState, trigger)) + { + // Loop through array to find open position + bool foundOpenPosition = false; + for (auto& regionPlayState : m_activeSounds) + { + if (!regionPlayState.active()) + { + regionPlayState = SfzRegionPlayState(this, trigger); + foundOpenPosition = true; + break; + } + } + if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in RegionPlayState buffer!"; } + } + + // Loop through all the active sounds to check if any need to be deactivated/released by the trigger + for (auto& regionPlayState : m_activeSounds) + { + if (regionPlayState.active()) + { + regionPlayState.processTrigger(trigger); + } + } +} + + +void SfzRegion::play(SampleFrame* workingBuffer, const fpp_t frames) +{ + for (auto& regionPlayState : m_activeSounds) + { + regionPlayState.play(m_tempBuffer, frames); + for (f_cnt_t f = 0; f < frames; ++f) + { + workingBuffer[f] += m_tempBuffer[f]; + } + } +} + + } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 33567c40e15..2717c2dc805 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -5,6 +5,7 @@ #include "SfzOpcodeState.h" #include "SfzGlobalState.h" #include "SfzTrigger.h" +#include "SfzRegionPlayState.h" namespace lmms { @@ -13,12 +14,28 @@ class SfzRegion : public SfzOpcodeState { public: SfzRegion(SfzOpcodeState opcodeState); + ~SfzRegion(); //! Returns true if the trigger event matches all of the requirements defined by the opcodes of this region //! For example, if lokey=24 and hikey=29, and the trigger is a NoteOn event on key 26, then it will return true //! If the trigger does not fall in the key range or velocity range or any other conditions are not met (including global state conditions, //! such as which switch key was last pressed) this will return false. bool triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger); + + //! Handles spawning a new sound if the all the conditions are met + //! Also handles sending the trigger to the currently active sounds, in case they need to release (such as on NoteOff) + void processTrigger(const SfzGlobalState& globalState, const SfzTrigger& trigger); + + //! Renders sound from each of the active SfzRegionPlayStates and writes it to the given buffer + void play(SampleFrame* workingBuffer, const fpp_t frames); + +private: + static constexpr int MAX_ACTIVE_SOUNDS = 128; + //! Array to store all active (and inactive) sound play states for this region + std::array m_activeSounds; + + //! Helper buffer to help accumulate audio from each of the active SfzRegionPlayStates + SampleFrame* m_tempBuffer; }; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 5cbf070ab40..4a11c887794 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -5,14 +5,14 @@ namespace lmms { -SfzRegionPlayState::SfzRegionPlayState(const SfzRegion& region, const SfzTrigger& trigger) +SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger) : m_trigger(trigger) - , m_region(®ion) + , m_region(region) { } -void SfzRegionPlayState::play(SampleFrame* buffer, fpp_t frames) +void SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) { } diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index d560b1a3477..5170ee39975 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -4,23 +4,25 @@ #include "SampleFrame.h" #include "SfzTrigger.h" -#include "SfzRegion.h" +#include "SfzOpcodeState.h" namespace lmms { +class SfzRegion; + class SfzRegionPlayState { public: - SfzRegionPlayState(const SfzRegion& region, const SfzTrigger& trigger); + SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger); SfzRegionPlayState() = default; // Needed to initialize array //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. - void play(SampleFrame* buffer, fpp_t frames); + void play(SampleFrame* buffer, const fpp_t frames); //! Handle incoming event to decide whether to deactivate/release - void processTrigger(const& SfzTrigger); + void processTrigger(const SfzTrigger& trigger); bool active() const { return m_active; } diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index a76f39a36f9..8ee7288da18 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -117,37 +117,11 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) { // Notify the global state to update which keys are active m_sfzGlobalState.processTrigger(trigger); + // Loop through all the regions to check if a new note should be played - for (int regionIndex = 0; regionIndex < m_sfzRegions.size(); ++regionIndex) + for (auto& region : m_sfzRegions) { - auto& region = m_sfzRegions.at(regionIndex); - if (region.triggerConditionsMet(m_sfzGlobalState, trigger)) - { - // Loop through array to find open position - bool foundOpenPosition = false; - for (int i = 0; i < MAX_ACTIVE_NOTES; ++i) - { - if (!m_activeNotes[i].active()) - { - m_activeNotes[i] = SfzRegionPlayState(region, trigger); - // Notify the global state to update the count of active notes in the region - m_sfzGlobalState.regionTriggered(regionIndex); - foundOpenPosition = true; - break; - } - } - if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in note state buffer!"; } - } - } - // Loop through all the active notes to check if any need to be deactivated/released by the trigger - for (auto regionPlayState : m_activeNotes) - { - if (regionPlayState.active()) - { - regionPlayState.processTrigger(trigger); - // If the trigger deactivated the sound, notify the global state to update the number of active notes in that region - if (!regionPlayState.active()) { m_sfzGlobalState.regionEnded(regionPlayState); } - } + region.processTrigger(m_sfzGlobalState, trigger); } } @@ -159,10 +133,11 @@ void SfzSampler::play(SampleFrame* workingBuffer) { const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - for (auto regionPlayState : m_activeNotes) + for (auto& region : m_sfzRegions) { - // Render audio from each of the active notes - regionPlayState.play(m_tempBuffer, frames); + // Render audio from each of the regions + // This amounts to the regions themselves rendering the audio from each of their active SfzRegionPlayStates + region.play(m_tempBuffer, frames); for (f_cnt_t f = 0; f < frames; ++f) { workingBuffer[f] += m_tempBuffer[f]; diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 623f4c0731c..da2299aadfa 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -61,7 +61,7 @@ class SfzSampler : public Instrument gui::PluginView* instantiateView(QWidget* parent) override; private: - void processTrigger(const SfzTrigger&); + void processTrigger(const SfzTrigger& trigger); SampleFrame* m_tempBuffer; @@ -72,9 +72,6 @@ class SfzSampler : public Instrument std::vector m_sfzRegions; - static constexpr int MAX_ACTIVE_NOTES = 128; - std::array m_activeNotes; - friend class gui::SfzSamplerView; }; } // namespace lmms From 74132e960b04fe49029bdbbcfa2e54a629198d24 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:37:24 -0500 Subject: [PATCH 011/131] Minor changes, temp array --- plugins/SfzSampler/SfzParser.cpp | 8 ++++---- plugins/SfzSampler/SfzRegion.cpp | 16 ++++++---------- plugins/SfzSampler/SfzRegion.h | 9 +++------ plugins/SfzSampler/SfzRegionPlayState.cpp | 3 ++- plugins/SfzSampler/SfzRegionPlayState.h | 3 ++- plugins/SfzSampler/SfzSampler.cpp | 16 ++++++---------- plugins/SfzSampler/SfzSampler.h | 3 +-- 7 files changed, 24 insertions(+), 34 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index a4c0994ff9b..7d0fadc8dfa 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -96,7 +96,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou if (segment == "") { // If we were previously in a , then wrap it up and add it to the output vector - if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + if (withinRegion) { outputRegions.emplace_back(currentRegionState); } withinGlobal = true; withinGroup = false; withinRegion = false; @@ -104,7 +104,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } else if (segment == "") { - if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + if (withinRegion) { outputRegions.emplace_back(currentRegionState); } withinGlobal = false; withinGroup = true; withinRegion = false; @@ -113,7 +113,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } else if (segment == "") { - if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + if (withinRegion) { outputRegions.emplace_back(currentRegionState); } withinGlobal = false; withinGroup = false; withinRegion = true; @@ -145,7 +145,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } } // Check one last time in case the file ended with a region and didn't get added - if (withinRegion) { outputRegions.push_back(SfzRegion(currentRegionState)); } + if (withinRegion) { outputRegions.emplace_back(currentRegionState); } // Now that all the opcodes have been parsed into regions and added to the output vector, we are done! // The samples themselves still need to be loaded, but that's a job for later diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index cd8dacc6b48..8cae954768a 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -17,15 +17,9 @@ namespace lmms SfzRegion::SfzRegion(SfzOpcodeState opcodeState) : SfzOpcodeState(opcodeState) - , m_tempBuffer(new SampleFrame[Engine::audioEngine()->framesPerPeriod()]) { } -SfzRegion::~SfzRegion() -{ - delete[] m_tempBuffer; -} - bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) { return false; @@ -61,16 +55,18 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg } -void SfzRegion::play(SampleFrame* workingBuffer, const fpp_t frames) +bool SfzRegion::play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames) { + bool anythingPlayed = false; for (auto& regionPlayState : m_activeSounds) { - regionPlayState.play(m_tempBuffer, frames); - for (f_cnt_t f = 0; f < frames; ++f) + anythingPlayed = anythingPlayed || regionPlayState.play(temporaryBuffer, frames); + if (anythingPlayed) { - workingBuffer[f] += m_tempBuffer[f]; + for (f_cnt_t f = 0; f < frames; ++f) { workingBuffer[f] += temporaryBuffer[f]; } } } + return anythingPlayed; } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 2717c2dc805..2fd69a618c7 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -14,7 +14,6 @@ class SfzRegion : public SfzOpcodeState { public: SfzRegion(SfzOpcodeState opcodeState); - ~SfzRegion(); //! Returns true if the trigger event matches all of the requirements defined by the opcodes of this region //! For example, if lokey=24 and hikey=29, and the trigger is a NoteOn event on key 26, then it will return true @@ -23,19 +22,17 @@ class SfzRegion : public SfzOpcodeState bool triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger); //! Handles spawning a new sound if the all the conditions are met - //! Also handles sending the trigger to the currently active sounds, in case they need to release (such as on NoteOff) + //! Also handles sending the trigger to the currently active sounds, in case they need to deactivate/release (such as on NoteOff) void processTrigger(const SfzGlobalState& globalState, const SfzTrigger& trigger); //! Renders sound from each of the active SfzRegionPlayStates and writes it to the given buffer - void play(SampleFrame* workingBuffer, const fpp_t frames); + //! Returns true if any sound was actually generated + bool play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames); private: static constexpr int MAX_ACTIVE_SOUNDS = 128; //! Array to store all active (and inactive) sound play states for this region std::array m_activeSounds; - - //! Helper buffer to help accumulate audio from each of the active SfzRegionPlayStates - SampleFrame* m_tempBuffer; }; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 4a11c887794..dbd7824f126 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -12,8 +12,9 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger } -void SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) +bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) { + return false; } diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 5170ee39975..e5f3421a02b 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -19,7 +19,8 @@ class SfzRegionPlayState SfzRegionPlayState() = default; // Needed to initialize array //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. - void play(SampleFrame* buffer, const fpp_t frames); + //! Returns true if any sound was generated, false if the buffer is left untouched + bool play(SampleFrame* buffer, const fpp_t frames); //! Handle incoming event to decide whether to deactivate/release void processTrigger(const SfzTrigger& trigger); diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 8ee7288da18..aa4193d673b 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -64,7 +64,7 @@ PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) - , m_tempBuffer(new SampleFrame[Engine::audioEngine()->framesPerPeriod()]) + , m_tempBuffer(std::make_unique(Engine::audioEngine()->framesPerPeriod())) , m_parentTrack(instrumentTrack) { QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; @@ -78,12 +78,6 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) } -SfzSampler::~SfzSampler() -{ - delete[] m_tempBuffer; -} - - bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) { @@ -137,10 +131,12 @@ void SfzSampler::play(SampleFrame* workingBuffer) { // Render audio from each of the regions // This amounts to the regions themselves rendering the audio from each of their active SfzRegionPlayStates - region.play(m_tempBuffer, frames); - for (f_cnt_t f = 0; f < frames; ++f) + // We pass a temporary buffer which can be used for rendering samples and later summing it to the working buffer. + bool anythingPlayed = region.play(workingBuffer, m_tempBuffer.get(), frames); + // The buffer cleared and reused for each region so that the regions don't have to worry about allocating it. + if (anythingPlayed) { - workingBuffer[f] += m_tempBuffer[f]; + for (f_cnt_t f = 0; f < frames; ++f) { m_tempBuffer[f] = SampleFrame{0.0f}; } // TODO can we avoid this? } } } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index da2299aadfa..1b4ca04bef1 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -44,7 +44,6 @@ class SfzSampler : public Instrument public: SfzSampler(InstrumentTrack* instrumentTrack); - ~SfzSampler(); void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; void deleteNotePluginData(NotePlayHandle* handle) override; @@ -63,7 +62,7 @@ class SfzSampler : public Instrument private: void processTrigger(const SfzTrigger& trigger); - SampleFrame* m_tempBuffer; + std::unique_ptr m_tempBuffer; InstrumentTrack* m_parentTrack; From 4f34d38547734c6d3e7c81c8c5c630150b99f6ac Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 20 Dec 2025 17:19:02 -0500 Subject: [PATCH 012/131] Loading samples --- plugins/SfzSampler/SfzOpcodeState.cpp | 2 +- plugins/SfzSampler/SfzOpcodeState.h | 2 +- plugins/SfzSampler/SfzRegion.cpp | 20 ++++++++++++++++++++ plugins/SfzSampler/SfzRegion.h | 13 +++++++++++++ plugins/SfzSampler/SfzSampler.cpp | 20 +++++++++++++++----- plugins/SfzSampler/SfzSampler.h | 2 +- 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 17365e11574..9aa82d91952 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -15,7 +15,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu if (name == "sample") { - m_sample = value; + m_sampleFile = value; } else if (name == "key") diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index fa2d792734f..06052f4895b 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -16,7 +16,7 @@ class SfzOpcodeState static int keyNumFromString(QString keyString, bool* successful); //private: - std::optional m_sample = std::nullopt; + std::optional m_sampleFile = std::nullopt; std::optional m_key = std::nullopt; std::optional m_lokey = 0; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 8cae954768a..f782c858507 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -8,6 +8,7 @@ #include "Engine.h" #include "AudioEngine.h" +#include "SampleLoader.h" #include @@ -70,4 +71,23 @@ bool SfzRegion::play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, c } + +bool SfzRegion::initializeSample(const QDir& parentDirectory) +{ + if (m_sampleFile == std::nullopt) + { + // It's weird for a region not to have a sample defined. That's literally all regions do, play samples, right? + qDebug() << "[SFZ Player] Warning: `sample` opcode not assigned"; + return false; + } + + if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_sampleFile.value_or("")))) + { + m_sample = Sample(std::move(buffer)); + return true; + } + return false; +} + + } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 2fd69a618c7..6fa860ca63f 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -7,6 +7,10 @@ #include "SfzTrigger.h" #include "SfzRegionPlayState.h" +#include "Sample.h" + +#include + namespace lmms { @@ -29,10 +33,19 @@ class SfzRegion : public SfzOpcodeState //! Returns true if any sound was actually generated bool play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames); + + //! Load the sample file given by the `sample` opcode into m_sample. + //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed + //! Returns true if successful + bool initializeSample(const QDir& parentDirectory); + private: static constexpr int MAX_ACTIVE_SOUNDS = 128; //! Array to store all active (and inactive) sound play states for this region std::array m_activeSounds; + + //! Sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first + Sample m_sample; }; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index aa4193d673b..cb12d9807c1 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -144,18 +144,28 @@ void SfzSampler::play(SampleFrame* workingBuffer) -void SfzSampler::loadFile(const QString& filepath) +void SfzSampler::loadFile(const QString& filePath) { // Parse all the headers of the .sfz (accounting for and defaults) and populate m_sfzRegions with the new SfzRegion - bool successful = SfzParser::parseSfzFile(filepath, m_sfzRegions); - // - qDebug() << "was okay?" << successful; + bool successfulParseFile = SfzParser::parseSfzFile(filePath, m_sfzRegions); + + qDebug() << "was okay?" << successfulParseFile; qDebug() << "num regions" << m_sfzRegions.size(); - qDebug() << "first region sample" << m_sfzRegions[0].m_sample.value_or("aaaa"); + qDebug() << "first region sample" << m_sfzRegions[0].m_sampleFile.value_or("aaaa"); qDebug() << "first region lokey" << m_sfzRegions[0].m_lokey.value_or(-1); qDebug() << "first region hikey" << m_sfzRegions[0].m_hikey.value_or(-1); qDebug() << "first region lovel" << m_sfzRegions[0].m_lovel.value_or(-1); qDebug() << "first region hivel" << m_sfzRegions[0].m_hivel.value_or(-1); + + // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples + // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them + // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: + QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + for (auto& region : m_sfzRegions) + { + bool successfulLoadSample = region.initializeSample(parentDirectory); + qDebug() << "sample was load okay?" << successfulLoadSample; + } } void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 1b4ca04bef1..69e4d5f74c9 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -54,7 +54,7 @@ class SfzSampler : public Instrument void saveSettings(QDomDocument& document, QDomElement& element) override; void loadSettings(const QDomElement& element) override; - void loadFile(const QString& file) override; + void loadFile(const QString& filePath) override; QString nodeName() const override; gui::PluginView* instantiateView(QWidget* parent) override; From b12a7cf5df393cc4749dca6406e69497d2c101b5 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:09:55 -0500 Subject: [PATCH 013/131] Makes sound, temp commit --- plugins/SfzSampler/SfzRegion.cpp | 19 +++++++++++++++ plugins/SfzSampler/SfzRegion.h | 3 ++- plugins/SfzSampler/SfzRegionPlayState.cpp | 28 +++++++++++++++++++++-- plugins/SfzSampler/SfzRegionPlayState.h | 9 ++++++-- plugins/SfzSampler/SfzTrigger.cpp | 8 +++---- plugins/SfzSampler/SfzTrigger.h | 9 +++++++- 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index f782c858507..2d24110fbd8 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -23,6 +23,25 @@ SfzRegion::SfzRegion(SfzOpcodeState opcodeState) bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) { + if (trigger.type() == SfzTrigger::Type::NoteOn) + { + int triggerKey = trigger.key().value_or(-1); + int triggerVelocity = trigger.velocity().value_or(-1); + + // `key` opcode + if (m_key != std::nullopt) + { + if (triggerKey != m_key.value_or(-1)) { return false; } + } + // `lokey` and `hikey` opcodes + if (triggerKey > m_hikey.value_or(-1) || triggerKey < m_lokey.value_or(-1)) { return false; } + + // `lovel` and `hivel` opcodes + if (triggerVelocity > m_hivel.value_or(-1) || triggerVelocity < m_lovel.value_or(-1)) { return false; } + + // If all the contitions passed, return true and spawn a sound + return true; + } return false; } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 6fa860ca63f..4c25117adfb 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -33,12 +33,13 @@ class SfzRegion : public SfzOpcodeState //! Returns true if any sound was actually generated bool play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames); - //! Load the sample file given by the `sample` opcode into m_sample. //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed //! Returns true if successful bool initializeSample(const QDir& parentDirectory); + const Sample& sample() const { return m_sample; } + private: static constexpr int MAX_ACTIVE_SOUNDS = 128; //! Array to store all active (and inactive) sound play states for this region diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index dbd7824f126..69bb000bb0a 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -2,11 +2,14 @@ #include "SfzRegionPlayState.h" +#include "SfzRegion.h" + namespace lmms { SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger) - : m_trigger(trigger) + : m_active(true) + , m_trigger(trigger) , m_region(region) { } @@ -14,12 +17,33 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) { - return false; + // If the sound is not active (note was released, off_by triggered, etc) then don't render any audio + if (!m_active) { return false; } + // If the initial m_frameCount is negative, that means the note hasn't started yet + if (m_frameCount < -static_cast(frames)) { m_frameCount += frames; return false; } // If the note doesn't start in this buffer, don't play anything + // If the start is within this buffer, get the number of frames until it starts + const f_cnt_t startFrameOffset = std::max(0, -m_frameCount); + const f_cnt_t framesToPlay = frames - startFrameOffset; + + // Initially render the sample into the buffer + // Sample::play returns whether the sample completed playback or not + m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay); + + for (f_cnt_t f = 0; f < frames; ++f) + { + m_frameCount++; + } + + return true; } void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) { + if (trigger.type() == SfzTrigger::Type::NoteOff) + { + m_active = false; // testing + } } } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index e5f3421a02b..c15a1c5289c 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -6,6 +6,8 @@ #include "SfzTrigger.h" #include "SfzOpcodeState.h" +#include "Sample.h" + namespace lmms { @@ -35,9 +37,12 @@ class SfzRegionPlayState bool m_released = false; //! The number of frames since the start of the sound. This may be negative if a note starts partway through a buffer. - int frameCount = 0; + int m_frameCount = 0; //! The frame at which the note was released, relative to the start of the note - int releaseFrame = 0; + int m_releaseFrame = 0; + + //! In order to play a Sample, we have to keep a persistance Sample::PlaybackState object which holds things likethe current frame position in the sample, etc + Sample::PlaybackState m_samplePlaybackState; //! The trigger event which caused this sound SfzTrigger m_trigger; diff --git a/plugins/SfzSampler/SfzTrigger.cpp b/plugins/SfzSampler/SfzTrigger.cpp index 73fcf493175..2f31a3a8621 100644 --- a/plugins/SfzSampler/SfzTrigger.cpp +++ b/plugins/SfzSampler/SfzTrigger.cpp @@ -5,21 +5,21 @@ namespace lmms { -const SfzTrigger SfzTrigger::noteOnEvent(const int key, const int vel) +const SfzTrigger SfzTrigger::noteOnEvent(const int key, const int velocity) { SfzTrigger trigger = SfzTrigger(); trigger.m_type = Type::NoteOn; trigger.m_key = key; - trigger.m_vel = vel; + trigger.m_velocity = velocity; return trigger; } -const SfzTrigger SfzTrigger::noteOffEvent(const int key, const int vel) +const SfzTrigger SfzTrigger::noteOffEvent(const int key, const int velocity) { SfzTrigger trigger = SfzTrigger(); trigger.m_type = Type::NoteOff; trigger.m_key = key; - trigger.m_vel = vel; + trigger.m_velocity = velocity; return trigger; } diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzSampler/SfzTrigger.h index 23702f05111..c4327f032d9 100644 --- a/plugins/SfzSampler/SfzTrigger.h +++ b/plugins/SfzSampler/SfzTrigger.h @@ -22,9 +22,16 @@ class SfzTrigger ControlChange, }; + const auto& type() const { return m_type; } + const auto& key() const { return m_key; } + const auto& velocity() const { return m_velocity; } + const auto& controlChangeNumber() const { return m_controlChangeNumber; } + const auto& controlChangeValue() const { return m_controlChangeValue; } + +private: Type m_type; std::optional m_key; - std::optional m_vel; + std::optional m_velocity; std::optional m_controlChangeNumber; std::optional m_controlChangeValue; }; From 214de19ac9f814fcea157552b4c2072a658066e7 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:07:13 -0500 Subject: [PATCH 014/131] Use max active voice index optimization --- plugins/SfzSampler/SfzRegion.cpp | 37 +++++++++++++++++++---- plugins/SfzSampler/SfzRegion.h | 8 +++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 4 +-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 2d24110fbd8..b90d00cfb74 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -52,11 +52,14 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg { // Loop through array to find open position bool foundOpenPosition = false; - for (auto& regionPlayState : m_activeSounds) + for (size_t i = 0; i < m_activeSounds.size(); ++i) { + auto& regionPlayState = m_activeSounds[i]; if (!regionPlayState.active()) { regionPlayState = SfzRegionPlayState(this, trigger); + // If this new index is above the current max active index, update it + m_maxActiveIndex = std::max(m_maxActiveIndex, i); foundOpenPosition = true; break; } @@ -65,11 +68,15 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg } // Loop through all the active sounds to check if any need to be deactivated/released by the trigger - for (auto& regionPlayState : m_activeSounds) + for (size_t i = 0; i < m_maxActiveIndex; ++i) { + auto& regionPlayState = m_activeSounds[i]; + if (regionPlayState.active()) { regionPlayState.processTrigger(trigger); + // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is + if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } } } } @@ -78,18 +85,36 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg bool SfzRegion::play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames) { bool anythingPlayed = false; - for (auto& regionPlayState : m_activeSounds) + for (size_t i = 0; i < m_maxActiveIndex; ++i) { - anythingPlayed = anythingPlayed || regionPlayState.play(temporaryBuffer, frames); - if (anythingPlayed) + auto& regionPlayState = m_activeSounds[i]; + + if (!regionPlayState.active()) { continue; } + + bool regionPlayStateProducedSound = regionPlayState.play(temporaryBuffer, frames); + // If the play state deactivated during playback, and this was the max active index, figure out what the new max active index is + if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } + + if (regionPlayStateProducedSound) { - for (f_cnt_t f = 0; f < frames; ++f) { workingBuffer[f] += temporaryBuffer[f]; } + for (f_cnt_t f = 0; f < frames; ++f) { workingBuffer[f] += temporaryBuffer[f]; } // TODO don't use temp buffer } + anythingPlayed = anythingPlayed || regionPlayStateProducedSound; } return anythingPlayed; } +void SfzRegion::recalculateMaxActiveIndex() +{ + // Loop backward from the old max active index to find the next play state which is active + while (m_maxActiveIndex > 0) + { + if (m_activeSounds[m_maxActiveIndex].active()) { return; } + else { m_maxActiveIndex--; } + } +} + bool SfzRegion::initializeSample(const QDir& parentDirectory) { diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 4c25117adfb..c6db82f42bf 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -47,6 +47,14 @@ class SfzRegion : public SfzOpcodeState //! Sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first Sample m_sample; + + //! Maximum active index in the play state array + //! By always spawning new sounds at the lowest open index and keeping track of the maximum index which contains an actice sound, + //! you only need to loop through the first n elements and ignore the rest since you know they are inactive (Thanks to Lost Robot for the idea) + size_t m_maxActiveIndex = 0; + + //! Helper function to figure out what the maximum active index is, in the event the maximum index deacticated + void recalculateMaxActiveIndex(); }; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 69bb000bb0a..98f9a0332ab 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -26,7 +26,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const f_cnt_t framesToPlay = frames - startFrameOffset; // Initially render the sample into the buffer - // Sample::play returns whether the sample completed playback or not + // TODO what about if other stuff is left in buffer? m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay); for (f_cnt_t f = 0; f < frames; ++f) @@ -40,7 +40,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) { - if (trigger.type() == SfzTrigger::Type::NoteOff) + if (trigger.key() == m_trigger.key() && trigger.type() == SfzTrigger::Type::NoteOff) { m_active = false; // testing } From b3e20291edccb37e7e410faa95ab44041b38ac1f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 21 Dec 2025 15:33:04 -0500 Subject: [PATCH 015/131] Fix a couple bugs --- plugins/SfzSampler/SfzOpcodeState.cpp | 10 +++++----- plugins/SfzSampler/SfzRegion.cpp | 6 +++--- plugins/SfzSampler/SfzSampler.cpp | 14 +++++++++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 9aa82d91952..5658af1f239 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -99,10 +99,7 @@ int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) QString key = keyString.chopped(1); int keyOffset = 0; - if (key == "a") { keyOffset = -3; } - else if (key == "a#" || key == "bb") { keyOffset = -2; } - else if (key == "b") { keyOffset = -1; } - else if (key == "c") { keyOffset = 0; } // C is 0 since that's where the octaves change + if (key == "c") { keyOffset = 0; } // C is 0 since that's where the octaves change else if (key == "c#" || key == "db") { keyOffset = 1; } else if (key == "d") { keyOffset = 2; } else if (key == "d#" || key == "eb") { keyOffset = 3; } @@ -111,6 +108,9 @@ int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) else if (key == "f#" || key == "gb") { keyOffset = 6; } else if (key == "g") { keyOffset = 7; } else if (key == "g#" || key == "ab") { keyOffset = 8; } + else if (key == "a") { keyOffset = 9; } + else if (key == "a#" || key == "bb") { keyOffset = 10; } + else if (key == "b") { keyOffset = 11; } else { *successful = false; @@ -118,7 +118,7 @@ int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) return -1; } *successful = true; - // For some reason, C1 is midi key 21, so eveything is offset by 24 + // For some reason, C1 is midi key 24, so eveything is offset by 24 return 24 + keyOffset + 12 * (octave - 1); } diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index b90d00cfb74..89f784e5596 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -52,7 +52,7 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg { // Loop through array to find open position bool foundOpenPosition = false; - for (size_t i = 0; i < m_activeSounds.size(); ++i) + for (size_t i = 0; i <= m_activeSounds.size(); ++i) { auto& regionPlayState = m_activeSounds[i]; if (!regionPlayState.active()) @@ -68,7 +68,7 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg } // Loop through all the active sounds to check if any need to be deactivated/released by the trigger - for (size_t i = 0; i < m_maxActiveIndex; ++i) + for (size_t i = 0; i <= m_maxActiveIndex; ++i) { auto& regionPlayState = m_activeSounds[i]; @@ -85,7 +85,7 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg bool SfzRegion::play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames) { bool anythingPlayed = false; - for (size_t i = 0; i < m_maxActiveIndex; ++i) + for (size_t i = 0; i <= m_maxActiveIndex; ++i) { auto& regionPlayState = m_activeSounds[i]; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index cb12d9807c1..4df61ae2d1f 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -151,11 +151,15 @@ void SfzSampler::loadFile(const QString& filePath) qDebug() << "was okay?" << successfulParseFile; qDebug() << "num regions" << m_sfzRegions.size(); - qDebug() << "first region sample" << m_sfzRegions[0].m_sampleFile.value_or("aaaa"); - qDebug() << "first region lokey" << m_sfzRegions[0].m_lokey.value_or(-1); - qDebug() << "first region hikey" << m_sfzRegions[0].m_hikey.value_or(-1); - qDebug() << "first region lovel" << m_sfzRegions[0].m_lovel.value_or(-1); - qDebug() << "first region hivel" << m_sfzRegions[0].m_hivel.value_or(-1); + for (auto& region : m_sfzRegions) + { + qDebug() << "------"; + qDebug() << "region sample" << region.m_sampleFile.value_or("aaaa"); + qDebug() << "region lokey" << region.m_lokey.value_or(-1); + qDebug() << "region hikey" << region.m_hikey.value_or(-1); + qDebug() << "region lovel" << region.m_lovel.value_or(-1); + qDebug() << "region hivel" << region.m_hivel.value_or(-1); + } // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them From 07e9f289153590cb064917c799597844595a6037 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 21 Dec 2025 15:55:00 -0500 Subject: [PATCH 016/131] Add pitch keytrack --- plugins/SfzSampler/SfzOpcodeState.cpp | 5 +++++ plugins/SfzSampler/SfzOpcodeState.h | 1 + plugins/SfzSampler/SfzRegionPlayState.cpp | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 5658af1f239..d79a9f41aa3 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -48,6 +48,11 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_pitch_keycenter = value.toInt(&successful); if (!successful) { m_pitch_keycenter = keyNumFromString(value, &successful); } } + else if (name == "pitch_keytrack") + { + m_pitch_keytrack = value.toInt(&successful); + if (!successful) { m_pitch_keytrack = keyNumFromString(value, &successful); } + } else if (name == "ampeg_delay") { diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 06052f4895b..79d704d36e2 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -26,6 +26,7 @@ class SfzOpcodeState std::optional m_hivel = 127; std::optional m_pitch_keycenter = 60; + std::optional m_pitch_keytrack = 100; std::optional m_ampeg_delay = 0; std::optional m_ampeg_attack = 0; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 98f9a0332ab..672b4b91114 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -24,10 +24,14 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // If the start is within this buffer, get the number of frames until it starts const f_cnt_t startFrameOffset = std::max(0, -m_frameCount); const f_cnt_t framesToPlay = frames - startFrameOffset; + + // Calculate pitch difference relative to original sample + const int semitoneDifference = m_trigger.key().value_or(-1) - m_region->m_pitch_keycenter.value_or(-1); + const float freqRatio = std::exp2(-semitoneDifference * m_region->m_pitch_keytrack.value_or(-1) / 1200.0f); // Initially render the sample into the buffer // TODO what about if other stuff is left in buffer? - m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay); + m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay, Sample::Loop::Off, freqRatio); for (f_cnt_t f = 0; f < frames; ++f) { From 9587c53025a5b9d647c1223d062162d70d2e96d4 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:49:37 -0500 Subject: [PATCH 017/131] Add amplitude envelope generator --- plugins/SfzSampler/SfzRegionPlayState.cpp | 60 ++++++++++++++++++++++- plugins/SfzSampler/SfzRegionPlayState.h | 3 ++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 672b4b91114..44ed0213b9e 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -4,6 +4,8 @@ #include "SfzRegion.h" +#include "lmms_math.h" + namespace lmms { @@ -15,6 +17,44 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger } + +float SfzRegionPlayState::envelopeGenerator(const f_cnt_t delay, const f_cnt_t attack, const f_cnt_t hold, const f_cnt_t decay, const float sustain, const f_cnt_t release) const +{ + // If the note hasn't started yet, don't do anything + if (m_frameCount < 0) { return 0.0f; } + // If the note has already been released, return the release amplitude + if (m_released && static_cast(m_frameCount) > m_releaseFrame) + { + // According to https://sfzformat.com/tutorials/envelope_generators/, release and decay follow exponential curves (linear in dB) which go from 0 to -90 dB + return dbfsToAmp(-90 * static_cast(m_frameCount - m_releaseFrame) / release); + } + // If it hasn't been released yet, do the normal envelope shape + else if (static_cast(m_frameCount) < delay) + { + return 0.f; + } + else if (static_cast(m_frameCount) < delay + attack) + { + return static_cast(m_frameCount - delay) / attack; + } + else if (static_cast(m_frameCount) < delay + attack + hold) + { + return 1.0f; + } + else if (static_cast(m_frameCount) < delay + attack + hold + decay) + { + // Follow an exponential curve from 0 to -90 dB, but stop at the sustain value + return std::max(sustain, dbfsToAmp(-90 * static_cast(m_frameCount - (delay + attack + hold)) / decay)); + } + else + { + return sustain; + } +} + + + + bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) { // If the sound is not active (note was released, off_by triggered, etc) then don't render any audio @@ -33,11 +73,28 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // TODO what about if other stuff is left in buffer? m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay, Sample::Loop::Off, freqRatio); + // Apply amplitude envelope + const float sampleRate = Engine::audioEngine()->outputSampleRate(); for (f_cnt_t f = 0; f < frames; ++f) { + float ampeg = envelopeGenerator( + m_region->m_ampeg_delay.value_or(0) * sampleRate, + m_region->m_ampeg_attack.value_or(0) * sampleRate, + m_region->m_ampeg_hold.value_or(0) * sampleRate, + m_region->m_ampeg_decay.value_or(0) * sampleRate, + m_region->m_ampeg_sustain.value_or(0) / 100.0f, // Sustain is stored in percent, so divide by 100 to get ratio + m_region->m_ampeg_release.value_or(0) * sampleRate + ); + buffer[f] *= ampeg; m_frameCount++; } + // Deactive the voice if it has been released and the release has finished + if (m_released && m_frameCount - m_releaseFrame > m_region->m_ampeg_release.value_or(0) * sampleRate) + { + m_active = false; + } + return true; } @@ -46,7 +103,8 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) { if (trigger.key() == m_trigger.key() && trigger.type() == SfzTrigger::Type::NoteOff) { - m_active = false; // testing + m_released = true; + m_releaseFrame = m_frameCount; // testing } } diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index c15a1c5289c..7b2c42b7f7e 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -29,6 +29,9 @@ class SfzRegionPlayState bool active() const { return m_active; } + //! Helper function for calculating the envelope value at the current frame + float envelopeGenerator(const f_cnt_t delay, const f_cnt_t attack, const f_cnt_t hold, const f_cnt_t decay, const float sustain, const f_cnt_t release) const; + private: //! Stores whether this object still represents a sound which exists. //! This will be true when the sound is active, but will become false once the release has ended or it has been forcefully deactivated. From bc3b4eb4b0c434f8a16cba0cb2b369e1516c4e0b Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:53:23 -0500 Subject: [PATCH 018/131] Use value instead of value_or --- plugins/SfzSampler/SfzRegion.cpp | 12 ++++++------ plugins/SfzSampler/SfzRegionPlayState.cpp | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 89f784e5596..98d1d768f06 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -25,19 +25,19 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf { if (trigger.type() == SfzTrigger::Type::NoteOn) { - int triggerKey = trigger.key().value_or(-1); - int triggerVelocity = trigger.velocity().value_or(-1); + int triggerKey = trigger.key().value(); + int triggerVelocity = trigger.velocity().value(); // `key` opcode if (m_key != std::nullopt) { - if (triggerKey != m_key.value_or(-1)) { return false; } + if (triggerKey != m_key.value()) { return false; } } // `lokey` and `hikey` opcodes - if (triggerKey > m_hikey.value_or(-1) || triggerKey < m_lokey.value_or(-1)) { return false; } + if (triggerKey > m_hikey.value() || triggerKey < m_lokey.value()) { return false; } // `lovel` and `hivel` opcodes - if (triggerVelocity > m_hivel.value_or(-1) || triggerVelocity < m_lovel.value_or(-1)) { return false; } + if (triggerVelocity > m_hivel.value() || triggerVelocity < m_lovel.value()) { return false; } // If all the contitions passed, return true and spawn a sound return true; @@ -125,7 +125,7 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory) return false; } - if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_sampleFile.value_or("")))) + if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_sampleFile.value()))) { m_sample = Sample(std::move(buffer)); return true; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 44ed0213b9e..d336d7352e2 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -66,8 +66,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const f_cnt_t framesToPlay = frames - startFrameOffset; // Calculate pitch difference relative to original sample - const int semitoneDifference = m_trigger.key().value_or(-1) - m_region->m_pitch_keycenter.value_or(-1); - const float freqRatio = std::exp2(-semitoneDifference * m_region->m_pitch_keytrack.value_or(-1) / 1200.0f); + const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); + const float freqRatio = std::exp2(-semitoneDifference * m_region->m_pitch_keytrack.value() / 1200.0f); // Initially render the sample into the buffer // TODO what about if other stuff is left in buffer? @@ -78,19 +78,19 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) for (f_cnt_t f = 0; f < frames; ++f) { float ampeg = envelopeGenerator( - m_region->m_ampeg_delay.value_or(0) * sampleRate, - m_region->m_ampeg_attack.value_or(0) * sampleRate, - m_region->m_ampeg_hold.value_or(0) * sampleRate, - m_region->m_ampeg_decay.value_or(0) * sampleRate, - m_region->m_ampeg_sustain.value_or(0) / 100.0f, // Sustain is stored in percent, so divide by 100 to get ratio - m_region->m_ampeg_release.value_or(0) * sampleRate + m_region->m_ampeg_delay.value() * sampleRate, + m_region->m_ampeg_attack.value() * sampleRate, + m_region->m_ampeg_hold.value() * sampleRate, + m_region->m_ampeg_decay.value() * sampleRate, + m_region->m_ampeg_sustain.value() / 100.0f, // Sustain is stored in percent, so divide by 100 to get ratio + m_region->m_ampeg_release.value() * sampleRate ); buffer[f] *= ampeg; m_frameCount++; } // Deactive the voice if it has been released and the release has finished - if (m_released && m_frameCount - m_releaseFrame > m_region->m_ampeg_release.value_or(0) * sampleRate) + if (m_released && m_frameCount - m_releaseFrame > m_region->m_ampeg_release.value() * sampleRate) { m_active = false; } From b26da76403af70d4c5cb52a7faef404742873ba0 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:14:57 -0500 Subject: [PATCH 019/131] Profiling statments, and optimizations --- plugins/SfzSampler/SfzRegionPlayState.cpp | 41 ++++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index d336d7352e2..791e1c5feae 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -6,6 +6,9 @@ #include "lmms_math.h" +#include "MicroTimer.h" +#include + namespace lmms { @@ -23,7 +26,7 @@ float SfzRegionPlayState::envelopeGenerator(const f_cnt_t delay, const f_cnt_t a // If the note hasn't started yet, don't do anything if (m_frameCount < 0) { return 0.0f; } // If the note has already been released, return the release amplitude - if (m_released && static_cast(m_frameCount) > m_releaseFrame) + if (m_released && m_frameCount > m_releaseFrame) { // According to https://sfzformat.com/tutorials/envelope_generators/, release and decay follow exponential curves (linear in dB) which go from 0 to -90 dB return dbfsToAmp(-90 * static_cast(m_frameCount - m_releaseFrame) / release); @@ -59,6 +62,14 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) { // If the sound is not active (note was released, off_by triggered, etc) then don't render any audio if (!m_active) { return false; } + + static int totalMicroseconds = 0; + static int totalSquaredMicroseconds = 0; + static int totalCalls = 0; + static int minElapsed = 10000000; + static int maxElapsed = 0; + MicroTimer profiler; + // If the initial m_frameCount is negative, that means the note hasn't started yet if (m_frameCount < -static_cast(frames)) { m_frameCount += frames; return false; } // If the note doesn't start in this buffer, don't play anything // If the start is within this buffer, get the number of frames until it starts @@ -73,28 +84,40 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // TODO what about if other stuff is left in buffer? m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay, Sample::Loop::Off, freqRatio); - // Apply amplitude envelope const float sampleRate = Engine::audioEngine()->outputSampleRate(); + + // Amplitude envelope + const f_cnt_t ampegDelayFrames = m_region->m_ampeg_delay.value() * sampleRate; + const f_cnt_t ampegAttackFrames = m_region->m_ampeg_attack.value() * sampleRate; + const f_cnt_t ampegHoldFrames = m_region->m_ampeg_hold.value() * sampleRate; + const f_cnt_t ampegDecayFrames = m_region->m_ampeg_decay.value() * sampleRate; + const float ampegSustain = m_region->m_ampeg_sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = m_region->m_ampeg_release.value() * sampleRate; + for (f_cnt_t f = 0; f < frames; ++f) { float ampeg = envelopeGenerator( - m_region->m_ampeg_delay.value() * sampleRate, - m_region->m_ampeg_attack.value() * sampleRate, - m_region->m_ampeg_hold.value() * sampleRate, - m_region->m_ampeg_decay.value() * sampleRate, - m_region->m_ampeg_sustain.value() / 100.0f, // Sustain is stored in percent, so divide by 100 to get ratio - m_region->m_ampeg_release.value() * sampleRate + ampegDelayFrames, + ampegAttackFrames, + ampegHoldFrames, + ampegDecayFrames, + ampegSustain, + ampegReleaseFrames ); buffer[f] *= ampeg; m_frameCount++; } // Deactive the voice if it has been released and the release has finished - if (m_released && m_frameCount - m_releaseFrame > m_region->m_ampeg_release.value() * sampleRate) + if (m_released && static_cast(m_frameCount - m_releaseFrame) > ampegReleaseFrames) { m_active = false; } + int elapsed = profiler.elapsed(); totalMicroseconds += elapsed; totalSquaredMicroseconds += elapsed * elapsed; totalCalls++; minElapsed = std::min(minElapsed, elapsed); maxElapsed = std::max(maxElapsed, elapsed); + float mean = 1.0f * totalMicroseconds / totalCalls, variance = (1.0f * totalSquaredMicroseconds - 1.0f * totalMicroseconds * totalMicroseconds / totalCalls / totalCalls) / totalCalls; + qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); + return true; } From 17c46063675d5c246a4bffba320cdc8074f2812c Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:08:11 -0500 Subject: [PATCH 020/131] Use custom sample buffer implementation --- plugins/SfzSampler/CMakeLists.txt | 3 ++ plugins/SfzSampler/SfzRegion.cpp | 13 ++----- plugins/SfzSampler/SfzRegion.h | 7 ++-- plugins/SfzSampler/SfzRegionPlayState.cpp | 17 ++++---- plugins/SfzSampler/SfzRegionPlayState.h | 4 +- plugins/SfzSampler/SfzSampleBuffer.cpp | 47 +++++++++++++++++++++++ plugins/SfzSampler/SfzSampleBuffer.h | 34 ++++++++++++++++ plugins/SfzSampler/SfzSampler.cpp | 8 +--- plugins/SfzSampler/SfzSampler.h | 2 - 9 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 plugins/SfzSampler/SfzSampleBuffer.cpp create mode 100644 plugins/SfzSampler/SfzSampleBuffer.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 67500a74236..da0777392ea 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -18,6 +18,8 @@ build_plugin(sfzsampler SfzGlobalState.h SfzTrigger.cpp SfzTrigger.h + SfzSampleBuffer.cpp + SfzSampleBuffer.h MOCFILES SfzSampler.h SfzSamplerView.h @@ -28,5 +30,6 @@ build_plugin(sfzsampler SfzRegionPlayState.h SfzGlobalState.h SfzTrigger.h + SfzSampleBuffer.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" ) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 98d1d768f06..90ce960247a 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -82,24 +82,17 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg } -bool SfzRegion::play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames) +bool SfzRegion::play(SampleFrame* workingBuffer, const fpp_t frames) { bool anythingPlayed = false; for (size_t i = 0; i <= m_maxActiveIndex; ++i) { auto& regionPlayState = m_activeSounds[i]; - if (!regionPlayState.active()) { continue; } - bool regionPlayStateProducedSound = regionPlayState.play(temporaryBuffer, frames); + anythingPlayed = regionPlayState.play(workingBuffer, frames) || anythingPlayed; // If the play state deactivated during playback, and this was the max active index, figure out what the new max active index is if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } - - if (regionPlayStateProducedSound) - { - for (f_cnt_t f = 0; f < frames; ++f) { workingBuffer[f] += temporaryBuffer[f]; } // TODO don't use temp buffer - } - anythingPlayed = anythingPlayed || regionPlayStateProducedSound; } return anythingPlayed; } @@ -127,7 +120,7 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory) if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_sampleFile.value()))) { - m_sample = Sample(std::move(buffer)); + m_sample = SfzSampleBuffer(buffer->data(), buffer->size(), buffer->sampleRate()); return true; } return false; diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index c6db82f42bf..b662737ff65 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -6,6 +6,7 @@ #include "SfzGlobalState.h" #include "SfzTrigger.h" #include "SfzRegionPlayState.h" +#include "SfzSampleBuffer.h" #include "Sample.h" @@ -31,14 +32,14 @@ class SfzRegion : public SfzOpcodeState //! Renders sound from each of the active SfzRegionPlayStates and writes it to the given buffer //! Returns true if any sound was actually generated - bool play(SampleFrame* workingBuffer, SampleFrame* temporaryBuffer, const fpp_t frames); + bool play(SampleFrame* workingBuffer, const fpp_t frames); //! Load the sample file given by the `sample` opcode into m_sample. //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed //! Returns true if successful bool initializeSample(const QDir& parentDirectory); - const Sample& sample() const { return m_sample; } + const SfzSampleBuffer& sample() const { return m_sample; } private: static constexpr int MAX_ACTIVE_SOUNDS = 128; @@ -46,7 +47,7 @@ class SfzRegion : public SfzOpcodeState std::array m_activeSounds; //! Sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first - Sample m_sample; + SfzSampleBuffer m_sample; //! Maximum active index in the play state array //! By always spawning new sounds at the lowest open index and keeping track of the maximum index which contains an actice sound, diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 791e1c5feae..ed80f2ea330 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -78,13 +78,14 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // Calculate pitch difference relative to original sample const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); - const float freqRatio = std::exp2(-semitoneDifference * m_region->m_pitch_keytrack.value() / 1200.0f); - - // Initially render the sample into the buffer - // TODO what about if other stuff is left in buffer? - m_region->sample().play(buffer + startFrameOffset, &m_samplePlaybackState, framesToPlay, Sample::Loop::Off, freqRatio); + const float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack.value() / 1200.0f); + // Sample rate of sample + const float sampleSampleRate = m_region->sample().sampleRate(); + // Sample rate of LMMS const float sampleRate = Engine::audioEngine()->outputSampleRate(); + // Play the sample faster/slower to match the correct sample rate + freqRatio *= sampleSampleRate / sampleRate; // Amplitude envelope const f_cnt_t ampegDelayFrames = m_region->m_ampeg_delay.value() * sampleRate; @@ -96,7 +97,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) for (f_cnt_t f = 0; f < frames; ++f) { - float ampeg = envelopeGenerator( + const float ampeg = envelopeGenerator( ampegDelayFrames, ampegAttackFrames, ampegHoldFrames, @@ -104,7 +105,9 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ampegSustain, ampegReleaseFrames ); - buffer[f] *= ampeg; + buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg; + buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg; + m_sampleFrame = std::min(static_cast(m_region->sample().size()), m_sampleFrame + freqRatio); m_frameCount++; } diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 7b2c42b7f7e..cd1fc852cad 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -44,8 +44,8 @@ class SfzRegionPlayState //! The frame at which the note was released, relative to the start of the note int m_releaseFrame = 0; - //! In order to play a Sample, we have to keep a persistance Sample::PlaybackState object which holds things likethe current frame position in the sample, etc - Sample::PlaybackState m_samplePlaybackState; + //! Stores the current frame index being played in the region's sample. This is a float, since interpolation is done to change pitch/sample rate + float m_sampleFrame = 0; //! The trigger event which caused this sound SfzTrigger m_trigger; diff --git a/plugins/SfzSampler/SfzSampleBuffer.cpp b/plugins/SfzSampler/SfzSampleBuffer.cpp new file mode 100644 index 00000000000..53d1bcf9cbc --- /dev/null +++ b/plugins/SfzSampler/SfzSampleBuffer.cpp @@ -0,0 +1,47 @@ + + +#include "SfzSampleBuffer.h" +#include "interpolation.h" + +namespace lmms +{ + + +SfzSampleBuffer::SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate) + : m_data(std::make_shared(size)) + , m_size(size) + , m_sampleRate(sampleRate) +{ + for (f_cnt_t f = 0; f < size; ++f) + { + for (size_t channel = 0; channel < NUM_CHANNELS; ++channel) + { + m_data[f][channel] = data[f][channel]; + } + } +} + + +float SfzSampleBuffer::at(const float index, const size_t channel) const +{ + if (index < 0 || index >= m_size) { return 0.0f; } + + const size_t indexFloor = static_cast(index); + + float frac = index - indexFloor; + + size_t i0 = indexFloor == 0 ? 0 : indexFloor - 1; + size_t i1 = indexFloor; + size_t i2 = std::min(indexFloor + 1, m_size - 1); + size_t i3 = std::min(indexFloor + 2, m_size - 1); + + float v0 = m_data[i0][channel]; + float v1 = m_data[i1][channel]; + float v2 = m_data[i2][channel]; + float v3 = m_data[i3][channel]; + + return hermiteInterpolate(v0, v1, v2, v3, frac); +} + + +} // namespace lmms diff --git a/plugins/SfzSampler/SfzSampleBuffer.h b/plugins/SfzSampler/SfzSampleBuffer.h new file mode 100644 index 00000000000..041fc2dbbad --- /dev/null +++ b/plugins/SfzSampler/SfzSampleBuffer.h @@ -0,0 +1,34 @@ + +#ifndef LMMS_SFZ_SAMPLE_BUFFER_H +#define LMMS_SFZ_SAMPLE_BUFFER_H + +#include "SampleFrame.h" +#include + +namespace lmms +{ + +class SampleFrame; + +class SfzSampleBuffer +{ +public: + SfzSampleBuffer() = default; + SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate); + + float at(const float index, const size_t channel) const; + + f_cnt_t size() const { return m_size; } + + float sampleRate() const { return m_sampleRate; } + +private: + static constexpr const size_t NUM_CHANNELS = 2; + std::shared_ptr m_data; + f_cnt_t m_size; + float m_sampleRate; +}; + +} // namespace lmms + +#endif // LMMS_SFZ_SAMPLE_BUFFER_H diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 4df61ae2d1f..965349e4eb1 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -64,7 +64,6 @@ PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) - , m_tempBuffer(std::make_unique(Engine::audioEngine()->framesPerPeriod())) , m_parentTrack(instrumentTrack) { QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; @@ -132,12 +131,7 @@ void SfzSampler::play(SampleFrame* workingBuffer) // Render audio from each of the regions // This amounts to the regions themselves rendering the audio from each of their active SfzRegionPlayStates // We pass a temporary buffer which can be used for rendering samples and later summing it to the working buffer. - bool anythingPlayed = region.play(workingBuffer, m_tempBuffer.get(), frames); - // The buffer cleared and reused for each region so that the regions don't have to worry about allocating it. - if (anythingPlayed) - { - for (f_cnt_t f = 0; f < frames; ++f) { m_tempBuffer[f] = SampleFrame{0.0f}; } // TODO can we avoid this? - } + bool anythingPlayed = region.play(workingBuffer, frames); } } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 69e4d5f74c9..3482c03ff98 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -62,8 +62,6 @@ class SfzSampler : public Instrument private: void processTrigger(const SfzTrigger& trigger); - std::unique_ptr m_tempBuffer; - InstrumentTrack* m_parentTrack; //! Holds information about the total number of notes active, last switch keys pressed, etc From 4d61b10da97a0108303bae97058c1bb7206109b1 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:09:50 -0500 Subject: [PATCH 021/131] Add #includes and #defines to sfz parser --- plugins/SfzSampler/SfzParser.cpp | 91 ++++++++++++++++++++++- plugins/SfzSampler/SfzParser.h | 5 ++ plugins/SfzSampler/SfzRegionPlayState.cpp | 2 +- plugins/SfzSampler/SfzSampler.cpp | 5 +- 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 7d0fadc8dfa..f9c0b13a396 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -2,7 +2,6 @@ #include "SfzParser.h" #include "SfzOpcodeState.h" -#include #include #include #include @@ -26,13 +25,21 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou return false; } + QString fileContents = file.readAll(); + + // Before parsing the headers and opcodes, we need to hande #include and #define statements + // This amounts to recursively loading and copy/pasting the contents of the other files where the #include is, and find/replacing the #define words with their values + fileContents = recursiveHandleIncludeAndDefineStatements(parentDirectory, fileContents); + + qDebug().noquote() << "TESTING: Loaded SFZ:\n" << fileContents; // testing + + // Now that all the includes/defines are handled, loop std::vector parsedSegments; - while (!file.atEnd()) + for (QString line : fileContents.split("\n")) { - QString line = file.readLine(); // Remove comments from end of line - line = line.split("\\")[0]; + line = line.split("//")[0]; // Split the line on whitespace to extract header and opcodes keywords // Fortunately, the SFZ format specifically states that opcode assignments cannot contain spaces, so there is no risk // of accidentally splitting the opcode name from the value. @@ -153,4 +160,80 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } + + + +QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map defineMap) +{ + // Reconstruct the file line by line as we parse the defines and includes + QStringList reconstructedSegments; + // We have to handle the defines in two loops (one before and one after the includes), since + // some people use defined $keywords in their include paths, but we also want the defines to affect the text from the included files + for (QString line : fileContents.split("\n")) + { + if (line.startsWith("#define")) + { + // Split on whitespace + const auto segments = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + // A define statement should have 3 parts, the #define, the $keyword, and the value + if (segments.size() != 3) + { + qDebug() << "[SFZ Parser] Ill-formed define statment:" << line; + continue; + } + const QString keyword = segments[1]; + const QString replacement = segments[2]; + defineMap[keyword] = replacement; + // A define keyword should probably start with a $. I couldn't find a requirement for this, but let's warn the user anyway + if (keyword.front() != "$") { qDebug() << "[SFZ Parser] Warning: Define keyword does not start with $:" << line; } + } + else if (line.startsWith("#include")) + { + // Replace any of the defined keywords before parsing the include path, since some SFZ files use $keywords in them + for (const auto& [keyword, replacement] : defineMap) + { + line.replace(keyword, replacement); + } + + const auto segments = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + // An include statement should have two parts, the #include and the path + if (segments.size() != 2) + { + qDebug() << "[SFZ Parser] Ill-formed include statment:" << line; + continue; + } + + QString relativePath = segments[1]; + relativePath.replace("\"", ""); // Remove " " from start and end + const QString absolutePath = parentDirectory.absoluteFilePath(relativePath); + + QFile file(absolutePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + qDebug() << "[SFZ Parser] Could not read included file:" << absolutePath << "from include statement:" << line; + continue; + } + + QString includedFileContents = file.readAll(); + + // Resolve any includes and defines in this new file too before pasting it in + includedFileContents = recursiveHandleIncludeAndDefineStatements(parentDirectory, includedFileContents, defineMap); + reconstructedSegments.push_back(includedFileContents); + } + else + { + // Replace any of the defined keywords before adding the line to the reconstructed file + for (const auto& [keyword, replacement] : defineMap) + { + line.replace(keyword, replacement); + } + reconstructedSegments.push_back(line); + } + } + + return reconstructedSegments.join("\n"); +} + + + } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index bd6ef0fb2a2..daf2e79f700 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -4,6 +4,7 @@ #include "SfzRegion.h" #include +#include #include namespace lmms @@ -13,6 +14,10 @@ class SfzParser { public: static bool parseSfzFile(const QString& filePath, std::vector& outputRegions); + + //! Helper function to parse any #include or #define statements from a string, recursively so that the included files have their includes and defines handled too + //! The defineMap parameter should be left blank, since it's only there to internally keep track of what $keywords are defined to be what as the recursion goes down each path + static QString recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map defineMap = {}); }; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index ed80f2ea330..3f74ee2b159 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -78,7 +78,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // Calculate pitch difference relative to original sample const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); - const float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack.value() / 1200.0f); + float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack.value() / 1200.0f); // Sample rate of sample const float sampleSampleRate = m_region->sample().sampleRate(); diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 965349e4eb1..a84778afc50 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -66,9 +66,10 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) , m_parentTrack(instrumentTrack) { - QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; + //QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; + //loadFile(path + "_jRhodes-stereo-looped.sfz"); - loadFile(path + "_jRhodes-stereo-looped.sfz"); + loadFile(ConfigManager::inst()->userSamplesDir() + "sfz/SplendidGrandPiano-master/Splendid\ Grand\ Piano.sfz"); auto iph = new InstrumentPlayHandle(this, instrumentTrack); Engine::audioEngine()->addPlayHandle(iph); From ecb99070ee34c3e6afaabc9ccb1fdb8b8f30eccf Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:36:24 -0500 Subject: [PATCH 022/131] Add better code for handling headers, and sample names with spaces --- plugins/SfzSampler/SfzParser.cpp | 162 +++++++++++++++++++++---------- plugins/SfzSampler/SfzParser.h | 12 +++ 2 files changed, 122 insertions(+), 52 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index f9c0b13a396..55fc7d02568 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -21,7 +21,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qDebug() << "[SFZ Parser] Could not read file:" << filePath; + qDebug() << "[SFZ Parser] Error, could not read file:" << filePath; return false; } @@ -41,13 +41,46 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // Remove comments from end of line line = line.split("//")[0]; // Split the line on whitespace to extract header and opcodes keywords - // Fortunately, the SFZ format specifically states that opcode assignments cannot contain spaces, so there is no risk + // Fortunately, the SFZ format specifically states that opcode assignments cannot contain spaces around the =, so there is no risk // of accidentally splitting the opcode name from the value. + line.replace(">", "> "); // Real quick, make sure there is whitespace between header keywords and anything else after them. One .sfz file I found did `curve_index=11` in it,, with no space between, which makes parsing complicated, so to fix it we just insert an extra space. for (QString segment : line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts)) { parsedSegments.push_back(segment); } } + + // Okay so ummm... there's a problem I didn't mention haha :sweat_smile: + // Unfortunately, sample opcode assignments *can* contain spaces and special characters except for "=". This means that the sample opcode lines could have gotten split up by what we juts did :| + // But that's okay. We can just loop through all of the segments, check if they have the sample opcode, and if so, check the next few segments too to see if they might be part of the file name (i.e., don't have = and aren't a header with < > aaround it) + for (size_t i = 0; i < parsedSegments.size(); ++i) + { + if (parsedSegments.at(i).startsWith("sample=")) + { + QStringList samplePathSegments; + // Add the initial part of the sample file, before any spaces + if (parsedSegments.at(i).split("=").size() != 2) + { + qDebug() << "[SFZ Parser] Warning, sample file path starts with a space? That's kind of weird:" << parsedSegments; + samplePathSegments.push_back(""); + } + else + { + samplePathSegments.push_back(parsedSegments.at(i).split("=")[1]); + } + // Look at the next few segments to see if they might be continuations of the sample file path + for (size_t j = i + 1; j < parsedSegments.size();) + { + QString nextSegment = parsedSegments.at(j); + if (nextSegment.contains("=")) { break; } // If there's an equals sign, it's an opcode assignment, not part of the sample file + if (nextSegment.front() == "<" && nextSegment.back() == ">") { break; } // If it has < > around it, it's a header, so not part of the sample file + samplePathSegments.push_back(nextSegment); + parsedSegments.erase(parsedSegments.begin() + j); // Get rid of that segment, since it's part of the file path. Don't increment the index, since everything will shift back. + } + // Now replace the sample opcode segment with the complete filename + parsedSegments.at(i) = "sample=" + samplePathSegments.join(" "); // Technically this doesn't account for samples with double spaces or tabs in the filename + } + } // Now that all the segments are collected, we can go through them all and construct the SfzRegions // First, the header(s) must be found. The SFZ format website does not guarantee that will @@ -87,72 +120,98 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // parsing all the s into SfzRegion objects // Regions can be within s, which can define default opcodes for all the regions inside them - // Keep track of the current group and region states. These will be initialized to whatever the global/group settings are, once we encounter a group ro region header - SfzOpcodeState currentGroupState; - SfzOpcodeState currentRegionState; - - // Track whether we are in global, a group, or a region header - withinGlobal = false; - bool withinGroup = false; - bool withinRegion = false; + // Keep track of the current group and region states. These will be re-initialized to whatever the global/group settings are, once we encounter a new group or region header + SfzOpcodeState currentGroupState = globalState; + SfzOpcodeState currentRegionState = globalState; + + // Track whether we are in global, group, region, or other header + Header currentHeader = Header::None; for (QString segment : parsedSegments) { // Track whether we are entering a new header region if (segment.front() == "<" && segment.back() == ">") { + // If we were previously in a , then wrap it up and add it to the output vector + if (currentHeader == Header::Region) { outputRegions.emplace_back(currentRegionState); } + if (segment == "") { - // If we were previously in a , then wrap it up and add it to the output vector - if (withinRegion) { outputRegions.emplace_back(currentRegionState); } - withinGlobal = true; - withinGroup = false; - withinRegion = false; + currentHeader = Header::Global; // Don't do anything special; we already handled the globals above } else if (segment == "") { - if (withinRegion) { outputRegions.emplace_back(currentRegionState); } - withinGlobal = false; - withinGroup = true; - withinRegion = false; + currentHeader = Header::Group; // Reset the current group settings to the global defaults currentGroupState = globalState; } else if (segment == "") { - if (withinRegion) { outputRegions.emplace_back(currentRegionState); } - withinGlobal = false; - withinGroup = false; - withinRegion = true; + currentHeader = Header::Region; // Reset the current region settings to the group defaults currentRegionState = currentGroupState; } + else if (segment == "") + { + currentHeader = Header::Control; + } + else if (segment == "") + { + currentHeader = Header::Curve; + } else { - qDebug() << "[SFZ Parser] Unknown header type:" << segment; + qDebug() << "[SFZ Parser] Error, unknown header type:" << segment; return false; } - continue; + continue; // If the header is recognized, move to the next line and start parsing things // TODO handle more header types } - // If we are in a group, update the opcodes of the current group state - if (withinGroup) + // If this line/segment isn't a new header, it must be an opcode assignment + // Depending on the current header/zone, opcode assignments are handled differently: + switch (currentHeader) { - auto opcodeNameAndValue = segment.split("="); - if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } - currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); - } - // If we are within a region, update the opcodes of the current region state - if (withinRegion) - { - auto opcodeNameAndValue = segment.split("="); - if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } - currentRegionState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + case Header::Global: + { + // Do nothing. The global headers were already handled above + break; + } + case Header::Group: + { + // If we are in a group, update the opcodes of the current group state + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } + currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + break; + } + case Header::Region: + { + // If we are within a region, update the opcodes of the current region state + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } + currentRegionState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + break; + } + case Header::Control: + { + qDebug() << "[SFZ Parser] Warning, the header has not been implemented yet. Encountered opcode assignment:" << segment; + break; + } + case Header::Curve: + { + qDebug() << "[SFZ Parser] Warning, the header has not been implemented yet. Encountered opcode assignment:" << segment; + break; + } + default: + { + qDebug() << "[SFZ Parser] Error, encountered line within invalid header" << segment; + return false; + } } } // Check one last time in case the file ended with a region and didn't get added - if (withinRegion) { outputRegions.emplace_back(currentRegionState); } + if (currentHeader == Header::Region) { outputRegions.emplace_back(currentRegionState); } // Now that all the opcodes have been parsed into regions and added to the output vector, we are done! // The samples themselves still need to be loaded, but that's a job for later @@ -167,32 +226,31 @@ QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentD { // Reconstruct the file line by line as we parse the defines and includes QStringList reconstructedSegments; - // We have to handle the defines in two loops (one before and one after the includes), since - // some people use defined $keywords in their include paths, but we also want the defines to affect the text from the included files + for (QString line : fileContents.split("\n")) { if (line.startsWith("#define")) { // Split on whitespace const auto segments = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); - // A define statement should have 3 parts, the #define, the $keyword, and the value + // A define statement should have 3 parts, the #define, the $variable name, and the value if (segments.size() != 3) { qDebug() << "[SFZ Parser] Ill-formed define statment:" << line; continue; } - const QString keyword = segments[1]; - const QString replacement = segments[2]; - defineMap[keyword] = replacement; - // A define keyword should probably start with a $. I couldn't find a requirement for this, but let's warn the user anyway - if (keyword.front() != "$") { qDebug() << "[SFZ Parser] Warning: Define keyword does not start with $:" << line; } + const QString variableName = segments[1]; + const QString variableValue = segments[2]; + defineMap[variableName] = variableValue; + // A define variable should probably start with a $ + if (variableName.front() != "$") { qDebug() << "[SFZ Parser] Warning: #define variable name does not start with $:" << line; } } else if (line.startsWith("#include")) { - // Replace any of the defined keywords before parsing the include path, since some SFZ files use $keywords in them - for (const auto& [keyword, replacement] : defineMap) + // Replace any of the defined variables before parsing the include path, since some SFZ files use $variables in them + for (const auto& [variableName, variableValue] : defineMap) { - line.replace(keyword, replacement); + line.replace(variableName, variableValue); } const auto segments = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); @@ -222,10 +280,10 @@ QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentD } else { - // Replace any of the defined keywords before adding the line to the reconstructed file - for (const auto& [keyword, replacement] : defineMap) + // Replace any of the defined variables before adding the line to the reconstructed file + for (const auto& [variableName, variableValue] : defineMap) { - line.replace(keyword, replacement); + line.replace(variableName, variableValue); } reconstructedSegments.push_back(line); } diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index daf2e79f700..6735f6f573c 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -15,9 +15,21 @@ class SfzParser public: static bool parseSfzFile(const QString& filePath, std::vector& outputRegions); +private: //! Helper function to parse any #include or #define statements from a string, recursively so that the included files have their includes and defines handled too //! The defineMap parameter should be left blank, since it's only there to internally keep track of what $keywords are defined to be what as the recursion goes down each path static QString recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map defineMap = {}); + + //! All the possible header types in an sfz file + enum class Header + { + Global, + Group, + Region, + Control, + Curve, + None + }; }; From 86f78936b7813418dfbc534a150ee2d895e48e13 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:44:30 -0500 Subject: [PATCH 023/131] Add default_path opcode --- plugins/SfzSampler/SfzOpcodeState.cpp | 4 ++++ plugins/SfzSampler/SfzOpcodeState.h | 2 ++ plugins/SfzSampler/SfzParser.cpp | 5 ++++- plugins/SfzSampler/SfzRegion.cpp | 3 ++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index d79a9f41aa3..375b0e32fe1 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -17,6 +17,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_sampleFile = value; } + else if (name == "default_path") + { + m_default_path = value; + } else if (name == "key") { diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 79d704d36e2..2a7b7347415 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -17,6 +17,8 @@ class SfzOpcodeState //private: std::optional m_sampleFile = std::nullopt; + std::optional m_default_path = std::nullopt; + std::optional m_key = std::nullopt; std::optional m_lokey = 0; diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 55fc7d02568..290cb75256e 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -195,7 +195,10 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } case Header::Control: { - qDebug() << "[SFZ Parser] Warning, the header has not been implemented yet. Encountered opcode assignment:" << segment; + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } + // This is Wrong. The control header is not the same thing as the global header. Buuuut.... it works. And from sfzformat.com, it sounds like different sfz players do different things with this header anyway, so it's fine. + globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } case Header::Curve: diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 90ce960247a..5da2b87cca7 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -118,7 +118,8 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory) return false; } - if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_sampleFile.value()))) + // TODO is simply adding the default path sufficient? + if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_default_path.value_or("") + m_sampleFile.value()))) { m_sample = SfzSampleBuffer(buffer->data(), buffer->size(), buffer->sampleRate()); return true; From 32f040c489d92ff500fd518dcbff109ed09eaab6 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 25 Dec 2025 19:22:49 -0500 Subject: [PATCH 024/131] Initial CC modulation support --- plugins/SfzSampler/SfzGlobalState.cpp | 4 ++ plugins/SfzSampler/SfzGlobalState.h | 8 ++++ plugins/SfzSampler/SfzOpcodeState.cpp | 32 +++++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 8 +++- plugins/SfzSampler/SfzRegion.cpp | 26 +++++++++++++ plugins/SfzSampler/SfzRegion.h | 10 +++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 47 ++++++++++++----------- plugins/SfzSampler/SfzSampler.cpp | 7 +++- 8 files changed, 118 insertions(+), 24 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index fab57734f2a..86c7cdeb1f7 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -7,6 +7,10 @@ namespace lmms void SfzGlobalState::processTrigger(const SfzTrigger& trigger) { + if (trigger.type() == SfzTrigger::Type::ControlChange) + { + m_ccValues.at(trigger.controlChangeNumber().value()) = trigger.controlChangeValue().value(); + } } int SfzGlobalState::lastKeyPressedInRange(const int lowKey, const int highKey) diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index edca80ffc3b..076799bab62 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -20,6 +20,9 @@ class SfzGlobalState // Returns the midi key number of the last pressed key in the range [lowKey, highKey] int lastKeyPressedInRange(const int lowKey, const int highKey); + //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. + int midiCCValue(const int index, const int defaultValue) const { return m_ccValues.at(index).value_or(defaultValue); } + private: //! Stores a number for every key, tracking which order the keys were played in //! The first key played gets a 1, the second key gets a 2, etc, overwriting when keys are played multiple times/ @@ -28,6 +31,11 @@ class SfzGlobalState //! Keeps track of which keys on the piano are currently being pressed std::array m_activeKeys; + + //! Stores the current value of all the midi CC knobs/controllers + //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 128 to get a float between 0 and 1 works fine. + //! std::optional is used so that sfz file can specify default cc values + std::array, 128> m_ccValues = {}; }; diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 375b0e32fe1..76f12801048 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -4,6 +4,7 @@ #include "SfzOpcodeState.h" #include +#include namespace lmms { @@ -82,6 +83,24 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_ampeg_release = value.toFloat(&successful); } + + else if (name.startsWith("ampeg_release_oncc") || name.startsWith("ampeg_releasecc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_ampeg_release_oncc.at(ccNumber) = value.toFloat(&successful); + } + + else if (name.startsWith("set_cc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_set_cc.at(ccNumber) = value.toInt(&successful); + } + else if (name.startsWith("set_hdcc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_set_cc.at(ccNumber) = value.toFloat(&successful) * 128; + } + else { qDebug() << "[SFZ Parser] Unknown opcode:" << name; @@ -98,6 +117,19 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu +int SfzOpcodeState::ccNumberFromOpcode(const QString& opcode) +{ + // Match for numbers + QRegularExpression re("\\d+"); + QRegularExpressionMatch match = re.match(opcode); + if (!match.hasMatch()) { qDebug() << "[SFZ Parser] Unable to find CC number in opcode:" << opcode; return 0; } + bool successfulToInt = false; + int ccNumber = match.captured(0).toInt(&successfulToInt); + if (!successfulToInt) { qDebug() << "[SFZ Parser] Unable to convert CC number to int:" << opcode; return 0; } + return ccNumber; +} + + int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) { keyString = keyString.toLower(); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 2a7b7347415..376b6107580 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -5,6 +5,7 @@ //#include "SfzOpcodes.h" #include #include +#include namespace lmms { @@ -14,12 +15,13 @@ class SfzOpcodeState public: bool setOpcodeByStrings(const QString& name, const QString& value); static int keyNumFromString(QString keyString, bool* successful); + static int ccNumberFromOpcode(const QString& opcode); //private: std::optional m_sampleFile = std::nullopt; std::optional m_default_path = std::nullopt; - + // TODO maybe these don't all have to be optionals std::optional m_key = std::nullopt; std::optional m_lokey = 0; std::optional m_hikey = 127; @@ -36,6 +38,10 @@ class SfzOpcodeState std::optional m_ampeg_decay = 0; std::optional m_ampeg_sustain = 100; std::optional m_ampeg_release = 0.001; + std::array m_ampeg_release_oncc = {}; + + //! Default midi CC values + std::array m_set_cc = {}; friend class SfzRegion; }; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 5da2b87cca7..1203181dd5d 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -19,6 +19,7 @@ namespace lmms SfzRegion::SfzRegion(SfzOpcodeState opcodeState) : SfzOpcodeState(opcodeState) { + recalculateTotalCCModulation(SfzGlobalState()); // The region objects don't currently have direct access to the global state, so pass in a blank object just to reset the CC modulations to their defaults. } bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) @@ -47,6 +48,12 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigger& trigger) { + // Before spawning an sounds, real quick do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer + if (trigger.type() == SfzTrigger::Type::ControlChange) + { + recalculateTotalCCModulation(globalState); + } + // If the trigger conditions are met, spawn a new sound if (triggerConditionsMet(globalState, trigger)) { @@ -109,6 +116,25 @@ void SfzRegion::recalculateMaxActiveIndex() } + +float SfzRegion::totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const +{ + float total = 0.0f; + for (int i = 0; i < 128; ++i) + { + total += ccModulationAmounts[i] * globalState.midiCCValue(i, m_set_cc[i]) / 128.0f; // m_set_cc stores the default CC values for each of the midi controllers + } + return total; +} + +void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) +{ + m_ampeg_release_totalCC = totalCCModulation(m_ampeg_release_oncc, globalState); + // TODO more +} + + + bool SfzRegion::initializeSample(const QDir& parentDirectory) { if (m_sampleFile == std::nullopt) diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index b662737ff65..ca6581dafe1 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -56,6 +56,16 @@ class SfzRegion : public SfzOpcodeState //! Helper function to figure out what the maximum active index is, in the event the maximum index deacticated void recalculateMaxActiveIndex(); + + //! Store the current total midi CC modulation amounts for the different targets, just so that we don't + // have to recalculate them every buffer, instead only when a trigger occurs. + float m_ampeg_release_totalCC = 0.0f; + + //! Helper function to calculate the total modulation of all midi CC controllers on a parameter. Essentially it just multiplies the modulation amounts by the current CC values and adds it all up. + float totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const; + void recalculateTotalCCModulation(const SfzGlobalState& globalState); + + friend class SfzRegionPlayState; // TODO this was just to make it easy but...? }; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 3f74ee2b159..e80dc4336d3 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -25,34 +25,38 @@ float SfzRegionPlayState::envelopeGenerator(const f_cnt_t delay, const f_cnt_t a { // If the note hasn't started yet, don't do anything if (m_frameCount < 0) { return 0.0f; } - // If the note has already been released, return the release amplitude - if (m_released && m_frameCount > m_releaseFrame) + // There is the possibility that the note may have released during attack, decay, etc, so we may need to multiply the release and the normal env values together to get a smooth output + float normalValue = 1.0f; + float releaseValue = 1.0f; + // Calculate the normal envelope value + if (static_cast(m_frameCount) < delay) { - // According to https://sfzformat.com/tutorials/envelope_generators/, release and decay follow exponential curves (linear in dB) which go from 0 to -90 dB - return dbfsToAmp(-90 * static_cast(m_frameCount - m_releaseFrame) / release); - } - // If it hasn't been released yet, do the normal envelope shape - else if (static_cast(m_frameCount) < delay) - { - return 0.f; + normalValue = 0.f; } else if (static_cast(m_frameCount) < delay + attack) { - return static_cast(m_frameCount - delay) / attack; + normalValue = static_cast(m_frameCount - delay) / attack; } else if (static_cast(m_frameCount) < delay + attack + hold) { - return 1.0f; + normalValue = 1.0f; } else if (static_cast(m_frameCount) < delay + attack + hold + decay) { // Follow an exponential curve from 0 to -90 dB, but stop at the sustain value - return std::max(sustain, dbfsToAmp(-90 * static_cast(m_frameCount - (delay + attack + hold)) / decay)); + normalValue = std::max(sustain, dbfsToAmp(-90 * static_cast(m_frameCount - (delay + attack + hold)) / decay)); } else { - return sustain; + normalValue = sustain; } + // If the note has already been released, find the release amplitude + if (m_released && m_frameCount > m_releaseFrame) + { + // According to https://sfzformat.com/tutorials/envelope_generators/, release and decay follow exponential curves (linear in dB) which go from 0 to -90 dB + releaseValue = dbfsToAmp(-90 * static_cast(m_frameCount - m_releaseFrame) / release); + } + return releaseValue * normalValue; } @@ -88,12 +92,12 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) freqRatio *= sampleSampleRate / sampleRate; // Amplitude envelope - const f_cnt_t ampegDelayFrames = m_region->m_ampeg_delay.value() * sampleRate; - const f_cnt_t ampegAttackFrames = m_region->m_ampeg_attack.value() * sampleRate; - const f_cnt_t ampegHoldFrames = m_region->m_ampeg_hold.value() * sampleRate; - const f_cnt_t ampegDecayFrames = m_region->m_ampeg_decay.value() * sampleRate; - const float ampegSustain = m_region->m_ampeg_sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = m_region->m_ampeg_release.value() * sampleRate; + const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay.value()) * sampleRate; + const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack.value()) * sampleRate; + const f_cnt_t ampegHoldFrames = (m_region->m_ampeg_hold.value()) * sampleRate; + const f_cnt_t ampegDecayFrames = (m_region->m_ampeg_decay.value()) * sampleRate; + const float ampegSustain = (m_region->m_ampeg_sustain.value()) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg_release.value() + m_region->m_ampeg_release_totalCC) * sampleRate; for (f_cnt_t f = 0; f < frames; ++f) { @@ -119,15 +123,14 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) int elapsed = profiler.elapsed(); totalMicroseconds += elapsed; totalSquaredMicroseconds += elapsed * elapsed; totalCalls++; minElapsed = std::min(minElapsed, elapsed); maxElapsed = std::max(maxElapsed, elapsed); float mean = 1.0f * totalMicroseconds / totalCalls, variance = (1.0f * totalSquaredMicroseconds - 1.0f * totalMicroseconds * totalMicroseconds / totalCalls / totalCalls) / totalCalls; - qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); - + //qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); return true; } void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) { - if (trigger.key() == m_trigger.key() && trigger.type() == SfzTrigger::Type::NoteOff) + if (trigger.key() == m_trigger.key() && trigger.type() == SfzTrigger::Type::NoteOff && !m_released) { m_released = true; m_releaseFrame = m_frameCount; // testing diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index a84778afc50..a9ee513dbe3 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -91,6 +91,11 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ processTrigger(SfzTrigger::noteOffEvent(event.key(), event.velocity())); return true; } + else if (event.type() == MidiControlChange) + { + processTrigger(SfzTrigger::controlChangeEvent(event.controllerNumber(), event.controllerValue())); + return true; + } return false; } @@ -109,7 +114,7 @@ void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) void SfzSampler::processTrigger(const SfzTrigger& trigger) { - // Notify the global state to update which keys are active + // Notify the global state to update which keys are active, update midi CC values, etc m_sfzGlobalState.processTrigger(trigger); // Loop through all the regions to check if a new note should be played From ca0989102fb5a936282f78bbb4d16d83ef2ccd38 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 26 Dec 2025 05:50:16 -0500 Subject: [PATCH 025/131] Add amp_veltrack opcode and clean up some stuff --- plugins/SfzSampler/SfzOpcodeState.cpp | 6 ++++- plugins/SfzSampler/SfzOpcodeState.h | 27 ++++++++++++----------- plugins/SfzSampler/SfzRegion.cpp | 4 ++-- plugins/SfzSampler/SfzRegionPlayState.cpp | 25 ++++++++++++--------- plugins/SfzSampler/SfzSampler.cpp | 9 -------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 76f12801048..c22a7b8b68f 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -56,7 +56,11 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "pitch_keytrack") { m_pitch_keytrack = value.toInt(&successful); - if (!successful) { m_pitch_keytrack = keyNumFromString(value, &successful); } + } + + else if (name == "amp_veltrack") + { + m_amp_veltrack = value.toFloat(&successful); } else if (name == "ampeg_delay") diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 376b6107580..e7855b05017 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -21,23 +21,24 @@ class SfzOpcodeState std::optional m_sampleFile = std::nullopt; std::optional m_default_path = std::nullopt; - // TODO maybe these don't all have to be optionals std::optional m_key = std::nullopt; - std::optional m_lokey = 0; - std::optional m_hikey = 127; + int m_lokey = 0; + int m_hikey = 127; - std::optional m_lovel = 0; - std::optional m_hivel = 127; + int m_lovel = 0; + int m_hivel = 127; - std::optional m_pitch_keycenter = 60; - std::optional m_pitch_keytrack = 100; + int m_pitch_keycenter = 60; + int m_pitch_keytrack = 100; - std::optional m_ampeg_delay = 0; - std::optional m_ampeg_attack = 0; - std::optional m_ampeg_hold = 0; - std::optional m_ampeg_decay = 0; - std::optional m_ampeg_sustain = 100; - std::optional m_ampeg_release = 0.001; + float m_amp_veltrack = 100; + + float m_ampeg_delay = 0; + float m_ampeg_attack = 0; + float m_ampeg_hold = 0; + float m_ampeg_decay = 0; + float m_ampeg_sustain = 100; + float m_ampeg_release = 0.001; std::array m_ampeg_release_oncc = {}; //! Default midi CC values diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 1203181dd5d..284e09d5661 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -35,10 +35,10 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf if (triggerKey != m_key.value()) { return false; } } // `lokey` and `hikey` opcodes - if (triggerKey > m_hikey.value() || triggerKey < m_lokey.value()) { return false; } + if (triggerKey > m_hikey || triggerKey < m_lokey) { return false; } // `lovel` and `hivel` opcodes - if (triggerVelocity > m_hivel.value() || triggerVelocity < m_lovel.value()) { return false; } + if (triggerVelocity > m_hivel || triggerVelocity < m_lovel) { return false; } // If all the contitions passed, return true and spawn a sound return true; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index e80dc4336d3..c735a1ee892 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -81,8 +81,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const f_cnt_t framesToPlay = frames - startFrameOffset; // Calculate pitch difference relative to original sample - const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); - float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack.value() / 1200.0f); + const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter; + float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack / 1200.0f); // Sample rate of sample const float sampleSampleRate = m_region->sample().sampleRate(); @@ -92,12 +92,17 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) freqRatio *= sampleSampleRate / sampleRate; // Amplitude envelope - const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay.value()) * sampleRate; - const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack.value()) * sampleRate; - const f_cnt_t ampegHoldFrames = (m_region->m_ampeg_hold.value()) * sampleRate; - const f_cnt_t ampegDecayFrames = (m_region->m_ampeg_decay.value()) * sampleRate; - const float ampegSustain = (m_region->m_ampeg_sustain.value()) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg_release.value() + m_region->m_ampeg_release_totalCC) * sampleRate; + const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay) * sampleRate; + const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack) * sampleRate; + const f_cnt_t ampegHoldFrames = (m_region->m_ampeg_hold) * sampleRate; + const f_cnt_t ampegDecayFrames = (m_region->m_ampeg_decay) * sampleRate; + const float ampegSustain = (m_region->m_ampeg_sustain) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg_release + m_region->m_ampeg_release_totalCC) * sampleRate; + + // Amplitude due to velocity + // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) + // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. + const float ampVelocity = 2 * ((m_trigger.velocity().value() / 127.0f - 0.5f) * (m_region->m_amp_veltrack / 100) * 0.5f + 0.5f); for (f_cnt_t f = 0; f < frames; ++f) { @@ -109,8 +114,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ampegSustain, ampegReleaseFrames ); - buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg; - buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg; + buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity; + buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity; m_sampleFrame = std::min(static_cast(m_region->sample().size()), m_sampleFrame + freqRatio); m_frameCount++; } diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index a9ee513dbe3..1e8ef4dceec 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -151,15 +151,6 @@ void SfzSampler::loadFile(const QString& filePath) qDebug() << "was okay?" << successfulParseFile; qDebug() << "num regions" << m_sfzRegions.size(); - for (auto& region : m_sfzRegions) - { - qDebug() << "------"; - qDebug() << "region sample" << region.m_sampleFile.value_or("aaaa"); - qDebug() << "region lokey" << region.m_lokey.value_or(-1); - qDebug() << "region hikey" << region.m_hikey.value_or(-1); - qDebug() << "region lovel" << region.m_lovel.value_or(-1); - qDebug() << "region hivel" << region.m_hivel.value_or(-1); - } // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them From 2518338649c5e64404c90fc3c238756d69a9a4c6 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:18:23 -0500 Subject: [PATCH 026/131] Add offset, tune, volume, and pan opcodes --- plugins/SfzSampler/SfzOpcodeState.cpp | 19 +++++++++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 7 +++++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 16 +++++++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index c22a7b8b68f..c68725b8ab6 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -48,6 +48,11 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_hivel = value.toInt(&successful); } + else if (name == "offset") + { + m_offset = value.toInt(&successful); + } + else if (name == "pitch_keycenter") { m_pitch_keycenter = value.toInt(&successful); @@ -58,6 +63,20 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_pitch_keytrack = value.toInt(&successful); } + else if (name == "tune") + { + m_tune = value.toFloat(&successful); + } + + else if (name == "volume" || name == "group_volume") + { + m_volume = value.toFloat(&successful); + } + else if (name == "pan") + { + m_pan = value.toFloat(&successful); + } + else if (name == "amp_veltrack") { m_amp_veltrack = value.toFloat(&successful); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index e7855b05017..fe6cb6b0ac1 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -28,8 +28,15 @@ class SfzOpcodeState int m_lovel = 0; int m_hivel = 127; + int m_offset = 0; // sample play offset in frames + int m_pitch_keycenter = 60; int m_pitch_keytrack = 100; + + float m_tune = 0.0f; // in cents + + float m_volume = 0.0f; // In decibals + float m_pan = 0.0f; float m_amp_veltrack = 100; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index c735a1ee892..9808f38baf8 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -17,6 +17,8 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger , m_trigger(trigger) , m_region(region) { + // Set initial sample start frame offset + m_sampleFrame += m_region->m_offset; } @@ -81,7 +83,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const f_cnt_t framesToPlay = frames - startFrameOffset; // Calculate pitch difference relative to original sample - const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter; + const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter + m_region->m_tune / 100; float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack / 1200.0f); // Sample rate of sample @@ -104,6 +106,14 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. const float ampVelocity = 2 * ((m_trigger.velocity().value() / 127.0f - 0.5f) * (m_region->m_amp_veltrack / 100) * 0.5f + 0.5f); + // Amplitude due to volume/gain + const float ampVolume = dbfsToAmp(m_region->m_volume); + + // Panning + const float pan = m_region->m_pan / 100; + const float rightPanAmp = std::min(1.0f, 1.0f + pan); + const float leftPanAmp = std::min(1.0f, 1.0f - pan); + for (f_cnt_t f = 0; f < frames; ++f) { const float ampeg = envelopeGenerator( @@ -114,8 +124,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ampegSustain, ampegReleaseFrames ); - buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity; - buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity; + buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity * ampVolume * rightPanAmp; + buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity * ampVolume * leftPanAmp; m_sampleFrame = std::min(static_cast(m_region->sample().size()), m_sampleFrame + freqRatio); m_frameCount++; } From e6d811d8532a1ac0dae43fa3794e3d72a7ba621b Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 09:28:41 -0500 Subject: [PATCH 027/131] Initial support for loop_mode --- plugins/SfzSampler/SfzOpcodeState.cpp | 12 ++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 9 +++++++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 19 ++++++++++++++++--- plugins/SfzSampler/SfzSampler.cpp | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index c68725b8ab6..afef6d28d6a 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -77,6 +77,18 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_pan = value.toFloat(&successful); } + else if (name == "loop_mode") + { + if (value == "no_loop") { m_loop_mode = LoopMode::NoLoop; } + else if (value == "one_shot") { m_loop_mode = LoopMode::OneShot; } + //else if (value == "loop_continuous") { m_loop_mode = LoopMode::LoopContinuous; } // Not implemented yet, since we don't currently have a way to get loop point data from sample files + //else if (value == "loop_sustain") { m_loop_mode = LoopMode::LoopSustain; } + else + { + qDebug() << "[SFZ Parser] Unknown loop_mode:" << value; + } + } + else if (name == "amp_veltrack") { m_amp_veltrack = value.toFloat(&successful); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index fe6cb6b0ac1..27a2d9e4f81 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -38,6 +38,15 @@ class SfzOpcodeState float m_volume = 0.0f; // In decibals float m_pan = 0.0f; + enum class LoopMode + { + NoLoop, + OneShot, + //LoopContinuous, + //LoopSustain + }; + LoopMode m_loop_mode = LoopMode::NoLoop; + float m_amp_veltrack = 100; float m_ampeg_delay = 0; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 9808f38baf8..7301d0beaea 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -136,6 +136,12 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) m_active = false; } + // If loop_mode is one_shot, no noteOff signal will ever come to release it, so we need to manually deactivate when we reach the end of the sample + if (m_region->m_loop_mode == SfzOpcodeState::LoopMode::OneShot && m_sampleFrame >= m_region->sample().size()) + { + m_active = false; // TODO should this forcefully decative or just release? + } + int elapsed = profiler.elapsed(); totalMicroseconds += elapsed; totalSquaredMicroseconds += elapsed * elapsed; totalCalls++; minElapsed = std::min(minElapsed, elapsed); maxElapsed = std::max(maxElapsed, elapsed); float mean = 1.0f * totalMicroseconds / totalCalls, variance = (1.0f * totalSquaredMicroseconds - 1.0f * totalMicroseconds * totalMicroseconds / totalCalls / totalCalls) / totalCalls; //qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); @@ -145,10 +151,17 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) { - if (trigger.key() == m_trigger.key() && trigger.type() == SfzTrigger::Type::NoteOff && !m_released) + if (m_released) { return; } // If we already released, don't do anything + + if (trigger.type() == SfzTrigger::Type::NoteOff) { - m_released = true; - m_releaseFrame = m_frameCount; // testing + if (m_region->m_loop_mode == SfzOpcodeState::LoopMode::OneShot) { return; } // If one_shot looping is enabled, the whole sample will play regardless of if the note is released + + if (trigger.key() == m_trigger.key()) + { + m_released = true; + m_releaseFrame = m_frameCount; // testing + } } } diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 1e8ef4dceec..39f72e5f1cf 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -69,7 +69,7 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) //QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; //loadFile(path + "_jRhodes-stereo-looped.sfz"); - loadFile(ConfigManager::inst()->userSamplesDir() + "sfz/SplendidGrandPiano-master/Splendid\ Grand\ Piano.sfz"); + //loadFile(ConfigManager::inst()->userSamplesDir() + "sfz/SplendidGrandPiano-master/Splendid\ Grand\ Piano.sfz"); auto iph = new InstrumentPlayHandle(this, instrumentTrack); Engine::audioEngine()->addPlayHandle(iph); From 9facfde4154ca26a6cef592c136ecbd853aa34b4 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 12:00:47 -0500 Subject: [PATCH 028/131] Add filter and fix amp_veltrack --- plugins/SfzSampler/SfzOpcodeState.cpp | 24 ++++++++ plugins/SfzSampler/SfzOpcodeState.h | 69 +++++++++++++++++++---- plugins/SfzSampler/SfzRegionPlayState.cpp | 55 +++++++++++++++++- plugins/SfzSampler/SfzRegionPlayState.h | 6 +- 4 files changed, 139 insertions(+), 15 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index afef6d28d6a..04044a473aa 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -86,9 +86,33 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else { qDebug() << "[SFZ Parser] Unknown loop_mode:" << value; + return false; } } + else if (name == "fil_type") + { + if (value == "lpf1p") { m_fil_type = FilterType::Lowpass1Pole; } + else if (value == "lpf2p") { m_fil_type = FilterType::Lowpass2Pole; } + else if (value == "hpf1p") { m_fil_type = FilterType::Highpass1Pole; } + else if (value == "hpf2p") { m_fil_type = FilterType::Highpass2Pole; } + else if (value == "bpf2p") { m_fil_type = FilterType::Bandpass2Pole; } + else if (value == "brf2p") { m_fil_type = FilterType::Bandstop2Pole; } + else + { + qDebug() << "[SFZ Parser] Unknown filter type:" << value; + return false; + } + } + else if (name == "cutoff") + { + m_cutoff = value.toFloat(&successful); + } + else if (name == "resonance") + { + m_resonance = value.toFloat(&successful); + } + else if (name == "amp_veltrack") { m_amp_veltrack = value.toFloat(&successful); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 27a2d9e4f81..cd22c2e1473 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -17,38 +17,72 @@ class SfzOpcodeState static int keyNumFromString(QString keyString, bool* successful); static int ccNumberFromOpcode(const QString& opcode); -//private: + + // + // File Paths + // std::optional m_sampleFile = std::nullopt; std::optional m_default_path = std::nullopt; + + // + // Key Trigger + // std::optional m_key = std::nullopt; int m_lokey = 0; int m_hikey = 127; + + // + // Velocity Trigger + // int m_lovel = 0; int m_hivel = 127; - int m_offset = 0; // sample play offset in frames - - int m_pitch_keycenter = 60; - int m_pitch_keytrack = 100; - - float m_tune = 0.0f; // in cents - float m_volume = 0.0f; // In decibals - float m_pan = 0.0f; + // + // Sample playback + // + int m_offset = 0; // sample play offset in frames enum class LoopMode { NoLoop, OneShot, - //LoopContinuous, + //LoopContinuous, // To be implemented //LoopSustain }; LoopMode m_loop_mode = LoopMode::NoLoop; - float m_amp_veltrack = 100; + // + // Pitch + // + int m_pitch_keycenter = 60; + int m_pitch_keytrack = 100; + + float m_tune = 0.0f; // in cents + + // + // Filter + // + enum class FilterType + { + Lowpass1Pole, + Lowpass2Pole, + Highpass1Pole, + Highpass2Pole, + Bandpass2Pole, + Bandstop2Pole + }; + FilterType m_fil_type = FilterType::Lowpass2Pole; + std::optional m_cutoff = std::nullopt; + float m_resonance = 0.0f; + + + // + // Amplitude + // float m_ampeg_delay = 0; float m_ampeg_attack = 0; float m_ampeg_hold = 0; @@ -57,6 +91,19 @@ class SfzOpcodeState float m_ampeg_release = 0.001; std::array m_ampeg_release_oncc = {}; + float m_amp_veltrack = 100; + + + // + // Misc Volume + // + float m_volume = 0.0f; // In decibals + float m_pan = 0.0f; + + + // + // Midi CC + // //! Default midi CC values std::array m_set_cc = {}; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 7301d0beaea..e79129c39f3 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -14,11 +14,37 @@ namespace lmms SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger) : m_active(true) + , m_filter(Engine::audioEngine()->outputSampleRate()) , m_trigger(trigger) , m_region(region) { // Set initial sample start frame offset m_sampleFrame += m_region->m_offset; + + // Setup the filter + switch (m_region->m_fil_type) + { + // This is not correct! I'm using the default filters from BasicFilters, but I don't believe they necessarily match the 1 pole vs 2 pole specifications for sfz. + // For example, the default lowpass is a biquad, which I believe has 2 poles. For now I'm using it for both lowpass types, but it should probably be changed in the future. + case SfzOpcodeState::FilterType::Lowpass1Pole: + m_filter.setFilterType(BasicFilters<2>::FilterType::LowPass); + break; + case SfzOpcodeState::FilterType::Lowpass2Pole: + m_filter.setFilterType(BasicFilters<2>::FilterType::LowPass); + break; + case SfzOpcodeState::FilterType::Highpass1Pole: + m_filter.setFilterType(BasicFilters<2>::FilterType::HiPass); + break; + case SfzOpcodeState::FilterType::Highpass2Pole: + m_filter.setFilterType(BasicFilters<2>::FilterType::HiPass); + break; + case SfzOpcodeState::FilterType::Bandpass2Pole: + m_filter.setFilterType(BasicFilters<2>::FilterType::BandPass_CSG); // I am not well versed in the difference between BandPass_CSG and BandPass_CZPG. It seems to be something about how it uses Q/resonance? I'm not sure. + break; + case SfzOpcodeState::FilterType::Bandstop2Pole: + m_filter.setFilterType(BasicFilters<2>::FilterType::Notch); + break; + } } @@ -104,7 +130,10 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // Amplitude due to velocity // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. - const float ampVelocity = 2 * ((m_trigger.velocity().value() / 127.0f - 0.5f) * (m_region->m_amp_veltrack / 100) * 0.5f + 0.5f); + // Essentially this means lerping between y = x, y = 1, and y = 1 - x, if you think of y being the amp and x being vel/127 + const float ampVelocity = m_region->m_amp_veltrack > 0 + ? (m_trigger.velocity().value() / 127.0f) * (m_region->m_amp_veltrack / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack / 100) + : (1.0f - m_trigger.velocity().value() / 127.0f) * (m_region->m_amp_veltrack / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack / -100); // Amplitude due to volume/gain const float ampVolume = dbfsToAmp(m_region->m_volume); @@ -114,8 +143,19 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const float rightPanAmp = std::min(1.0f, 1.0f + pan); const float leftPanAmp = std::min(1.0f, 1.0f - pan); + // Filter + const bool filterEnabled = m_region->m_cutoff != std::nullopt; + // For performance, only update the cutoff frequency/resonance once per buffer + if (filterEnabled) + { + // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it + const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. + m_filter.calcFilterCoeffs(m_region->m_cutoff.value(), q); + } + for (f_cnt_t f = 0; f < frames; ++f) { + // The amplitude envelope is computed every frame, since doing it once per buffer could result in discontinuities/clicks const float ampeg = envelopeGenerator( ampegDelayFrames, ampegAttackFrames, @@ -124,12 +164,21 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ampegSustain, ampegReleaseFrames ); - buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity * ampVolume * rightPanAmp; - buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity * ampVolume * leftPanAmp; + if (filterEnabled) // TODO does this if statement make it faster? + { + buffer[f][0] += m_filter.update(m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); + buffer[f][1] += m_filter.update(m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); + } + else + { + buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity * ampVolume * rightPanAmp; + buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity * ampVolume * leftPanAmp; + } m_sampleFrame = std::min(static_cast(m_region->sample().size()), m_sampleFrame + freqRatio); m_frameCount++; } + // Deactive the voice if it has been released and the release has finished if (m_released && static_cast(m_frameCount - m_releaseFrame) > ampegReleaseFrames) { diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index cd1fc852cad..bbea5b3b775 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -6,7 +6,7 @@ #include "SfzTrigger.h" #include "SfzOpcodeState.h" -#include "Sample.h" +#include "BasicFilters.h" namespace lmms { @@ -47,6 +47,10 @@ class SfzRegionPlayState //! Stores the current frame index being played in the region's sample. This is a float, since interpolation is done to change pitch/sample rate float m_sampleFrame = 0; + //! Some SFZ's utilize filters (e.g, a lowpass which changes cutoff depending on velocity), so each voice needs to have a filter object + // TODO this assumes 2 channels--is that okay? + BasicFilters<2> m_filter = {44100}; // This just initializes it to something so this class can be default-initialized in an array, but this will be reinitialized later. + //! The trigger event which caused this sound SfzTrigger m_trigger; From 1b6d9a6c5e1bc37b1dc61dfdfcf7d162af5bf2e8 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:52:18 -0500 Subject: [PATCH 029/131] Add fil_veltrack and fix some stuff --- plugins/SfzSampler/SfzOpcodeState.cpp | 20 ++++++++++++++------ plugins/SfzSampler/SfzOpcodeState.h | 4 +++- plugins/SfzSampler/SfzRegionPlayState.cpp | 20 +++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 04044a473aa..664659a096b 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -62,6 +62,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_pitch_keytrack = value.toInt(&successful); } + else if (name == "pitch_veltrack") + { + m_pitch_veltrack = value.toInt(&successful); + } else if (name == "tune") { @@ -92,12 +96,12 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "fil_type") { - if (value == "lpf1p") { m_fil_type = FilterType::Lowpass1Pole; } - else if (value == "lpf2p") { m_fil_type = FilterType::Lowpass2Pole; } - else if (value == "hpf1p") { m_fil_type = FilterType::Highpass1Pole; } - else if (value == "hpf2p") { m_fil_type = FilterType::Highpass2Pole; } - else if (value == "bpf2p") { m_fil_type = FilterType::Bandpass2Pole; } - else if (value == "brf2p") { m_fil_type = FilterType::Bandstop2Pole; } + if (value == "lpf_1p") { m_fil_type = FilterType::Lowpass1Pole; } + else if (value == "lpf_2p") { m_fil_type = FilterType::Lowpass2Pole; } + else if (value == "hpf_1p") { m_fil_type = FilterType::Highpass1Pole; } + else if (value == "hpf_2p") { m_fil_type = FilterType::Highpass2Pole; } + else if (value == "bpf_2p") { m_fil_type = FilterType::Bandpass2Pole; } + else if (value == "brf_2p") { m_fil_type = FilterType::Bandstop2Pole; } else { qDebug() << "[SFZ Parser] Unknown filter type:" << value; @@ -112,6 +116,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_resonance = value.toFloat(&successful); } + else if (name == "fil_veltrack") + { + m_fil_veltrack = value.toInt(&successful); + } else if (name == "amp_veltrack") { diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index cd22c2e1473..003d817c599 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -59,7 +59,8 @@ class SfzOpcodeState // Pitch // int m_pitch_keycenter = 60; - int m_pitch_keytrack = 100; + int m_pitch_keytrack = 100; // in cents + int m_pitch_veltrack = 0; // in cents float m_tune = 0.0f; // in cents @@ -78,6 +79,7 @@ class SfzOpcodeState FilterType m_fil_type = FilterType::Lowpass2Pole; std::optional m_cutoff = std::nullopt; float m_resonance = 0.0f; + int m_fil_veltrack = 0; // in cents // diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index e79129c39f3..9b2e02a66bb 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -108,9 +108,16 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const f_cnt_t startFrameOffset = std::max(0, -m_frameCount); const f_cnt_t framesToPlay = frames - startFrameOffset; + // Helper variable + const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; + // Calculate pitch difference relative to original sample - const int semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter + m_region->m_tune / 100; - float freqRatio = std::exp2(semitoneDifference * m_region->m_pitch_keytrack / 1200.0f); + const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter; + // The total pitch depends on 1. the key offset 2. the fine `tune` adjustment 3. the velocity, if pitch_veltrack is nonzero + const float pitch = semitoneDifference * m_region->m_pitch_keytrack / 100.0f + + m_region->m_tune / 100.0f + + normalizedVelocity * m_region->m_pitch_veltrack / 100.0f; + float freqRatio = std::exp2(pitch / 12.0f); // Sample rate of sample const float sampleSampleRate = m_region->sample().sampleRate(); @@ -132,8 +139,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. // Essentially this means lerping between y = x, y = 1, and y = 1 - x, if you think of y being the amp and x being vel/127 const float ampVelocity = m_region->m_amp_veltrack > 0 - ? (m_trigger.velocity().value() / 127.0f) * (m_region->m_amp_veltrack / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack / 100) - : (1.0f - m_trigger.velocity().value() / 127.0f) * (m_region->m_amp_veltrack / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack / -100); + ? (normalizedVelocity) * (m_region->m_amp_veltrack / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack / 100) + : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack / -100); // Amplitude due to volume/gain const float ampVolume = dbfsToAmp(m_region->m_volume); @@ -148,9 +155,12 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // For performance, only update the cutoff frequency/resonance once per buffer if (filterEnabled) { + // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them + const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack / 100.0f; + const float filterCutoff = m_region->m_cutoff.value() + std::exp2(filterCutoffPitchOffset / 12.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. - m_filter.calcFilterCoeffs(m_region->m_cutoff.value(), q); + m_filter.calcFilterCoeffs(filterCutoff, q); } for (f_cnt_t f = 0; f < frames; ++f) From 062c54775d15c0792be2b6754938a1e3ec5bd8a7 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:14:08 -0500 Subject: [PATCH 030/131] Add all the ampeg vel2 and oncc opcodes, and clean up stuff --- plugins/SfzSampler/SfzOpcodeState.cpp | 126 +++++++++++++++++--------- plugins/SfzSampler/SfzOpcodeState.h | 37 ++++++-- 2 files changed, 112 insertions(+), 51 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 664659a096b..a80c5d74783 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -14,6 +14,9 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { bool successful = true; + // + // File paths + // if (name == "sample") { m_sampleFile = value; @@ -23,6 +26,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_default_path = value; } + + // + // Key Trigger + // else if (name == "key") { m_key = value.toInt(&successful); @@ -39,6 +46,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu if (!successful) { m_hikey = keyNumFromString(value, &successful); } } + + // + // Velocity Trigger + // else if (name == "lovel") { m_lovel = value.toInt(&successful); @@ -48,11 +59,35 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_hivel = value.toInt(&successful); } + + // + // Sample playback options + // else if (name == "offset") { m_offset = value.toInt(&successful); } + else if (name == "loop_mode") + { + if (value == "no_loop") { m_loop_mode = LoopMode::NoLoop; } + else if (value == "one_shot") { m_loop_mode = LoopMode::OneShot; } + //else if (value == "loop_continuous") { m_loop_mode = LoopMode::LoopContinuous; } // Not implemented yet, since we don't currently have a way to get loop point data from sample files + //else if (value == "loop_sustain") { m_loop_mode = LoopMode::LoopSustain; } + else + { + qDebug() << "[SFZ Parser] Unknown loop_mode:" << value; + return false; + } + } + + // + // Pitch + // + else if (name == "tune") + { + m_tune = value.toInt(&successful); + } else if (name == "pitch_keycenter") { m_pitch_keycenter = value.toInt(&successful); @@ -67,33 +102,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_pitch_veltrack = value.toInt(&successful); } - else if (name == "tune") - { - m_tune = value.toFloat(&successful); - } - - else if (name == "volume" || name == "group_volume") - { - m_volume = value.toFloat(&successful); - } - else if (name == "pan") - { - m_pan = value.toFloat(&successful); - } - - else if (name == "loop_mode") - { - if (value == "no_loop") { m_loop_mode = LoopMode::NoLoop; } - else if (value == "one_shot") { m_loop_mode = LoopMode::OneShot; } - //else if (value == "loop_continuous") { m_loop_mode = LoopMode::LoopContinuous; } // Not implemented yet, since we don't currently have a way to get loop point data from sample files - //else if (value == "loop_sustain") { m_loop_mode = LoopMode::LoopSustain; } - else - { - qDebug() << "[SFZ Parser] Unknown loop_mode:" << value; - return false; - } - } + // + // Filter + // else if (name == "fil_type") { if (value == "lpf_1p") { m_fil_type = FilterType::Lowpass1Pole; } @@ -121,42 +133,73 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_fil_veltrack = value.toInt(&successful); } + // + // Amplitude + // else if (name == "amp_veltrack") { m_amp_veltrack = value.toFloat(&successful); } - else if (name == "ampeg_delay") + // + // Amplitude Envelope Generator (ampeg) + // + else if (name == "ampeg_delay") { m_ampeg_delay = value.toFloat(&successful); } + else if (name == "ampeg_attack") { m_ampeg_attack = value.toFloat(&successful); } + else if (name == "ampeg_hold") { m_ampeg_hold = value.toFloat(&successful); } + else if (name == "ampeg_decay") { m_ampeg_decay = value.toFloat(&successful); } + else if (name == "ampeg_sustain") { m_ampeg_sustain = value.toFloat(&successful); } + else if (name == "ampeg_release") { m_ampeg_release = value.toFloat(&successful); } + // Velocity modulation amounts + else if (name == "ampeg_vel2delay") { m_ampeg_vel2delay = value.toFloat(&successful); } + else if (name == "ampeg_vel2attack") { m_ampeg_vel2attack = value.toFloat(&successful); } + else if (name == "ampeg_vel2hold") { m_ampeg_vel2hold = value.toFloat(&successful); } + else if (name == "ampeg_vel2decay") { m_ampeg_vel2decay = value.toFloat(&successful); } + else if (name == "ampeg_vel2sustain") { m_ampeg_vel2sustain = value.toFloat(&successful); } + else if (name == "ampeg_vel2release") { m_ampeg_vel2release = value.toFloat(&successful); } + // Midi CC modulation amounts + else if (name.startsWith("ampeg_delay_oncc") || name.startsWith("ampeg_delaycc")) { - m_ampeg_delay = value.toFloat(&successful); + m_ampeg_delay_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } - else if (name == "ampeg_attack") + else if (name.startsWith("ampeg_attack_oncc") || name.startsWith("ampeg_attackcc")) { - m_ampeg_attack = value.toFloat(&successful); + m_ampeg_attack_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } - else if (name == "ampeg_hold") + else if (name.startsWith("ampeg_hold_oncc") || name.startsWith("ampeg_holdcc")) { - m_ampeg_hold = value.toFloat(&successful); + m_ampeg_hold_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } - else if (name == "ampeg_decay") + else if (name.startsWith("ampeg_decay_oncc") || name.startsWith("ampeg_decaycc")) { - m_ampeg_decay = value.toFloat(&successful); + m_ampeg_decay_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } - else if (name == "ampeg_sustain") + else if (name.startsWith("ampeg_sustain_oncc") || name.startsWith("ampeg_sustaincc")) { - m_ampeg_sustain = value.toFloat(&successful); + m_ampeg_sustain_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } - else if (name == "ampeg_release") + else if (name.startsWith("ampeg_release_oncc") || name.startsWith("ampeg_releasecc")) { - m_ampeg_release = value.toFloat(&successful); + m_ampeg_release_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } - else if (name.startsWith("ampeg_release_oncc") || name.startsWith("ampeg_releasecc")) + + // + // Misc Volume + // + else if (name == "volume" || name == "group_volume") { - int ccNumber = ccNumberFromOpcode(name); - m_ampeg_release_oncc.at(ccNumber) = value.toFloat(&successful); + m_volume = value.toFloat(&successful); } + else if (name == "pan") + { + m_pan = value.toFloat(&successful); + } + + // + // Midi CC + // else if (name.startsWith("set_cc")) { int ccNumber = ccNumberFromOpcode(name); @@ -174,6 +217,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu return false; } + if (!successful) { qDebug() << "[SFZ Parser] Unable to convert value to number:" << name << "=" << value; diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 003d817c599..6a2da39db39 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -58,12 +58,11 @@ class SfzOpcodeState // // Pitch // + int m_tune = 0; // in cents int m_pitch_keycenter = 60; int m_pitch_keytrack = 100; // in cents int m_pitch_veltrack = 0; // in cents - float m_tune = 0.0f; // in cents - // // Filter // @@ -85,17 +84,35 @@ class SfzOpcodeState // // Amplitude // - float m_ampeg_delay = 0; - float m_ampeg_attack = 0; - float m_ampeg_hold = 0; - float m_ampeg_decay = 0; - float m_ampeg_sustain = 100; - float m_ampeg_release = 0.001; - std::array m_ampeg_release_oncc = {}; - + // Overall amplitute velocity modulation float m_amp_veltrack = 100; + // + // Amplitude Envelope Generator (ampeg) + // + float m_ampeg_delay = 0.0f; + float m_ampeg_attack = 0.0f; + float m_ampeg_hold = 0.0f; + float m_ampeg_decay = 0.0f; + float m_ampeg_sustain = 100.0f; + float m_ampeg_release = 0.001f; + // Velocity modulation amount + float m_ampeg_vel2delay = 0.0f; + float m_ampeg_vel2attack = 0.0f; + float m_ampeg_vel2hold = 0.0f; + float m_ampeg_vel2decay = 0.0f; + float m_ampeg_vel2sustain = 0.0f; + float m_ampeg_vel2release = 0.0f; + // Midi CC modulation amounts + std::array m_ampeg_delay_oncc = {}; + std::array m_ampeg_attack_oncc = {}; + std::array m_ampeg_hold_oncc = {}; + std::array m_ampeg_decay_oncc = {}; + std::array m_ampeg_sustain_oncc = {}; + std::array m_ampeg_release_oncc = {}; + + // // Misc Volume // From 753694d764d0530e91f9d3a5f81a0acf017db4e7 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:35:20 -0500 Subject: [PATCH 031/131] Fix key opcode --- plugins/SfzSampler/SfzOpcodeState.cpp | 11 ++++++----- plugins/SfzSampler/SfzOpcodeState.h | 1 - plugins/SfzSampler/SfzRegion.cpp | 5 ----- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index a80c5d74783..eb476dade46 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -30,11 +30,6 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // // Key Trigger // - else if (name == "key") - { - m_key = value.toInt(&successful); - if (!successful) { m_key = keyNumFromString(value, &successful); } - } else if (name == "lokey") { m_lokey = value.toInt(&successful); @@ -45,6 +40,12 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_hikey = value.toInt(&successful); if (!successful) { m_hikey = keyNumFromString(value, &successful); } } + else if (name == "key") + { + // Setting "key" on its own is equivalent to setting lokey, hikey, and pitch_keycenter all to the same value + m_lokey = m_hikey = m_pitch_keycenter = value.toInt(&successful); + if (!successful) { m_lokey = m_hikey = m_pitch_keycenter = keyNumFromString(value, &successful); } + } // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 6a2da39db39..4c46e2a5faa 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -28,7 +28,6 @@ class SfzOpcodeState // // Key Trigger // - std::optional m_key = std::nullopt; int m_lokey = 0; int m_hikey = 127; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 284e09d5661..3b4abf48929 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -29,11 +29,6 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf int triggerKey = trigger.key().value(); int triggerVelocity = trigger.velocity().value(); - // `key` opcode - if (m_key != std::nullopt) - { - if (triggerKey != m_key.value()) { return false; } - } // `lokey` and `hikey` opcodes if (triggerKey > m_hikey || triggerKey < m_lokey) { return false; } From 3083d5e11cc21aedfaa9f38fe66e1ec0d5a1063c Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:03:20 -0500 Subject: [PATCH 032/131] Fix parsing issue with spaces, and fix error with >128 midi CC numbers --- plugins/SfzSampler/SfzGlobalState.h | 3 +- plugins/SfzSampler/SfzOpcodeState.cpp | 2 + plugins/SfzSampler/SfzOpcodeState.h | 16 +++-- plugins/SfzSampler/SfzParser.cpp | 87 +++++++++++++---------- plugins/SfzSampler/SfzRegion.cpp | 9 ++- plugins/SfzSampler/SfzRegion.h | 7 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 10 +-- 7 files changed, 82 insertions(+), 52 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index 076799bab62..87af445c586 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -5,6 +5,7 @@ #include #include +#include "SfzOpcodeState.h" #include "SfzTrigger.h" namespace lmms @@ -35,7 +36,7 @@ class SfzGlobalState //! Stores the current value of all the midi CC knobs/controllers //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 128 to get a float between 0 and 1 works fine. //! std::optional is used so that sfz file can specify default cc values - std::array, 128> m_ccValues = {}; + std::array, SfzOpcodeState::NumMidiCCs> m_ccValues = {}; }; diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index eb476dade46..d5c76302258 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -238,6 +238,8 @@ int SfzOpcodeState::ccNumberFromOpcode(const QString& opcode) bool successfulToInt = false; int ccNumber = match.captured(0).toInt(&successfulToInt); if (!successfulToInt) { qDebug() << "[SFZ Parser] Unable to convert CC number to int:" << opcode; return 0; } + // Normal midi lets you choose between 0-127 for the CC number. According to the SFZ format website, ARIA and SFZ 2 allow for extended CC numbers. Most have not been implemented here, so to keep things safe, we cap it and give a warning. + if (ccNumber < 0 || ccNumber > NumMidiCCs) { qDebug() << "[SFZ Parser] Midi CC number" << ccNumber << "is out of range 0-127. This is not yet implmented."; return 0;} return ccNumber; } diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 4c46e2a5faa..c7437d1e7f0 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -17,6 +17,8 @@ class SfzOpcodeState static int keyNumFromString(QString keyString, bool* successful); static int ccNumberFromOpcode(const QString& opcode); + // Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. + static constexpr const int NumMidiCCs = 128; // // File Paths @@ -104,12 +106,12 @@ class SfzOpcodeState float m_ampeg_vel2sustain = 0.0f; float m_ampeg_vel2release = 0.0f; // Midi CC modulation amounts - std::array m_ampeg_delay_oncc = {}; - std::array m_ampeg_attack_oncc = {}; - std::array m_ampeg_hold_oncc = {}; - std::array m_ampeg_decay_oncc = {}; - std::array m_ampeg_sustain_oncc = {}; - std::array m_ampeg_release_oncc = {}; + std::array m_ampeg_delay_oncc = {}; + std::array m_ampeg_attack_oncc = {}; + std::array m_ampeg_hold_oncc = {}; + std::array m_ampeg_decay_oncc = {}; + std::array m_ampeg_sustain_oncc = {}; + std::array m_ampeg_release_oncc = {}; // @@ -123,7 +125,7 @@ class SfzOpcodeState // Midi CC // //! Default midi CC values - std::array m_set_cc = {}; + std::array m_set_cc = {}; friend class SfzRegion; }; diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 290cb75256e..de8cf67cfd2 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -33,55 +33,70 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou qDebug().noquote() << "TESTING: Loaded SFZ:\n" << fileContents; // testing - // Now that all the includes/defines are handled, loop + // Now that all the includes/defines are handled, loop through the whole contents and split it up into segments so that it can be parsed + // For example, if you have a sfz file like: + /* + + sample=test.wav + key=70 + */ + // Then the parsed segments would be "", "sample=test.wav", and "key=70" + // In this example they were all on separate lines, but they don't have to be: + /* + ampeg_release=0.3 lokey=45 hikey=49 + */ + // This would still be parsed into segments as "", "ampeg_release=0.3", "lokey=45", and "hikey=49" + // According to the SFZ format website, there must never be a space on either side of the = for an opcode assignment. That makes things simpler + // However, there can be spaces in the value assigned to the opcode. For example, if your sample file name has spaces: + /* + sample=Grand Piano G4 MP.wav key=49 + */ + // This would be parsed as "", "sample=Grand Piano G4 MP.wav", and "key=49" + // Essentially, we need a way to be able to split up the file on whitespace, while leaving intact any opcode assignments which have spaces in their right hand side. std::vector parsedSegments; + // We start by splitting on newline for (QString line : fileContents.split("\n")) { // Remove comments from end of line line = line.split("//")[0]; - // Split the line on whitespace to extract header and opcodes keywords - // Fortunately, the SFZ format specifically states that opcode assignments cannot contain spaces around the =, so there is no risk - // of accidentally splitting the opcode name from the value. - line.replace(">", "> "); // Real quick, make sure there is whitespace between header keywords and anything else after them. One .sfz file I found did `curve_index=11` in it,, with no space between, which makes parsing complicated, so to fix it we just insert an extra space. - for (QString segment : line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts)) + + line.replace(">", "> "); // Real quick, make sure there is whitespace between header keywords and anything else after them. One .sfz file I found did `curve_index=11` in it, with no space between, which makes parsing complicated, so to fix it we just insert an extra space. + + // We can start by splitting on whitespace, but then we will have to go back and group together any chunks which belong to the same opcode assignment, if for example the sample file included spaces + // For example, if we had the line: + // sample=My Favorite Sample.flac key=99 + // Then it would initially by split into "", "sample=My", "Favorite", "Sample.flac", and "key=99" + // However, we notice that "Favorite" and "Sample.flac" are not valid opcode assignments since they don't have an "=", and they're not headers, since you don't have those + // So they must belong to the previous opcode assignment, "sample=My" + // If we connect them together, we get "", "sample=My Favorite Sample.flac", and "key=99", just as we wanted! + std::vector lineSegments; + for (QString segment : line.split(QRegularExpression("\\s"), Qt::SkipEmptyParts)) // Note: Technically by skipping empty parts, double-spaces within names will be lost. Is this okay? I'm not sure. { - parsedSegments.push_back(segment); - } - } - - // Okay so ummm... there's a problem I didn't mention haha :sweat_smile: - // Unfortunately, sample opcode assignments *can* contain spaces and special characters except for "=". This means that the sample opcode lines could have gotten split up by what we juts did :| - // But that's okay. We can just loop through all of the segments, check if they have the sample opcode, and if so, check the next few segments too to see if they might be part of the file name (i.e., don't have = and aren't a header with < > aaround it) - for (size_t i = 0; i < parsedSegments.size(); ++i) - { - if (parsedSegments.at(i).startsWith("sample=")) - { - QStringList samplePathSegments; - // Add the initial part of the sample file, before any spaces - if (parsedSegments.at(i).split("=").size() != 2) + if (segment.contains("=") || (segment.front() == "<" && segment.back() == ">")) { - qDebug() << "[SFZ Parser] Warning, sample file path starts with a space? That's kind of weird:" << parsedSegments; - samplePathSegments.push_back(""); + // If this is an opcode assignment or a
, go ahead and add it to the list as it is + lineSegments.push_back(segment); } else { - samplePathSegments.push_back(parsedSegments.at(i).split("=")[1]); + // If it's not, then it must belong to the previous segment, so let's concatinate it (with a space, to account for the space taken from the split) + if (lineSegments.size() > 0) + { + lineSegments.back() += " " + segment; + } + else + { + qDebug() << "[SFZ Parser] Warning: Encountered non-header, non-opcode assignment at start of line:" << line; + } } - // Look at the next few segments to see if they might be continuations of the sample file path - for (size_t j = i + 1; j < parsedSegments.size();) - { - QString nextSegment = parsedSegments.at(j); - if (nextSegment.contains("=")) { break; } // If there's an equals sign, it's an opcode assignment, not part of the sample file - if (nextSegment.front() == "<" && nextSegment.back() == ">") { break; } // If it has < > around it, it's a header, so not part of the sample file - samplePathSegments.push_back(nextSegment); - parsedSegments.erase(parsedSegments.begin() + j); // Get rid of that segment, since it's part of the file path. Don't increment the index, since everything will shift back. - } - // Now replace the sample opcode segment with the complete filename - parsedSegments.at(i) = "sample=" + samplePathSegments.join(" "); // Technically this doesn't account for samples with double spaces or tabs in the filename } + // Add the segments from the current line to the overall list + parsedSegments.insert(parsedSegments.end(), lineSegments.begin(), lineSegments.end()); } + + // Now that all the segments are collected, we can go through them all and construct the SfzRegions // First, the header(s) must be found. The SFZ format website does not guarantee that will // be the first header, so we have to search through all the segments to find the globals before parsing any headers @@ -203,12 +218,12 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } case Header::Curve: { - qDebug() << "[SFZ Parser] Warning, the header has not been implemented yet. Encountered opcode assignment:" << segment; + qDebug() << "[SFZ Parser] Warning: The header has not been implemented yet. Encountered opcode assignment:" << segment; break; } default: { - qDebug() << "[SFZ Parser] Error, encountered line within invalid header" << segment; + qDebug() << "[SFZ Parser] Error: Encountered line within invalid header" << segment; return false; } } diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 3b4abf48929..4ecb01dcc73 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -112,10 +112,10 @@ void SfzRegion::recalculateMaxActiveIndex() -float SfzRegion::totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const +float SfzRegion::totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const { float total = 0.0f; - for (int i = 0; i < 128; ++i) + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { total += ccModulationAmounts[i] * globalState.midiCCValue(i, m_set_cc[i]) / 128.0f; // m_set_cc stores the default CC values for each of the midi controllers } @@ -124,6 +124,11 @@ float SfzRegion::totalCCModulation(const std::array& ccModulationAmo void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) { + m_ampeg_delay_totalCC = totalCCModulation(m_ampeg_delay_oncc, globalState); + m_ampeg_attack_totalCC = totalCCModulation(m_ampeg_attack_oncc, globalState); + m_ampeg_hold_totalCC = totalCCModulation(m_ampeg_hold_oncc, globalState); + m_ampeg_decay_totalCC = totalCCModulation(m_ampeg_decay_oncc, globalState); + m_ampeg_sustain_totalCC = totalCCModulation(m_ampeg_sustain_oncc, globalState); m_ampeg_release_totalCC = totalCCModulation(m_ampeg_release_oncc, globalState); // TODO more } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index ca6581dafe1..c1bf271ed9d 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -59,10 +59,15 @@ class SfzRegion : public SfzOpcodeState //! Store the current total midi CC modulation amounts for the different targets, just so that we don't // have to recalculate them every buffer, instead only when a trigger occurs. + float m_ampeg_delay_totalCC = 0.0f; + float m_ampeg_attack_totalCC = 0.0f; + float m_ampeg_hold_totalCC = 0.0f; + float m_ampeg_decay_totalCC = 0.0f; + float m_ampeg_sustain_totalCC = 0.0f; float m_ampeg_release_totalCC = 0.0f; //! Helper function to calculate the total modulation of all midi CC controllers on a parameter. Essentially it just multiplies the modulation amounts by the current CC values and adds it all up. - float totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const; + float totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const; void recalculateTotalCCModulation(const SfzGlobalState& globalState); friend class SfzRegionPlayState; // TODO this was just to make it easy but...? diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 9b2e02a66bb..06fa41482f1 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -127,11 +127,11 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) freqRatio *= sampleSampleRate / sampleRate; // Amplitude envelope - const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay) * sampleRate; - const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack) * sampleRate; - const f_cnt_t ampegHoldFrames = (m_region->m_ampeg_hold) * sampleRate; - const f_cnt_t ampegDecayFrames = (m_region->m_ampeg_decay) * sampleRate; - const float ampegSustain = (m_region->m_ampeg_sustain) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay + m_region->m_ampeg_delay_totalCC) * sampleRate; + const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack + m_region->m_ampeg_attack_totalCC) * sampleRate; + const f_cnt_t ampegHoldFrames = (m_region->m_ampeg_hold + m_region->m_ampeg_hold_totalCC) * sampleRate; + const f_cnt_t ampegDecayFrames = (m_region->m_ampeg_decay + m_region->m_ampeg_decay_totalCC) * sampleRate; + const float ampegSustain = (m_region->m_ampeg_sustain + m_region->m_ampeg_sustain_totalCC) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg_release + m_region->m_ampeg_release_totalCC) * sampleRate; // Amplitude due to velocity From 29046070bffbaf5e1ba678b2c34883a8c79d2a61 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:36:31 -0500 Subject: [PATCH 033/131] Add Round Robin Support --- plugins/SfzSampler/SfzOpcodeState.cpp | 13 +++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 6 ++++++ plugins/SfzSampler/SfzRegion.cpp | 4 ++++ plugins/SfzSampler/SfzRegion.h | 3 +++ 4 files changed, 26 insertions(+) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index d5c76302258..b7cc005b0bf 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -61,6 +61,19 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } + // + // Round Robin Trigger + // + else if (name == "seq_length") + { + m_seq_length = value.toInt(&successful); + } + else if (name == "seq_position") + { + m_seq_position = value.toInt(&successful); + } + + // // Sample playback options // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index c7437d1e7f0..b3c9dab3ca9 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -40,6 +40,12 @@ class SfzOpcodeState int m_lovel = 0; int m_hivel = 127; + // + // Round Robin Trigger + // + int m_seq_length = 1; + int m_seq_position = 1; + // // Sample playback diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 4ecb01dcc73..ad5d75f1f22 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -35,6 +35,10 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // `lovel` and `hivel` opcodes if (triggerVelocity > m_hivel || triggerVelocity < m_lovel) { return false; } + // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. + m_roundRobinCount++; + if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn + // If all the contitions passed, return true and spawn a sound return true; } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index c1bf271ed9d..46159eecc52 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -57,6 +57,9 @@ class SfzRegion : public SfzOpcodeState //! Helper function to figure out what the maximum active index is, in the event the maximum index deacticated void recalculateMaxActiveIndex(); + //! In order to do round robin, the region needs to keep track of how many notes it has played in its lifetime. Or rather, the number of notes it *would* have played if it weren't restricted to only play a note when the round-robin counter hit the right numbers. + size_t m_roundRobinCount = 0; + //! Store the current total midi CC modulation amounts for the different targets, just so that we don't // have to recalculate them every buffer, instead only when a trigger occurs. float m_ampeg_delay_totalCC = 0.0f; From 6571ff0539dbc693e5ddf0c7ded5324520393816 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:47:49 -0500 Subject: [PATCH 034/131] Fix SampleLoaded includes --- plugins/SfzSampler/SfzRegion.cpp | 4 ++-- plugins/SfzSampler/SfzSampler.cpp | 1 - plugins/SfzSampler/SfzSamplerView.cpp | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index ad5d75f1f22..6f8fdceca65 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -8,7 +8,7 @@ #include "Engine.h" #include "AudioEngine.h" -#include "SampleLoader.h" +#include "SampleBuffer.h" #include @@ -149,7 +149,7 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory) } // TODO is simply adding the default path sufficient? - if (auto buffer = gui::SampleLoader::createBufferFromFile(parentDirectory.absoluteFilePath(m_default_path.value_or("") + m_sampleFile.value()))) + if (auto buffer = SampleBuffer::fromFile(parentDirectory.absoluteFilePath(m_default_path.value_or("") + m_sampleFile.value()))) { m_sample = SfzSampleBuffer(buffer->data(), buffer->size(), buffer->sampleRate()); return true; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 39f72e5f1cf..254d2c89c87 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -33,7 +33,6 @@ #include "InstrumentTrack.h" #include "PathUtil.h" #include "ConfigManager.h" -#include "SampleLoader.h" #include "SfzSamplerView.h" #include "Song.h" #include "embed.h" diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 3c32d5f2bdc..3be86bd5cd0 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -35,7 +35,6 @@ #include "Knob.h" #include "LcdSpinBox.h" #include "PixmapButton.h" -#include "SampleLoader.h" #include "SfzSampler.h" #include "StringPairDrag.h" #include "Track.h" From 15c261e7a9d52e52eff5bdd0a3dac3cfaf9986aa Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:59:42 -0500 Subject: [PATCH 035/131] Add amplitude and amplitue_onccN opcodes --- plugins/SfzSampler/SfzOpcodeState.cpp | 10 ++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 3 +++ plugins/SfzSampler/SfzRegion.cpp | 2 ++ plugins/SfzSampler/SfzRegion.h | 2 ++ plugins/SfzSampler/SfzRegionPlayState.cpp | 11 +++++++---- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index b7cc005b0bf..569e7de4146 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -150,6 +150,16 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // // Amplitude // + else if (name == "amplitude") + { + m_amplitude = value.toFloat(&successful); + } + else if (name.startsWith("amplitude_oncc") || name.startsWith("amplitude_cc")) + { + m_amplitude_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); + } + + else if (name == "amp_veltrack") { m_amp_veltrack = value.toFloat(&successful); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index b3c9dab3ca9..c94c72a4c53 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -91,6 +91,9 @@ class SfzOpcodeState // // Amplitude // + float m_amplitude = 100.0f; // In percent + std::array m_amplitude_oncc = {}; + // Overall amplitute velocity modulation float m_amp_veltrack = 100; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 6f8fdceca65..275cd5075de 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -128,6 +128,8 @@ float SfzRegion::totalCCModulation(const std::arraym_amplitude + m_region->m_amplitude_totalCC) / 100.0f; // Amplitude is stored as a percent + // Amplitude envelope const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay + m_region->m_ampeg_delay_totalCC) * sampleRate; const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack + m_region->m_ampeg_attack_totalCC) * sampleRate; @@ -176,13 +179,13 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ); if (filterEnabled) // TODO does this if statement make it faster? { - buffer[f][0] += m_filter.update(m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); - buffer[f][1] += m_filter.update(m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); + buffer[f][0] += m_filter.update(m_region->sample().at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); + buffer[f][1] += m_filter.update(m_region->sample().at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); } else { - buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * ampeg * ampVelocity * ampVolume * rightPanAmp; - buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * ampeg * ampVelocity * ampVolume * leftPanAmp; + buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp; + buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp; } m_sampleFrame = std::min(static_cast(m_region->sample().size()), m_sampleFrame + freqRatio); m_frameCount++; From b838ddd1016d586209c01cae9e64e752920a02f8 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:09:21 -0500 Subject: [PATCH 036/131] Save the filepath to the sfz file --- plugins/SfzSampler/SfzSampler.cpp | 8 ++++++++ plugins/SfzSampler/SfzSampler.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 254d2c89c87..7d9ce5d31c4 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -160,14 +160,22 @@ void SfzSampler::loadFile(const QString& filePath) bool successfulLoadSample = region.initializeSample(parentDirectory); qDebug() << "sample was load okay?" << successfulLoadSample; } + + m_sfzFilePath = filePath; } void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) { + element.setAttribute("sfzfile", m_sfzFilePath); } void SfzSampler::loadSettings(const QDomElement& element) { + m_sfzFilePath = element.attribute("sfzfile"); + if (!m_sfzFilePath.isEmpty()) + { + loadFile(m_sfzFilePath); + } } QString SfzSampler::nodeName() const diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 3482c03ff98..031518d7216 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -64,6 +64,8 @@ class SfzSampler : public Instrument InstrumentTrack* m_parentTrack; + QString m_sfzFilePath = ""; + //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; From f9592c0f079897a1f801e62f3ce1f2a3931ae596 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:29:47 -0500 Subject: [PATCH 037/131] Fix includes with spaces in filename --- plugins/SfzSampler/SfzParser.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index de8cf67cfd2..63f8495f829 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -263,7 +263,7 @@ QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentD // A define variable should probably start with a $ if (variableName.front() != "$") { qDebug() << "[SFZ Parser] Warning: #define variable name does not start with $:" << line; } } - else if (line.startsWith("#include")) + else if (line.startsWith("#include ")) { // Replace any of the defined variables before parsing the include path, since some SFZ files use $variables in them for (const auto& [variableName, variableValue] : defineMap) @@ -273,13 +273,13 @@ QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentD const auto segments = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); // An include statement should have two parts, the #include and the path - if (segments.size() != 2) + if (line.split("#include ").size() != 2) { qDebug() << "[SFZ Parser] Ill-formed include statment:" << line; continue; } - QString relativePath = segments[1]; + QString relativePath = line.split("#include ")[1]; relativePath.replace("\"", ""); // Remove " " from start and end const QString absolutePath = parentDirectory.absoluteFilePath(relativePath); From 7b6c660dfe66c1715d8541a82ea9cbb691543e7d Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:23:25 -0500 Subject: [PATCH 038/131] Add keyswitch support --- plugins/SfzSampler/SfzGlobalState.cpp | 29 +++++++++++++++++++++++++-- plugins/SfzSampler/SfzGlobalState.h | 15 +++++++++----- plugins/SfzSampler/SfzOpcodeState.cpp | 25 +++++++++++++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 7 +++++++ plugins/SfzSampler/SfzRegion.cpp | 8 ++++++-- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 86c7cdeb1f7..4c3cff0b233 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -11,11 +11,36 @@ void SfzGlobalState::processTrigger(const SfzTrigger& trigger) { m_ccValues.at(trigger.controlChangeNumber().value()) = trigger.controlChangeValue().value(); } + else if (trigger.type() == SfzTrigger::Type::NoteOn) + { + m_activeKeys.at(trigger.key().value()) = true; + m_keyPressCounter++; + m_lastPlayedKeys.at(trigger.key().value()) = m_keyPressCounter; + } + else if (trigger.type() == SfzTrigger::Type::NoteOff) + { + m_activeKeys.at(trigger.key().value()) = false; + } } -int SfzGlobalState::lastKeyPressedInRange(const int lowKey, const int highKey) +std::optional SfzGlobalState::lastKeyPressedInRange(const int lowKey, const int highKey, const std::optional defaultKey) const { - return 0; + std::optional lastPlayedKey = std::nullopt; + std::optional bestScore = std::nullopt; + + for (int key = lowKey; key <= highKey; ++key) + { + if (m_lastPlayedKeys.at(key) == std::nullopt) { continue; } // This key has not been played yet + + if (bestScore == std::nullopt || m_lastPlayedKeys.at(key) > bestScore) + { + lastPlayedKey = key; + bestScore = m_lastPlayedKeys.at(key); + } + } + // If no keys in the region have been played yet, return the default key + if (lastPlayedKey == std::nullopt) { return defaultKey; } + else { return lastPlayedKey; } } diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index 87af445c586..c4bd0618417 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -18,20 +18,25 @@ class SfzGlobalState //! Handles updating the last played keys and keeps track of which keys are currently being pressed void processTrigger(const SfzTrigger& trigger); - // Returns the midi key number of the last pressed key in the range [lowKey, highKey] - int lastKeyPressedInRange(const int lowKey, const int highKey); + // Returns the midi key number of the last pressed key in the range [lowKey, highKey]. + // If no keys have been pressed yet, it returns defaultKey. + // If no default key was given, it returns std::nullopt + // TODO add unit tests + std::optional lastKeyPressedInRange(const int lowKey, const int highKey, const std::optional defaultKey) const; //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index, const int defaultValue) const { return m_ccValues.at(index).value_or(defaultValue); } private: //! Stores a number for every key, tracking which order the keys were played in - //! The first key played gets a 1, the second key gets a 2, etc, overwriting when keys are played multiple times/ + //! The first key played gets a 1, the second key gets a 2, etc, overwriting when keys are played multiple times. //! By finding the maximum number in a range, you can find the last played key - std::array, 128> m_lastPlayedKeys; + std::array, 128> m_lastPlayedKeys = {}; + //! Consequently, we also have to track how many keys have been played so far + unsigned long m_keyPressCounter = 0; //! Keeps track of which keys on the piano are currently being pressed - std::array m_activeKeys; + std::array m_activeKeys = {}; //! Stores the current value of all the midi CC knobs/controllers //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 128 to get a float between 0 and 1 works fine. diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 569e7de4146..4c55933b3bb 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -48,6 +48,31 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } + // + // Key Switches + // + else if (name == "sw_lokey") + { + m_sw_lokey = value.toInt(&successful); + if (!successful) { m_sw_lokey = keyNumFromString(value, &successful); } + } + else if (name == "sw_hikey") + { + m_sw_hikey = value.toInt(&successful); + if (!successful) { m_sw_hikey = keyNumFromString(value, &successful); } + } + else if (name == "sw_last") + { + m_sw_last = value.toInt(&successful); + if (!successful) { m_sw_last = keyNumFromString(value, &successful); } + } + else if (name == "sw_default") + { + m_sw_default = value.toInt(&successful); + if (!successful) { m_sw_default = keyNumFromString(value, &successful); } + } + + // // Velocity Trigger // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index c94c72a4c53..b741c2804fe 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -33,6 +33,13 @@ class SfzOpcodeState int m_lokey = 0; int m_hikey = 127; + // + // Key Switches + // + int m_sw_lokey = 0; + int m_sw_hikey = 127; + std::optional m_sw_last = std::nullopt; // The SFZ opcode spec says this should default to 0, but I don't think that's right? + std::optional m_sw_default = std::nullopt; // // Velocity Trigger diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 275cd5075de..d30c8e893ed 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -29,12 +29,16 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf int triggerKey = trigger.key().value(); int triggerVelocity = trigger.velocity().value(); - // `lokey` and `hikey` opcodes + // Ensure the key was pressed between the `lokey` and `hikey` opcodes if (triggerKey > m_hikey || triggerKey < m_lokey) { return false; } - // `lovel` and `hivel` opcodes + // And had velocity between `lovel` and `hivel` opcodes if (triggerVelocity > m_hivel || triggerVelocity < m_lovel) { return false; } + // If a keyswitch range was defined, ensure the last pressed key in that range matches the specified keyswitch for this region + // TODO add unit tests + if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default) != m_sw_last) { return false; } + // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. m_roundRobinCount++; if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn From 1ba2ec6c6ecfc1926331227d02f58ad52cc241c0 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:28:07 -0500 Subject: [PATCH 039/131] Remove unused SfzOpcodes.h --- plugins/SfzSampler/SfzOpcodes.h | 50 --------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 plugins/SfzSampler/SfzOpcodes.h diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h deleted file mode 100644 index 300991cd3e5..00000000000 --- a/plugins/SfzSampler/SfzOpcodes.h +++ /dev/null @@ -1,50 +0,0 @@ - -#ifndef LMMS_SFZ_OPCODES_H -#define LMMS_SFZ_OPCODES_H - -#include -#include - -namespace lmms -{ - -enum class SfzOpcodes -{ - Sample, - - Key, - LoKey, - HiKey, - - LoVel, - HiVel, - - PitchKeyCenter, - - AmpEGDelay, - AmpEGAttack, - AmpEGHold, - AmpEGSustain, - AmpEGDecay, - AmpEGRelease, -}; - - -std::map SfzOpcodeNames = { - {SfzOpcodes::Sample, "sample"}, - - {SfzOpcodes::Key, "key"}, - {SfzOpcodes::LoKey, "lokey"}, - {SfzOpcodes::HiKey, "hikey"}, - - {SfzOpcodes::LoVel, "lovel"}, - {SfzOpcodes::HiVel, "hivel"}, - - {SfzOpcodes::PitchKeyCenter, "pitch_keycenter"}, -}; - - -} // namespace lmms - - -#endif // LMMS_SFZ_OPCODES_H \ No newline at end of file From eec128f71f0019235e7debe6989a56501f2d378f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:07:23 -0500 Subject: [PATCH 040/131] Add locc and hicc --- plugins/SfzSampler/CMakeLists.txt | 2 -- plugins/SfzSampler/SfzOpcodeState.cpp | 18 ++++++++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 4 ++++ plugins/SfzSampler/SfzRegion.cpp | 9 ++++++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index da0777392ea..488f3ce87f6 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -5,7 +5,6 @@ build_plugin(sfzsampler SfzSampler.h SfzSamplerView.cpp SfzSamplerView.h - SfzOpcodes.h SfzOpcodeState.cpp SfzOpcodeState.h SfzRegion.cpp @@ -23,7 +22,6 @@ build_plugin(sfzsampler MOCFILES SfzSampler.h SfzSamplerView.h - SfzOpcodes.h SfzOpcodeState.h SfzRegion.h SfzParser.h diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 4c55933b3bb..7c557283af6 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -10,6 +10,14 @@ namespace lmms { +SfzOpcodeState::SfzOpcodeState() +{ + // Certain arrays need to be initialized here in the constructor, since doing it in the header file is difficult + m_hicc.fill(127); +} + + + bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& value) { bool successful = true; @@ -249,6 +257,16 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // // Midi CC // + else if (name.startsWith("locc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_locc.at(ccNumber) = value.toInt(&successful); + } + else if (name.startsWith("hicc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_hicc.at(ccNumber) = value.toInt(&successful); + } else if (name.startsWith("set_cc")) { int ccNumber = ccNumberFromOpcode(name); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index b741c2804fe..c74198e43c4 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -13,6 +13,8 @@ namespace lmms class SfzOpcodeState { public: + SfzOpcodeState(); // We need a constructor to initialize things like m_hicc + bool setOpcodeByStrings(const QString& name, const QString& value); static int keyNumFromString(QString keyString, bool* successful); static int ccNumberFromOpcode(const QString& opcode); @@ -140,6 +142,8 @@ class SfzOpcodeState // // Midi CC // + std::array m_locc = {}; + std::array m_hicc = {}; // NOTE this needs to be initialized to contain all 127 in the constructor, since it's hard to fill an array directly within a header file //! Default midi CC values std::array m_set_cc = {}; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index d30c8e893ed..fa8c0acd506 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -39,6 +39,13 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // TODO add unit tests if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default) != m_sw_last) { return false; } + // If midi CC ranges are defined, make sure the current CC values are within range + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + const int ccValue = globalState.midiCCValue(i, m_set_cc.at(i)); + if (ccValue > m_hicc.at(i) || ccValue < m_locc.at(i)) { return false; } + } + // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. m_roundRobinCount++; if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn @@ -125,7 +132,7 @@ float SfzRegion::totalCCModulation(const std::array Date: Sun, 11 Jan 2026 19:23:50 -0500 Subject: [PATCH 041/131] Make noteOn/noteOff events use frame offset --- plugins/SfzSampler/SfzOpcodeState.cpp | 2 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 2 ++ plugins/SfzSampler/SfzSampler.cpp | 6 +++--- plugins/SfzSampler/SfzTrigger.cpp | 9 ++++++--- plugins/SfzSampler/SfzTrigger.h | 16 +++++++++------- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 7c557283af6..a93a5d42c37 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -275,7 +275,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name.startsWith("set_hdcc")) { int ccNumber = ccNumberFromOpcode(name); - m_set_cc.at(ccNumber) = value.toFloat(&successful) * 128; + m_set_cc.at(ccNumber) = value.toFloat(&successful) * 127; } else diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 39798310803..46baaca7e5c 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -18,6 +18,8 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger , m_trigger(trigger) , m_region(region) { + // Delay the start of the playback by the trigger offset + m_frameCount -= trigger.frameOffset(); // Set initial sample start frame offset m_sampleFrame += m_region->m_offset; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 7d9ce5d31c4..8965fb707e9 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -82,17 +82,17 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ { if (event.type() == MidiNoteOn) { - processTrigger(SfzTrigger::noteOnEvent(event.key(), event.velocity())); + processTrigger(SfzTrigger::noteOnEvent(offset, event.key(), event.velocity())); return true; } else if (event.type() == MidiNoteOff) { - processTrigger(SfzTrigger::noteOffEvent(event.key(), event.velocity())); + processTrigger(SfzTrigger::noteOffEvent(offset, event.key(), event.velocity())); return true; } else if (event.type() == MidiControlChange) { - processTrigger(SfzTrigger::controlChangeEvent(event.controllerNumber(), event.controllerValue())); + processTrigger(SfzTrigger::controlChangeEvent(offset, event.controllerNumber(), event.controllerValue())); return true; } return false; diff --git a/plugins/SfzSampler/SfzTrigger.cpp b/plugins/SfzSampler/SfzTrigger.cpp index 2f31a3a8621..acfd9691954 100644 --- a/plugins/SfzSampler/SfzTrigger.cpp +++ b/plugins/SfzSampler/SfzTrigger.cpp @@ -5,30 +5,33 @@ namespace lmms { -const SfzTrigger SfzTrigger::noteOnEvent(const int key, const int velocity) +const SfzTrigger SfzTrigger::noteOnEvent(const int frameOffset, const int key, const int velocity) { SfzTrigger trigger = SfzTrigger(); trigger.m_type = Type::NoteOn; trigger.m_key = key; trigger.m_velocity = velocity; + trigger.m_frameOffset = frameOffset; return trigger; } -const SfzTrigger SfzTrigger::noteOffEvent(const int key, const int velocity) +const SfzTrigger SfzTrigger::noteOffEvent(const int frameOffset, const int key, const int velocity) { SfzTrigger trigger = SfzTrigger(); trigger.m_type = Type::NoteOff; trigger.m_key = key; trigger.m_velocity = velocity; + trigger.m_frameOffset = frameOffset; return trigger; } -const SfzTrigger SfzTrigger::controlChangeEvent(const int controlNumber, const int value) +const SfzTrigger SfzTrigger::controlChangeEvent(const int frameOffset, const int controlNumber, const int value) { SfzTrigger trigger = SfzTrigger(); trigger.m_type = Type::ControlChange; trigger.m_controlChangeNumber = controlNumber; trigger.m_controlChangeValue = value; + trigger.m_frameOffset = frameOffset; // Frame offset is ignored for CC events, since making them sample-exact would be inconvenient return trigger; } diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzSampler/SfzTrigger.h index c4327f032d9..b24a8ac87ee 100644 --- a/plugins/SfzSampler/SfzTrigger.h +++ b/plugins/SfzSampler/SfzTrigger.h @@ -11,9 +11,9 @@ namespace lmms class SfzTrigger { public: - static const SfzTrigger noteOnEvent(const int key, const int vel); - static const SfzTrigger noteOffEvent(const int key, const int vel); - static const SfzTrigger controlChangeEvent(const int controlNumber, const int value); + static const SfzTrigger noteOnEvent(const int frameOffset, const int key, const int vel); + static const SfzTrigger noteOffEvent(const int frameOffset, const int key, const int vel); + static const SfzTrigger controlChangeEvent(const int frameOffset, const int controlNumber, const int value); enum class Type { @@ -27,13 +27,15 @@ class SfzTrigger const auto& velocity() const { return m_velocity; } const auto& controlChangeNumber() const { return m_controlChangeNumber; } const auto& controlChangeValue() const { return m_controlChangeValue; } + const int frameOffset() const { return m_frameOffset; } private: Type m_type; - std::optional m_key; - std::optional m_velocity; - std::optional m_controlChangeNumber; - std::optional m_controlChangeValue; + std::optional m_key = std::nullopt; + std::optional m_velocity = std::nullopt; + std::optional m_controlChangeNumber = std::nullopt; + std::optional m_controlChangeValue = std::nullopt; + int m_frameOffset = 0; }; From ca1ca5a9943386dcd15b30a289d310577ae26b37 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:13:06 -0500 Subject: [PATCH 042/131] Add delay and delay_random opcodes --- plugins/SfzSampler/SfzOpcodeState.cpp | 13 +++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 6 ++++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 6 ++++++ plugins/SfzSampler/SfzRegionPlayState.h | 2 +- plugins/SfzSampler/SfzTrigger.cpp | 2 +- 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index a93a5d42c37..188ba8dc1ee 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -128,6 +128,19 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } + // + // Delay + // + else if (name == "delay") + { + m_delay = value.toFloat(&successful); + } + else if (name == "delay_random") + { + m_delay_random = value.toFloat(&successful); + } + + // // Pitch // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index c74198e43c4..036a91028a4 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -70,6 +70,12 @@ class SfzOpcodeState }; LoopMode m_loop_mode = LoopMode::NoLoop; + // + // Delay + // + float m_delay = 0; // In seconds + float m_delay_random = 0; + // // Pitch diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 46baaca7e5c..f2faae337d1 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -18,8 +18,14 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger , m_trigger(trigger) , m_region(region) { + const float sampleRate = Engine::audioEngine()->outputSampleRate(); // Delay the start of the playback by the trigger offset m_frameCount -= trigger.frameOffset(); + // And by the delay opcode in seconds + m_frameCount -= region->m_delay * sampleRate; + // And any random delay amount + m_frameCount -= fastRand(1.0f) * region->m_delay_random * sampleRate; + // Set initial sample start frame offset m_sampleFrame += m_region->m_offset; diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index bbea5b3b775..86213a20fac 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -39,7 +39,7 @@ class SfzRegionPlayState //! Stores whether the note has been released yet bool m_released = false; - //! The number of frames since the start of the sound. This may be negative if a note starts partway through a buffer. + //! The number of frames since the start of the sound. This may be negative if the region has delay or the note starts partway through a buffer. int m_frameCount = 0; //! The frame at which the note was released, relative to the start of the note int m_releaseFrame = 0; diff --git a/plugins/SfzSampler/SfzTrigger.cpp b/plugins/SfzSampler/SfzTrigger.cpp index acfd9691954..0c769af737e 100644 --- a/plugins/SfzSampler/SfzTrigger.cpp +++ b/plugins/SfzSampler/SfzTrigger.cpp @@ -31,7 +31,7 @@ const SfzTrigger SfzTrigger::controlChangeEvent(const int frameOffset, const int trigger.m_type = Type::ControlChange; trigger.m_controlChangeNumber = controlNumber; trigger.m_controlChangeValue = value; - trigger.m_frameOffset = frameOffset; // Frame offset is ignored for CC events, since making them sample-exact would be inconvenient + trigger.m_frameOffset = frameOffset; // Frame offset is not currently for CC events, since making them sample-exact would be inconvenient return trigger; } From 56ec920906fceab577de6ac9cea52ea34420b990 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:06:51 -0500 Subject: [PATCH 043/131] Initial GUI --- plugins/SfzSampler/CMakeLists.txt | 2 +- plugins/SfzSampler/SfzSamplerView.cpp | 139 +++++++++++++++++++++++++- plugins/SfzSampler/SfzSamplerView.h | 73 ++++++++++++++ plugins/SfzSampler/full_logo.png | Bin 2535 -> 0 bytes plugins/SfzSampler/logo.png | Bin 759 -> 0 bytes plugins/SfzSampler/logo.svg | 35 +++++++ 6 files changed, 245 insertions(+), 4 deletions(-) delete mode 100644 plugins/SfzSampler/full_logo.png delete mode 100644 plugins/SfzSampler/logo.png create mode 100644 plugins/SfzSampler/logo.svg diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 488f3ce87f6..4fa3c6cdc30 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -29,5 +29,5 @@ build_plugin(sfzsampler SfzGlobalState.h SfzTrigger.h SfzSampleBuffer.h - EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.png" + EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.svg" ) diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 3be86bd5cd0..5fe5b2ed38b 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -50,16 +50,75 @@ namespace gui { SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) : InstrumentView(instrument, parent) + , m_logo(PLUGIN_NAME::getIconPixmap("logo")) , m_instrument(instrument) + , m_hBoxLayout(new QHBoxLayout) + , m_vBoxLayout(new QVBoxLayout) + , m_sidebarWidget(new SfzSidebarWidget(this)) + , m_controlsWidget(new SfzControlsWidget(this)) + , m_pianoWidget(new SfzPianoWidget(this)) + , m_controlsLayout(new QBoxLayout(QBoxLayout::TopToBottom, m_controlsWidget)) + , m_sidebarLayout(new QVBoxLayout(m_sidebarWidget)) { // window settings setAcceptDrops(true); setAutoFillBackground(true); setMaximumSize(QSize(10000, 10000)); - setMinimumSize(QSize(516, 400)); + setMinimumSize(QSize(s_sidebarMinWidth + s_controlsAreaWidth, s_controlsAreaHeight + s_keyboardAreaMinHeight)); - auto openfilebutton = new QPushButton("Open SFZ File", this); + setLayout(m_hBoxLayout); + m_hBoxLayout->addWidget(m_sidebarWidget); + m_hBoxLayout->addWidget(new SfzDividerLine(this, 3, 11, false)); + m_hBoxLayout->addLayout(m_vBoxLayout); + m_vBoxLayout->addWidget(m_controlsWidget); + m_vBoxLayout->addWidget(new SfzDividerLine(this, 2, 8)); + m_vBoxLayout->addWidget(m_pianoWidget); + + m_hBoxLayout->setContentsMargins(0, 0, 0, 0); + m_hBoxLayout->setSpacing(0); + m_vBoxLayout->setContentsMargins(0, 0, 0, 0); + m_vBoxLayout->setSpacing(0); + + m_controlsWidget->setFixedSize(QSize(s_controlsAreaWidth, s_controlsAreaHeight)); + m_sidebarWidget->setMinimumWidth(s_sidebarMinWidth); + m_pianoWidget->setMinimumHeight(s_keyboardAreaMinHeight); + + auto logoFrame = new SfzSidebarGroupBox(m_sidebarWidget); + auto logoLabel = new QLabel(logoFrame); + logoLabel->setPixmap(m_logo); + logoLabel->setAlignment(Qt::AlignCenter); + logoLabel->setFixedHeight(70); + logoFrame->layout()->setContentsMargins(0, 0, 0, 0); + logoFrame->layout()->addWidget(logoLabel); + + auto frame1 = new SfzSidebarGroupBox(m_sidebarWidget); + auto label1 = new QLabel("Testing1", frame1); + frame1->layout()->addWidget(label1); + + auto frame2 = new SfzSidebarGroupBox(m_sidebarWidget); + auto label2 = new QLabel("Testing2\nvery long\nwow", frame2); + auto label3 = new QLabel("another thing!", frame2); + frame2->layout()->addWidget(label2); + frame2->layout()->addWidget(label3); + + + m_sidebarLayout->addWidget(logoFrame); + m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); + m_sidebarLayout->addWidget(frame1); + m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); + m_sidebarLayout->addWidget(frame2); + m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); + m_sidebarLayout->addStretch(); + + + m_emptyFileLabel = new QLabel("Drag and drop or click to load SFZ file", m_controlsWidget); + m_emptyFileLabel->setAlignment(Qt::AlignCenter); + m_emptyFileLabel->move(s_controlsAreaWidth / 2, s_controlsAreaHeight / 2); + m_controlsLayout->addWidget(m_emptyFileLabel, 0, Qt::AlignCenter); + + + auto openfilebutton = new QPushButton("Open SFZ File", m_pianoWidget); openfilebutton->setIcon(embed::getIconPixmap("folder")); openfilebutton->setToolTip(tr("Open SFZ File")); connect(openfilebutton, &PixmapButton::clicked, [this](){ @@ -85,14 +144,88 @@ void SfzSamplerView::dropEvent(QDropEvent* de) { } +void SfzSamplerView::resizeEvent(QResizeEvent* re) +{ +} + void SfzSamplerView::paintEvent(QPaintEvent* pe) { QPainter brush(this); + brush.fillRect(rect(), QColor(19, 19, 20)); } -void SfzSamplerView::resizeEvent(QResizeEvent* re) +void SfzSidebarWidget::paintEvent(QPaintEvent* pe) { + QPainter brush(this); + brush.fillRect(rect(), QColor(19, 19, 20)); } +void SfzControlsWidget::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); + brush.fillRect(rect(), QColor(19, 19, 20)); +} + +void SfzPianoWidget::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); + brush.fillRect(rect(), QColor(72, 72, 75)); +} + +SfzSidebarGroupBox::SfzSidebarGroupBox(QWidget* parent) + : QFrame(parent) +{ + setLayout(new QVBoxLayout(this)); +} + +void SfzSidebarGroupBox::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); + brush.fillRect(rect(), QColor(34, 39, 39)); +} + + +SfzDividerLine::SfzDividerLine(QWidget* parent, float lineWidth, float dividerWidth, bool horizontal) + : QWidget(parent) + , m_lineWidth(lineWidth) + , m_dividerWidth(dividerWidth) + , m_horizontal(horizontal) +{ + if (horizontal) + { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(m_dividerWidth); + } + else + { + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + setFixedWidth(m_dividerWidth); + } +} + +void SfzDividerLine::paintEvent(QPaintEvent* pe) +{ + QPainter brush(this); + const float padding = 0.5f * (m_dividerWidth); + const float x1 = padding, y1 = padding, x2 = width() - padding, y2 = height() - padding; + + // Glow background of line + const int numPasses = 4; + for (int i = 1; i <= numPasses; ++i) + { + const float ratio = static_cast(i) / numPasses; + brush.setPen(QPen(QColor(131, 149, 255), m_dividerWidth * (1.0f - ratio), Qt::SolidLine, Qt::RoundCap)); + brush.setOpacity(ratio); + brush.drawLine(x1, y1, x2, y2); + } + // Actual line + brush.setPen(QPen(QColor(145, 232, 233), m_lineWidth, Qt::SolidLine, Qt::RoundCap)); + brush.setOpacity(1.0f); + brush.drawLine(x1, y1, x2, y2); +} + + + + } // namespace gui } // namespace lmms diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index cc39efb2efd..ba47856a5f4 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -25,6 +25,10 @@ #ifndef LMMS_GUI_SFZSAMPLER_VIEW_H #define LMMS_GUI_SFZSAMPLER_VIEW_H +#include +#include +#include +#include #include "InstrumentView.h" @@ -41,6 +45,51 @@ class Knob; class LcdSpinBox; class PixmapButton; +class SfzControlsWidget : public QWidget +{ +public: + using QWidget::QWidget; +protected: + void paintEvent(QPaintEvent* pe) override; +}; + +class SfzSidebarWidget : public QWidget +{ +public: + using QWidget::QWidget; +protected: + void paintEvent(QPaintEvent* pe) override; +}; + +class SfzPianoWidget : public QWidget +{ +public: + using QWidget::QWidget; +protected: + void paintEvent(QPaintEvent* pe) override; +}; + +class SfzSidebarGroupBox : public QFrame +{ +public: + SfzSidebarGroupBox(QWidget* parent = nullptr); +protected: + void paintEvent(QPaintEvent* pe) override; +}; + +class SfzDividerLine : public QWidget +{ +public: + SfzDividerLine(QWidget* parent = nullptr, float lineWidth = 3, float dividerWidth = 11, bool horizontal = true); +protected: + void paintEvent(QPaintEvent* pe) override; + float m_lineWidth = 3; + float m_dividerWidth = 11; + bool m_horizontal = true; +}; + + + class SfzSamplerView : public InstrumentView { Q_OBJECT @@ -48,6 +97,13 @@ class SfzSamplerView : public InstrumentView public: SfzSamplerView(SfzSampler* instrument, QWidget* parent); + static constexpr int s_controlsAreaWidth = 775; // Once parsing aria xml gui configurations is implemented, these hardcoded values will not be as needed + static constexpr int s_controlsAreaHeight = 335; + + static constexpr int s_sidebarMinWidth = 200; + + static constexpr int s_keyboardAreaMinHeight = 50; + protected: void dragEnterEvent(QDragEnterEvent* dee) override; void dropEvent(QDropEvent* de) override; @@ -58,8 +114,25 @@ class SfzSamplerView : public InstrumentView private: bool isResizable() const override { return true; } + QPixmap m_logo; + SfzSampler* m_instrument; + + QHBoxLayout* m_hBoxLayout; + QVBoxLayout* m_vBoxLayout; + SfzSidebarWidget* m_sidebarWidget; + SfzControlsWidget* m_controlsWidget; + SfzPianoWidget* m_pianoWidget; + + QBoxLayout* m_controlsLayout; + + QVBoxLayout* m_sidebarLayout; + + QLabel* m_emptyFileLabel; }; + } // namespace gui + } // namespace lmms + #endif // LMMS_GUI_SFZSAMPLER_VIEW_H diff --git a/plugins/SfzSampler/full_logo.png b/plugins/SfzSampler/full_logo.png deleted file mode 100644 index aa8c1d26a0073971e9b502b17ab0667f76d0659a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2535 zcmVEX>4Tx04R}tkv&MmKpe$iQ^g{!4t5Z6$WX<>f>;qptwIqhlv<%x2a`*`ph-iL z;^HW{799LotU9+0Yt2!bCV&JIqBE>hzEl0u6Z503ls?%w0>9UwF+Of|bE09CV$ zbRsThbE{&{D+1_42xEvz%+%*nsU$qd*FAiEy^HcJ?{j~SkdikU;1h{wnQmCb8^qI_ zmd<&fILu0tLVQjQ9~IOScuZ9kzyiE`*9EdkmFC0OD0zt zj2sK7LWSh`!T;cQw`L(W=_Uo^K=+Gne~bV$WEE0hc?#;FB&Hk|X(P3WWmjen#Jv0|st^-Zi(k);>-jfDCn&ya5gl zfzcvmuY0^Z(AnF+XIlOJ0Iu0`zXUl&T>t<824YJ`L;wH)0002_L%V+f000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j~b64k#Nt%0=-2000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000NRNklD_%+ajpL z6&MKI1Ux`nzcB8bbI+N<4>SSLR-MVJgAuOEuPHziPzTiNYXi{Q zZw$QQ=^A4U{PdliBsAdxNmUp!6_h++B5*&Dp`ZH|mM?&_dej1K>T&!ox6>qz+?-128VP)7){4V~oiMRsoZNe7$%qk}D!L z+E)p%TJ`HhzxU+dPW^U1(ByJEeUaLy#Br0B z0vpw>^$9V?2=FUC()y?)841h*<``p^f3UCgOv#G%^?(6}G_+ovIBs}e+?`u3DIS;G z*%)p6lo)S74)Di52+elj2mK}$7O~8o_pOI2ow8VD%esN+QP)>&2b}2I+Y_ z@FH+PQ?yv5HGy+{r3*AI!0&*=`b@tWiM=rT--n!MMmd(2u8M2dns$|}T2t-w`EA$R zJ+r)>-U)uce?-@I*P#T033I@oi|-CI9a;vnMuG5ZJ!UENVH*N1?LQ~P|p__ zW7dm^uNQ+eKlFzuBGofZG^zDAa9-d3JsQ)ByWM!2B zUc~AlP+|2dWQhp&hXPv&RDS`E-0hgH$G}l|sx|P8X0ld=YrV!CX#}dKOA;`@D?uZd zmsNbvVJ}LFGZtw8snygRD1LU6cd3rBY4lrA<+1NH||yNc-M8O^l;6uiBn%v5%(#$j;A&6SMJ`{e?LGzE zw3`J_eaAOsNQTvFwXo{fE3UjXYU|to*s=ee-Uf+?{F^X~&c>L>fOqwN*?`r?n8o+9 z6cX!N)c|5&r%KTqy*@-&*S2b8JBl401BGTTCIyJ;D zwkU0JT|`VAP`$6&>=som1hyGto)r;4y7OuotZ@{ovrv5cS?}#=YVWhI=jC{6qB7%GhrDSrdc(S9j@evo7M^RyGtLtKCq1 z%;=HR6D(K~4PSX##pQW37j4zp;*I+HdPlr{Enc$dmoB&S7jwQndv}Ci>^jZj8tB6! zBCCO|A*yA-hJG327-MK|ZlU371N;9~%G;aY%?|w-#3TY>5!qpk`6sXxSU_M$T-Lq# z@!X-j2xRl+a>`4_m>cnIfV(4m#+lVvfdRk>HU1P}mZsU+1hyHZ5!f9ctU34tpkRP{ z0+rkk)eC$CRJq*FPLk3$yk8WMhu)uboW;lZs6j4Yz39O)>0j?2P zh-=pO|A}UMVp94TI9>iSkp+BBGoyTkB}=mk4}r}|O$4g6pKGSlgzk=axt;!`nfp07 zT9O|Qj8#Cg^>LIzU{iGPG4r{4kMmkQs|VV;?u1WDKLha@GO{13~A;2@8szjXir002ovPDHLkV1iv7uQ>n! diff --git a/plugins/SfzSampler/logo.png b/plugins/SfzSampler/logo.png deleted file mode 100644 index f2c4fabf14facdba53038995a70700bd9a14ad06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 759 zcmVe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Li0L_t(&-tC%AXcJKsM$g1( zO9)OEDF{*!@h3D0y3#C^P!MVnbfa{kM07J11ee)}h%SO|G*S&!Qlp?kMT=Teiedz@ zRTo975~bCGDYOPFQwfc2(sA<|CJka2NjtC1x4iS1`3`sPeRqbZghMe_f}8-M@bQwM zs;awyAP@k&7V(+_l0aP7^#SvRa43dj;c8V?uLJ#nW+4cK7Y*na9!um{0Fe-jtu9S? zEe8>l%@UPGvpF*kQ%z_R*0?6T7c&x+xM2m&AOVR%0%j1e#30_Xt|hZ<5KE3I%eoo( zPU7Es00pHaUYN^suDhkOAgwJLr-ElH3h?dw7uv72vvc$QG6-p2t)IVs5b3x`lkW(t z98NiFWIC7SO6LW3Z#+uPYL}eFUMS@0?253(wHMXpk+;|%Kf6!giv;ePH~(F07;}{e z>3j8v(Xru0-LP@f4o)0CT@uUvnPzzO6CdBE$z(^!WJi{#{Z39zFz|k2Q8!%YSzi|W z{B?-Cce>cqc!;13D;vZHu|aGQ8^i{&L2M8k#G0zwf33lE4} + + + + + + + + + + + + From 6e97ff79fc0d2f74ee65782bb49c8042116e9761 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 14 Jan 2026 16:29:17 -0500 Subject: [PATCH 044/131] Initial piano view --- plugins/SfzSampler/SfzSamplerView.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 5fe5b2ed38b..2f91cb96501 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -154,24 +154,46 @@ void SfzSamplerView::paintEvent(QPaintEvent* pe) brush.fillRect(rect(), QColor(19, 19, 20)); } + void SfzSidebarWidget::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.fillRect(rect(), QColor(19, 19, 20)); } + void SfzControlsWidget::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.fillRect(rect(), QColor(19, 19, 20)); } + void SfzPianoWidget::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.fillRect(rect(), QColor(72, 72, 75)); + + brush.setPen(QPen(QColor(47, 54, 94), 1)); + for (int key = 0; key < 128; ++key) + { + bool isBlack = false; + if (key % 12 == 1 || key % 12 == 3 || key % 12 == 6 || key % 12 == 8 || key % 12 == 10) { isBlack = true; } + if (isBlack) + { + QRect noteRect(key * width() / 128.0f, 0, width() / 128.0f, height() / 2); + brush.drawRect(noteRect); + brush.fillRect(noteRect, QColor(22, 22, 25)); + brush.drawLine((key + 0.5f) * width() / 128.0f, height() / 2, (key + 0.5f) * width() / 128.0f, height()); + } + if (key % 12 == 0 || key % 12 == 5) + { + brush.drawLine(key * width() / 128.0f, 0, key * width() / 128.0f, height()); + } + } } + SfzSidebarGroupBox::SfzSidebarGroupBox(QWidget* parent) : QFrame(parent) { From 0a4e91260f24f246fa3efd8eae5a8d0becc11acd Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:05:53 -0500 Subject: [PATCH 045/131] More gui testing --- data/themes/classic/style.css | 4 ++ data/themes/default/style.css | 5 ++ plugins/SfzSampler/SfzSamplerView.cpp | 75 ++++++++++++++++++--------- plugins/SfzSampler/SfzSamplerView.h | 10 ++++ 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index baaaab6ec56..8c80c78cd78 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -1028,6 +1028,10 @@ lmms--gui--SlicerTView lmms--gui--Knob { qproperty-lineWidth: 3; } +lmms--gui--SfzSamplerView QLabel { + font: 10pt; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 583ef8ec7ea..fbdaa6c30ff 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -1088,6 +1088,11 @@ lmms--gui--SlicerTView lmms--gui--Knob { qproperty-lineWidth: 3; } + +lmms--gui--SfzSamplerView QLabel { + font: 10pt; +} + lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 2f91cb96501..cb626256762 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -84,6 +84,7 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) m_sidebarWidget->setMinimumWidth(s_sidebarMinWidth); m_pianoWidget->setMinimumHeight(s_keyboardAreaMinHeight); + // Logo at top of sidebar auto logoFrame = new SfzSidebarGroupBox(m_sidebarWidget); auto logoLabel = new QLabel(logoFrame); logoLabel->setPixmap(m_logo); @@ -92,19 +93,20 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) logoFrame->layout()->setContentsMargins(0, 0, 0, 0); logoFrame->layout()->addWidget(logoLabel); + m_sidebarLayout->addWidget(logoFrame); + m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); + + // Setup info labels auto frame1 = new SfzSidebarGroupBox(m_sidebarWidget); - auto label1 = new QLabel("Testing1", frame1); - frame1->layout()->addWidget(label1); + m_sfzPathLabel = new QLabel(frame1); + frame1->layout()->addWidget(m_sfzPathLabel); + m_sampleCountLabel = new QLabel(frame1); + frame1->layout()->addWidget(m_sampleCountLabel); auto frame2 = new SfzSidebarGroupBox(m_sidebarWidget); - auto label2 = new QLabel("Testing2\nvery long\nwow", frame2); - auto label3 = new QLabel("another thing!", frame2); - frame2->layout()->addWidget(label2); - frame2->layout()->addWidget(label3); - + m_activeVoiceCountLabel = new QLabel(frame2); + frame2->layout()->addWidget(m_activeVoiceCountLabel); - m_sidebarLayout->addWidget(logoFrame); - m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); m_sidebarLayout->addWidget(frame1); m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); m_sidebarLayout->addWidget(frame2); @@ -112,36 +114,49 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) m_sidebarLayout->addStretch(); - m_emptyFileLabel = new QLabel("Drag and drop or click to load SFZ file", m_controlsWidget); + m_emptyFileLabel = new QLabel(tr("Drag and drop or click to load SFZ file"), m_controlsWidget); m_emptyFileLabel->setAlignment(Qt::AlignCenter); m_emptyFileLabel->move(s_controlsAreaWidth / 2, s_controlsAreaHeight / 2); m_controlsLayout->addWidget(m_emptyFileLabel, 0, Qt::AlignCenter); + update(); +} - auto openfilebutton = new QPushButton("Open SFZ File", m_pianoWidget); - openfilebutton->setIcon(embed::getIconPixmap("folder")); - openfilebutton->setToolTip(tr("Open SFZ File")); - connect(openfilebutton, &PixmapButton::clicked, [this](){ - auto openFileDialog = FileDialog(nullptr, QObject::tr("Open SFZ File")); - auto dir = ConfigManager::inst()->userSamplesDir(); - openFileDialog.setDirectory(dir); - if (openFileDialog.exec() == QDialog::Accepted) - { - if (openFileDialog.selectedFiles().isEmpty()) { return; } - m_instrument->loadFile(openFileDialog.selectedFiles()[0]); - } - }); - update(); +void SfzSamplerView::openFile() +{ + auto openFileDialog = FileDialog(nullptr, QObject::tr("Open SFZ File")); + auto dir = ConfigManager::inst()->userSamplesDir(); + openFileDialog.setDirectory(dir); + if (openFileDialog.exec() == QDialog::Accepted) + { + if (openFileDialog.selectedFiles().isEmpty()) { return; } + m_instrument->loadFile(openFileDialog.selectedFiles()[0]); + } } void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) { + QString value = StringPairDrag::decodeValue(dee); + if (value.endsWith(".sfz")) + { + dee->accept(); + m_instrument->loadFile(value); + } + dee->ignore(); } void SfzSamplerView::dropEvent(QDropEvent* de) { + QString value = StringPairDrag::decodeValue(de); + if (value.endsWith(".sfz")) + { + de->accept(); + m_instrument->loadFile(value); + return; + } + de->ignore(); } void SfzSamplerView::resizeEvent(QResizeEvent* re) @@ -152,6 +167,13 @@ void SfzSamplerView::paintEvent(QPaintEvent* pe) { QPainter brush(this); brush.fillRect(rect(), QColor(19, 19, 20)); + + // Update info labels + m_sfzPathLabel->setText(tr("Loaded File: %1").arg("todo")); + + m_sampleCountLabel->setText(tr("Sample Count: %1").arg("123")); + + m_activeVoiceCountLabel->setText(tr("Active Voices: %1").arg("3")); } @@ -162,6 +184,11 @@ void SfzSidebarWidget::paintEvent(QPaintEvent* pe) } +void SfzControlsWidget::mousePressEvent(QMouseEvent* pe) +{ + static_cast(parent())->openFile(); +} + void SfzControlsWidget::paintEvent(QPaintEvent* pe) { QPainter brush(this); diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index ba47856a5f4..6a41bc5027f 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -50,6 +50,7 @@ class SfzControlsWidget : public QWidget public: using QWidget::QWidget; protected: + void mousePressEvent(QMouseEvent* pe) override; void paintEvent(QPaintEvent* pe) override; }; @@ -104,6 +105,9 @@ class SfzSamplerView : public InstrumentView static constexpr int s_keyboardAreaMinHeight = 50; +public slots: + void openFile(); + protected: void dragEnterEvent(QDragEnterEvent* dee) override; void dropEvent(QDropEvent* de) override; @@ -128,6 +132,12 @@ class SfzSamplerView : public InstrumentView QVBoxLayout* m_sidebarLayout; + QLabel* m_sfzPathLabel; + QLabel* m_sampleCountLabel; + + QLabel* m_activeVoiceCountLabel; + + QLabel* m_emptyFileLabel; }; From 43d79aeacd8188f6459bcd5de12607b85d46713a Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:12:25 -0500 Subject: [PATCH 046/131] Remove GUI. Maybe the initial version doesn't need a fancy GUI --- plugins/SfzSampler/SfzSamplerView.cpp | 174 +------------------------- plugins/SfzSampler/SfzSamplerView.h | 85 ------------- 2 files changed, 4 insertions(+), 255 deletions(-) diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index cb626256762..fd5df8cc15a 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -50,74 +50,14 @@ namespace gui { SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) : InstrumentView(instrument, parent) - , m_logo(PLUGIN_NAME::getIconPixmap("logo")) , m_instrument(instrument) - , m_hBoxLayout(new QHBoxLayout) - , m_vBoxLayout(new QVBoxLayout) - , m_sidebarWidget(new SfzSidebarWidget(this)) - , m_controlsWidget(new SfzControlsWidget(this)) - , m_pianoWidget(new SfzPianoWidget(this)) - , m_controlsLayout(new QBoxLayout(QBoxLayout::TopToBottom, m_controlsWidget)) - , m_sidebarLayout(new QVBoxLayout(m_sidebarWidget)) { // window settings setAcceptDrops(true); setAutoFillBackground(true); setMaximumSize(QSize(10000, 10000)); - setMinimumSize(QSize(s_sidebarMinWidth + s_controlsAreaWidth, s_controlsAreaHeight + s_keyboardAreaMinHeight)); - - setLayout(m_hBoxLayout); - m_hBoxLayout->addWidget(m_sidebarWidget); - m_hBoxLayout->addWidget(new SfzDividerLine(this, 3, 11, false)); - m_hBoxLayout->addLayout(m_vBoxLayout); - m_vBoxLayout->addWidget(m_controlsWidget); - m_vBoxLayout->addWidget(new SfzDividerLine(this, 2, 8)); - m_vBoxLayout->addWidget(m_pianoWidget); - - m_hBoxLayout->setContentsMargins(0, 0, 0, 0); - m_hBoxLayout->setSpacing(0); - m_vBoxLayout->setContentsMargins(0, 0, 0, 0); - m_vBoxLayout->setSpacing(0); - - m_controlsWidget->setFixedSize(QSize(s_controlsAreaWidth, s_controlsAreaHeight)); - m_sidebarWidget->setMinimumWidth(s_sidebarMinWidth); - m_pianoWidget->setMinimumHeight(s_keyboardAreaMinHeight); - - // Logo at top of sidebar - auto logoFrame = new SfzSidebarGroupBox(m_sidebarWidget); - auto logoLabel = new QLabel(logoFrame); - logoLabel->setPixmap(m_logo); - logoLabel->setAlignment(Qt::AlignCenter); - logoLabel->setFixedHeight(70); - logoFrame->layout()->setContentsMargins(0, 0, 0, 0); - logoFrame->layout()->addWidget(logoLabel); - - m_sidebarLayout->addWidget(logoFrame); - m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); - - // Setup info labels - auto frame1 = new SfzSidebarGroupBox(m_sidebarWidget); - m_sfzPathLabel = new QLabel(frame1); - frame1->layout()->addWidget(m_sfzPathLabel); - m_sampleCountLabel = new QLabel(frame1); - frame1->layout()->addWidget(m_sampleCountLabel); - - auto frame2 = new SfzSidebarGroupBox(m_sidebarWidget); - m_activeVoiceCountLabel = new QLabel(frame2); - frame2->layout()->addWidget(m_activeVoiceCountLabel); - - m_sidebarLayout->addWidget(frame1); - m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); - m_sidebarLayout->addWidget(frame2); - m_sidebarLayout->addWidget(new SfzDividerLine(this, 2, 8)); - m_sidebarLayout->addStretch(); - - - m_emptyFileLabel = new QLabel(tr("Drag and drop or click to load SFZ file"), m_controlsWidget); - m_emptyFileLabel->setAlignment(Qt::AlignCenter); - m_emptyFileLabel->move(s_controlsAreaWidth / 2, s_controlsAreaHeight / 2); - m_controlsLayout->addWidget(m_emptyFileLabel, 0, Qt::AlignCenter); + setMinimumSize(QSize(250, 250)); update(); } @@ -143,6 +83,7 @@ void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) { dee->accept(); m_instrument->loadFile(value); + return; } dee->ignore(); } @@ -165,116 +106,9 @@ void SfzSamplerView::resizeEvent(QResizeEvent* re) void SfzSamplerView::paintEvent(QPaintEvent* pe) { - QPainter brush(this); - brush.fillRect(rect(), QColor(19, 19, 20)); - - // Update info labels - m_sfzPathLabel->setText(tr("Loaded File: %1").arg("todo")); - - m_sampleCountLabel->setText(tr("Sample Count: %1").arg("123")); - - m_activeVoiceCountLabel->setText(tr("Active Voices: %1").arg("3")); -} - - -void SfzSidebarWidget::paintEvent(QPaintEvent* pe) -{ - QPainter brush(this); - brush.fillRect(rect(), QColor(19, 19, 20)); -} - - -void SfzControlsWidget::mousePressEvent(QMouseEvent* pe) -{ - static_cast(parent())->openFile(); + //QPainter brush(this); + //brush.fillRect(rect(), QColor(19, 19, 20)); } -void SfzControlsWidget::paintEvent(QPaintEvent* pe) -{ - QPainter brush(this); - brush.fillRect(rect(), QColor(19, 19, 20)); -} - - -void SfzPianoWidget::paintEvent(QPaintEvent* pe) -{ - QPainter brush(this); - brush.fillRect(rect(), QColor(72, 72, 75)); - - brush.setPen(QPen(QColor(47, 54, 94), 1)); - for (int key = 0; key < 128; ++key) - { - bool isBlack = false; - if (key % 12 == 1 || key % 12 == 3 || key % 12 == 6 || key % 12 == 8 || key % 12 == 10) { isBlack = true; } - if (isBlack) - { - QRect noteRect(key * width() / 128.0f, 0, width() / 128.0f, height() / 2); - brush.drawRect(noteRect); - brush.fillRect(noteRect, QColor(22, 22, 25)); - brush.drawLine((key + 0.5f) * width() / 128.0f, height() / 2, (key + 0.5f) * width() / 128.0f, height()); - } - if (key % 12 == 0 || key % 12 == 5) - { - brush.drawLine(key * width() / 128.0f, 0, key * width() / 128.0f, height()); - } - } -} - - -SfzSidebarGroupBox::SfzSidebarGroupBox(QWidget* parent) - : QFrame(parent) -{ - setLayout(new QVBoxLayout(this)); -} - -void SfzSidebarGroupBox::paintEvent(QPaintEvent* pe) -{ - QPainter brush(this); - brush.fillRect(rect(), QColor(34, 39, 39)); -} - - -SfzDividerLine::SfzDividerLine(QWidget* parent, float lineWidth, float dividerWidth, bool horizontal) - : QWidget(parent) - , m_lineWidth(lineWidth) - , m_dividerWidth(dividerWidth) - , m_horizontal(horizontal) -{ - if (horizontal) - { - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - setFixedHeight(m_dividerWidth); - } - else - { - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); - setFixedWidth(m_dividerWidth); - } -} - -void SfzDividerLine::paintEvent(QPaintEvent* pe) -{ - QPainter brush(this); - const float padding = 0.5f * (m_dividerWidth); - const float x1 = padding, y1 = padding, x2 = width() - padding, y2 = height() - padding; - - // Glow background of line - const int numPasses = 4; - for (int i = 1; i <= numPasses; ++i) - { - const float ratio = static_cast(i) / numPasses; - brush.setPen(QPen(QColor(131, 149, 255), m_dividerWidth * (1.0f - ratio), Qt::SolidLine, Qt::RoundCap)); - brush.setOpacity(ratio); - brush.drawLine(x1, y1, x2, y2); - } - // Actual line - brush.setPen(QPen(QColor(145, 232, 233), m_lineWidth, Qt::SolidLine, Qt::RoundCap)); - brush.setOpacity(1.0f); - brush.drawLine(x1, y1, x2, y2); -} - - - - } // namespace gui } // namespace lmms diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index 6a41bc5027f..01f3584dcee 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -25,72 +25,14 @@ #ifndef LMMS_GUI_SFZSAMPLER_VIEW_H #define LMMS_GUI_SFZSAMPLER_VIEW_H -#include -#include -#include -#include - #include "InstrumentView.h" -class QPushButton; - namespace lmms { class SfzSampler; namespace gui { -class ComboBox; -class Knob; -class LcdSpinBox; -class PixmapButton; - -class SfzControlsWidget : public QWidget -{ -public: - using QWidget::QWidget; -protected: - void mousePressEvent(QMouseEvent* pe) override; - void paintEvent(QPaintEvent* pe) override; -}; - -class SfzSidebarWidget : public QWidget -{ -public: - using QWidget::QWidget; -protected: - void paintEvent(QPaintEvent* pe) override; -}; - -class SfzPianoWidget : public QWidget -{ -public: - using QWidget::QWidget; -protected: - void paintEvent(QPaintEvent* pe) override; -}; - -class SfzSidebarGroupBox : public QFrame -{ -public: - SfzSidebarGroupBox(QWidget* parent = nullptr); -protected: - void paintEvent(QPaintEvent* pe) override; -}; - -class SfzDividerLine : public QWidget -{ -public: - SfzDividerLine(QWidget* parent = nullptr, float lineWidth = 3, float dividerWidth = 11, bool horizontal = true); -protected: - void paintEvent(QPaintEvent* pe) override; - float m_lineWidth = 3; - float m_dividerWidth = 11; - bool m_horizontal = true; -}; - - - class SfzSamplerView : public InstrumentView { Q_OBJECT @@ -98,13 +40,6 @@ class SfzSamplerView : public InstrumentView public: SfzSamplerView(SfzSampler* instrument, QWidget* parent); - static constexpr int s_controlsAreaWidth = 775; // Once parsing aria xml gui configurations is implemented, these hardcoded values will not be as needed - static constexpr int s_controlsAreaHeight = 335; - - static constexpr int s_sidebarMinWidth = 200; - - static constexpr int s_keyboardAreaMinHeight = 50; - public slots: void openFile(); @@ -118,27 +53,7 @@ public slots: private: bool isResizable() const override { return true; } - QPixmap m_logo; - SfzSampler* m_instrument; - - QHBoxLayout* m_hBoxLayout; - QVBoxLayout* m_vBoxLayout; - SfzSidebarWidget* m_sidebarWidget; - SfzControlsWidget* m_controlsWidget; - SfzPianoWidget* m_pianoWidget; - - QBoxLayout* m_controlsLayout; - - QVBoxLayout* m_sidebarLayout; - - QLabel* m_sfzPathLabel; - QLabel* m_sampleCountLabel; - - QLabel* m_activeVoiceCountLabel; - - - QLabel* m_emptyFileLabel; }; } // namespace gui From 635569a8fdf4764716b539e1092399ca7fb95794 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:03:48 -0500 Subject: [PATCH 047/131] Initial GUI, and some provisions for dedicated handling of header --- plugins/SfzSampler/SfzGlobalState.cpp | 9 ++++++++ plugins/SfzSampler/SfzGlobalState.h | 9 +++++--- plugins/SfzSampler/SfzParser.cpp | 6 ++++-- plugins/SfzSampler/SfzParser.h | 2 +- plugins/SfzSampler/SfzRegion.cpp | 4 ++-- plugins/SfzSampler/SfzSampler.cpp | 9 +++++++- plugins/SfzSampler/SfzSampler.h | 10 +++++++++ plugins/SfzSampler/SfzSamplerView.cpp | 31 ++++++++++++++++++++++++++- 8 files changed, 70 insertions(+), 10 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 4c3cff0b233..68e3e100176 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -44,4 +44,13 @@ std::optional SfzGlobalState::lastKeyPressedInRange(const int lowKey, const } +void SfzGlobalState::initializeMidiCCValues(const SfzOpcodeState& controlsConfig) +{ + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + m_ccValues.at(i) = controlsConfig.m_set_cc.at(i); + } +} + + } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index c4bd0618417..4f54159d33e 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -25,7 +25,10 @@ class SfzGlobalState std::optional lastKeyPressedInRange(const int lowKey, const int highKey, const std::optional defaultKey) const; //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. - int midiCCValue(const int index, const int defaultValue) const { return m_ccValues.at(index).value_or(defaultValue); } + int midiCCValue(const int index) const { return m_ccValues.at(index); } + + // Sets initial values for the controls, based on any `set_ccN` opcodes in the header + void initializeMidiCCValues(const SfzOpcodeState& controlsConfig); private: //! Stores a number for every key, tracking which order the keys were played in @@ -39,9 +42,9 @@ class SfzGlobalState std::array m_activeKeys = {}; //! Stores the current value of all the midi CC knobs/controllers - //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 128 to get a float between 0 and 1 works fine. + //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 127 to get a float between 0 and 1 works fine. //! std::optional is used so that sfz file can specify default cc values - std::array, SfzOpcodeState::NumMidiCCs> m_ccValues = {}; + std::array m_ccValues = {}; }; diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 63f8495f829..5a117947979 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -10,7 +10,7 @@ namespace lmms { -bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions) +bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzOpcodeState& controlsConfig) { // Clear the vector of regions just in case anything is inside it from before outputRegions.clear(); @@ -212,7 +212,9 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou { auto opcodeNameAndValue = segment.split("="); if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } - // This is Wrong. The control header is not the same thing as the global header. Buuuut.... it works. And from sfzformat.com, it sounds like different sfz players do different things with this header anyway, so it's fine. + controlsConfig.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + // Also set the opcode for the global state, that way it will get propagated down to all other regions (this is needed for things like `default_path` to work for all regions) + // Technically it could be reworked so that the regions fetch any needed info from the controls state, but for now this way is simpler. globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index 6735f6f573c..7173d8b0548 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -13,7 +13,7 @@ namespace lmms class SfzParser { public: - static bool parseSfzFile(const QString& filePath, std::vector& outputRegions); + static bool parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzOpcodeState& controlsConfig); private: //! Helper function to parse any #include or #define statements from a string, recursively so that the included files have their includes and defines handled too diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index fa8c0acd506..67696748c60 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -42,7 +42,7 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // If midi CC ranges are defined, make sure the current CC values are within range for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { - const int ccValue = globalState.midiCCValue(i, m_set_cc.at(i)); + const int ccValue = globalState.midiCCValue(i); if (ccValue > m_hicc.at(i) || ccValue < m_locc.at(i)) { return false; } } @@ -132,7 +132,7 @@ float SfzRegion::totalCCModulation(const std::array headers of the .sfz (accounting for and defaults) and populate m_sfzRegions with the new SfzRegion - bool successfulParseFile = SfzParser::parseSfzFile(filePath, m_sfzRegions); + // The header is also parsed into a separate object for easy access by the gui + bool successfulParseFile = SfzParser::parseSfzFile(filePath, m_sfzRegions, m_controlsConfig); qDebug() << "was okay?" << successfulParseFile; qDebug() << "num regions" << m_sfzRegions.size(); @@ -161,7 +162,13 @@ void SfzSampler::loadFile(const QString& filePath) qDebug() << "sample was load okay?" << successfulLoadSample; } + // Reset the note counts, midi cc values, etc + m_sfzGlobalState = SfzGlobalState(); + // Set the initial cc values based on any `set_ccN` opcodes in the header + m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); + m_sfzFilePath = filePath; + emit fileLoaded(); } void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 031518d7216..5cc5958f894 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -59,6 +59,9 @@ class SfzSampler : public Instrument QString nodeName() const override; gui::PluginView* instantiateView(QWidget* parent) override; +signals: + void fileLoaded(); + private: void processTrigger(const SfzTrigger& trigger); @@ -69,8 +72,15 @@ class SfzSampler : public Instrument //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; + //! Holds information about midi CC default values, control labels, etc + SfzOpcodeState m_controlsConfig; + std::vector m_sfzRegions; + // The GUI needs models to connect to midi CC knobs, so a bunch of dummy models are defined here. + // Ideally it would be better to reuse the midi CC models for the instrument track + std::array m_ccModels; + friend class gui::SfzSamplerView; }; } // namespace lmms diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index fd5df8cc15a..2521fb4418a 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include "Clipboard.h" #include "ComboBox.h" @@ -43,6 +45,7 @@ #include "FileDialog.h" #include "PathUtil.h" #include "embed.h" +#include "MidiEvent.h" namespace lmms { @@ -52,13 +55,39 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) : InstrumentView(instrument, parent) , m_instrument(instrument) { - // window settings setAcceptDrops(true); setAutoFillBackground(true); setMaximumSize(QSize(10000, 10000)); setMinimumSize(QSize(250, 250)); + auto layout1 = new QVBoxLayout(this); + + auto openFileButton = new QPushButton(embed::getIconPixmap("folder"), tr("Open SFZ File"), this); + connect(openFileButton, &PixmapButton::clicked, this, &SfzSamplerView::openFile); + layout1->addWidget(openFileButton); + + // Initialize Midi CC knobs + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + m_instrument->m_ccModels.at(i).setRange(0, 127, 1); + m_instrument->m_ccModels.at(i).setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); + auto ccKnob = new Knob(KnobType::Bright26, tr("CC %1").arg(i), this); + ccKnob->setModel(&m_instrument->m_ccModels.at(i)); + layout1->addWidget(ccKnob); + connect(&m_instrument->m_ccModels.at(i), &AutomatableModel::dataChanged, this, [i, this](){ + m_instrument->handleMidiEvent(MidiEvent(MidiControlChange, 0, i, m_instrument->m_ccModels.at(i).value())); + }); + } + + // Whenever a new SFZ file is loaded, set the default CC values + connect(m_instrument, &SfzSampler::fileLoaded, [this](){ + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + m_instrument->m_ccModels.at(i).setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); + } + }); + update(); } From 5ea1dd76f970e34d93cba88a19a6cb4d4653005c Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:24:02 -0500 Subject: [PATCH 048/131] Add label_ccN opcode --- plugins/SfzSampler/SfzOpcodeState.cpp | 5 +++++ plugins/SfzSampler/SfzOpcodeState.h | 2 ++ plugins/SfzSampler/SfzParser.cpp | 1 + plugins/SfzSampler/SfzSamplerView.cpp | 10 +++++++--- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 188ba8dc1ee..3104b39e464 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -290,6 +290,11 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu int ccNumber = ccNumberFromOpcode(name); m_set_cc.at(ccNumber) = value.toFloat(&successful) * 127; } + else if (name.startsWith("label_cc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_label_cc.at(ccNumber) = value; + } else { diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 036a91028a4..68dfce330f8 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -152,6 +152,8 @@ class SfzOpcodeState std::array m_hicc = {}; // NOTE this needs to be initialized to contain all 127 in the constructor, since it's hard to fill an array directly within a header file //! Default midi CC values std::array m_set_cc = {}; + //! Human readable names for the controls + std::array, NumMidiCCs> m_label_cc = {}; friend class SfzRegion; }; diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 5a117947979..4ae7a1300b7 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -190,6 +190,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou case Header::Global: { // Do nothing. The global headers were already handled above + // TODO this is wrong, some sfz's treat global headers as local, expecting that one after another will overwrite the last break; } case Header::Group: diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 2521fb4418a..7dc80b35e15 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -59,7 +59,7 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) setAutoFillBackground(true); setMaximumSize(QSize(10000, 10000)); - setMinimumSize(QSize(250, 250)); + setMinimumSize(QSize(500, 500)); auto layout1 = new QVBoxLayout(this); @@ -67,14 +67,18 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) connect(openFileButton, &PixmapButton::clicked, this, &SfzSamplerView::openFile); layout1->addWidget(openFileButton); + auto layout2 = new QGridLayout(); + layout1->addLayout(layout2); + // Initialize Midi CC knobs for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { m_instrument->m_ccModels.at(i).setRange(0, 127, 1); m_instrument->m_ccModels.at(i).setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); - auto ccKnob = new Knob(KnobType::Bright26, tr("CC %1").arg(i), this); + const QString& controlLabel = m_instrument->m_controlsConfig.m_label_cc.at(i).value_or(tr("CC %1").arg(i)); + auto ccKnob = new Knob(KnobType::Bright26, controlLabel, this); ccKnob->setModel(&m_instrument->m_ccModels.at(i)); - layout1->addWidget(ccKnob); + layout2->addWidget(ccKnob, i / 12, i % 12); connect(&m_instrument->m_ccModels.at(i), &AutomatableModel::dataChanged, this, [i, this](){ m_instrument->handleMidiEvent(MidiEvent(MidiControlChange, 0, i, m_instrument->m_ccModels.at(i).value())); }); From d87e7f0fb2de1e722d41cd18b421fd80e1f337a6 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:08:54 -0500 Subject: [PATCH 049/131] Show only used midi CC's on gui --- plugins/SfzSampler/CMakeLists.txt | 2 + plugins/SfzSampler/SfzControlsConfig.h | 21 ++++++++++ plugins/SfzSampler/SfzOpcodeState.cpp | 8 ++-- plugins/SfzSampler/SfzParser.cpp | 18 +++++---- plugins/SfzSampler/SfzParser.h | 3 +- plugins/SfzSampler/SfzSampler.cpp | 7 +++- plugins/SfzSampler/SfzSampler.h | 5 ++- plugins/SfzSampler/SfzSamplerView.cpp | 56 ++++++++++++++++---------- plugins/SfzSampler/SfzSamplerView.h | 6 +++ 9 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 plugins/SfzSampler/SfzControlsConfig.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 4fa3c6cdc30..81f617fa4e8 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -15,6 +15,7 @@ build_plugin(sfzsampler SfzRegionPlayState.h SfzGlobalState.cpp SfzGlobalState.h + SfzControlsConfig.h SfzTrigger.cpp SfzTrigger.h SfzSampleBuffer.cpp @@ -27,6 +28,7 @@ build_plugin(sfzsampler SfzParser.h SfzRegionPlayState.h SfzGlobalState.h + SfzControlsConfig.h SfzTrigger.h SfzSampleBuffer.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.svg" diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzSampler/SfzControlsConfig.h new file mode 100644 index 00000000000..b3ebebdea9e --- /dev/null +++ b/plugins/SfzSampler/SfzControlsConfig.h @@ -0,0 +1,21 @@ + +#ifndef LMMS_SFZ_CONTROLS_CONFIG_H +#define LMMS_SFZ_CONTROLS_CONFIG_H + +#include "SfzOpcodeState.h" + +namespace lmms +{ + +class SfzControlsConfig : public SfzOpcodeState +{ +public: + //! Keeps track of which CC's are actually used by the instrument, so that the GUI only needs to display those, not all 128 + std::array m_activeMidiCCs = {}; +}; + + +} // namespace lmms + + +#endif // LMMS_SFZ_CONTROLS_CONFIG_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 3104b39e464..69f1eaf15a2 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -315,12 +315,14 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu int SfzOpcodeState::ccNumberFromOpcode(const QString& opcode) { - // Match for numbers - QRegularExpression re("\\d+"); + // Match for `ccN` where N is a number + QRegularExpression re("cc\\d+"); QRegularExpressionMatch match = re.match(opcode); if (!match.hasMatch()) { qDebug() << "[SFZ Parser] Unable to find CC number in opcode:" << opcode; return 0; } + // Get the number from that string + QString numberPart = match.captured(0).split("cc")[1]; bool successfulToInt = false; - int ccNumber = match.captured(0).toInt(&successfulToInt); + int ccNumber = numberPart.toInt(&successfulToInt); if (!successfulToInt) { qDebug() << "[SFZ Parser] Unable to convert CC number to int:" << opcode; return 0; } // Normal midi lets you choose between 0-127 for the CC number. According to the SFZ format website, ARIA and SFZ 2 allow for extended CC numbers. Most have not been implemented here, so to keep things safe, we cap it and give a warning. if (ccNumber < 0 || ccNumber > NumMidiCCs) { qDebug() << "[SFZ Parser] Midi CC number" << ccNumber << "is out of range 0-127. This is not yet implmented."; return 0;} diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 4ae7a1300b7..69abd987927 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -10,7 +10,7 @@ namespace lmms { -bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzOpcodeState& controlsConfig) +bool SfzParser::parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzControlsConfig& controlsConfig) { // Clear the vector of regions just in case anything is inside it from before outputRegions.clear(); @@ -184,6 +184,8 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } // If this line/segment isn't a new header, it must be an opcode assignment + auto opcodeNameAndValue = segment.split("="); + if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } // Depending on the current header/zone, opcode assignments are handled differently: switch (currentHeader) { @@ -196,23 +198,17 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou case Header::Group: { // If we are in a group, update the opcodes of the current group state - auto opcodeNameAndValue = segment.split("="); - if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } case Header::Region: { // If we are within a region, update the opcodes of the current region state - auto opcodeNameAndValue = segment.split("="); - if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } currentRegionState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } case Header::Control: { - auto opcodeNameAndValue = segment.split("="); - if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } controlsConfig.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); // Also set the opcode for the global state, that way it will get propagated down to all other regions (this is needed for things like `default_path` to work for all regions) // Technically it could be reworked so that the regions fetch any needed info from the controls state, but for now this way is simpler. @@ -230,6 +226,14 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou return false; } } + // For the GUI, it's nice to keep track of which midi CC's are being used, and only display those (not all 128) + // This is a bit hacky, but just checking if the opcode has "ccN" inside it and parsing that number works fine + QRegularExpression re("cc\\d+"); + QRegularExpressionMatch match = re.match(opcodeNameAndValue[0]); + if (match.hasMatch()) + { + controlsConfig.m_activeMidiCCs.at(match.captured(0).split("cc")[1].toInt()) = true; + } } // Check one last time in case the file ended with a region and didn't get added if (currentHeader == Header::Region) { outputRegions.emplace_back(currentRegionState); } diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index 7173d8b0548..26a58b27d8b 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -3,6 +3,7 @@ #define LMMS_SFZ_PARSER_H #include "SfzRegion.h" +#include "SfzControlsConfig.h" #include #include #include @@ -13,7 +14,7 @@ namespace lmms class SfzParser { public: - static bool parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzOpcodeState& controlsConfig); + static bool parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzControlsConfig& controlsConfig); private: //! Helper function to parse any #include or #define statements from a string, recursively so that the included files have their includes and defines handled too diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index bfe78857f35..1f8342d58f7 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -145,6 +145,11 @@ void SfzSampler::play(SampleFrame* workingBuffer) void SfzSampler::loadFile(const QString& filePath) { + // Reset the note counts, midi cc values, etc + m_sfzGlobalState = SfzGlobalState(); + // And any info about control labels, default values, etc + m_controlsConfig = SfzControlsConfig(); + // Parse all the headers of the .sfz (accounting for and defaults) and populate m_sfzRegions with the new SfzRegion // The header is also parsed into a separate object for easy access by the gui bool successfulParseFile = SfzParser::parseSfzFile(filePath, m_sfzRegions, m_controlsConfig); @@ -162,8 +167,6 @@ void SfzSampler::loadFile(const QString& filePath) qDebug() << "sample was load okay?" << successfulLoadSample; } - // Reset the note counts, midi cc values, etc - m_sfzGlobalState = SfzGlobalState(); // Set the initial cc values based on any `set_ccN` opcodes in the header m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 5cc5958f894..5b9685897a0 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -35,6 +35,7 @@ #include "SfzRegion.h" #include "SfzRegionPlayState.h" #include "SfzGlobalState.h" +#include "SfzControlsConfig.h" namespace lmms { @@ -72,8 +73,8 @@ class SfzSampler : public Instrument //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; - //! Holds information about midi CC default values, control labels, etc - SfzOpcodeState m_controlsConfig; + //! Holds information about midi CC default values, labels, which CC's are actually used, etc + SfzControlsConfig m_controlsConfig; std::vector m_sfzRegions; diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 7dc80b35e15..c2cbfeba380 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -54,12 +54,14 @@ namespace gui { SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) : InstrumentView(instrument, parent) , m_instrument(instrument) + , m_controlsWidget(new QWidget(this)) + , m_knobLayout(new QGridLayout(m_controlsWidget)) { setAcceptDrops(true); setAutoFillBackground(true); setMaximumSize(QSize(10000, 10000)); - setMinimumSize(QSize(500, 500)); + setMinimumSize(QSize(500, 250)); auto layout1 = new QVBoxLayout(this); @@ -67,32 +69,44 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) connect(openFileButton, &PixmapButton::clicked, this, &SfzSamplerView::openFile); layout1->addWidget(openFileButton); - auto layout2 = new QGridLayout(); - layout1->addLayout(layout2); + layout1->addWidget(m_controlsWidget); - // Initialize Midi CC knobs + // Whenever a new SFZ file is loaded, set the default CC values + connect(m_instrument, &SfzSampler::fileLoaded, [this](){ updateKnobs(); }); // this lambda is so bad, but it doesn't work as a slot for some reason + + updateKnobs(); + + update(); +} + + +void SfzSamplerView::updateKnobs() +{ + // Remove any old knobs + delete m_controlsWidget; + m_controlsWidget = new QWidget(this); + m_knobLayout = new QGridLayout(m_controlsWidget); + layout()->addWidget(m_controlsWidget); + + // Initialize new knobs + int activeControlCount = 0; for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { - m_instrument->m_ccModels.at(i).setRange(0, 127, 1); - m_instrument->m_ccModels.at(i).setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); - const QString& controlLabel = m_instrument->m_controlsConfig.m_label_cc.at(i).value_or(tr("CC %1").arg(i)); - auto ccKnob = new Knob(KnobType::Bright26, controlLabel, this); - ccKnob->setModel(&m_instrument->m_ccModels.at(i)); - layout2->addWidget(ccKnob, i / 12, i % 12); - connect(&m_instrument->m_ccModels.at(i), &AutomatableModel::dataChanged, this, [i, this](){ - m_instrument->handleMidiEvent(MidiEvent(MidiControlChange, 0, i, m_instrument->m_ccModels.at(i).value())); - }); - } - - // Whenever a new SFZ file is loaded, set the default CC values - connect(m_instrument, &SfzSampler::fileLoaded, [this](){ - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + // Only add a knob if the control is actually used + if (m_instrument->m_controlsConfig.m_activeMidiCCs.at(i)) { + m_instrument->m_ccModels.at(i).setRange(0, 127, 1); m_instrument->m_ccModels.at(i).setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); + const QString& controlLabel = m_instrument->m_controlsConfig.m_label_cc.at(i).value_or(tr("CC %1").arg(i)); + auto ccKnob = new Knob(KnobType::Bright26, controlLabel, m_controlsWidget); + ccKnob->setModel(&m_instrument->m_ccModels.at(i)); + m_knobLayout->addWidget(ccKnob, activeControlCount / 8, activeControlCount % 8); + connect(&m_instrument->m_ccModels.at(i), &AutomatableModel::dataChanged, this, [i, this](){ + m_instrument->handleMidiEvent(MidiEvent(MidiControlChange, 0, i, m_instrument->m_ccModels.at(i).value())); + }); + activeControlCount++; } - }); - - update(); + } } diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index 01f3584dcee..cf64e70a488 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -27,6 +27,8 @@ #include "InstrumentView.h" +#include + namespace lmms { class SfzSampler; @@ -41,6 +43,7 @@ class SfzSamplerView : public InstrumentView SfzSamplerView(SfzSampler* instrument, QWidget* parent); public slots: + void updateKnobs(); void openFile(); protected: @@ -54,6 +57,9 @@ public slots: bool isResizable() const override { return true; } SfzSampler* m_instrument; + + QWidget* m_controlsWidget; + QGridLayout* m_knobLayout; }; } // namespace gui From 0be50904239fb1e9475651815f52eea473271e1f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:40:24 -0500 Subject: [PATCH 050/131] List switch keys on GUI --- data/themes/classic/style.css | 4 -- data/themes/default/style.css | 5 -- plugins/SfzSampler/SfzControlsConfig.h | 3 ++ plugins/SfzSampler/SfzOpcodeState.cpp | 68 ++++++++++++++++++++++---- plugins/SfzSampler/SfzOpcodeState.h | 4 +- plugins/SfzSampler/SfzParser.cpp | 14 +++++- plugins/SfzSampler/SfzSamplerView.cpp | 27 ++++++++-- plugins/SfzSampler/SfzSamplerView.h | 6 ++- 8 files changed, 106 insertions(+), 25 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 8c80c78cd78..baaaab6ec56 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -1028,10 +1028,6 @@ lmms--gui--SlicerTView lmms--gui--Knob { qproperty-lineWidth: 3; } -lmms--gui--SfzSamplerView QLabel { - font: 10pt; -} - lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/data/themes/default/style.css b/data/themes/default/style.css index fbdaa6c30ff..583ef8ec7ea 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -1088,11 +1088,6 @@ lmms--gui--SlicerTView lmms--gui--Knob { qproperty-lineWidth: 3; } - -lmms--gui--SfzSamplerView QLabel { - font: 10pt; -} - lmms--gui--WatsynView lmms--gui--Knob { qproperty-innerRadius: 1; qproperty-outerRadius: 7; diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzSampler/SfzControlsConfig.h index b3ebebdea9e..33fbdcbbe97 100644 --- a/plugins/SfzSampler/SfzControlsConfig.h +++ b/plugins/SfzSampler/SfzControlsConfig.h @@ -12,6 +12,9 @@ class SfzControlsConfig : public SfzOpcodeState public: //! Keeps track of which CC's are actually used by the instrument, so that the GUI only needs to display those, not all 128 std::array m_activeMidiCCs = {}; + + //! Stores a list of defined switch keys and their labels (if defined), so that the GUI can display a list of them to the user + std::map m_switchKeyLabels; }; diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 69f1eaf15a2..a4082dbb991 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -41,18 +41,18 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "lokey") { m_lokey = value.toInt(&successful); - if (!successful) { m_lokey = keyNumFromString(value, &successful); } + if (!successful) { m_lokey = stringToKeyNum(value, &successful); } } else if (name == "hikey") { m_hikey = value.toInt(&successful); - if (!successful) { m_hikey = keyNumFromString(value, &successful); } + if (!successful) { m_hikey = stringToKeyNum(value, &successful); } } else if (name == "key") { // Setting "key" on its own is equivalent to setting lokey, hikey, and pitch_keycenter all to the same value m_lokey = m_hikey = m_pitch_keycenter = value.toInt(&successful); - if (!successful) { m_lokey = m_hikey = m_pitch_keycenter = keyNumFromString(value, &successful); } + if (!successful) { m_lokey = m_hikey = m_pitch_keycenter = stringToKeyNum(value, &successful); } } @@ -62,22 +62,26 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "sw_lokey") { m_sw_lokey = value.toInt(&successful); - if (!successful) { m_sw_lokey = keyNumFromString(value, &successful); } + if (!successful) { m_sw_lokey = stringToKeyNum(value, &successful); } } else if (name == "sw_hikey") { m_sw_hikey = value.toInt(&successful); - if (!successful) { m_sw_hikey = keyNumFromString(value, &successful); } + if (!successful) { m_sw_hikey = stringToKeyNum(value, &successful); } } else if (name == "sw_last") { m_sw_last = value.toInt(&successful); - if (!successful) { m_sw_last = keyNumFromString(value, &successful); } + if (!successful) { m_sw_last = stringToKeyNum(value, &successful); } } else if (name == "sw_default") { m_sw_default = value.toInt(&successful); - if (!successful) { m_sw_default = keyNumFromString(value, &successful); } + if (!successful) { m_sw_default = stringToKeyNum(value, &successful); } + } + else if (name == "sw_label") + { + m_sw_label = value; } @@ -151,7 +155,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu else if (name == "pitch_keycenter") { m_pitch_keycenter = value.toInt(&successful); - if (!successful) { m_pitch_keycenter = keyNumFromString(value, &successful); } + if (!successful) { m_pitch_keycenter = stringToKeyNum(value, &successful); } } else if (name == "pitch_keytrack") { @@ -330,7 +334,7 @@ int SfzOpcodeState::ccNumberFromOpcode(const QString& opcode) } -int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) +int SfzOpcodeState::stringToKeyNum(QString keyString, bool* successful) { keyString = keyString.toLower(); // The last character is the octave number @@ -364,5 +368,51 @@ int SfzOpcodeState::keyNumFromString(QString keyString, bool* successful) } +QString SfzOpcodeState::keyNumToString(int keyNum) +{ + QString octave = QString::number((keyNum - 24) / 12 + 1); + QString key = ""; + switch (keyNum % 12) + { + case 0: + key = "C"; + break; + case 1: + key = "C#"; + break; + case 2: + key = "D"; + break; + case 3: + key = "D#"; + break; + case 4: + key = "E"; + break; + case 5: + key = "F"; + break; + case 6: + key = "F#"; + break; + case 7: + key = "G"; + break; + case 8: + key = "G#"; + break; + case 9: + key = "A"; + break; + case 10: + key = "A#"; + break; + case 11: + key = "B"; + break; + } + return key + octave; +} + } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 68dfce330f8..1f9e2cc5cc0 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -16,7 +16,8 @@ class SfzOpcodeState SfzOpcodeState(); // We need a constructor to initialize things like m_hicc bool setOpcodeByStrings(const QString& name, const QString& value); - static int keyNumFromString(QString keyString, bool* successful); + static int stringToKeyNum(QString keyString, bool* successful); + static QString keyNumToString(int keyNum); static int ccNumberFromOpcode(const QString& opcode); // Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. @@ -42,6 +43,7 @@ class SfzOpcodeState int m_sw_hikey = 127; std::optional m_sw_last = std::nullopt; // The SFZ opcode spec says this should default to 0, but I don't think that's right? std::optional m_sw_default = std::nullopt; + std::optional m_sw_label = std::nullopt; // // Velocity Trigger diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 69abd987927..138c0ada9c2 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -232,12 +232,24 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou QRegularExpressionMatch match = re.match(opcodeNameAndValue[0]); if (match.hasMatch()) { - controlsConfig.m_activeMidiCCs.at(match.captured(0).split("cc")[1].toInt()) = true; + int ccNumber = match.captured(0).split("cc")[1].toInt(); + if (ccNumber >= 0 && ccNumber <= SfzOpcodeState::NumMidiCCs) { controlsConfig.m_activeMidiCCs.at(ccNumber) = true; } } } // Check one last time in case the file ended with a region and didn't get added if (currentHeader == Header::Region) { outputRegions.emplace_back(currentRegionState); } + + // Just so that the GUI doesn't have to loop over all the regions to find the switch keys, let's also + // make a list of them in the controlsConfig object for easy access + for (const auto& region : outputRegions) + { + if (region.m_sw_last != std::nullopt) + { + controlsConfig.m_switchKeyLabels.insert({region.m_sw_last.value(), region.m_sw_label.value_or("")}); + } + } + // Now that all the opcodes have been parsed into regions and added to the output vector, we are done! // The samples themselves still need to be loaded, but that's a job for later return true; diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index c2cbfeba380..8e143b86f8c 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include "Clipboard.h" @@ -54,6 +56,7 @@ namespace gui { SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) : InstrumentView(instrument, parent) , m_instrument(instrument) + , m_switchKeysLabel(new QLabel(this)) , m_controlsWidget(new QWidget(this)) , m_knobLayout(new QGridLayout(m_controlsWidget)) { @@ -69,18 +72,21 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) connect(openFileButton, &PixmapButton::clicked, this, &SfzSamplerView::openFile); layout1->addWidget(openFileButton); + layout1->addWidget(m_switchKeysLabel); + layout1->addWidget(m_controlsWidget); + // Whenever a new SFZ file is loaded, set the default CC values - connect(m_instrument, &SfzSampler::fileLoaded, [this](){ updateKnobs(); }); // this lambda is so bad, but it doesn't work as a slot for some reason + connect(m_instrument, &SfzSampler::fileLoaded, [this](){ onFileLoaded(); }); // this lambda is so bad, but it doesn't work as a slot for some reason - updateKnobs(); + onFileLoaded(); update(); } -void SfzSamplerView::updateKnobs() +void SfzSamplerView::onFileLoaded() { // Remove any old knobs delete m_controlsWidget; @@ -107,6 +113,21 @@ void SfzSamplerView::updateKnobs() activeControlCount++; } } + + // Update the switch key list + QStringList switchKeyLabels; + for (const auto& [key, label] : m_instrument->m_controlsConfig.m_switchKeyLabels) + { + switchKeyLabels.push_back(QString("- %1 %2").arg(SfzOpcodeState::keyNumToString(key)).arg(label)); + } + if (switchKeyLabels.size() > 0) + { + m_switchKeysLabel->setText(tr("Switch Keys:\n") + switchKeyLabels.join("\n")); + } + else + { + m_switchKeysLabel->setText(""); + } } diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index cf64e70a488..53de61238df 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -27,7 +27,8 @@ #include "InstrumentView.h" -#include +class QGridLayout; +class QLabel; namespace lmms { @@ -43,7 +44,7 @@ class SfzSamplerView : public InstrumentView SfzSamplerView(SfzSampler* instrument, QWidget* parent); public slots: - void updateKnobs(); + void onFileLoaded(); void openFile(); protected: @@ -58,6 +59,7 @@ public slots: SfzSampler* m_instrument; + QLabel* m_switchKeysLabel; QWidget* m_controlsWidget; QGridLayout* m_knobLayout; }; From 18af475931a49a444f3d0d756ef9a74078decd38 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 17 Jan 2026 18:53:26 -0500 Subject: [PATCH 051/131] Improve debug --- plugins/SfzSampler/SfzSampler.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 1f8342d58f7..7162437239e 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -145,6 +145,8 @@ void SfzSampler::play(SampleFrame* workingBuffer) void SfzSampler::loadFile(const QString& filePath) { + // Prevent the audio thread from accidentally looping through the region vector while it's being edited + const auto guard = Engine::audioEngine()->requestChangesGuard(); // Reset the note counts, midi cc values, etc m_sfzGlobalState = SfzGlobalState(); // And any info about control labels, default values, etc @@ -154,17 +156,19 @@ void SfzSampler::loadFile(const QString& filePath) // The header is also parsed into a separate object for easy access by the gui bool successfulParseFile = SfzParser::parseSfzFile(filePath, m_sfzRegions, m_controlsConfig); - qDebug() << "was okay?" << successfulParseFile; - qDebug() << "num regions" << m_sfzRegions.size(); + if (!successfulParseFile) { qDebug() << "[SFZ Player] An error occurred when parsing the SFZ file."; return; } // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + int i = 0; for (auto& region : m_sfzRegions) { + qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_sfzRegions.size() << region.m_sampleFile.value_or("N/A"); bool successfulLoadSample = region.initializeSample(parentDirectory); - qDebug() << "sample was load okay?" << successfulLoadSample; + if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } + i++; } // Set the initial cc values based on any `set_ccN` opcodes in the header From 36bd00e983d069eeaeb7984318caf30fff318926 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:38:47 -0500 Subject: [PATCH 052/131] Initial sample pool to prevent duplicate sample loading --- plugins/SfzSampler/CMakeLists.txt | 3 +++ plugins/SfzSampler/SfzRegion.cpp | 15 +++++------ plugins/SfzSampler/SfzRegion.h | 12 ++++----- plugins/SfzSampler/SfzRegionPlayState.cpp | 16 +++++++----- plugins/SfzSampler/SfzSamplePool.cpp | 32 +++++++++++++++++++++++ plugins/SfzSampler/SfzSamplePool.h | 26 ++++++++++++++++++ plugins/SfzSampler/SfzSampler.cpp | 5 +++- plugins/SfzSampler/SfzSampler.h | 4 +++ 8 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 plugins/SfzSampler/SfzSamplePool.cpp create mode 100644 plugins/SfzSampler/SfzSamplePool.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 81f617fa4e8..3c7eb026c89 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -20,6 +20,8 @@ build_plugin(sfzsampler SfzTrigger.h SfzSampleBuffer.cpp SfzSampleBuffer.h + SfzSamplePool.cpp + SfzSamplePool.h MOCFILES SfzSampler.h SfzSamplerView.h @@ -31,5 +33,6 @@ build_plugin(sfzsampler SfzControlsConfig.h SfzTrigger.h SfzSampleBuffer.h + SfzSamplePool.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.svg" ) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 67696748c60..6bc84ab7024 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -8,7 +8,6 @@ #include "Engine.h" #include "AudioEngine.h" -#include "SampleBuffer.h" #include @@ -152,7 +151,7 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) -bool SfzRegion::initializeSample(const QDir& parentDirectory) +bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool) { if (m_sampleFile == std::nullopt) { @@ -161,13 +160,11 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory) return false; } - // TODO is simply adding the default path sufficient? - if (auto buffer = SampleBuffer::fromFile(parentDirectory.absoluteFilePath(m_default_path.value_or("") + m_sampleFile.value()))) - { - m_sample = SfzSampleBuffer(buffer->data(), buffer->size(), buffer->sampleRate()); - return true; - } - return false; + QString path = parentDirectory.absoluteFilePath(m_default_path.value_or("") + m_sampleFile.value()); + // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory + m_sample = samplePool.loadSample(path); + + return m_sample != nullptr; } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index c89f154d136..90f86ed7498 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -7,8 +7,7 @@ #include "SfzTrigger.h" #include "SfzRegionPlayState.h" #include "SfzSampleBuffer.h" - -#include "Sample.h" +#include "SfzSamplePool.h" #include @@ -37,17 +36,18 @@ class SfzRegion : public SfzOpcodeState //! Load the sample file given by the `sample` opcode into m_sample. //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed //! Returns true if successful - bool initializeSample(const QDir& parentDirectory); + bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool); - const SfzSampleBuffer& sample() const { return m_sample; } + const SfzSampleBuffer* sample() const { return m_sample; } private: static constexpr int MAX_ACTIVE_SOUNDS = 128; //! Array to store all active (and inactive) sound play states for this region std::array m_activeSounds; - //! Sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first - SfzSampleBuffer m_sample; + //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first + //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. + const SfzSampleBuffer* m_sample = nullptr; //! Maximum active index in the play state array //! By always spawning new sounds at the lowest open index and keeping track of the maximum index which contains an actice sound, diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index f2faae337d1..9eb710e878c 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -4,6 +4,8 @@ #include "SfzRegion.h" +#include "AudioEngine.h" +#include "Engine.h" #include "lmms_math.h" #include "MicroTimer.h" @@ -128,7 +130,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) float freqRatio = std::exp2(pitch / 12.0f); // Sample rate of sample - const float sampleSampleRate = m_region->sample().sampleRate(); + const float sampleSampleRate = m_region->sample()->sampleRate(); // Sample rate of LMMS const float sampleRate = Engine::audioEngine()->outputSampleRate(); // Play the sample faster/slower to match the correct sample rate @@ -187,15 +189,15 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ); if (filterEnabled) // TODO does this if statement make it faster? { - buffer[f][0] += m_filter.update(m_region->sample().at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); - buffer[f][1] += m_filter.update(m_region->sample().at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); + buffer[f][0] += m_filter.update(m_region->sample()->at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); + buffer[f][1] += m_filter.update(m_region->sample()->at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); } else { - buffer[f][0] += m_region->sample().at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp; - buffer[f][1] += m_region->sample().at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp; + buffer[f][0] += m_region->sample()->at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp; + buffer[f][1] += m_region->sample()->at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp; } - m_sampleFrame = std::min(static_cast(m_region->sample().size()), m_sampleFrame + freqRatio); + m_sampleFrame = std::min(static_cast(m_region->sample()->size()), m_sampleFrame + freqRatio); m_frameCount++; } @@ -207,7 +209,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) } // If loop_mode is one_shot, no noteOff signal will ever come to release it, so we need to manually deactivate when we reach the end of the sample - if (m_region->m_loop_mode == SfzOpcodeState::LoopMode::OneShot && m_sampleFrame >= m_region->sample().size()) + if (m_region->m_loop_mode == SfzOpcodeState::LoopMode::OneShot && m_sampleFrame >= m_region->sample()->size()) { m_active = false; // TODO should this forcefully decative or just release? } diff --git a/plugins/SfzSampler/SfzSamplePool.cpp b/plugins/SfzSampler/SfzSamplePool.cpp new file mode 100644 index 00000000000..31aa1424305 --- /dev/null +++ b/plugins/SfzSampler/SfzSamplePool.cpp @@ -0,0 +1,32 @@ + + + + +#include "SfzSamplePool.h" +#include "SampleBuffer.h" + +namespace lmms +{ + + +SfzSampleBuffer* const SfzSamplePool::loadSample(const QString& path) +{ + // If the sample has already been loaded before, just return a pointer to it + if (m_samplePool.contains(path)) + { + return m_samplePool.at(path).get(); + } + else if (auto buffer = SampleBuffer::fromFile(path)) + { + m_samplePool.insert({path, std::make_shared(buffer->data(), buffer->size(), buffer->sampleRate())}); + return m_samplePool.at(path).get(); + } + else + { + return nullptr; + } +} + + +} // namespace lmms + diff --git a/plugins/SfzSampler/SfzSamplePool.h b/plugins/SfzSampler/SfzSamplePool.h new file mode 100644 index 00000000000..a57f5159a34 --- /dev/null +++ b/plugins/SfzSampler/SfzSamplePool.h @@ -0,0 +1,26 @@ + +#ifndef LMMS_SFZ_SAMPLE_POOL_H +#define LMMS_SFZ_SAMPLE_POOL_H + +#include "SfzSampleBuffer.h" + +#include +#include + +namespace lmms +{ + +class SfzSamplePool +{ +public: + SfzSampleBuffer* const loadSample(const QString& path); + + const int sampleCount() const { return m_samplePool.size(); } + +private: + std::map> m_samplePool; +}; + +} // namespace lmms + +#endif // LMMS_SFZ_SAMPLE_POOL_H diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 7162437239e..bef99f74bd1 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -151,6 +151,8 @@ void SfzSampler::loadFile(const QString& filePath) m_sfzGlobalState = SfzGlobalState(); // And any info about control labels, default values, etc m_controlsConfig = SfzControlsConfig(); + // Reset any loaded samples + m_samplePool = SfzSamplePool(); // Parse all the headers of the .sfz (accounting for and defaults) and populate m_sfzRegions with the new SfzRegion // The header is also parsed into a separate object for easy access by the gui @@ -166,10 +168,11 @@ void SfzSampler::loadFile(const QString& filePath) for (auto& region : m_sfzRegions) { qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_sfzRegions.size() << region.m_sampleFile.value_or("N/A"); - bool successfulLoadSample = region.initializeSample(parentDirectory); + bool successfulLoadSample = region.initializeSample(parentDirectory, m_samplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } i++; } + qDebug() << "Loaded" << m_sfzRegions.size() << "regions and" << m_samplePool.sampleCount() << "samples."; // Set the initial cc values based on any `set_ccN` opcodes in the header m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 5b9685897a0..91fb788af0a 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -36,6 +36,7 @@ #include "SfzRegionPlayState.h" #include "SfzGlobalState.h" #include "SfzControlsConfig.h" +#include "SfzSamplePool.h" namespace lmms { @@ -78,6 +79,9 @@ class SfzSampler : public Instrument std::vector m_sfzRegions; + //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded + SfzSamplePool m_samplePool; + // The GUI needs models to connect to midi CC knobs, so a bunch of dummy models are defined here. // Ideally it would be better to reuse the midi CC models for the instrument track std::array m_ccModels; From 16bdfca2ce6c7fd11ccb16c810d477dd27f8d360 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:27:32 -0500 Subject: [PATCH 053/131] Switch key improvements --- plugins/SfzSampler/SfzControlsConfig.h | 11 +++++++++-- plugins/SfzSampler/SfzGlobalState.cpp | 8 +++++++- plugins/SfzSampler/SfzGlobalState.h | 3 ++- plugins/SfzSampler/SfzParser.cpp | 7 ++++++- plugins/SfzSampler/SfzSamplerView.cpp | 11 +++++++++-- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzSampler/SfzControlsConfig.h index 33fbdcbbe97..3690f1433e4 100644 --- a/plugins/SfzSampler/SfzControlsConfig.h +++ b/plugins/SfzSampler/SfzControlsConfig.h @@ -13,8 +13,15 @@ class SfzControlsConfig : public SfzOpcodeState //! Keeps track of which CC's are actually used by the instrument, so that the GUI only needs to display those, not all 128 std::array m_activeMidiCCs = {}; - //! Stores a list of defined switch keys and their labels (if defined), so that the GUI can display a list of them to the user - std::map m_switchKeyLabels; + //! Stores information about each switch key, to make it easy to display on the GUI + struct SwitchKeyInfo + { + QString sw_label; + std::optional sw_default; + int sw_lokey; + int sw_hikey; + }; + std::map m_switchKeyInfo; }; diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 68e3e100176..ec6a49aa63e 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -23,8 +23,14 @@ void SfzGlobalState::processTrigger(const SfzTrigger& trigger) } } -std::optional SfzGlobalState::lastKeyPressedInRange(const int lowKey, const int highKey, const std::optional defaultKey) const +std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey) const { + // Some SFZs pass -1 as the lokey, so make sure to clamp it into the range 0-127 before accessing the array to prevent out of range errors + lowKey = std::max(lowKey, 0); + highKey = std::min(highKey, 127); + // If the range is invalid, return the default key. + if (lowKey > highKey) { return defaultKey; } + std::optional lastPlayedKey = std::nullopt; std::optional bestScore = std::nullopt; diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index 4f54159d33e..053960e7741 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -20,9 +20,10 @@ class SfzGlobalState // Returns the midi key number of the last pressed key in the range [lowKey, highKey]. // If no keys have been pressed yet, it returns defaultKey. + // If the key range is invalid, it returns defaultKey // If no default key was given, it returns std::nullopt // TODO add unit tests - std::optional lastKeyPressedInRange(const int lowKey, const int highKey, const std::optional defaultKey) const; + std::optional lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey) const; //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 138c0ada9c2..22ab6f5197c 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -246,7 +246,12 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou { if (region.m_sw_last != std::nullopt) { - controlsConfig.m_switchKeyLabels.insert({region.m_sw_last.value(), region.m_sw_label.value_or("")}); + SfzControlsConfig::SwitchKeyInfo info; + info.sw_label = region.m_sw_label.value_or(""); + info.sw_lokey = region.m_sw_lokey; + info.sw_hikey = region.m_sw_hikey; + info.sw_default = region.m_sw_default; + controlsConfig.m_switchKeyInfo.insert({region.m_sw_last.value(), info}); } } diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 8e143b86f8c..e415aa6da21 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -116,9 +116,16 @@ void SfzSamplerView::onFileLoaded() // Update the switch key list QStringList switchKeyLabels; - for (const auto& [key, label] : m_instrument->m_controlsConfig.m_switchKeyLabels) + for (const auto& [key, info] : m_instrument->m_controlsConfig.m_switchKeyInfo) { - switchKeyLabels.push_back(QString("- %1 %2").arg(SfzOpcodeState::keyNumToString(key)).arg(label)); + switchKeyLabels.push_back( + QString("- %1 %2 [range: %3 - %4] default: %5") + .arg(SfzOpcodeState::keyNumToString(key)) + .arg(info.sw_label) + .arg(SfzOpcodeState::keyNumToString(info.sw_lokey)) + .arg(SfzOpcodeState::keyNumToString(info.sw_hikey)) + .arg(SfzOpcodeState::keyNumToString(info.sw_default.value())) + ); } if (switchKeyLabels.size() > 0) { From ec9bb7b47f25f06124d4c9c0e9dcbe39def9a53f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:39:57 -0500 Subject: [PATCH 054/131] Fix sw_last determination and remove probably incorrect parsing --- plugins/SfzSampler/SfzGlobalState.cpp | 17 ++++++-- plugins/SfzSampler/SfzGlobalState.h | 8 +++- plugins/SfzSampler/SfzParser.cpp | 51 +++++------------------ plugins/SfzSampler/SfzRegion.cpp | 14 +++++-- plugins/SfzSampler/SfzRegion.h | 2 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 4 +- plugins/SfzSampler/SfzSamplerView.cpp | 18 ++++---- 7 files changed, 54 insertions(+), 60 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index ec6a49aa63e..90e0136070d 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -1,6 +1,8 @@ #include "SfzGlobalState.h" +#include +#include namespace lmms { @@ -23,8 +25,15 @@ void SfzGlobalState::processTrigger(const SfzTrigger& trigger) } } -std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey) const +void SfzGlobalState::switchKeyPressed(const int key) { + // Assuming the m_keyPressCounter was already updated in processTrigger + m_lastPlayedSwitchKeys.at(key) = m_keyPressCounter; +} + +std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey, bool switchKeysOnly) const +{ + auto& lastKeyPressArray = switchKeysOnly ? m_lastPlayedSwitchKeys : m_lastPlayedKeys; // Some SFZs pass -1 as the lokey, so make sure to clamp it into the range 0-127 before accessing the array to prevent out of range errors lowKey = std::max(lowKey, 0); highKey = std::min(highKey, 127); @@ -36,12 +45,12 @@ std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey for (int key = lowKey; key <= highKey; ++key) { - if (m_lastPlayedKeys.at(key) == std::nullopt) { continue; } // This key has not been played yet + if (lastKeyPressArray.at(key) == std::nullopt) { continue; } // This key has not been played yet - if (bestScore == std::nullopt || m_lastPlayedKeys.at(key) > bestScore) + if (bestScore == std::nullopt || lastKeyPressArray.at(key) > bestScore) { lastPlayedKey = key; - bestScore = m_lastPlayedKeys.at(key); + bestScore = lastKeyPressArray.at(key); } } // If no keys in the region have been played yet, return the default key diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index 053960e7741..da9a4fc71b3 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -18,12 +18,16 @@ class SfzGlobalState //! Handles updating the last played keys and keeps track of which keys are currently being pressed void processTrigger(const SfzTrigger& trigger); + //! Handles updating m_lastPlayedSwitchKeys whenever a key defined by sw_last is pressed + void switchKeyPressed(const int key); + // Returns the midi key number of the last pressed key in the range [lowKey, highKey]. + // If switchKeysOnly is true, it uses the m_lastPlayedSwitchKeys array which only tracks when valid switch keys have been pressed, not just any key. // If no keys have been pressed yet, it returns defaultKey. // If the key range is invalid, it returns defaultKey // If no default key was given, it returns std::nullopt // TODO add unit tests - std::optional lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey) const; + std::optional lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey, bool switchKeysOnly = false) const; //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } @@ -36,6 +40,8 @@ class SfzGlobalState //! The first key played gets a 1, the second key gets a 2, etc, overwriting when keys are played multiple times. //! By finding the maximum number in a range, you can find the last played key std::array, 128> m_lastPlayedKeys = {}; + //! Same as above but only for switch keys (ones defined by sw_last) + std::array, 128> m_lastPlayedSwitchKeys = {}; //! Consequently, we also have to track how many keys have been played so far unsigned long m_keyPressCounter = 0; diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 22ab6f5197c..66be352846d 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -31,7 +31,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // This amounts to recursively loading and copy/pasting the contents of the other files where the #include is, and find/replacing the #define words with their values fileContents = recursiveHandleIncludeAndDefineStatements(parentDirectory, fileContents); - qDebug().noquote() << "TESTING: Loaded SFZ:\n" << fileContents; // testing + qDebug().noquote() << "[SFZ Parser] DEBUG: Preprocessed SFZ file contents:\n" << fileContents; // testing // Now that all the includes/defines are handled, loop through the whole contents and split it up into segments so that it can be parsed // For example, if you have a sfz file like: @@ -66,7 +66,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // We can start by splitting on whitespace, but then we will have to go back and group together any chunks which belong to the same opcode assignment, if for example the sample file included spaces // For example, if we had the line: // sample=My Favorite Sample.flac key=99 - // Then it would initially by split into "", "sample=My", "Favorite", "Sample.flac", and "key=99" + // Then it would initially be split into "", "sample=My", "Favorite", "Sample.flac", and "key=99" // However, we notice that "Favorite" and "Sample.flac" are not valid opcode assignments since they don't have an "=", and they're not headers, since you don't have those // So they must belong to the previous opcode assignment, "sample=My" // If we connect them together, we get "", "sample=My Favorite Sample.flac", and "key=99", just as we wanted! @@ -98,42 +98,9 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // Now that all the segments are collected, we can go through them all and construct the SfzRegions - // First, the header(s) must be found. The SFZ format website does not guarantee that will - // be the first header, so we have to search through all the segments to find the globals before parsing any headers - // Create a base opcode state list to keep track of which defaults are global + // Create a base opcode state list to keep track of which defaults are SfzOpcodeState globalState; - - bool withinGlobal = false; - for (QString segment : parsedSegments) - { - // Track whether we are entering a header region - if (segment.front() == "<" && segment.back() == ">") - { - if (segment == "") - { - withinGlobal = true; - } - else - { - withinGlobal = false; - } - continue; - } - - // If we are in , update the global opcode state - if (withinGlobal) - { - // Opcodes are stored in name=value format, with no spaces, so splitting on the "=" always works - auto opcodeNameAndValue = segment.split("="); - if (opcodeNameAndValue.size() != 2) { qDebug() << "[SFZ Parser] Syntax error, could not parse opcode assignment:" << segment; return false; } - globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); - } - } - - // Now that all the global opcode defaults have been parsed, we can start stepping through the file again and - // parsing all the s into SfzRegion objects - // Regions can be within s, which can define default opcodes for all the regions inside them // Keep track of the current group and region states. These will be re-initialized to whatever the global/group settings are, once we encounter a new group or region header SfzOpcodeState currentGroupState = globalState; @@ -152,7 +119,9 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou if (segment == "") { currentHeader = Header::Global; - // Don't do anything special; we already handled the globals above + // If we are entering a new global header, reset the current global settings + // TODO is this correct? + globalState = SfzOpcodeState(); } else if (segment == "") { @@ -191,13 +160,13 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou { case Header::Global: { - // Do nothing. The global headers were already handled above - // TODO this is wrong, some sfz's treat global headers as local, expecting that one after another will overwrite the last + // If we are in the global header, update the opcodes of the global state, which will propagate down to any groups or regions after it + globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } case Header::Group: { - // If we are in a group, update the opcodes of the current group state + // If we are in a group, update the opcodes of the current group state, which will propagate to any regions below it currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } @@ -226,7 +195,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou return false; } } - // For the GUI, it's nice to keep track of which midi CC's are being used, and only display those (not all 128) + // For the GUI, it's nice to keep track of which midi CC's are being used, and only display those (instead of all 128) // This is a bit hacky, but just checking if the opcode has "ccN" inside it and parsing that number works fine QRegularExpression re("cc\\d+"); QRegularExpressionMatch match = re.match(opcodeNameAndValue[0]); diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 6bc84ab7024..f7599a091ba 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -34,9 +34,10 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // And had velocity between `lovel` and `hivel` opcodes if (triggerVelocity > m_hivel || triggerVelocity < m_lovel) { return false; } - // If a keyswitch range was defined, ensure the last pressed key in that range matches the specified keyswitch for this region + // If a keyswitch range was defined, ensure the last pressed valid switch key in that range matches the specified keyswitch for this region + // The argument `true` at the end signifies that only switch keys will be considered // TODO add unit tests - if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default) != m_sw_last) { return false; } + if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default, true) != m_sw_last) { return false; } // If midi CC ranges are defined, make sure the current CC values are within range for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) @@ -55,8 +56,14 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf return false; } -void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigger& trigger) +void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger) { + // Notify the global state whether a switch key has been pressed, so that it can correctly track when `sw_last` is met + if (trigger.type() == SfzTrigger::Type::NoteOn && m_sw_last != std::nullopt && m_sw_last == trigger.key().value()) + { + globalState.switchKeyPressed(trigger.key().value()); + } + // Before spawning an sounds, real quick do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer if (trigger.type() == SfzTrigger::Type::ControlChange) { @@ -66,6 +73,7 @@ void SfzRegion::processTrigger(const SfzGlobalState& globalState, const SfzTrigg // If the trigger conditions are met, spawn a new sound if (triggerConditionsMet(globalState, trigger)) { + qDebug() << "Spawning sound!" << m_sampleFile.value_or("N/A"); // Loop through array to find open position bool foundOpenPosition = false; for (size_t i = 0; i <= m_activeSounds.size(); ++i) diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 90f86ed7498..986db9f94cc 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -27,7 +27,7 @@ class SfzRegion : public SfzOpcodeState //! Handles spawning a new sound if the all the conditions are met //! Also handles sending the trigger to the currently active sounds, in case they need to deactivate/release (such as on NoteOff) - void processTrigger(const SfzGlobalState& globalState, const SfzTrigger& trigger); + void processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger); //! Renders sound from each of the active SfzRegionPlayStates and writes it to the given buffer //! Returns true if any sound was actually generated diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 9eb710e878c..e280199c438 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -168,8 +168,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // For performance, only update the cutoff frequency/resonance once per buffer if (filterEnabled) { - // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them - const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack / 100.0f; + // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I removed the /100.0f because I'm not sure. + const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack; const float filterCutoff = m_region->m_cutoff.value() + std::exp2(filterCutoffPitchOffset / 12.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index e415aa6da21..621b850121b 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -118,14 +118,16 @@ void SfzSamplerView::onFileLoaded() QStringList switchKeyLabels; for (const auto& [key, info] : m_instrument->m_controlsConfig.m_switchKeyInfo) { - switchKeyLabels.push_back( - QString("- %1 %2 [range: %3 - %4] default: %5") - .arg(SfzOpcodeState::keyNumToString(key)) - .arg(info.sw_label) - .arg(SfzOpcodeState::keyNumToString(info.sw_lokey)) - .arg(SfzOpcodeState::keyNumToString(info.sw_hikey)) - .arg(SfzOpcodeState::keyNumToString(info.sw_default.value())) - ); + QString label = QString("- %1 %2 [range: %3 - %4]") + .arg(SfzOpcodeState::keyNumToString(key)) + .arg(info.sw_label) + .arg(SfzOpcodeState::keyNumToString(info.sw_lokey)) + .arg(SfzOpcodeState::keyNumToString(info.sw_hikey)); + if (info.sw_default != std::nullopt) + { + label += QString(" default: %1").arg(SfzOpcodeState::keyNumToString(info.sw_default.value())); + } + switchKeyLabels.push_back(label); } if (switchKeyLabels.size() > 0) { From c0bcc0aa0dc2a63be778986e0325c5e4c6320776 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:07:41 -0500 Subject: [PATCH 055/131] Add partial support for trigger opcode --- plugins/SfzSampler/SfzGlobalState.cpp | 2 -- plugins/SfzSampler/SfzOpcodeState.cpp | 18 ++++++++++++++ plugins/SfzSampler/SfzOpcodeState.h | 13 ++++++++++ plugins/SfzSampler/SfzRegion.cpp | 34 ++++++++++++++++----------- plugins/SfzSampler/SfzSampler.cpp | 7 +++++- plugins/SfzSampler/SfzSampler.h | 4 ++++ 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 90e0136070d..23433ec836b 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -1,8 +1,6 @@ #include "SfzGlobalState.h" -#include -#include namespace lmms { diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index a4082dbb991..13af0cd9287 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -35,6 +35,24 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } + // + // Trigger Type + // + else if (name == "trigger") + { + if (value == "attack") { m_trigger = TriggerType::Attack; } + else if (value == "release") { m_trigger = TriggerType::Release; } + //else if (value == "first") { m_trigger = TriggerType::First; } // TODO To be implemented + //else if (value == "legato") { m_trigger = TriggerType::Legato; } // TODO To be implemented + //else if (value == "release_key") { m_trigger = TriggerType::ReleaseKey; } // TODO To be implemented + else + { + qDebug() << "[SFZ Parser] Unknown trigger:" << value; + return false; + } + } + + // // Key Trigger // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 1f9e2cc5cc0..457ff9bd1dd 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -30,6 +30,19 @@ class SfzOpcodeState std::optional m_default_path = std::nullopt; + // + // Trigger Type + // + enum class TriggerType + { + Attack, + Release, + //First, // TODO To be implemented + //Legato, // TODO To be implemented + //ReleaseKey // TODO To be implemented + }; + TriggerType m_trigger = TriggerType::Attack; + // // Key Trigger // diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index f7599a091ba..1b57dd24e35 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -23,7 +23,14 @@ SfzRegion::SfzRegion(SfzOpcodeState opcodeState) bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) { - if (trigger.type() == SfzTrigger::Type::NoteOn) + if (trigger.type() == SfzTrigger::Type::ControlChange) { return false; } // TODO. It is possible for midi CC's to trigger regions, such as using the sustain pedal or on_locc/on_hicc + + // Make sure the trigger type matches + if (trigger.type() == SfzTrigger::Type::NoteOn && m_trigger != TriggerType::Attack) { return false; } + if (trigger.type() == SfzTrigger::Type::NoteOff && m_trigger != TriggerType::Release) { return false; } + + // Assuming the trigger has key/vel info (i.e., it's a noteOn/noteOff, not a midi CC event), make sure all the key/vel selectors match + if (trigger.type() == SfzTrigger::Type::NoteOn || trigger.type() == SfzTrigger::Type::NoteOff) { int triggerKey = trigger.key().value(); int triggerVelocity = trigger.velocity().value(); @@ -38,22 +45,21 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // The argument `true` at the end signifies that only switch keys will be considered // TODO add unit tests if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default, true) != m_sw_last) { return false; } + } - // If midi CC ranges are defined, make sure the current CC values are within range - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) - { - const int ccValue = globalState.midiCCValue(i); - if (ccValue > m_hicc.at(i) || ccValue < m_locc.at(i)) { return false; } - } + // If midi CC ranges are defined, make sure the current CC values are within range + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + const int ccValue = globalState.midiCCValue(i); + if (ccValue > m_hicc.at(i) || ccValue < m_locc.at(i)) { return false; } + } - // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. - m_roundRobinCount++; - if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn + // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. + m_roundRobinCount++; + if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn - // If all the contitions passed, return true and spawn a sound - return true; - } - return false; + // If all the contitions passed, return true and spawn a sound + return true; } void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index bef99f74bd1..c8fba476e3e 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -83,11 +83,16 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ if (event.type() == MidiNoteOn) { processTrigger(SfzTrigger::noteOnEvent(offset, event.key(), event.velocity())); + // Store the velocity of this event so that we can apply it to the corresponding NoteOff event (see comment below) + m_previousNoteOnVelocity.at(event.key()) = event.velocity(); return true; } else if (event.type() == MidiNoteOff) { - processTrigger(SfzTrigger::noteOffEvent(offset, event.key(), event.velocity())); + // TODO: Currently, LMMS by default has NoteOff events have 0 velocity. SFZ requires that the NoteOff velocity match its corresponding NoteOn velocity + // To account for this, we track the last pressed velocity of each note in an array and use that + const int previousVelocity = m_previousNoteOnVelocity.at(event.key()); + processTrigger(SfzTrigger::noteOffEvent(offset, event.key(), previousVelocity)); return true; } else if (event.type() == MidiControlChange) diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 91fb788af0a..bd8ea5d996c 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -86,6 +86,10 @@ class SfzSampler : public Instrument // Ideally it would be better to reuse the midi CC models for the instrument track std::array m_ccModels; + //! Unfortunately, LMMS by default has the velocity of NoteOff events be 0. However, SFZ expects them to match the velocity of the corresponding NoteOn event. + //! To account for this, we store the velocity of the last NoteOn event for each key, and use that value when hanlding NoteOff events. + std::array m_previousNoteOnVelocity = {}; + friend class gui::SfzSamplerView; }; } // namespace lmms From 798883345d702a7ba0e5e8e33a98feb1d4a2841e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:42:11 -0500 Subject: [PATCH 056/131] Add gain_oncc/gain_cc opcode --- plugins/SfzSampler/SfzOpcodeState.cpp | 4 ++++ plugins/SfzSampler/SfzOpcodeState.h | 2 ++ plugins/SfzSampler/SfzRegion.cpp | 2 ++ plugins/SfzSampler/SfzRegion.h | 2 ++ plugins/SfzSampler/SfzRegionPlayState.cpp | 2 +- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 13af0cd9287..6d09d1bb2a2 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -283,6 +283,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_volume = value.toFloat(&successful); } + else if (name.startsWith("gain_oncc") || name.startsWith("gain_cc")) + { + m_gain_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); + } else if (name == "pan") { m_pan = value.toFloat(&successful); diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 457ff9bd1dd..91b70a051f7 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -157,6 +157,8 @@ class SfzOpcodeState // Misc Volume // float m_volume = 0.0f; // In decibals + // Gain is the same as volume. Some opcodes use the word volume, some use gain, both are decibals + std::array m_gain_oncc = {}; float m_pan = 0.0f; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 1b57dd24e35..9306465becf 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -160,6 +160,8 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) m_ampeg_decay_totalCC = totalCCModulation(m_ampeg_decay_oncc, globalState); m_ampeg_sustain_totalCC = totalCCModulation(m_ampeg_sustain_oncc, globalState); m_ampeg_release_totalCC = totalCCModulation(m_ampeg_release_oncc, globalState); + + m_gain_totalCC = totalCCModulation(m_gain_oncc, globalState); // TODO more } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 986db9f94cc..00e59761128 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -71,6 +71,8 @@ class SfzRegion : public SfzOpcodeState float m_ampeg_sustain_totalCC = 0.0f; float m_ampeg_release_totalCC = 0.0f; + float m_gain_totalCC = 0.0f; + //! Helper function to calculate the total modulation of all midi CC controllers on a parameter. Essentially it just multiplies the modulation amounts by the current CC values and adds it all up. float totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const; void recalculateTotalCCModulation(const SfzGlobalState& globalState); diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index e280199c438..a50644a130a 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -156,7 +156,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack / -100); // Amplitude due to volume/gain - const float ampVolume = dbfsToAmp(m_region->m_volume); + const float ampVolume = dbfsToAmp(m_region->m_volume + m_region->m_gain_totalCC); // Panning const float pan = m_region->m_pan / 100; From 7ea0a546dab47c399362882d6476c446e6cf1092 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:05:28 -0500 Subject: [PATCH 057/131] Add volume_oncc opcode --- plugins/SfzSampler/SfzOpcodeState.cpp | 2 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 6d09d1bb2a2..2086e1f9468 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -283,7 +283,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_volume = value.toFloat(&successful); } - else if (name.startsWith("gain_oncc") || name.startsWith("gain_cc")) + else if (name.startsWith("gain_oncc") || name.startsWith("gain_cc") || name.startsWith("volume_oncc")) { m_gain_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index a50644a130a..adbf4d193e4 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -168,7 +168,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // For performance, only update the cutoff frequency/resonance once per buffer if (filterEnabled) { - // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I removed the /100.0f because I'm not sure. + // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I saw 20000 being used in Metal GTX which is kind of high for cents (200 octaves?) const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack; const float filterCutoff = m_region->m_cutoff.value() + std::exp2(filterCutoffPitchOffset / 12.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it From b12ee04ecefbbf04dd24606468e366dd4c54baee Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:31:55 -0500 Subject: [PATCH 058/131] Use a single buffer for voices, instead of one buffer per region --- plugins/SfzSampler/SfzRegion.cpp | 66 ++----------------------------- plugins/SfzSampler/SfzRegion.h | 19 +-------- plugins/SfzSampler/SfzSampler.cpp | 65 +++++++++++++++++++++++++----- plugins/SfzSampler/SfzSampler.h | 25 ++++++++---- 4 files changed, 77 insertions(+), 98 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 9306465becf..21dee74ef6d 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -70,72 +70,11 @@ void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& tr globalState.switchKeyPressed(trigger.key().value()); } - // Before spawning an sounds, real quick do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer + // Before spawning a sound, do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer if (trigger.type() == SfzTrigger::Type::ControlChange) { recalculateTotalCCModulation(globalState); } - - // If the trigger conditions are met, spawn a new sound - if (triggerConditionsMet(globalState, trigger)) - { - qDebug() << "Spawning sound!" << m_sampleFile.value_or("N/A"); - // Loop through array to find open position - bool foundOpenPosition = false; - for (size_t i = 0; i <= m_activeSounds.size(); ++i) - { - auto& regionPlayState = m_activeSounds[i]; - if (!regionPlayState.active()) - { - regionPlayState = SfzRegionPlayState(this, trigger); - // If this new index is above the current max active index, update it - m_maxActiveIndex = std::max(m_maxActiveIndex, i); - foundOpenPosition = true; - break; - } - } - if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in RegionPlayState buffer!"; } - } - - // Loop through all the active sounds to check if any need to be deactivated/released by the trigger - for (size_t i = 0; i <= m_maxActiveIndex; ++i) - { - auto& regionPlayState = m_activeSounds[i]; - - if (regionPlayState.active()) - { - regionPlayState.processTrigger(trigger); - // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is - if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } - } - } -} - - -bool SfzRegion::play(SampleFrame* workingBuffer, const fpp_t frames) -{ - bool anythingPlayed = false; - for (size_t i = 0; i <= m_maxActiveIndex; ++i) - { - auto& regionPlayState = m_activeSounds[i]; - if (!regionPlayState.active()) { continue; } - - anythingPlayed = regionPlayState.play(workingBuffer, frames) || anythingPlayed; - // If the play state deactivated during playback, and this was the max active index, figure out what the new max active index is - if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } - } - return anythingPlayed; -} - - -void SfzRegion::recalculateMaxActiveIndex() -{ - // Loop backward from the old max active index to find the next play state which is active - while (m_maxActiveIndex > 0) - { - if (m_activeSounds[m_maxActiveIndex].active()) { return; } - else { m_maxActiveIndex--; } - } } @@ -143,9 +82,10 @@ void SfzRegion::recalculateMaxActiveIndex() float SfzRegion::totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const { float total = 0.0f; + // TODO this may be optimized if we only loop through the CC's which are actually used by the region for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { - total += ccModulationAmounts.at(i) * globalState.midiCCValue(i) / 128.0f; // m_set_cc stores the default CC values for each of the midi controllers + total += ccModulationAmounts.at(i) * globalState.midiCCValue(i) / 128.0f; } return total; } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 00e59761128..97bfb0ec509 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -25,14 +25,9 @@ class SfzRegion : public SfzOpcodeState //! such as which switch key was last pressed) this will return false. bool triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger); - //! Handles spawning a new sound if the all the conditions are met - //! Also handles sending the trigger to the currently active sounds, in case they need to deactivate/release (such as on NoteOff) + //! Updates cached CC modulations, and helps update keyswitch states void processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger); - //! Renders sound from each of the active SfzRegionPlayStates and writes it to the given buffer - //! Returns true if any sound was actually generated - bool play(SampleFrame* workingBuffer, const fpp_t frames); - //! Load the sample file given by the `sample` opcode into m_sample. //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed //! Returns true if successful @@ -41,22 +36,10 @@ class SfzRegion : public SfzOpcodeState const SfzSampleBuffer* sample() const { return m_sample; } private: - static constexpr int MAX_ACTIVE_SOUNDS = 128; - //! Array to store all active (and inactive) sound play states for this region - std::array m_activeSounds; - //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. const SfzSampleBuffer* m_sample = nullptr; - //! Maximum active index in the play state array - //! By always spawning new sounds at the lowest open index and keeping track of the maximum index which contains an actice sound, - //! you only need to loop through the first n elements and ignore the rest since you know they are inactive (Thanks to Lost Robot for the idea) - size_t m_maxActiveIndex = 0; - - //! Helper function to figure out what the maximum active index is, in the event the maximum index deacticated - void recalculateMaxActiveIndex(); - //! In order to do round robin, the region needs to keep track of how many notes it has played in its lifetime. Or rather, the number of notes it *would* have played if it weren't restricted to only play a note when the round-robin counter hit the right numbers. size_t m_roundRobinCount = 0; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index c8fba476e3e..06a5b9e265a 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -65,11 +65,6 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) , m_parentTrack(instrumentTrack) { - //QString path = ConfigManager::inst()->userSamplesDir() + "sfz/jlearman.jRhodes3c-master/jRhodes3c-looped-flac-sfz/"; - //loadFile(path + "_jRhodes-stereo-looped.sfz"); - - //loadFile(ConfigManager::inst()->userSamplesDir() + "sfz/SplendidGrandPiano-master/Splendid\ Grand\ Piano.sfz"); - auto iph = new InstrumentPlayHandle(this, instrumentTrack); Engine::audioEngine()->addPlayHandle(iph); @@ -124,7 +119,42 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) // Loop through all the regions to check if a new note should be played for (auto& region : m_sfzRegions) { + // Notify the region of the event so that it can update cached CC modulations, keyswitch states, etc region.processTrigger(m_sfzGlobalState, trigger); + + // If the trigger conditions are met, spawn a new sound + if (region.triggerConditionsMet(m_sfzGlobalState, trigger)) + { + qDebug() << "Spawning sound!" << region.m_sampleFile.value_or("N/A"); + // Loop through array to find open position + bool foundOpenPosition = false; + for (size_t i = 0; i <= m_voices.size(); ++i) + { + auto& regionPlayState = m_voices[i]; + if (!regionPlayState.active()) + { + regionPlayState = SfzRegionPlayState(®ion, trigger); + // If this new index is above the current max active index, update it + m_maxActiveIndex = std::max(m_maxActiveIndex, i); + foundOpenPosition = true; + break; + } + } + if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } + } + + // Loop through all the active sounds to check if any need to be deactivated/released by the trigger + for (size_t i = 0; i <= m_maxActiveIndex; ++i) + { + auto& regionPlayState = m_voices[i]; + + if (regionPlayState.active()) + { + regionPlayState.processTrigger(trigger); + // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is + if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } + } + } } } @@ -136,12 +166,27 @@ void SfzSampler::play(SampleFrame* workingBuffer) { const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); - for (auto& region : m_sfzRegions) + // Render audio from each of the active voices + for (size_t i = 0; i <= m_maxActiveIndex; ++i) + { + auto& regionPlayState = m_voices[i]; + if (!regionPlayState.active()) { continue; } + + regionPlayState.play(workingBuffer, frames); + // If the play state deactivated during playback, and this was the max active index, figure out what the new max active index is + if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } + } +} + + + +void SfzSampler::recalculateMaxActiveIndex() +{ + // Loop backward from the old max active index to find the next play state which is active + while (m_maxActiveIndex > 0) { - // Render audio from each of the regions - // This amounts to the regions themselves rendering the audio from each of their active SfzRegionPlayStates - // We pass a temporary buffer which can be used for rendering samples and later summing it to the working buffer. - bool anythingPlayed = region.play(workingBuffer, frames); + if (m_voices[m_maxActiveIndex].active()) { return; } + else { m_maxActiveIndex--; } } } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index bd8ea5d996c..3882f1681ed 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -67,9 +67,21 @@ class SfzSampler : public Instrument private: void processTrigger(const SfzTrigger& trigger); - InstrumentTrack* m_parentTrack; + //! Holds the configurations for each of the samples/regions: what keys/velocities/etc trigger it, the volume, filter, envelopes, etc + std::vector m_sfzRegions; - QString m_sfzFilePath = ""; + static constexpr int MAX_ACTIVE_SOUNDS = 128; + //! Array to store all active (and inactive) sound play-states across all regions + std::array m_voices; + //! Maximum active index in the play state array + //! By always spawning new sounds at the lowest open index and keeping track of the maximum index which contains an actice sound, + //! you only need to loop through the first n elements and ignore the rest since you know they are inactive (Thanks to Lost Robot for the idea) + size_t m_maxActiveIndex = 0; + //! Helper function to figure out what the maximum active index is, in the event the maximum index deacticated + void recalculateMaxActiveIndex(); + + //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded + SfzSamplePool m_samplePool; //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; @@ -77,11 +89,6 @@ class SfzSampler : public Instrument //! Holds information about midi CC default values, labels, which CC's are actually used, etc SfzControlsConfig m_controlsConfig; - std::vector m_sfzRegions; - - //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded - SfzSamplePool m_samplePool; - // The GUI needs models to connect to midi CC knobs, so a bunch of dummy models are defined here. // Ideally it would be better to reuse the midi CC models for the instrument track std::array m_ccModels; @@ -90,6 +97,10 @@ class SfzSampler : public Instrument //! To account for this, we store the velocity of the last NoteOn event for each key, and use that value when hanlding NoteOff events. std::array m_previousNoteOnVelocity = {}; + InstrumentTrack* m_parentTrack; + + QString m_sfzFilePath = ""; + friend class gui::SfzSamplerView; }; } // namespace lmms From 70cbc1f9140d97621251497bc3ee9baff6b6d9f3 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:02:24 -0500 Subject: [PATCH 059/131] Optimize checking loccN/hiccN --- plugins/SfzSampler/SfzRegion.cpp | 11 +++++++++-- plugins/SfzSampler/SfzRegion.h | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 21dee74ef6d..fe36fdbd4b4 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -19,6 +19,12 @@ SfzRegion::SfzRegion(SfzOpcodeState opcodeState) : SfzOpcodeState(opcodeState) { recalculateTotalCCModulation(SfzGlobalState()); // The region objects don't currently have direct access to the global state, so pass in a blank object just to reset the CC modulations to their defaults. + + // Cache which loccN/hiccN opcodes are defined in this region, so that we only have to loop through and check those upon a trigger, not all 128 + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + if (m_locc.at(i) != 0 || m_hicc.at(i) != 127) { m_lohiccDefinedCCNumbers.push_back(i); } + } } bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) @@ -48,7 +54,8 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf } // If midi CC ranges are defined, make sure the current CC values are within range - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + // Only loop over the CC's which have lo/hiccN defined, instead of checking all 128 every time + for (const int i : m_lohiccDefinedCCNumbers) { const int ccValue = globalState.midiCCValue(i); if (ccValue > m_hicc.at(i) || ccValue < m_locc.at(i)) { return false; } @@ -67,7 +74,7 @@ void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& tr // Notify the global state whether a switch key has been pressed, so that it can correctly track when `sw_last` is met if (trigger.type() == SfzTrigger::Type::NoteOn && m_sw_last != std::nullopt && m_sw_last == trigger.key().value()) { - globalState.switchKeyPressed(trigger.key().value()); + globalState.switchKeyPressed(trigger.key().value()); // TODO this can probably be moved somewhere else so that it isn't done for every region (if you have 10000+ regions, it needs to be optimized) } // Before spawning a sound, do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 97bfb0ec509..7b3e5a386aa 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -56,6 +56,10 @@ class SfzRegion : public SfzOpcodeState float m_gain_totalCC = 0.0f; + //! When checking whether all the current CC values fall between loccN and hiccN, it's useful to only check the ones where lo/hiccN is actually defined, not all 128 + //! This vector stores a list of which CC numbers have lo/hiccN opcodes in this region + std::vector m_lohiccDefinedCCNumbers; + //! Helper function to calculate the total modulation of all midi CC controllers on a parameter. Essentially it just multiplies the modulation amounts by the current CC values and adds it all up. float totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const; void recalculateTotalCCModulation(const SfzGlobalState& globalState); From e0478e3e7e475760a03eb559769a4ddafce14d1d Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:22:20 -0500 Subject: [PATCH 060/131] Add SfzRegionManager to speed up trigger processing by using key as index when searching for lists of potentially matching regions --- plugins/SfzSampler/CMakeLists.txt | 3 ++ plugins/SfzSampler/SfzRegion.cpp | 7 +-- plugins/SfzSampler/SfzRegion.h | 1 + plugins/SfzSampler/SfzRegionManager.cpp | 61 +++++++++++++++++++++++++ plugins/SfzSampler/SfzRegionManager.h | 40 ++++++++++++++++ plugins/SfzSampler/SfzSampler.cpp | 60 ++++++++++++++---------- plugins/SfzSampler/SfzSampler.h | 6 ++- 7 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 plugins/SfzSampler/SfzRegionManager.cpp create mode 100644 plugins/SfzSampler/SfzRegionManager.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index 3c7eb026c89..e13b920f9da 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -13,6 +13,8 @@ build_plugin(sfzsampler SfzParser.h SfzRegionPlayState.cpp SfzRegionPlayState.h + SfzRegionManager.cpp + SfzRegionManager.h SfzGlobalState.cpp SfzGlobalState.h SfzControlsConfig.h @@ -29,6 +31,7 @@ build_plugin(sfzsampler SfzRegion.h SfzParser.h SfzRegionPlayState.h + SfzRegionManager.h SfzGlobalState.h SfzControlsConfig.h SfzTrigger.h diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index fe36fdbd4b4..5d2a4b27ee7 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -38,10 +38,11 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // Assuming the trigger has key/vel info (i.e., it's a noteOn/noteOff, not a midi CC event), make sure all the key/vel selectors match if (trigger.type() == SfzTrigger::Type::NoteOn || trigger.type() == SfzTrigger::Type::NoteOff) { - int triggerKey = trigger.key().value(); - int triggerVelocity = trigger.velocity().value(); + const int triggerKey = trigger.key().value(); + const int triggerVelocity = trigger.velocity().value(); // Ensure the key was pressed between the `lokey` and `hikey` opcodes + // TODO this can be removed now that SfzRegionManager handles lookup tables for regions by key if (triggerKey > m_hikey || triggerKey < m_lokey) { return false; } // And had velocity between `lovel` and `hivel` opcodes @@ -62,7 +63,7 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf } // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. - m_roundRobinCount++; + m_roundRobinCount++; // TODO it would be nice if this function could be const and we didn't have to update this here. idk. if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn // If all the contitions passed, return true and spawn a sound diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 7b3e5a386aa..ddc285c2fc9 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -23,6 +23,7 @@ class SfzRegion : public SfzOpcodeState //! For example, if lokey=24 and hikey=29, and the trigger is a NoteOn event on key 26, then it will return true //! If the trigger does not fall in the key range or velocity range or any other conditions are not met (including global state conditions, //! such as which switch key was last pressed) this will return false. + //! TODO this method also increments the round robin counter, if applicable. Ideally I feel like this method should be const, but I'm not sure how to best organize it. bool triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger); //! Updates cached CC modulations, and helps update keyswitch states diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp new file mode 100644 index 00000000000..952aa9878a3 --- /dev/null +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -0,0 +1,61 @@ + + +#include "SfzRegionManager.h" +#include + + +namespace lmms +{ + + +SfzRegionManager::SfzRegionManager(std::vector& regions) + : m_regions(regions) +{ + // For each key, figure out what list of regions could include it, to make a kind of look-up table + for (int key = 0; key < 128; ++key) + { + for (auto& region : m_regions) + { + if (key >= region.m_lokey && key <= region.m_hikey) + { + // Additionally sort by trigger type + switch (region.m_trigger) + { + case SfzOpcodeState::TriggerType::Attack: + m_noteOnRegions.at(key).push_back(®ion); + break; + case SfzOpcodeState::TriggerType::Release: + m_noteOffRegions.at(key).push_back(®ion); + break; + } + } + } + } + + // Unfortunately, it's more difficult to make lookup tables for midi CC events. Instead, just use a big vector with all the regions by default + // TODO or is it? there's probably a way. But currently we actually don't support cc events triggering regions, so technically this will never be used at the moment. + for (auto& region : m_regions) + { + m_allRegions.push_back(®ion); + } +} + + + +const std::vector& SfzRegionManager::findPotentialMatchingRegions(const SfzTrigger& trigger) const +{ + switch (trigger.type()) + { + case SfzTrigger::Type::NoteOn: + return m_noteOnRegions.at(trigger.key().value()); + case SfzTrigger::Type::NoteOff: + return m_noteOffRegions.at(trigger.key().value()); + case SfzTrigger::Type::ControlChange: + return m_allRegions; // TODO is it possible to somehow make a lookup table for CC events? + } +} + + + + +} // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegionManager.h b/plugins/SfzSampler/SfzRegionManager.h new file mode 100644 index 00000000000..be4098bc443 --- /dev/null +++ b/plugins/SfzSampler/SfzRegionManager.h @@ -0,0 +1,40 @@ + +#ifndef LMMS_SFZ_REGION_MANAGER_H +#define LMMS_SFZ_REGION_MANAGER_H + +#include "SfzRegion.h" + + +namespace lmms +{ + +class SfzRegionManager +{ +public: + //! The point of this class is to speed up processing triggers (NoteOn/NoteOff/etc events) + //! Normally, if your SFZ file has tens of thousands of regions, you have to loop over every single one each time a key + //! is pressed to check whether all the conditions are met and it should spawn a voice or not (is the key within lokey/hikey range, + //! is the velocity within lovel/hivel range, has the right switch key been pressed, are the midi CC's in the right ranges, etc) + //! This is not optimal. + //! Instead, because there are only 128 keys, we can construct a mapping, where each key is paired with a list of regions where lokey/hikey matches it + //! Doing this for both NoteOn and NoteOff events means having 256 lists of pointers to regions, which is not bad. + SfzRegionManager() = default; + SfzRegionManager(std::vector& regions); + + //! Based on the trigger type, key, etc, returns a list of regions which might match the trigger. + //! This is meant to intelligently narrow down the number of regions where we manually have to loop over and check + const std::vector& findPotentialMatchingRegions(const SfzTrigger& trigger) const; + + const std::vector& allRegions() const { return m_allRegions; } + +private: + std::vector m_regions; + + std::vector m_allRegions; + std::array, 128> m_noteOnRegions; + std::array, 128> m_noteOffRegions; +}; + +} // namespace lmms + +#endif // LMMS_SFZ_REGION_MANAGER_H diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 06a5b9e265a..2bb70e5ead9 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -117,15 +117,19 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) m_sfzGlobalState.processTrigger(trigger); // Loop through all the regions to check if a new note should be played - for (auto& region : m_sfzRegions) + // TODO can we get rid of this loop + for (auto* region : m_regionManager.allRegions()) { // Notify the region of the event so that it can update cached CC modulations, keyswitch states, etc - region.processTrigger(m_sfzGlobalState, trigger); - + region->processTrigger(m_sfzGlobalState, trigger); + } + + for (auto* region : m_regionManager.findPotentialMatchingRegions(trigger)) + { // If the trigger conditions are met, spawn a new sound - if (region.triggerConditionsMet(m_sfzGlobalState, trigger)) + if (region->triggerConditionsMet(m_sfzGlobalState, trigger)) { - qDebug() << "Spawning sound!" << region.m_sampleFile.value_or("N/A"); + qDebug() << "Spawning sound!" << region->m_sampleFile.value_or("N/A"); // Loop through array to find open position bool foundOpenPosition = false; for (size_t i = 0; i <= m_voices.size(); ++i) @@ -133,7 +137,7 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) auto& regionPlayState = m_voices[i]; if (!regionPlayState.active()) { - regionPlayState = SfzRegionPlayState(®ion, trigger); + regionPlayState = SfzRegionPlayState(region, trigger); // If this new index is above the current max active index, update it m_maxActiveIndex = std::max(m_maxActiveIndex, i); foundOpenPosition = true; @@ -142,18 +146,18 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) } if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } } + } - // Loop through all the active sounds to check if any need to be deactivated/released by the trigger - for (size_t i = 0; i <= m_maxActiveIndex; ++i) - { - auto& regionPlayState = m_voices[i]; + // Loop through all the active sounds to check if any need to be deactivated/released by the trigger + for (size_t i = 0; i <= m_maxActiveIndex; ++i) + { + auto& regionPlayState = m_voices[i]; - if (regionPlayState.active()) - { - regionPlayState.processTrigger(trigger); - // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is - if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } - } + if (regionPlayState.active()) + { + regionPlayState.processTrigger(trigger); + // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is + if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } } } } @@ -195,34 +199,40 @@ void SfzSampler::recalculateMaxActiveIndex() void SfzSampler::loadFile(const QString& filePath) { - // Prevent the audio thread from accidentally looping through the region vector while it's being edited + // Prevent the audio thread from accidentally looping through the regions while they are being edited const auto guard = Engine::audioEngine()->requestChangesGuard(); // Reset the note counts, midi cc values, etc m_sfzGlobalState = SfzGlobalState(); // And any info about control labels, default values, etc m_controlsConfig = SfzControlsConfig(); - // Reset any loaded samples - m_samplePool = SfzSamplePool(); - // Parse all the headers of the .sfz (accounting for and defaults) and populate m_sfzRegions with the new SfzRegion + // Temporary vector to store SfzRegion objects + std::vector regions; + // Parse all the headers of the .sfz (accounting for and defaults) and populate the regions vector with the new SfzRegions // The header is also parsed into a separate object for easy access by the gui - bool successfulParseFile = SfzParser::parseSfzFile(filePath, m_sfzRegions, m_controlsConfig); + bool successfulParseFile = SfzParser::parseSfzFile(filePath, regions, m_controlsConfig); if (!successfulParseFile) { qDebug() << "[SFZ Player] An error occurred when parsing the SFZ file."; return; } + // Hand off the vector of regions to SfzRegionManager, which will sort them out to optimize trigger selection + m_regionManager = SfzRegionManager(regions); // TODO should move semantics be used here? + // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + // Reset any loaded samples + m_samplePool = SfzSamplePool(); int i = 0; - for (auto& region : m_sfzRegions) + for (auto* region : m_regionManager.allRegions()) { - qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_sfzRegions.size() << region.m_sampleFile.value_or("N/A"); - bool successfulLoadSample = region.initializeSample(parentDirectory, m_samplePool); + qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_regionManager.allRegions().size() << region->m_sampleFile.value_or("N/A"); + bool successfulLoadSample = region->initializeSample(parentDirectory, m_samplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } i++; } - qDebug() << "Loaded" << m_sfzRegions.size() << "regions and" << m_samplePool.sampleCount() << "samples."; + qDebug() << "Loaded" << m_regionManager.allRegions().size() << "regions and" << m_samplePool.sampleCount() << "samples."; + // Set the initial cc values based on any `set_ccN` opcodes in the header m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 3882f1681ed..612b9c80278 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -37,6 +37,7 @@ #include "SfzGlobalState.h" #include "SfzControlsConfig.h" #include "SfzSamplePool.h" +#include "SfzRegionManager.h" namespace lmms { @@ -67,8 +68,9 @@ class SfzSampler : public Instrument private: void processTrigger(const SfzTrigger& trigger); - //! Holds the configurations for each of the samples/regions: what keys/velocities/etc trigger it, the volume, filter, envelopes, etc - std::vector m_sfzRegions; + //! Holds a list of all the SfzRegion objects, which have the configurations for each of the samples/regions: what keys/velocities/etc trigger it, the volume, filter, envelopes, etc + //! This object helps map triggers to lists of potentially matching regions, so that we don't have to loop over all the regions, checking every single one whether all conditions are matched before spawning a voice. + SfzRegionManager m_regionManager; static constexpr int MAX_ACTIVE_SOUNDS = 128; //! Array to store all active (and inactive) sound play-states across all regions From e9970537e0e4defddc826e8b3f83f00f624f4b0f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:10:57 -0500 Subject: [PATCH 061/131] Remove unneeded include --- plugins/SfzSampler/SfzRegionManager.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index 952aa9878a3..11f7291acb4 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -1,7 +1,6 @@ #include "SfzRegionManager.h" -#include namespace lmms From 86fb56cf45f8d4b48153f446f30ca9e19910dc16 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:06:38 -0500 Subject: [PATCH 062/131] Fix bug with default_path sometimes not parsing if came after --- plugins/SfzSampler/SfzParser.cpp | 3 --- plugins/SfzSampler/SfzRegion.cpp | 2 +- plugins/SfzSampler/SfzSampler.cpp | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 66be352846d..0ae6005a34d 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -179,9 +179,6 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou case Header::Control: { controlsConfig.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); - // Also set the opcode for the global state, that way it will get propagated down to all other regions (this is needed for things like `default_path` to work for all regions) - // Technically it could be reworked so that the regions fetch any needed info from the controls state, but for now this way is simpler. - globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } case Header::Curve: diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 5d2a4b27ee7..ee70b1f75c2 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -124,7 +124,7 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam return false; } - QString path = parentDirectory.absoluteFilePath(m_default_path.value_or("") + m_sampleFile.value()); + QString path = parentDirectory.absoluteFilePath(m_sampleFile.value()); // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path); diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 2bb70e5ead9..8feba97f367 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -220,7 +220,7 @@ void SfzSampler::loadFile(const QString& filePath) // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: - QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + QDir parentDirectory = QDir(QFileInfo(filePath).absoluteDir().absoluteFilePath(m_controlsConfig.m_default_path.value_or(""))); // Reset any loaded samples m_samplePool = SfzSamplePool(); int i = 0; From e040a1a4ecb86e2e1c4223da6e757c2838ec877e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:14:57 -0500 Subject: [PATCH 063/131] Use the instrument track's midi CC's, not our own --- plugins/SfzSampler/SfzSampler.cpp | 6 ++++++ plugins/SfzSampler/SfzSampler.h | 4 ---- plugins/SfzSampler/SfzSamplerView.cpp | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 8feba97f367..fd00d031a55 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -253,6 +253,12 @@ void SfzSampler::loadSettings(const QDomElement& element) { loadFile(m_sfzFilePath); } + // Make sure the midi CC knobs send their current values so that saved presets work normally upon loading + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + // TODO should the trigger be passed to the whole instrument or just the global state? We don't really want to trigger any regions which are triggered by cc events (not implemented yet). But this doesn't feel very clean either. + m_sfzGlobalState.processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); + } } QString SfzSampler::nodeName() const diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 612b9c80278..e735db726d6 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -91,10 +91,6 @@ class SfzSampler : public Instrument //! Holds information about midi CC default values, labels, which CC's are actually used, etc SfzControlsConfig m_controlsConfig; - // The GUI needs models to connect to midi CC knobs, so a bunch of dummy models are defined here. - // Ideally it would be better to reuse the midi CC models for the instrument track - std::array m_ccModels; - //! Unfortunately, LMMS by default has the velocity of NoteOff events be 0. However, SFZ expects them to match the velocity of the corresponding NoteOn event. //! To account for this, we store the velocity of the last NoteOn event for each key, and use that value when hanlding NoteOff events. std::array m_previousNoteOnVelocity = {}; diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 621b850121b..fff5279a263 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -48,6 +48,7 @@ #include "PathUtil.h" #include "embed.h" #include "MidiEvent.h" +#include "InstrumentTrack.h" namespace lmms { @@ -101,18 +102,18 @@ void SfzSamplerView::onFileLoaded() // Only add a knob if the control is actually used if (m_instrument->m_controlsConfig.m_activeMidiCCs.at(i)) { - m_instrument->m_ccModels.at(i).setRange(0, 127, 1); - m_instrument->m_ccModels.at(i).setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); const QString& controlLabel = m_instrument->m_controlsConfig.m_label_cc.at(i).value_or(tr("CC %1").arg(i)); auto ccKnob = new Knob(KnobType::Bright26, controlLabel, m_controlsWidget); - ccKnob->setModel(&m_instrument->m_ccModels.at(i)); + ccKnob->setModel(m_instrument->m_parentTrack->midiCCModel(i)); m_knobLayout->addWidget(ccKnob, activeControlCount / 8, activeControlCount % 8); - connect(&m_instrument->m_ccModels.at(i), &AutomatableModel::dataChanged, this, [i, this](){ - m_instrument->handleMidiEvent(MidiEvent(MidiControlChange, 0, i, m_instrument->m_ccModels.at(i).value())); - }); activeControlCount++; } } + // If we are using midi CC's make sure to enable them in the instrument track + if (activeControlCount > 0) + { + m_instrument->m_parentTrack->midiCCEnableModel()->setValue(true, true); + } // Update the switch key list QStringList switchKeyLabels; @@ -149,6 +150,11 @@ void SfzSamplerView::openFile() { if (openFileDialog.selectedFiles().isEmpty()) { return; } m_instrument->loadFile(openFileDialog.selectedFiles()[0]); + // If the file is being loaded due to the user clicking the button (not opening a save file) then we also need to update the midi CC knobs + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + m_instrument->m_parentTrack->midiCCModel(i)->setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); + } } } From bd8bf1c60dfd039d83537ab2f16418647097fd4a Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:00:20 -0500 Subject: [PATCH 064/131] Add getters for instrument track midi cc models --- include/InstrumentTrack.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index b8777f88a44..61ae03b7343 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -239,6 +239,12 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor void autoAssignMidiDevice( bool ); + //! Returns a non-owning pointer to the LED in the MIDI CC rack which toggles whether the knobs send midi control change events + BoolModel* midiCCEnableModel() const { return m_midiCCEnable.get(); } + + //! Returns a non-owning pointer to the model for the knob at the given index in the track's MIDI CC rack + FloatModel* midiCCModel(int index) const { return m_midiCCModel[index].get(); } + signals: void instrumentChanged(); void midiNoteOn( const lmms::Note& ); From a8be0e152cac5a9103a140605134835aab282adc Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:54:10 -0500 Subject: [PATCH 065/131] Add copyright info --- plugins/SfzSampler/SfzGlobalState.cpp | 24 +++++++++++++++++++ plugins/SfzSampler/SfzGlobalState.h | 24 +++++++++++++++++++ plugins/SfzSampler/SfzOpcodeState.cpp | 26 ++++++++++++++++++--- plugins/SfzSampler/SfzOpcodeState.h | 23 +++++++++++++++++++ plugins/SfzSampler/SfzParser.cpp | 24 ++++++++++++++++++- plugins/SfzSampler/SfzParser.h | 23 +++++++++++++++++++ plugins/SfzSampler/SfzRegion.cpp | 28 +++++++++++++++++++---- plugins/SfzSampler/SfzRegion.h | 23 +++++++++++++++++++ plugins/SfzSampler/SfzRegionManager.cpp | 24 ++++++++++++++++++- plugins/SfzSampler/SfzRegionManager.h | 23 +++++++++++++++++++ plugins/SfzSampler/SfzRegionPlayState.cpp | 24 ++++++++++++++++++- plugins/SfzSampler/SfzRegionPlayState.h | 24 +++++++++++++++++++ plugins/SfzSampler/SfzSampleBuffer.cpp | 24 ++++++++++++++++++- plugins/SfzSampler/SfzSampleBuffer.h | 23 +++++++++++++++++++ plugins/SfzSampler/SfzSamplePool.cpp | 26 ++++++++++++++++++--- plugins/SfzSampler/SfzSamplePool.h | 23 +++++++++++++++++++ plugins/SfzSampler/SfzSampler.cpp | 4 ++-- plugins/SfzSampler/SfzSampler.h | 4 ++-- plugins/SfzSampler/SfzSamplerView.cpp | 4 ++-- plugins/SfzSampler/SfzSamplerView.h | 4 ++-- plugins/SfzSampler/SfzTrigger.cpp | 24 ++++++++++++++++++- plugins/SfzSampler/SfzTrigger.h | 23 +++++++++++++++++++ 22 files changed, 425 insertions(+), 24 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 23433ec836b..24b777dcec9 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -1,3 +1,27 @@ +/* + * SfzGlobalState.cpp - Class which holds information about the current state of the + * SFZ player, such as currently/previously pressed keys, and current midi CC values + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzGlobalState.h" diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index da9a4fc71b3..ec57d843241 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -1,3 +1,27 @@ +/* + * SfzGlobalState.h - Class which holds information about the current state of the + * SFZ player, such as currently/previously pressed keys, and current midi CC values + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_GLOBAL_STATE_H #define LMMS_SFZ_GLOBAL_STATE_H diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 2086e1f9468..951c6e4989e 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -1,6 +1,26 @@ - - - +/* + * SfzOpcodeState.cpp - Representation of a -like object in an SFZ file, which stores all the configurations regarding how and when a sample should be played + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzOpcodeState.h" #include diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 91b70a051f7..de122b2ea3c 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -1,3 +1,26 @@ +/* + * SfzOpcodeState.h - Representation of a -like object in an SFZ file, which stores all the configurations regarding how and when a sample should be played + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_OPCODE_STATE_H #define LMMS_SFZ_OPCODE_STATE_H diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 0ae6005a34d..63d9684ded9 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -1,4 +1,26 @@ - +/* + * SfzParser.cpp - Helper class for parsing .sfz files into objects which can be used by the SFZ player + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzParser.h" #include "SfzOpcodeState.h" diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index 26a58b27d8b..c03f1b018c6 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -1,3 +1,26 @@ +/* + * SfzParser.h - Helper class for parsing .sfz files into objects which can be used by the SFZ player + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_PARSER_H #define LMMS_SFZ_PARSER_H diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index ee70b1f75c2..b259dbf5be9 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -1,8 +1,26 @@ - - - - - +/* + * SfzRegion.cpp - Wrapper class for SfzOpcodeState which handles the actual sample, along with helper functions for triggers + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzRegion.h" diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index ddc285c2fc9..6f69e1de80b 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -1,3 +1,26 @@ +/* + * SfzRegion.h - Wrapper class for SfzOpcodeState which handles the actual sample, along with helper functions for triggers + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_REGION_H #define LMMS_SFZ_REGION_H diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index 11f7291acb4..cbd8faa1072 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -1,4 +1,26 @@ - +/* + * SfzRegionManager.cpp - Helper class for optimizing region selection based on trigger + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzRegionManager.h" diff --git a/plugins/SfzSampler/SfzRegionManager.h b/plugins/SfzSampler/SfzRegionManager.h index be4098bc443..19e662b25db 100644 --- a/plugins/SfzSampler/SfzRegionManager.h +++ b/plugins/SfzSampler/SfzRegionManager.h @@ -1,3 +1,26 @@ +/* + * SfzRegionManager.h - Helper class for optimizing region selection based on trigger + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_REGION_MANAGER_H #define LMMS_SFZ_REGION_MANAGER_H diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index adbf4d193e4..f8e9a2b0d0b 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -1,4 +1,26 @@ - +/* + * SfzRegionPlayState.cpp - Handles generating audio for a single voice based on the configuration of its parent SfzRegion + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzRegionPlayState.h" diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 86213a20fac..00e7c09e217 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -1,3 +1,27 @@ +/* + * SfzRegionPlayState.h - Handles generating audio for a single voice based on the configuration of its parent SfzRegion + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + #ifndef LMMS_SFZ_REGION_PLAY_STATE_H #define LMMS_SFZ_REGION_PLAY_STATE_H diff --git a/plugins/SfzSampler/SfzSampleBuffer.cpp b/plugins/SfzSampler/SfzSampleBuffer.cpp index 53d1bcf9cbc..d0db323c10e 100644 --- a/plugins/SfzSampler/SfzSampleBuffer.cpp +++ b/plugins/SfzSampler/SfzSampleBuffer.cpp @@ -1,4 +1,26 @@ - +/* + * SfzSampleBuffer.cpp - Custom sample data class + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzSampleBuffer.h" #include "interpolation.h" diff --git a/plugins/SfzSampler/SfzSampleBuffer.h b/plugins/SfzSampler/SfzSampleBuffer.h index 041fc2dbbad..d740b87c889 100644 --- a/plugins/SfzSampler/SfzSampleBuffer.h +++ b/plugins/SfzSampler/SfzSampleBuffer.h @@ -1,3 +1,26 @@ +/* + * SfzSampleBuffer.h - Custom sample data class + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_SAMPLE_BUFFER_H #define LMMS_SFZ_SAMPLE_BUFFER_H diff --git a/plugins/SfzSampler/SfzSamplePool.cpp b/plugins/SfzSampler/SfzSamplePool.cpp index 31aa1424305..888686fb45f 100644 --- a/plugins/SfzSampler/SfzSamplePool.cpp +++ b/plugins/SfzSampler/SfzSamplePool.cpp @@ -1,6 +1,26 @@ - - - +/* + * SfzSamplePool.cpp - Helper class for handling loading of sample files + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzSamplePool.h" #include "SampleBuffer.h" diff --git a/plugins/SfzSampler/SfzSamplePool.h b/plugins/SfzSampler/SfzSamplePool.h index a57f5159a34..a3488b1bf8b 100644 --- a/plugins/SfzSampler/SfzSamplePool.h +++ b/plugins/SfzSampler/SfzSamplePool.h @@ -1,3 +1,26 @@ +/* + * SfzSamplePool.h - Helper class for handling loading of sample files + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_SAMPLE_POOL_H #define LMMS_SFZ_SAMPLE_POOL_H diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index fd00d031a55..a5feb044d96 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -1,7 +1,7 @@ /* - * SfzSampler.cpp - Simple SFZ instrument loader/editor + * SfzSampler.cpp - Simple SFZ instrument player * - * Copyright (c) 2023 Daniel Kauss Serna + * Copyright (c) 2026 Keratin * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index e735db726d6..43b49c842e5 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -1,7 +1,7 @@ /* - * SfzSampler.h - Declaration of SfzSampler class, Simple SFZ instrument loader/editor + * SfzSampler.h - Simple SFZ instrument player * - * Copyright (c) 2023 Daniel Kauss Serna + * Copyright (c) 2026 Keratin * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index fff5279a263..0ef062e9b9d 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -1,7 +1,7 @@ /* - * SfzSamplerView.cpp - Controls View for SfzSampler + * SfzSamplerView.cpp - GUI for SfzSampler * - * Copyright (c) 2023 Daniel Kauss Serna + * Copyright (c) 2026 Keratin * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index 53de61238df..dc02daab447 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -1,7 +1,7 @@ /* - * SfzSamplerView.h - Declaration of class SfzSamplerView + * SfzSamplerView.h - GUI for SfzSampler * - * Copyright (c) 2023 Daniel Kauss Serna + * Copyright (c) 2026 Keratin * * This file is part of LMMS - https://lmms.io * diff --git a/plugins/SfzSampler/SfzTrigger.cpp b/plugins/SfzSampler/SfzTrigger.cpp index 0c769af737e..d2113ea8f2a 100644 --- a/plugins/SfzSampler/SfzTrigger.cpp +++ b/plugins/SfzSampler/SfzTrigger.cpp @@ -1,4 +1,26 @@ - +/* + * SfzTrigger.cpp - Custom class for representing MIDI events relevant to the SFZ player + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #include "SfzTrigger.h" diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzSampler/SfzTrigger.h index b24a8ac87ee..95e37360494 100644 --- a/plugins/SfzSampler/SfzTrigger.h +++ b/plugins/SfzSampler/SfzTrigger.h @@ -1,3 +1,26 @@ +/* + * SfzTrigger.h - Custom class for representing MIDI events relevant to the SFZ player + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ #ifndef LMMS_SFZ_TRIGGER_H #define LMMS_SFZ_TRIGGER_H From ea9f20d1596e0437ae7d3b195cc65665f0b6ac1c Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:01:50 -0500 Subject: [PATCH 066/131] Fix warnings --- plugins/SfzSampler/SfzRegion.h | 2 +- plugins/SfzSampler/SfzRegionManager.cpp | 1 + plugins/SfzSampler/SfzRegionPlayState.cpp | 13 ------------- plugins/SfzSampler/SfzSampler.cpp | 6 +++--- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 6f69e1de80b..35bcba3ba49 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -65,7 +65,7 @@ class SfzRegion : public SfzOpcodeState const SfzSampleBuffer* m_sample = nullptr; //! In order to do round robin, the region needs to keep track of how many notes it has played in its lifetime. Or rather, the number of notes it *would* have played if it weren't restricted to only play a note when the round-robin counter hit the right numbers. - size_t m_roundRobinCount = 0; + int m_roundRobinCount = 0; //! Store the current total midi CC modulation amounts for the different targets, just so that we don't // have to recalculate them every buffer, instead only when a trigger occurs. diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index cbd8faa1072..3c09b5974e1 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -74,6 +74,7 @@ const std::vector& SfzRegionManager::findPotentialMatchingRegions(co case SfzTrigger::Type::ControlChange: return m_allRegions; // TODO is it possible to somehow make a lookup table for CC events? } + return m_allRegions; // This was added to prevent a warning, but I don't think it's necessary, since the switch covers all cases. } diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index f8e9a2b0d0b..698765e76ec 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -127,18 +127,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // If the sound is not active (note was released, off_by triggered, etc) then don't render any audio if (!m_active) { return false; } - static int totalMicroseconds = 0; - static int totalSquaredMicroseconds = 0; - static int totalCalls = 0; - static int minElapsed = 10000000; - static int maxElapsed = 0; - MicroTimer profiler; - // If the initial m_frameCount is negative, that means the note hasn't started yet if (m_frameCount < -static_cast(frames)) { m_frameCount += frames; return false; } // If the note doesn't start in this buffer, don't play anything - // If the start is within this buffer, get the number of frames until it starts - const f_cnt_t startFrameOffset = std::max(0, -m_frameCount); - const f_cnt_t framesToPlay = frames - startFrameOffset; // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; @@ -236,9 +226,6 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) m_active = false; // TODO should this forcefully decative or just release? } - int elapsed = profiler.elapsed(); totalMicroseconds += elapsed; totalSquaredMicroseconds += elapsed * elapsed; totalCalls++; minElapsed = std::min(minElapsed, elapsed); maxElapsed = std::max(maxElapsed, elapsed); - float mean = 1.0f * totalMicroseconds / totalCalls, variance = (1.0f * totalSquaredMicroseconds - 1.0f * totalMicroseconds * totalMicroseconds / totalCalls / totalCalls) / totalCalls; - //qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); return true; } diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index a5feb044d96..275e947b6d1 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -100,9 +100,9 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { - int noteIndex = handle->key(); - const fpp_t frames = handle->framesLeftForCurrentPeriod(); - const f_cnt_t offset = handle->noteOffset(); + //int noteIndex = handle->key(); + //const fpp_t frames = handle->framesLeftForCurrentPeriod(); + //const f_cnt_t offset = handle->noteOffset(); } void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) From 245098eed4e19251ba6f0c5e752fb3ff0b7873d3 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:38:50 -0500 Subject: [PATCH 067/131] Allow drag/drop from side panel file browser --- include/FileBrowser.h | 1 + plugins/SfzSampler/SfzSampler.cpp | 2 +- src/gui/FileBrowser.cpp | 14 ++++++++++++-- src/gui/editors/TrackContainerView.cpp | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/include/FileBrowser.h b/include/FileBrowser.h index 11bd612c41d..5fab2856c8d 100644 --- a/include/FileBrowser.h +++ b/include/FileBrowser.h @@ -248,6 +248,7 @@ class FileItem : public FileBrowserWidgetItem Preset, Sample, SoundFont, + SFZ, Patch, Midi, VstPlugin, diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 275e947b6d1..d960e2bd9fd 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -51,7 +51,7 @@ Plugin::Descriptor PLUGIN_EXPORT sfzsampler_plugin_descriptor = { 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader("logo"), - nullptr, + "sfz", nullptr, }; PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index bd30d22d16d..a26d8ee980f 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -811,7 +811,7 @@ void FileBrowserTreeWidget::previewFileItem(FileItem* file) const bool isPlugin = file->handling() == FileItem::FileHandling::LoadByPlugin; newPPH = new PresetPreviewPlayHandle(fileName, isPlugin); } - else if (file->type() != FileItem::FileType::VstPlugin && file->isTrack()) + else if (file->type() != FileItem::FileType::VstPlugin && file->type() != FileItem::FileType::SFZ && file->isTrack()) { DataFile dataFile(fileName); if (dataFile.validate(ext)) @@ -881,6 +881,10 @@ void FileBrowserTreeWidget::mouseMoveEvent( QMouseEvent * me ) new StringPairDrag( "soundfontfile", f->fullName(), embed::getIconPixmap( "soundfont_file" ), this ); break; + case FileItem::FileType::SFZ: + new StringPairDrag("sfzfile", f->fullName(), + embed::getIconPixmap("soundfont_file"), this); + break; case FileItem::FileType::Patch: new StringPairDrag( "patchfile", f->fullName(), embed::getIconPixmap( "sample_file" ), this ); @@ -1196,6 +1200,7 @@ void FileItem::initPixmaps() setIcon(0, s_presetFilePixmap); break; case FileType::SoundFont: + case FileType::SFZ: setIcon(0, s_soundfontFilePixmap); break; case FileType::VstPlugin: @@ -1242,6 +1247,10 @@ void FileItem::determineFileType() { m_type = FileType::SoundFont; } + else if (ext == "sfz") + { + m_type = FileType::SFZ; + } else if( ext == "pat" ) { m_type = FileType::Patch; @@ -1310,6 +1319,7 @@ QString FileItem::defaultFilters() const auto projectFilters = QStringList{"*.mmp", "*.mpt", "*.mmpz"}; const auto presetFilters = QStringList{"*.xpf", "*.xml", "*.xiz", "*.lv2"}; const auto soundFontFilters = QStringList{"*.sf2", "*.sf3"}; + const auto sfzFilters = QStringList{"*.sfz"}; const auto patchFilters = QStringList{"*.pat"}; const auto midiFilters = QStringList{"*.mid", "*.midi", "*.rmi"}; @@ -1324,7 +1334,7 @@ QString FileItem::defaultFilters() audioFilters.append("*.mp3"); #endif - const auto extensions = projectFilters + presetFilters + soundFontFilters + patchFilters + midiFilters + const auto extensions = projectFilters + presetFilters + soundFontFilters + sfzFilters + patchFilters + midiFilters + vstPluginFilters + audioFilters; return extensions.join(" "); diff --git a/src/gui/editors/TrackContainerView.cpp b/src/gui/editors/TrackContainerView.cpp index 1747e671d51..b413c9f8764 100644 --- a/src/gui/editors/TrackContainerView.cpp +++ b/src/gui/editors/TrackContainerView.cpp @@ -369,7 +369,7 @@ void TrackContainerView::dragEnterEvent( QDragEnterEvent * _dee ) { StringPairDrag::processDragEnterEvent( _dee, QString( "presetfile,pluginpresetfile,samplefile,instrument," - "importedproject,soundfontfile,patchfile,vstpluginfile,projectfile," + "importedproject,soundfontfile,sfzfile,patchfile,vstpluginfile,projectfile," "track_%1,track_%2" ). arg( static_cast(Track::Type::Instrument) ). arg( static_cast(Track::Type::Sample) ) ); @@ -401,7 +401,7 @@ void TrackContainerView::dropEvent( QDropEvent * _de ) } else if( type == "samplefile" || type == "pluginpresetfile" || type == "soundfontfile" || type == "vstpluginfile" - || type == "patchfile" ) + || type == "sfzfile" || type == "patchfile" ) { auto it = dynamic_cast(Track::create(Track::Type::Instrument, m_tc)); PluginFactory::PluginInfoAndKey piakn = From d386bc493a80840f6f3fa05de28b19d2e00429dd Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:40:57 -0500 Subject: [PATCH 068/131] Attempt to fix warning --- plugins/SfzSampler/SfzParser.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 63d9684ded9..98edab03136 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -95,7 +95,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou std::vector lineSegments; for (QString segment : line.split(QRegularExpression("\\s"), Qt::SkipEmptyParts)) // Note: Technically by skipping empty parts, double-spaces within names will be lost. Is this okay? I'm not sure. { - if (segment.contains("=") || (segment.front() == "<" && segment.back() == ">")) + if (segment.contains("=") || (segment.front() == '<' && segment.back() == '>')) { // If this is an opcode assignment or a
, go ahead and add it to the list as it is lineSegments.push_back(segment); @@ -133,7 +133,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou for (QString segment : parsedSegments) { // Track whether we are entering a new header region - if (segment.front() == "<" && segment.back() == ">") + if (segment.front() == '<' && segment.back() == '>') { // If we were previously in a , then wrap it up and add it to the output vector if (currentHeader == Header::Region) { outputRegions.emplace_back(currentRegionState); } @@ -273,7 +273,7 @@ QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentD const QString variableValue = segments[2]; defineMap[variableName] = variableValue; // A define variable should probably start with a $ - if (variableName.front() != "$") { qDebug() << "[SFZ Parser] Warning: #define variable name does not start with $:" << line; } + if (variableName.front() != '$') { qDebug() << "[SFZ Parser] Warning: #define variable name does not start with $:" << line; } } else if (line.startsWith("#include ")) { From 4ececef3e74887b887a53c3b3aeb76e275c88896 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:24:06 -0500 Subject: [PATCH 069/131] Don't allow drag/drop onto instrument --- plugins/SfzSampler/SfzSamplerView.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 0ef062e9b9d..0141996a421 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -151,6 +151,7 @@ void SfzSamplerView::openFile() if (openFileDialog.selectedFiles().isEmpty()) { return; } m_instrument->loadFile(openFileDialog.selectedFiles()[0]); // If the file is being loaded due to the user clicking the button (not opening a save file) then we also need to update the midi CC knobs + // TODO this doesn't work with drag/drop for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { m_instrument->m_parentTrack->midiCCModel(i)->setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); @@ -161,6 +162,7 @@ void SfzSamplerView::openFile() void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) { + /* QString value = StringPairDrag::decodeValue(dee); if (value.endsWith(".sfz")) { @@ -169,10 +171,12 @@ void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) return; } dee->ignore(); + */ } void SfzSamplerView::dropEvent(QDropEvent* de) { + /* QString value = StringPairDrag::decodeValue(de); if (value.endsWith(".sfz")) { @@ -181,6 +185,7 @@ void SfzSamplerView::dropEvent(QDropEvent* de) return; } de->ignore(); + */ } void SfzSamplerView::resizeEvent(QResizeEvent* re) From 18328bdd6f0db143f703b4af605cdbfeaa003f5f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:29:56 -0500 Subject: [PATCH 070/131] Attempt to fix make_shared error --- plugins/SfzSampler/SfzSampleBuffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzSampleBuffer.cpp b/plugins/SfzSampler/SfzSampleBuffer.cpp index d0db323c10e..b16fe6e4518 100644 --- a/plugins/SfzSampler/SfzSampleBuffer.cpp +++ b/plugins/SfzSampler/SfzSampleBuffer.cpp @@ -30,7 +30,7 @@ namespace lmms SfzSampleBuffer::SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate) - : m_data(std::make_shared(size)) + : m_data(std::shared_ptr{new float[size][NUM_CHANNELS]}) , m_size(size) , m_sampleRate(sampleRate) { From 0e5844743220b35c8703ee5f5198c2b3f0edd974 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:07:04 -0500 Subject: [PATCH 071/131] Fix drag/drop resetting CC knobs to 0 --- plugins/SfzSampler/SfzSampler.cpp | 23 ++++++++++++++++++----- plugins/SfzSampler/SfzSampler.h | 2 ++ plugins/SfzSampler/SfzSamplerView.cpp | 6 ------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index d960e2bd9fd..a632b80a89b 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -196,8 +196,21 @@ void SfzSampler::recalculateMaxActiveIndex() - void SfzSampler::loadFile(const QString& filePath) +{ + loadSfzFile(filePath); + // Reset the instrument track's midi CC knobs to the defaults of the SFZ + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + m_parentTrack->midiCCModel(i)->setInitValue(m_sfzGlobalState.midiCCValue(i)); + // For some reason it seems calling `setValue` on the CC models doesn't send a midi event to the instrument when doing drag/drop, + // so we also send a trigger to update the region's + processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this + } +} + + +void SfzSampler::loadSfzFile(const QString& filePath) { // Prevent the audio thread from accidentally looping through the regions while they are being edited const auto guard = Engine::audioEngine()->requestChangesGuard(); @@ -251,13 +264,13 @@ void SfzSampler::loadSettings(const QDomElement& element) m_sfzFilePath = element.attribute("sfzfile"); if (!m_sfzFilePath.isEmpty()) { - loadFile(m_sfzFilePath); + // Using `loadSfzFile` instead of `loadFile` to bypass resetting the midi CC knobs + loadSfzFile(m_sfzFilePath); } - // Make sure the midi CC knobs send their current values so that saved presets work normally upon loading + // Sync the internal CC values so that saved presets/projects work normally upon loading for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) { - // TODO should the trigger be passed to the whole instrument or just the global state? We don't really want to trigger any regions which are triggered by cc events (not implemented yet). But this doesn't feel very clean either. - m_sfzGlobalState.processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); + processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); } } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 43b49c842e5..4d291d24ee1 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -58,6 +58,8 @@ class SfzSampler : public Instrument void loadSettings(const QDomElement& element) override; void loadFile(const QString& filePath) override; + //! This extra function for loading files is needed so that we can choose only to reset the CC knobs when the user is loading a new file, not a preset/project + void loadSfzFile(const QString& filePath); QString nodeName() const override; gui::PluginView* instantiateView(QWidget* parent) override; diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 0141996a421..3e08ecc65a0 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -150,12 +150,6 @@ void SfzSamplerView::openFile() { if (openFileDialog.selectedFiles().isEmpty()) { return; } m_instrument->loadFile(openFileDialog.selectedFiles()[0]); - // If the file is being loaded due to the user clicking the button (not opening a save file) then we also need to update the midi CC knobs - // TODO this doesn't work with drag/drop - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) - { - m_instrument->m_parentTrack->midiCCModel(i)->setValue(m_instrument->m_sfzGlobalState.midiCCValue(i), true); - } } } From e6e6d4c4494d2bcad6d262c66733aaa6cbc9ebe9 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:57:18 -0500 Subject: [PATCH 072/131] Add more comments --- plugins/SfzSampler/SfzControlsConfig.h | 24 ++++++++++++++++++++++++ plugins/SfzSampler/SfzGlobalState.h | 13 +++++++------ plugins/SfzSampler/SfzOpcodeState.h | 21 +++++++++++++++++---- plugins/SfzSampler/SfzParser.h | 5 ++++- plugins/SfzSampler/SfzRegionManager.h | 4 ++++ plugins/SfzSampler/SfzRegionPlayState.h | 3 ++- plugins/SfzSampler/SfzSampleBuffer.h | 9 +++++++++ plugins/SfzSampler/SfzSamplePool.h | 1 + plugins/SfzSampler/SfzSampler.h | 1 + plugins/SfzSampler/SfzSamplerView.h | 4 ++++ plugins/SfzSampler/SfzTrigger.h | 3 +++ 11 files changed, 76 insertions(+), 12 deletions(-) diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzSampler/SfzControlsConfig.h index 3690f1433e4..c230017beff 100644 --- a/plugins/SfzSampler/SfzControlsConfig.h +++ b/plugins/SfzSampler/SfzControlsConfig.h @@ -1,3 +1,27 @@ +/* + * SfzControlsConfig.h - Helper class for storing info about which keyswitches exist, and which midi CC's are used by the sfz + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + #ifndef LMMS_SFZ_CONTROLS_CONFIG_H #define LMMS_SFZ_CONTROLS_CONFIG_H diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index ec57d843241..bcb424c7f84 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -43,20 +43,21 @@ class SfzGlobalState void processTrigger(const SfzTrigger& trigger); //! Handles updating m_lastPlayedSwitchKeys whenever a key defined by sw_last is pressed + // TODO move this inside of processTrigger so that fewer regions need to be looped over void switchKeyPressed(const int key); - // Returns the midi key number of the last pressed key in the range [lowKey, highKey]. - // If switchKeysOnly is true, it uses the m_lastPlayedSwitchKeys array which only tracks when valid switch keys have been pressed, not just any key. - // If no keys have been pressed yet, it returns defaultKey. - // If the key range is invalid, it returns defaultKey - // If no default key was given, it returns std::nullopt + //! Returns the midi key number of the last pressed key in the range [lowKey, highKey]. + //! If switchKeysOnly is true, it uses the m_lastPlayedSwitchKeys array which only tracks when valid switch keys have been pressed, not just any key. + //! If no keys have been pressed yet, it returns defaultKey. + //! If the key range is invalid, it returns defaultKey + //! If no default key was given, it returns std::nullopt // TODO add unit tests std::optional lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey, bool switchKeysOnly = false) const; //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } - // Sets initial values for the controls, based on any `set_ccN` opcodes in the header + //! Sets initial values for the controls, based on any `set_ccN` opcodes in the header void initializeMidiCCValues(const SfzOpcodeState& controlsConfig); private: diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index de122b2ea3c..dc9a13b4f0c 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -25,7 +25,6 @@ #ifndef LMMS_SFZ_OPCODE_STATE_H #define LMMS_SFZ_OPCODE_STATE_H -//#include "SfzOpcodes.h" #include #include #include @@ -38,14 +37,28 @@ class SfzOpcodeState public: SfzOpcodeState(); // We need a constructor to initialize things like m_hicc + //! Given an opcode assignment such as `lokey=45`, passing "m_lokey" and "45" to this function + //! will take those values and figure out if "lokey" is a valid opcode, and whether "45" is a valid value for it. + //! If so, it will set the internal member variable corresponding to it, and return false. + //! If it was unsucessful, it will print an error message and return false. bool setOpcodeByStrings(const QString& name, const QString& value); + + //! Helper function for converting strings like "A5" or "B#2" into integers representing keys on the midi keyboard. static int stringToKeyNum(QString keyString, bool* successful); + //! Helper function for converting integers like 24 or 45 representing midi keys back into strings representing the key and octave on the keyboard (essentially the inverse of `stringToKeyNum`) static QString keyNumToString(int keyNum); + //! Helper function for taking an opcode name such as "set_cc45" and extracting the number "45" from it. + //! If the string does not contain a substring in the form "ccN" where N is a number, it will return print an error and return 0. + //! Likewise, if the number is outside the range 0-127, it will print an error and return 0. static int ccNumberFromOpcode(const QString& opcode); // Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. static constexpr const int NumMidiCCs = 128; + /***********************************************************************/ + // SFZ OPCODE DEFINITIONS + /***********************************************************************/ + // // File Paths // @@ -67,7 +80,7 @@ class SfzOpcodeState TriggerType m_trigger = TriggerType::Attack; // - // Key Trigger + // Key Conditions // int m_lokey = 0; int m_hikey = 127; @@ -82,13 +95,13 @@ class SfzOpcodeState std::optional m_sw_label = std::nullopt; // - // Velocity Trigger + // Velocity Conditions // int m_lovel = 0; int m_hivel = 127; // - // Round Robin Trigger + // Round Robin Conditions // int m_seq_length = 1; int m_seq_position = 1; diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index c03f1b018c6..d732e355cf6 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -37,6 +37,9 @@ namespace lmms class SfzParser { public: + //! Loads the .sfz file at `filePath`, parses it for all the sfz headers and opcode assignments, and populates the `outputRegions` vector + //! with new SfzRegion objects based on the configurations in the .sfz file. Additionally, it sets up `controlsConfig` with misc info about + //! switch keys and midi CC's, which is useful for the GUI. static bool parseSfzFile(const QString& filePath, std::vector& outputRegions, SfzControlsConfig& controlsConfig); private: @@ -51,7 +54,7 @@ class SfzParser Group, Region, Control, - Curve, + Curve, // TODO Not implemented yet None }; }; diff --git a/plugins/SfzSampler/SfzRegionManager.h b/plugins/SfzSampler/SfzRegionManager.h index 19e662b25db..893dda01339 100644 --- a/plugins/SfzSampler/SfzRegionManager.h +++ b/plugins/SfzSampler/SfzRegionManager.h @@ -41,6 +41,8 @@ class SfzRegionManager //! This is not optimal. //! Instead, because there are only 128 keys, we can construct a mapping, where each key is paired with a list of regions where lokey/hikey matches it //! Doing this for both NoteOn and NoteOff events means having 256 lists of pointers to regions, which is not bad. + //! That way, whenever a note is pressed, you can simply look up in the table a list of all the regions which might match. + //! This is usually 10-100x fewer regions than the total, which is much faster to loop over. SfzRegionManager() = default; SfzRegionManager(std::vector& regions); @@ -48,9 +50,11 @@ class SfzRegionManager //! This is meant to intelligently narrow down the number of regions where we manually have to loop over and check const std::vector& findPotentialMatchingRegions(const SfzTrigger& trigger) const; + //! Returns a vector of pointers for all the regions, in case any code needs to loop over all of them. const std::vector& allRegions() const { return m_allRegions; } private: + //! Stores the actual region objects std::vector m_regions; std::vector m_allRegions; diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 00e7c09e217..3881f1cc499 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -42,7 +42,7 @@ class SfzRegionPlayState { public: SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger); - SfzRegionPlayState() = default; // Needed to initialize array + SfzRegionPlayState() = default; // The default constructor is needed to initialize arrays of the object //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. //! Returns true if any sound was generated, false if the buffer is left untouched @@ -51,6 +51,7 @@ class SfzRegionPlayState //! Handle incoming event to decide whether to deactivate/release void processTrigger(const SfzTrigger& trigger); + //! Returns whether this voice is done playing or not. If m_active is false, this voice object is avaiable to be overwritten and used as a new voice. bool active() const { return m_active; } //! Helper function for calculating the envelope value at the current frame diff --git a/plugins/SfzSampler/SfzSampleBuffer.h b/plugins/SfzSampler/SfzSampleBuffer.h index d740b87c889..05e3ca5dda2 100644 --- a/plugins/SfzSampler/SfzSampleBuffer.h +++ b/plugins/SfzSampler/SfzSampleBuffer.h @@ -39,15 +39,24 @@ class SfzSampleBuffer SfzSampleBuffer() = default; SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate); + //! Returns a hermite-interpolated value for the data at the given sample index and channel. + //! The interpolation is so that pitch shifting and resampling is as easy as possible + //! If index is out of range, this function returns 0.0f float at(const float index, const size_t channel) const; + //! Returns the number of frames in the sample. f_cnt_t size() const { return m_size; } + //! Returns the sample rate of the audio float sampleRate() const { return m_sampleRate; } private: static constexpr const size_t NUM_CHANNELS = 2; + //! The raw sample data is stored as a shared pointer to a 2d array of floats. + //! This was originally done so that it would not be copied unnecessarily if, for example, the vector or map containing the SfzSampleBuffer were resized/reallocated + //! TODO: is this optimal? std::shared_ptr m_data; + f_cnt_t m_size; float m_sampleRate; }; diff --git a/plugins/SfzSampler/SfzSamplePool.h b/plugins/SfzSampler/SfzSamplePool.h index a3488b1bf8b..874c48c78f1 100644 --- a/plugins/SfzSampler/SfzSamplePool.h +++ b/plugins/SfzSampler/SfzSamplePool.h @@ -38,6 +38,7 @@ class SfzSamplePool public: SfzSampleBuffer* const loadSample(const QString& path); + //! Returns the number of samples currently loaded in the pool const int sampleCount() const { return m_samplePool.size(); } private: diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 4d291d24ee1..a03357185e5 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -99,6 +99,7 @@ class SfzSampler : public Instrument InstrumentTrack* m_parentTrack; + //! The path to the currently loaded SFZ file QString m_sfzFilePath = ""; friend class gui::SfzSamplerView; diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index dc02daab447..ec6bcec27c5 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -36,6 +36,10 @@ class SfzSampler; namespace gui { + +//! This class is planned to be completely redone! +//! In it's current form, it's simply a temporary GUI which shows the keyswitch info, the parameters, and a button to load a file. +//! In the future, it would be great to have a much prettier and useful GUI class SfzSamplerView : public InstrumentView { Q_OBJECT diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzSampler/SfzTrigger.h index 95e37360494..a754018f895 100644 --- a/plugins/SfzSampler/SfzTrigger.h +++ b/plugins/SfzSampler/SfzTrigger.h @@ -34,6 +34,7 @@ namespace lmms class SfzTrigger { public: + //! Helper functions for creating different types of events static const SfzTrigger noteOnEvent(const int frameOffset, const int key, const int vel); static const SfzTrigger noteOffEvent(const int frameOffset, const int key, const int vel); static const SfzTrigger controlChangeEvent(const int frameOffset, const int controlNumber, const int value); @@ -54,10 +55,12 @@ class SfzTrigger private: Type m_type; + // TODO should these be optionals or not? std::optional m_key = std::nullopt; std::optional m_velocity = std::nullopt; std::optional m_controlChangeNumber = std::nullopt; std::optional m_controlChangeValue = std::nullopt; + //! The event probably occurred partway through a buffer, so this variable keeps track of when exactly it occurred relative to the start of the buffer int m_frameOffset = 0; }; From f38572d82d72459aa7277cbb603634df25234f6a Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:07:42 -0500 Subject: [PATCH 073/131] Add header --- plugins/SfzSampler/SfzParser.cpp | 30 ++++++++++++++++++++++++++---- plugins/SfzSampler/SfzParser.h | 3 ++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 98edab03136..aa2d09a15ae 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -123,8 +123,11 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // Create a base opcode state list to keep track of which defaults are SfzOpcodeState globalState; - // Regions can be within s, which can define default opcodes for all the regions inside them + // Regions can be further organized within groups, which can define default opcodes for all the regions inside them // Keep track of the current group and region states. These will be re-initialized to whatever the global/group settings are, once we encounter a new group or region header + // (Also, ARIA supports an additional header, which is essentially the same as a group, but one level up, so you can group your groups under masters. Not a whole lot of SFZ's use it, but some do.) + // (The heirarchy of headers essentially goes: , , , ) + SfzOpcodeState currentMasterState = globalState; SfzOpcodeState currentGroupState = globalState; SfzOpcodeState currentRegionState = globalState; @@ -145,14 +148,27 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // TODO is this correct? globalState = SfzOpcodeState(); } + else if (segment == "") + { + currentHeader = Header::Master; + // Reset the current master settings to the global defaults + currentMasterState = globalState; + } else if (segment == "") { + // If the SFZ file skipped over the heirarchy of global->master->group->region and just went global->group, we need to make sure + // the master state is also updated + if (currentHeader == Header::Global) { currentMasterState = globalState; } currentHeader = Header::Group; - // Reset the current group settings to the global defaults - currentGroupState = globalState; + // Reset the current group settings to the master defaults + currentGroupState = currentMasterState; } else if (segment == "") { + // If the SFZ file skipped directly from to , make sure to update the and states in between + if (currentHeader == Header::Global) { currentMasterState = globalState; currentGroupState = currentMasterState; } + // Or if it skipped from to , just update the state + if (currentHeader == Header::Master) { currentGroupState = currentMasterState; } currentHeader = Header::Region; // Reset the current region settings to the group defaults currentRegionState = currentGroupState; @@ -182,10 +198,16 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou { case Header::Global: { - // If we are in the global header, update the opcodes of the global state, which will propagate down to any groups or regions after it + // If we are in the global header, update the opcodes of the global state, which will propagate down to any groups or regions (or master, if the sfz uses any) after it globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); break; } + case Header::Master: + { + // If we are in the master header, update the opcodes of the master state, which will similarly propagate down to any groups or regions after it + currentMasterState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + break; + } case Header::Group: { // If we are in a group, update the opcodes of the current group state, which will propagate to any regions below it diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index d732e355cf6..b6c1a6e7a1e 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -50,10 +50,11 @@ class SfzParser //! All the possible header types in an sfz file enum class Header { + Control, Global, + Master, Group, Region, - Control, Curve, // TODO Not implemented yet None }; From f3220080d06793bda0c243ed529bfad39a886494 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:17:29 -0500 Subject: [PATCH 074/131] Make default_path propagate correctly --- plugins/SfzSampler/SfzParser.cpp | 8 ++++++++ plugins/SfzSampler/SfzRegion.cpp | 3 ++- plugins/SfzSampler/SfzSampler.cpp | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index aa2d09a15ae..94c103ac1e5 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -223,6 +223,14 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou case Header::Control: { controlsConfig.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + // For some reason, one of the things in the controls header is `default_path`, which is supposed to propagate down to the regions below. This doesn't feel very control-related, but it is what it is. + // To do this, we also force-set the `default_path` for the current , , and , to make sure it reaches all future regions (until another header changes it) + if (opcodeNameAndValue[0] == "default_path") + { + globalState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + currentMasterState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + currentGroupState.setOpcodeByStrings(opcodeNameAndValue[0], opcodeNameAndValue[1]); + } break; } case Header::Curve: diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index b259dbf5be9..85b1c9a4c1a 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -142,7 +142,8 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam return false; } - QString path = parentDirectory.absoluteFilePath(m_sampleFile.value()); + QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value_or(""))); + QString path = defaultDirectory.absoluteFilePath(m_sampleFile.value()); // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path); diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index a632b80a89b..3357972cb0f 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -233,7 +233,7 @@ void SfzSampler::loadSfzFile(const QString& filePath) // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: - QDir parentDirectory = QDir(QFileInfo(filePath).absoluteDir().absoluteFilePath(m_controlsConfig.m_default_path.value_or(""))); + QDir parentDirectory = QFileInfo(filePath).absoluteDir(); // Reset any loaded samples m_samplePool = SfzSamplePool(); int i = 0; From 65f80928e926362570322e4ce1b2b0014d588f12 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:27:43 -0500 Subject: [PATCH 075/131] Fix edge cases for opcode state propagation --- plugins/SfzSampler/SfzParser.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 94c103ac1e5..f8e429adc91 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -138,7 +138,13 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // Track whether we are entering a new header region if (segment.front() == '<' && segment.back() == '>') { - // If we were previously in a , then wrap it up and add it to the output vector + // If we were previously in , propagate down the current defaults to any future , , or headers + if (currentHeader == Header::Global) { currentMasterState = currentGroupState = currentRegionState = globalState; } + // If we were previously in a , do the same for and + if (currentHeader == Header::Master) { currentGroupState = currentRegionState = currentMasterState; } + // If we were previously in a , do the same for + if (currentHeader == Header::Group) { currentRegionState = currentGroupState; } + // If we were previously in a , wrap it up and add it to the output vector if (currentHeader == Header::Region) { outputRegions.emplace_back(currentRegionState); } if (segment == "") @@ -147,6 +153,8 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // If we are entering a new global header, reset the current global settings // TODO is this correct? globalState = SfzOpcodeState(); + // (also real quick, because the header sets the `default_path` opcode but might come before any header, we have to also set it here just in case) + globalState.m_default_path = controlsConfig.m_default_path; } else if (segment == "") { @@ -156,19 +164,12 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou } else if (segment == "") { - // If the SFZ file skipped over the heirarchy of global->master->group->region and just went global->group, we need to make sure - // the master state is also updated - if (currentHeader == Header::Global) { currentMasterState = globalState; } currentHeader = Header::Group; // Reset the current group settings to the master defaults currentGroupState = currentMasterState; } else if (segment == "") { - // If the SFZ file skipped directly from to , make sure to update the and states in between - if (currentHeader == Header::Global) { currentMasterState = globalState; currentGroupState = currentMasterState; } - // Or if it skipped from to , just update the state - if (currentHeader == Header::Master) { currentGroupState = currentMasterState; } currentHeader = Header::Region; // Reset the current region settings to the group defaults currentRegionState = currentGroupState; From 7bd88d14223a8cb3171d3fd4dc8fd1ad9e90997e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:06:17 -0500 Subject: [PATCH 076/131] Make #defines persist across recursion branches --- plugins/SfzSampler/SfzParser.cpp | 5 +++-- plugins/SfzSampler/SfzParser.h | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index f8e429adc91..8c7aff29c69 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -51,7 +51,8 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // Before parsing the headers and opcodes, we need to hande #include and #define statements // This amounts to recursively loading and copy/pasting the contents of the other files where the #include is, and find/replacing the #define words with their values - fileContents = recursiveHandleIncludeAndDefineStatements(parentDirectory, fileContents); + std::map defineMap = {}; // Just a temporary helper variable so that the recursion branches have a persistance object to keep track of #defines + fileContents = recursiveHandleIncludeAndDefineStatements(parentDirectory, fileContents, defineMap); qDebug().noquote() << "[SFZ Parser] DEBUG: Preprocessed SFZ file contents:\n" << fileContents; // testing @@ -283,7 +284,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou -QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map defineMap) +QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map& defineMap) { // Reconstruct the file line by line as we parse the defines and includes QStringList reconstructedSegments; diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index b6c1a6e7a1e..3f3bae4dde2 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -44,8 +44,9 @@ class SfzParser private: //! Helper function to parse any #include or #define statements from a string, recursively so that the included files have their includes and defines handled too - //! The defineMap parameter should be left blank, since it's only there to internally keep track of what $keywords are defined to be what as the recursion goes down each path - static QString recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map defineMap = {}); + //! The defineMap parameter should be empty {} when initially called; it internally keeps track of what $keywords are defined to be what as the recursion goes down each path + //! The defineMap is passes as a reference so that it persists between recursion branches. This doesn't feel quite right, but some SFZ's use it to, for instance, #define the sample file extensions (.flac, .wav, etc) in a separate #include file, and expect that #define to apply to the remainder of the file. + static QString recursiveHandleIncludeAndDefineStatements(const QDir& parentDirectory, QString fileContents, std::map& defineMap); //! All the possible header types in an sfz file enum class Header From 6b6792838439361c1df8ba064fb37c011e3644ee Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:14:20 -0500 Subject: [PATCH 077/131] Add debug statements for region lookup tables --- plugins/SfzSampler/SfzRegionManager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index 3c09b5974e1..9f8768e3270 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -24,6 +24,7 @@ #include "SfzRegionManager.h" +#include namespace lmms { @@ -32,6 +33,7 @@ namespace lmms SfzRegionManager::SfzRegionManager(std::vector& regions) : m_regions(regions) { + qDebug() << "[SFZ Player] Constructing key/region lookup tables..."; // For each key, figure out what list of regions could include it, to make a kind of look-up table for (int key = 0; key < 128; ++key) { @@ -51,6 +53,7 @@ SfzRegionManager::SfzRegionManager(std::vector& regions) } } } + qDebug() << "[SFZ Player] Key" << key << ":" << m_noteOnRegions.at(key).size() << "NoteOn regions," << m_noteOffRegions.at(key).size() << "NoteOff regions"; } // Unfortunately, it's more difficult to make lookup tables for midi CC events. Instead, just use a big vector with all the regions by default From 2fed7af15f56f7fedf005dfa078eec7f5d58a534 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:42:46 -0500 Subject: [PATCH 078/131] Add lorand/hirand opcodes --- plugins/SfzSampler/SfzGlobalState.cpp | 3 +++ plugins/SfzSampler/SfzGlobalState.h | 6 ++++++ plugins/SfzSampler/SfzOpcodeState.cpp | 19 ++++++++++++++++--- plugins/SfzSampler/SfzOpcodeState.h | 6 ++++++ plugins/SfzSampler/SfzRegion.cpp | 4 ++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 24b777dcec9..97d999fbe38 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -25,6 +25,7 @@ #include "SfzGlobalState.h" +#include "lmms_math.h" namespace lmms { @@ -45,6 +46,8 @@ void SfzGlobalState::processTrigger(const SfzTrigger& trigger) { m_activeKeys.at(trigger.key().value()) = false; } + // Update the current random value + m_rand = fastRand(1.0f); } void SfzGlobalState::switchKeyPressed(const int key) diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index bcb424c7f84..b56d51a3016 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -57,6 +57,9 @@ class SfzGlobalState //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } + //! Returns the random value for the current trigger + const float rand() const { return m_rand; } + //! Sets initial values for the controls, based on any `set_ccN` opcodes in the header void initializeMidiCCValues(const SfzOpcodeState& controlsConfig); @@ -73,6 +76,9 @@ class SfzGlobalState //! Keeps track of which keys on the piano are currently being pressed std::array m_activeKeys = {}; + //! Keeps track of the current random value, updated every trigger. Some regions use lorand/hirand opcodes to decide randomy whether to activate or not, and this is the random value they will compare to. + float m_rand = 0.0f; + //! Stores the current value of all the midi CC knobs/controllers //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 127 to get a float between 0 and 1 works fine. //! std::optional is used so that sfz file can specify default cc values diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 951c6e4989e..179fc99aacb 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -74,7 +74,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // - // Key Trigger + // Key Conditions // else if (name == "lokey") { @@ -124,7 +124,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // - // Velocity Trigger + // Velocity Conditions // else if (name == "lovel") { @@ -137,7 +137,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // - // Round Robin Trigger + // Round Robin Conditions // else if (name == "seq_length") { @@ -149,6 +149,19 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } + // + // Random Conditions + // + else if (name == "lorand") + { + m_lorand = value.toFloat(&successful); + } + else if (name == "hirand") + { + m_hirand = value.toFloat(&successful); + } + + // // Sample playback options // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index dc9a13b4f0c..437058249af 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -106,6 +106,12 @@ class SfzOpcodeState int m_seq_length = 1; int m_seq_position = 1; + // + // Random Conditions + // + float m_lorand = 0.0f; + float m_hirand = 1.0f; + // // Sample playback diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 85b1c9a4c1a..d938b3f5155 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -72,6 +72,10 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default, true) != m_sw_last) { return false; } } + // If the region uses lorand/hirand, the current random value stored in the global state (updated every trigger) is compared with the range + // Note: the upper bound is inclusive, so lorand=0.2 hirand=0.4 will be triggered by any rand value >0.2 or <=0.4 + if (globalState.rand() > m_hirand || globalState.rand() <= m_lorand) { return false; } + // If midi CC ranges are defined, make sure the current CC values are within range // Only loop over the CC's which have lo/hiccN defined, instead of checking all 128 every time for (const int i : m_lohiccDefinedCCNumbers) From 201b435c99995fcfa08ae4f254c6d409bc96b9c8 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:54:59 -0500 Subject: [PATCH 079/131] Add pan_oncc/pan_cc opcodes --- plugins/SfzSampler/SfzOpcodeState.cpp | 4 ++++ plugins/SfzSampler/SfzOpcodeState.h | 1 + plugins/SfzSampler/SfzRegion.cpp | 1 + plugins/SfzSampler/SfzRegion.h | 1 + plugins/SfzSampler/SfzRegionPlayState.cpp | 2 +- 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 179fc99aacb..933813ffb2c 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -324,6 +324,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_pan = value.toFloat(&successful); } + else if (name.startsWith("pan_oncc") || name.startsWith("pan_cc")) + { + m_pan_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); + } // diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 437058249af..0dc6891c1f9 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -202,6 +202,7 @@ class SfzOpcodeState // Gain is the same as volume. Some opcodes use the word volume, some use gain, both are decibals std::array m_gain_oncc = {}; float m_pan = 0.0f; + std::array m_pan_oncc = {}; // diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index d938b3f5155..2b0a84236ab 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -132,6 +132,7 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) m_ampeg_release_totalCC = totalCCModulation(m_ampeg_release_oncc, globalState); m_gain_totalCC = totalCCModulation(m_gain_oncc, globalState); + m_pan_totalCC = totalCCModulation(m_pan_oncc, globalState); // TODO more } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 35bcba3ba49..1e38455bb94 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -79,6 +79,7 @@ class SfzRegion : public SfzOpcodeState float m_ampeg_release_totalCC = 0.0f; float m_gain_totalCC = 0.0f; + float m_pan_totalCC = 0.0f; //! When checking whether all the current CC values fall between loccN and hiccN, it's useful to only check the ones where lo/hiccN is actually defined, not all 128 //! This vector stores a list of which CC numbers have lo/hiccN opcodes in this region diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 698765e76ec..8f0fac9c7a5 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -171,7 +171,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const float ampVolume = dbfsToAmp(m_region->m_volume + m_region->m_gain_totalCC); // Panning - const float pan = m_region->m_pan / 100; + const float pan = (m_region->m_pan + m_region->m_pan_totalCC) / 100; const float rightPanAmp = std::min(1.0f, 1.0f + pan); const float leftPanAmp = std::min(1.0f, 1.0f - pan); From 3d802bfe65d1d8bc50d4ff9aa57e3c3ab8a2e7e8 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:38:20 -0500 Subject: [PATCH 080/131] Add dummy support for header --- plugins/SfzSampler/SfzParser.cpp | 9 +++++++++ plugins/SfzSampler/SfzParser.h | 1 + 2 files changed, 10 insertions(+) diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 8c7aff29c69..3078d148c00 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -183,6 +183,10 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou { currentHeader = Header::Curve; } + else if (segment == "") + { + currentHeader = Header::Effect; + } else { qDebug() << "[SFZ Parser] Error, unknown header type:" << segment; @@ -240,6 +244,11 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou qDebug() << "[SFZ Parser] Warning: The header has not been implemented yet. Encountered opcode assignment:" << segment; break; } + case Header::Effect: + { + qDebug() << "[SFZ Parser] Warning: The header has not been implemented yet. Encountered opcode assignment:" << segment; + break; + } default: { qDebug() << "[SFZ Parser] Error: Encountered line within invalid header" << segment; diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index 3f3bae4dde2..9522aa4fe90 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -57,6 +57,7 @@ class SfzParser Group, Region, Curve, // TODO Not implemented yet + Effect, // TODO Not implemented yet None }; }; From 715c40f33b0ff72eb98176bc21ff8d6cf1a97688 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:52:24 -0400 Subject: [PATCH 081/131] Temporary commit, refactoring opcode system --- plugins/SfzSampler/SfzGlobalState.h | 2 + plugins/SfzSampler/SfzOpcodeState.cpp | 117 +++++++++++++++++- plugins/SfzSampler/SfzOpcodeState.h | 140 ++++++++++++++++++---- plugins/SfzSampler/SfzOpcodes.cpp | 23 ++++ plugins/SfzSampler/SfzOpcodes.h | 36 ++++++ plugins/SfzSampler/SfzRegion.cpp | 15 ++- plugins/SfzSampler/SfzRegionManager.cpp | 2 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 12 +- plugins/SfzSampler/SfzSampler.cpp | 4 +- 9 files changed, 308 insertions(+), 43 deletions(-) create mode 100644 plugins/SfzSampler/SfzOpcodes.cpp create mode 100644 plugins/SfzSampler/SfzOpcodes.h diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index b56d51a3016..62588306391 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -56,6 +56,8 @@ class SfzGlobalState //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } + //! Returns a const array of the current CC knob values + const std::array midiCCValues() const { return m_ccValues; } //! Returns the random value for the current trigger const float rand() const { return m_rand; } diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 933813ffb2c..84ddc9fc278 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -42,16 +42,17 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { bool successful = true; + m_ampeg.parseEnvelopeGeneratorOpcode(name, value, &successful); // // File paths // if (name == "sample") { - m_sampleFile = value; + m_sampleFile.setValue(value); } else if (name == "default_path") { - m_default_path = value; + m_default_path.setValue(value); } @@ -60,8 +61,8 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // else if (name == "trigger") { - if (value == "attack") { m_trigger = TriggerType::Attack; } - else if (value == "release") { m_trigger = TriggerType::Release; } + if (value == "attack") { m_trigger.setValue(TriggerType::Attack); } + else if (value == "release") { m_trigger.setValue(TriggerType::Release); } //else if (value == "first") { m_trigger = TriggerType::First; } // TODO To be implemented //else if (value == "legato") { m_trigger = TriggerType::Legato; } // TODO To be implemented //else if (value == "release_key") { m_trigger = TriggerType::ReleaseKey; } // TODO To be implemented @@ -269,6 +270,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu // // Amplitude Envelope Generator (ampeg) // + /*TODO else if (name == "ampeg_delay") { m_ampeg_delay = value.toFloat(&successful); } else if (name == "ampeg_attack") { m_ampeg_attack = value.toFloat(&successful); } else if (name == "ampeg_hold") { m_ampeg_hold = value.toFloat(&successful); } @@ -307,6 +309,7 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu { m_ampeg_release_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); } + */ // @@ -474,4 +477,110 @@ QString SfzOpcodeState::keyNumToString(int keyNum) } +template<> +void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) +{ + if (m_opcodeName != opcodeName) { return; } + m_value = opcodeValue.toFloat(successful); + qDebug() << "Float opcode" << m_opcodeName << "set to" << m_value; +} + +template<> +void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) +{ + if (m_opcodeName != opcodeName) { return; } + m_value = opcodeValue.toInt(successful); + // Integer opcodes can also define keys, which can be specified either by a key number of by a string like e4 or c#5 + if (!successful) { m_value = stringToKeyNum(opcodeValue, successful); } +} + +template<> +void SfzOpcodeState::Opcode>::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) +{ + if (m_opcodeName != opcodeName) { return; } + m_value = opcodeValue; + *successful = true; +} + +// Modulatable Opcodes are a type of float opcode which can be adjusted based on different midi CC knob values +// They have both an initial value defined by their opcodename=value +// but also opcodename_oncc123=modulation_amount +// or opcodenamecc123=modulation_amount +void SfzOpcodeState::ModulatableOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) +{ + if (opcodeName == m_opcodeName) + { + m_value = opcodeValue.toFloat(successful); + qDebug() << "ModulatableOpcode" << m_opcodeName << "set to" << m_value; + } + else if (opcodeName.startsWith(m_opcodeName + "_oncc") || opcodeName.startsWith(m_opcodeName + "cc")) + { + value_oncc.at(ccNumberFromOpcode(opcodeName)) = opcodeValue.toFloat(successful); + qDebug() << "ModulatableOpcode" << m_opcodeName << "set cc mod" << ccNumberFromOpcode(opcodeName) << value_oncc.at(ccNumberFromOpcode(opcodeName)); + } +} + + +// The modulation of Modulatable opcodes can change whenever a midi CC knob changes. There are 128 of them, +// which is a bit much to keep checking every single frame when generating audio. +// Instead, the current modulation value is only calculated and cached each time a midi CC event is sent, which saves a lot of compute. +void SfzOpcodeState::ModulatableOpcode::updateCachedModulation(const std::array& ccValues) +{ + cachedModulation = 0.0f; + // TODO this may be optimized if we only loop through the CC's which are actually used by the region + for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + { + cachedModulation += value_oncc.at(i) * ccValues.at(i) / 128.0f; + } +} + + + + +// Envelope Generator Opcodes +void SfzOpcodeState::EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* successful) +{ + // Pass the opcode name/value to all of the bundled opcodes. + // If it matches one of them, that's great. If not, they will return and nothing will happen. + // TODO is this error handling with "successful" correct? + delay.parseFromString(opcode, value, successful); + attack.parseFromString(opcode, value, successful); + hold.parseFromString(opcode, value, successful); + decay.parseFromString(opcode, value, successful); + sustain.parseFromString(opcode, value, successful); + release.parseFromString(opcode, value, successful); + + vel2delay.parseFromString(opcode, value, successful); + vel2attack.parseFromString(opcode, value, successful); + vel2hold.parseFromString(opcode, value, successful); + vel2decay.parseFromString(opcode, value, successful); + vel2sustain.parseFromString(opcode, value, successful); + vel2release.parseFromString(opcode, value, successful); +} + + + + +// +// Bespoke Enum-like Opcodes +// + +template<> +void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) +{ + if (m_opcodeName != opcodeName) { return; } + + if (opcodeValue == "attack") { m_value = TriggerType::Attack; } + else if (opcodeValue == "release") { m_value = TriggerType::Release; } + //else if (value == "first") { m_trigger = TriggerType::First; } // TODO To be implemented + //else if (value == "legato") { m_trigger = TriggerType::Legato; } // TODO To be implemented + //else if (value == "release_key") { m_trigger = TriggerType::ReleaseKey; } // TODO To be implemented + else + { + qDebug() << "[SFZ Parser] Unknown trigger:" << opcodeValue; + return; + } + *successful = true; +} + } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 0dc6891c1f9..ee4bb5fd354 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -39,7 +39,7 @@ class SfzOpcodeState //! Given an opcode assignment such as `lokey=45`, passing "m_lokey" and "45" to this function //! will take those values and figure out if "lokey" is a valid opcode, and whether "45" is a valid value for it. - //! If so, it will set the internal member variable corresponding to it, and return false. + //! If so, it will set the internal member variable corresponding to it, and return true. //! If it was unsucessful, it will print an error message and return false. bool setOpcodeByStrings(const QString& name, const QString& value); @@ -55,6 +55,109 @@ class SfzOpcodeState // Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. static constexpr const int NumMidiCCs = 128; + + /* Helper structs for opcodes */ + + // A base struct for all opcodes, just a name and a value. + template + struct Opcode + { + QString m_opcodeName; + T m_value; + + Opcode(QString name, T defaultValue) : m_opcodeName(name), m_value(defaultValue) {} + void setValue(const T& value) { m_value = value; } + const T& value() const { return m_value; } + + // Function for taking in a string like "pitch_keytrack=1200", split into name/value as "pitch_keytrack", "1200", and updating the value if the name matches. + virtual void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) {}; + }; + + // Float/decimal opcodes (such as amplitude, panning, etc) + struct FloatOpcode : Opcode + { + FloatOpcode(QString name = "", float defaultValue = 0.0f) : Opcode(name, defaultValue) {}; + //void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; + }; + + // Key opcodes (such as lokey, hikey, pitch_keycenter, etc) + struct KeyOpcode : Opcode + { + KeyOpcode(QString name = "", int defaultValue = 0) : Opcode(name, defaultValue) {}; + //void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; + }; + + // String opcodes (such as sample file path) + struct StringOpcode : Opcode> + { + StringOpcode(QString name, std::optional defaultValue) : Opcode>(name, defaultValue) {}; + //void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; + }; + + // Many opcodes can be modulated by MIDI CC knobs, so each of them needs an array to store the modulation weights for each one. + struct ModulatableOpcode : FloatOpcode + { + std::array value_oncc = {}; + //! Store the current total midi CC modulation amounts for the different targets, just so that we don't + // have to recalculate them every buffer, instead only when a trigger occurs. + float cachedModulation = 0.0f; + + ModulatableOpcode(QString name = "", float defaultValue = 0.0f) : FloatOpcode(name, defaultValue) {}; + // Helper function for parsing these kinds of opcodes, where you have both `opcode` and `opcode_onccN` where N is the midi cc number, so + // that the code isn't duplicated for every modulatable parameter. + void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; + // Helper function to update the cached CC modulation amount every time a midi Control Change trigger occurs + void updateCachedModulation(const std::array& ccValues); + }; + + // Additionally, things like amplitude, pitch, and filter freq envelopes and lfo's all have very similar parameters, so it makes sense to put + // them all together in a handy definition + struct EnvelopeOpcodes + { + // Envelope parameters (these can be modulated by midi CCs) + ModulatableOpcode delay; + ModulatableOpcode attack; + ModulatableOpcode hold; + ModulatableOpcode decay; + ModulatableOpcode sustain; + ModulatableOpcode release; + // Velocity modulation amount + FloatOpcode vel2delay; + FloatOpcode vel2attack; + FloatOpcode vel2hold; + FloatOpcode vel2decay; + FloatOpcode vel2sustain; + FloatOpcode vel2release; + + // Initialize opcodes with correct names and default values + EnvelopeOpcodes(QString name) + : delay(name + "_delay", 0.0f) + , attack(name + "_attack", 0.0f) + , hold(name + "_hold", 0.0f) + , decay(name + "_decay", 0.0f) + , sustain(name + "_sustain", 100.0f) + , release(name + "_release", 0.0f) + , vel2delay(name + "_vel2delay", 0.0f) + , vel2attack(name + "_vel2attack", 0.0f) + , vel2hold(name + "_vel2hold", 0.0f) + , vel2decay(name + "_vel2decay", 0.0f) + , vel2sustain(name + "_vel2sustain", 0.0f) + , vel2release(name + "_vel2release", 0.0f) + {} + // Helper function to update all of the cached modulations for the individual opcodes + void updateCachedModulation(const std::array& ccValues) + { + delay.updateCachedModulation(ccValues); + attack.updateCachedModulation(ccValues); + hold.updateCachedModulation(ccValues); + decay.updateCachedModulation(ccValues); + sustain.updateCachedModulation(ccValues); + release.updateCachedModulation(ccValues); + } + // Helper function for parsing all these envelope generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter envelopes + void parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* successful); + }; + /***********************************************************************/ // SFZ OPCODE DEFINITIONS /***********************************************************************/ @@ -62,8 +165,8 @@ class SfzOpcodeState // // File Paths // - std::optional m_sampleFile = std::nullopt; - std::optional m_default_path = std::nullopt; + StringOpcode m_sampleFile {"sample", std::nullopt}; + StringOpcode m_default_path {"default_path", std::nullopt}; // @@ -77,7 +180,7 @@ class SfzOpcodeState //Legato, // TODO To be implemented //ReleaseKey // TODO To be implemented }; - TriggerType m_trigger = TriggerType::Attack; + Opcode m_trigger {"trigger", TriggerType::Attack}; // // Key Conditions @@ -173,26 +276,7 @@ class SfzOpcodeState // // Amplitude Envelope Generator (ampeg) // - float m_ampeg_delay = 0.0f; - float m_ampeg_attack = 0.0f; - float m_ampeg_hold = 0.0f; - float m_ampeg_decay = 0.0f; - float m_ampeg_sustain = 100.0f; - float m_ampeg_release = 0.001f; - // Velocity modulation amount - float m_ampeg_vel2delay = 0.0f; - float m_ampeg_vel2attack = 0.0f; - float m_ampeg_vel2hold = 0.0f; - float m_ampeg_vel2decay = 0.0f; - float m_ampeg_vel2sustain = 0.0f; - float m_ampeg_vel2release = 0.0f; - // Midi CC modulation amounts - std::array m_ampeg_delay_oncc = {}; - std::array m_ampeg_attack_oncc = {}; - std::array m_ampeg_hold_oncc = {}; - std::array m_ampeg_decay_oncc = {}; - std::array m_ampeg_sustain_oncc = {}; - std::array m_ampeg_release_oncc = {}; + EnvelopeOpcodes m_ampeg {"ampeg"}; // @@ -219,6 +303,14 @@ class SfzOpcodeState }; + +// Template specializations + +// Trigger Type +template<> void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful); + + + } // namespace lmms diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp new file mode 100644 index 00000000000..05a9b40c0f6 --- /dev/null +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -0,0 +1,23 @@ +/* + * SfzOpcodes.cpp - Template specializations for parsing different opcode types + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h new file mode 100644 index 00000000000..75bd3aa17ce --- /dev/null +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -0,0 +1,36 @@ +/* + * SfzOpcodeState.h - Defining all supported opcodes, along with helper classes/structs/functions for parsing and storing data + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SFZ_OPCODES_H +#define LMMS_SFZ_OPCODES_H + +namespace lmms +{ + + + +} // namespace lmms + + +#endif // LMMS_SFZ_OPCODE_STATE_H \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 2b0a84236ab..11bfba3f550 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -50,8 +50,8 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf if (trigger.type() == SfzTrigger::Type::ControlChange) { return false; } // TODO. It is possible for midi CC's to trigger regions, such as using the sustain pedal or on_locc/on_hicc // Make sure the trigger type matches - if (trigger.type() == SfzTrigger::Type::NoteOn && m_trigger != TriggerType::Attack) { return false; } - if (trigger.type() == SfzTrigger::Type::NoteOff && m_trigger != TriggerType::Release) { return false; } + if (trigger.type() == SfzTrigger::Type::NoteOn && m_trigger.value() != TriggerType::Attack) { return false; } + if (trigger.type() == SfzTrigger::Type::NoteOff && m_trigger.value() != TriggerType::Release) { return false; } // Assuming the trigger has key/vel info (i.e., it's a noteOn/noteOff, not a midi CC event), make sure all the key/vel selectors match if (trigger.type() == SfzTrigger::Type::NoteOn || trigger.type() == SfzTrigger::Type::NoteOff) @@ -103,7 +103,8 @@ void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& tr // Before spawning a sound, do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer if (trigger.type() == SfzTrigger::Type::ControlChange) { - recalculateTotalCCModulation(globalState); + //recalculateTotalCCModulation(globalState); + m_ampeg.updateCachedModulation(globalState.midiCCValues()); } } @@ -124,12 +125,14 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) { m_amplitude_totalCC = totalCCModulation(m_amplitude_oncc, globalState); + /*TODO m_ampeg_delay_totalCC = totalCCModulation(m_ampeg_delay_oncc, globalState); m_ampeg_attack_totalCC = totalCCModulation(m_ampeg_attack_oncc, globalState); m_ampeg_hold_totalCC = totalCCModulation(m_ampeg_hold_oncc, globalState); m_ampeg_decay_totalCC = totalCCModulation(m_ampeg_decay_oncc, globalState); m_ampeg_sustain_totalCC = totalCCModulation(m_ampeg_sustain_oncc, globalState); m_ampeg_release_totalCC = totalCCModulation(m_ampeg_release_oncc, globalState); + */ m_gain_totalCC = totalCCModulation(m_gain_oncc, globalState); m_pan_totalCC = totalCCModulation(m_pan_oncc, globalState); @@ -140,15 +143,15 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool) { - if (m_sampleFile == std::nullopt) + if (m_sampleFile.value() == std::nullopt) { // It's weird for a region not to have a sample defined. That's literally all regions do, play samples, right? qDebug() << "[SFZ Player] Warning: `sample` opcode not assigned"; return false; } - QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value_or(""))); - QString path = defaultDirectory.absoluteFilePath(m_sampleFile.value()); + QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value().value_or(""))); // TODO + QString path = defaultDirectory.absoluteFilePath(m_sampleFile.value().value()); // TODO // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path); diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index 9f8768e3270..2639b754933 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -42,7 +42,7 @@ SfzRegionManager::SfzRegionManager(std::vector& regions) if (key >= region.m_lokey && key <= region.m_hikey) { // Additionally sort by trigger type - switch (region.m_trigger) + switch (region.m_trigger.value()) { case SfzOpcodeState::TriggerType::Attack: m_noteOnRegions.at(key).push_back(®ion); diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 8f0fac9c7a5..380389a2913 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -152,12 +152,12 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const float amplitude = (m_region->m_amplitude + m_region->m_amplitude_totalCC) / 100.0f; // Amplitude is stored as a percent // Amplitude envelope - const f_cnt_t ampegDelayFrames = (m_region->m_ampeg_delay + m_region->m_ampeg_delay_totalCC) * sampleRate; - const f_cnt_t ampegAttackFrames = (m_region->m_ampeg_attack + m_region->m_ampeg_attack_totalCC) * sampleRate; - const f_cnt_t ampegHoldFrames = (m_region->m_ampeg_hold + m_region->m_ampeg_hold_totalCC) * sampleRate; - const f_cnt_t ampegDecayFrames = (m_region->m_ampeg_decay + m_region->m_ampeg_decay_totalCC) * sampleRate; - const float ampegSustain = (m_region->m_ampeg_sustain + m_region->m_ampeg_sustain_totalCC) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg_release + m_region->m_ampeg_release_totalCC) * sampleRate; + const f_cnt_t ampegDelayFrames = (m_region->m_ampeg.delay.value() + m_region->m_ampeg.delay.cachedModulation) * sampleRate; + const f_cnt_t ampegAttackFrames = (m_region->m_ampeg.attack.value() + m_region->m_ampeg.attack.cachedModulation) * sampleRate; + const f_cnt_t ampegHoldFrames = (m_region->m_ampeg.hold.value() + m_region->m_ampeg.hold.cachedModulation) * sampleRate; + const f_cnt_t ampegDecayFrames = (m_region->m_ampeg.decay.value() + m_region->m_ampeg.decay.cachedModulation) * sampleRate; + const float ampegSustain = (m_region->m_ampeg.sustain.value() + m_region->m_ampeg.sustain.cachedModulation) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg.release.value() + m_region->m_ampeg.release.cachedModulation) * sampleRate; // Amplitude due to velocity // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 3357972cb0f..d0f9a820b5d 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -129,7 +129,7 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) // If the trigger conditions are met, spawn a new sound if (region->triggerConditionsMet(m_sfzGlobalState, trigger)) { - qDebug() << "Spawning sound!" << region->m_sampleFile.value_or("N/A"); + qDebug() << "Spawning sound!" << region->m_sampleFile.value().value_or("N/A"); // Loop through array to find open position bool foundOpenPosition = false; for (size_t i = 0; i <= m_voices.size(); ++i) @@ -239,7 +239,7 @@ void SfzSampler::loadSfzFile(const QString& filePath) int i = 0; for (auto* region : m_regionManager.allRegions()) { - qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_regionManager.allRegions().size() << region->m_sampleFile.value_or("N/A"); + qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_regionManager.allRegions().size() << region->m_sampleFile.value().value_or("N/A"); bool successfulLoadSample = region->initializeSample(parentDirectory, m_samplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } i++; From b1fb26c200b02d02974e1e2d6d4f8a378384baed Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 1 May 2026 14:12:41 -0400 Subject: [PATCH 082/131] Moving things around, and changing all opcodes into new template format --- plugins/SfzSampler/CMakeLists.txt | 3 + plugins/SfzSampler/SfzControlsConfig.h | 2 +- plugins/SfzSampler/SfzGlobalState.cpp | 2 +- plugins/SfzSampler/SfzGlobalState.h | 4 +- plugins/SfzSampler/SfzOpcodeState.cpp | 374 ++++++++-------------- plugins/SfzSampler/SfzOpcodeState.h | 213 ++---------- plugins/SfzSampler/SfzOpcodes.cpp | 292 ++++++++++++++++- plugins/SfzSampler/SfzOpcodes.h | 174 +++++++++- plugins/SfzSampler/SfzParser.cpp | 14 +- plugins/SfzSampler/SfzRegion.cpp | 50 +-- plugins/SfzSampler/SfzRegion.h | 17 +- plugins/SfzSampler/SfzRegionManager.cpp | 6 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 64 ++-- plugins/SfzSampler/SfzSampler.cpp | 4 +- plugins/SfzSampler/SfzSamplerView.cpp | 10 +- 15 files changed, 693 insertions(+), 536 deletions(-) diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index e13b920f9da..a002b1434c7 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -5,6 +5,8 @@ build_plugin(sfzsampler SfzSampler.h SfzSamplerView.cpp SfzSamplerView.h + SfzOpcodes.cpp + SfzOpcodes.h SfzOpcodeState.cpp SfzOpcodeState.h SfzRegion.cpp @@ -27,6 +29,7 @@ build_plugin(sfzsampler MOCFILES SfzSampler.h SfzSamplerView.h + SfzOpcodes.h SfzOpcodeState.h SfzRegion.h SfzParser.h diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzSampler/SfzControlsConfig.h index c230017beff..6053ccf961c 100644 --- a/plugins/SfzSampler/SfzControlsConfig.h +++ b/plugins/SfzSampler/SfzControlsConfig.h @@ -35,7 +35,7 @@ class SfzControlsConfig : public SfzOpcodeState { public: //! Keeps track of which CC's are actually used by the instrument, so that the GUI only needs to display those, not all 128 - std::array m_activeMidiCCs = {}; + std::array m_activeMidiCCs = {}; //! Stores information about each switch key, to make it easy to display on the GUI struct SwitchKeyInfo diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 97d999fbe38..5a34633a114 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -86,7 +86,7 @@ std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey void SfzGlobalState::initializeMidiCCValues(const SfzOpcodeState& controlsConfig) { - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + for (int i = 0; i < NumMidiCCs; ++i) { m_ccValues.at(i) = controlsConfig.m_set_cc.at(i); } diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index 62588306391..578cb40060b 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -57,7 +57,7 @@ class SfzGlobalState //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } //! Returns a const array of the current CC knob values - const std::array midiCCValues() const { return m_ccValues; } + const std::array midiCCValues() const { return m_ccValues; } //! Returns the random value for the current trigger const float rand() const { return m_rand; } @@ -84,7 +84,7 @@ class SfzGlobalState //! Stores the current value of all the midi CC knobs/controllers //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 127 to get a float between 0 and 1 works fine. //! std::optional is used so that sfz file can specify default cc values - std::array m_ccValues = {}; + std::array m_ccValues = {}; }; diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 84ddc9fc278..9f2437dc7e4 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -24,7 +24,6 @@ #include "SfzOpcodeState.h" #include -#include namespace lmms { @@ -40,8 +39,128 @@ SfzOpcodeState::SfzOpcodeState() bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& value) { + std::vector opcodeList = { + &m_sampleFile, + &m_default_path, + + &m_trigger, + + &m_lokey, + &m_hikey, + + &m_sw_lokey, + &m_sw_hikey, + &m_sw_last, + &m_sw_default, + &m_sw_label, + + &m_lovel, + &m_hivel, + + &m_seq_length, + &m_seq_position, + + &m_lorand, + &m_hirand, + + &m_offset, + + &m_loop_mode, + + &m_delay, + &m_delay_random, + + &m_tune, + &m_pitch_keycenter, + &m_pitch_keytrack, + &m_pitch_veltrack, + + &m_fil_type, + &m_cutoff, + &m_resonance, + &m_fil_veltrack, + + &m_amplitude, + &m_amp_veltrack, + + &m_volume, + &m_pan + }; + + std::vector envelopeGeneratorList = { + &m_ampeg + }; + + bool parsed = false; bool successful = true; + for (auto* opcode : opcodeList) + { + //if (parsed) { break; } // Do not break even if the opcode has already been parsed, since some opcodes like "key" are effectively aliases for multiple opcodes (lokey, hikey, and pitch_keytrack) + opcode->parseFromString(name, value, &parsed, &successful); + } + + for (auto* envelopeGenerator : envelopeGeneratorList) + { + //if (parsed) { break; } + envelopeGenerator->parseEnvelopeGeneratorOpcode(name, value, &parsed, &successful); + } + + + // The per-CC opcodes are handled separately here for now, just to make the code simpler + if (name.startsWith("locc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_locc.at(ccNumber) = value.toInt(&successful); + parsed = true; + } + else if (name.startsWith("hicc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_hicc.at(ccNumber) = value.toInt(&successful); + parsed = true; + } + else if (name.startsWith("set_cc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_set_cc.at(ccNumber) = value.toInt(&successful); + parsed = true; + } + else if (name.startsWith("set_hdcc")) + { + int ccNumber = ccNumberFromOpcode(name); + m_set_cc.at(ccNumber) = value.toFloat(&successful) * 127; + parsed = true; + } + else if (name.startsWith("label_cc")) + { + int ccNumber = ccNumberFromOpcode(name); // TODO have some kind of error handling for invalid cc numbers + m_label_cc.at(ccNumber) = value; + parsed = true; + successful = true; + } + + + // + // Special cases + // + /* + if (name == "key") + { + // Setting "key" on its own is equivalent to setting lokey, hikey, and pitch_keycenter all to the same value + m_lokey.setValue(value.toInt(&successful)); + m_hikey.setValue(value.toInt(&successful)); + m_pitch_keycenter.setValue(value.toInt(&successful)); + if (!successful) { + m_lokey.setValue(stringToKeyNum(value, &successful)); + m_hikey.setValue(stringToKeyNum(value, &successful)); + m_pitch_keycenter.setValue(stringToKeyNum(value, &successful)); + } + parsed = true; + } + */ + + /* m_ampeg.parseEnvelopeGeneratorOpcode(name, value, &successful); // // File paths @@ -267,50 +386,6 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu m_amp_veltrack = value.toFloat(&successful); } - // - // Amplitude Envelope Generator (ampeg) - // - /*TODO - else if (name == "ampeg_delay") { m_ampeg_delay = value.toFloat(&successful); } - else if (name == "ampeg_attack") { m_ampeg_attack = value.toFloat(&successful); } - else if (name == "ampeg_hold") { m_ampeg_hold = value.toFloat(&successful); } - else if (name == "ampeg_decay") { m_ampeg_decay = value.toFloat(&successful); } - else if (name == "ampeg_sustain") { m_ampeg_sustain = value.toFloat(&successful); } - else if (name == "ampeg_release") { m_ampeg_release = value.toFloat(&successful); } - // Velocity modulation amounts - else if (name == "ampeg_vel2delay") { m_ampeg_vel2delay = value.toFloat(&successful); } - else if (name == "ampeg_vel2attack") { m_ampeg_vel2attack = value.toFloat(&successful); } - else if (name == "ampeg_vel2hold") { m_ampeg_vel2hold = value.toFloat(&successful); } - else if (name == "ampeg_vel2decay") { m_ampeg_vel2decay = value.toFloat(&successful); } - else if (name == "ampeg_vel2sustain") { m_ampeg_vel2sustain = value.toFloat(&successful); } - else if (name == "ampeg_vel2release") { m_ampeg_vel2release = value.toFloat(&successful); } - // Midi CC modulation amounts - else if (name.startsWith("ampeg_delay_oncc") || name.startsWith("ampeg_delaycc")) - { - m_ampeg_delay_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - else if (name.startsWith("ampeg_attack_oncc") || name.startsWith("ampeg_attackcc")) - { - m_ampeg_attack_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - else if (name.startsWith("ampeg_hold_oncc") || name.startsWith("ampeg_holdcc")) - { - m_ampeg_hold_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - else if (name.startsWith("ampeg_decay_oncc") || name.startsWith("ampeg_decaycc")) - { - m_ampeg_decay_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - else if (name.startsWith("ampeg_sustain_oncc") || name.startsWith("ampeg_sustaincc")) - { - m_ampeg_sustain_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - else if (name.startsWith("ampeg_release_oncc") || name.startsWith("ampeg_releasecc")) - { - m_ampeg_release_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - */ - // // Misc Volume @@ -367,220 +442,23 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu qDebug() << "[SFZ Parser] Unknown opcode:" << name; return false; } + */ if (!successful) { - qDebug() << "[SFZ Parser] Unable to convert value to number:" << name << "=" << value; + qDebug() << "[SFZ Parser] Unable to convert value from string:" << name << "=" << value; return false; } - return true; -} - - - -int SfzOpcodeState::ccNumberFromOpcode(const QString& opcode) -{ - // Match for `ccN` where N is a number - QRegularExpression re("cc\\d+"); - QRegularExpressionMatch match = re.match(opcode); - if (!match.hasMatch()) { qDebug() << "[SFZ Parser] Unable to find CC number in opcode:" << opcode; return 0; } - // Get the number from that string - QString numberPart = match.captured(0).split("cc")[1]; - bool successfulToInt = false; - int ccNumber = numberPart.toInt(&successfulToInt); - if (!successfulToInt) { qDebug() << "[SFZ Parser] Unable to convert CC number to int:" << opcode; return 0; } - // Normal midi lets you choose between 0-127 for the CC number. According to the SFZ format website, ARIA and SFZ 2 allow for extended CC numbers. Most have not been implemented here, so to keep things safe, we cap it and give a warning. - if (ccNumber < 0 || ccNumber > NumMidiCCs) { qDebug() << "[SFZ Parser] Midi CC number" << ccNumber << "is out of range 0-127. This is not yet implmented."; return 0;} - return ccNumber; -} - - -int SfzOpcodeState::stringToKeyNum(QString keyString, bool* successful) -{ - keyString = keyString.toLower(); - // The last character is the octave number - int octave = QString(keyString.back()).toInt(successful); - if (!*successful) {qDebug() << "[SFZ Parser] Unable to parse key string, Invalid octave number:" << keyString; return -1; } - // The remaining characters at the start define the key - QString key = keyString.chopped(1); - int keyOffset = 0; - - if (key == "c") { keyOffset = 0; } // C is 0 since that's where the octaves change - else if (key == "c#" || key == "db") { keyOffset = 1; } - else if (key == "d") { keyOffset = 2; } - else if (key == "d#" || key == "eb") { keyOffset = 3; } - else if (key == "e") { keyOffset = 4; } - else if (key == "f") { keyOffset = 5; } - else if (key == "f#" || key == "gb") { keyOffset = 6; } - else if (key == "g") { keyOffset = 7; } - else if (key == "g#" || key == "ab") { keyOffset = 8; } - else if (key == "a") { keyOffset = 9; } - else if (key == "a#" || key == "bb") { keyOffset = 10; } - else if (key == "b") { keyOffset = 11; } - else + else if (!parsed) { - *successful = false; - qDebug() << "[SFZ Parser] Unable to parse key string, Invalid key:" << keyString; - return -1; - } - *successful = true; - // For some reason, C1 is midi key 24, so eveything is offset by 24 - return 24 + keyOffset + 12 * (octave - 1); -} - - -QString SfzOpcodeState::keyNumToString(int keyNum) -{ - QString octave = QString::number((keyNum - 24) / 12 + 1); - QString key = ""; - switch (keyNum % 12) - { - case 0: - key = "C"; - break; - case 1: - key = "C#"; - break; - case 2: - key = "D"; - break; - case 3: - key = "D#"; - break; - case 4: - key = "E"; - break; - case 5: - key = "F"; - break; - case 6: - key = "F#"; - break; - case 7: - key = "G"; - break; - case 8: - key = "G#"; - break; - case 9: - key = "A"; - break; - case 10: - key = "A#"; - break; - case 11: - key = "B"; - break; - } - return key + octave; -} - - -template<> -void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) -{ - if (m_opcodeName != opcodeName) { return; } - m_value = opcodeValue.toFloat(successful); - qDebug() << "Float opcode" << m_opcodeName << "set to" << m_value; -} - -template<> -void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) -{ - if (m_opcodeName != opcodeName) { return; } - m_value = opcodeValue.toInt(successful); - // Integer opcodes can also define keys, which can be specified either by a key number of by a string like e4 or c#5 - if (!successful) { m_value = stringToKeyNum(opcodeValue, successful); } -} - -template<> -void SfzOpcodeState::Opcode>::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) -{ - if (m_opcodeName != opcodeName) { return; } - m_value = opcodeValue; - *successful = true; -} - -// Modulatable Opcodes are a type of float opcode which can be adjusted based on different midi CC knob values -// They have both an initial value defined by their opcodename=value -// but also opcodename_oncc123=modulation_amount -// or opcodenamecc123=modulation_amount -void SfzOpcodeState::ModulatableOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) -{ - if (opcodeName == m_opcodeName) - { - m_value = opcodeValue.toFloat(successful); - qDebug() << "ModulatableOpcode" << m_opcodeName << "set to" << m_value; - } - else if (opcodeName.startsWith(m_opcodeName + "_oncc") || opcodeName.startsWith(m_opcodeName + "cc")) - { - value_oncc.at(ccNumberFromOpcode(opcodeName)) = opcodeValue.toFloat(successful); - qDebug() << "ModulatableOpcode" << m_opcodeName << "set cc mod" << ccNumberFromOpcode(opcodeName) << value_oncc.at(ccNumberFromOpcode(opcodeName)); - } -} - - -// The modulation of Modulatable opcodes can change whenever a midi CC knob changes. There are 128 of them, -// which is a bit much to keep checking every single frame when generating audio. -// Instead, the current modulation value is only calculated and cached each time a midi CC event is sent, which saves a lot of compute. -void SfzOpcodeState::ModulatableOpcode::updateCachedModulation(const std::array& ccValues) -{ - cachedModulation = 0.0f; - // TODO this may be optimized if we only loop through the CC's which are actually used by the region - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) - { - cachedModulation += value_oncc.at(i) * ccValues.at(i) / 128.0f; + qDebug() << "[SFZ Parser] Unknown opcode:" << name; + return false; } + return true; } -// Envelope Generator Opcodes -void SfzOpcodeState::EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* successful) -{ - // Pass the opcode name/value to all of the bundled opcodes. - // If it matches one of them, that's great. If not, they will return and nothing will happen. - // TODO is this error handling with "successful" correct? - delay.parseFromString(opcode, value, successful); - attack.parseFromString(opcode, value, successful); - hold.parseFromString(opcode, value, successful); - decay.parseFromString(opcode, value, successful); - sustain.parseFromString(opcode, value, successful); - release.parseFromString(opcode, value, successful); - - vel2delay.parseFromString(opcode, value, successful); - vel2attack.parseFromString(opcode, value, successful); - vel2hold.parseFromString(opcode, value, successful); - vel2decay.parseFromString(opcode, value, successful); - vel2sustain.parseFromString(opcode, value, successful); - vel2release.parseFromString(opcode, value, successful); -} - - - - -// -// Bespoke Enum-like Opcodes -// - -template<> -void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) -{ - if (m_opcodeName != opcodeName) { return; } - - if (opcodeValue == "attack") { m_value = TriggerType::Attack; } - else if (opcodeValue == "release") { m_value = TriggerType::Release; } - //else if (value == "first") { m_trigger = TriggerType::First; } // TODO To be implemented - //else if (value == "legato") { m_trigger = TriggerType::Legato; } // TODO To be implemented - //else if (value == "release_key") { m_trigger = TriggerType::ReleaseKey; } // TODO To be implemented - else - { - qDebug() << "[SFZ Parser] Unknown trigger:" << opcodeValue; - return; - } - *successful = true; -} - } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index ee4bb5fd354..08752fed0d5 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -25,6 +25,7 @@ #ifndef LMMS_SFZ_OPCODE_STATE_H #define LMMS_SFZ_OPCODE_STATE_H +#include "SfzOpcodes.h" #include #include #include @@ -43,120 +44,6 @@ class SfzOpcodeState //! If it was unsucessful, it will print an error message and return false. bool setOpcodeByStrings(const QString& name, const QString& value); - //! Helper function for converting strings like "A5" or "B#2" into integers representing keys on the midi keyboard. - static int stringToKeyNum(QString keyString, bool* successful); - //! Helper function for converting integers like 24 or 45 representing midi keys back into strings representing the key and octave on the keyboard (essentially the inverse of `stringToKeyNum`) - static QString keyNumToString(int keyNum); - //! Helper function for taking an opcode name such as "set_cc45" and extracting the number "45" from it. - //! If the string does not contain a substring in the form "ccN" where N is a number, it will return print an error and return 0. - //! Likewise, if the number is outside the range 0-127, it will print an error and return 0. - static int ccNumberFromOpcode(const QString& opcode); - - // Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. - static constexpr const int NumMidiCCs = 128; - - - /* Helper structs for opcodes */ - - // A base struct for all opcodes, just a name and a value. - template - struct Opcode - { - QString m_opcodeName; - T m_value; - - Opcode(QString name, T defaultValue) : m_opcodeName(name), m_value(defaultValue) {} - void setValue(const T& value) { m_value = value; } - const T& value() const { return m_value; } - - // Function for taking in a string like "pitch_keytrack=1200", split into name/value as "pitch_keytrack", "1200", and updating the value if the name matches. - virtual void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) {}; - }; - - // Float/decimal opcodes (such as amplitude, panning, etc) - struct FloatOpcode : Opcode - { - FloatOpcode(QString name = "", float defaultValue = 0.0f) : Opcode(name, defaultValue) {}; - //void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; - }; - - // Key opcodes (such as lokey, hikey, pitch_keycenter, etc) - struct KeyOpcode : Opcode - { - KeyOpcode(QString name = "", int defaultValue = 0) : Opcode(name, defaultValue) {}; - //void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; - }; - - // String opcodes (such as sample file path) - struct StringOpcode : Opcode> - { - StringOpcode(QString name, std::optional defaultValue) : Opcode>(name, defaultValue) {}; - //void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; - }; - - // Many opcodes can be modulated by MIDI CC knobs, so each of them needs an array to store the modulation weights for each one. - struct ModulatableOpcode : FloatOpcode - { - std::array value_oncc = {}; - //! Store the current total midi CC modulation amounts for the different targets, just so that we don't - // have to recalculate them every buffer, instead only when a trigger occurs. - float cachedModulation = 0.0f; - - ModulatableOpcode(QString name = "", float defaultValue = 0.0f) : FloatOpcode(name, defaultValue) {}; - // Helper function for parsing these kinds of opcodes, where you have both `opcode` and `opcode_onccN` where N is the midi cc number, so - // that the code isn't duplicated for every modulatable parameter. - void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful) override; - // Helper function to update the cached CC modulation amount every time a midi Control Change trigger occurs - void updateCachedModulation(const std::array& ccValues); - }; - - // Additionally, things like amplitude, pitch, and filter freq envelopes and lfo's all have very similar parameters, so it makes sense to put - // them all together in a handy definition - struct EnvelopeOpcodes - { - // Envelope parameters (these can be modulated by midi CCs) - ModulatableOpcode delay; - ModulatableOpcode attack; - ModulatableOpcode hold; - ModulatableOpcode decay; - ModulatableOpcode sustain; - ModulatableOpcode release; - // Velocity modulation amount - FloatOpcode vel2delay; - FloatOpcode vel2attack; - FloatOpcode vel2hold; - FloatOpcode vel2decay; - FloatOpcode vel2sustain; - FloatOpcode vel2release; - - // Initialize opcodes with correct names and default values - EnvelopeOpcodes(QString name) - : delay(name + "_delay", 0.0f) - , attack(name + "_attack", 0.0f) - , hold(name + "_hold", 0.0f) - , decay(name + "_decay", 0.0f) - , sustain(name + "_sustain", 100.0f) - , release(name + "_release", 0.0f) - , vel2delay(name + "_vel2delay", 0.0f) - , vel2attack(name + "_vel2attack", 0.0f) - , vel2hold(name + "_vel2hold", 0.0f) - , vel2decay(name + "_vel2decay", 0.0f) - , vel2sustain(name + "_vel2sustain", 0.0f) - , vel2release(name + "_vel2release", 0.0f) - {} - // Helper function to update all of the cached modulations for the individual opcodes - void updateCachedModulation(const std::array& ccValues) - { - delay.updateCachedModulation(ccValues); - attack.updateCachedModulation(ccValues); - hold.updateCachedModulation(ccValues); - decay.updateCachedModulation(ccValues); - sustain.updateCachedModulation(ccValues); - release.updateCachedModulation(ccValues); - } - // Helper function for parsing all these envelope generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter envelopes - void parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* successful); - }; /***********************************************************************/ // SFZ OPCODE DEFINITIONS @@ -172,105 +59,80 @@ class SfzOpcodeState // // Trigger Type // - enum class TriggerType - { - Attack, - Release, - //First, // TODO To be implemented - //Legato, // TODO To be implemented - //ReleaseKey // TODO To be implemented - }; Opcode m_trigger {"trigger", TriggerType::Attack}; // // Key Conditions // - int m_lokey = 0; - int m_hikey = 127; + // Adding "key" as an alias here, since setting key=something automatically sets all lokey, hikey, and pitch_keycenter to the same value + KeyOpcode m_lokey {{"lokey", "key"}, 0}; + KeyOpcode m_hikey {{"hikey", "key"}, 127}; // // Key Switches // - int m_sw_lokey = 0; - int m_sw_hikey = 127; - std::optional m_sw_last = std::nullopt; // The SFZ opcode spec says this should default to 0, but I don't think that's right? - std::optional m_sw_default = std::nullopt; - std::optional m_sw_label = std::nullopt; + KeyOpcode m_sw_lokey {"sw_lokey", 0}; + KeyOpcode m_sw_hikey {"sw_hikey", 127}; + OptionalKeyOpcode m_sw_last {"sw_last", std::nullopt}; // The SFZ opcode spec says this should default to 0, but I don't think that's right? + OptionalKeyOpcode m_sw_default {"sw_default", std::nullopt}; + StringOpcode m_sw_label {"sw_label", std::nullopt}; // // Velocity Conditions // - int m_lovel = 0; - int m_hivel = 127; + Opcode m_lovel {"lovel", 0}; + Opcode m_hivel {"hivel", 127}; // // Round Robin Conditions // - int m_seq_length = 1; - int m_seq_position = 1; + Opcode m_seq_length {"seq_length", 1}; + Opcode m_seq_position {"seq_position", 1}; // // Random Conditions // - float m_lorand = 0.0f; - float m_hirand = 1.0f; + FloatOpcode m_lorand {"lorand", 0.0f}; + FloatOpcode m_hirand {"hirand", 1.0f}; // // Sample playback // - int m_offset = 0; // sample play offset in frames + Opcode m_offset {"offset", 0}; // sample play offset in frames - enum class LoopMode - { - NoLoop, - OneShot, - //LoopContinuous, // To be implemented - //LoopSustain - }; - LoopMode m_loop_mode = LoopMode::NoLoop; + Opcode m_loop_mode {"loop_mode", LoopMode::NoLoop}; // // Delay // - float m_delay = 0; // In seconds - float m_delay_random = 0; + FloatOpcode m_delay {"delay", 0.0f}; // In seconds + FloatOpcode m_delay_random {"delay_random", 0.0f}; // // Pitch // - int m_tune = 0; // in cents - int m_pitch_keycenter = 60; - int m_pitch_keytrack = 100; // in cents - int m_pitch_veltrack = 0; // in cents + Opcode m_tune {"tune", 0}; // in cents + KeyOpcode m_pitch_keycenter {{"pitch_keycenter", "key"}, 60}; // "key" as an alias since it automatically sets "lokey, hikey, and pitch_keycenter to the same value + Opcode m_pitch_keytrack {"pitch_keytrack", 100}; // in cents + Opcode m_pitch_veltrack {"pitch_veltrack", 0}; // in cents // // Filter // - enum class FilterType - { - Lowpass1Pole, - Lowpass2Pole, - Highpass1Pole, - Highpass2Pole, - Bandpass2Pole, - Bandstop2Pole - }; - FilterType m_fil_type = FilterType::Lowpass2Pole; - std::optional m_cutoff = std::nullopt; - float m_resonance = 0.0f; - int m_fil_veltrack = 0; // in cents + Opcode m_fil_type {"fil_type", FilterType::Lowpass2Pole}; + OptionalFloatOpcode m_cutoff {"cutoff", std::nullopt}; + FloatOpcode m_resonance {"resonance", 0.0f}; + Opcode m_fil_veltrack {"fil_veltrack", 0}; // in cents // // Amplitude // - float m_amplitude = 100.0f; // In percent - std::array m_amplitude_oncc = {}; - + ModulatableOpcode m_amplitude {"amplitude", 100.0f}; // In percent // Overall amplitute velocity modulation - float m_amp_veltrack = 100; + FloatOpcode m_amp_veltrack {"amp_veltrack", 100.0f}; // @@ -282,16 +144,16 @@ class SfzOpcodeState // // Misc Volume // - float m_volume = 0.0f; // In decibals // Gain is the same as volume. Some opcodes use the word volume, some use gain, both are decibals - std::array m_gain_oncc = {}; - float m_pan = 0.0f; - std::array m_pan_oncc = {}; + // It seems that "gain" by itself isn't a real opcode, but gain_ccN and gain_onccN are... just to make the logic eaiser, I have gain as a normal alias. + ModulatableOpcode m_volume {{"volume", "gain", "group_volume"}, 0.0f}; // In decibals + ModulatableOpcode m_pan {"pan", 0.0f}; // // Midi CC // + // These are handled separately from the other opcodes (for now) because they are more like arrays of opcodes std::array m_locc = {}; std::array m_hicc = {}; // NOTE this needs to be initialized to contain all 127 in the constructor, since it's hard to fill an array directly within a header file //! Default midi CC values @@ -302,15 +164,6 @@ class SfzOpcodeState friend class SfzRegion; }; - - -// Template specializations - -// Trigger Type -template<> void SfzOpcodeState::Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* successful); - - - } // namespace lmms diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp index 05a9b40c0f6..d2f57f60715 100644 --- a/plugins/SfzSampler/SfzOpcodes.cpp +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -20,4 +20,294 @@ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA. * - */ \ No newline at end of file + */ + +#include "SfzOpcodes.h" + +#include +#include + +namespace lmms +{ + +template<> +void FloatOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + m_value = opcodeValue.toFloat(successful); + *parsed = true; +} + +// Same function but for optional floats +template<> +void OptionalFloatOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + m_value = opcodeValue.toFloat(successful); + *parsed = true; +} + +template<> +void KeyOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + m_value = opcodeValue.toInt(successful); + // Integer opcodes can also define keys, which can be specified either by a key number of by a string like e4 or c#5 + if (!*successful) { m_value = stringToKeyNum(opcodeValue, successful); } + *parsed = true; +} + +// Same function but for optional keys +template<> +void OptionalKeyOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + m_value = opcodeValue.toInt(successful); + // Integer opcodes can also define keys, which can be specified either by a key number of by a string like e4 or c#5 + if (!*successful) { m_value = stringToKeyNum(opcodeValue, successful); } + *parsed = true; +} + +template<> +void StringOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + m_value = opcodeValue; + *parsed = true; + *successful = true; +} + +// Modulatable Opcodes are a type of float opcode which can be adjusted based on different midi CC knob values +// They have both an initial value defined by their opcodename=value +// but also opcodename_oncc123=modulation_amount +// or opcodenamecc123=modulation_amount +void ModulatableOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + for (QString alias : m_opcodeNames) + { + if (opcodeName == alias) + { + m_value = opcodeValue.toFloat(successful); + *parsed = true; + } + else if (opcodeName.startsWith(alias + "_oncc") || opcodeName.startsWith(alias + "cc")) + { + value_oncc.at(ccNumberFromOpcode(opcodeName)) = opcodeValue.toFloat(successful); + *parsed = true; + } + } +} + + +// The modulation of Modulatable opcodes can change whenever a midi CC knob changes. There are 128 of them, +// which is a bit much to keep checking every single frame when generating audio. +// Instead, the current modulation value is only calculated and cached each time a midi CC event is sent, which saves a lot of compute. +void ModulatableOpcode::updateCachedModulation(const std::array& ccValues) +{ + cachedModulation = 0.0f; + // TODO this may be optimized if we only loop through the CC's which are actually used by the region + for (int i = 0; i < NumMidiCCs; ++i) + { + cachedModulation += value_oncc.at(i) * ccValues.at(i) / 128.0f; + } +} + + + + +// Envelope Generator Opcodes +void EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful) +{ + // Pass the opcode name/value to all of the bundled opcodes. + // If it matches one of them, that's great. If not, they will return and nothing will happen. + // TODO is this error handling with "successful" correct? + delay.parseFromString(opcode, value, parsed, successful); + attack.parseFromString(opcode, value, parsed, successful); + hold.parseFromString(opcode, value, parsed, successful); + decay.parseFromString(opcode, value, parsed, successful); + sustain.parseFromString(opcode, value, parsed, successful); + release.parseFromString(opcode, value, parsed, successful); + + vel2delay.parseFromString(opcode, value, parsed, successful); + vel2attack.parseFromString(opcode, value, parsed, successful); + vel2hold.parseFromString(opcode, value, parsed, successful); + vel2decay.parseFromString(opcode, value, parsed, successful); + vel2sustain.parseFromString(opcode, value, parsed, successful); + vel2release.parseFromString(opcode, value, parsed, successful); +} + + + + +// +// Bespoke Enum-like Opcodes +// + +template<> +void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + + if (opcodeValue == "attack") { m_value = TriggerType::Attack; } + else if (opcodeValue == "release") { m_value = TriggerType::Release; } + //else if (opcodeValue == "first") { m_value = TriggerType::First; } // TODO To be implemented + //else if (opcodeValue == "legato") { m_value = TriggerType::Legato; } // TODO To be implemented + //else if (opcodeValue == "release_key") { m_value = TriggerType::ReleaseKey; } // TODO To be implemented + else + { + qDebug() << "[SFZ Parser] Unknown trigger:" << opcodeValue; + return; + } + *parsed = true; + *successful = true; +} + + +template<> +void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + + if (opcodeValue == "no_loop") { m_value = LoopMode::NoLoop; } + else if (opcodeValue == "one_shot") { m_value = LoopMode::OneShot; } + //else if (opcodeValue == "loop_continuous") { m_value = LoopMode::LoopContinuous; } // Not implemented yet, since we don't currently have a way to get loop point data from sample files + //else if (opcodeValue == "loop_sustain") { m_value = LoopMode::LoopSustain; } + else + { + qDebug() << "[SFZ Parser] Unknown loop_mode:" << opcodeValue; + return; + } + *parsed = true; + *successful = true; +} + + +template<> +void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) +{ + if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + + if (opcodeValue == "lpf_1p") { m_value = FilterType::Lowpass1Pole; } + else if (opcodeValue == "lpf_2p") { m_value = FilterType::Lowpass2Pole; } + else if (opcodeValue == "hpf_1p") { m_value = FilterType::Highpass1Pole; } + else if (opcodeValue == "hpf_2p") { m_value = FilterType::Highpass2Pole; } + else if (opcodeValue == "bpf_2p") { m_value = FilterType::Bandpass2Pole; } + else if (opcodeValue == "brf_2p") { m_value = FilterType::Bandstop2Pole; } + else + { + qDebug() << "[SFZ Parser] Unknown filter type:" << opcodeValue; + return; + } + *parsed = true; + *successful = true; +} + + + + + + + +// +// Helper Functions +// + +int ccNumberFromOpcode(const QString& opcode) +{ + // Match for `ccN` where N is a number + QRegularExpression re("cc\\d+"); + QRegularExpressionMatch match = re.match(opcode); + if (!match.hasMatch()) { qDebug() << "[SFZ Parser] Unable to find CC number in opcode:" << opcode; return 0; } + // Get the number from that string + QString numberPart = match.captured(0).split("cc")[1]; + bool successfulToInt = false; + int ccNumber = numberPart.toInt(&successfulToInt); + if (!successfulToInt) { qDebug() << "[SFZ Parser] Unable to convert CC number to int:" << opcode; return 0; } + // Normal midi lets you choose between 0-127 for the CC number. According to the SFZ format website, ARIA and SFZ 2 allow for extended CC numbers. Most have not been implemented here, so to keep things safe, we cap it and give a warning. + if (ccNumber < 0 || ccNumber > NumMidiCCs) { qDebug() << "[SFZ Parser] Midi CC number" << ccNumber << "is out of range 0-127. This is not yet implmented."; return 0;} + return ccNumber; +} + + +int stringToKeyNum(QString keyString, bool* successful) +{ + keyString = keyString.toLower(); + // The last character is the octave number + int octave = QString(keyString.back()).toInt(successful); + if (!*successful) {qDebug() << "[SFZ Parser] Unable to parse key string, Invalid octave number:" << keyString; return -1; } + // The remaining characters at the start define the key + QString key = keyString.chopped(1); + int keyOffset = 0; + + if (key == "c") { keyOffset = 0; } // C is 0 since that's where the octaves change + else if (key == "c#" || key == "db") { keyOffset = 1; } + else if (key == "d") { keyOffset = 2; } + else if (key == "d#" || key == "eb") { keyOffset = 3; } + else if (key == "e") { keyOffset = 4; } + else if (key == "f") { keyOffset = 5; } + else if (key == "f#" || key == "gb") { keyOffset = 6; } + else if (key == "g") { keyOffset = 7; } + else if (key == "g#" || key == "ab") { keyOffset = 8; } + else if (key == "a") { keyOffset = 9; } + else if (key == "a#" || key == "bb") { keyOffset = 10; } + else if (key == "b") { keyOffset = 11; } + else + { + *successful = false; + qDebug() << "[SFZ Parser] Unable to parse key string, Invalid key:" << keyString; + return -1; + } + *successful = true; + // For some reason, C1 is midi key 24, so eveything is offset by 24 + return 24 + keyOffset + 12 * (octave - 1); +} + + +QString keyNumToString(int keyNum) +{ + QString octave = QString::number((keyNum - 24) / 12 + 1); + QString key = ""; + switch (keyNum % 12) + { + case 0: + key = "C"; + break; + case 1: + key = "C#"; + break; + case 2: + key = "D"; + break; + case 3: + key = "D#"; + break; + case 4: + key = "E"; + break; + case 5: + key = "F"; + break; + case 6: + key = "F#"; + break; + case 7: + key = "G"; + break; + case 8: + key = "G#"; + break; + case 9: + key = "A"; + break; + case 10: + key = "A#"; + break; + case 11: + key = "B"; + break; + } + return key + octave; +} + + +} // namespace lmms diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index 75bd3aa17ce..bf793290ad0 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -25,12 +25,184 @@ #ifndef LMMS_SFZ_OPCODES_H #define LMMS_SFZ_OPCODES_H +#include +#include +#include +#include +#include + namespace lmms { +//! Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. +constexpr const int NumMidiCCs = 128; + +/* Helper Functions */ + +//! Helper function for converting strings like "A5" or "B#2" into integers representing keys on the midi keyboard. +int stringToKeyNum(QString keyString, bool* successful); +//! Helper function for converting integers like 24 or 45 representing midi keys back into strings representing the key and octave on the keyboard (essentially the inverse of `stringToKeyNum`) +QString keyNumToString(int keyNum); +//! Helper function for taking an opcode name such as "set_cc45" and extracting the number "45" from it. +//! If the string does not contain a substring in the form "ccN" where N is a number, it will return print an error and return 0. +//! Likewise, if the number is outside the range 0-127, it will print an error and return 0. +int ccNumberFromOpcode(const QString& opcode); + + + +/* Helper structs for opcodes */ + +//! Due to issues with having different template object pointers in an array, this base class which all opcodes derive from makes things easier +struct BaseOpcode +{ + //! Given a string like "pitch_keytrack=1200" which has been split into name/value as "pitch_keytrack", "1200", this function updates the internal value if the name matches. + virtual void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) = 0; +}; +//! A base struct for all opcodes, just a name and a value. +template +struct Opcode : BaseOpcode +{ + // Using a vector here, since some opcodes have multiple aliases + std::vector m_opcodeNames; + T m_value; + + Opcode(QString name, T defaultValue) : m_opcodeNames({name}), m_value(defaultValue) {} + Opcode(std::vector names, T defaultValue) : m_opcodeNames(names), m_value(defaultValue) {} + void setValue(const T& value) { m_value = value; } + const T& value() const { return m_value; } + + void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; +}; + +//! Float/decimal opcodes (such as amplitude, panning, etc) +using FloatOpcode = Opcode; + +//! Some float opcodes may take on a null default value (filter cutoff) +using OptionalFloatOpcode = Opcode>; + +//! Integer opcodes (such as lovel, seq_length, seq_position, offset, etc) +using IntOpcode = Opcode; + +//! Key opcodes (such as lokey, hikey, pitch_keycenter, etc) +using KeyOpcode = Opcode; + +//! Some key opcodes (sw_last, sw_default) make sense to have null default values +using OptionalKeyOpcode = Opcode>; + +//! String opcodes (such as sample file path) +using StringOpcode = Opcode>; + +//! Many opcodes can be modulated by MIDI CC knobs, so each of them needs an array to store the modulation weights for each one. +struct ModulatableOpcode : FloatOpcode +{ + std::array value_oncc = {}; + //! Store the current total midi CC modulation amounts for the different targets, just so that we don't + // have to recalculate them every buffer, instead only when a trigger occurs. + float cachedModulation = 0.0f; + + ModulatableOpcode(QString name = "", float defaultValue = 0.0f) : FloatOpcode(name, defaultValue) {}; + ModulatableOpcode(std::vector names = {}, float defaultValue = 0.0f) : FloatOpcode(names, defaultValue) {}; + //! Function which returns the sum of the base opcode value and whatever the modulation is currently + float modulatedValue() const { return m_value + cachedModulation; } + //! Helper function for parsing these kinds of opcodes, where you have both `opcode` and `opcode_onccN` where N is the midi cc number, so + // that the code isn't duplicated for every modulatable parameter. + void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; + //! Helper function to update the cached CC modulation amount every time a midi Control Change trigger occurs + void updateCachedModulation(const std::array& ccValues); +}; + +// Additionally, things like amplitude, pitch, and filter freq envelopes and lfo's all have very similar parameters, so it makes sense to put +// them all together in a handy definition +struct EnvelopeOpcodes +{ + // Envelope parameters (these can be modulated by midi CCs) + ModulatableOpcode delay; + ModulatableOpcode attack; + ModulatableOpcode hold; + ModulatableOpcode decay; + ModulatableOpcode sustain; + ModulatableOpcode release; + // Velocity modulation amount + FloatOpcode vel2delay; + FloatOpcode vel2attack; + FloatOpcode vel2hold; + FloatOpcode vel2decay; + FloatOpcode vel2sustain; + FloatOpcode vel2release; + + // Initialize opcodes with correct names and default values + EnvelopeOpcodes(QString name) + : delay(name + "_delay", 0.0f) + , attack(name + "_attack", 0.0f) + , hold(name + "_hold", 0.0f) + , decay(name + "_decay", 0.0f) + , sustain(name + "_sustain", 100.0f) + , release(name + "_release", 0.0f) + , vel2delay(name + "_vel2delay", 0.0f) + , vel2attack(name + "_vel2attack", 0.0f) + , vel2hold(name + "_vel2hold", 0.0f) + , vel2decay(name + "_vel2decay", 0.0f) + , vel2sustain(name + "_vel2sustain", 0.0f) + , vel2release(name + "_vel2release", 0.0f) + {} + //! Helper function to update all of the cached modulations for the individual opcodes + void updateCachedModulation(const std::array& ccValues) + { + delay.updateCachedModulation(ccValues); + attack.updateCachedModulation(ccValues); + hold.updateCachedModulation(ccValues); + decay.updateCachedModulation(ccValues); + sustain.updateCachedModulation(ccValues); + release.updateCachedModulation(ccValues); + } + //! Helper function for parsing all these envelope generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter envelopes + void parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful); +}; + + + + +// Specific Template specializations for Enum-like opcodes + +// Trigger Type +enum class TriggerType +{ + Attack, + Release, + //First, // TODO To be implemented + //Legato, // TODO To be implemented + //ReleaseKey // TODO To be implemented +}; +template<> void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful); + + + +enum class LoopMode +{ + NoLoop, + OneShot, + //LoopContinuous, // To be implemented + //LoopSustain +}; +template<> void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful); + + + +enum class FilterType +{ + Lowpass1Pole, + Lowpass2Pole, + Highpass1Pole, + Highpass2Pole, + Bandpass2Pole, + Bandstop2Pole +}; +template<> void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful); + + } // namespace lmms -#endif // LMMS_SFZ_OPCODE_STATE_H \ No newline at end of file +#endif // LMMS_SFZ_OPCODES \ No newline at end of file diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 3078d148c00..11850683c25 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -262,7 +262,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou if (match.hasMatch()) { int ccNumber = match.captured(0).split("cc")[1].toInt(); - if (ccNumber >= 0 && ccNumber <= SfzOpcodeState::NumMidiCCs) { controlsConfig.m_activeMidiCCs.at(ccNumber) = true; } + if (ccNumber >= 0 && ccNumber <= NumMidiCCs) { controlsConfig.m_activeMidiCCs.at(ccNumber) = true; } } } // Check one last time in case the file ended with a region and didn't get added @@ -273,14 +273,14 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // make a list of them in the controlsConfig object for easy access for (const auto& region : outputRegions) { - if (region.m_sw_last != std::nullopt) + if (region.m_sw_last.value() != std::nullopt) { SfzControlsConfig::SwitchKeyInfo info; - info.sw_label = region.m_sw_label.value_or(""); - info.sw_lokey = region.m_sw_lokey; - info.sw_hikey = region.m_sw_hikey; - info.sw_default = region.m_sw_default; - controlsConfig.m_switchKeyInfo.insert({region.m_sw_last.value(), info}); + info.sw_label = region.m_sw_label.value().value_or(""); + info.sw_lokey = region.m_sw_lokey.value(); + info.sw_hikey = region.m_sw_hikey.value(); + info.sw_default = region.m_sw_default.value(); + controlsConfig.m_switchKeyInfo.insert({region.m_sw_last.value().value(), info}); } } diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 11bfba3f550..9623fc6703d 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -39,7 +39,7 @@ SfzRegion::SfzRegion(SfzOpcodeState opcodeState) recalculateTotalCCModulation(SfzGlobalState()); // The region objects don't currently have direct access to the global state, so pass in a blank object just to reset the CC modulations to their defaults. // Cache which loccN/hiccN opcodes are defined in this region, so that we only have to loop through and check those upon a trigger, not all 128 - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + for (int i = 0; i < NumMidiCCs; ++i) { if (m_locc.at(i) != 0 || m_hicc.at(i) != 127) { m_lohiccDefinedCCNumbers.push_back(i); } } @@ -61,20 +61,20 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // Ensure the key was pressed between the `lokey` and `hikey` opcodes // TODO this can be removed now that SfzRegionManager handles lookup tables for regions by key - if (triggerKey > m_hikey || triggerKey < m_lokey) { return false; } + if (triggerKey > m_hikey.value() || triggerKey < m_lokey.value()) { return false; } // And had velocity between `lovel` and `hivel` opcodes - if (triggerVelocity > m_hivel || triggerVelocity < m_lovel) { return false; } + if (triggerVelocity > m_hivel.value() || triggerVelocity < m_lovel.value()) { return false; } // If a keyswitch range was defined, ensure the last pressed valid switch key in that range matches the specified keyswitch for this region // The argument `true` at the end signifies that only switch keys will be considered // TODO add unit tests - if (m_sw_last != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default, true) != m_sw_last) { return false; } + if (m_sw_last.value() != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey.value(), m_sw_hikey.value(), m_sw_default.value(), true) != m_sw_last.value()) { return false; } } // If the region uses lorand/hirand, the current random value stored in the global state (updated every trigger) is compared with the range // Note: the upper bound is inclusive, so lorand=0.2 hirand=0.4 will be triggered by any rand value >0.2 or <=0.4 - if (globalState.rand() > m_hirand || globalState.rand() <= m_lorand) { return false; } + if (globalState.rand() > m_hirand.value() || globalState.rand() <= m_lorand.value()) { return false; } // If midi CC ranges are defined, make sure the current CC values are within range // Only loop over the CC's which have lo/hiccN defined, instead of checking all 128 every time @@ -86,7 +86,7 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. m_roundRobinCount++; // TODO it would be nice if this function could be const and we didn't have to update this here. idk. - if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn + if (m_roundRobinCount % m_seq_length.value() != m_seq_position.value() - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn // If all the contitions passed, return true and spawn a sound return true; @@ -95,7 +95,7 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger) { // Notify the global state whether a switch key has been pressed, so that it can correctly track when `sw_last` is met - if (trigger.type() == SfzTrigger::Type::NoteOn && m_sw_last != std::nullopt && m_sw_last == trigger.key().value()) + if (trigger.type() == SfzTrigger::Type::NoteOn && m_sw_last.value() != std::nullopt && m_sw_last.value() == trigger.key().value()) { globalState.switchKeyPressed(trigger.key().value()); // TODO this can probably be moved somewhere else so that it isn't done for every region (if you have 10000+ regions, it needs to be optimized) } @@ -103,42 +103,18 @@ void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& tr // Before spawning a sound, do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer if (trigger.type() == SfzTrigger::Type::ControlChange) { - //recalculateTotalCCModulation(globalState); - m_ampeg.updateCachedModulation(globalState.midiCCValues()); + recalculateTotalCCModulation(globalState); } } - - -float SfzRegion::totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const -{ - float total = 0.0f; - // TODO this may be optimized if we only loop through the CC's which are actually used by the region - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) - { - total += ccModulationAmounts.at(i) * globalState.midiCCValue(i) / 128.0f; - } - return total; -} - void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) { - m_amplitude_totalCC = totalCCModulation(m_amplitude_oncc, globalState); - - /*TODO - m_ampeg_delay_totalCC = totalCCModulation(m_ampeg_delay_oncc, globalState); - m_ampeg_attack_totalCC = totalCCModulation(m_ampeg_attack_oncc, globalState); - m_ampeg_hold_totalCC = totalCCModulation(m_ampeg_hold_oncc, globalState); - m_ampeg_decay_totalCC = totalCCModulation(m_ampeg_decay_oncc, globalState); - m_ampeg_sustain_totalCC = totalCCModulation(m_ampeg_sustain_oncc, globalState); - m_ampeg_release_totalCC = totalCCModulation(m_ampeg_release_oncc, globalState); - */ - - m_gain_totalCC = totalCCModulation(m_gain_oncc, globalState); - m_pan_totalCC = totalCCModulation(m_pan_oncc, globalState); - // TODO more -} + m_amplitude.updateCachedModulation(globalState.midiCCValues()); + m_volume.updateCachedModulation(globalState.midiCCValues()); + m_pan.updateCachedModulation(globalState.midiCCValues()); + m_ampeg.updateCachedModulation(globalState.midiCCValues()); +} bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool) diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 1e38455bb94..3f82fa09bfe 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -67,26 +67,11 @@ class SfzRegion : public SfzOpcodeState //! In order to do round robin, the region needs to keep track of how many notes it has played in its lifetime. Or rather, the number of notes it *would* have played if it weren't restricted to only play a note when the round-robin counter hit the right numbers. int m_roundRobinCount = 0; - //! Store the current total midi CC modulation amounts for the different targets, just so that we don't - // have to recalculate them every buffer, instead only when a trigger occurs. - float m_amplitude_totalCC = 0.0f; - - float m_ampeg_delay_totalCC = 0.0f; - float m_ampeg_attack_totalCC = 0.0f; - float m_ampeg_hold_totalCC = 0.0f; - float m_ampeg_decay_totalCC = 0.0f; - float m_ampeg_sustain_totalCC = 0.0f; - float m_ampeg_release_totalCC = 0.0f; - - float m_gain_totalCC = 0.0f; - float m_pan_totalCC = 0.0f; - //! When checking whether all the current CC values fall between loccN and hiccN, it's useful to only check the ones where lo/hiccN is actually defined, not all 128 //! This vector stores a list of which CC numbers have lo/hiccN opcodes in this region std::vector m_lohiccDefinedCCNumbers; - //! Helper function to calculate the total modulation of all midi CC controllers on a parameter. Essentially it just multiplies the modulation amounts by the current CC values and adds it all up. - float totalCCModulation(const std::array& ccModulationAmounts, const SfzGlobalState& globalState) const; + //! Helper function to update the total modulation of all midi CC controllers on each modulatable parameter. void recalculateTotalCCModulation(const SfzGlobalState& globalState); friend class SfzRegionPlayState; // TODO this was just to make it easy but...? diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index 2639b754933..b1f86ef2904 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -39,15 +39,15 @@ SfzRegionManager::SfzRegionManager(std::vector& regions) { for (auto& region : m_regions) { - if (key >= region.m_lokey && key <= region.m_hikey) + if (key >= region.m_lokey.value() && key <= region.m_hikey.value()) { // Additionally sort by trigger type switch (region.m_trigger.value()) { - case SfzOpcodeState::TriggerType::Attack: + case TriggerType::Attack: m_noteOnRegions.at(key).push_back(®ion); break; - case SfzOpcodeState::TriggerType::Release: + case TriggerType::Release: m_noteOffRegions.at(key).push_back(®ion); break; } diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 380389a2913..285778c4d7c 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -46,34 +46,34 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger // Delay the start of the playback by the trigger offset m_frameCount -= trigger.frameOffset(); // And by the delay opcode in seconds - m_frameCount -= region->m_delay * sampleRate; + m_frameCount -= region->m_delay.value() * sampleRate; // And any random delay amount - m_frameCount -= fastRand(1.0f) * region->m_delay_random * sampleRate; + m_frameCount -= fastRand(1.0f) * region->m_delay_random.value() * sampleRate; // Set initial sample start frame offset - m_sampleFrame += m_region->m_offset; + m_sampleFrame += m_region->m_offset.value(); // Setup the filter - switch (m_region->m_fil_type) + switch (m_region->m_fil_type.value()) { // This is not correct! I'm using the default filters from BasicFilters, but I don't believe they necessarily match the 1 pole vs 2 pole specifications for sfz. // For example, the default lowpass is a biquad, which I believe has 2 poles. For now I'm using it for both lowpass types, but it should probably be changed in the future. - case SfzOpcodeState::FilterType::Lowpass1Pole: + case FilterType::Lowpass1Pole: m_filter.setFilterType(BasicFilters<2>::FilterType::LowPass); break; - case SfzOpcodeState::FilterType::Lowpass2Pole: + case FilterType::Lowpass2Pole: m_filter.setFilterType(BasicFilters<2>::FilterType::LowPass); break; - case SfzOpcodeState::FilterType::Highpass1Pole: + case FilterType::Highpass1Pole: m_filter.setFilterType(BasicFilters<2>::FilterType::HiPass); break; - case SfzOpcodeState::FilterType::Highpass2Pole: + case FilterType::Highpass2Pole: m_filter.setFilterType(BasicFilters<2>::FilterType::HiPass); break; - case SfzOpcodeState::FilterType::Bandpass2Pole: + case FilterType::Bandpass2Pole: m_filter.setFilterType(BasicFilters<2>::FilterType::BandPass_CSG); // I am not well versed in the difference between BandPass_CSG and BandPass_CZPG. It seems to be something about how it uses Q/resonance? I'm not sure. break; - case SfzOpcodeState::FilterType::Bandstop2Pole: + case FilterType::Bandstop2Pole: m_filter.setFilterType(BasicFilters<2>::FilterType::Notch); break; } @@ -134,11 +134,11 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; // Calculate pitch difference relative to original sample - const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter; + const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); // The total pitch depends on 1. the key offset 2. the fine `tune` adjustment 3. the velocity, if pitch_veltrack is nonzero - const float pitch = semitoneDifference * m_region->m_pitch_keytrack / 100.0f - + m_region->m_tune / 100.0f - + normalizedVelocity * m_region->m_pitch_veltrack / 100.0f; + const float pitch = semitoneDifference * m_region->m_pitch_keytrack.value() / 100.0f + + m_region->m_tune.value() / 100.0f + + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f; float freqRatio = std::exp2(pitch / 12.0f); // Sample rate of sample @@ -149,42 +149,42 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) freqRatio *= sampleSampleRate / sampleRate; // Amplitude - const float amplitude = (m_region->m_amplitude + m_region->m_amplitude_totalCC) / 100.0f; // Amplitude is stored as a percent + const float amplitude = m_region->m_amplitude.modulatedValue() / 100.0f; // Amplitude is stored as a percent // Amplitude envelope - const f_cnt_t ampegDelayFrames = (m_region->m_ampeg.delay.value() + m_region->m_ampeg.delay.cachedModulation) * sampleRate; - const f_cnt_t ampegAttackFrames = (m_region->m_ampeg.attack.value() + m_region->m_ampeg.attack.cachedModulation) * sampleRate; - const f_cnt_t ampegHoldFrames = (m_region->m_ampeg.hold.value() + m_region->m_ampeg.hold.cachedModulation) * sampleRate; - const f_cnt_t ampegDecayFrames = (m_region->m_ampeg.decay.value() + m_region->m_ampeg.decay.cachedModulation) * sampleRate; - const float ampegSustain = (m_region->m_ampeg.sustain.value() + m_region->m_ampeg.sustain.cachedModulation) / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = (m_region->m_ampeg.release.value() + m_region->m_ampeg.release.cachedModulation) * sampleRate; + const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.modulatedValue() * sampleRate; + const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.modulatedValue() * sampleRate; + const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.modulatedValue() * sampleRate; + const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.modulatedValue() * sampleRate; + const float ampegSustain = m_region->m_ampeg.sustain.modulatedValue() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.modulatedValue() * sampleRate; // Amplitude due to velocity // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. // Essentially this means lerping between y = x, y = 1, and y = 1 - x, if you think of y being the amp and x being vel/127 - const float ampVelocity = m_region->m_amp_veltrack > 0 - ? (normalizedVelocity) * (m_region->m_amp_veltrack / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack / 100) - : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack / -100); + const float ampVelocity = m_region->m_amp_veltrack.value() > 0 + ? (normalizedVelocity) * (m_region->m_amp_veltrack.value() / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / 100) + : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack.value() / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / -100); // Amplitude due to volume/gain - const float ampVolume = dbfsToAmp(m_region->m_volume + m_region->m_gain_totalCC); + const float ampVolume = dbfsToAmp(m_region->m_volume.modulatedValue()); // Panning - const float pan = (m_region->m_pan + m_region->m_pan_totalCC) / 100; + const float pan = m_region->m_pan.modulatedValue() / 100; const float rightPanAmp = std::min(1.0f, 1.0f + pan); const float leftPanAmp = std::min(1.0f, 1.0f - pan); // Filter - const bool filterEnabled = m_region->m_cutoff != std::nullopt; + const bool filterEnabled = m_region->m_cutoff.value() != std::nullopt; // For performance, only update the cutoff frequency/resonance once per buffer if (filterEnabled) { // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I saw 20000 being used in Metal GTX which is kind of high for cents (200 octaves?) - const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack; - const float filterCutoff = m_region->m_cutoff.value() + std::exp2(filterCutoffPitchOffset / 12.0f); + const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack.value(); + const float filterCutoff = m_region->m_cutoff.value().value() + std::exp2(filterCutoffPitchOffset / 12.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it - const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. + const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance.value()); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. m_filter.calcFilterCoeffs(filterCutoff, q); } @@ -221,7 +221,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) } // If loop_mode is one_shot, no noteOff signal will ever come to release it, so we need to manually deactivate when we reach the end of the sample - if (m_region->m_loop_mode == SfzOpcodeState::LoopMode::OneShot && m_sampleFrame >= m_region->sample()->size()) + if (m_region->m_loop_mode.value() == LoopMode::OneShot && m_sampleFrame >= m_region->sample()->size()) { m_active = false; // TODO should this forcefully decative or just release? } @@ -236,7 +236,7 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) if (trigger.type() == SfzTrigger::Type::NoteOff) { - if (m_region->m_loop_mode == SfzOpcodeState::LoopMode::OneShot) { return; } // If one_shot looping is enabled, the whole sample will play regardless of if the note is released + if (m_region->m_loop_mode.value() == LoopMode::OneShot) { return; } // If one_shot looping is enabled, the whole sample will play regardless of if the note is released if (trigger.key() == m_trigger.key()) { diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index d0f9a820b5d..8022580b840 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -200,7 +200,7 @@ void SfzSampler::loadFile(const QString& filePath) { loadSfzFile(filePath); // Reset the instrument track's midi CC knobs to the defaults of the SFZ - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + for (int i = 0; i < NumMidiCCs; ++i) { m_parentTrack->midiCCModel(i)->setInitValue(m_sfzGlobalState.midiCCValue(i)); // For some reason it seems calling `setValue` on the CC models doesn't send a midi event to the instrument when doing drag/drop, @@ -268,7 +268,7 @@ void SfzSampler::loadSettings(const QDomElement& element) loadSfzFile(m_sfzFilePath); } // Sync the internal CC values so that saved presets/projects work normally upon loading - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + for (int i = 0; i < NumMidiCCs; ++i) { processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); } diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 3e08ecc65a0..9b9fecad728 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -97,7 +97,7 @@ void SfzSamplerView::onFileLoaded() // Initialize new knobs int activeControlCount = 0; - for (int i = 0; i < SfzOpcodeState::NumMidiCCs; ++i) + for (int i = 0; i < NumMidiCCs; ++i) { // Only add a knob if the control is actually used if (m_instrument->m_controlsConfig.m_activeMidiCCs.at(i)) @@ -120,13 +120,13 @@ void SfzSamplerView::onFileLoaded() for (const auto& [key, info] : m_instrument->m_controlsConfig.m_switchKeyInfo) { QString label = QString("- %1 %2 [range: %3 - %4]") - .arg(SfzOpcodeState::keyNumToString(key)) + .arg(keyNumToString(key)) .arg(info.sw_label) - .arg(SfzOpcodeState::keyNumToString(info.sw_lokey)) - .arg(SfzOpcodeState::keyNumToString(info.sw_hikey)); + .arg(keyNumToString(info.sw_lokey)) + .arg(keyNumToString(info.sw_hikey)); if (info.sw_default != std::nullopt) { - label += QString(" default: %1").arg(SfzOpcodeState::keyNumToString(info.sw_default.value())); + label += QString(" default: %1").arg(keyNumToString(info.sw_default.value())); } switchKeyLabels.push_back(label); } From 218c826034d35c64e3ce3d30222a2098d4147cf0 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 1 May 2026 15:33:00 -0400 Subject: [PATCH 083/131] Make modulatedValue override value --- plugins/SfzSampler/SfzOpcodes.h | 8 ++++---- plugins/SfzSampler/SfzRegionPlayState.cpp | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index bf793290ad0..1c6e2f7a5bb 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -69,8 +69,8 @@ struct Opcode : BaseOpcode Opcode(QString name, T defaultValue) : m_opcodeNames({name}), m_value(defaultValue) {} Opcode(std::vector names, T defaultValue) : m_opcodeNames(names), m_value(defaultValue) {} - void setValue(const T& value) { m_value = value; } - const T& value() const { return m_value; } + virtual void setValue(const T& value) { m_value = value; } + virtual const T value() const { return m_value; } void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; }; @@ -103,8 +103,8 @@ struct ModulatableOpcode : FloatOpcode ModulatableOpcode(QString name = "", float defaultValue = 0.0f) : FloatOpcode(name, defaultValue) {}; ModulatableOpcode(std::vector names = {}, float defaultValue = 0.0f) : FloatOpcode(names, defaultValue) {}; - //! Function which returns the sum of the base opcode value and whatever the modulation is currently - float modulatedValue() const { return m_value + cachedModulation; } + //! Redefine the value() function to return the sum of the base opcode value and whatever the modulation is currently + const float value() const override { return m_value + cachedModulation; } //! Helper function for parsing these kinds of opcodes, where you have both `opcode` and `opcode_onccN` where N is the midi cc number, so // that the code isn't duplicated for every modulatable parameter. void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 285778c4d7c..8baea156207 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -149,15 +149,15 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) freqRatio *= sampleSampleRate / sampleRate; // Amplitude - const float amplitude = m_region->m_amplitude.modulatedValue() / 100.0f; // Amplitude is stored as a percent + const float amplitude = m_region->m_amplitude.value() / 100.0f; // Amplitude is stored as a percent // Amplitude envelope - const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.modulatedValue() * sampleRate; - const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.modulatedValue() * sampleRate; - const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.modulatedValue() * sampleRate; - const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.modulatedValue() * sampleRate; - const float ampegSustain = m_region->m_ampeg.sustain.modulatedValue() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.modulatedValue() * sampleRate; + const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.value() * sampleRate; + const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.value() * sampleRate; + const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.value() * sampleRate; + const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.value() * sampleRate; + const float ampegSustain = m_region->m_ampeg.sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.value() * sampleRate; // Amplitude due to velocity // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) @@ -168,10 +168,10 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack.value() / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / -100); // Amplitude due to volume/gain - const float ampVolume = dbfsToAmp(m_region->m_volume.modulatedValue()); + const float ampVolume = dbfsToAmp(m_region->m_volume.value()); // Panning - const float pan = m_region->m_pan.modulatedValue() / 100; + const float pan = m_region->m_pan.value() / 100; const float rightPanAmp = std::min(1.0f, 1.0f + pan); const float leftPanAmp = std::min(1.0f, 1.0f - pan); From 1b5e56e0a7ccc5d000fe59a8d13a53cdc2c8a17e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 1 May 2026 16:15:27 -0400 Subject: [PATCH 084/131] Make delay modulatable --- plugins/SfzSampler/SfzOpcodeState.h | 2 +- plugins/SfzSampler/SfzOpcodes.cpp | 2 +- plugins/SfzSampler/SfzRegion.cpp | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 08752fed0d5..bf65add7d32 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -106,7 +106,7 @@ class SfzOpcodeState // // Delay // - FloatOpcode m_delay {"delay", 0.0f}; // In seconds + ModulatableOpcode m_delay {"delay", 0.0f}; // In seconds FloatOpcode m_delay_random {"delay_random", 0.0f}; diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp index d2f57f60715..6f58366a244 100644 --- a/plugins/SfzSampler/SfzOpcodes.cpp +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -90,7 +90,7 @@ void ModulatableOpcode::parseFromString(const QString& opcodeName, const QString m_value = opcodeValue.toFloat(successful); *parsed = true; } - else if (opcodeName.startsWith(alias + "_oncc") || opcodeName.startsWith(alias + "cc")) + else if (opcodeName.startsWith(alias + "_oncc") || opcodeName.startsWith(alias + "_cc") || opcodeName.startsWith(alias + "cc")) { value_oncc.at(ccNumberFromOpcode(opcodeName)) = opcodeValue.toFloat(successful); *parsed = true; diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 9623fc6703d..c95559d1b35 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -112,6 +112,7 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) m_amplitude.updateCachedModulation(globalState.midiCCValues()); m_volume.updateCachedModulation(globalState.midiCCValues()); m_pan.updateCachedModulation(globalState.midiCCValues()); + m_delay.updateCachedModulation(globalState.midiCCValues()); m_ampeg.updateCachedModulation(globalState.midiCCValues()); } From 1dbefbf576870e963b9536a61f5c96f54600c2f1 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 1 May 2026 17:01:39 -0400 Subject: [PATCH 085/131] Fix order of assignment --- plugins/SfzSampler/SfzOpcodes.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp index 6f58366a244..1f71601ec87 100644 --- a/plugins/SfzSampler/SfzOpcodes.cpp +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -80,7 +80,9 @@ void StringOpcode::parseFromString(const QString& opcodeName, const QString& opc // Modulatable Opcodes are a type of float opcode which can be adjusted based on different midi CC knob values // They have both an initial value defined by their opcodename=value // but also opcodename_oncc123=modulation_amount +// or opcodename_cc123=modulation_amount // or opcodenamecc123=modulation_amount +// will make the value be increased proportional to modulation_amount when the midi CC knob 123 is turned up void ModulatableOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { for (QString alias : m_opcodeNames) @@ -147,6 +149,7 @@ template<> void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + *parsed = true; if (opcodeValue == "attack") { m_value = TriggerType::Attack; } else if (opcodeValue == "release") { m_value = TriggerType::Release; } @@ -158,7 +161,6 @@ void Opcode::parseFromString(const QString& opcodeName, const QStri qDebug() << "[SFZ Parser] Unknown trigger:" << opcodeValue; return; } - *parsed = true; *successful = true; } @@ -167,6 +169,7 @@ template<> void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + *parsed = true; if (opcodeValue == "no_loop") { m_value = LoopMode::NoLoop; } else if (opcodeValue == "one_shot") { m_value = LoopMode::OneShot; } @@ -177,7 +180,6 @@ void Opcode::parseFromString(const QString& opcodeName, const QString& qDebug() << "[SFZ Parser] Unknown loop_mode:" << opcodeValue; return; } - *parsed = true; *successful = true; } @@ -186,6 +188,7 @@ template<> void Opcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } + *parsed = true; if (opcodeValue == "lpf_1p") { m_value = FilterType::Lowpass1Pole; } else if (opcodeValue == "lpf_2p") { m_value = FilterType::Lowpass2Pole; } @@ -198,7 +201,6 @@ void Opcode::parseFromString(const QString& opcodeName, const QStrin qDebug() << "[SFZ Parser] Unknown filter type:" << opcodeValue; return; } - *parsed = true; *successful = true; } From 0fea1ccb7cb975fdd6d33574e97c77398afa502f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 2 May 2026 19:39:23 -0400 Subject: [PATCH 086/131] Add basic waves and pitcheg --- plugins/SfzSampler/CMakeLists.txt | 3 + plugins/SfzSampler/SfzBasicWaves.cpp | 87 +++++++++++++++++++++++ plugins/SfzSampler/SfzBasicWaves.h | 55 ++++++++++++++ plugins/SfzSampler/SfzOpcodeState.cpp | 3 +- plugins/SfzSampler/SfzOpcodeState.h | 6 ++ plugins/SfzSampler/SfzOpcodes.cpp | 2 + plugins/SfzSampler/SfzOpcodes.h | 6 ++ plugins/SfzSampler/SfzRegion.cpp | 26 +++++-- plugins/SfzSampler/SfzRegion.h | 5 ++ plugins/SfzSampler/SfzRegionPlayState.cpp | 71 +++++++++++++----- 10 files changed, 240 insertions(+), 24 deletions(-) create mode 100644 plugins/SfzSampler/SfzBasicWaves.cpp create mode 100644 plugins/SfzSampler/SfzBasicWaves.h diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzSampler/CMakeLists.txt index a002b1434c7..cc52e98ea61 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzSampler/CMakeLists.txt @@ -26,6 +26,8 @@ build_plugin(sfzsampler SfzSampleBuffer.h SfzSamplePool.cpp SfzSamplePool.h + SfzBasicWaves.cpp + SfzBasicWaves.h MOCFILES SfzSampler.h SfzSamplerView.h @@ -40,5 +42,6 @@ build_plugin(sfzsampler SfzTrigger.h SfzSampleBuffer.h SfzSamplePool.h + SfzBasicWaves.h EMBEDDED_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.svg" ) diff --git a/plugins/SfzSampler/SfzBasicWaves.cpp b/plugins/SfzSampler/SfzBasicWaves.cpp new file mode 100644 index 00000000000..24c18d9a548 --- /dev/null +++ b/plugins/SfzSampler/SfzBasicWaves.cpp @@ -0,0 +1,87 @@ +/* + * SfzBasicWaves.cpp - Generators of basic wave shapes + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "SfzBasicWaves.h" +#include "lmms_math.h" +#include + +namespace lmms +{ + +const float SfzBasicWaves::generate(Shape shape, float sampleRate, int frame) +{ + // Amount through period + // By default, the basic waves are played at C4 (midi key 60). This isn't in the spec afaik, but it makes things simple since pitch_keycenter defaults to 60. + constexpr float baseFreq = 261.625565301f; + const float t = absFraction(frame * baseFreq / sampleRate); + // The region playback state handles incrementing the frame count at different rates based on the pitch, so we don't need to handle the actual frequency calculation here. + + switch (shape) + { + case Shape::Sine: + return sine(t); + case Shape::Saw: + return saw(t); + case Shape::Square: + return square(t); + case Shape::Triangle: + return triangle(t); + case Shape::Noise: + return noise(t); + case Shape::Silence: + return 0.0f; + } + return 0.0f; +} + + +const float SfzBasicWaves::sine(float t) +{ + return sin(t * 2 * std::numbers::pi); +} + +const float SfzBasicWaves::saw(float t) +{ + return 2.0f * t - 1.0f; +} + +const float SfzBasicWaves::square(float t) +{ + return t > 0.5 ? 1.0f : -1.0f; +} + +const float SfzBasicWaves::triangle(float t) +{ + return t < 0.5f + ? 4.0f * t - 1.0f + : -(4.0f * (t - 0.5f) - 1.0f); +} + +const float SfzBasicWaves::noise(float t) +{ + return fastRand(-1.0f, 1.0f); +} + + +} // namespace lmms diff --git a/plugins/SfzSampler/SfzBasicWaves.h b/plugins/SfzSampler/SfzBasicWaves.h new file mode 100644 index 00000000000..e93b6350c3d --- /dev/null +++ b/plugins/SfzSampler/SfzBasicWaves.h @@ -0,0 +1,55 @@ +/* + * SfzBasicWaves.h - Generators for audio wave shapes such as sine, saw, triangle, noise, etc + * + * Copyright (c) 2026 Keratin + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_SFZ_BASIC_WAVES_H +#define LMMS_SFZ_BASIC_WAVES_H + +namespace lmms +{ + +class SfzBasicWaves +{ +public: + enum class Shape + { + Sine, + Saw, + Square, + Triangle, + Noise, + Silence + }; + static const float generate(Shape shape, float sampleRate, int frame); + +private: + static const float sine(float t); + static const float saw(float t); + static const float square(float t); + static const float triangle(float t); + static const float noise(float t); +}; + +} // namespace lmms + +#endif // LMMS_SFZ_BASIC_WAVES_H diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 9f2437dc7e4..b3bfa4c655e 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -88,7 +88,8 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu }; std::vector envelopeGeneratorList = { - &m_ampeg + &m_ampeg, + &m_pitcheg }; bool parsed = false; diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index bf65add7d32..052b28376c5 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -141,6 +141,12 @@ class SfzOpcodeState EnvelopeOpcodes m_ampeg {"ampeg"}; + // + // Pitch Envelope Generator (pitcheg) + // + EnvelopeOpcodes m_pitcheg {"pitcheg"}; + + // // Misc Volume // diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp index 1f71601ec87..6b41d63ab00 100644 --- a/plugins/SfzSampler/SfzOpcodes.cpp +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -129,6 +129,7 @@ void EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const decay.parseFromString(opcode, value, parsed, successful); sustain.parseFromString(opcode, value, parsed, successful); release.parseFromString(opcode, value, parsed, successful); + depth.parseFromString(opcode, value, parsed, successful); vel2delay.parseFromString(opcode, value, parsed, successful); vel2attack.parseFromString(opcode, value, parsed, successful); @@ -136,6 +137,7 @@ void EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const vel2decay.parseFromString(opcode, value, parsed, successful); vel2sustain.parseFromString(opcode, value, parsed, successful); vel2release.parseFromString(opcode, value, parsed, successful); + vel2depth.parseFromString(opcode, value, parsed, successful); } diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index 1c6e2f7a5bb..d2a22fd9998 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -123,6 +123,8 @@ struct EnvelopeOpcodes ModulatableOpcode decay; ModulatableOpcode sustain; ModulatableOpcode release; + // Depth is only used for pitcheg and fileg, not ampeg, but we have it here anyway + ModulatableOpcode depth; // Velocity modulation amount FloatOpcode vel2delay; FloatOpcode vel2attack; @@ -130,6 +132,7 @@ struct EnvelopeOpcodes FloatOpcode vel2decay; FloatOpcode vel2sustain; FloatOpcode vel2release; + FloatOpcode vel2depth; // Initialize opcodes with correct names and default values EnvelopeOpcodes(QString name) @@ -139,12 +142,14 @@ struct EnvelopeOpcodes , decay(name + "_decay", 0.0f) , sustain(name + "_sustain", 100.0f) , release(name + "_release", 0.0f) + , depth(name + "_depth", 0.0f) , vel2delay(name + "_vel2delay", 0.0f) , vel2attack(name + "_vel2attack", 0.0f) , vel2hold(name + "_vel2hold", 0.0f) , vel2decay(name + "_vel2decay", 0.0f) , vel2sustain(name + "_vel2sustain", 0.0f) , vel2release(name + "_vel2release", 0.0f) + , vel2depth(name + "_vel2depth", 0.0f) {} //! Helper function to update all of the cached modulations for the individual opcodes void updateCachedModulation(const std::array& ccValues) @@ -155,6 +160,7 @@ struct EnvelopeOpcodes decay.updateCachedModulation(ccValues); sustain.updateCachedModulation(ccValues); release.updateCachedModulation(ccValues); + depth.updateCachedModulation(ccValues); } //! Helper function for parsing all these envelope generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter envelopes void parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful); diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index c95559d1b35..c978a616e88 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -115,6 +115,7 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) m_delay.updateCachedModulation(globalState.midiCCValues()); m_ampeg.updateCachedModulation(globalState.midiCCValues()); + m_pitcheg.updateCachedModulation(globalState.midiCCValues()); } @@ -127,12 +128,27 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam return false; } - QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value().value_or(""))); // TODO - QString path = defaultDirectory.absoluteFilePath(m_sampleFile.value().value()); // TODO - // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory - m_sample = samplePool.loadSample(path); + // There are some special sample keywords for simple wave shapes, such as *sine, *triangle, *silence, etc + // If one of them is used, we don't load a sample file, instead leave m_sample as nullptr and set m_basicWaveShape + // to let the region know that we are doing a procedurally generated wave + if (m_sampleFile.value() == "*sine") { m_basicWaveShape = SfzBasicWaves::Shape::Sine; } + else if (m_sampleFile.value() == "*saw") { m_basicWaveShape = SfzBasicWaves::Shape::Saw; } + else if (m_sampleFile.value() == "*square") { m_basicWaveShape = SfzBasicWaves::Shape::Square; } + else if (m_sampleFile.value() == "*triangle") { m_basicWaveShape = SfzBasicWaves::Shape::Triangle; } + else if (m_sampleFile.value() == "*tri") { m_basicWaveShape = SfzBasicWaves::Shape::Triangle; } + else if (m_sampleFile.value() == "*noise") { m_basicWaveShape = SfzBasicWaves::Shape::Noise; } + else if (m_sampleFile.value() == "*silence") { m_basicWaveShape = SfzBasicWaves::Shape::Silence; } + else + { + // If it's not a basic wave, load the real sample file. + QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value().value_or(""))); // TODO + QString path = defaultDirectory.absoluteFilePath(m_sampleFile.value().value()); // TODO + // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory + m_sample = samplePool.loadSample(path); - return m_sample != nullptr; + return m_sample != nullptr; + } + return true; } diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 3f82fa09bfe..c02afa3c548 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -31,6 +31,7 @@ #include "SfzRegionPlayState.h" #include "SfzSampleBuffer.h" #include "SfzSamplePool.h" +#include "SfzBasicWaves.h" #include @@ -58,11 +59,15 @@ class SfzRegion : public SfzOpcodeState bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool); const SfzSampleBuffer* sample() const { return m_sample; } + const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } private: //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. const SfzSampleBuffer* m_sample = nullptr; + //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) + SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; + //! In order to do round robin, the region needs to keep track of how many notes it has played in its lifetime. Or rather, the number of notes it *would* have played if it weren't restricted to only play a note when the round-robin counter hit the right numbers. int m_roundRobinCount = 0; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 8baea156207..f76aea94ae6 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -133,31 +133,48 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; + + // Sample rate of LMMS + const float lmmsSampleRate = Engine::audioEngine()->outputSampleRate(); + // Sample rate of sample (if we are using a sample, not a basic wave like *sine or *saw) + const float sampleSampleRate = m_region->sample() != nullptr + ? m_region->sample()->sampleRate() + : lmmsSampleRate; // If we are using a basic wave instead of a sample, set it to LMMS's sample rate + + + // Pitch envelope + // This is only computed once per buffer to same compute, since doing it per-frame seems a bit excessive + const float pitcheg = envelopeGenerator( + m_region->m_pitcheg.delay.value() * lmmsSampleRate, + m_region->m_pitcheg.attack.value() * lmmsSampleRate, + m_region->m_pitcheg.hold.value() * lmmsSampleRate, + m_region->m_pitcheg.decay.value() * lmmsSampleRate, + m_region->m_pitcheg.sustain.value() / 100.0f, // Sustain is stored in percent, so divide by 100 to get ratio, + m_region->m_pitcheg.release.value() * lmmsSampleRate + ) * m_region->m_pitcheg.depth.value(); + // Calculate pitch difference relative to original sample const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); - // The total pitch depends on 1. the key offset 2. the fine `tune` adjustment 3. the velocity, if pitch_veltrack is nonzero + // The total pitch depends on 1. the key offset 2. the fine `tune` adjustment 3. the velocity, if pitch_veltrack is nonzero 4. the pitch envelope const float pitch = semitoneDifference * m_region->m_pitch_keytrack.value() / 100.0f + m_region->m_tune.value() / 100.0f - + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f; + + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f + + pitcheg / 100.0f; float freqRatio = std::exp2(pitch / 12.0f); - // Sample rate of sample - const float sampleSampleRate = m_region->sample()->sampleRate(); - // Sample rate of LMMS - const float sampleRate = Engine::audioEngine()->outputSampleRate(); // Play the sample faster/slower to match the correct sample rate - freqRatio *= sampleSampleRate / sampleRate; + freqRatio *= sampleSampleRate / lmmsSampleRate; // Amplitude const float amplitude = m_region->m_amplitude.value() / 100.0f; // Amplitude is stored as a percent // Amplitude envelope - const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.value() * sampleRate; - const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.value() * sampleRate; - const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.value() * sampleRate; - const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.value() * sampleRate; + const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.value() * lmmsSampleRate; + const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.value() * lmmsSampleRate; + const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.value() * lmmsSampleRate; + const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.value() * lmmsSampleRate; const float ampegSustain = m_region->m_ampeg.sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.value() * sampleRate; + const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.value() * lmmsSampleRate; // Amplitude due to velocity // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) @@ -199,17 +216,35 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) ampegSustain, ampegReleaseFrames ); + // If a sample file was loaded, use the buffer to get the audio data + // Otherwise, if a basic wave shape is being used (like *sine, *saw, *silence, etc) use a function to generate the shape + float sampleLeftValue = 0.0f; + float sampleRightValue = 0.0f; + if (m_region->sample() != nullptr) // TODO: should this check be outside of the loop? + { + sampleLeftValue = m_region->sample()->at(m_sampleFrame, 0); + sampleRightValue = m_region->sample()->at(m_sampleFrame, 1); + } + else + { + sampleLeftValue = SfzBasicWaves::generate(m_region->basicWaveShape(), lmmsSampleRate, m_sampleFrame); + sampleRightValue = SfzBasicWaves::generate(m_region->basicWaveShape(), lmmsSampleRate, m_sampleFrame); + } + if (filterEnabled) // TODO does this if statement make it faster? { - buffer[f][0] += m_filter.update(m_region->sample()->at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); - buffer[f][1] += m_filter.update(m_region->sample()->at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); + buffer[f][0] += m_filter.update(sampleLeftValue * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); + buffer[f][1] += m_filter.update(sampleRightValue * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); } else { - buffer[f][0] += m_region->sample()->at(m_sampleFrame, 0) * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp; - buffer[f][1] += m_region->sample()->at(m_sampleFrame, 1) * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp; + buffer[f][0] += sampleLeftValue * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp; + buffer[f][1] += sampleRightValue * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp; } - m_sampleFrame = std::min(static_cast(m_region->sample()->size()), m_sampleFrame + freqRatio); + // Increment the frame count. If we are using a sample, make sure to stop at the end, but if we are using a basic wave like *sine or *saw, there's no need + m_sampleFrame = m_region->sample() != nullptr + ? std::min(static_cast(m_region->sample()->size()), m_sampleFrame + freqRatio) + : m_sampleFrame + freqRatio; m_frameCount++; } @@ -221,7 +256,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) } // If loop_mode is one_shot, no noteOff signal will ever come to release it, so we need to manually deactivate when we reach the end of the sample - if (m_region->m_loop_mode.value() == LoopMode::OneShot && m_sampleFrame >= m_region->sample()->size()) + if (m_region->m_loop_mode.value() == LoopMode::OneShot && m_region->sample() != nullptr && m_sampleFrame >= m_region->sample()->size()) { m_active = false; // TODO should this forcefully decative or just release? } From c1bc58989b712feb9bc707974259c95e7e1d5b39 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 3 May 2026 08:17:48 -0400 Subject: [PATCH 087/131] Add LFOs and pitch envelope/lfo --- plugins/SfzSampler/SfzOpcodeState.cpp | 11 +- plugins/SfzSampler/SfzOpcodeState.h | 9 + plugins/SfzSampler/SfzOpcodes.cpp | 12 ++ plugins/SfzSampler/SfzOpcodes.h | 31 +++- plugins/SfzSampler/SfzRegion.cpp | 3 + plugins/SfzSampler/SfzRegionPlayState.cpp | 214 ++++++++++++++-------- plugins/SfzSampler/SfzRegionPlayState.h | 19 ++ 7 files changed, 218 insertions(+), 81 deletions(-) diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index b3bfa4c655e..6866dccb10d 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -92,6 +92,11 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu &m_pitcheg }; + std::vector lfoGeneratorList = { + &m_amplfo, + &m_pitchlfo + }; + bool parsed = false; bool successful = true; @@ -100,12 +105,14 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu //if (parsed) { break; } // Do not break even if the opcode has already been parsed, since some opcodes like "key" are effectively aliases for multiple opcodes (lokey, hikey, and pitch_keytrack) opcode->parseFromString(name, value, &parsed, &successful); } - for (auto* envelopeGenerator : envelopeGeneratorList) { - //if (parsed) { break; } envelopeGenerator->parseEnvelopeGeneratorOpcode(name, value, &parsed, &successful); } + for (auto* lfoGenerator : lfoGeneratorList) + { + lfoGenerator->parseLfoGeneratorOpcode(name, value, &parsed, &successful); + } // The per-CC opcodes are handled separately here for now, just to make the code simpler diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index 052b28376c5..aaf56b38d69 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -140,12 +140,21 @@ class SfzOpcodeState // EnvelopeOpcodes m_ampeg {"ampeg"}; + // + // Amplitude LFO + // + LfoOpcodes m_amplfo {"amplfo"}; + // // Pitch Envelope Generator (pitcheg) // EnvelopeOpcodes m_pitcheg {"pitcheg"}; + // + // Pitch LFO + // + LfoOpcodes m_pitchlfo {"pitchlfo"}; // // Misc Volume diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp index 6b41d63ab00..e5b4529d27b 100644 --- a/plugins/SfzSampler/SfzOpcodes.cpp +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -141,6 +141,18 @@ void EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const } +// LFO Opcodes +void LfoOpcodes::parseLfoGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful) +{ + // Pass the opcode name/value to all of the bundled opcodes. + // If it matches one of them, that's great. If not, they will return and nothing will happen. + // TODO is this error handling with "successful" correct? + delay.parseFromString(opcode, value, parsed, successful); + fade.parseFromString(opcode, value, parsed, successful); + freq.parseFromString(opcode, value, parsed, successful); + depth.parseFromString(opcode, value, parsed, successful); +} + // diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index d2a22fd9998..6f4486b818a 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -112,8 +112,8 @@ struct ModulatableOpcode : FloatOpcode void updateCachedModulation(const std::array& ccValues); }; -// Additionally, things like amplitude, pitch, and filter freq envelopes and lfo's all have very similar parameters, so it makes sense to put -// them all together in a handy definition +// Things like amplitude, pitch, and filter freq envelopes and lfo's all have very similar parameters, so it makes sense to put +// them all together in handy definitions struct EnvelopeOpcodes { // Envelope parameters (these can be modulated by midi CCs) @@ -166,6 +166,33 @@ struct EnvelopeOpcodes void parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful); }; +// Simple SFZ1 LFO Opcodes, for amplfo, pitchlfo, and fillfo +struct LfoOpcodes +{ + ModulatableOpcode delay; + ModulatableOpcode fade; + ModulatableOpcode freq; + ModulatableOpcode depth; + + // Initialize opcodes with correct names and default values + LfoOpcodes(QString name) + : delay(name + "_delay", 0.0f) + , fade(name + "_fade", 0.0f) + , freq(name + "_freq", 0.0f) + , depth(name + "_depth", 0.0f) + {} + //! Helper function to update all of the cached modulations for the individual opcodes + void updateCachedModulation(const std::array& ccValues) + { + delay.updateCachedModulation(ccValues); + fade.updateCachedModulation(ccValues); + freq.updateCachedModulation(ccValues); + depth.updateCachedModulation(ccValues); + } + //! Helper function for parsing these lfo generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter lfos + void parseLfoGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful); +}; + diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index c978a616e88..5cea42b79e7 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -116,6 +116,9 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) m_ampeg.updateCachedModulation(globalState.midiCCValues()); m_pitcheg.updateCachedModulation(globalState.midiCCValues()); + + m_amplfo.updateCachedModulation(globalState.midiCCValues()); + m_pitchlfo.updateCachedModulation(globalState.midiCCValues()); } diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index f76aea94ae6..0df61370420 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -38,17 +38,19 @@ namespace lmms SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger) : m_active(true) - , m_filter(Engine::audioEngine()->outputSampleRate()) + , m_lmmsSampleRate(Engine::audioEngine()->outputSampleRate()) + , m_filter(m_lmmsSampleRate) , m_trigger(trigger) , m_region(region) { - const float sampleRate = Engine::audioEngine()->outputSampleRate(); + // Calculate the base pitch and amplitude + precomputeBaseValues(); // Delay the start of the playback by the trigger offset m_frameCount -= trigger.frameOffset(); // And by the delay opcode in seconds - m_frameCount -= region->m_delay.value() * sampleRate; + m_frameCount -= region->m_delay.value() * m_lmmsSampleRate; // And any random delay amount - m_frameCount -= fastRand(1.0f) * region->m_delay_random.value() * sampleRate; + m_frameCount -= fastRand(1.0f) * region->m_delay_random.value() * m_lmmsSampleRate; // Set initial sample start frame offset m_sampleFrame += m_region->m_offset.value(); @@ -81,6 +83,58 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger +void SfzRegionPlayState::precomputeBaseValues() +{ + // Helper variable + const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; + + // Compute the base pitch + // The pitch env/lfo will be applied on top of this, as they are calcualted per frame + + // Calculate pitch difference relative to original sample + const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); + // The base pitch depends on 1. the key offset, 2. the fine `tune` adjustment, 3. the velocity, if pitch_veltrack is nonzero + // These are all in cents, so divide by 100 to get semitones + const float pitch = semitoneDifference * m_region->m_pitch_keytrack.value() / 100.0f + + m_region->m_tune.value() / 100.0f + + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f; + m_baseFreqRatio = std::exp2(pitch / 12.0f); + + // Sample rate of sample (if we are using a sample, not a basic wave like *sine or *saw) + const float sampleSampleRate = m_region->sample() != nullptr + ? m_region->sample()->sampleRate() + : m_lmmsSampleRate; // If we are using a basic wave instead of a sample, set it to LMMS's sample rate + // Play the sample faster/slower to match the correct sample rate + m_baseFreqRatio *= sampleSampleRate / m_lmmsSampleRate; + + + // Compute the base amplitude + + // Amplitude opcode + const float amplitude = m_region->m_amplitude.value() / 100.0f; // Amplitude is stored as a percent + + // Amplitude due to velocity + // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) + // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. + // Essentially this means lerping between y = x, y = 1, and y = 1 - x, if you think of y being the amp and x being vel/127 + const float ampVelocity = m_region->m_amp_veltrack.value() > 0 + ? (normalizedVelocity) * (m_region->m_amp_veltrack.value() / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / 100) + : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack.value() / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / -100); + + // Amplitude due to volume/gain + const float ampVolume = dbfsToAmp(m_region->m_volume.value()); + + // Panning + const float pan = m_region->m_pan.value() / 100; + const float rightPanAmp = std::min(1.0f, 1.0f + pan); + const float leftPanAmp = std::min(1.0f, 1.0f - pan); + + m_baseAmplitudeLeft = amplitude * ampVelocity * ampVolume * leftPanAmp; + m_baseAmplitudeRight = amplitude * ampVelocity * ampVolume * rightPanAmp; +} + + +// Helper function for envelope shapes float SfzRegionPlayState::envelopeGenerator(const f_cnt_t delay, const f_cnt_t attack, const f_cnt_t hold, const f_cnt_t decay, const float sustain, const f_cnt_t release) const { // If the note hasn't started yet, don't do anything @@ -121,6 +175,30 @@ float SfzRegionPlayState::envelopeGenerator(const f_cnt_t delay, const f_cnt_t a +// Helper function for LFOs +float SfzRegionPlayState::lfoGenerator(const f_cnt_t delay, const f_cnt_t fade, const float freq) const +{ + // If the note hasn't started yet, don't do anything + if (m_frameCount < 0) { return 0.0f; } + + if (static_cast(m_frameCount) < delay) { return 0.0f; } + + float lfoValue = std::sin(static_cast(m_frameCount - delay - fade) / m_lmmsSampleRate * freq * 2 * std::numbers::pi); + + // Make the lfo ramp up to its max amplitude during the fade frames + if (static_cast(m_frameCount) < delay + fade) + { + return static_cast(m_frameCount - delay) / fade * lfoValue; + } + else + { + return lfoValue; + } +} + + + + bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) { @@ -133,64 +211,35 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; - - // Sample rate of LMMS - const float lmmsSampleRate = Engine::audioEngine()->outputSampleRate(); - // Sample rate of sample (if we are using a sample, not a basic wave like *sine or *saw) - const float sampleSampleRate = m_region->sample() != nullptr - ? m_region->sample()->sampleRate() - : lmmsSampleRate; // If we are using a basic wave instead of a sample, set it to LMMS's sample rate - - - // Pitch envelope - // This is only computed once per buffer to same compute, since doing it per-frame seems a bit excessive - const float pitcheg = envelopeGenerator( - m_region->m_pitcheg.delay.value() * lmmsSampleRate, - m_region->m_pitcheg.attack.value() * lmmsSampleRate, - m_region->m_pitcheg.hold.value() * lmmsSampleRate, - m_region->m_pitcheg.decay.value() * lmmsSampleRate, - m_region->m_pitcheg.sustain.value() / 100.0f, // Sustain is stored in percent, so divide by 100 to get ratio, - m_region->m_pitcheg.release.value() * lmmsSampleRate - ) * m_region->m_pitcheg.depth.value(); - - // Calculate pitch difference relative to original sample - const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); - // The total pitch depends on 1. the key offset 2. the fine `tune` adjustment 3. the velocity, if pitch_veltrack is nonzero 4. the pitch envelope - const float pitch = semitoneDifference * m_region->m_pitch_keytrack.value() / 100.0f - + m_region->m_tune.value() / 100.0f - + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f - + pitcheg / 100.0f; - float freqRatio = std::exp2(pitch / 12.0f); - - // Play the sample faster/slower to match the correct sample rate - freqRatio *= sampleSampleRate / lmmsSampleRate; - - // Amplitude - const float amplitude = m_region->m_amplitude.value() / 100.0f; // Amplitude is stored as a percent - - // Amplitude envelope - const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.value() * lmmsSampleRate; - const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.value() * lmmsSampleRate; - const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.value() * lmmsSampleRate; - const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.value() * lmmsSampleRate; + // Amplitude Envelope Parameters + const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.value() * m_lmmsSampleRate; + const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.value() * m_lmmsSampleRate; + const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.value() * m_lmmsSampleRate; + const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.value() * m_lmmsSampleRate; const float ampegSustain = m_region->m_ampeg.sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.value() * lmmsSampleRate; - - // Amplitude due to velocity - // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) - // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. - // Essentially this means lerping between y = x, y = 1, and y = 1 - x, if you think of y being the amp and x being vel/127 - const float ampVelocity = m_region->m_amp_veltrack.value() > 0 - ? (normalizedVelocity) * (m_region->m_amp_veltrack.value() / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / 100) - : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack.value() / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / -100); + const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.value() * m_lmmsSampleRate; + + // Amplitude LFO parameters + const f_cnt_t amplfoDelayFrames = m_region->m_amplfo.delay.value() * m_lmmsSampleRate; + const f_cnt_t amplfoFadeFrames = m_region->m_amplfo.fade.value() * m_lmmsSampleRate; + const float amplfoFreq = m_region->m_amplfo.freq.value(); + const float amplfoDepth = m_region->m_amplfo.depth.value(); + + // Pitch Envelope Parameters + const f_cnt_t pitchegDelayFrames = m_region->m_pitcheg.delay.value() * m_lmmsSampleRate; + const f_cnt_t pitchegAttackFrames = m_region->m_pitcheg.attack.value() * m_lmmsSampleRate; + const f_cnt_t pitchegHoldFrames = m_region->m_pitcheg.hold.value() * m_lmmsSampleRate; + const f_cnt_t pitchegDecayFrames = m_region->m_pitcheg.decay.value() * m_lmmsSampleRate; + const float pitchegSustain = m_region->m_pitcheg.sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t pitchegReleaseFrames = m_region->m_pitcheg.release.value() * m_lmmsSampleRate; + const float pitchegDepth = m_region->m_pitcheg.depth.value(); + + // Pitch LFO parameters + const f_cnt_t pitchlfoDelayFrames = m_region->m_pitchlfo.delay.value() * m_lmmsSampleRate; + const f_cnt_t pitchlfoFadeFrames = m_region->m_pitchlfo.fade.value() * m_lmmsSampleRate; + const float pitchlfoFreq = m_region->m_pitchlfo.freq.value(); + const float pitchlfoDepth = m_region->m_pitchlfo.depth.value(); - // Amplitude due to volume/gain - const float ampVolume = dbfsToAmp(m_region->m_volume.value()); - - // Panning - const float pan = m_region->m_pan.value() / 100; - const float rightPanAmp = std::min(1.0f, 1.0f + pan); - const float leftPanAmp = std::min(1.0f, 1.0f - pan); // Filter const bool filterEnabled = m_region->m_cutoff.value() != std::nullopt; @@ -205,17 +254,21 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) m_filter.calcFilterCoeffs(filterCutoff, q); } + // Now render the audio for (f_cnt_t f = 0; f < frames; ++f) { - // The amplitude envelope is computed every frame, since doing it once per buffer could result in discontinuities/clicks - const float ampeg = envelopeGenerator( - ampegDelayFrames, - ampegAttackFrames, - ampegHoldFrames, - ampegDecayFrames, - ampegSustain, - ampegReleaseFrames - ); + const float ampeg = envelopeGenerator(ampegDelayFrames, ampegAttackFrames, ampegHoldFrames, ampegDecayFrames, ampegSustain, ampegReleaseFrames); + const float amplfo = amplfoDepth != 0.0f // Only compute the amplitude lfo if the depth is nonzero + ? dbfsToAmp(lfoGenerator(amplfoDelayFrames, amplfoFadeFrames, amplfoFreq) * amplfoDepth) // amplfo depth is in decibels, so convert to amplitude + : 1.0f; + const float pitcheg = envelopeGenerator(pitchegDelayFrames, pitchegAttackFrames, pitchegHoldFrames, pitchegDecayFrames, pitchegSustain, pitchegReleaseFrames) * pitchegDepth; + const float pitchlfo = pitchlfoDepth != 0.0f // Only compute the pitch lfo if the depth is nonzero + ? lfoGenerator(pitchlfoDelayFrames, pitchlfoFadeFrames, pitchlfoFreq) * pitchlfoDepth + : 0.0f; + const float pitchmodFreqRatio = (pitcheg + pitchlfo != 0) // Only calculate the pitch per-frame if the pitch envelope/lfo is actually active + ? std::exp2((pitcheg + pitchlfo) / 1200) + : 1.0f; + // If a sample file was loaded, use the buffer to get the audio data // Otherwise, if a basic wave shape is being used (like *sine, *saw, *silence, etc) use a function to generate the shape float sampleLeftValue = 0.0f; @@ -227,24 +280,25 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) } else { - sampleLeftValue = SfzBasicWaves::generate(m_region->basicWaveShape(), lmmsSampleRate, m_sampleFrame); - sampleRightValue = SfzBasicWaves::generate(m_region->basicWaveShape(), lmmsSampleRate, m_sampleFrame); + sampleLeftValue = SfzBasicWaves::generate(m_region->basicWaveShape(), m_lmmsSampleRate, m_sampleFrame); + sampleRightValue = SfzBasicWaves::generate(m_region->basicWaveShape(), m_lmmsSampleRate, m_sampleFrame); } if (filterEnabled) // TODO does this if statement make it faster? { - buffer[f][0] += m_filter.update(sampleLeftValue * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp, 0); - buffer[f][1] += m_filter.update(sampleRightValue * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp, 1); + buffer[f][0] += m_filter.update(sampleLeftValue * m_baseAmplitudeLeft * ampeg * amplfo, 0); + buffer[f][1] += m_filter.update(sampleRightValue * m_baseAmplitudeRight * ampeg * amplfo, 1); } else { - buffer[f][0] += sampleLeftValue * amplitude * ampeg * ampVelocity * ampVolume * rightPanAmp; - buffer[f][1] += sampleRightValue * amplitude * ampeg * ampVelocity * ampVolume * leftPanAmp; + buffer[f][0] += sampleLeftValue * m_baseAmplitudeLeft * ampeg * amplfo; + buffer[f][1] += sampleRightValue * m_baseAmplitudeRight * ampeg * amplfo; } // Increment the frame count. If we are using a sample, make sure to stop at the end, but if we are using a basic wave like *sine or *saw, there's no need + const float frameIncrement = m_baseFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback m_sampleFrame = m_region->sample() != nullptr - ? std::min(static_cast(m_region->sample()->size()), m_sampleFrame + freqRatio) - : m_sampleFrame + freqRatio; + ? std::min(static_cast(m_region->sample()->size()), m_sampleFrame + frameIncrement) + : m_sampleFrame + frameIncrement; m_frameCount++; } @@ -279,6 +333,12 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) m_releaseFrame = m_frameCount; // testing } } + // Since the base pitch and amplitude are precomputed in the constructor, they need to be re-computed if any of the CC modulations may have changed + // For simplicity, if any CC trigger occurs, recompute everything + if (trigger.type() == SfzTrigger::Type::ControlChange) + { + precomputeBaseValues(); + } } } // namespace lmms \ No newline at end of file diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 3881f1cc499..7169ff9920b 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -44,6 +44,11 @@ class SfzRegionPlayState SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger); SfzRegionPlayState() = default; // The default constructor is needed to initialize arrays of the object + //! Helper function to calculate the base pitch and amplitude so that it doesn't have to be done per-buffer. + // This is done once during the constructor, but it must also be done whenever a CC knob + // changes, since that could affect the base pitch/amp of the playback. + void precomputeBaseValues(); + //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. //! Returns true if any sound was generated, false if the buffer is left untouched bool play(SampleFrame* buffer, const fpp_t frames); @@ -57,6 +62,10 @@ class SfzRegionPlayState //! Helper function for calculating the envelope value at the current frame float envelopeGenerator(const f_cnt_t delay, const f_cnt_t attack, const f_cnt_t hold, const f_cnt_t decay, const float sustain, const f_cnt_t release) const; + //! Helper function for calculating the lfo value at the current frame + float lfoGenerator(const f_cnt_t delay, const f_cnt_t fade, const float freq) const; + + private: //! Stores whether this object still represents a sound which exists. //! This will be true when the sound is active, but will become false once the release has ended or it has been forcefully deactivated. @@ -64,6 +73,16 @@ class SfzRegionPlayState //! Stores whether the note has been released yet bool m_released = false; + //! Stores the current sample rate for convenience + float m_lmmsSampleRate = 0.0f; + + //! Cache the rate the sample should be played (based on pitch and sample rate, compared to lmms's sample rate) + // rather than computing it per buffer/frame. This does not include the effect of the pitch env/lfo, since those are computed every frame + float m_baseFreqRatio = 0.0f; + //! Cache the base amplitude. The amplitude envelope/lfo will be applied on top of this. + float m_baseAmplitudeLeft = 1.0f; + float m_baseAmplitudeRight = 1.0f; + //! The number of frames since the start of the sound. This may be negative if the region has delay or the note starts partway through a buffer. int m_frameCount = 0; //! The frame at which the note was released, relative to the start of the note From 9a602e3ebfc236f9cad924b9813c98da242ba985 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 3 May 2026 09:16:01 -0400 Subject: [PATCH 088/131] Fix LFO fade --- plugins/SfzSampler/SfzRegionPlayState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index 0df61370420..d53f3d945e7 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -183,7 +183,7 @@ float SfzRegionPlayState::lfoGenerator(const f_cnt_t delay, const f_cnt_t fade, if (static_cast(m_frameCount) < delay) { return 0.0f; } - float lfoValue = std::sin(static_cast(m_frameCount - delay - fade) / m_lmmsSampleRate * freq * 2 * std::numbers::pi); + float lfoValue = std::sin(static_cast(m_frameCount - delay) / m_lmmsSampleRate * freq * 2 * std::numbers::pi); // Make the lfo ramp up to its max amplitude during the fade frames if (static_cast(m_frameCount) < delay + fade) From 4706ffc1b6b08df3fcb44c48a140a82ffee7a89e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 3 May 2026 11:27:05 -0400 Subject: [PATCH 089/131] Add comment to fix scripted-checks --- plugins/SfzSampler/SfzOpcodes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index 6f4486b818a..b168851bf17 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -238,4 +238,4 @@ template<> void Opcode::parseFromString(const QString& opcodeName, c } // namespace lmms -#endif // LMMS_SFZ_OPCODES \ No newline at end of file +#endif // LMMS_SFZ_OPCODES_H \ No newline at end of file From 65401e875c4aa6815a9d5b28d5d6342f1a64fff8 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 3 May 2026 14:21:54 -0400 Subject: [PATCH 090/131] Make loading new sfz file not crash when notes are playing --- plugins/SfzSampler/SfzSampler.cpp | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 8022580b840..61863a7e091 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -212,8 +212,6 @@ void SfzSampler::loadFile(const QString& filePath) void SfzSampler::loadSfzFile(const QString& filePath) { - // Prevent the audio thread from accidentally looping through the regions while they are being edited - const auto guard = Engine::audioEngine()->requestChangesGuard(); // Reset the note counts, midi cc values, etc m_sfzGlobalState = SfzGlobalState(); // And any info about control labels, default values, etc @@ -228,23 +226,35 @@ void SfzSampler::loadSfzFile(const QString& filePath) if (!successfulParseFile) { qDebug() << "[SFZ Player] An error occurred when parsing the SFZ file."; return; } // Hand off the vector of regions to SfzRegionManager, which will sort them out to optimize trigger selection - m_regionManager = SfzRegionManager(regions); // TODO should move semantics be used here? + // Don't immediately set m_regionManager, since the audio threads may still be accessing it; instead use a temporary object here + SfzRegionManager newRegionManager = SfzRegionManager(regions); // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: QDir parentDirectory = QFileInfo(filePath).absoluteDir(); // Reset any loaded samples - m_samplePool = SfzSamplePool(); + // Create a new sample pool, but don't delete the old one just yet, since the audio thread may still be accessing the samples + SfzSamplePool newSamplePool = SfzSamplePool(); int i = 0; - for (auto* region : m_regionManager.allRegions()) + for (auto* region : newRegionManager.allRegions()) { - qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_regionManager.allRegions().size() << region->m_sampleFile.value().value_or("N/A"); - bool successfulLoadSample = region->initializeSample(parentDirectory, m_samplePool); + qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << newRegionManager.allRegions().size() << region->m_sampleFile.value().value_or("N/A"); + bool successfulLoadSample = region->initializeSample(parentDirectory, newSamplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } i++; } - qDebug() << "Loaded" << m_regionManager.allRegions().size() << "regions and" << m_samplePool.sampleCount() << "samples."; + qDebug() << "Loaded" << newRegionManager.allRegions().size() << "regions and" << newSamplePool.sampleCount() << "samples."; + + // Now delete the old sample pool by replacing it with the new one, and also swap out the region data + // Make sure whatever audio processing currently being done is finished before swapping out the data + Engine::audioEngine()->requestChangeInModel(); + // TODO are these moves correct? + m_samplePool = std::move(newSamplePool); + m_regionManager = std::move(newRegionManager); + // Also delete all the current voices, since they still have m_region pointers to the old region data + std::fill(m_voices.begin(), m_voices.end(), SfzRegionPlayState()); + Engine::audioEngine()->doneChangeInModel(); // Set the initial cc values based on any `set_ccN` opcodes in the header From e6915911c3d167bf5df01a58baaba00c1c20c9bd Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 3 May 2026 14:40:02 -0400 Subject: [PATCH 091/131] Re-enable drag-drop sfz files into instrument window --- plugins/SfzSampler/SfzSamplerView.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 9b9fecad728..34669d2fdea 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -156,21 +156,17 @@ void SfzSamplerView::openFile() void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) { - /* QString value = StringPairDrag::decodeValue(dee); if (value.endsWith(".sfz")) { dee->accept(); - m_instrument->loadFile(value); return; } dee->ignore(); - */ } void SfzSamplerView::dropEvent(QDropEvent* de) { - /* QString value = StringPairDrag::decodeValue(de); if (value.endsWith(".sfz")) { @@ -179,7 +175,6 @@ void SfzSamplerView::dropEvent(QDropEvent* de) return; } de->ignore(); - */ } void SfzSamplerView::resizeEvent(QResizeEvent* re) From cb000377abe5e3835b2cda09b39031c504408a65 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 6 May 2026 16:36:09 -0400 Subject: [PATCH 092/131] Sample Loading Thread Partial Fix --- plugins/SfzSampler/SfzRegion.h | 5 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 17 ++-- plugins/SfzSampler/SfzRegionPlayState.h | 6 ++ plugins/SfzSampler/SfzSamplePool.cpp | 6 +- plugins/SfzSampler/SfzSamplePool.h | 2 +- plugins/SfzSampler/SfzSampler.cpp | 114 ++++++++++++++-------- plugins/SfzSampler/SfzSampler.h | 35 ++++++- 7 files changed, 127 insertions(+), 58 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index c02afa3c548..d97bce26f70 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -58,13 +58,14 @@ class SfzRegion : public SfzOpcodeState //! Returns true if successful bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool); - const SfzSampleBuffer* sample() const { return m_sample; } + std::shared_ptr sample() const { return m_sample; } const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } private: //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. - const SfzSampleBuffer* m_sample = nullptr; + //! This uses an std::shared_ptr so that the sample object doesn't get unexpectedly deleted in the middle of the audio processing while a new sfz file is being loaded. + std::shared_ptr m_sample = nullptr; //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index d53f3d945e7..d33e6b57279 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -42,6 +42,7 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger , m_filter(m_lmmsSampleRate) , m_trigger(trigger) , m_region(region) + , m_sampleObject(region->sample()) { // Calculate the base pitch and amplitude precomputeBaseValues(); @@ -101,8 +102,8 @@ void SfzRegionPlayState::precomputeBaseValues() m_baseFreqRatio = std::exp2(pitch / 12.0f); // Sample rate of sample (if we are using a sample, not a basic wave like *sine or *saw) - const float sampleSampleRate = m_region->sample() != nullptr - ? m_region->sample()->sampleRate() + const float sampleSampleRate = m_sampleObject != nullptr + ? m_sampleObject->sampleRate() : m_lmmsSampleRate; // If we are using a basic wave instead of a sample, set it to LMMS's sample rate // Play the sample faster/slower to match the correct sample rate m_baseFreqRatio *= sampleSampleRate / m_lmmsSampleRate; @@ -273,10 +274,10 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) // Otherwise, if a basic wave shape is being used (like *sine, *saw, *silence, etc) use a function to generate the shape float sampleLeftValue = 0.0f; float sampleRightValue = 0.0f; - if (m_region->sample() != nullptr) // TODO: should this check be outside of the loop? + if (m_sampleObject != nullptr) // TODO: should this check be outside of the loop? { - sampleLeftValue = m_region->sample()->at(m_sampleFrame, 0); - sampleRightValue = m_region->sample()->at(m_sampleFrame, 1); + sampleLeftValue = m_sampleObject->at(m_sampleFrame, 0); + sampleRightValue = m_sampleObject->at(m_sampleFrame, 1); } else { @@ -296,8 +297,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) } // Increment the frame count. If we are using a sample, make sure to stop at the end, but if we are using a basic wave like *sine or *saw, there's no need const float frameIncrement = m_baseFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback - m_sampleFrame = m_region->sample() != nullptr - ? std::min(static_cast(m_region->sample()->size()), m_sampleFrame + frameIncrement) + m_sampleFrame = m_sampleObject != nullptr + ? std::min(static_cast(m_sampleObject->size()), m_sampleFrame + frameIncrement) : m_sampleFrame + frameIncrement; m_frameCount++; } @@ -310,7 +311,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) } // If loop_mode is one_shot, no noteOff signal will ever come to release it, so we need to manually deactivate when we reach the end of the sample - if (m_region->m_loop_mode.value() == LoopMode::OneShot && m_region->sample() != nullptr && m_sampleFrame >= m_region->sample()->size()) + if (m_region->m_loop_mode.value() == LoopMode::OneShot && m_sampleObject != nullptr && m_sampleFrame >= m_sampleObject->size()) { m_active = false; // TODO should this forcefully decative or just release? } diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 7169ff9920b..289637bf881 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -29,9 +29,12 @@ #include "SampleFrame.h" #include "SfzTrigger.h" #include "SfzOpcodeState.h" +#include "SfzSampleBuffer.h" #include "BasicFilters.h" +#include + namespace lmms { @@ -100,6 +103,9 @@ class SfzRegionPlayState //! The region this sound originated from const SfzRegion* m_region = nullptr; + + //! A shared pointer to the sample object of the parent region + std::shared_ptr m_sampleObject = nullptr; }; diff --git a/plugins/SfzSampler/SfzSamplePool.cpp b/plugins/SfzSampler/SfzSamplePool.cpp index 888686fb45f..18744d1cd49 100644 --- a/plugins/SfzSampler/SfzSamplePool.cpp +++ b/plugins/SfzSampler/SfzSamplePool.cpp @@ -29,17 +29,17 @@ namespace lmms { -SfzSampleBuffer* const SfzSamplePool::loadSample(const QString& path) +std::shared_ptr SfzSamplePool::loadSample(const QString& path) { // If the sample has already been loaded before, just return a pointer to it if (m_samplePool.contains(path)) { - return m_samplePool.at(path).get(); + return m_samplePool.at(path); } else if (auto buffer = SampleBuffer::fromFile(path)) { m_samplePool.insert({path, std::make_shared(buffer->data(), buffer->size(), buffer->sampleRate())}); - return m_samplePool.at(path).get(); + return m_samplePool.at(path); } else { diff --git a/plugins/SfzSampler/SfzSamplePool.h b/plugins/SfzSampler/SfzSamplePool.h index 874c48c78f1..41d1d771bb9 100644 --- a/plugins/SfzSampler/SfzSamplePool.h +++ b/plugins/SfzSampler/SfzSamplePool.h @@ -36,7 +36,7 @@ namespace lmms class SfzSamplePool { public: - SfzSampleBuffer* const loadSample(const QString& path); + std::shared_ptr loadSample(const QString& path); //! Returns the number of samples currently loaded in the pool const int sampleCount() const { return m_samplePool.size(); } diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 61863a7e091..2501391212a 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -180,6 +180,36 @@ void SfzSampler::play(SampleFrame* workingBuffer) // If the play state deactivated during playback, and this was the max active index, figure out what the new max active index is if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } } + + + // In the event that the user tries to load a new sfz file while the current one is playing, the region and sample data will be + // loaded into temporary member variables, and it's the job of the audio thread to swap them into place when the data is ready and it gets a chance + if (m_newSfzDataReady) + { + m_regionManager = std::move(m_tempRegionManager); + m_samplePool = std::move(m_tempSamplePool); + // And reset the voices, since they may have invalid pointers to old region objects + std::fill(m_voices.begin(), m_voices.end(), SfzRegionPlayState()); + m_newSfzDataReady = false; + m_justSwappedData = true; + // Also set the midi CC knobs to match the defaults, or whatever the current InstrumentTrack midi CC knobs are + // TODO can this be moved to the main thread? + for (int i = 0; i < NumMidiCCs; ++i) + { + if (m_resetCCKnobs) + { + // Reset the instrument track's midi CC knobs to the defaults of the SFZ + m_parentTrack->midiCCModel(i)->setInitValue(m_sfzGlobalState.midiCCValue(i)); + qDebug() << "Resetting lmms CC knob" << i << m_sfzGlobalState.midiCCValue(i); + } + qDebug() << "Resetting internal CC" << i << m_parentTrack->midiCCModel(i)->value(); + // Sync the internal knobs to the LMMS knobs. TODO why does `setInitValue` not trigger this? + processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this + } + qDebug() << "Audio thread: new data ready, swapped."; + } + // Increment the buffer counter so that the main thread knows when it's safe to touch the temp objects again + m_bufferCounter++; } @@ -198,20 +228,25 @@ void SfzSampler::recalculateMaxActiveIndex() void SfzSampler::loadFile(const QString& filePath) { - loadSfzFile(filePath); - // Reset the instrument track's midi CC knobs to the defaults of the SFZ - for (int i = 0; i < NumMidiCCs; ++i) - { - m_parentTrack->midiCCModel(i)->setInitValue(m_sfzGlobalState.midiCCValue(i)); - // For some reason it seems calling `setValue` on the CC models doesn't send a midi event to the instrument when doing drag/drop, - // so we also send a trigger to update the region's - processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this - } + loadSfzFile(filePath, true); // Passing true to reset the InstrumentTrack's midi CC knobs to the SFZ file's defaults } -void SfzSampler::loadSfzFile(const QString& filePath) +void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) { + // If the sample loading thread is already running, don't let the user load another file. + if (m_currentlyLoadingSamples) { qDebug() << "main thread: Currently loading samples, can't right now."; return; } + // Or, if the new sample/region data was just swapped into place a couple buffers ago, return just to be safe + if (m_justSwappedData && m_bufferCounter <= m_bufferCounterWhenDataReady + 2) { qDebug() << "main thread: not enough buffers passed, not safe"; return; } + else if (m_justSwappedData) + { + // If enough buffers have passed we assume the swap was successful, so reset the flag, and prep the internal CC values to match the knobs + m_justSwappedData = false; + } + // Set this variable so that when the audio thread is swapping data and reset the CC knobs, it knows whether to reset them to the SFZ file defaults or just to the current InstrumentTrack CC knob values + m_resetCCKnobs = resetCCKnobs; + + // Reset the note counts, midi cc values, etc m_sfzGlobalState = SfzGlobalState(); // And any info about control labels, default values, etc @@ -226,44 +261,43 @@ void SfzSampler::loadSfzFile(const QString& filePath) if (!successfulParseFile) { qDebug() << "[SFZ Player] An error occurred when parsing the SFZ file."; return; } // Hand off the vector of regions to SfzRegionManager, which will sort them out to optimize trigger selection - // Don't immediately set m_regionManager, since the audio threads may still be accessing it; instead use a temporary object here - SfzRegionManager newRegionManager = SfzRegionManager(regions); + // Don't immediately set m_regionManager, since the audio thread may still be accessing it; instead set the temporary object + m_tempRegionManager = SfzRegionManager(regions); // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: QDir parentDirectory = QFileInfo(filePath).absoluteDir(); - // Reset any loaded samples - // Create a new sample pool, but don't delete the old one just yet, since the audio thread may still be accessing the samples - SfzSamplePool newSamplePool = SfzSamplePool(); - int i = 0; - for (auto* region : newRegionManager.allRegions()) - { - qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << newRegionManager.allRegions().size() << region->m_sampleFile.value().value_or("N/A"); - bool successfulLoadSample = region->initializeSample(parentDirectory, newSamplePool); - if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } - i++; - } - qDebug() << "Loaded" << newRegionManager.allRegions().size() << "regions and" << newSamplePool.sampleCount() << "samples."; - - // Now delete the old sample pool by replacing it with the new one, and also swap out the region data - // Make sure whatever audio processing currently being done is finished before swapping out the data - Engine::audioEngine()->requestChangeInModel(); - // TODO are these moves correct? - m_samplePool = std::move(newSamplePool); - m_regionManager = std::move(newRegionManager); - // Also delete all the current voices, since they still have m_region pointers to the old region data - std::fill(m_voices.begin(), m_voices.end(), SfzRegionPlayState()); - Engine::audioEngine()->doneChangeInModel(); - // Set the initial cc values based on any `set_ccN` opcodes in the header m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); m_sfzFilePath = filePath; emit fileLoaded(); + + // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle everything + // This thread will send back a signal when each sample is loaded, along with a final signal when it's finished + m_currentlyLoadingSamples = true; + m_sampleLoadingThread = std::jthread([this, parentDirectory](){ + int i = 0; + for (auto* region : m_tempRegionManager.allRegions()) + { + qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_tempRegionManager.allRegions().size() << region->m_sampleFile.value().value_or("N/A"); + // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. + bool successfulLoadSample = region->initializeSample(parentDirectory, m_tempSamplePool); // TODO does this copy the sample pool object? + if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } + //emit sampleLoaded(i, m_tempRegionManager.allRegions().size(), region->m_sampleFile.value().value_or("N/A")); + i++; + } + qDebug() << "Loaded" << m_tempRegionManager.allRegions().size() << "regions and" << m_tempSamplePool.sampleCount() << "samples."; + // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data + m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it's safe to touch the temp objects again + m_newSfzDataReady = true; + m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? Maybe it doesn't matter since jthread handles destruction more nicely. + }); } + void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) { element.setAttribute("sfzfile", m_sfzFilePath); @@ -274,14 +308,8 @@ void SfzSampler::loadSettings(const QDomElement& element) m_sfzFilePath = element.attribute("sfzfile"); if (!m_sfzFilePath.isEmpty()) { - // Using `loadSfzFile` instead of `loadFile` to bypass resetting the midi CC knobs - loadSfzFile(m_sfzFilePath); - } - // Sync the internal CC values so that saved presets/projects work normally upon loading - for (int i = 0; i < NumMidiCCs; ++i) - { - processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); - } + loadSfzFile(m_sfzFilePath, true); // Passing false to leave the user-set midi CC knobs alone, since they were loaded by the InstrumentTrack and shouldn't be reset to the SFZ's defaults. + } // TODO add error handling, if path doesn't exist } QString SfzSampler::nodeName() const diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index a03357185e5..4b15e1ea1e9 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -39,6 +39,8 @@ #include "SfzSamplePool.h" #include "SfzRegionManager.h" +#include + namespace lmms { class SfzSampler : public Instrument @@ -59,7 +61,7 @@ class SfzSampler : public Instrument void loadFile(const QString& filePath) override; //! This extra function for loading files is needed so that we can choose only to reset the CC knobs when the user is loading a new file, not a preset/project - void loadSfzFile(const QString& filePath); + void loadSfzFile(const QString& filePath, const bool resetCCKnobs); QString nodeName() const override; gui::PluginView* instantiateView(QWidget* parent) override; @@ -102,7 +104,38 @@ class SfzSampler : public Instrument //! The path to the currently loaded SFZ file QString m_sfzFilePath = ""; + + //! Helper thread for loading sample files so that the main thread isn't blocked + std::jthread m_sampleLoadingThread; + + //! Unfortunately, dealing with multiple threads gets complicated. + //! There is the possibility that the audio thread could access the region/sample data while the main thread is loading a new SFZ file and the samples + //! We need some way to swap out the current region/samples with the new data without breaking real-time safety. + //! To do this, essentially we have temporary buffers which the main thread works on while loading the files. When it's done, it sets a flag + //! which tells the audio thread that it can swap out the temporary objects for the real objects, and continue processing the audio. + + //! When a new SFZ file is being loaded and the regions/samples need to be swapped out, the main thread sets this flag to let the + //! audio thread know to move the data from the temporary variables into the real region/sample stores. The audio thread will set this back to false when it has finished swapping + std::atomic m_newSfzDataReady = false; + std::atomic m_justSwappedData = false; // And another flag so the main thread knows if it should be waiting for enough buffers to pass before loaidng another sfz file + //! Also have a flag for whether the sample loading thread is active or not, so that we don't accidentally try to start up a new sample loading thread while samples are already being loaded. + std::atomic m_currentlyLoadingSamples = false; + //! Temporary variables for the region and sample data, which the audio thread will swap with the real ones when m_newSfzDataReady is true + SfzRegionManager m_tempRegionManager; + SfzSamplePool m_tempSamplePool; + //! Counts the number of buffers which have been processed. This is used for determining how long it has been since the audio thread has swapped out the region/sample data + //! when loading a new SFZ file (Thanks to Lost Robot for the idea) + std::atomic m_bufferCounter = 0; + //! The main thread will also save the buffer counter when m_newSfzDataReady was set so that it will know if enough buffers have passed that the audio thread can be guaranteed to have swapped the data already. + //! If the user tries to load another SFZ file within one or two buffers of a previous SFZ file being loaded, it will refuse, because the audio thread may still be swapping the data from the temporary objects + size_t m_bufferCounterWhenDataReady = 0; + //! Also another flag so that the main thread can communicate to the audio thread whether the midi CC knobs should be reset to the SFZ file's defaults. This isn't great, ideally it could be done only by the main thread, idk + std::atomic m_resetCCKnobs = false; + + friend class SampleLoadingThread; friend class gui::SfzSamplerView; }; + } // namespace lmms + #endif // LMMS_SFZSAMPLER_H From 35c2ca43bcb5118d202f3d37c201b740f5eb9404 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 6 May 2026 18:18:56 -0400 Subject: [PATCH 093/131] Hopefully realtime safe now --- plugins/SfzSampler/SfzRegion.h | 2 +- plugins/SfzSampler/SfzSampler.cpp | 109 +++++++++++++++++++----------- plugins/SfzSampler/SfzSampler.h | 19 ++++-- 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index d97bce26f70..37f93e4777e 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -64,7 +64,7 @@ class SfzRegion : public SfzOpcodeState private: //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. - //! This uses an std::shared_ptr so that the sample object doesn't get unexpectedly deleted in the middle of the audio processing while a new sfz file is being loaded. + //! This uses an std::shared_ptr so that the sample object doesn't get unexpectedly deleted in the middle of the audio processing while a new sfz file is being loaded. TODO is this an issue? std::shared_ptr m_sample = nullptr; //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 2501391212a..44a55a20ff6 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -39,6 +39,9 @@ #include "interpolation.h" #include "plugin_export.h" +#include "GuiApplication.h" +#include "MainWindow.h" + namespace lmms { @@ -68,6 +71,10 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) auto iph = new InstrumentPlayHandle(this, instrumentTrack); Engine::audioEngine()->addPlayHandle(iph); + // When a user loads a new SFZ file, the main thread prepares the new data to be swapped out by the audio thread, but the main thread + // needs to delete the old data afterwards, so here we connect some kind of run-loop so that the main thread gets a chance to check + connect(lmms::gui::getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(mainThreadUpdateAfterDataSwap())); // I really don't like this. Would it be better to use our own QTimer, not LMMS's gui's? + emit dataChanged(); } @@ -113,18 +120,20 @@ void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) void SfzSampler::processTrigger(const SfzTrigger& trigger) { + if (m_regionManager == nullptr) { return; } // Before any SFZ file is loaded, m_regionManager is nullptr + // Notify the global state to update which keys are active, update midi CC values, etc m_sfzGlobalState.processTrigger(trigger); // Loop through all the regions to check if a new note should be played // TODO can we get rid of this loop - for (auto* region : m_regionManager.allRegions()) + for (auto* region : m_regionManager->allRegions()) { // Notify the region of the event so that it can update cached CC modulations, keyswitch states, etc region->processTrigger(m_sfzGlobalState, trigger); } - for (auto* region : m_regionManager.findPotentialMatchingRegions(trigger)) + for (auto* region : m_regionManager->findPotentialMatchingRegions(trigger)) { // If the trigger conditions are met, spawn a new sound if (region->triggerConditionsMet(m_sfzGlobalState, trigger)) @@ -184,31 +193,8 @@ void SfzSampler::play(SampleFrame* workingBuffer) // In the event that the user tries to load a new sfz file while the current one is playing, the region and sample data will be // loaded into temporary member variables, and it's the job of the audio thread to swap them into place when the data is ready and it gets a chance - if (m_newSfzDataReady) - { - m_regionManager = std::move(m_tempRegionManager); - m_samplePool = std::move(m_tempSamplePool); - // And reset the voices, since they may have invalid pointers to old region objects - std::fill(m_voices.begin(), m_voices.end(), SfzRegionPlayState()); - m_newSfzDataReady = false; - m_justSwappedData = true; - // Also set the midi CC knobs to match the defaults, or whatever the current InstrumentTrack midi CC knobs are - // TODO can this be moved to the main thread? - for (int i = 0; i < NumMidiCCs; ++i) - { - if (m_resetCCKnobs) - { - // Reset the instrument track's midi CC knobs to the defaults of the SFZ - m_parentTrack->midiCCModel(i)->setInitValue(m_sfzGlobalState.midiCCValue(i)); - qDebug() << "Resetting lmms CC knob" << i << m_sfzGlobalState.midiCCValue(i); - } - qDebug() << "Resetting internal CC" << i << m_parentTrack->midiCCModel(i)->value(); - // Sync the internal knobs to the LMMS knobs. TODO why does `setInitValue` not trigger this? - processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this - } - qDebug() << "Audio thread: new data ready, swapped."; - } - // Increment the buffer counter so that the main thread knows when it's safe to touch the temp objects again + audioThreadHandleNewSfzData(); + // Increment the buffer counter so that the main thread knows when it's safe to delete the old objects m_bufferCounter++; } @@ -235,15 +221,18 @@ void SfzSampler::loadFile(const QString& filePath) void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) { // If the sample loading thread is already running, don't let the user load another file. - if (m_currentlyLoadingSamples) { qDebug() << "main thread: Currently loading samples, can't right now."; return; } + if (m_currentlyLoadingSamples) + { + qDebug() << "[SFZ Player] Requested to load new SFZ file while still loading samples from previous SFZ file. Ignoring."; + return; + } // Or, if the new sample/region data was just swapped into place a couple buffers ago, return just to be safe - if (m_justSwappedData && m_bufferCounter <= m_bufferCounterWhenDataReady + 2) { qDebug() << "main thread: not enough buffers passed, not safe"; return; } - else if (m_justSwappedData) + if (m_justSwappedData && m_bufferCounter <= m_bufferCounterWhenDataReady + 2) { - // If enough buffers have passed we assume the swap was successful, so reset the flag, and prep the internal CC values to match the knobs - m_justSwappedData = false; + qDebug() << "[SFZ Player] Requested to load new SFZ file while new region/sample data from previous SFZ file have not yet been swapped into place. Ignoring."; + return; } - // Set this variable so that when the audio thread is swapping data and reset the CC knobs, it knows whether to reset them to the SFZ file defaults or just to the current InstrumentTrack CC knob values + // Set this variable so that after the audio thread swaps the data, the main thread remembers whether to reset the CC knobs to the SFZ file defaults or just to the current InstrumentTrack CC knob values m_resetCCKnobs = resetCCKnobs; @@ -262,41 +251,79 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // Hand off the vector of regions to SfzRegionManager, which will sort them out to optimize trigger selection // Don't immediately set m_regionManager, since the audio thread may still be accessing it; instead set the temporary object - m_tempRegionManager = SfzRegionManager(regions); + m_tempRegionManager = new SfzRegionManager(regions); // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + // Initialize a new sample pool for the new samples to be loaded in + m_tempSamplePool = new SfzSamplePool(); + // Set the initial cc values based on any `set_ccN` opcodes in the header m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); m_sfzFilePath = filePath; emit fileLoaded(); - // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle everything - // This thread will send back a signal when each sample is loaded, along with a final signal when it's finished + // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything + // TODO: This thread will send back a signal when each sample is loaded, along with a final signal when it's finished m_currentlyLoadingSamples = true; m_sampleLoadingThread = std::jthread([this, parentDirectory](){ int i = 0; - for (auto* region : m_tempRegionManager.allRegions()) + for (auto* region : m_tempRegionManager->allRegions()) { - qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_tempRegionManager.allRegions().size() << region->m_sampleFile.value().value_or("N/A"); + qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_tempRegionManager->allRegions().size() << region->m_sampleFile.value().value_or("N/A"); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. - bool successfulLoadSample = region->initializeSample(parentDirectory, m_tempSamplePool); // TODO does this copy the sample pool object? + bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } //emit sampleLoaded(i, m_tempRegionManager.allRegions().size(), region->m_sampleFile.value().value_or("N/A")); i++; } - qDebug() << "Loaded" << m_tempRegionManager.allRegions().size() << "regions and" << m_tempSamplePool.sampleCount() << "samples."; + qDebug() << "[SFZ Player] Loaded" << m_tempRegionManager->allRegions().size() << "regions and" << m_tempSamplePool->sampleCount() << "samples."; // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data - m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it's safe to touch the temp objects again + m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data m_newSfzDataReady = true; m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? Maybe it doesn't matter since jthread handles destruction more nicely. }); } +void SfzSampler::audioThreadHandleNewSfzData() +{ + if (m_newSfzDataReady) + { + std::swap(m_regionManager, m_tempRegionManager); + std::swap(m_samplePool, m_tempSamplePool); + // And reset the active voices, since they may have invalid pointers to old region objects + std::fill_n(m_voices.begin(), m_maxActiveIndex + 1, SfzRegionPlayState()); + m_newSfzDataReady = false; + m_justSwappedData = true; + } +} + +void SfzSampler::mainThreadUpdateAfterDataSwap() +{ + if (m_justSwappedData && m_bufferCounter > m_bufferCounterWhenDataReady + 2) + { + m_justSwappedData = false; + // Now that the audio thread has swapped the data, delete the old sample pool and region manager pointers + if (m_tempRegionManager != nullptr) { delete m_tempRegionManager; } // these may be nullptr at first when no SFZ file has been loaded previously + if (m_tempSamplePool != nullptr) { delete m_tempSamplePool; } + // Also set the midi CC knobs to match the defaults, or whatever the current InstrumentTrack midi CC knobs are + for (int i = 0; i < NumMidiCCs; ++i) + { + if (m_resetCCKnobs) + { + // Reset the instrument track's midi CC knobs to the defaults of the SFZ + m_parentTrack->midiCCModel(i)->setInitValue(m_sfzGlobalState.midiCCValue(i)); + } + // Sync the internal knobs to the LMMS knobs. TODO why does `setInitValue` not trigger this? + processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this + } + } +} + void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) { diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 4b15e1ea1e9..388af631fe4 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -74,7 +74,7 @@ class SfzSampler : public Instrument //! Holds a list of all the SfzRegion objects, which have the configurations for each of the samples/regions: what keys/velocities/etc trigger it, the volume, filter, envelopes, etc //! This object helps map triggers to lists of potentially matching regions, so that we don't have to loop over all the regions, checking every single one whether all conditions are matched before spawning a voice. - SfzRegionManager m_regionManager; + SfzRegionManager* m_regionManager = nullptr; static constexpr int MAX_ACTIVE_SOUNDS = 128; //! Array to store all active (and inactive) sound play-states across all regions @@ -87,7 +87,7 @@ class SfzSampler : public Instrument void recalculateMaxActiveIndex(); //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded - SfzSamplePool m_samplePool; + SfzSamplePool* m_samplePool = nullptr; //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; @@ -121,18 +121,23 @@ class SfzSampler : public Instrument //! Also have a flag for whether the sample loading thread is active or not, so that we don't accidentally try to start up a new sample loading thread while samples are already being loaded. std::atomic m_currentlyLoadingSamples = false; //! Temporary variables for the region and sample data, which the audio thread will swap with the real ones when m_newSfzDataReady is true - SfzRegionManager m_tempRegionManager; - SfzSamplePool m_tempSamplePool; + SfzRegionManager* m_tempRegionManager = nullptr; + SfzSamplePool* m_tempSamplePool = nullptr; //! Counts the number of buffers which have been processed. This is used for determining how long it has been since the audio thread has swapped out the region/sample data //! when loading a new SFZ file (Thanks to Lost Robot for the idea) std::atomic m_bufferCounter = 0; //! The main thread will also save the buffer counter when m_newSfzDataReady was set so that it will know if enough buffers have passed that the audio thread can be guaranteed to have swapped the data already. //! If the user tries to load another SFZ file within one or two buffers of a previous SFZ file being loaded, it will refuse, because the audio thread may still be swapping the data from the temporary objects size_t m_bufferCounterWhenDataReady = 0; - //! Also another flag so that the main thread can communicate to the audio thread whether the midi CC knobs should be reset to the SFZ file's defaults. This isn't great, ideally it could be done only by the main thread, idk - std::atomic m_resetCCKnobs = false; + //! Also another flag just for the main thread so that it can remember whether the midi CC knobs should be reset to the SFZ file's defaults, since it has to wait until the audio thread has finished the swap. + bool m_resetCCKnobs = false; + + //! A helper function for the audio thread to handle swapping the data + void audioThreadHandleNewSfzData(); + //! A helper function for the main thread to delete the old data and update things like the CC knobs after the audio thread has swapped the data. +private slots: + void mainThreadUpdateAfterDataSwap(); - friend class SampleLoadingThread; friend class gui::SfzSamplerView; }; From 9565af5f55f1eec2049d2fd22dafc31f0e62c123 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 7 May 2026 12:57:38 -0400 Subject: [PATCH 094/131] Add more info to gui --- plugins/SfzSampler/SfzSampler.cpp | 26 ++++++++++++++++++------- plugins/SfzSampler/SfzSampler.h | 4 ++++ plugins/SfzSampler/SfzSamplerView.cpp | 28 ++++++++++++++++++++++++++- plugins/SfzSampler/SfzSamplerView.h | 4 ++++ 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 44a55a20ff6..f79b6393cb5 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -138,7 +138,6 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) // If the trigger conditions are met, spawn a new sound if (region->triggerConditionsMet(m_sfzGlobalState, trigger)) { - qDebug() << "Spawning sound!" << region->m_sampleFile.value().value_or("N/A"); // Loop through array to find open position bool foundOpenPosition = false; for (size_t i = 0; i <= m_voices.size(); ++i) @@ -153,6 +152,8 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) break; } } + // For fun, display the last played sample on the GUI + sendStatusInfo(QString("Last Played Sample: %1").arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } } } @@ -265,23 +266,21 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); m_sfzFilePath = filePath; - emit fileLoaded(); // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything - // TODO: This thread will send back a signal when each sample is loaded, along with a final signal when it's finished m_currentlyLoadingSamples = true; m_sampleLoadingThread = std::jthread([this, parentDirectory](){ int i = 0; for (auto* region : m_tempRegionManager->allRegions()) { - qDebug() << "[SFZ Player] Loading sample" << i + 1 << "/" << m_tempRegionManager->allRegions().size() << region->m_sampleFile.value().value_or("N/A"); + // Update the GUI info text to notify the user as samples are loaded + sendStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); - if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } - //emit sampleLoaded(i, m_tempRegionManager.allRegions().size(), region->m_sampleFile.value().value_or("N/A")); + if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } // TODO organize this debug info i++; } - qDebug() << "[SFZ Player] Loaded" << m_tempRegionManager->allRegions().size() << "regions and" << m_tempSamplePool->sampleCount() << "samples."; + sendStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data m_newSfzDataReady = true; @@ -321,6 +320,8 @@ void SfzSampler::mainThreadUpdateAfterDataSwap() // Sync the internal knobs to the LMMS knobs. TODO why does `setInitValue` not trigger this? processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this } + // Update the GUI + emit fileLoaded(); } } @@ -349,4 +350,15 @@ gui::PluginView* SfzSampler::instantiateView(QWidget* parent) return new gui::SfzSamplerView(this, parent); } + + +void SfzSampler::sendStatusInfo(const QString& text) +{ + // Print to console + qDebug() << "[SFZ Player]" << text; + // And send to GUI + emit statusInfo(text); +} + + } // namespace lmms diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 388af631fe4..65f7e77d6a9 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -68,6 +68,7 @@ class SfzSampler : public Instrument signals: void fileLoaded(); + void statusInfo(const QString& statusText); // Pass info to the GUI to display things like current samples being loaded, last sample played, etc private: void processTrigger(const SfzTrigger& trigger); @@ -104,6 +105,9 @@ class SfzSampler : public Instrument //! The path to the currently loaded SFZ file QString m_sfzFilePath = ""; + //! Helper function for printing debug info/passing status info to the GUI + void sendStatusInfo(const QString& text); + //! Helper thread for loading sample files so that the main thread isn't blocked std::jthread m_sampleLoadingThread; diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 34669d2fdea..026d1102903 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -57,7 +57,10 @@ namespace gui { SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) : InstrumentView(instrument, parent) , m_instrument(instrument) + , m_statusLabel(new QLabel(this)) + , m_generalInfoLabel(new QLabel(this)) , m_switchKeysLabel(new QLabel(this)) + , m_infoLabelsWidget(new QWidget(this)) , m_controlsWidget(new QWidget(this)) , m_knobLayout(new QGridLayout(m_controlsWidget)) { @@ -73,7 +76,14 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) connect(openFileButton, &PixmapButton::clicked, this, &SfzSamplerView::openFile); layout1->addWidget(openFileButton); - layout1->addWidget(m_switchKeysLabel); + layout1->addWidget(m_statusLabel); + + layout1->addWidget(m_infoLabelsWidget); + + auto layout2 = new QHBoxLayout(m_infoLabelsWidget); + layout2->setContentsMargins(0, 0, 0, 0); + layout2->addWidget(m_generalInfoLabel); + layout2->addWidget(m_switchKeysLabel); layout1->addWidget(m_controlsWidget); @@ -81,6 +91,8 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) // Whenever a new SFZ file is loaded, set the default CC values connect(m_instrument, &SfzSampler::fileLoaded, [this](){ onFileLoaded(); }); // this lambda is so bad, but it doesn't work as a slot for some reason + connect(m_instrument, &SfzSampler::statusInfo, [this](const QString& statusText){ updateStatusInfo(statusText); }); + onFileLoaded(); update(); @@ -138,6 +150,20 @@ void SfzSamplerView::onFileLoaded() { m_switchKeysLabel->setText(""); } + + // Update general info + m_generalInfoLabel->setText( + QString("File: %1\nRegions: %2\nSamples: %3") + .arg(QFileInfo(m_instrument->m_sfzFilePath).fileName()) + .arg(m_instrument->m_regionManager->allRegions().size()) + .arg(m_instrument->m_samplePool->sampleCount()) + ); +} + +void SfzSamplerView::updateStatusInfo(const QString& statusText) +{ + m_statusLabel->setText(statusText); + update(); // For some reason the gui doesn't always update if the user isn't interacting with it or panning the workspace view } diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index ec6bcec27c5..411f01b8112 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -49,6 +49,7 @@ class SfzSamplerView : public InstrumentView public slots: void onFileLoaded(); + void updateStatusInfo(const QString& statusText); void openFile(); protected: @@ -63,7 +64,10 @@ public slots: SfzSampler* m_instrument; + QLabel* m_statusLabel; + QLabel* m_generalInfoLabel; QLabel* m_switchKeysLabel; + QWidget* m_infoLabelsWidget; QWidget* m_controlsWidget; QGridLayout* m_knobLayout; }; From 091ca05ff1850dc5dc34aa44c90730a7557cc639 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 7 May 2026 13:40:24 -0400 Subject: [PATCH 095/131] Rework GUI status text --- plugins/SfzSampler/SfzSampler.cpp | 11 ++++++----- plugins/SfzSampler/SfzSampler.h | 3 ++- plugins/SfzSampler/SfzSamplerView.cpp | 27 ++++++++++++++++----------- plugins/SfzSampler/SfzSamplerView.h | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index f79b6393cb5..9fe9fac2711 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -153,7 +153,7 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) } } // For fun, display the last played sample on the GUI - sendStatusInfo(QString("Last Played Sample: %1").arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); + setStatusInfo("Last Played Sample: " + QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName()); if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } } } @@ -274,13 +274,13 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) for (auto* region : m_tempRegionManager->allRegions()) { // Update the GUI info text to notify the user as samples are loaded - sendStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); + setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } // TODO organize this debug info i++; } - sendStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); + setStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data m_newSfzDataReady = true; @@ -352,12 +352,13 @@ gui::PluginView* SfzSampler::instantiateView(QWidget* parent) -void SfzSampler::sendStatusInfo(const QString& text) +void SfzSampler::setStatusInfo(const QString& text) { // Print to console qDebug() << "[SFZ Player]" << text; // And send to GUI - emit statusInfo(text); + // (Instead of actually sending a Qt signal, which seemed to cause off crashes, simply set a member variable and let the GUI read it when it wants) + m_statusText = text; } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 65f7e77d6a9..ee6bbc82fb0 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -106,7 +106,8 @@ class SfzSampler : public Instrument QString m_sfzFilePath = ""; //! Helper function for printing debug info/passing status info to the GUI - void sendStatusInfo(const QString& text); + void setStatusInfo(const QString& text); + QString m_statusText = ""; //! Helper thread for loading sample files so that the main thread isn't blocked diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzSampler/SfzSamplerView.cpp index 026d1102903..87b45d28178 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzSampler/SfzSamplerView.cpp @@ -49,6 +49,8 @@ #include "embed.h" #include "MidiEvent.h" #include "InstrumentTrack.h" +#include "GuiApplication.h" +#include "MainWindow.h" namespace lmms { @@ -88,10 +90,11 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) layout1->addWidget(m_controlsWidget); - // Whenever a new SFZ file is loaded, set the default CC values + // Whenever a new SFZ file is loaded, set the default CC values and update the info text connect(m_instrument, &SfzSampler::fileLoaded, [this](){ onFileLoaded(); }); // this lambda is so bad, but it doesn't work as a slot for some reason - connect(m_instrument, &SfzSampler::statusInfo, [this](const QString& statusText){ updateStatusInfo(statusText); }); + // Instead of sending a signal from the plugin every time a note is pressed (that seemed to cause odd crashes), simply update things periodically + connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); onFileLoaded(); @@ -152,18 +155,20 @@ void SfzSamplerView::onFileLoaded() } // Update general info - m_generalInfoLabel->setText( - QString("File: %1\nRegions: %2\nSamples: %3") - .arg(QFileInfo(m_instrument->m_sfzFilePath).fileName()) - .arg(m_instrument->m_regionManager->allRegions().size()) - .arg(m_instrument->m_samplePool->sampleCount()) - ); + if (m_instrument->m_regionManager != nullptr && m_instrument->m_samplePool != nullptr) + { + m_generalInfoLabel->setText( + QString("File: %1\nRegions: %2\nSamples: %3") + .arg(QFileInfo(m_instrument->m_sfzFilePath).fileName()) + .arg(m_instrument->m_regionManager->allRegions().size()) + .arg(m_instrument->m_samplePool->sampleCount()) + ); + } } -void SfzSamplerView::updateStatusInfo(const QString& statusText) +void SfzSamplerView::periodicUpdate() { - m_statusLabel->setText(statusText); - update(); // For some reason the gui doesn't always update if the user isn't interacting with it or panning the workspace view + m_statusLabel->setText(m_instrument->m_statusText); } diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzSampler/SfzSamplerView.h index 411f01b8112..cef0dac6e76 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzSampler/SfzSamplerView.h @@ -49,7 +49,7 @@ class SfzSamplerView : public InstrumentView public slots: void onFileLoaded(); - void updateStatusInfo(const QString& statusText); + void periodicUpdate(); void openFile(); protected: From b669cb4c64c9a1045e79ecef0a092db2d011c033 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 7 May 2026 15:28:34 -0400 Subject: [PATCH 096/131] use std::thread instead of std::jthread to fix builds --- plugins/SfzSampler/SfzSampler.cpp | 10 ++++++++-- plugins/SfzSampler/SfzSampler.h | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 9fe9fac2711..491089bce7f 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -78,6 +78,11 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) emit dataChanged(); } +SfzSampler::~SfzSampler() +{ + m_sampleLoadingThread.join(); // Make sure to finish the sample loading thread before the plugin exits, or else the program might terminate +} + bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) @@ -269,7 +274,8 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; - m_sampleLoadingThread = std::jthread([this, parentDirectory](){ + if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active + m_sampleLoadingThread = std::thread([this, parentDirectory](){ int i = 0; for (auto* region : m_tempRegionManager->allRegions()) { @@ -284,7 +290,7 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data m_newSfzDataReady = true; - m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? Maybe it doesn't matter since jthread handles destruction more nicely. + m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? }); } diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index ee6bbc82fb0..0011301d0b5 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -49,6 +49,7 @@ class SfzSampler : public Instrument public: SfzSampler(InstrumentTrack* instrumentTrack); + ~SfzSampler(); void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; void deleteNotePluginData(NotePlayHandle* handle) override; @@ -111,7 +112,7 @@ class SfzSampler : public Instrument //! Helper thread for loading sample files so that the main thread isn't blocked - std::jthread m_sampleLoadingThread; + std::thread m_sampleLoadingThread; //! Unfortunately, dealing with multiple threads gets complicated. //! There is the possibility that the audio thread could access the region/sample data while the main thread is loading a new SFZ file and the samples From 9c68382ba1f3315cd6bd6e4d20d31cbb80fa9286 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 7 May 2026 15:46:23 -0400 Subject: [PATCH 097/131] Move sample loader thread function into real function instead of lambda --- plugins/SfzSampler/SfzSampler.cpp | 37 +++++++++++++++++-------------- plugins/SfzSampler/SfzSampler.h | 2 ++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 491089bce7f..2d1236ebc06 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -275,23 +275,26 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active - m_sampleLoadingThread = std::thread([this, parentDirectory](){ - int i = 0; - for (auto* region : m_tempRegionManager->allRegions()) - { - // Update the GUI info text to notify the user as samples are loaded - setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); - // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. - bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); - if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } // TODO organize this debug info - i++; - } - setStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); - // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data - m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data - m_newSfzDataReady = true; - m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? - }); + m_sampleLoadingThread = std::thread(&SfzSampler::sampleLoadingThreadFunction, this, parentDirectory); +} + +void SfzSampler::sampleLoadingThreadFunction(const QDir& parentDirectory) +{ + int i = 0; + for (auto* region : m_tempRegionManager->allRegions()) + { + // Update the GUI info text to notify the user as samples are loaded + setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); + // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. + bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); + if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } // TODO organize this debug info + i++; + } + setStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); + // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data + m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data + m_newSfzDataReady = true; + m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? } void SfzSampler::audioThreadHandleNewSfzData() diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 0011301d0b5..334f9e7c7e1 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -113,6 +113,8 @@ class SfzSampler : public Instrument //! Helper thread for loading sample files so that the main thread isn't blocked std::thread m_sampleLoadingThread; + //! The function which the sample loading thread uses to load all the samples + void sampleLoadingThreadFunction(const QDir& parentDirectory); //! Unfortunately, dealing with multiple threads gets complicated. //! There is the possibility that the audio thread could access the region/sample data while the main thread is loading a new SFZ file and the samples From 3053eb3f5ba760800e012aea989ed5e5178acba7 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 7 May 2026 16:23:28 -0400 Subject: [PATCH 098/131] Fix issues from merge --- plugins/SfzSampler/SfzRegionPlayState.cpp | 2 +- plugins/SfzSampler/SfzRegionPlayState.h | 2 +- plugins/SfzSampler/SfzSampler.cpp | 2 +- src/gui/FileBrowser.cpp | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index d33e6b57279..cc9aa8e802e 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -201,7 +201,7 @@ float SfzRegionPlayState::lfoGenerator(const f_cnt_t delay, const f_cnt_t fade, -bool SfzRegionPlayState::play(SampleFrame* buffer, const fpp_t frames) +bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) { // If the sound is not active (note was released, off_by triggered, etc) then don't render any audio if (!m_active) { return false; } diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 289637bf881..79951b4b32a 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -54,7 +54,7 @@ class SfzRegionPlayState //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. //! Returns true if any sound was generated, false if the buffer is left untouched - bool play(SampleFrame* buffer, const fpp_t frames); + bool play(SampleFrame* buffer, const f_cnt_t frames); //! Handle incoming event to decide whether to deactivate/release void processTrigger(const SfzTrigger& trigger); diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 2d1236ebc06..9b8f9e67fcd 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -183,7 +183,7 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) void SfzSampler::play(SampleFrame* workingBuffer) { - const fpp_t frames = Engine::audioEngine()->framesPerPeriod(); + const f_cnt_t frames = Engine::audioEngine()->framesPerPeriod(); // Render audio from each of the active voices for (size_t i = 0; i <= m_maxActiveIndex; ++i) diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index b6beb686031..cb70010c38b 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -717,6 +717,7 @@ QList FileBrowserTreeWidget::getContextActions(FileItem* file, bool so case FileItem::FileType::SoundFont: case FileItem::FileType::Patch: case FileItem::FileType::VstPlugin: + case FileItem::FileType::SFZ: fileCanBeInstrument = true; case FileItem::FileType::Project: From 734d2652aef827dd86cfaf6e1af6ae8fd98c74fe Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 7 May 2026 18:03:44 -0400 Subject: [PATCH 099/131] Go through and fix comments, minor issues, etc --- plugins/SfzSampler/SfzBasicWaves.cpp | 2 +- plugins/SfzSampler/SfzBasicWaves.h | 1 + plugins/SfzSampler/SfzControlsConfig.h | 2 +- plugins/SfzSampler/SfzGlobalState.cpp | 2 +- plugins/SfzSampler/SfzGlobalState.h | 1 - plugins/SfzSampler/SfzOpcodeState.cpp | 313 +--------------------- plugins/SfzSampler/SfzOpcodeState.h | 11 +- plugins/SfzSampler/SfzOpcodes.cpp | 15 +- plugins/SfzSampler/SfzOpcodes.h | 17 +- plugins/SfzSampler/SfzParser.cpp | 6 +- plugins/SfzSampler/SfzParser.h | 2 +- plugins/SfzSampler/SfzRegion.cpp | 2 +- plugins/SfzSampler/SfzRegion.h | 6 +- plugins/SfzSampler/SfzRegionManager.cpp | 11 +- plugins/SfzSampler/SfzRegionPlayState.cpp | 12 +- plugins/SfzSampler/SfzRegionPlayState.h | 8 +- plugins/SfzSampler/SfzSampler.cpp | 36 +-- plugins/SfzSampler/SfzSampler.h | 3 +- plugins/SfzSampler/SfzTrigger.h | 2 +- 19 files changed, 69 insertions(+), 383 deletions(-) diff --git a/plugins/SfzSampler/SfzBasicWaves.cpp b/plugins/SfzSampler/SfzBasicWaves.cpp index 24c18d9a548..55197fc6f40 100644 --- a/plugins/SfzSampler/SfzBasicWaves.cpp +++ b/plugins/SfzSampler/SfzBasicWaves.cpp @@ -31,9 +31,9 @@ namespace lmms const float SfzBasicWaves::generate(Shape shape, float sampleRate, int frame) { - // Amount through period // By default, the basic waves are played at C4 (midi key 60). This isn't in the spec afaik, but it makes things simple since pitch_keycenter defaults to 60. constexpr float baseFreq = 261.625565301f; + // Amount through period const float t = absFraction(frame * baseFreq / sampleRate); // The region playback state handles incrementing the frame count at different rates based on the pitch, so we don't need to handle the actual frequency calculation here. diff --git a/plugins/SfzSampler/SfzBasicWaves.h b/plugins/SfzSampler/SfzBasicWaves.h index e93b6350c3d..68c2e98f16a 100644 --- a/plugins/SfzSampler/SfzBasicWaves.h +++ b/plugins/SfzSampler/SfzBasicWaves.h @@ -40,6 +40,7 @@ class SfzBasicWaves Noise, Silence }; + //! Returns the calculated value of the waveform shape at the given number of frames from the start, given the sample rate. static const float generate(Shape shape, float sampleRate, int frame); private: diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzSampler/SfzControlsConfig.h index 6053ccf961c..515c404881f 100644 --- a/plugins/SfzSampler/SfzControlsConfig.h +++ b/plugins/SfzSampler/SfzControlsConfig.h @@ -52,4 +52,4 @@ class SfzControlsConfig : public SfzOpcodeState } // namespace lmms -#endif // LMMS_SFZ_CONTROLS_CONFIG_H \ No newline at end of file +#endif // LMMS_SFZ_CONTROLS_CONFIG_H diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzSampler/SfzGlobalState.cpp index 5a34633a114..25143d21341 100644 --- a/plugins/SfzSampler/SfzGlobalState.cpp +++ b/plugins/SfzSampler/SfzGlobalState.cpp @@ -93,4 +93,4 @@ void SfzGlobalState::initializeMidiCCValues(const SfzOpcodeState& controlsConfig } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzSampler/SfzGlobalState.h index 578cb40060b..f5231b65d90 100644 --- a/plugins/SfzSampler/SfzGlobalState.h +++ b/plugins/SfzSampler/SfzGlobalState.h @@ -83,7 +83,6 @@ class SfzGlobalState //! Stores the current value of all the midi CC knobs/controllers //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 127 to get a float between 0 and 1 works fine. - //! std::optional is used so that sfz file can specify default cc values std::array m_ccValues = {}; }; diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzSampler/SfzOpcodeState.cpp index 6866dccb10d..ef1bf22c24a 100644 --- a/plugins/SfzSampler/SfzOpcodeState.cpp +++ b/plugins/SfzSampler/SfzOpcodeState.cpp @@ -39,6 +39,7 @@ SfzOpcodeState::SfzOpcodeState() bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& value) { + // These lists hold pointers to all of the opcodes to make them easier to iterate over std::vector opcodeList = { &m_sampleFile, &m_default_path, @@ -115,10 +116,10 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } - // The per-CC opcodes are handled separately here for now, just to make the code simpler + // A few of the per-CC opcodes are handled separately here for now, just to make the code simpler if (name.startsWith("locc")) { - int ccNumber = ccNumberFromOpcode(name); + int ccNumber = ccNumberFromOpcode(name); // TODO have some kind of error handling for invalid cc numbers m_locc.at(ccNumber) = value.toInt(&successful); parsed = true; } @@ -142,317 +143,13 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu } else if (name.startsWith("label_cc")) { - int ccNumber = ccNumberFromOpcode(name); // TODO have some kind of error handling for invalid cc numbers + int ccNumber = ccNumberFromOpcode(name); m_label_cc.at(ccNumber) = value; parsed = true; successful = true; } - // - // Special cases - // - /* - if (name == "key") - { - // Setting "key" on its own is equivalent to setting lokey, hikey, and pitch_keycenter all to the same value - m_lokey.setValue(value.toInt(&successful)); - m_hikey.setValue(value.toInt(&successful)); - m_pitch_keycenter.setValue(value.toInt(&successful)); - if (!successful) { - m_lokey.setValue(stringToKeyNum(value, &successful)); - m_hikey.setValue(stringToKeyNum(value, &successful)); - m_pitch_keycenter.setValue(stringToKeyNum(value, &successful)); - } - parsed = true; - } - */ - - /* - m_ampeg.parseEnvelopeGeneratorOpcode(name, value, &successful); - // - // File paths - // - if (name == "sample") - { - m_sampleFile.setValue(value); - } - else if (name == "default_path") - { - m_default_path.setValue(value); - } - - - // - // Trigger Type - // - else if (name == "trigger") - { - if (value == "attack") { m_trigger.setValue(TriggerType::Attack); } - else if (value == "release") { m_trigger.setValue(TriggerType::Release); } - //else if (value == "first") { m_trigger = TriggerType::First; } // TODO To be implemented - //else if (value == "legato") { m_trigger = TriggerType::Legato; } // TODO To be implemented - //else if (value == "release_key") { m_trigger = TriggerType::ReleaseKey; } // TODO To be implemented - else - { - qDebug() << "[SFZ Parser] Unknown trigger:" << value; - return false; - } - } - - - // - // Key Conditions - // - else if (name == "lokey") - { - m_lokey = value.toInt(&successful); - if (!successful) { m_lokey = stringToKeyNum(value, &successful); } - } - else if (name == "hikey") - { - m_hikey = value.toInt(&successful); - if (!successful) { m_hikey = stringToKeyNum(value, &successful); } - } - else if (name == "key") - { - // Setting "key" on its own is equivalent to setting lokey, hikey, and pitch_keycenter all to the same value - m_lokey = m_hikey = m_pitch_keycenter = value.toInt(&successful); - if (!successful) { m_lokey = m_hikey = m_pitch_keycenter = stringToKeyNum(value, &successful); } - } - - - // - // Key Switches - // - else if (name == "sw_lokey") - { - m_sw_lokey = value.toInt(&successful); - if (!successful) { m_sw_lokey = stringToKeyNum(value, &successful); } - } - else if (name == "sw_hikey") - { - m_sw_hikey = value.toInt(&successful); - if (!successful) { m_sw_hikey = stringToKeyNum(value, &successful); } - } - else if (name == "sw_last") - { - m_sw_last = value.toInt(&successful); - if (!successful) { m_sw_last = stringToKeyNum(value, &successful); } - } - else if (name == "sw_default") - { - m_sw_default = value.toInt(&successful); - if (!successful) { m_sw_default = stringToKeyNum(value, &successful); } - } - else if (name == "sw_label") - { - m_sw_label = value; - } - - - // - // Velocity Conditions - // - else if (name == "lovel") - { - m_lovel = value.toInt(&successful); - } - else if (name == "hivel") - { - m_hivel = value.toInt(&successful); - } - - - // - // Round Robin Conditions - // - else if (name == "seq_length") - { - m_seq_length = value.toInt(&successful); - } - else if (name == "seq_position") - { - m_seq_position = value.toInt(&successful); - } - - - // - // Random Conditions - // - else if (name == "lorand") - { - m_lorand = value.toFloat(&successful); - } - else if (name == "hirand") - { - m_hirand = value.toFloat(&successful); - } - - - // - // Sample playback options - // - else if (name == "offset") - { - m_offset = value.toInt(&successful); - } - else if (name == "loop_mode") - { - if (value == "no_loop") { m_loop_mode = LoopMode::NoLoop; } - else if (value == "one_shot") { m_loop_mode = LoopMode::OneShot; } - //else if (value == "loop_continuous") { m_loop_mode = LoopMode::LoopContinuous; } // Not implemented yet, since we don't currently have a way to get loop point data from sample files - //else if (value == "loop_sustain") { m_loop_mode = LoopMode::LoopSustain; } - else - { - qDebug() << "[SFZ Parser] Unknown loop_mode:" << value; - return false; - } - } - - - // - // Delay - // - else if (name == "delay") - { - m_delay = value.toFloat(&successful); - } - else if (name == "delay_random") - { - m_delay_random = value.toFloat(&successful); - } - - - // - // Pitch - // - else if (name == "tune") - { - m_tune = value.toInt(&successful); - } - else if (name == "pitch_keycenter") - { - m_pitch_keycenter = value.toInt(&successful); - if (!successful) { m_pitch_keycenter = stringToKeyNum(value, &successful); } - } - else if (name == "pitch_keytrack") - { - m_pitch_keytrack = value.toInt(&successful); - } - else if (name == "pitch_veltrack") - { - m_pitch_veltrack = value.toInt(&successful); - } - - - // - // Filter - // - else if (name == "fil_type") - { - if (value == "lpf_1p") { m_fil_type = FilterType::Lowpass1Pole; } - else if (value == "lpf_2p") { m_fil_type = FilterType::Lowpass2Pole; } - else if (value == "hpf_1p") { m_fil_type = FilterType::Highpass1Pole; } - else if (value == "hpf_2p") { m_fil_type = FilterType::Highpass2Pole; } - else if (value == "bpf_2p") { m_fil_type = FilterType::Bandpass2Pole; } - else if (value == "brf_2p") { m_fil_type = FilterType::Bandstop2Pole; } - else - { - qDebug() << "[SFZ Parser] Unknown filter type:" << value; - return false; - } - } - else if (name == "cutoff") - { - m_cutoff = value.toFloat(&successful); - } - else if (name == "resonance") - { - m_resonance = value.toFloat(&successful); - } - else if (name == "fil_veltrack") - { - m_fil_veltrack = value.toInt(&successful); - } - - // - // Amplitude - // - else if (name == "amplitude") - { - m_amplitude = value.toFloat(&successful); - } - else if (name.startsWith("amplitude_oncc") || name.startsWith("amplitude_cc")) - { - m_amplitude_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - - - else if (name == "amp_veltrack") - { - m_amp_veltrack = value.toFloat(&successful); - } - - - // - // Misc Volume - // - else if (name == "volume" || name == "group_volume") - { - m_volume = value.toFloat(&successful); - } - else if (name.startsWith("gain_oncc") || name.startsWith("gain_cc") || name.startsWith("volume_oncc")) - { - m_gain_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - else if (name == "pan") - { - m_pan = value.toFloat(&successful); - } - else if (name.startsWith("pan_oncc") || name.startsWith("pan_cc")) - { - m_pan_oncc.at(ccNumberFromOpcode(name)) = value.toFloat(&successful); - } - - - // - // Midi CC - // - else if (name.startsWith("locc")) - { - int ccNumber = ccNumberFromOpcode(name); - m_locc.at(ccNumber) = value.toInt(&successful); - } - else if (name.startsWith("hicc")) - { - int ccNumber = ccNumberFromOpcode(name); - m_hicc.at(ccNumber) = value.toInt(&successful); - } - else if (name.startsWith("set_cc")) - { - int ccNumber = ccNumberFromOpcode(name); - m_set_cc.at(ccNumber) = value.toInt(&successful); - } - else if (name.startsWith("set_hdcc")) - { - int ccNumber = ccNumberFromOpcode(name); - m_set_cc.at(ccNumber) = value.toFloat(&successful) * 127; - } - else if (name.startsWith("label_cc")) - { - int ccNumber = ccNumberFromOpcode(name); - m_label_cc.at(ccNumber) = value; - } - - else - { - qDebug() << "[SFZ Parser] Unknown opcode:" << name; - return false; - } - */ - - if (!successful) { qDebug() << "[SFZ Parser] Unable to convert value from string:" << name << "=" << value; @@ -469,4 +166,4 @@ bool SfzOpcodeState::setOpcodeByStrings(const QString& name, const QString& valu -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzSampler/SfzOpcodeState.h index aaf56b38d69..d85f853d7e2 100644 --- a/plugins/SfzSampler/SfzOpcodeState.h +++ b/plugins/SfzSampler/SfzOpcodeState.h @@ -48,6 +48,7 @@ class SfzOpcodeState /***********************************************************************/ // SFZ OPCODE DEFINITIONS /***********************************************************************/ + // These are initailized with {opcode_name, default_value}, or if the opcode has multiple aliases, {{name1, name2, ...}, default_value} // // File Paths @@ -55,7 +56,6 @@ class SfzOpcodeState StringOpcode m_sampleFile {"sample", std::nullopt}; StringOpcode m_default_path {"default_path", std::nullopt}; - // // Trigger Type // @@ -95,12 +95,10 @@ class SfzOpcodeState FloatOpcode m_lorand {"lorand", 0.0f}; FloatOpcode m_hirand {"hirand", 1.0f}; - // // Sample playback // Opcode m_offset {"offset", 0}; // sample play offset in frames - Opcode m_loop_mode {"loop_mode", LoopMode::NoLoop}; // @@ -109,7 +107,6 @@ class SfzOpcodeState ModulatableOpcode m_delay {"delay", 0.0f}; // In seconds FloatOpcode m_delay_random {"delay_random", 0.0f}; - // // Pitch // @@ -126,7 +123,6 @@ class SfzOpcodeState FloatOpcode m_resonance {"resonance", 0.0f}; Opcode m_fil_veltrack {"fil_veltrack", 0}; // in cents - // // Amplitude // @@ -134,7 +130,6 @@ class SfzOpcodeState // Overall amplitute velocity modulation FloatOpcode m_amp_veltrack {"amp_veltrack", 100.0f}; - // // Amplitude Envelope Generator (ampeg) // @@ -145,7 +140,6 @@ class SfzOpcodeState // LfoOpcodes m_amplfo {"amplfo"}; - // // Pitch Envelope Generator (pitcheg) // @@ -164,7 +158,6 @@ class SfzOpcodeState ModulatableOpcode m_volume {{"volume", "gain", "group_volume"}, 0.0f}; // In decibals ModulatableOpcode m_pan {"pan", 0.0f}; - // // Midi CC // @@ -182,4 +175,4 @@ class SfzOpcodeState } // namespace lmms -#endif // LMMS_SFZ_OPCODE_STATE_H \ No newline at end of file +#endif // LMMS_SFZ_OPCODE_STATE_H diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzSampler/SfzOpcodes.cpp index e5b4529d27b..b9cb5e16bad 100644 --- a/plugins/SfzSampler/SfzOpcodes.cpp +++ b/plugins/SfzSampler/SfzOpcodes.cpp @@ -33,6 +33,7 @@ namespace lmms template<> void FloatOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { + // Check if the name matches one of this opcode's aliases if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } m_value = opcodeValue.toFloat(successful); *parsed = true; @@ -52,7 +53,7 @@ void KeyOpcode::parseFromString(const QString& opcodeName, const QString& opcode { if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } m_value = opcodeValue.toInt(successful); - // Integer opcodes can also define keys, which can be specified either by a key number of by a string like e4 or c#5 + // Integer opcodes can also define keys, which can be specified either by a key number or by a string like e4 or c#5 if (!*successful) { m_value = stringToKeyNum(opcodeValue, successful); } *parsed = true; } @@ -63,7 +64,6 @@ void OptionalKeyOpcode::parseFromString(const QString& opcodeName, const QString { if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } m_value = opcodeValue.toInt(successful); - // Integer opcodes can also define keys, which can be specified either by a key number of by a string like e4 or c#5 if (!*successful) { m_value = stringToKeyNum(opcodeValue, successful); } *parsed = true; } @@ -78,11 +78,11 @@ void StringOpcode::parseFromString(const QString& opcodeName, const QString& opc } // Modulatable Opcodes are a type of float opcode which can be adjusted based on different midi CC knob values -// They have both an initial value defined by their opcodename=value -// but also opcodename_oncc123=modulation_amount +// They have both an initial value defined by their: opcodename=value +// but also modulation defined by: opcodename_oncc123=modulation_amount // or opcodename_cc123=modulation_amount // or opcodenamecc123=modulation_amount -// will make the value be increased proportional to modulation_amount when the midi CC knob 123 is turned up +// which will make the value be increased proportional to modulation_amount when the midi CC knob 123 is turned up void ModulatableOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { for (QString alias : m_opcodeNames) @@ -144,9 +144,6 @@ void EnvelopeOpcodes::parseEnvelopeGeneratorOpcode(const QString& opcode, const // LFO Opcodes void LfoOpcodes::parseLfoGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful) { - // Pass the opcode name/value to all of the bundled opcodes. - // If it matches one of them, that's great. If not, they will return and nothing will happen. - // TODO is this error handling with "successful" correct? delay.parseFromString(opcode, value, parsed, successful); fade.parseFromString(opcode, value, parsed, successful); freq.parseFromString(opcode, value, parsed, successful); @@ -156,7 +153,7 @@ void LfoOpcodes::parseLfoGeneratorOpcode(const QString& opcode, const QString& v // -// Bespoke Enum-like Opcodes +// Specialized Enum-like Opcodes // template<> diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzSampler/SfzOpcodes.h index b168851bf17..a800cca8ad2 100644 --- a/plugins/SfzSampler/SfzOpcodes.h +++ b/plugins/SfzSampler/SfzOpcodes.h @@ -38,6 +38,7 @@ namespace lmms //! Normal MIDI CC's range from 0 to 127. More advanced SFZ's go beyond that, but for now we cap it at 128. This should be extended in the future. constexpr const int NumMidiCCs = 128; + /* Helper Functions */ //! Helper function for converting strings like "A5" or "B#2" into integers representing keys on the midi keyboard. @@ -59,7 +60,7 @@ struct BaseOpcode //! Given a string like "pitch_keytrack=1200" which has been split into name/value as "pitch_keytrack", "1200", this function updates the internal value if the name matches. virtual void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) = 0; }; -//! A base struct for all opcodes, just a name and a value. +//! A base struct for all opcodes, just a name(s) and a value. template struct Opcode : BaseOpcode { @@ -70,7 +71,7 @@ struct Opcode : BaseOpcode Opcode(QString name, T defaultValue) : m_opcodeNames({name}), m_value(defaultValue) {} Opcode(std::vector names, T defaultValue) : m_opcodeNames(names), m_value(defaultValue) {} virtual void setValue(const T& value) { m_value = value; } - virtual const T value() const { return m_value; } + virtual const T value() const { return m_value; } // TODO override operators instead void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; }; @@ -78,7 +79,7 @@ struct Opcode : BaseOpcode //! Float/decimal opcodes (such as amplitude, panning, etc) using FloatOpcode = Opcode; -//! Some float opcodes may take on a null default value (filter cutoff) +//! Some float opcodes may take on a null default value (like filter cutoff) using OptionalFloatOpcode = Opcode>; //! Integer opcodes (such as lovel, seq_length, seq_position, offset, etc) @@ -125,7 +126,7 @@ struct EnvelopeOpcodes ModulatableOpcode release; // Depth is only used for pitcheg and fileg, not ampeg, but we have it here anyway ModulatableOpcode depth; - // Velocity modulation amount + // Velocity modulation amount (TODO: not used yet) FloatOpcode vel2delay; FloatOpcode vel2attack; FloatOpcode vel2hold; @@ -162,7 +163,7 @@ struct EnvelopeOpcodes release.updateCachedModulation(ccValues); depth.updateCachedModulation(ccValues); } - //! Helper function for parsing all these envelope generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter envelopes + //! Helper function for parsing all these envelope generator opcodes void parseEnvelopeGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful); }; @@ -189,7 +190,7 @@ struct LfoOpcodes freq.updateCachedModulation(ccValues); depth.updateCachedModulation(ccValues); } - //! Helper function for parsing these lfo generator opcodes, so that the code isn't duplicated for the amplitude, pitch, and filter lfos + //! Helper function for parsing these lfo generator opcodes void parseLfoGeneratorOpcode(const QString& opcode, const QString& value, bool* parsed, bool* successful); }; @@ -198,7 +199,7 @@ struct LfoOpcodes // Specific Template specializations for Enum-like opcodes -// Trigger Type + enum class TriggerType { Attack, @@ -238,4 +239,4 @@ template<> void Opcode::parseFromString(const QString& opcodeName, c } // namespace lmms -#endif // LMMS_SFZ_OPCODES_H \ No newline at end of file +#endif // LMMS_SFZ_OPCODES_H diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzSampler/SfzParser.cpp index 11850683c25..d2005c1af79 100644 --- a/plugins/SfzSampler/SfzParser.cpp +++ b/plugins/SfzSampler/SfzParser.cpp @@ -51,7 +51,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // Before parsing the headers and opcodes, we need to hande #include and #define statements // This amounts to recursively loading and copy/pasting the contents of the other files where the #include is, and find/replacing the #define words with their values - std::map defineMap = {}; // Just a temporary helper variable so that the recursion branches have a persistance object to keep track of #defines + std::map defineMap = {}; // Just a temporary helper variable so that the recursion branches have a persistant object to keep track of #defines fileContents = recursiveHandleIncludeAndDefineStatements(parentDirectory, fileContents, defineMap); qDebug().noquote() << "[SFZ Parser] DEBUG: Preprocessed SFZ file contents:\n" << fileContents; // testing @@ -69,7 +69,7 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou ampeg_release=0.3 lokey=45 hikey=49 */ // This would still be parsed into segments as "", "ampeg_release=0.3", "lokey=45", and "hikey=49" - // According to the SFZ format website, there must never be a space on either side of the = for an opcode assignment. That makes things simpler + // According to the SFZ format website, there must never be a space on either side of the = for an opcode assignment (so something like "lokey = 45" would be invalid). That makes things simpler. // However, there can be spaces in the value assigned to the opcode. For example, if your sample file name has spaces: /* sample=Grand Piano G4 MP.wav key=49 @@ -365,4 +365,4 @@ QString SfzParser::recursiveHandleIncludeAndDefineStatements(const QDir& parentD -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzSampler/SfzParser.h index 9522aa4fe90..7744bafd69b 100644 --- a/plugins/SfzSampler/SfzParser.h +++ b/plugins/SfzSampler/SfzParser.h @@ -67,4 +67,4 @@ class SfzParser } // namespace lmms -#endif // LMMS_SFZ_PARSER_H \ No newline at end of file +#endif // LMMS_SFZ_PARSER_H diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzSampler/SfzRegion.cpp index 5cea42b79e7..65ebc26ebd4 100644 --- a/plugins/SfzSampler/SfzRegion.cpp +++ b/plugins/SfzSampler/SfzRegion.cpp @@ -155,4 +155,4 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzSampler/SfzRegion.h index 37f93e4777e..9516aa6cc92 100644 --- a/plugins/SfzSampler/SfzRegion.h +++ b/plugins/SfzSampler/SfzRegion.h @@ -58,13 +58,15 @@ class SfzRegion : public SfzOpcodeState //! Returns true if successful bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool); + //! Returns a shared pointer to the sample object for this region std::shared_ptr sample() const { return m_sample; } + //! Returns the type of basic wave for this region, if it specified instead of a real sample. + //! If a sample was specified, this returns SfzBasicWaves::Shape::Silence by default. const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } private: //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. - //! This uses an std::shared_ptr so that the sample object doesn't get unexpectedly deleted in the middle of the audio processing while a new sfz file is being loaded. TODO is this an issue? std::shared_ptr m_sample = nullptr; //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; @@ -87,4 +89,4 @@ class SfzRegion : public SfzOpcodeState } // namespace lmms -#endif // LMMS_SFZ_REGION_H \ No newline at end of file +#endif // LMMS_SFZ_REGION_H diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzSampler/SfzRegionManager.cpp index b1f86ef2904..4a0d82aa447 100644 --- a/plugins/SfzSampler/SfzRegionManager.cpp +++ b/plugins/SfzSampler/SfzRegionManager.cpp @@ -31,7 +31,7 @@ namespace lmms SfzRegionManager::SfzRegionManager(std::vector& regions) - : m_regions(regions) + : m_regions(regions) // TODO should this use std::move? { qDebug() << "[SFZ Player] Constructing key/region lookup tables..."; // For each key, figure out what list of regions could include it, to make a kind of look-up table @@ -41,7 +41,7 @@ SfzRegionManager::SfzRegionManager(std::vector& regions) { if (key >= region.m_lokey.value() && key <= region.m_hikey.value()) { - // Additionally sort by trigger type + // Additionally organize by trigger type switch (region.m_trigger.value()) { case TriggerType::Attack: @@ -57,7 +57,7 @@ SfzRegionManager::SfzRegionManager(std::vector& regions) } // Unfortunately, it's more difficult to make lookup tables for midi CC events. Instead, just use a big vector with all the regions by default - // TODO or is it? there's probably a way. But currently we actually don't support cc events triggering regions, so technically this will never be used at the moment. + // TODO or is it? there's probably a way. But currently we actually don't support cc events triggering regions, so it's not as important at the moment. for (auto& region : m_regions) { m_allRegions.push_back(®ion); @@ -80,7 +80,4 @@ const std::vector& SfzRegionManager::findPotentialMatchingRegions(co return m_allRegions; // This was added to prevent a warning, but I don't think it's necessary, since the switch covers all cases. } - - - -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzSampler/SfzRegionPlayState.cpp index cc9aa8e802e..c0324b46edb 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.cpp +++ b/plugins/SfzSampler/SfzRegionPlayState.cpp @@ -30,7 +30,6 @@ #include "Engine.h" #include "lmms_math.h" -#include "MicroTimer.h" #include namespace lmms @@ -44,7 +43,7 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger , m_region(region) , m_sampleObject(region->sample()) { - // Calculate the base pitch and amplitude + // Calculate the base pitch and amplitude so that we don't need to every buffer precomputeBaseValues(); // Delay the start of the playback by the trigger offset m_frameCount -= trigger.frameOffset(); @@ -90,7 +89,7 @@ void SfzRegionPlayState::precomputeBaseValues() const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; // Compute the base pitch - // The pitch env/lfo will be applied on top of this, as they are calcualted per frame + // The pitch env/lfo will be applied on top of this, as those are calcualted per frame // Calculate pitch difference relative to original sample const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); @@ -101,7 +100,7 @@ void SfzRegionPlayState::precomputeBaseValues() + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f; m_baseFreqRatio = std::exp2(pitch / 12.0f); - // Sample rate of sample (if we are using a sample, not a basic wave like *sine or *saw) + // Sample rate of sample const float sampleSampleRate = m_sampleObject != nullptr ? m_sampleObject->sampleRate() : m_lmmsSampleRate; // If we are using a basic wave instead of a sample, set it to LMMS's sample rate @@ -258,6 +257,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // Now render the audio for (f_cnt_t f = 0; f < frames; ++f) { + // Compute the current envelope/lfo values const float ampeg = envelopeGenerator(ampegDelayFrames, ampegAttackFrames, ampegHoldFrames, ampegDecayFrames, ampegSustain, ampegReleaseFrames); const float amplfo = amplfoDepth != 0.0f // Only compute the amplitude lfo if the depth is nonzero ? dbfsToAmp(lfoGenerator(amplfoDelayFrames, amplfoFadeFrames, amplfoFreq) * amplfoDepth) // amplfo depth is in decibels, so convert to amplitude @@ -331,7 +331,7 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) if (trigger.key() == m_trigger.key()) { m_released = true; - m_releaseFrame = m_frameCount; // testing + m_releaseFrame = m_frameCount; } } // Since the base pitch and amplitude are precomputed in the constructor, they need to be re-computed if any of the CC modulations may have changed @@ -342,4 +342,4 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) } } -} // namespace lmms \ No newline at end of file +} // namespace lmms diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzSampler/SfzRegionPlayState.h index 79951b4b32a..4797dead29d 100644 --- a/plugins/SfzSampler/SfzRegionPlayState.h +++ b/plugins/SfzSampler/SfzRegionPlayState.h @@ -48,8 +48,8 @@ class SfzRegionPlayState SfzRegionPlayState() = default; // The default constructor is needed to initialize arrays of the object //! Helper function to calculate the base pitch and amplitude so that it doesn't have to be done per-buffer. - // This is done once during the constructor, but it must also be done whenever a CC knob - // changes, since that could affect the base pitch/amp of the playback. + //! This is done once during the constructor, but it must also be done whenever a CC knob + //! changes, since that could affect the base pitch/amp of the playback. void precomputeBaseValues(); //! Generates the next buffer of audio from this sound. If m_active is false, this function does nothing. @@ -82,7 +82,7 @@ class SfzRegionPlayState //! Cache the rate the sample should be played (based on pitch and sample rate, compared to lmms's sample rate) // rather than computing it per buffer/frame. This does not include the effect of the pitch env/lfo, since those are computed every frame float m_baseFreqRatio = 0.0f; - //! Cache the base amplitude. The amplitude envelope/lfo will be applied on top of this. + //! Similarly cache the base amplitude. The amplitude envelope/lfo will be applied on top of this. float m_baseAmplitudeLeft = 1.0f; float m_baseAmplitudeRight = 1.0f; @@ -112,4 +112,4 @@ class SfzRegionPlayState } // namespace lmms -#endif // LMMS_SFZ_REGION_PLAY_STATE_H \ No newline at end of file +#endif // LMMS_SFZ_REGION_PLAY_STATE_H diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index 9b8f9e67fcd..c43c628f732 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -72,7 +72,7 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) Engine::audioEngine()->addPlayHandle(iph); // When a user loads a new SFZ file, the main thread prepares the new data to be swapped out by the audio thread, but the main thread - // needs to delete the old data afterwards, so here we connect some kind of run-loop so that the main thread gets a chance to check + // needs to delete the old data afterwards, so here we connect some kind of run-loop so that the main thread gets a chance to check after the audio thread acts connect(lmms::gui::getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(mainThreadUpdateAfterDataSwap())); // I really don't like this. Would it be better to use our own QTimer, not LMMS's gui's? emit dataChanged(); @@ -80,7 +80,8 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) SfzSampler::~SfzSampler() { - m_sampleLoadingThread.join(); // Make sure to finish the sample loading thread before the plugin exits, or else the program might terminate + // Make sure to end the sample loading thread before the plugin exits, or else the program might forcefully terminate + if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } } @@ -112,20 +113,18 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { - //int noteIndex = handle->key(); - //const fpp_t frames = handle->framesLeftForCurrentPeriod(); - //const f_cnt_t offset = handle->noteOffset(); + // TODO: Currently this instrument only uses midi events as input, and a single audio buffer as output. + // It might be possible to split things up to use individual NotePlayHandles, but that is for another time. } void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) { - //delete static_cast(handle->m_pluginData); } void SfzSampler::processTrigger(const SfzTrigger& trigger) { - if (m_regionManager == nullptr) { return; } // Before any SFZ file is loaded, m_regionManager is nullptr + if (m_regionManager == nullptr) { return; } // Before any SFZ file is loaded, m_regionManager is nullptr, so there's no regions to trigger. // Notify the global state to update which keys are active, update midi CC values, etc m_sfzGlobalState.processTrigger(trigger); @@ -157,9 +156,9 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) break; } } + if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } // For fun, display the last played sample on the GUI setStatusInfo("Last Played Sample: " + QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName()); - if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } } } @@ -198,7 +197,7 @@ void SfzSampler::play(SampleFrame* workingBuffer) // In the event that the user tries to load a new sfz file while the current one is playing, the region and sample data will be - // loaded into temporary member variables, and it's the job of the audio thread to swap them into place when the data is ready and it gets a chance + // loaded into temporary member variables, and it's the job of the audio thread to swap them into place when the data is ready audioThreadHandleNewSfzData(); // Increment the buffer counter so that the main thread knows when it's safe to delete the old objects m_bufferCounter++; @@ -235,7 +234,7 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // Or, if the new sample/region data was just swapped into place a couple buffers ago, return just to be safe if (m_justSwappedData && m_bufferCounter <= m_bufferCounterWhenDataReady + 2) { - qDebug() << "[SFZ Player] Requested to load new SFZ file while new region/sample data from previous SFZ file have not yet been swapped into place. Ignoring."; + qDebug() << "[SFZ Player] Requested to load new SFZ file while new region/sample data from previous SFZ file may not yet have been swapped into place. Ignoring."; return; } // Set this variable so that after the audio thread swaps the data, the main thread remembers whether to reset the CC knobs to the SFZ file defaults or just to the current InstrumentTrack CC knob values @@ -247,7 +246,7 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // And any info about control labels, default values, etc m_controlsConfig = SfzControlsConfig(); - // Temporary vector to store SfzRegion objects + // Temporary vector to store SfzRegion objects as they are parsed std::vector regions; // Parse all the headers of the .sfz (accounting for and defaults) and populate the regions vector with the new SfzRegions // The header is also parsed into a separate object for easy access by the gui @@ -255,6 +254,11 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) if (!successfulParseFile) { qDebug() << "[SFZ Player] An error occurred when parsing the SFZ file."; return; } + // Set the initial cc values based on any `set_ccN` opcodes in the header + m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); + + m_sfzFilePath = filePath; + // Hand off the vector of regions to SfzRegionManager, which will sort them out to optimize trigger selection // Don't immediately set m_regionManager, since the audio thread may still be accessing it; instead set the temporary object m_tempRegionManager = new SfzRegionManager(regions); @@ -267,11 +271,6 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // Initialize a new sample pool for the new samples to be loaded in m_tempSamplePool = new SfzSamplePool(); - // Set the initial cc values based on any `set_ccN` opcodes in the header - m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); - - m_sfzFilePath = filePath; - // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active @@ -301,9 +300,10 @@ void SfzSampler::audioThreadHandleNewSfzData() { if (m_newSfzDataReady) { + // Swap the temporary object pointers with the real ones std::swap(m_regionManager, m_tempRegionManager); std::swap(m_samplePool, m_tempSamplePool); - // And reset the active voices, since they may have invalid pointers to old region objects + // And reset the active voices, since they have pointers to old region objects std::fill_n(m_voices.begin(), m_maxActiveIndex + 1, SfzRegionPlayState()); m_newSfzDataReady = false; m_justSwappedData = true; @@ -364,7 +364,7 @@ gui::PluginView* SfzSampler::instantiateView(QWidget* parent) void SfzSampler::setStatusInfo(const QString& text) { // Print to console - qDebug() << "[SFZ Player]" << text; + qDebug().noquote() << "[SFZ Player]" << text; // And send to GUI // (Instead of actually sending a Qt signal, which seemed to cause off crashes, simply set a member variable and let the GUI read it when it wants) m_statusText = text; diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzSampler/SfzSampler.h index 334f9e7c7e1..8f495787859 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzSampler/SfzSampler.h @@ -69,7 +69,6 @@ class SfzSampler : public Instrument signals: void fileLoaded(); - void statusInfo(const QString& statusText); // Pass info to the GUI to display things like current samples being loaded, last sample played, etc private: void processTrigger(const SfzTrigger& trigger); @@ -85,7 +84,7 @@ class SfzSampler : public Instrument //! By always spawning new sounds at the lowest open index and keeping track of the maximum index which contains an actice sound, //! you only need to loop through the first n elements and ignore the rest since you know they are inactive (Thanks to Lost Robot for the idea) size_t m_maxActiveIndex = 0; - //! Helper function to figure out what the maximum active index is, in the event the maximum index deacticated + //! Helper function to figure out what the maximum active index is, in the event the maximum index deactivated void recalculateMaxActiveIndex(); //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzSampler/SfzTrigger.h index a754018f895..95c976e329a 100644 --- a/plugins/SfzSampler/SfzTrigger.h +++ b/plugins/SfzSampler/SfzTrigger.h @@ -68,4 +68,4 @@ class SfzTrigger } // namespace lmms -#endif // LMMS_SFZ_TRIGGER_H \ No newline at end of file +#endif // LMMS_SFZ_TRIGGER_H From 712194a38e1401514275884c92dfadbe20db10ba Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 8 May 2026 10:17:07 -0400 Subject: [PATCH 100/131] Use f_cnt_t instead of size_t to fix macos builds --- plugins/SfzSampler/SfzSampleBuffer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/SfzSampler/SfzSampleBuffer.cpp b/plugins/SfzSampler/SfzSampleBuffer.cpp index b16fe6e4518..6f8c56ae636 100644 --- a/plugins/SfzSampler/SfzSampleBuffer.cpp +++ b/plugins/SfzSampler/SfzSampleBuffer.cpp @@ -48,14 +48,14 @@ float SfzSampleBuffer::at(const float index, const size_t channel) const { if (index < 0 || index >= m_size) { return 0.0f; } - const size_t indexFloor = static_cast(index); + const f_cnt_t indexFloor = static_cast(index); float frac = index - indexFloor; - size_t i0 = indexFloor == 0 ? 0 : indexFloor - 1; - size_t i1 = indexFloor; - size_t i2 = std::min(indexFloor + 1, m_size - 1); - size_t i3 = std::min(indexFloor + 2, m_size - 1); + f_cnt_t i0 = indexFloor == 0 ? 0 : indexFloor - 1; + f_cnt_t i1 = indexFloor; + f_cnt_t i2 = std::min(indexFloor + 1, m_size - 1); + f_cnt_t i3 = std::min(indexFloor + 2, m_size - 1); float v0 = m_data[i0][channel]; float v1 = m_data[i1][channel]; From e4b7d66ea0f9f15ecca996b1bdf2c1bc5864da65 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 8 May 2026 10:58:31 -0400 Subject: [PATCH 101/131] Set name of instrument when drag/drop file into song editor --- plugins/SfzSampler/SfzSampler.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzSampler/SfzSampler.cpp index c43c628f732..f184b339c92 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzSampler/SfzSampler.cpp @@ -220,6 +220,8 @@ void SfzSampler::recalculateMaxActiveIndex() void SfzSampler::loadFile(const QString& filePath) { loadSfzFile(filePath, true); // Passing true to reset the InstrumentTrack's midi CC knobs to the SFZ file's defaults + // Set the instrument name to the filename + m_parentTrack->setName(PathUtil::cleanName(filePath)); } From dc86d26b20585be902694905ea38a17cebb9d32a Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 8 May 2026 11:49:39 -0400 Subject: [PATCH 102/131] Rename SfzSampler to SFZ Player --- cmake/modules/PluginList.cmake | 3 +- .../{SfzSampler => SfzPlayer}/CMakeLists.txt | 14 ++--- .../SfzBasicWaves.cpp | 0 .../{SfzSampler => SfzPlayer}/SfzBasicWaves.h | 0 .../SfzControlsConfig.h | 0 .../SfzGlobalState.cpp | 0 .../SfzGlobalState.h | 0 .../SfzOpcodeState.cpp | 0 .../SfzOpcodeState.h | 0 .../{SfzSampler => SfzPlayer}/SfzOpcodes.cpp | 0 .../{SfzSampler => SfzPlayer}/SfzOpcodes.h | 0 .../{SfzSampler => SfzPlayer}/SfzParser.cpp | 0 plugins/{SfzSampler => SfzPlayer}/SfzParser.h | 0 .../SfzPlayer.cpp} | 60 +++++++++---------- .../SfzSampler.h => SfzPlayer/SfzPlayer.h} | 18 +++--- .../SfzPlayerView.cpp} | 26 ++++---- .../SfzPlayerView.h} | 16 ++--- .../{SfzSampler => SfzPlayer}/SfzRegion.cpp | 0 plugins/{SfzSampler => SfzPlayer}/SfzRegion.h | 0 .../SfzRegionManager.cpp | 0 .../SfzRegionManager.h | 0 .../SfzRegionPlayState.cpp | 0 .../SfzRegionPlayState.h | 0 .../SfzSampleBuffer.cpp | 0 .../SfzSampleBuffer.h | 0 .../SfzSamplePool.cpp | 0 .../{SfzSampler => SfzPlayer}/SfzSamplePool.h | 0 .../{SfzSampler => SfzPlayer}/SfzTrigger.cpp | 0 .../{SfzSampler => SfzPlayer}/SfzTrigger.h | 0 plugins/{SfzSampler => SfzPlayer}/logo.svg | 0 30 files changed, 68 insertions(+), 69 deletions(-) rename plugins/{SfzSampler => SfzPlayer}/CMakeLists.txt (85%) rename plugins/{SfzSampler => SfzPlayer}/SfzBasicWaves.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzBasicWaves.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzControlsConfig.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzGlobalState.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzGlobalState.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzOpcodeState.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzOpcodeState.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzOpcodes.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzOpcodes.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzParser.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzParser.h (100%) rename plugins/{SfzSampler/SfzSampler.cpp => SfzPlayer/SfzPlayer.cpp} (88%) rename plugins/{SfzSampler/SfzSampler.h => SfzPlayer/SfzPlayer.h} (96%) rename plugins/{SfzSampler/SfzSamplerView.cpp => SfzPlayer/SfzPlayerView.cpp} (88%) rename plugins/{SfzSampler/SfzSamplerView.h => SfzPlayer/SfzPlayerView.h} (85%) rename plugins/{SfzSampler => SfzPlayer}/SfzRegion.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzRegion.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzRegionManager.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzRegionManager.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzRegionPlayState.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzRegionPlayState.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzSampleBuffer.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzSampleBuffer.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzSamplePool.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzSamplePool.h (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzTrigger.cpp (100%) rename plugins/{SfzSampler => SfzPlayer}/SfzTrigger.h (100%) rename plugins/{SfzSampler => SfzPlayer}/logo.svg (100%) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 06339b7e097..f5e3b68c34c 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -62,8 +62,7 @@ SET(LMMS_PLUGIN_LIST ReverbSC Sf2Player Sfxr - Sid - SfzSampler + SfzPlayer SlewDistortion SlicerT SpectrumAnalyzer diff --git a/plugins/SfzSampler/CMakeLists.txt b/plugins/SfzPlayer/CMakeLists.txt similarity index 85% rename from plugins/SfzSampler/CMakeLists.txt rename to plugins/SfzPlayer/CMakeLists.txt index cc52e98ea61..a29c702627e 100644 --- a/plugins/SfzSampler/CMakeLists.txt +++ b/plugins/SfzPlayer/CMakeLists.txt @@ -1,10 +1,10 @@ include(BuildPlugin) -build_plugin(sfzsampler - SfzSampler.cpp - SfzSampler.h - SfzSamplerView.cpp - SfzSamplerView.h +build_plugin(sfzplayer + SfzPlayer.cpp + SfzPlayer.h + SfzPlayerView.cpp + SfzPlayerView.h SfzOpcodes.cpp SfzOpcodes.h SfzOpcodeState.cpp @@ -29,8 +29,8 @@ build_plugin(sfzsampler SfzBasicWaves.cpp SfzBasicWaves.h MOCFILES - SfzSampler.h - SfzSamplerView.h + SfzPlayer.h + SfzPlayerView.h SfzOpcodes.h SfzOpcodeState.h SfzRegion.h diff --git a/plugins/SfzSampler/SfzBasicWaves.cpp b/plugins/SfzPlayer/SfzBasicWaves.cpp similarity index 100% rename from plugins/SfzSampler/SfzBasicWaves.cpp rename to plugins/SfzPlayer/SfzBasicWaves.cpp diff --git a/plugins/SfzSampler/SfzBasicWaves.h b/plugins/SfzPlayer/SfzBasicWaves.h similarity index 100% rename from plugins/SfzSampler/SfzBasicWaves.h rename to plugins/SfzPlayer/SfzBasicWaves.h diff --git a/plugins/SfzSampler/SfzControlsConfig.h b/plugins/SfzPlayer/SfzControlsConfig.h similarity index 100% rename from plugins/SfzSampler/SfzControlsConfig.h rename to plugins/SfzPlayer/SfzControlsConfig.h diff --git a/plugins/SfzSampler/SfzGlobalState.cpp b/plugins/SfzPlayer/SfzGlobalState.cpp similarity index 100% rename from plugins/SfzSampler/SfzGlobalState.cpp rename to plugins/SfzPlayer/SfzGlobalState.cpp diff --git a/plugins/SfzSampler/SfzGlobalState.h b/plugins/SfzPlayer/SfzGlobalState.h similarity index 100% rename from plugins/SfzSampler/SfzGlobalState.h rename to plugins/SfzPlayer/SfzGlobalState.h diff --git a/plugins/SfzSampler/SfzOpcodeState.cpp b/plugins/SfzPlayer/SfzOpcodeState.cpp similarity index 100% rename from plugins/SfzSampler/SfzOpcodeState.cpp rename to plugins/SfzPlayer/SfzOpcodeState.cpp diff --git a/plugins/SfzSampler/SfzOpcodeState.h b/plugins/SfzPlayer/SfzOpcodeState.h similarity index 100% rename from plugins/SfzSampler/SfzOpcodeState.h rename to plugins/SfzPlayer/SfzOpcodeState.h diff --git a/plugins/SfzSampler/SfzOpcodes.cpp b/plugins/SfzPlayer/SfzOpcodes.cpp similarity index 100% rename from plugins/SfzSampler/SfzOpcodes.cpp rename to plugins/SfzPlayer/SfzOpcodes.cpp diff --git a/plugins/SfzSampler/SfzOpcodes.h b/plugins/SfzPlayer/SfzOpcodes.h similarity index 100% rename from plugins/SfzSampler/SfzOpcodes.h rename to plugins/SfzPlayer/SfzOpcodes.h diff --git a/plugins/SfzSampler/SfzParser.cpp b/plugins/SfzPlayer/SfzParser.cpp similarity index 100% rename from plugins/SfzSampler/SfzParser.cpp rename to plugins/SfzPlayer/SfzParser.cpp diff --git a/plugins/SfzSampler/SfzParser.h b/plugins/SfzPlayer/SfzParser.h similarity index 100% rename from plugins/SfzSampler/SfzParser.h rename to plugins/SfzPlayer/SfzParser.h diff --git a/plugins/SfzSampler/SfzSampler.cpp b/plugins/SfzPlayer/SfzPlayer.cpp similarity index 88% rename from plugins/SfzSampler/SfzSampler.cpp rename to plugins/SfzPlayer/SfzPlayer.cpp index f184b339c92..760b8de10c6 100644 --- a/plugins/SfzSampler/SfzSampler.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -1,5 +1,5 @@ /* - * SfzSampler.cpp - Simple SFZ instrument player + * SfzPlayer.cpp - Simple SFZ instrument player * * Copyright (c) 2026 Keratin * @@ -22,7 +22,7 @@ * */ -#include "SfzSampler.h" +#include "SfzPlayer.h" #include "SfzParser.h" #include @@ -33,7 +33,7 @@ #include "InstrumentTrack.h" #include "PathUtil.h" #include "ConfigManager.h" -#include "SfzSamplerView.h" +#include "SfzPlayerView.h" #include "Song.h" #include "embed.h" #include "interpolation.h" @@ -46,11 +46,11 @@ namespace lmms { extern "C" { -Plugin::Descriptor PLUGIN_EXPORT sfzsampler_plugin_descriptor = { +Plugin::Descriptor PLUGIN_EXPORT sfzplayer_plugin_descriptor = { LMMS_STRINGIFY(PLUGIN_NAME), - "SfzSampler", - QT_TRANSLATE_NOOP("PluginBrowser", "Basic Slicer"), - "Daniel Kauss Serna ", + "SFZ Player", + QT_TRANSLATE_NOOP("PluginBrowser", "Load .sfz Instrument Files"), + "Keratin <3", 0x0100, Plugin::Type::Instrument, new PluginPixmapLoader("logo"), @@ -59,13 +59,13 @@ Plugin::Descriptor PLUGIN_EXPORT sfzsampler_plugin_descriptor = { }; PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) { - return new SfzSampler(static_cast(m)); + return new SfzPlayer(static_cast(m)); } } // end extern -SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) - : Instrument(instrumentTrack, &sfzsampler_plugin_descriptor, nullptr, Flag::IsSingleStreamed) +SfzPlayer::SfzPlayer(InstrumentTrack* instrumentTrack) + : Instrument(instrumentTrack, &sfzplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed) , m_parentTrack(instrumentTrack) { auto iph = new InstrumentPlayHandle(this, instrumentTrack); @@ -78,7 +78,7 @@ SfzSampler::SfzSampler(InstrumentTrack* instrumentTrack) emit dataChanged(); } -SfzSampler::~SfzSampler() +SfzPlayer::~SfzPlayer() { // Make sure to end the sample loading thread before the plugin exits, or else the program might forcefully terminate if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } @@ -86,7 +86,7 @@ SfzSampler::~SfzSampler() -bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) +bool SfzPlayer::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) { if (event.type() == MidiNoteOn) { @@ -111,18 +111,18 @@ bool SfzSampler::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_ return false; } -void SfzSampler::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) +void SfzPlayer::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { // TODO: Currently this instrument only uses midi events as input, and a single audio buffer as output. // It might be possible to split things up to use individual NotePlayHandles, but that is for another time. } -void SfzSampler::deleteNotePluginData(NotePlayHandle* handle) +void SfzPlayer::deleteNotePluginData(NotePlayHandle* handle) { } -void SfzSampler::processTrigger(const SfzTrigger& trigger) +void SfzPlayer::processTrigger(const SfzTrigger& trigger) { if (m_regionManager == nullptr) { return; } // Before any SFZ file is loaded, m_regionManager is nullptr, so there's no regions to trigger. @@ -180,7 +180,7 @@ void SfzSampler::processTrigger(const SfzTrigger& trigger) -void SfzSampler::play(SampleFrame* workingBuffer) +void SfzPlayer::play(SampleFrame* workingBuffer) { const f_cnt_t frames = Engine::audioEngine()->framesPerPeriod(); @@ -205,7 +205,7 @@ void SfzSampler::play(SampleFrame* workingBuffer) -void SfzSampler::recalculateMaxActiveIndex() +void SfzPlayer::recalculateMaxActiveIndex() { // Loop backward from the old max active index to find the next play state which is active while (m_maxActiveIndex > 0) @@ -217,7 +217,7 @@ void SfzSampler::recalculateMaxActiveIndex() -void SfzSampler::loadFile(const QString& filePath) +void SfzPlayer::loadFile(const QString& filePath) { loadSfzFile(filePath, true); // Passing true to reset the InstrumentTrack's midi CC knobs to the SFZ file's defaults // Set the instrument name to the filename @@ -225,7 +225,7 @@ void SfzSampler::loadFile(const QString& filePath) } -void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) +void SfzPlayer::loadSfzFile(const QString& filePath, const bool resetCCKnobs) { // If the sample loading thread is already running, don't let the user load another file. if (m_currentlyLoadingSamples) @@ -276,10 +276,10 @@ void SfzSampler::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active - m_sampleLoadingThread = std::thread(&SfzSampler::sampleLoadingThreadFunction, this, parentDirectory); + m_sampleLoadingThread = std::thread(&SfzPlayer::sampleLoadingThreadFunction, this, parentDirectory); } -void SfzSampler::sampleLoadingThreadFunction(const QDir& parentDirectory) +void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) { int i = 0; for (auto* region : m_tempRegionManager->allRegions()) @@ -298,7 +298,7 @@ void SfzSampler::sampleLoadingThreadFunction(const QDir& parentDirectory) m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? } -void SfzSampler::audioThreadHandleNewSfzData() +void SfzPlayer::audioThreadHandleNewSfzData() { if (m_newSfzDataReady) { @@ -312,7 +312,7 @@ void SfzSampler::audioThreadHandleNewSfzData() } } -void SfzSampler::mainThreadUpdateAfterDataSwap() +void SfzPlayer::mainThreadUpdateAfterDataSwap() { if (m_justSwappedData && m_bufferCounter > m_bufferCounterWhenDataReady + 2) { @@ -337,12 +337,12 @@ void SfzSampler::mainThreadUpdateAfterDataSwap() } -void SfzSampler::saveSettings(QDomDocument& document, QDomElement& element) +void SfzPlayer::saveSettings(QDomDocument& document, QDomElement& element) { element.setAttribute("sfzfile", m_sfzFilePath); } -void SfzSampler::loadSettings(const QDomElement& element) +void SfzPlayer::loadSettings(const QDomElement& element) { m_sfzFilePath = element.attribute("sfzfile"); if (!m_sfzFilePath.isEmpty()) @@ -351,19 +351,19 @@ void SfzSampler::loadSettings(const QDomElement& element) } // TODO add error handling, if path doesn't exist } -QString SfzSampler::nodeName() const +QString SfzPlayer::nodeName() const { - return sfzsampler_plugin_descriptor.name; + return sfzplayer_plugin_descriptor.name; } -gui::PluginView* SfzSampler::instantiateView(QWidget* parent) +gui::PluginView* SfzPlayer::instantiateView(QWidget* parent) { - return new gui::SfzSamplerView(this, parent); + return new gui::SfzPlayerView(this, parent); } -void SfzSampler::setStatusInfo(const QString& text) +void SfzPlayer::setStatusInfo(const QString& text) { // Print to console qDebug().noquote() << "[SFZ Player]" << text; diff --git a/plugins/SfzSampler/SfzSampler.h b/plugins/SfzPlayer/SfzPlayer.h similarity index 96% rename from plugins/SfzSampler/SfzSampler.h rename to plugins/SfzPlayer/SfzPlayer.h index 8f495787859..5b0bdcf54f9 100644 --- a/plugins/SfzSampler/SfzSampler.h +++ b/plugins/SfzPlayer/SfzPlayer.h @@ -1,5 +1,5 @@ /* - * SfzSampler.h - Simple SFZ instrument player + * SfzPlayer.h - Simple SFZ instrument player * * Copyright (c) 2026 Keratin * @@ -22,15 +22,15 @@ * */ -#ifndef LMMS_SFZSAMPLER_H -#define LMMS_SFZSAMPLER_H +#ifndef LMMS_SFZPLAYER_H +#define LMMS_SFZPLAYER_H #include "AutomatableModel.h" #include "ComboBoxModel.h" #include "Instrument.h" #include "Note.h" #include "Sample.h" -#include "SfzSamplerView.h" +#include "SfzPlayerView.h" #include "SfzParser.h" #include "SfzRegion.h" #include "SfzRegionPlayState.h" @@ -43,13 +43,13 @@ namespace lmms { -class SfzSampler : public Instrument +class SfzPlayer : public Instrument { Q_OBJECT public: - SfzSampler(InstrumentTrack* instrumentTrack); - ~SfzSampler(); + SfzPlayer(InstrumentTrack* instrumentTrack); + ~SfzPlayer(); void playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) override; void deleteNotePluginData(NotePlayHandle* handle) override; @@ -145,9 +145,9 @@ class SfzSampler : public Instrument private slots: void mainThreadUpdateAfterDataSwap(); - friend class gui::SfzSamplerView; + friend class gui::SfzPlayerView; }; } // namespace lmms -#endif // LMMS_SFZSAMPLER_H +#endif // LMMS_SFZPLAYER_H diff --git a/plugins/SfzSampler/SfzSamplerView.cpp b/plugins/SfzPlayer/SfzPlayerView.cpp similarity index 88% rename from plugins/SfzSampler/SfzSamplerView.cpp rename to plugins/SfzPlayer/SfzPlayerView.cpp index 87b45d28178..632e652dfc7 100644 --- a/plugins/SfzSampler/SfzSamplerView.cpp +++ b/plugins/SfzPlayer/SfzPlayerView.cpp @@ -1,5 +1,5 @@ /* - * SfzSamplerView.cpp - GUI for SfzSampler + * SfzPlayerView.cpp - GUI for SfzPlayer * * Copyright (c) 2026 Keratin * @@ -22,7 +22,7 @@ * */ -#include "SfzSamplerView.h" +#include "SfzPlayerView.h" #include #include @@ -39,7 +39,7 @@ #include "Knob.h" #include "LcdSpinBox.h" #include "PixmapButton.h" -#include "SfzSampler.h" +#include "SfzPlayer.h" #include "StringPairDrag.h" #include "Track.h" #include "embed.h" @@ -56,7 +56,7 @@ namespace lmms { namespace gui { -SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) +SfzPlayerView::SfzPlayerView(SfzPlayer* instrument, QWidget* parent) : InstrumentView(instrument, parent) , m_instrument(instrument) , m_statusLabel(new QLabel(this)) @@ -75,7 +75,7 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) auto layout1 = new QVBoxLayout(this); auto openFileButton = new QPushButton(embed::getIconPixmap("folder"), tr("Open SFZ File"), this); - connect(openFileButton, &PixmapButton::clicked, this, &SfzSamplerView::openFile); + connect(openFileButton, &PixmapButton::clicked, this, &SfzPlayerView::openFile); layout1->addWidget(openFileButton); layout1->addWidget(m_statusLabel); @@ -91,7 +91,7 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) // Whenever a new SFZ file is loaded, set the default CC values and update the info text - connect(m_instrument, &SfzSampler::fileLoaded, [this](){ onFileLoaded(); }); // this lambda is so bad, but it doesn't work as a slot for some reason + connect(m_instrument, &SfzPlayer::fileLoaded, [this](){ onFileLoaded(); }); // this lambda is so bad, but it doesn't work as a slot for some reason // Instead of sending a signal from the plugin every time a note is pressed (that seemed to cause odd crashes), simply update things periodically connect(getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(periodicUpdate())); @@ -102,7 +102,7 @@ SfzSamplerView::SfzSamplerView(SfzSampler* instrument, QWidget* parent) } -void SfzSamplerView::onFileLoaded() +void SfzPlayerView::onFileLoaded() { // Remove any old knobs delete m_controlsWidget; @@ -166,13 +166,13 @@ void SfzSamplerView::onFileLoaded() } } -void SfzSamplerView::periodicUpdate() +void SfzPlayerView::periodicUpdate() { m_statusLabel->setText(m_instrument->m_statusText); } -void SfzSamplerView::openFile() +void SfzPlayerView::openFile() { auto openFileDialog = FileDialog(nullptr, QObject::tr("Open SFZ File")); auto dir = ConfigManager::inst()->userSamplesDir(); @@ -185,7 +185,7 @@ void SfzSamplerView::openFile() } -void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) +void SfzPlayerView::dragEnterEvent(QDragEnterEvent* dee) { QString value = StringPairDrag::decodeValue(dee); if (value.endsWith(".sfz")) @@ -196,7 +196,7 @@ void SfzSamplerView::dragEnterEvent(QDragEnterEvent* dee) dee->ignore(); } -void SfzSamplerView::dropEvent(QDropEvent* de) +void SfzPlayerView::dropEvent(QDropEvent* de) { QString value = StringPairDrag::decodeValue(de); if (value.endsWith(".sfz")) @@ -208,11 +208,11 @@ void SfzSamplerView::dropEvent(QDropEvent* de) de->ignore(); } -void SfzSamplerView::resizeEvent(QResizeEvent* re) +void SfzPlayerView::resizeEvent(QResizeEvent* re) { } -void SfzSamplerView::paintEvent(QPaintEvent* pe) +void SfzPlayerView::paintEvent(QPaintEvent* pe) { //QPainter brush(this); //brush.fillRect(rect(), QColor(19, 19, 20)); diff --git a/plugins/SfzSampler/SfzSamplerView.h b/plugins/SfzPlayer/SfzPlayerView.h similarity index 85% rename from plugins/SfzSampler/SfzSamplerView.h rename to plugins/SfzPlayer/SfzPlayerView.h index cef0dac6e76..3bff4c28b6a 100644 --- a/plugins/SfzSampler/SfzSamplerView.h +++ b/plugins/SfzPlayer/SfzPlayerView.h @@ -1,5 +1,5 @@ /* - * SfzSamplerView.h - GUI for SfzSampler + * SfzPlayerView.h - GUI for SfzPlayer * * Copyright (c) 2026 Keratin * @@ -22,8 +22,8 @@ * */ -#ifndef LMMS_GUI_SFZSAMPLER_VIEW_H -#define LMMS_GUI_SFZSAMPLER_VIEW_H +#ifndef LMMS_GUI_SFZPLAYER_VIEW_H +#define LMMS_GUI_SFZPLAYER_VIEW_H #include "InstrumentView.h" @@ -32,7 +32,7 @@ class QLabel; namespace lmms { -class SfzSampler; +class SfzPlayer; namespace gui { @@ -40,12 +40,12 @@ namespace gui { //! This class is planned to be completely redone! //! In it's current form, it's simply a temporary GUI which shows the keyswitch info, the parameters, and a button to load a file. //! In the future, it would be great to have a much prettier and useful GUI -class SfzSamplerView : public InstrumentView +class SfzPlayerView : public InstrumentView { Q_OBJECT public: - SfzSamplerView(SfzSampler* instrument, QWidget* parent); + SfzPlayerView(SfzPlayer* instrument, QWidget* parent); public slots: void onFileLoaded(); @@ -62,7 +62,7 @@ public slots: private: bool isResizable() const override { return true; } - SfzSampler* m_instrument; + SfzPlayer* m_instrument; QLabel* m_statusLabel; QLabel* m_generalInfoLabel; @@ -76,4 +76,4 @@ public slots: } // namespace lmms -#endif // LMMS_GUI_SFZSAMPLER_VIEW_H +#endif // LMMS_GUI_SFZPLAYER_VIEW_H diff --git a/plugins/SfzSampler/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp similarity index 100% rename from plugins/SfzSampler/SfzRegion.cpp rename to plugins/SfzPlayer/SfzRegion.cpp diff --git a/plugins/SfzSampler/SfzRegion.h b/plugins/SfzPlayer/SfzRegion.h similarity index 100% rename from plugins/SfzSampler/SfzRegion.h rename to plugins/SfzPlayer/SfzRegion.h diff --git a/plugins/SfzSampler/SfzRegionManager.cpp b/plugins/SfzPlayer/SfzRegionManager.cpp similarity index 100% rename from plugins/SfzSampler/SfzRegionManager.cpp rename to plugins/SfzPlayer/SfzRegionManager.cpp diff --git a/plugins/SfzSampler/SfzRegionManager.h b/plugins/SfzPlayer/SfzRegionManager.h similarity index 100% rename from plugins/SfzSampler/SfzRegionManager.h rename to plugins/SfzPlayer/SfzRegionManager.h diff --git a/plugins/SfzSampler/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp similarity index 100% rename from plugins/SfzSampler/SfzRegionPlayState.cpp rename to plugins/SfzPlayer/SfzRegionPlayState.cpp diff --git a/plugins/SfzSampler/SfzRegionPlayState.h b/plugins/SfzPlayer/SfzRegionPlayState.h similarity index 100% rename from plugins/SfzSampler/SfzRegionPlayState.h rename to plugins/SfzPlayer/SfzRegionPlayState.h diff --git a/plugins/SfzSampler/SfzSampleBuffer.cpp b/plugins/SfzPlayer/SfzSampleBuffer.cpp similarity index 100% rename from plugins/SfzSampler/SfzSampleBuffer.cpp rename to plugins/SfzPlayer/SfzSampleBuffer.cpp diff --git a/plugins/SfzSampler/SfzSampleBuffer.h b/plugins/SfzPlayer/SfzSampleBuffer.h similarity index 100% rename from plugins/SfzSampler/SfzSampleBuffer.h rename to plugins/SfzPlayer/SfzSampleBuffer.h diff --git a/plugins/SfzSampler/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp similarity index 100% rename from plugins/SfzSampler/SfzSamplePool.cpp rename to plugins/SfzPlayer/SfzSamplePool.cpp diff --git a/plugins/SfzSampler/SfzSamplePool.h b/plugins/SfzPlayer/SfzSamplePool.h similarity index 100% rename from plugins/SfzSampler/SfzSamplePool.h rename to plugins/SfzPlayer/SfzSamplePool.h diff --git a/plugins/SfzSampler/SfzTrigger.cpp b/plugins/SfzPlayer/SfzTrigger.cpp similarity index 100% rename from plugins/SfzSampler/SfzTrigger.cpp rename to plugins/SfzPlayer/SfzTrigger.cpp diff --git a/plugins/SfzSampler/SfzTrigger.h b/plugins/SfzPlayer/SfzTrigger.h similarity index 100% rename from plugins/SfzSampler/SfzTrigger.h rename to plugins/SfzPlayer/SfzTrigger.h diff --git a/plugins/SfzSampler/logo.svg b/plugins/SfzPlayer/logo.svg similarity index 100% rename from plugins/SfzSampler/logo.svg rename to plugins/SfzPlayer/logo.svg From 78ead8f516ed08de97b60b5f2de625e08c867517 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 8 May 2026 11:57:35 -0400 Subject: [PATCH 103/131] Accidentally removed Sid --- cmake/modules/PluginList.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index f5e3b68c34c..34435972f75 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -62,6 +62,7 @@ SET(LMMS_PLUGIN_LIST ReverbSC Sf2Player Sfxr + Sid SfzPlayer SlewDistortion SlicerT From c1570b288a1c9caffa4376242712693dd72d3483 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 9 May 2026 14:57:44 -0400 Subject: [PATCH 104/131] Add basic modulation curves and mod types, and other minor changes --- plugins/SfzPlayer/SfzOpcodeState.cpp | 13 +++++++ plugins/SfzPlayer/SfzOpcodeState.h | 2 +- plugins/SfzPlayer/SfzOpcodes.cpp | 57 +++++++++++++++++++++++++++- plugins/SfzPlayer/SfzOpcodes.h | 29 +++++++++++++- plugins/SfzPlayer/SfzPlayer.cpp | 10 ++++- plugins/SfzPlayer/SfzPlayerView.cpp | 2 + plugins/SfzPlayer/SfzRegion.cpp | 2 +- 7 files changed, 107 insertions(+), 8 deletions(-) diff --git a/plugins/SfzPlayer/SfzOpcodeState.cpp b/plugins/SfzPlayer/SfzOpcodeState.cpp index ef1bf22c24a..0d663e28b99 100644 --- a/plugins/SfzPlayer/SfzOpcodeState.cpp +++ b/plugins/SfzPlayer/SfzOpcodeState.cpp @@ -33,6 +33,19 @@ SfzOpcodeState::SfzOpcodeState() { // Certain arrays need to be initialized here in the constructor, since doing it in the header file is difficult m_hicc.fill(127); + + // There are some special exceptions with some opcodes which need to be handled separately + // For example, when modulation the `pan` with `pan_oncc10`, according to the SFZ website, the curve type should default to bipolar [-1 to 1], instead of [0 to 1] + // Actually nevermind, it seems like some SFZ's assume it is the default 0-1 curve? Whatever, it's fine + /*if (m_pan.value_curvecc.at(10) == std::nullopt) + { + m_pan.value_curvecc.at(10) = CurveType::Bipolar; + }*/ + // Also, apparantly `amplitude` modulation type defaults to multiplication, not addition + if (m_amplitude.modulationType == std::nullopt) + { + m_amplitude.modulationType = ModulatableOpcode::ModulationType::Mult; + } } diff --git a/plugins/SfzPlayer/SfzOpcodeState.h b/plugins/SfzPlayer/SfzOpcodeState.h index d85f853d7e2..501ec36838d 100644 --- a/plugins/SfzPlayer/SfzOpcodeState.h +++ b/plugins/SfzPlayer/SfzOpcodeState.h @@ -36,7 +36,7 @@ namespace lmms class SfzOpcodeState { public: - SfzOpcodeState(); // We need a constructor to initialize things like m_hicc + SfzOpcodeState(); // We need a constructor to initialize things like m_hicc, also to handle special opcode exceptions/defaults //! Given an opcode assignment such as `lokey=45`, passing "m_lokey" and "45" to this function //! will take those values and figure out if "lokey" is a valid opcode, and whether "45" is a valid value for it. diff --git a/plugins/SfzPlayer/SfzOpcodes.cpp b/plugins/SfzPlayer/SfzOpcodes.cpp index b9cb5e16bad..8c5b08142b9 100644 --- a/plugins/SfzPlayer/SfzOpcodes.cpp +++ b/plugins/SfzPlayer/SfzOpcodes.cpp @@ -97,6 +97,18 @@ void ModulatableOpcode::parseFromString(const QString& opcodeName, const QString value_oncc.at(ccNumberFromOpcode(opcodeName)) = opcodeValue.toFloat(successful); *parsed = true; } + else if (opcodeName.startsWith(alias + "_curvecc")) + { + value_curvecc.at(ccNumberFromOpcode(opcodeName)) = opcodeValue.toInt(successful); + *parsed = true; + } + else if (opcodeName.startsWith(alias + "_mod")) + { + *parsed = true; + if (opcodeValue == "add") { modulationType = ModulationType::Add; } + else if (opcodeValue == "mult") { modulationType = ModulationType::Mult; } + else { qDebug() << "[SFZ Parser] Warning: Unknown Modulation Type:" << opcodeValue; *successful = false; } + } } } @@ -106,11 +118,52 @@ void ModulatableOpcode::parseFromString(const QString& opcodeName, const QString // Instead, the current modulation value is only calculated and cached each time a midi CC event is sent, which saves a lot of compute. void ModulatableOpcode::updateCachedModulation(const std::array& ccValues) { - cachedModulation = 0.0f; + // Reset the modulation amount and recompute it + // Depending on the modulation type, the initial modulation should be 0 for addition (add nothing) or 1 for multiplication (multiply by identity). + switch (modulationType.value_or(ModulationType::Add)) + { + case ModulationType::Add: + cachedModulation = 0.0f; + break; + case ModulationType::Mult: + cachedModulation = 1.0f; + break; + } + // TODO this may be optimized if we only loop through the CC's which are actually used by the region for (int i = 0; i < NumMidiCCs; ++i) { - cachedModulation += value_oncc.at(i) * ccValues.at(i) / 128.0f; + if (value_oncc.at(i) == 0.0f) { continue; } // Skip unused CC's. This helps with the multiply modulation type, since multiplying by the default 0 values would not be great. + float scaledModulationValue = 0.0f; + switch (value_curvecc.at(i).value_or(0)) + { + case CurveType::Default: + scaledModulationValue = value_oncc.at(i) * (ccValues.at(i) / 127.0f); + break; + case CurveType::Bipolar: + scaledModulationValue = value_oncc.at(i) * (2.0f * ccValues.at(i) / 127.0f - 1.0f); + break; + case CurveType::Inverted: + scaledModulationValue = value_oncc.at(i) * (1.0f - ccValues.at(i) / 127.0f); + break; + case CurveType::BipolarInverted: + scaledModulationValue = value_oncc.at(i) * (1.0f - 2.0f * ccValues.at(i) / 127.0f); + break; + default: + // Unknown/custom curve type. These have not yet been implemented, so default to Default + scaledModulationValue = value_oncc.at(i) * ccValues.at(i) / 127.0f; + break; + } + // Depending on the modulation type, add or multiply the modulation value. + switch (modulationType.value_or(ModulationType::Add)) + { + case ModulationType::Add: + cachedModulation += scaledModulationValue; + break; + case ModulationType::Mult: + cachedModulation *= scaledModulationValue / 100; // Divide by 100, since most SFZ's seem to use percents + break; + } } } diff --git a/plugins/SfzPlayer/SfzOpcodes.h b/plugins/SfzPlayer/SfzOpcodes.h index a800cca8ad2..e9f1f1b5730 100644 --- a/plugins/SfzPlayer/SfzOpcodes.h +++ b/plugins/SfzPlayer/SfzOpcodes.h @@ -98,20 +98,45 @@ using StringOpcode = Opcode>; struct ModulatableOpcode : FloatOpcode { std::array value_oncc = {}; + //! Additionally, each modulation can be mapped with a curve, such as to go from 0 to 1, -1 to 1, something nonlinear, etc, so here's a helper enum for that + std::array, NumMidiCCs> value_curvecc = {}; // This uses std::optional because some opcode curves are meant to default to certain values, (pan_curvecc7 defaults to bipolar), so we need a way to tell if it was set or not. + //! Also, I never knew this, but apparantly some opcodes can have different modulation types set with opcode_mod=add or opcode_mod=mult, for whether to add the modulation or multiply by it https://sfzformat.com/opcodes/_mod/ + enum class ModulationType + { + Add, + Mult + }; + std::optional modulationType; // Once again using std::optional since some opcodes have different defaults, and we need to know if it was intentionally set or not so that we don't override it. //! Store the current total midi CC modulation amounts for the different targets, just so that we don't // have to recalculate them every buffer, instead only when a trigger occurs. float cachedModulation = 0.0f; ModulatableOpcode(QString name = "", float defaultValue = 0.0f) : FloatOpcode(name, defaultValue) {}; ModulatableOpcode(std::vector names = {}, float defaultValue = 0.0f) : FloatOpcode(names, defaultValue) {}; - //! Redefine the value() function to return the sum of the base opcode value and whatever the modulation is currently - const float value() const override { return m_value + cachedModulation; } + //! Redefine the value() function to return the sum of the base opcode value and whatever the modulation is currently. Or, if the modultion type if multiplication, multiply the two. + const float value() const override + { + return (modulationType.value_or(ModulationType::Add) == ModulationType::Add) + ? m_value + cachedModulation + : m_value * cachedModulation; + } //! Helper function for parsing these kinds of opcodes, where you have both `opcode` and `opcode_onccN` where N is the midi cc number, so // that the code isn't duplicated for every modulatable parameter. void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; //! Helper function to update the cached CC modulation amount every time a midi Control Change trigger occurs void updateCachedModulation(const std::array& ccValues); }; +// Helper class for curve types. This may have to be reworked when custom headers are added +enum CurveType +{ + Default = 0, // 0 to 1 + Bipolar = 1, // -1 to 1 + Inverted = 2, // 1 to 0 + BipolarInverted = 3, // 1 to -1 + //Concave = 4, // TODO not implemented yet + //XfinPowerCurve = 5, // TODO not implemented yet + //XfoutPowerCurve = 6, // TODO not implemented yet +}; // Things like amplitude, pitch, and filter freq envelopes and lfo's all have very similar parameters, so it makes sense to put // them all together in handy definitions diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 760b8de10c6..11cb7502528 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -156,9 +156,15 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) break; } } - if (!foundOpenPosition) { qDebug() << "[SFZ Player] Could not find vacant position in m_voices buffer!"; } // For fun, display the last played sample on the GUI - setStatusInfo("Last Played Sample: " + QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName()); + if (foundOpenPosition) + { + setStatusInfo("Last Played Sample: " + QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName()); + } + else + { + setStatusInfo("Warning: Too many active voices. Could not find vacant position buffer. "); + } } } diff --git a/plugins/SfzPlayer/SfzPlayerView.cpp b/plugins/SfzPlayer/SfzPlayerView.cpp index 632e652dfc7..c3aac09213e 100644 --- a/plugins/SfzPlayer/SfzPlayerView.cpp +++ b/plugins/SfzPlayer/SfzPlayerView.cpp @@ -78,6 +78,8 @@ SfzPlayerView::SfzPlayerView(SfzPlayer* instrument, QWidget* parent) connect(openFileButton, &PixmapButton::clicked, this, &SfzPlayerView::openFile); layout1->addWidget(openFileButton); + layout1->addWidget(new QLabel("This SFZ Player is in beta! Not all opcodes are supported yet.", this)); + layout1->addWidget(m_statusLabel); layout1->addWidget(m_infoLabelsWidget); diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index 65ebc26ebd4..54c23a2bc53 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -103,7 +103,7 @@ void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& tr // Before spawning a sound, do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer if (trigger.type() == SfzTrigger::Type::ControlChange) { - recalculateTotalCCModulation(globalState); + recalculateTotalCCModulation(globalState); // TODO this can be optimized, perhaps by storing the modulations only per-opcode, rather than per-region-per-opcide } } From 5e6181178b185722199fdc469f98f3d8023bc41b Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 9 May 2026 15:29:57 -0400 Subject: [PATCH 105/131] Fix issue with samples starting partway through --- plugins/SfzPlayer/SfzRegionPlayState.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index c0324b46edb..954f7a9b330 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -296,10 +296,13 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) buffer[f][1] += sampleRightValue * m_baseAmplitudeRight * ampeg * amplfo; } // Increment the frame count. If we are using a sample, make sure to stop at the end, but if we are using a basic wave like *sine or *saw, there's no need - const float frameIncrement = m_baseFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback - m_sampleFrame = m_sampleObject != nullptr - ? std::min(static_cast(m_sampleObject->size()), m_sampleFrame + frameIncrement) - : m_sampleFrame + frameIncrement; + if (m_frameCount >= 0) // Do not start playing the sample until we reach frame 0 (start of note), otherwise the sample frame will start moving even though it's not outputting any audio, causing the note to start partway through the sample (often with a discontinuity) + { + const float frameIncrement = m_baseFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback + m_sampleFrame = m_sampleObject != nullptr + ? std::min(static_cast(m_sampleObject->size()), m_sampleFrame + frameIncrement) + : m_sampleFrame + frameIncrement; + } m_frameCount++; } From 44f4600a0560a097a484816dcf5d2d82f0c73b9f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 9 May 2026 18:31:37 -0400 Subject: [PATCH 106/131] Fix voices staying active way too long when sustain is active --- plugins/SfzPlayer/SfzRegionPlayState.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index 954f7a9b330..e879f773472 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -29,6 +29,7 @@ #include "AudioEngine.h" #include "Engine.h" #include "lmms_math.h" +#include "MicroTimer.h" #include @@ -208,6 +209,14 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // If the initial m_frameCount is negative, that means the note hasn't started yet if (m_frameCount < -static_cast(frames)) { m_frameCount += frames; return false; } // If the note doesn't start in this buffer, don't play anything + /* + static int totalMicroseconds = 0; + static int totalSquaredMicroseconds = 0; + static int totalCalls = 0; + static int minElapsed = 10000000; + static int maxElapsed = 0; + MicroTimer profiler;*/ + // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; @@ -313,12 +322,16 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) m_active = false; } - // If loop_mode is one_shot, no noteOff signal will ever come to release it, so we need to manually deactivate when we reach the end of the sample - if (m_region->m_loop_mode.value() == LoopMode::OneShot && m_sampleObject != nullptr && m_sampleFrame >= m_sampleObject->size()) + // If the end of the sample is reached and the region's loop mode does not loop, deactivate this voice. + if ((m_region->m_loop_mode.value() == LoopMode::OneShot || m_region->m_loop_mode.value() == LoopMode::NoLoop) && m_sampleObject != nullptr && m_sampleFrame >= m_sampleObject->size()) { m_active = false; // TODO should this forcefully decative or just release? } + //int elapsed = profiler.elapsed(); totalMicroseconds += elapsed; totalSquaredMicroseconds += elapsed * elapsed; totalCalls++; minElapsed = std::min(minElapsed, elapsed); maxElapsed = std::max(maxElapsed, elapsed); + //float mean = 1.0f * totalMicroseconds / totalCalls, variance = (1.0f * totalSquaredMicroseconds - 1.0f * totalMicroseconds * totalMicroseconds / totalCalls / totalCalls) / totalCalls; + //qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); + return true; } From 98dfae451d4b3eabeecf092500f8d18e93fca532 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 10 May 2026 13:47:45 -0400 Subject: [PATCH 107/131] Store sample objects (mostly) as raw pointers for speed, and add extra check for pitch depth --- plugins/SfzPlayer/SfzRegion.h | 4 ++-- plugins/SfzPlayer/SfzRegionPlayState.cpp | 7 +++++-- plugins/SfzPlayer/SfzRegionPlayState.h | 4 ++-- plugins/SfzPlayer/SfzSampleBuffer.cpp | 17 +++++++++++------ plugins/SfzPlayer/SfzSampleBuffer.h | 7 +++---- plugins/SfzPlayer/SfzSamplePool.cpp | 8 ++++---- plugins/SfzPlayer/SfzSamplePool.h | 4 ++-- 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/plugins/SfzPlayer/SfzRegion.h b/plugins/SfzPlayer/SfzRegion.h index 9516aa6cc92..bce07d6c424 100644 --- a/plugins/SfzPlayer/SfzRegion.h +++ b/plugins/SfzPlayer/SfzRegion.h @@ -59,7 +59,7 @@ class SfzRegion : public SfzOpcodeState bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool); //! Returns a shared pointer to the sample object for this region - std::shared_ptr sample() const { return m_sample; } + const SfzSampleBuffer* sample() const { return m_sample; } //! Returns the type of basic wave for this region, if it specified instead of a real sample. //! If a sample was specified, this returns SfzBasicWaves::Shape::Silence by default. const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } @@ -67,7 +67,7 @@ class SfzRegion : public SfzOpcodeState private: //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. - std::shared_ptr m_sample = nullptr; + const SfzSampleBuffer* m_sample = nullptr; //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index e879f773472..79b088eef58 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -215,7 +215,8 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) static int totalCalls = 0; static int minElapsed = 10000000; static int maxElapsed = 0; - MicroTimer profiler;*/ + MicroTimer profiler; + */ // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; @@ -271,7 +272,9 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) const float amplfo = amplfoDepth != 0.0f // Only compute the amplitude lfo if the depth is nonzero ? dbfsToAmp(lfoGenerator(amplfoDelayFrames, amplfoFadeFrames, amplfoFreq) * amplfoDepth) // amplfo depth is in decibels, so convert to amplitude : 1.0f; - const float pitcheg = envelopeGenerator(pitchegDelayFrames, pitchegAttackFrames, pitchegHoldFrames, pitchegDecayFrames, pitchegSustain, pitchegReleaseFrames) * pitchegDepth; + const float pitcheg = pitchegDepth != 0.0f // Only compute the pitch envelope if the depth is nonzero + ? envelopeGenerator(pitchegDelayFrames, pitchegAttackFrames, pitchegHoldFrames, pitchegDecayFrames, pitchegSustain, pitchegReleaseFrames) * pitchegDepth + : 0.0f; const float pitchlfo = pitchlfoDepth != 0.0f // Only compute the pitch lfo if the depth is nonzero ? lfoGenerator(pitchlfoDelayFrames, pitchlfoFadeFrames, pitchlfoFreq) * pitchlfoDepth : 0.0f; diff --git a/plugins/SfzPlayer/SfzRegionPlayState.h b/plugins/SfzPlayer/SfzRegionPlayState.h index 4797dead29d..528b16b14fe 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.h +++ b/plugins/SfzPlayer/SfzRegionPlayState.h @@ -104,8 +104,8 @@ class SfzRegionPlayState //! The region this sound originated from const SfzRegion* m_region = nullptr; - //! A shared pointer to the sample object of the parent region - std::shared_ptr m_sampleObject = nullptr; + //! The sample object of the parent region + const SfzSampleBuffer* m_sampleObject = nullptr; }; diff --git a/plugins/SfzPlayer/SfzSampleBuffer.cpp b/plugins/SfzPlayer/SfzSampleBuffer.cpp index 6f8c56ae636..20abc43e8a6 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.cpp +++ b/plugins/SfzPlayer/SfzSampleBuffer.cpp @@ -30,7 +30,7 @@ namespace lmms SfzSampleBuffer::SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate) - : m_data(std::shared_ptr{new float[size][NUM_CHANNELS]}) + : m_data(new float[size * NUM_CHANNELS]) , m_size(size) , m_sampleRate(sampleRate) { @@ -38,11 +38,16 @@ SfzSampleBuffer::SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, co { for (size_t channel = 0; channel < NUM_CHANNELS; ++channel) { - m_data[f][channel] = data[f][channel]; + m_data[f * NUM_CHANNELS + channel] = data[f][channel]; } } } +SfzSampleBuffer::~SfzSampleBuffer() +{ + delete[] m_data; +} + float SfzSampleBuffer::at(const float index, const size_t channel) const { @@ -57,10 +62,10 @@ float SfzSampleBuffer::at(const float index, const size_t channel) const f_cnt_t i2 = std::min(indexFloor + 1, m_size - 1); f_cnt_t i3 = std::min(indexFloor + 2, m_size - 1); - float v0 = m_data[i0][channel]; - float v1 = m_data[i1][channel]; - float v2 = m_data[i2][channel]; - float v3 = m_data[i3][channel]; + float v0 = m_data[NUM_CHANNELS * i0 + channel]; + float v1 = m_data[NUM_CHANNELS * i1 + channel]; + float v2 = m_data[NUM_CHANNELS * i2 + channel]; + float v3 = m_data[NUM_CHANNELS * i3 + channel]; return hermiteInterpolate(v0, v1, v2, v3, frac); } diff --git a/plugins/SfzPlayer/SfzSampleBuffer.h b/plugins/SfzPlayer/SfzSampleBuffer.h index 05e3ca5dda2..c75d3a2e87b 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.h +++ b/plugins/SfzPlayer/SfzSampleBuffer.h @@ -38,6 +38,7 @@ class SfzSampleBuffer public: SfzSampleBuffer() = default; SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate); + ~SfzSampleBuffer(); //! Returns a hermite-interpolated value for the data at the given sample index and channel. //! The interpolation is so that pitch shifting and resampling is as easy as possible @@ -52,10 +53,8 @@ class SfzSampleBuffer private: static constexpr const size_t NUM_CHANNELS = 2; - //! The raw sample data is stored as a shared pointer to a 2d array of floats. - //! This was originally done so that it would not be copied unnecessarily if, for example, the vector or map containing the SfzSampleBuffer were resized/reallocated - //! TODO: is this optimal? - std::shared_ptr m_data; + //! The raw sample data is stored as a raw pointer to an interleaved array of floats in (number_of_samples)x(channels) format. + float* m_data = nullptr; f_cnt_t m_size; float m_sampleRate; diff --git a/plugins/SfzPlayer/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp index 18744d1cd49..6868a948208 100644 --- a/plugins/SfzPlayer/SfzSamplePool.cpp +++ b/plugins/SfzPlayer/SfzSamplePool.cpp @@ -29,17 +29,17 @@ namespace lmms { -std::shared_ptr SfzSamplePool::loadSample(const QString& path) +const SfzSampleBuffer* SfzSamplePool::loadSample(const QString& path) { // If the sample has already been loaded before, just return a pointer to it if (m_samplePool.contains(path)) { - return m_samplePool.at(path); + return m_samplePool.at(path).get(); } else if (auto buffer = SampleBuffer::fromFile(path)) { - m_samplePool.insert({path, std::make_shared(buffer->data(), buffer->size(), buffer->sampleRate())}); - return m_samplePool.at(path); + m_samplePool.insert({path, std::make_unique(buffer->data(), buffer->size(), buffer->sampleRate())}); + return m_samplePool.at(path).get(); } else { diff --git a/plugins/SfzPlayer/SfzSamplePool.h b/plugins/SfzPlayer/SfzSamplePool.h index 41d1d771bb9..b3e658f742c 100644 --- a/plugins/SfzPlayer/SfzSamplePool.h +++ b/plugins/SfzPlayer/SfzSamplePool.h @@ -36,13 +36,13 @@ namespace lmms class SfzSamplePool { public: - std::shared_ptr loadSample(const QString& path); + const SfzSampleBuffer* loadSample(const QString& path); //! Returns the number of samples currently loaded in the pool const int sampleCount() const { return m_samplePool.size(); } private: - std::map> m_samplePool; + std::map> m_samplePool; }; } // namespace lmms From 190f417bed4b35698ea390d3dc4dbec78bf87a94 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 10 May 2026 14:30:04 -0400 Subject: [PATCH 108/131] Remember to delete region/sample pools --- plugins/SfzPlayer/SfzPlayer.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 11cb7502528..8c72c9b0427 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -82,6 +82,11 @@ SfzPlayer::~SfzPlayer() { // Make sure to end the sample loading thread before the plugin exits, or else the program might forcefully terminate if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } + + if (m_regionManager != nullptr) { delete m_regionManager; } // these may be nullptr at first when no SFZ file has been loaded previously + if (m_samplePool != nullptr) { delete m_samplePool; } + if (m_tempRegionManager != nullptr) { delete m_tempRegionManager; } + if (m_tempSamplePool != nullptr) { delete m_tempSamplePool; } } @@ -324,8 +329,8 @@ void SfzPlayer::mainThreadUpdateAfterDataSwap() { m_justSwappedData = false; // Now that the audio thread has swapped the data, delete the old sample pool and region manager pointers - if (m_tempRegionManager != nullptr) { delete m_tempRegionManager; } // these may be nullptr at first when no SFZ file has been loaded previously - if (m_tempSamplePool != nullptr) { delete m_tempSamplePool; } + if (m_tempRegionManager != nullptr) { delete m_tempRegionManager; m_tempRegionManager = nullptr; } // these may be nullptr at first when no SFZ file has been loaded previously + if (m_tempSamplePool != nullptr) { delete m_tempSamplePool; m_tempSamplePool = nullptr; } // Also set the midi CC knobs to match the defaults, or whatever the current InstrumentTrack midi CC knobs are for (int i = 0; i < NumMidiCCs; ++i) { From e72ae0c5c93dc9fd9d8642c9f1eaf8ccb270e848 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 12 May 2026 14:47:39 -0400 Subject: [PATCH 109/131] Fix key parsing --- plugins/SfzPlayer/SfzOpcodes.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/SfzPlayer/SfzOpcodes.cpp b/plugins/SfzPlayer/SfzOpcodes.cpp index 8c5b08142b9..b094e2536ad 100644 --- a/plugins/SfzPlayer/SfzOpcodes.cpp +++ b/plugins/SfzPlayer/SfzOpcodes.cpp @@ -26,6 +26,7 @@ #include #include +#include namespace lmms { @@ -298,11 +299,17 @@ int ccNumberFromOpcode(const QString& opcode) int stringToKeyNum(QString keyString, bool* successful) { keyString = keyString.toLower(); - // The last character is the octave number - int octave = QString(keyString.back()).toInt(successful); - if (!*successful) {qDebug() << "[SFZ Parser] Unable to parse key string, Invalid octave number:" << keyString; return -1; } - // The remaining characters at the start define the key - QString key = keyString.chopped(1); + // Match for a number at the end (potentially including a "-" at the start) + QRegularExpression octaveRE("(-)?\\d+$"); + QRegularExpressionMatch octaveMatch = octaveRE.match(keyString); + if (!octaveMatch.hasMatch()) { qDebug() << "[SFZ Parser] Unable to parse octave from key string:" << keyString; return -1; } + int octave = octaveMatch.captured(0).toInt(successful); + // Match for the letters at the start (including #) + QRegularExpression keyRE("^[a-zA-Z#]+"); + QRegularExpressionMatch keyMatch = keyRE.match(keyString); + if (!keyMatch.hasMatch()) { qDebug() << "[SFZ Parser] Unable to parse key from key string:" << keyString; return -1; } + QString key = keyMatch.captured(0); + int keyOffset = 0; if (key == "c") { keyOffset = 0; } // C is 0 since that's where the octaves change @@ -331,7 +338,7 @@ int stringToKeyNum(QString keyString, bool* successful) QString keyNumToString(int keyNum) { - QString octave = QString::number((keyNum - 24) / 12 + 1); + QString octave = QString::number(std::floor(static_cast(keyNum - 24) / 12) + 1); QString key = ""; switch (keyNum % 12) { From 41d2a16986dd4f514033494b168fa8e9cc21a885 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 12 May 2026 15:05:31 -0400 Subject: [PATCH 110/131] Rework keyswitches and other loops for performance --- plugins/SfzPlayer/SfzGlobalState.cpp | 56 +++++++++++++----------- plugins/SfzPlayer/SfzGlobalState.h | 40 +++++++++-------- plugins/SfzPlayer/SfzPlayer.cpp | 16 +++---- plugins/SfzPlayer/SfzPlayer.h | 2 +- plugins/SfzPlayer/SfzRegion.cpp | 17 +------ plugins/SfzPlayer/SfzRegion.h | 3 -- plugins/SfzPlayer/SfzRegionPlayState.cpp | 7 ++- plugins/SfzPlayer/SfzRegionPlayState.h | 9 ++-- 8 files changed, 70 insertions(+), 80 deletions(-) diff --git a/plugins/SfzPlayer/SfzGlobalState.cpp b/plugins/SfzPlayer/SfzGlobalState.cpp index 25143d21341..604ccf52b75 100644 --- a/plugins/SfzPlayer/SfzGlobalState.cpp +++ b/plugins/SfzPlayer/SfzGlobalState.cpp @@ -38,44 +38,41 @@ void SfzGlobalState::processTrigger(const SfzTrigger& trigger) } else if (trigger.type() == SfzTrigger::Type::NoteOn) { - m_activeKeys.at(trigger.key().value()) = true; - m_keyPressCounter++; - m_lastPlayedKeys.at(trigger.key().value()) = m_keyPressCounter; + if (m_lastPlayedSwitchKeys.contains(trigger.key().value())) + { + m_switchKeyPressCounter++; + m_lastPlayedSwitchKeys.at(trigger.key().value()) = m_switchKeyPressCounter; + m_pressedSwitchKeys.at(trigger.key().value()) = true; + m_lastPlayedSwitchKey = trigger.key().value(); + } } else if (trigger.type() == SfzTrigger::Type::NoteOff) { - m_activeKeys.at(trigger.key().value()) = false; + if (m_lastPlayedSwitchKeys.contains(trigger.key().value())) + { + m_pressedSwitchKeys.at(trigger.key().value()) = false; + } } // Update the current random value m_rand = fastRand(1.0f); } -void SfzGlobalState::switchKeyPressed(const int key) +std::optional SfzGlobalState::lastSwitchKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey) const { - // Assuming the m_keyPressCounter was already updated in processTrigger - m_lastPlayedSwitchKeys.at(key) = m_keyPressCounter; -} - -std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey, bool switchKeysOnly) const -{ - auto& lastKeyPressArray = switchKeysOnly ? m_lastPlayedSwitchKeys : m_lastPlayedKeys; - // Some SFZs pass -1 as the lokey, so make sure to clamp it into the range 0-127 before accessing the array to prevent out of range errors - lowKey = std::max(lowKey, 0); - highKey = std::min(highKey, 127); - // If the range is invalid, return the default key. - if (lowKey > highKey) { return defaultKey; } - + // The range might only include some of the switch keys + // If the last played switch key happens to fall in the range, that's awesome! + if (m_lastPlayedSwitchKey >= lowKey && m_lastPlayedSwitchKey <= highKey) { return m_lastPlayedSwitchKey; } + // Otherwise, we need to loop over the list of when each switch key was played and figure out which one was last played std::optional lastPlayedKey = std::nullopt; std::optional bestScore = std::nullopt; - - for (int key = lowKey; key <= highKey; ++key) + for (const auto& [key, counter] : m_lastPlayedSwitchKeys) { - if (lastKeyPressArray.at(key) == std::nullopt) { continue; } // This key has not been played yet - - if (bestScore == std::nullopt || lastKeyPressArray.at(key) > bestScore) + if (counter == std::nullopt) { continue; } // This key has not been played yet + + if (bestScore == std::nullopt || counter > bestScore) { lastPlayedKey = key; - bestScore = lastKeyPressArray.at(key); + bestScore = counter; } } // If no keys in the region have been played yet, return the default key @@ -84,7 +81,7 @@ std::optional SfzGlobalState::lastKeyPressedInRange(int lowKey, int highKey } -void SfzGlobalState::initializeMidiCCValues(const SfzOpcodeState& controlsConfig) +void SfzGlobalState::initializeMidiCCValues(const SfzControlsConfig& controlsConfig) { for (int i = 0; i < NumMidiCCs; ++i) { @@ -92,5 +89,14 @@ void SfzGlobalState::initializeMidiCCValues(const SfzOpcodeState& controlsConfig } } +void SfzGlobalState::initializeSwitchKeysMap(const SfzControlsConfig& controlsConfig) +{ + for (const auto& [key, info] : controlsConfig.m_switchKeyInfo) + { + m_lastPlayedSwitchKeys[key] = std::nullopt; + m_pressedSwitchKeys[key] = false; + } +} + } // namespace lmms diff --git a/plugins/SfzPlayer/SfzGlobalState.h b/plugins/SfzPlayer/SfzGlobalState.h index f5231b65d90..a0386ffa6e1 100644 --- a/plugins/SfzPlayer/SfzGlobalState.h +++ b/plugins/SfzPlayer/SfzGlobalState.h @@ -27,9 +27,10 @@ #define LMMS_SFZ_GLOBAL_STATE_H #include +#include #include -#include "SfzOpcodeState.h" +#include "SfzControlsConfig.h" #include "SfzTrigger.h" namespace lmms @@ -39,20 +40,16 @@ namespace lmms class SfzGlobalState { public: - //! Handles updating the last played keys and keeps track of which keys are currently being pressed + //! Handles updating the last played switch keys and keeps track of which keys are currently being pressed void processTrigger(const SfzTrigger& trigger); - //! Handles updating m_lastPlayedSwitchKeys whenever a key defined by sw_last is pressed - // TODO move this inside of processTrigger so that fewer regions need to be looped over - void switchKeyPressed(const int key); - - //! Returns the midi key number of the last pressed key in the range [lowKey, highKey]. - //! If switchKeysOnly is true, it uses the m_lastPlayedSwitchKeys array which only tracks when valid switch keys have been pressed, not just any key. + //! Returns the midi key number of the last pressed switch key in the range [lowKey, highKey]. + //! It uses m_lastPlayedSwitchKeys array which only tracks when valid switch keys have been pressed, not just any key. //! If no keys have been pressed yet, it returns defaultKey. //! If the key range is invalid, it returns defaultKey //! If no default key was given, it returns std::nullopt // TODO add unit tests - std::optional lastKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey, bool switchKeysOnly = false) const; + std::optional lastSwitchKeyPressedInRange(int lowKey, int highKey, const std::optional defaultKey) const; //! Returns the current value of the given midi CC knob/controller, or the default value if we haven't recieved any midi CC signals for it yet. int midiCCValue(const int index) const { return m_ccValues.at(index); } @@ -63,20 +60,25 @@ class SfzGlobalState const float rand() const { return m_rand; } //! Sets initial values for the controls, based on any `set_ccN` opcodes in the header - void initializeMidiCCValues(const SfzOpcodeState& controlsConfig); + void initializeMidiCCValues(const SfzControlsConfig& controlsConfig); + + //! Creates a std::map with an element for each switch key defined in the sfz file, indexed by key, with the value being the key press counter of the last time that key was pressed (which is initially 0, or std::nullopt) + void initializeSwitchKeysMap(const SfzControlsConfig& controlsConfig); private: - //! Stores a number for every key, tracking which order the keys were played in + //! Stores a number for every switch key, tracking which order the keys were played in //! The first key played gets a 1, the second key gets a 2, etc, overwriting when keys are played multiple times. //! By finding the maximum number in a range, you can find the last played key - std::array, 128> m_lastPlayedKeys = {}; - //! Same as above but only for switch keys (ones defined by sw_last) - std::array, 128> m_lastPlayedSwitchKeys = {}; - //! Consequently, we also have to track how many keys have been played so far - unsigned long m_keyPressCounter = 0; - - //! Keeps track of which keys on the piano are currently being pressed - std::array m_activeKeys = {}; + //! This uses a std::map instead of a 128-element array for speed, since most keys are not switch keys. + //! The index is the key number, and the value is the key press counter when it was last pressed, or std::nullopt if it has never been pressed. + std::map> m_lastPlayedSwitchKeys = {}; + //! Stores the midi key index of the last played switch key, for easy access. + std::optional m_lastPlayedSwitchKey = std::nullopt; + //! Consequently, we also have to track how many switch keys have been played so far + unsigned long m_switchKeyPressCounter = 0; + + //! Keeps track of which switch keys on the piano are currently being pressed + std::map m_pressedSwitchKeys = {}; // TODO this is not currently used, but may be once `sw_down` opcode is implemented. //! Keeps track of the current random value, updated every trigger. Some regions use lorand/hirand opcodes to decide randomy whether to activate or not, and this is the random value they will compare to. float m_rand = 0.0f; diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 8c72c9b0427..0b10142b2a4 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -131,16 +131,8 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) { if (m_regionManager == nullptr) { return; } // Before any SFZ file is loaded, m_regionManager is nullptr, so there's no regions to trigger. - // Notify the global state to update which keys are active, update midi CC values, etc + // Notify the global state to update which switch keys are active, update midi CC values, etc m_sfzGlobalState.processTrigger(trigger); - - // Loop through all the regions to check if a new note should be played - // TODO can we get rid of this loop - for (auto* region : m_regionManager->allRegions()) - { - // Notify the region of the event so that it can update cached CC modulations, keyswitch states, etc - region->processTrigger(m_sfzGlobalState, trigger); - } for (auto* region : m_regionManager->findPotentialMatchingRegions(trigger)) { @@ -154,7 +146,7 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) auto& regionPlayState = m_voices[i]; if (!regionPlayState.active()) { - regionPlayState = SfzRegionPlayState(region, trigger); + regionPlayState = SfzRegionPlayState(region, trigger, m_sfzGlobalState); // If this new index is above the current max active index, update it m_maxActiveIndex = std::max(m_maxActiveIndex, i); foundOpenPosition = true; @@ -180,7 +172,7 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) if (regionPlayState.active()) { - regionPlayState.processTrigger(trigger); + regionPlayState.processTrigger(trigger, m_sfzGlobalState); // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } } @@ -269,6 +261,8 @@ void SfzPlayer::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // Set the initial cc values based on any `set_ccN` opcodes in the header m_sfzGlobalState.initializeMidiCCValues(m_controlsConfig); + // Setup a map for keeping track of which switch keys may be pressed. This is more efficient than having array with 128 element, since that takes much longer to loop over, and most keys are not switch keys + m_sfzGlobalState.initializeSwitchKeysMap(m_controlsConfig); m_sfzFilePath = filePath; diff --git a/plugins/SfzPlayer/SfzPlayer.h b/plugins/SfzPlayer/SfzPlayer.h index 5b0bdcf54f9..0a5b0e30f11 100644 --- a/plugins/SfzPlayer/SfzPlayer.h +++ b/plugins/SfzPlayer/SfzPlayer.h @@ -88,7 +88,7 @@ class SfzPlayer : public Instrument void recalculateMaxActiveIndex(); //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded - SfzSamplePool* m_samplePool = nullptr; + SfzSamplePool* m_samplePool = nullptr; // TODO use unique pointer or something //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index 54c23a2bc53..99034724fa1 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -67,9 +67,8 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf if (triggerVelocity > m_hivel.value() || triggerVelocity < m_lovel.value()) { return false; } // If a keyswitch range was defined, ensure the last pressed valid switch key in that range matches the specified keyswitch for this region - // The argument `true` at the end signifies that only switch keys will be considered // TODO add unit tests - if (m_sw_last.value() != std::nullopt && globalState.lastKeyPressedInRange(m_sw_lokey.value(), m_sw_hikey.value(), m_sw_default.value(), true) != m_sw_last.value()) { return false; } + if (m_sw_last.value() != std::nullopt && globalState.lastSwitchKeyPressedInRange(m_sw_lokey.value(), m_sw_hikey.value(), m_sw_default.value()) != m_sw_last.value()) { return false; } } // If the region uses lorand/hirand, the current random value stored in the global state (updated every trigger) is compared with the range @@ -92,20 +91,6 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf return true; } -void SfzRegion::processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger) -{ - // Notify the global state whether a switch key has been pressed, so that it can correctly track when `sw_last` is met - if (trigger.type() == SfzTrigger::Type::NoteOn && m_sw_last.value() != std::nullopt && m_sw_last.value() == trigger.key().value()) - { - globalState.switchKeyPressed(trigger.key().value()); // TODO this can probably be moved somewhere else so that it isn't done for every region (if you have 10000+ regions, it needs to be optimized) - } - - // Before spawning a sound, do some pre-calculation of the midi CC modulation amounts so that we don't have to do it every buffer - if (trigger.type() == SfzTrigger::Type::ControlChange) - { - recalculateTotalCCModulation(globalState); // TODO this can be optimized, perhaps by storing the modulations only per-opcode, rather than per-region-per-opcide - } -} void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) { diff --git a/plugins/SfzPlayer/SfzRegion.h b/plugins/SfzPlayer/SfzRegion.h index bce07d6c424..db7ce81e7d5 100644 --- a/plugins/SfzPlayer/SfzRegion.h +++ b/plugins/SfzPlayer/SfzRegion.h @@ -50,9 +50,6 @@ class SfzRegion : public SfzOpcodeState //! TODO this method also increments the round robin counter, if applicable. Ideally I feel like this method should be const, but I'm not sure how to best organize it. bool triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger); - //! Updates cached CC modulations, and helps update keyswitch states - void processTrigger(SfzGlobalState& globalState, const SfzTrigger& trigger); - //! Load the sample file given by the `sample` opcode into m_sample. //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed //! Returns true if successful diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index 79b088eef58..e4ebe58efb0 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -36,7 +36,7 @@ namespace lmms { -SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger) +SfzRegionPlayState::SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trigger, const SfzGlobalState& globalState) : m_active(true) , m_lmmsSampleRate(Engine::audioEngine()->outputSampleRate()) , m_filter(m_lmmsSampleRate) @@ -44,6 +44,8 @@ SfzRegionPlayState::SfzRegionPlayState(const SfzRegion* region, const SfzTrigger , m_region(region) , m_sampleObject(region->sample()) { + // Make sure the parent region's modulations are up to date + m_region->recalculateTotalCCModulation(globalState); // Calculate the base pitch and amplitude so that we don't need to every buffer precomputeBaseValues(); // Delay the start of the playback by the trigger offset @@ -339,7 +341,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) } -void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) +void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger, SfzGlobalState& globalState) { if (m_released) { return; } // If we already released, don't do anything @@ -357,6 +359,7 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) // For simplicity, if any CC trigger occurs, recompute everything if (trigger.type() == SfzTrigger::Type::ControlChange) { + m_region->recalculateTotalCCModulation(globalState); precomputeBaseValues(); } } diff --git a/plugins/SfzPlayer/SfzRegionPlayState.h b/plugins/SfzPlayer/SfzRegionPlayState.h index 528b16b14fe..e6e8260feb8 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.h +++ b/plugins/SfzPlayer/SfzRegionPlayState.h @@ -28,6 +28,7 @@ #include "SampleFrame.h" #include "SfzTrigger.h" +#include "SfzGlobalState.h" #include "SfzOpcodeState.h" #include "SfzSampleBuffer.h" @@ -44,7 +45,8 @@ class SfzRegion; class SfzRegionPlayState { public: - SfzRegionPlayState(const SfzRegion* region, const SfzTrigger& trigger); + // I wish `region` could be const, but it's faster to have the voice update the region's cached modulation values than looping over all the regions elsewhere in the plugin, so for now it will have to be non-const. + SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trigger, const SfzGlobalState& globalState); SfzRegionPlayState() = default; // The default constructor is needed to initialize arrays of the object //! Helper function to calculate the base pitch and amplitude so that it doesn't have to be done per-buffer. @@ -57,7 +59,8 @@ class SfzRegionPlayState bool play(SampleFrame* buffer, const f_cnt_t frames); //! Handle incoming event to decide whether to deactivate/release - void processTrigger(const SfzTrigger& trigger); + //! Also handles updating the parent region's precomputed modulation values whenever a midi CC event occurs + void processTrigger(const SfzTrigger& trigger, SfzGlobalState& globalState); //! Returns whether this voice is done playing or not. If m_active is false, this voice object is avaiable to be overwritten and used as a new voice. bool active() const { return m_active; } @@ -102,7 +105,7 @@ class SfzRegionPlayState SfzTrigger m_trigger; //! The region this sound originated from - const SfzRegion* m_region = nullptr; + SfzRegion* m_region = nullptr; //! The sample object of the parent region const SfzSampleBuffer* m_sampleObject = nullptr; From 4691980f1b788938a4a1c1f3594b7f7d0c64da4e Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 12 May 2026 15:57:45 -0400 Subject: [PATCH 111/131] Fix knobs not loading --- plugins/SfzPlayer/SfzPlayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 0b10142b2a4..d9a04bb1257 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -352,7 +352,7 @@ void SfzPlayer::loadSettings(const QDomElement& element) m_sfzFilePath = element.attribute("sfzfile"); if (!m_sfzFilePath.isEmpty()) { - loadSfzFile(m_sfzFilePath, true); // Passing false to leave the user-set midi CC knobs alone, since they were loaded by the InstrumentTrack and shouldn't be reset to the SFZ's defaults. + loadSfzFile(m_sfzFilePath, false); // Passing false to leave the user-set midi CC knobs alone, since they were loaded by the InstrumentTrack and shouldn't be reset to the SFZ's defaults. } // TODO add error handling, if path doesn't exist } From ecbea4b4965e1eedfbc2976dc0d59b679d099bdc Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 15 May 2026 14:35:07 -0400 Subject: [PATCH 112/131] Overload operators for Opcodes to make them easier to use --- plugins/SfzPlayer/SfzOpcodeState.h | 7 +- plugins/SfzPlayer/SfzOpcodes.cpp | 19 ------ plugins/SfzPlayer/SfzOpcodes.h | 36 +++++------ plugins/SfzPlayer/SfzParser.cpp | 12 ++-- plugins/SfzPlayer/SfzPlayer.cpp | 4 +- plugins/SfzPlayer/SfzRegion.cpp | 34 +++++----- plugins/SfzPlayer/SfzRegionManager.cpp | 4 +- plugins/SfzPlayer/SfzRegionPlayState.cpp | 82 ++++++++++++------------ 8 files changed, 89 insertions(+), 109 deletions(-) diff --git a/plugins/SfzPlayer/SfzOpcodeState.h b/plugins/SfzPlayer/SfzOpcodeState.h index 501ec36838d..7a08d9829b4 100644 --- a/plugins/SfzPlayer/SfzOpcodeState.h +++ b/plugins/SfzPlayer/SfzOpcodeState.h @@ -27,7 +27,6 @@ #include "SfzOpcodes.h" #include -#include #include namespace lmms @@ -73,8 +72,8 @@ class SfzOpcodeState // KeyOpcode m_sw_lokey {"sw_lokey", 0}; KeyOpcode m_sw_hikey {"sw_hikey", 127}; - OptionalKeyOpcode m_sw_last {"sw_last", std::nullopt}; // The SFZ opcode spec says this should default to 0, but I don't think that's right? - OptionalKeyOpcode m_sw_default {"sw_default", std::nullopt}; + KeyOpcode m_sw_last {"sw_last", std::nullopt}; // The SFZ opcode spec says this should default to 0, but I don't think that's right? + KeyOpcode m_sw_default {"sw_default", std::nullopt}; StringOpcode m_sw_label {"sw_label", std::nullopt}; // @@ -119,7 +118,7 @@ class SfzOpcodeState // Filter // Opcode m_fil_type {"fil_type", FilterType::Lowpass2Pole}; - OptionalFloatOpcode m_cutoff {"cutoff", std::nullopt}; + FloatOpcode m_cutoff {"cutoff", std::nullopt}; FloatOpcode m_resonance {"resonance", 0.0f}; Opcode m_fil_veltrack {"fil_veltrack", 0}; // in cents diff --git a/plugins/SfzPlayer/SfzOpcodes.cpp b/plugins/SfzPlayer/SfzOpcodes.cpp index b094e2536ad..ef6587636e0 100644 --- a/plugins/SfzPlayer/SfzOpcodes.cpp +++ b/plugins/SfzPlayer/SfzOpcodes.cpp @@ -40,15 +40,6 @@ void FloatOpcode::parseFromString(const QString& opcodeName, const QString& opco *parsed = true; } -// Same function but for optional floats -template<> -void OptionalFloatOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) -{ - if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } - m_value = opcodeValue.toFloat(successful); - *parsed = true; -} - template<> void KeyOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { @@ -59,16 +50,6 @@ void KeyOpcode::parseFromString(const QString& opcodeName, const QString& opcode *parsed = true; } -// Same function but for optional keys -template<> -void OptionalKeyOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) -{ - if (std::find(m_opcodeNames.begin(), m_opcodeNames.end(), opcodeName) == m_opcodeNames.end()) { return; } - m_value = opcodeValue.toInt(successful); - if (!*successful) { m_value = stringToKeyNum(opcodeValue, successful); } - *parsed = true; -} - template<> void StringOpcode::parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) { diff --git a/plugins/SfzPlayer/SfzOpcodes.h b/plugins/SfzPlayer/SfzOpcodes.h index e9f1f1b5730..3177d146027 100644 --- a/plugins/SfzPlayer/SfzOpcodes.h +++ b/plugins/SfzPlayer/SfzOpcodes.h @@ -60,18 +60,24 @@ struct BaseOpcode //! Given a string like "pitch_keytrack=1200" which has been split into name/value as "pitch_keytrack", "1200", this function updates the internal value if the name matches. virtual void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) = 0; }; -//! A base struct for all opcodes, just a name(s) and a value. +//! A base struct for all opcodes, with the name(s) and the value template struct Opcode : BaseOpcode { // Using a vector here, since some opcodes have multiple aliases std::vector m_opcodeNames; - T m_value; - - Opcode(QString name, T defaultValue) : m_opcodeNames({name}), m_value(defaultValue) {} - Opcode(std::vector names, T defaultValue) : m_opcodeNames(names), m_value(defaultValue) {} - virtual void setValue(const T& value) { m_value = value; } - virtual const T value() const { return m_value; } // TODO override operators instead + //! Due to issues with the compiler not recognizing implicit conversion from Opcode> directly to "type", all opcodes use std::optional on the inside. + std::optional m_value; + + Opcode(QString name, std::optional defaultValue) : m_opcodeNames({name}), m_value(defaultValue) {} + Opcode(std::vector names, std::optional defaultValue) : m_opcodeNames(names), m_value(defaultValue) {} + // Redefining operators to make opcodes easy to work with + Opcode& operator =(const T value) { m_value = value; return *this; } + operator T() const { return m_value.value(); } + // This is not ideal, but here's a wrapper function which does the same thing as std::optional's value_or + const T value_or(const T alternative) const { return m_value.value_or(alternative); } + // Comparing the opcode to std::nullopt had issues, since the compiler did not want to do two implicit conversions(?) so here the comparision is expressly defined. + bool operator==(std::nullopt_t other) const { return m_value == std::nullopt; } void parseFromString(const QString& opcodeName, const QString& opcodeValue, bool* parsed, bool* successful) override; }; @@ -79,20 +85,14 @@ struct Opcode : BaseOpcode //! Float/decimal opcodes (such as amplitude, panning, etc) using FloatOpcode = Opcode; -//! Some float opcodes may take on a null default value (like filter cutoff) -using OptionalFloatOpcode = Opcode>; - //! Integer opcodes (such as lovel, seq_length, seq_position, offset, etc) using IntOpcode = Opcode; //! Key opcodes (such as lokey, hikey, pitch_keycenter, etc) using KeyOpcode = Opcode; -//! Some key opcodes (sw_last, sw_default) make sense to have null default values -using OptionalKeyOpcode = Opcode>; - //! String opcodes (such as sample file path) -using StringOpcode = Opcode>; +using StringOpcode = Opcode; //! Many opcodes can be modulated by MIDI CC knobs, so each of them needs an array to store the modulation weights for each one. struct ModulatableOpcode : FloatOpcode @@ -113,12 +113,12 @@ struct ModulatableOpcode : FloatOpcode ModulatableOpcode(QString name = "", float defaultValue = 0.0f) : FloatOpcode(name, defaultValue) {}; ModulatableOpcode(std::vector names = {}, float defaultValue = 0.0f) : FloatOpcode(names, defaultValue) {}; - //! Redefine the value() function to return the sum of the base opcode value and whatever the modulation is currently. Or, if the modultion type if multiplication, multiply the two. - const float value() const override + //! Redefine the value/conversion function to return the sum of the base opcode value and whatever the modulation is currently. Or, if the modultion type if multiplication, multiply the two. + operator float() const { return (modulationType.value_or(ModulationType::Add) == ModulationType::Add) - ? m_value + cachedModulation - : m_value * cachedModulation; + ? m_value.value() + cachedModulation + : m_value.value() * cachedModulation; } //! Helper function for parsing these kinds of opcodes, where you have both `opcode` and `opcode_onccN` where N is the midi cc number, so // that the code isn't duplicated for every modulatable parameter. diff --git a/plugins/SfzPlayer/SfzParser.cpp b/plugins/SfzPlayer/SfzParser.cpp index d2005c1af79..94009c717ae 100644 --- a/plugins/SfzPlayer/SfzParser.cpp +++ b/plugins/SfzPlayer/SfzParser.cpp @@ -273,14 +273,14 @@ bool SfzParser::parseSfzFile(const QString& filePath, std::vector& ou // make a list of them in the controlsConfig object for easy access for (const auto& region : outputRegions) { - if (region.m_sw_last.value() != std::nullopt) + if (region.m_sw_last != std::nullopt) { SfzControlsConfig::SwitchKeyInfo info; - info.sw_label = region.m_sw_label.value().value_or(""); - info.sw_lokey = region.m_sw_lokey.value(); - info.sw_hikey = region.m_sw_hikey.value(); - info.sw_default = region.m_sw_default.value(); - controlsConfig.m_switchKeyInfo.insert({region.m_sw_last.value().value(), info}); + info.sw_label = region.m_sw_label.value_or(""); + info.sw_lokey = region.m_sw_lokey; + info.sw_hikey = region.m_sw_hikey; + info.sw_default = region.m_sw_default.m_value; // Directly accessing m_value, since casting to std::optional is difficult; it always wants to cast to int or something, and crashes when it's std::nullopt? + controlsConfig.m_switchKeyInfo.insert({region.m_sw_last, info}); } } diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index d9a04bb1257..6d3ca2dc015 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -156,7 +156,7 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) // For fun, display the last played sample on the GUI if (foundOpenPosition) { - setStatusInfo("Last Played Sample: " + QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName()); + setStatusInfo("Last Played Sample: " + QFileInfo(region->m_sampleFile.value_or("N/A")).fileName()); } else { @@ -290,7 +290,7 @@ void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) for (auto* region : m_tempRegionManager->allRegions()) { // Update the GUI info text to notify the user as samples are loaded - setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value().value_or("N/A")).fileName())); + setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } // TODO organize this debug info diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index 99034724fa1..7bc6a805472 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -50,8 +50,8 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf if (trigger.type() == SfzTrigger::Type::ControlChange) { return false; } // TODO. It is possible for midi CC's to trigger regions, such as using the sustain pedal or on_locc/on_hicc // Make sure the trigger type matches - if (trigger.type() == SfzTrigger::Type::NoteOn && m_trigger.value() != TriggerType::Attack) { return false; } - if (trigger.type() == SfzTrigger::Type::NoteOff && m_trigger.value() != TriggerType::Release) { return false; } + if (trigger.type() == SfzTrigger::Type::NoteOn && m_trigger != TriggerType::Attack) { return false; } + if (trigger.type() == SfzTrigger::Type::NoteOff && m_trigger != TriggerType::Release) { return false; } // Assuming the trigger has key/vel info (i.e., it's a noteOn/noteOff, not a midi CC event), make sure all the key/vel selectors match if (trigger.type() == SfzTrigger::Type::NoteOn || trigger.type() == SfzTrigger::Type::NoteOff) @@ -61,19 +61,19 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // Ensure the key was pressed between the `lokey` and `hikey` opcodes // TODO this can be removed now that SfzRegionManager handles lookup tables for regions by key - if (triggerKey > m_hikey.value() || triggerKey < m_lokey.value()) { return false; } + if (triggerKey > m_hikey || triggerKey < m_lokey) { return false; } // And had velocity between `lovel` and `hivel` opcodes - if (triggerVelocity > m_hivel.value() || triggerVelocity < m_lovel.value()) { return false; } + if (triggerVelocity > m_hivel || triggerVelocity < m_lovel) { return false; } // If a keyswitch range was defined, ensure the last pressed valid switch key in that range matches the specified keyswitch for this region // TODO add unit tests - if (m_sw_last.value() != std::nullopt && globalState.lastSwitchKeyPressedInRange(m_sw_lokey.value(), m_sw_hikey.value(), m_sw_default.value()) != m_sw_last.value()) { return false; } + if (m_sw_last != std::nullopt && globalState.lastSwitchKeyPressedInRange(m_sw_lokey, m_sw_hikey, m_sw_default.m_value /*Accessing m_value due to issues with implicit cast to std::optional*/) != m_sw_last) { return false; } } // If the region uses lorand/hirand, the current random value stored in the global state (updated every trigger) is compared with the range // Note: the upper bound is inclusive, so lorand=0.2 hirand=0.4 will be triggered by any rand value >0.2 or <=0.4 - if (globalState.rand() > m_hirand.value() || globalState.rand() <= m_lorand.value()) { return false; } + if (globalState.rand() > m_hirand || globalState.rand() <= m_lorand) { return false; } // If midi CC ranges are defined, make sure the current CC values are within range // Only loop over the CC's which have lo/hiccN defined, instead of checking all 128 every time @@ -85,7 +85,7 @@ bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const Sf // If all conditions up until now have passed, that means we're ready to play sound. However, if round-robin is set up, we only do it if it's our turn. m_roundRobinCount++; // TODO it would be nice if this function could be const and we didn't have to update this here. idk. - if (m_roundRobinCount % m_seq_length.value() != m_seq_position.value() - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn + if (m_roundRobinCount % m_seq_length != m_seq_position - 1 /*Minus 1 because the opcode is 1-indexed*/) { return false; } // Not our turn // If all the contitions passed, return true and spawn a sound return true; @@ -109,7 +109,7 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool) { - if (m_sampleFile.value() == std::nullopt) + if (m_sampleFile == std::nullopt) { // It's weird for a region not to have a sample defined. That's literally all regions do, play samples, right? qDebug() << "[SFZ Player] Warning: `sample` opcode not assigned"; @@ -119,18 +119,18 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam // There are some special sample keywords for simple wave shapes, such as *sine, *triangle, *silence, etc // If one of them is used, we don't load a sample file, instead leave m_sample as nullptr and set m_basicWaveShape // to let the region know that we are doing a procedurally generated wave - if (m_sampleFile.value() == "*sine") { m_basicWaveShape = SfzBasicWaves::Shape::Sine; } - else if (m_sampleFile.value() == "*saw") { m_basicWaveShape = SfzBasicWaves::Shape::Saw; } - else if (m_sampleFile.value() == "*square") { m_basicWaveShape = SfzBasicWaves::Shape::Square; } - else if (m_sampleFile.value() == "*triangle") { m_basicWaveShape = SfzBasicWaves::Shape::Triangle; } - else if (m_sampleFile.value() == "*tri") { m_basicWaveShape = SfzBasicWaves::Shape::Triangle; } - else if (m_sampleFile.value() == "*noise") { m_basicWaveShape = SfzBasicWaves::Shape::Noise; } - else if (m_sampleFile.value() == "*silence") { m_basicWaveShape = SfzBasicWaves::Shape::Silence; } + if (m_sampleFile == "*sine") { m_basicWaveShape = SfzBasicWaves::Shape::Sine; } + else if (m_sampleFile == "*saw") { m_basicWaveShape = SfzBasicWaves::Shape::Saw; } + else if (m_sampleFile == "*square") { m_basicWaveShape = SfzBasicWaves::Shape::Square; } + else if (m_sampleFile == "*triangle") { m_basicWaveShape = SfzBasicWaves::Shape::Triangle; } + else if (m_sampleFile == "*tri") { m_basicWaveShape = SfzBasicWaves::Shape::Triangle; } + else if (m_sampleFile == "*noise") { m_basicWaveShape = SfzBasicWaves::Shape::Noise; } + else if (m_sampleFile == "*silence") { m_basicWaveShape = SfzBasicWaves::Shape::Silence; } else { // If it's not a basic wave, load the real sample file. - QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value().value_or(""))); // TODO - QString path = defaultDirectory.absoluteFilePath(m_sampleFile.value().value()); // TODO + QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value_or(""))); // TODO + QString path = defaultDirectory.absoluteFilePath(m_sampleFile); // TODO // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path); diff --git a/plugins/SfzPlayer/SfzRegionManager.cpp b/plugins/SfzPlayer/SfzRegionManager.cpp index 4a0d82aa447..5eaff15f62e 100644 --- a/plugins/SfzPlayer/SfzRegionManager.cpp +++ b/plugins/SfzPlayer/SfzRegionManager.cpp @@ -39,10 +39,10 @@ SfzRegionManager::SfzRegionManager(std::vector& regions) { for (auto& region : m_regions) { - if (key >= region.m_lokey.value() && key <= region.m_hikey.value()) + if (key >= region.m_lokey && key <= region.m_hikey) { // Additionally organize by trigger type - switch (region.m_trigger.value()) + switch (region.m_trigger) { case TriggerType::Attack: m_noteOnRegions.at(key).push_back(®ion); diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index e4ebe58efb0..5a29b15cbfd 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -51,15 +51,15 @@ SfzRegionPlayState::SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trig // Delay the start of the playback by the trigger offset m_frameCount -= trigger.frameOffset(); // And by the delay opcode in seconds - m_frameCount -= region->m_delay.value() * m_lmmsSampleRate; + m_frameCount -= region->m_delay * m_lmmsSampleRate; // And any random delay amount - m_frameCount -= fastRand(1.0f) * region->m_delay_random.value() * m_lmmsSampleRate; + m_frameCount -= fastRand(1.0f) * region->m_delay_random * m_lmmsSampleRate; // Set initial sample start frame offset - m_sampleFrame += m_region->m_offset.value(); + m_sampleFrame += m_region->m_offset; // Setup the filter - switch (m_region->m_fil_type.value()) + switch (m_region->m_fil_type) { // This is not correct! I'm using the default filters from BasicFilters, but I don't believe they necessarily match the 1 pole vs 2 pole specifications for sfz. // For example, the default lowpass is a biquad, which I believe has 2 poles. For now I'm using it for both lowpass types, but it should probably be changed in the future. @@ -95,12 +95,12 @@ void SfzRegionPlayState::precomputeBaseValues() // The pitch env/lfo will be applied on top of this, as those are calcualted per frame // Calculate pitch difference relative to original sample - const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter.value(); + const float semitoneDifference = m_trigger.key().value() - m_region->m_pitch_keycenter; // The base pitch depends on 1. the key offset, 2. the fine `tune` adjustment, 3. the velocity, if pitch_veltrack is nonzero // These are all in cents, so divide by 100 to get semitones - const float pitch = semitoneDifference * m_region->m_pitch_keytrack.value() / 100.0f - + m_region->m_tune.value() / 100.0f - + normalizedVelocity * m_region->m_pitch_veltrack.value() / 100.0f; + const float pitch = semitoneDifference * m_region->m_pitch_keytrack / 100.0f + + m_region->m_tune / 100.0f + + normalizedVelocity * m_region->m_pitch_veltrack / 100.0f; m_baseFreqRatio = std::exp2(pitch / 12.0f); // Sample rate of sample @@ -114,21 +114,21 @@ void SfzRegionPlayState::precomputeBaseValues() // Compute the base amplitude // Amplitude opcode - const float amplitude = m_region->m_amplitude.value() / 100.0f; // Amplitude is stored as a percent + const float amplitude = m_region->m_amplitude / 100.0f; // Amplitude is stored as a percent // Amplitude due to velocity // If amp_keytrack is 100, then 0 velocity = 0 amp, and 127 velocity = 1.0f amp (as expected) // If amp_keytrack is -100, it's the reverse. If amp_keytrack is 0, the volume is not affected by the velocity. // Essentially this means lerping between y = x, y = 1, and y = 1 - x, if you think of y being the amp and x being vel/127 - const float ampVelocity = m_region->m_amp_veltrack.value() > 0 - ? (normalizedVelocity) * (m_region->m_amp_veltrack.value() / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / 100) - : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack.value() / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack.value() / -100); + const float ampVelocity = m_region->m_amp_veltrack > 0 + ? (normalizedVelocity) * (m_region->m_amp_veltrack / 100) + 1.0f * (1.0f - m_region->m_amp_veltrack / 100) + : (1.0f - normalizedVelocity) * (m_region->m_amp_veltrack / -100) + 1.0f * (1.0f - m_region->m_amp_veltrack / -100); // Amplitude due to volume/gain - const float ampVolume = dbfsToAmp(m_region->m_volume.value()); + const float ampVolume = dbfsToAmp(m_region->m_volume); // Panning - const float pan = m_region->m_pan.value() / 100; + const float pan = m_region->m_pan / 100; const float rightPanAmp = std::min(1.0f, 1.0f + pan); const float leftPanAmp = std::min(1.0f, 1.0f - pan); @@ -224,45 +224,45 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; // Amplitude Envelope Parameters - const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay.value() * m_lmmsSampleRate; - const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack.value() * m_lmmsSampleRate; - const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold.value() * m_lmmsSampleRate; - const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay.value() * m_lmmsSampleRate; - const float ampegSustain = m_region->m_ampeg.sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release.value() * m_lmmsSampleRate; + const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay * m_lmmsSampleRate; + const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack * m_lmmsSampleRate; + const f_cnt_t ampegHoldFrames = m_region->m_ampeg.hold * m_lmmsSampleRate; + const f_cnt_t ampegDecayFrames = m_region->m_ampeg.decay * m_lmmsSampleRate; + const float ampegSustain = m_region->m_ampeg.sustain / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t ampegReleaseFrames = m_region->m_ampeg.release * m_lmmsSampleRate; // Amplitude LFO parameters - const f_cnt_t amplfoDelayFrames = m_region->m_amplfo.delay.value() * m_lmmsSampleRate; - const f_cnt_t amplfoFadeFrames = m_region->m_amplfo.fade.value() * m_lmmsSampleRate; - const float amplfoFreq = m_region->m_amplfo.freq.value(); - const float amplfoDepth = m_region->m_amplfo.depth.value(); + const f_cnt_t amplfoDelayFrames = m_region->m_amplfo.delay * m_lmmsSampleRate; + const f_cnt_t amplfoFadeFrames = m_region->m_amplfo.fade * m_lmmsSampleRate; + const float amplfoFreq = m_region->m_amplfo.freq; + const float amplfoDepth = m_region->m_amplfo.depth; // Pitch Envelope Parameters - const f_cnt_t pitchegDelayFrames = m_region->m_pitcheg.delay.value() * m_lmmsSampleRate; - const f_cnt_t pitchegAttackFrames = m_region->m_pitcheg.attack.value() * m_lmmsSampleRate; - const f_cnt_t pitchegHoldFrames = m_region->m_pitcheg.hold.value() * m_lmmsSampleRate; - const f_cnt_t pitchegDecayFrames = m_region->m_pitcheg.decay.value() * m_lmmsSampleRate; - const float pitchegSustain = m_region->m_pitcheg.sustain.value() / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio - const f_cnt_t pitchegReleaseFrames = m_region->m_pitcheg.release.value() * m_lmmsSampleRate; - const float pitchegDepth = m_region->m_pitcheg.depth.value(); + const f_cnt_t pitchegDelayFrames = m_region->m_pitcheg.delay * m_lmmsSampleRate; + const f_cnt_t pitchegAttackFrames = m_region->m_pitcheg.attack * m_lmmsSampleRate; + const f_cnt_t pitchegHoldFrames = m_region->m_pitcheg.hold * m_lmmsSampleRate; + const f_cnt_t pitchegDecayFrames = m_region->m_pitcheg.decay * m_lmmsSampleRate; + const float pitchegSustain = m_region->m_pitcheg.sustain / 100.0f; // Sustain is stored in percent, so divide by 100 to get ratio + const f_cnt_t pitchegReleaseFrames = m_region->m_pitcheg.release * m_lmmsSampleRate; + const float pitchegDepth = m_region->m_pitcheg.depth; // Pitch LFO parameters - const f_cnt_t pitchlfoDelayFrames = m_region->m_pitchlfo.delay.value() * m_lmmsSampleRate; - const f_cnt_t pitchlfoFadeFrames = m_region->m_pitchlfo.fade.value() * m_lmmsSampleRate; - const float pitchlfoFreq = m_region->m_pitchlfo.freq.value(); - const float pitchlfoDepth = m_region->m_pitchlfo.depth.value(); + const f_cnt_t pitchlfoDelayFrames = m_region->m_pitchlfo.delay * m_lmmsSampleRate; + const f_cnt_t pitchlfoFadeFrames = m_region->m_pitchlfo.fade * m_lmmsSampleRate; + const float pitchlfoFreq = m_region->m_pitchlfo.freq; + const float pitchlfoDepth = m_region->m_pitchlfo.depth; // Filter - const bool filterEnabled = m_region->m_cutoff.value() != std::nullopt; + const bool filterEnabled = m_region->m_cutoff != std::nullopt; // For performance, only update the cutoff frequency/resonance once per buffer if (filterEnabled) { // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I saw 20000 being used in Metal GTX which is kind of high for cents (200 octaves?) - const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack.value(); - const float filterCutoff = m_region->m_cutoff.value().value() + std::exp2(filterCutoffPitchOffset / 12.0f); + const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack; + const float filterCutoff = m_region->m_cutoff + std::exp2(filterCutoffPitchOffset / 12.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it - const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance.value()); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. + const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. m_filter.calcFilterCoeffs(filterCutoff, q); } @@ -328,7 +328,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) } // If the end of the sample is reached and the region's loop mode does not loop, deactivate this voice. - if ((m_region->m_loop_mode.value() == LoopMode::OneShot || m_region->m_loop_mode.value() == LoopMode::NoLoop) && m_sampleObject != nullptr && m_sampleFrame >= m_sampleObject->size()) + if ((m_region->m_loop_mode == LoopMode::OneShot || m_region->m_loop_mode == LoopMode::NoLoop) && m_sampleObject != nullptr && m_sampleFrame >= m_sampleObject->size()) { m_active = false; // TODO should this forcefully decative or just release? } @@ -347,7 +347,7 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger, SfzGlobalStat if (trigger.type() == SfzTrigger::Type::NoteOff) { - if (m_region->m_loop_mode.value() == LoopMode::OneShot) { return; } // If one_shot looping is enabled, the whole sample will play regardless of if the note is released + if (m_region->m_loop_mode == LoopMode::OneShot) { return; } // If one_shot looping is enabled, the whole sample will play regardless of if the note is released if (trigger.key() == m_trigger.key()) { From 3c802968a4a3612c371fcb8b7e103033abf7b29c Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 15 May 2026 15:05:35 -0400 Subject: [PATCH 113/131] Handle failed loading samples without popping up hudnreds of message box windows --- plugins/SfzPlayer/SfzPlayer.cpp | 16 ++++++++++++++-- plugins/SfzPlayer/SfzSampleBuffer.cpp | 8 ++++---- plugins/SfzPlayer/SfzSampleBuffer.h | 4 ++-- plugins/SfzPlayer/SfzSamplePool.cpp | 25 +++++++++++++++++++------ plugins/SfzPlayer/SfzSamplePool.h | 1 + 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 6d3ca2dc015..d3157912c1d 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -287,16 +287,28 @@ void SfzPlayer::loadSfzFile(const QString& filePath, const bool resetCCKnobs) void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) { int i = 0; + int samplesFailedToLoad = 0; for (auto* region : m_tempRegionManager->allRegions()) { // Update the GUI info text to notify the user as samples are loaded setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); - if (!successfulLoadSample) { qDebug() << "[SFZ Player] An error occured when loading a sample."; } // TODO organize this debug info + if (!successfulLoadSample) + { + samplesFailedToLoad++; + setStatusInfo(QString("An error occured when loading sample %1").arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); + } i++; } - setStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); + if (samplesFailedToLoad == 0) + { + setStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); + } + else + { + setStatusInfo(QString("Loaded %1 regions and %2 samples.\nWARNING: Failed to load %3 samples, see logs for details.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount()).arg(samplesFailedToLoad)); + } // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data m_newSfzDataReady = true; diff --git a/plugins/SfzPlayer/SfzSampleBuffer.cpp b/plugins/SfzPlayer/SfzSampleBuffer.cpp index 20abc43e8a6..cfb66159f47 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.cpp +++ b/plugins/SfzPlayer/SfzSampleBuffer.cpp @@ -29,12 +29,12 @@ namespace lmms { -SfzSampleBuffer::SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate) - : m_data(new float[size * NUM_CHANNELS]) - , m_size(size) +SfzSampleBuffer::SfzSampleBuffer(const std::vector& data, const float sampleRate) + : m_data(new float[data.size() * NUM_CHANNELS]) + , m_size(data.size()) , m_sampleRate(sampleRate) { - for (f_cnt_t f = 0; f < size; ++f) + for (f_cnt_t f = 0; f < m_size; ++f) { for (size_t channel = 0; channel < NUM_CHANNELS; ++channel) { diff --git a/plugins/SfzPlayer/SfzSampleBuffer.h b/plugins/SfzPlayer/SfzSampleBuffer.h index c75d3a2e87b..0c50a4b0767 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.h +++ b/plugins/SfzPlayer/SfzSampleBuffer.h @@ -26,7 +26,7 @@ #define LMMS_SFZ_SAMPLE_BUFFER_H #include "SampleFrame.h" -#include +#include namespace lmms { @@ -37,7 +37,7 @@ class SfzSampleBuffer { public: SfzSampleBuffer() = default; - SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate); + SfzSampleBuffer(const std::vector& data, const float sampleRate); ~SfzSampleBuffer(); //! Returns a hermite-interpolated value for the data at the given sample index and channel. diff --git a/plugins/SfzPlayer/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp index 6868a948208..ef0f6030c2b 100644 --- a/plugins/SfzPlayer/SfzSamplePool.cpp +++ b/plugins/SfzPlayer/SfzSamplePool.cpp @@ -24,6 +24,10 @@ #include "SfzSamplePool.h" #include "SampleBuffer.h" +#include "PathUtil.h" +#include "SampleDecoder.h" + +#include namespace lmms { @@ -36,15 +40,24 @@ const SfzSampleBuffer* SfzSamplePool::loadSample(const QString& path) { return m_samplePool.at(path).get(); } - else if (auto buffer = SampleBuffer::fromFile(path)) - { - m_samplePool.insert({path, std::make_unique(buffer->data(), buffer->size(), buffer->sampleRate())}); - return m_samplePool.at(path).get(); - } - else + + // Copied from SampleBuffer.cpp + // This is done instead of calling SampleBuffer::fromFile, since that function creates a warning box/window which the user needs to close, every single time a sample fails to load. + // For an SFZ with thousands of samples, if one fails to load due to an invalid path, it's likely all will fail to load, and the user needs to close thousands or windows, or forcefully close lmms. + // Doing it manually here bypasses the gui. + const auto absolutePath = PathUtil::toAbsolute(path); + auto result = SampleDecoder::decode(absolutePath); + + if (!result) { + qWarning() << QObject::tr("Failed to load sample at path %1, the file may not exist, be corrupted, or is unsupported.").arg(absolutePath); return nullptr; } + + auto& [data, sampleRate] = *result; + + m_samplePool.insert({path, std::make_unique(std::move(data), sampleRate)}); + return m_samplePool.at(path).get(); } diff --git a/plugins/SfzPlayer/SfzSamplePool.h b/plugins/SfzPlayer/SfzSamplePool.h index b3e658f742c..2458efd6bf1 100644 --- a/plugins/SfzPlayer/SfzSamplePool.h +++ b/plugins/SfzPlayer/SfzSamplePool.h @@ -28,6 +28,7 @@ #include "SfzSampleBuffer.h" #include +#include #include namespace lmms From cbd5e3e6787d86ad0bedb20da7edba0acc0d0c57 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 15 May 2026 15:41:21 -0400 Subject: [PATCH 114/131] Make SampleDecoder available to plugins to fix windows builds --- include/SampleDecoder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/SampleDecoder.h b/include/SampleDecoder.h index 5013554ddcb..793c9b27de1 100644 --- a/include/SampleDecoder.h +++ b/include/SampleDecoder.h @@ -33,7 +33,7 @@ #include "SampleFrame.h" namespace lmms { -class SampleDecoder +class LMMS_EXPORT SampleDecoder { public: struct Result From 2129bfea00a04777a15c1120981bac582bfa1da9 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 15 May 2026 15:46:33 -0400 Subject: [PATCH 115/131] Revert sample loading message box fix --- include/SampleDecoder.h | 2 +- plugins/SfzPlayer/SfzSampleBuffer.cpp | 8 ++++---- plugins/SfzPlayer/SfzSampleBuffer.h | 4 ++-- plugins/SfzPlayer/SfzSamplePool.cpp | 25 ++++++------------------- plugins/SfzPlayer/SfzSamplePool.h | 1 - 5 files changed, 13 insertions(+), 27 deletions(-) diff --git a/include/SampleDecoder.h b/include/SampleDecoder.h index 793c9b27de1..5013554ddcb 100644 --- a/include/SampleDecoder.h +++ b/include/SampleDecoder.h @@ -33,7 +33,7 @@ #include "SampleFrame.h" namespace lmms { -class LMMS_EXPORT SampleDecoder +class SampleDecoder { public: struct Result diff --git a/plugins/SfzPlayer/SfzSampleBuffer.cpp b/plugins/SfzPlayer/SfzSampleBuffer.cpp index cfb66159f47..20abc43e8a6 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.cpp +++ b/plugins/SfzPlayer/SfzSampleBuffer.cpp @@ -29,12 +29,12 @@ namespace lmms { -SfzSampleBuffer::SfzSampleBuffer(const std::vector& data, const float sampleRate) - : m_data(new float[data.size() * NUM_CHANNELS]) - , m_size(data.size()) +SfzSampleBuffer::SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate) + : m_data(new float[size * NUM_CHANNELS]) + , m_size(size) , m_sampleRate(sampleRate) { - for (f_cnt_t f = 0; f < m_size; ++f) + for (f_cnt_t f = 0; f < size; ++f) { for (size_t channel = 0; channel < NUM_CHANNELS; ++channel) { diff --git a/plugins/SfzPlayer/SfzSampleBuffer.h b/plugins/SfzPlayer/SfzSampleBuffer.h index 0c50a4b0767..c75d3a2e87b 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.h +++ b/plugins/SfzPlayer/SfzSampleBuffer.h @@ -26,7 +26,7 @@ #define LMMS_SFZ_SAMPLE_BUFFER_H #include "SampleFrame.h" -#include +#include namespace lmms { @@ -37,7 +37,7 @@ class SfzSampleBuffer { public: SfzSampleBuffer() = default; - SfzSampleBuffer(const std::vector& data, const float sampleRate); + SfzSampleBuffer(const SampleFrame* data, const f_cnt_t size, const float sampleRate); ~SfzSampleBuffer(); //! Returns a hermite-interpolated value for the data at the given sample index and channel. diff --git a/plugins/SfzPlayer/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp index ef0f6030c2b..6868a948208 100644 --- a/plugins/SfzPlayer/SfzSamplePool.cpp +++ b/plugins/SfzPlayer/SfzSamplePool.cpp @@ -24,10 +24,6 @@ #include "SfzSamplePool.h" #include "SampleBuffer.h" -#include "PathUtil.h" -#include "SampleDecoder.h" - -#include namespace lmms { @@ -40,24 +36,15 @@ const SfzSampleBuffer* SfzSamplePool::loadSample(const QString& path) { return m_samplePool.at(path).get(); } - - // Copied from SampleBuffer.cpp - // This is done instead of calling SampleBuffer::fromFile, since that function creates a warning box/window which the user needs to close, every single time a sample fails to load. - // For an SFZ with thousands of samples, if one fails to load due to an invalid path, it's likely all will fail to load, and the user needs to close thousands or windows, or forcefully close lmms. - // Doing it manually here bypasses the gui. - const auto absolutePath = PathUtil::toAbsolute(path); - auto result = SampleDecoder::decode(absolutePath); - - if (!result) + else if (auto buffer = SampleBuffer::fromFile(path)) + { + m_samplePool.insert({path, std::make_unique(buffer->data(), buffer->size(), buffer->sampleRate())}); + return m_samplePool.at(path).get(); + } + else { - qWarning() << QObject::tr("Failed to load sample at path %1, the file may not exist, be corrupted, or is unsupported.").arg(absolutePath); return nullptr; } - - auto& [data, sampleRate] = *result; - - m_samplePool.insert({path, std::make_unique(std::move(data), sampleRate)}); - return m_samplePool.at(path).get(); } diff --git a/plugins/SfzPlayer/SfzSamplePool.h b/plugins/SfzPlayer/SfzSamplePool.h index 2458efd6bf1..b3e658f742c 100644 --- a/plugins/SfzPlayer/SfzSamplePool.h +++ b/plugins/SfzPlayer/SfzSamplePool.h @@ -28,7 +28,6 @@ #include "SfzSampleBuffer.h" #include -#include #include namespace lmms From cffb7ef3cd166d2e8ad915cd558c08e2c6646137 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 15 May 2026 15:47:55 -0400 Subject: [PATCH 116/131] Attempt new fix --- include/SampleBuffer.h | 2 +- plugins/SfzPlayer/SfzSamplePool.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index 1fe41985a1c..b5866f2c5ab 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -83,7 +83,7 @@ class LMMS_EXPORT SampleBuffer static auto emptyBuffer() -> std::shared_ptr; - static std::shared_ptr fromFile(const QString& path); + static std::shared_ptr fromFile(const QString& path, const bool displayErrorWindow = true); static std::shared_ptr fromBase64( const QString& str, int sampleRate = Engine::audioEngine()->outputSampleRate()); diff --git a/plugins/SfzPlayer/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp index 6868a948208..acbd1762503 100644 --- a/plugins/SfzPlayer/SfzSamplePool.cpp +++ b/plugins/SfzPlayer/SfzSamplePool.cpp @@ -36,7 +36,7 @@ const SfzSampleBuffer* SfzSamplePool::loadSample(const QString& path) { return m_samplePool.at(path).get(); } - else if (auto buffer = SampleBuffer::fromFile(path)) + else if (auto buffer = SampleBuffer::fromFile(path, false)) { m_samplePool.insert({path, std::make_unique(buffer->data(), buffer->size(), buffer->sampleRate())}); return m_samplePool.at(path).get(); From 8bae2c3c8c652ab40d6893be20744d316735a346 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 15 May 2026 16:01:48 -0400 Subject: [PATCH 117/131] Attempt to fix bad commit --- include/SampleBuffer.h | 2 +- plugins/SfzPlayer/SfzSamplePool.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SampleBuffer.h b/include/SampleBuffer.h index b5866f2c5ab..1fe41985a1c 100644 --- a/include/SampleBuffer.h +++ b/include/SampleBuffer.h @@ -83,7 +83,7 @@ class LMMS_EXPORT SampleBuffer static auto emptyBuffer() -> std::shared_ptr; - static std::shared_ptr fromFile(const QString& path, const bool displayErrorWindow = true); + static std::shared_ptr fromFile(const QString& path); static std::shared_ptr fromBase64( const QString& str, int sampleRate = Engine::audioEngine()->outputSampleRate()); diff --git a/plugins/SfzPlayer/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp index acbd1762503..6868a948208 100644 --- a/plugins/SfzPlayer/SfzSamplePool.cpp +++ b/plugins/SfzPlayer/SfzSamplePool.cpp @@ -36,7 +36,7 @@ const SfzSampleBuffer* SfzSamplePool::loadSample(const QString& path) { return m_samplePool.at(path).get(); } - else if (auto buffer = SampleBuffer::fromFile(path, false)) + else if (auto buffer = SampleBuffer::fromFile(path)) { m_samplePool.insert({path, std::make_unique(buffer->data(), buffer->size(), buffer->sampleRate())}); return m_samplePool.at(path).get(); From 97ab20c92cb35bcfaa10d514e4f70c7b11863bc4 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 16 May 2026 15:45:01 -0400 Subject: [PATCH 118/131] Fix style and remove old profiling code --- plugins/SfzPlayer/SfzGlobalState.h | 2 +- plugins/SfzPlayer/SfzRegionPlayState.cpp | 13 ------------- plugins/SfzPlayer/SfzTrigger.cpp | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/plugins/SfzPlayer/SfzGlobalState.h b/plugins/SfzPlayer/SfzGlobalState.h index a0386ffa6e1..db45373befa 100644 --- a/plugins/SfzPlayer/SfzGlobalState.h +++ b/plugins/SfzPlayer/SfzGlobalState.h @@ -92,4 +92,4 @@ class SfzGlobalState } // namespace lmms -#endif // LMMS_SFZ_GLOBAL_STATE_H \ No newline at end of file +#endif // LMMS_SFZ_GLOBAL_STATE_H diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index 5a29b15cbfd..e851399d1e5 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -211,15 +211,6 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // If the initial m_frameCount is negative, that means the note hasn't started yet if (m_frameCount < -static_cast(frames)) { m_frameCount += frames; return false; } // If the note doesn't start in this buffer, don't play anything - /* - static int totalMicroseconds = 0; - static int totalSquaredMicroseconds = 0; - static int totalCalls = 0; - static int minElapsed = 10000000; - static int maxElapsed = 0; - MicroTimer profiler; - */ - // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; @@ -333,10 +324,6 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) m_active = false; // TODO should this forcefully decative or just release? } - //int elapsed = profiler.elapsed(); totalMicroseconds += elapsed; totalSquaredMicroseconds += elapsed * elapsed; totalCalls++; minElapsed = std::min(minElapsed, elapsed); maxElapsed = std::max(maxElapsed, elapsed); - //float mean = 1.0f * totalMicroseconds / totalCalls, variance = (1.0f * totalSquaredMicroseconds - 1.0f * totalMicroseconds * totalMicroseconds / totalCalls / totalCalls) / totalCalls; - //qDebug() << "SfzRegionPlayState::play profiler:" << elapsed << "Min:" << minElapsed << "Max:" << maxElapsed << "Total calls" << totalCalls << "Mean:" << mean << "Stdev:" << std::sqrt(variance) << "Stdev of mean:" << sqrt(variance / totalCalls); - return true; } diff --git a/plugins/SfzPlayer/SfzTrigger.cpp b/plugins/SfzPlayer/SfzTrigger.cpp index d2113ea8f2a..0383e56cf6b 100644 --- a/plugins/SfzPlayer/SfzTrigger.cpp +++ b/plugins/SfzPlayer/SfzTrigger.cpp @@ -57,4 +57,4 @@ const SfzTrigger SfzTrigger::controlChangeEvent(const int frameOffset, const int return trigger; } -} // namespace lmms \ No newline at end of file +} // namespace lmms From 3d92c5aeb69cd517965cc13a54594f956adec1cb Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 16 May 2026 19:17:03 -0400 Subject: [PATCH 119/131] Fix fil_veltrack calculation --- plugins/SfzPlayer/SfzRegionPlayState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index e851399d1e5..b14067da5fc 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -251,7 +251,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) { // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I saw 20000 being used in Metal GTX which is kind of high for cents (200 octaves?) const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack; - const float filterCutoff = m_region->m_cutoff + std::exp2(filterCutoffPitchOffset / 12.0f); + const float filterCutoff = m_region->m_cutoff * std::exp2(filterCutoffPitchOffset / 1200.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it const float q = std::sqrt(2.0f) * dbfsToAmp(m_region->m_resonance); // TODO is this equation correct? I'm sort of basing it off https://www.musicdsp.org/en/latest/Filters/180-cool-sounding-lowpass-with-decibel-measured-resonance.html but I'm not sure. m_filter.calcFilterCoeffs(filterCutoff, q); From 6bddf78134e895ec89767d634186a03d12112c2f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sat, 16 May 2026 19:26:26 -0400 Subject: [PATCH 120/131] Update comment --- plugins/SfzPlayer/SfzRegionPlayState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index b14067da5fc..5ac2439cba7 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -249,7 +249,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // For performance, only update the cutoff frequency/resonance once per buffer if (filterEnabled) { - // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them TODO: are we sure it's cents? I saw 20000 being used in Metal GTX which is kind of high for cents (200 octaves?) + // The cutoff frequency is in hertz, but things like fil_veltrack are defined in cents, so we have to convert them const float filterCutoffPitchOffset = normalizedVelocity * m_region->m_fil_veltrack; const float filterCutoff = m_region->m_cutoff * std::exp2(filterCutoffPitchOffset / 1200.0f); // SFZ has the resonance given in decibals, which is not the same as the resonance passed to the filter, so we have to convert it From 5e1f56473fbe2cf91fddfa13d1e9baac5c04d134 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Wed, 27 May 2026 17:46:54 -0400 Subject: [PATCH 121/131] Add pitchbending, microtuning, and note panning --- plugins/SfzPlayer/SfzGlobalState.cpp | 13 ++++++++++++ plugins/SfzPlayer/SfzGlobalState.h | 14 +++++++++++++ plugins/SfzPlayer/SfzPlayer.cpp | 9 ++++++-- plugins/SfzPlayer/SfzRegionPlayState.cpp | 26 ++++++++++++++++-------- plugins/SfzPlayer/SfzRegionPlayState.h | 9 ++++++-- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/plugins/SfzPlayer/SfzGlobalState.cpp b/plugins/SfzPlayer/SfzGlobalState.cpp index 604ccf52b75..cc1abf2463b 100644 --- a/plugins/SfzPlayer/SfzGlobalState.cpp +++ b/plugins/SfzPlayer/SfzGlobalState.cpp @@ -81,6 +81,19 @@ std::optional SfzGlobalState::lastSwitchKeyPressedInRange(int lowKey, int h } +void SfzGlobalState::updateNphFreq(const int key, const float freq) +{ + const float originalKeyFreq = 440.0f * std::exp2((key - 69) / 12.0f); + m_nphFreqRatios.at(key) = freq / originalKeyFreq; +} + +void SfzGlobalState::updateNphPanning(const int key, const float panning) +{ + m_nphPanning.at(key) = panning; +} + + + void SfzGlobalState::initializeMidiCCValues(const SfzControlsConfig& controlsConfig) { for (int i = 0; i < NumMidiCCs; ++i) diff --git a/plugins/SfzPlayer/SfzGlobalState.h b/plugins/SfzPlayer/SfzGlobalState.h index db45373befa..228c25e1231 100644 --- a/plugins/SfzPlayer/SfzGlobalState.h +++ b/plugins/SfzPlayer/SfzGlobalState.h @@ -56,6 +56,15 @@ class SfzGlobalState //! Returns a const array of the current CC knob values const std::array midiCCValues() const { return m_ccValues; } + //! Returns the ratio of the frequency of the last played NotePlayHandle on this key compared to the base freq of the key. + //! This is updated every buffer when a NotePlayHandle is active, and is useful for applying pitch bending and microtuning + const float nphKeyFreqRatio(const int key) const { return m_nphFreqRatios.at(key); } + //! Updates the current freq ratio of the key, given the current freq of the active NotePlayHandle + void updateNphFreq(const int key, const float freq); + // Similar functions for NotePlayHandle panning + const float nphKeyPanning(const int key) const { return m_nphPanning.at(key); } + void updateNphPanning(const int key, const float panning); + //! Returns the random value for the current trigger const float rand() const { return m_rand; } @@ -86,6 +95,11 @@ class SfzGlobalState //! Stores the current value of all the midi CC knobs/controllers //! Technically, floats should probably be used to allow for HDCC (high definition CC's) as used in ARIA, but for now dividing by 127 to get a float between 0 and 1 works fine. std::array m_ccValues = {}; + + //! Stores the current frequencies of active NotePlayHandles (one per key, so overlaping NPH's may not work correctly) as a ratio compared to the freq of the key + std::array m_nphFreqRatios = {}; + //! Stores the current panning of active NotePlayHandles, per key. + std::array m_nphPanning = {}; }; diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index d3157912c1d..81e684b1ff3 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -120,6 +120,11 @@ void SfzPlayer::playNote(NotePlayHandle* handle, SampleFrame* workingBuffer) { // TODO: Currently this instrument only uses midi events as input, and a single audio buffer as output. // It might be possible to split things up to use individual NotePlayHandles, but that is for another time. + + // In order for pitch bending and microtuning to work, the exact freq of the NotePlayHandle needs to be conveyed to the voices + m_sfzGlobalState.updateNphFreq(handle->midiKey(), handle->frequency()); + // Same thing for note panning in the piano roll + m_sfzGlobalState.updateNphPanning(handle->midiKey(), handle->getPanning()); } void SfzPlayer::deleteNotePluginData(NotePlayHandle* handle) @@ -146,7 +151,7 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) auto& regionPlayState = m_voices[i]; if (!regionPlayState.active()) { - regionPlayState = SfzRegionPlayState(region, trigger, m_sfzGlobalState); + regionPlayState = SfzRegionPlayState(region, trigger, &m_sfzGlobalState); // If this new index is above the current max active index, update it m_maxActiveIndex = std::max(m_maxActiveIndex, i); foundOpenPosition = true; @@ -172,7 +177,7 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) if (regionPlayState.active()) { - regionPlayState.processTrigger(trigger, m_sfzGlobalState); + regionPlayState.processTrigger(trigger); // If this was the max active index and the trigger caused it to deactivate, figure out what the next active index is if (!regionPlayState.active() && i == m_maxActiveIndex) { recalculateMaxActiveIndex(); } } diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index 5ac2439cba7..5dc78f6a6e2 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -36,16 +36,17 @@ namespace lmms { -SfzRegionPlayState::SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trigger, const SfzGlobalState& globalState) +SfzRegionPlayState::SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trigger, const SfzGlobalState* globalState) : m_active(true) , m_lmmsSampleRate(Engine::audioEngine()->outputSampleRate()) , m_filter(m_lmmsSampleRate) , m_trigger(trigger) , m_region(region) + , m_globalState(globalState) , m_sampleObject(region->sample()) { // Make sure the parent region's modulations are up to date - m_region->recalculateTotalCCModulation(globalState); + m_region->recalculateTotalCCModulation(*m_globalState); // Calculate the base pitch and amplitude so that we don't need to every buffer precomputeBaseValues(); // Delay the start of the playback by the trigger offset @@ -214,6 +215,13 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; + // Pitch difference due to pitchbending/microtuning. This is provided by the NotePlayHandle every buffer. The NPH might not have been the one which created this voice (SFZ is a midi based format and doesn't really work well with the NPH system of lmms), so we just get the freq of the last NPH on the same key as the event which triggered this voice. + const float notePlayHandleFreqRatio = m_globalState->nphKeyFreqRatio(m_trigger.key().value()); // TODO can this ever be 0.0f, like if the midi event is processed before the note play handle and it doesn't get set in time? I don't think so but I'm not 100% sure. + // Panning from the NotePlayHandle + const float notePlayHandlePanning = m_globalState->nphKeyPanning(m_trigger.key().value()); + const float nphPanningLeftAmp = std::min(1.0f, 1.0f - notePlayHandlePanning / 100.0f); + const float nphPanningRightAmp = std::min(1.0f, 1.0f + notePlayHandlePanning / 100.0f); + // Amplitude Envelope Parameters const f_cnt_t ampegDelayFrames = m_region->m_ampeg.delay * m_lmmsSampleRate; const f_cnt_t ampegAttackFrames = m_region->m_ampeg.attack * m_lmmsSampleRate; @@ -292,18 +300,18 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) if (filterEnabled) // TODO does this if statement make it faster? { - buffer[f][0] += m_filter.update(sampleLeftValue * m_baseAmplitudeLeft * ampeg * amplfo, 0); - buffer[f][1] += m_filter.update(sampleRightValue * m_baseAmplitudeRight * ampeg * amplfo, 1); + buffer[f][0] += m_filter.update(sampleLeftValue * m_baseAmplitudeLeft * nphPanningLeftAmp * ampeg * amplfo, 0); + buffer[f][1] += m_filter.update(sampleRightValue * m_baseAmplitudeRight * nphPanningRightAmp * ampeg * amplfo, 1); } else { - buffer[f][0] += sampleLeftValue * m_baseAmplitudeLeft * ampeg * amplfo; - buffer[f][1] += sampleRightValue * m_baseAmplitudeRight * ampeg * amplfo; + buffer[f][0] += sampleLeftValue * m_baseAmplitudeLeft * nphPanningLeftAmp * ampeg * amplfo; + buffer[f][1] += sampleRightValue * m_baseAmplitudeRight * nphPanningRightAmp * ampeg * amplfo; } // Increment the frame count. If we are using a sample, make sure to stop at the end, but if we are using a basic wave like *sine or *saw, there's no need if (m_frameCount >= 0) // Do not start playing the sample until we reach frame 0 (start of note), otherwise the sample frame will start moving even though it's not outputting any audio, causing the note to start partway through the sample (often with a discontinuity) { - const float frameIncrement = m_baseFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback + const float frameIncrement = m_baseFreqRatio * notePlayHandleFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback m_sampleFrame = m_sampleObject != nullptr ? std::min(static_cast(m_sampleObject->size()), m_sampleFrame + frameIncrement) : m_sampleFrame + frameIncrement; @@ -328,7 +336,7 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) } -void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger, SfzGlobalState& globalState) +void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger) { if (m_released) { return; } // If we already released, don't do anything @@ -346,7 +354,7 @@ void SfzRegionPlayState::processTrigger(const SfzTrigger& trigger, SfzGlobalStat // For simplicity, if any CC trigger occurs, recompute everything if (trigger.type() == SfzTrigger::Type::ControlChange) { - m_region->recalculateTotalCCModulation(globalState); + m_region->recalculateTotalCCModulation(*m_globalState); precomputeBaseValues(); } } diff --git a/plugins/SfzPlayer/SfzRegionPlayState.h b/plugins/SfzPlayer/SfzRegionPlayState.h index e6e8260feb8..19b8a8f7a9a 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.h +++ b/plugins/SfzPlayer/SfzRegionPlayState.h @@ -46,7 +46,7 @@ class SfzRegionPlayState { public: // I wish `region` could be const, but it's faster to have the voice update the region's cached modulation values than looping over all the regions elsewhere in the plugin, so for now it will have to be non-const. - SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trigger, const SfzGlobalState& globalState); + SfzRegionPlayState(SfzRegion* region, const SfzTrigger& trigger, const SfzGlobalState* globalState); SfzRegionPlayState() = default; // The default constructor is needed to initialize arrays of the object //! Helper function to calculate the base pitch and amplitude so that it doesn't have to be done per-buffer. @@ -60,7 +60,7 @@ class SfzRegionPlayState //! Handle incoming event to decide whether to deactivate/release //! Also handles updating the parent region's precomputed modulation values whenever a midi CC event occurs - void processTrigger(const SfzTrigger& trigger, SfzGlobalState& globalState); + void processTrigger(const SfzTrigger& trigger); //! Returns whether this voice is done playing or not. If m_active is false, this voice object is avaiable to be overwritten and used as a new voice. bool active() const { return m_active; } @@ -107,6 +107,11 @@ class SfzRegionPlayState //! The region this sound originated from SfzRegion* m_region = nullptr; + //! A pointer to the global sfz player state. This variable is needed since pitchbending/microtuning requires the frequency + //! of the key to be updated (potentially every frame), so we need some way to check the current frequency of the key this + //! voice originated from every buffer + const SfzGlobalState* m_globalState = nullptr; + //! The sample object of the parent region const SfzSampleBuffer* m_sampleObject = nullptr; }; From 5d9f5d454b1877e48ce00537b2a7cb0b1ede1940 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 29 May 2026 16:41:54 -0400 Subject: [PATCH 122/131] Use double instead of float to store sample frame index --- plugins/SfzPlayer/SfzRegionPlayState.cpp | 4 ++-- plugins/SfzPlayer/SfzRegionPlayState.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index 5dc78f6a6e2..c7460d7605a 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -311,9 +311,9 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // Increment the frame count. If we are using a sample, make sure to stop at the end, but if we are using a basic wave like *sine or *saw, there's no need if (m_frameCount >= 0) // Do not start playing the sample until we reach frame 0 (start of note), otherwise the sample frame will start moving even though it's not outputting any audio, causing the note to start partway through the sample (often with a discontinuity) { - const float frameIncrement = m_baseFreqRatio * notePlayHandleFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback + const double frameIncrement = m_baseFreqRatio * notePlayHandleFreqRatio * pitchmodFreqRatio; // Apply the pitch modulation by speeding up/slowing down the playback m_sampleFrame = m_sampleObject != nullptr - ? std::min(static_cast(m_sampleObject->size()), m_sampleFrame + frameIncrement) + ? std::min(static_cast(m_sampleObject->size()), m_sampleFrame + frameIncrement) : m_sampleFrame + frameIncrement; } m_frameCount++; diff --git a/plugins/SfzPlayer/SfzRegionPlayState.h b/plugins/SfzPlayer/SfzRegionPlayState.h index 19b8a8f7a9a..5587ca8d5a2 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.h +++ b/plugins/SfzPlayer/SfzRegionPlayState.h @@ -94,8 +94,8 @@ class SfzRegionPlayState //! The frame at which the note was released, relative to the start of the note int m_releaseFrame = 0; - //! Stores the current frame index being played in the region's sample. This is a float, since interpolation is done to change pitch/sample rate - float m_sampleFrame = 0; + //! Stores the current frame index being played in the region's sample. This is a decimal, since interpolation is done to change pitch/sample rate. Double is needed, since float did not provide enough precision and led to the pitch drifting up and down ever so slightly during playback. + double m_sampleFrame = 0.0; //! Some SFZ's utilize filters (e.g, a lowpass which changes cutoff depending on velocity), so each voice needs to have a filter object // TODO this assumes 2 channels--is that okay? From 2008e704cc85a8bbb33cd8ceb1bedffd7dc25cf3 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 31 May 2026 16:22:36 -0400 Subject: [PATCH 123/131] Share sample pool between instances --- plugins/SfzPlayer/SfzPlayer.cpp | 34 ++++++++++------- plugins/SfzPlayer/SfzPlayer.h | 20 +++++----- plugins/SfzPlayer/SfzPlayerView.cpp | 20 +++++----- plugins/SfzPlayer/SfzPlayerView.h | 1 + plugins/SfzPlayer/SfzRegion.cpp | 4 +- plugins/SfzPlayer/SfzRegion.h | 11 +++--- plugins/SfzPlayer/SfzSampleBuffer.h | 7 ++-- plugins/SfzPlayer/SfzSamplePool.cpp | 59 ++++++++++++++++++++++++++--- plugins/SfzPlayer/SfzSamplePool.h | 22 ++++++++++- 9 files changed, 126 insertions(+), 52 deletions(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 81e684b1ff3..cc35d0e63b7 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -66,6 +66,7 @@ PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* m, void*) SfzPlayer::SfzPlayer(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed) + , m_samplePool(SfzSamplePool::instance()) , m_parentTrack(instrumentTrack) { auto iph = new InstrumentPlayHandle(this, instrumentTrack); @@ -83,10 +84,9 @@ SfzPlayer::~SfzPlayer() // Make sure to end the sample loading thread before the plugin exits, or else the program might forcefully terminate if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } + // Deleting the region objects will also destroy their shared_ptrs to the sample objects, which will delete them if no other SfzPlayers are also using them. if (m_regionManager != nullptr) { delete m_regionManager; } // these may be nullptr at first when no SFZ file has been loaded previously - if (m_samplePool != nullptr) { delete m_samplePool; } if (m_tempRegionManager != nullptr) { delete m_tempRegionManager; } - if (m_tempSamplePool != nullptr) { delete m_tempSamplePool; } } @@ -280,9 +280,6 @@ void SfzPlayer::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: QDir parentDirectory = QFileInfo(filePath).absoluteDir(); - // Initialize a new sample pool for the new samples to be loaded in - m_tempSamplePool = new SfzSamplePool(); - // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active @@ -291,15 +288,23 @@ void SfzPlayer::loadSfzFile(const QString& filePath, const bool resetCCKnobs) void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) { - int i = 0; - int samplesFailedToLoad = 0; + int i = 0; // Count the number of regions + int samplesLoadedFromDisk = 0; // Count the number of samples which had to be loaded from disk + int samplesAlreadyInPool = 0; // Count the number of samples which had previously been loaded into the pool and didn't need to be loaded from disk + int samplesFailedToLoad = 0; // Count the number of samples which couldn't be loaded (invalid path, etc) for (auto* region : m_tempRegionManager->allRegions()) { // Update the GUI info text to notify the user as samples are loaded setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. - bool successfulLoadSample = region->initializeSample(parentDirectory, *m_tempSamplePool); - if (!successfulLoadSample) + bool sampleInPool = false; + bool successfulLoadSample = region->initializeSample(parentDirectory, *m_samplePool, &sampleInPool); + if (successfulLoadSample) + { + if (sampleInPool) { samplesAlreadyInPool++; } + else { samplesLoadedFromDisk++; } + } + else { samplesFailedToLoad++; setStatusInfo(QString("An error occured when loading sample %1").arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); @@ -308,11 +313,11 @@ void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) } if (samplesFailedToLoad == 0) { - setStatusInfo(QString("Loaded %1 regions and %2 samples.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount())); + setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.").arg(m_tempRegionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool)); } else { - setStatusInfo(QString("Loaded %1 regions and %2 samples.\nWARNING: Failed to load %3 samples, see logs for details.").arg(m_tempRegionManager->allRegions().size()).arg(m_tempSamplePool->sampleCount()).arg(samplesFailedToLoad)); + setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.\nWARNING: Failed to load %3 samples, see logs for details.").arg(m_tempRegionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool).arg(samplesFailedToLoad)); } // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data @@ -324,9 +329,8 @@ void SfzPlayer::audioThreadHandleNewSfzData() { if (m_newSfzDataReady) { - // Swap the temporary object pointers with the real ones + // Swap the temporary regions with the real ones std::swap(m_regionManager, m_tempRegionManager); - std::swap(m_samplePool, m_tempSamplePool); // And reset the active voices, since they have pointers to old region objects std::fill_n(m_voices.begin(), m_maxActiveIndex + 1, SfzRegionPlayState()); m_newSfzDataReady = false; @@ -340,8 +344,8 @@ void SfzPlayer::mainThreadUpdateAfterDataSwap() { m_justSwappedData = false; // Now that the audio thread has swapped the data, delete the old sample pool and region manager pointers + // Deleting the region objects will also delete the sample object shared_ptrs, so if no other SfzPlayers are using them, the samples willl be cleaned up. if (m_tempRegionManager != nullptr) { delete m_tempRegionManager; m_tempRegionManager = nullptr; } // these may be nullptr at first when no SFZ file has been loaded previously - if (m_tempSamplePool != nullptr) { delete m_tempSamplePool; m_tempSamplePool = nullptr; } // Also set the midi CC knobs to match the defaults, or whatever the current InstrumentTrack midi CC knobs are for (int i = 0; i < NumMidiCCs; ++i) { @@ -353,6 +357,8 @@ void SfzPlayer::mainThreadUpdateAfterDataSwap() // Sync the internal knobs to the LMMS knobs. TODO why does `setInitValue` not trigger this? processTrigger(SfzTrigger::controlChangeEvent(0, i, m_parentTrack->midiCCModel(i)->value())); // TODO there may be a cleaner way to do this } + // So that the GUI shows an accurate sample count, refresh the sample pool so that old pointers to samples are removed + m_samplePool->clearExpiredWeakPtrs(); // Update the GUI emit fileLoaded(); } diff --git a/plugins/SfzPlayer/SfzPlayer.h b/plugins/SfzPlayer/SfzPlayer.h index 0a5b0e30f11..c7a9dfe7486 100644 --- a/plugins/SfzPlayer/SfzPlayer.h +++ b/plugins/SfzPlayer/SfzPlayer.h @@ -36,7 +36,6 @@ #include "SfzRegionPlayState.h" #include "SfzGlobalState.h" #include "SfzControlsConfig.h" -#include "SfzSamplePool.h" #include "SfzRegionManager.h" #include @@ -87,15 +86,15 @@ class SfzPlayer : public Instrument //! Helper function to figure out what the maximum active index is, in the event the maximum index deactivated void recalculateMaxActiveIndex(); - //! So that the regions don't accidentally load the same sample multiple times, we store all the sames in one place and the regions ask it to load each sample/retrieve a pointer if it's already been loaded - SfzSamplePool* m_samplePool = nullptr; // TODO use unique pointer or something - //! Holds information about the total number of notes active, last switch keys pressed, etc SfzGlobalState m_sfzGlobalState; //! Holds information about midi CC default values, labels, which CC's are actually used, etc SfzControlsConfig m_controlsConfig; + //! Shared pointer of the current SfzSamplePool, shared across all instances of SfzPlayer. Holding this shared_ptr keeps the sample pool alive, since after the last SfzPlayer is deleted, the sample pool is deleted too. + std::shared_ptr m_samplePool; + //! Unfortunately, LMMS by default has the velocity of NoteOff events be 0. However, SFZ expects them to match the velocity of the corresponding NoteOn event. //! To account for this, we store the velocity of the last NoteOn event for each key, and use that value when hanlding NoteOff events. std::array m_previousNoteOnVelocity = {}; @@ -117,20 +116,19 @@ class SfzPlayer : public Instrument //! Unfortunately, dealing with multiple threads gets complicated. //! There is the possibility that the audio thread could access the region/sample data while the main thread is loading a new SFZ file and the samples - //! We need some way to swap out the current region/samples with the new data without breaking real-time safety. + //! We need some way to swap out the current regions with the new data without breaking real-time safety. //! To do this, essentially we have temporary buffers which the main thread works on while loading the files. When it's done, it sets a flag - //! which tells the audio thread that it can swap out the temporary objects for the real objects, and continue processing the audio. + //! which tells the audio thread that it can swap out the temporary pointer for the real pointer, and continue processing the audio. - //! When a new SFZ file is being loaded and the regions/samples need to be swapped out, the main thread sets this flag to let the - //! audio thread know to move the data from the temporary variables into the real region/sample stores. The audio thread will set this back to false when it has finished swapping + //! When a new SFZ file is being loaded and the regions need to be swapped out, the main thread sets this flag to let the + //! audio thread know to move the data from the temporary pointer variable into the real region manager pointer variable. The audio thread will set this back to false when it has finished swapping std::atomic m_newSfzDataReady = false; std::atomic m_justSwappedData = false; // And another flag so the main thread knows if it should be waiting for enough buffers to pass before loaidng another sfz file //! Also have a flag for whether the sample loading thread is active or not, so that we don't accidentally try to start up a new sample loading thread while samples are already being loaded. std::atomic m_currentlyLoadingSamples = false; - //! Temporary variables for the region and sample data, which the audio thread will swap with the real ones when m_newSfzDataReady is true + //! Temporary variable for the region data, which the audio thread will swap with the current pointer when m_newSfzDataReady is true SfzRegionManager* m_tempRegionManager = nullptr; - SfzSamplePool* m_tempSamplePool = nullptr; - //! Counts the number of buffers which have been processed. This is used for determining how long it has been since the audio thread has swapped out the region/sample data + //! Counts the number of buffers which have been processed. This is used for determining how long it has been since the audio thread has swapped out the region data //! when loading a new SFZ file (Thanks to Lost Robot for the idea) std::atomic m_bufferCounter = 0; //! The main thread will also save the buffer counter when m_newSfzDataReady was set so that it will know if enough buffers have passed that the audio thread can be guaranteed to have swapped the data already. diff --git a/plugins/SfzPlayer/SfzPlayerView.cpp b/plugins/SfzPlayer/SfzPlayerView.cpp index c3aac09213e..5d1ca9e86f6 100644 --- a/plugins/SfzPlayer/SfzPlayerView.cpp +++ b/plugins/SfzPlayer/SfzPlayerView.cpp @@ -40,6 +40,7 @@ #include "LcdSpinBox.h" #include "PixmapButton.h" #include "SfzPlayer.h" +#include "SfzSamplePool.h" #include "StringPairDrag.h" #include "Track.h" #include "embed.h" @@ -155,24 +156,25 @@ void SfzPlayerView::onFileLoaded() { m_switchKeysLabel->setText(""); } +} + +void SfzPlayerView::periodicUpdate() +{ + m_statusLabel->setText(m_instrument->m_statusText); - // Update general info - if (m_instrument->m_regionManager != nullptr && m_instrument->m_samplePool != nullptr) + // Update general info every frame, since when loading other SfzPlayers, the number of loaded samples changes. There might be a better way to do this. + if (m_instrument->m_regionManager != nullptr) { m_generalInfoLabel->setText( - QString("File: %1\nRegions: %2\nSamples: %3") + QString("File: %1\nRegions: %2\nSamples (across all instances): %3 (%4 MB)") .arg(QFileInfo(m_instrument->m_sfzFilePath).fileName()) .arg(m_instrument->m_regionManager->allRegions().size()) - .arg(m_instrument->m_samplePool->sampleCount()) + .arg(SfzSamplePool::instance()->sampleCount()) + .arg(QString::number(SfzSamplePool::instance()->sampleMemoryUsage() / 1000000.0f, 'g', 4)) ); } } -void SfzPlayerView::periodicUpdate() -{ - m_statusLabel->setText(m_instrument->m_statusText); -} - void SfzPlayerView::openFile() { diff --git a/plugins/SfzPlayer/SfzPlayerView.h b/plugins/SfzPlayer/SfzPlayerView.h index 3bff4c28b6a..d5ae7929a6b 100644 --- a/plugins/SfzPlayer/SfzPlayerView.h +++ b/plugins/SfzPlayer/SfzPlayerView.h @@ -66,6 +66,7 @@ public slots: QLabel* m_statusLabel; QLabel* m_generalInfoLabel; + QLabel* m_samplePoolLabel; QLabel* m_switchKeysLabel; QWidget* m_infoLabelsWidget; QWidget* m_controlsWidget; diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index 7bc6a805472..29e572d1e57 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -107,7 +107,7 @@ void SfzRegion::recalculateTotalCCModulation(const SfzGlobalState& globalState) } -bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool) +bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool, bool* sampleInPool) { if (m_sampleFile == std::nullopt) { @@ -132,7 +132,7 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value_or(""))); // TODO QString path = defaultDirectory.absoluteFilePath(m_sampleFile); // TODO // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory - m_sample = samplePool.loadSample(path); + m_sample = samplePool.loadSample(path, sampleInPool); // sampleInPool is passed so that we can tell the SfzPlayer if it actually needed to load it from disk or whether it was previously loaded and could be retrieved. return m_sample != nullptr; } diff --git a/plugins/SfzPlayer/SfzRegion.h b/plugins/SfzPlayer/SfzRegion.h index db7ce81e7d5..ebcec3ba2f1 100644 --- a/plugins/SfzPlayer/SfzRegion.h +++ b/plugins/SfzPlayer/SfzRegion.h @@ -52,19 +52,20 @@ class SfzRegion : public SfzOpcodeState //! Load the sample file given by the `sample` opcode into m_sample. //! The sample path is treated as relative to the path to the sfz file, so the parent directory is also needed + //! Sets `sampleInPool` to true if it was able to find the sample previously loaded in the sample pool, or false if it needed to load it from disk. //! Returns true if successful - bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool); + bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool, bool* sampleInPool); - //! Returns a shared pointer to the sample object for this region - const SfzSampleBuffer* sample() const { return m_sample; } + //! Returns a non-owning raw pointer to the sample object for this region + const SfzSampleBuffer* sample() const { return m_sample.get(); } //! Returns the type of basic wave for this region, if it specified instead of a real sample. //! If a sample was specified, this returns SfzBasicWaves::Shape::Silence by default. const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } private: - //! Pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first + //! Shared pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. - const SfzSampleBuffer* m_sample = nullptr; + std::shared_ptr m_sample; //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; diff --git a/plugins/SfzPlayer/SfzSampleBuffer.h b/plugins/SfzPlayer/SfzSampleBuffer.h index c75d3a2e87b..cb3bbf9f4b7 100644 --- a/plugins/SfzPlayer/SfzSampleBuffer.h +++ b/plugins/SfzPlayer/SfzSampleBuffer.h @@ -51,13 +51,14 @@ class SfzSampleBuffer //! Returns the sample rate of the audio float sampleRate() const { return m_sampleRate; } -private: static constexpr const size_t NUM_CHANNELS = 2; + +private: //! The raw sample data is stored as a raw pointer to an interleaved array of floats in (number_of_samples)x(channels) format. float* m_data = nullptr; - f_cnt_t m_size; - float m_sampleRate; + f_cnt_t m_size = 0; + float m_sampleRate = 0.0f; }; } // namespace lmms diff --git a/plugins/SfzPlayer/SfzSamplePool.cpp b/plugins/SfzPlayer/SfzSamplePool.cpp index 6868a948208..31565a475eb 100644 --- a/plugins/SfzPlayer/SfzSamplePool.cpp +++ b/plugins/SfzPlayer/SfzSamplePool.cpp @@ -28,25 +28,72 @@ namespace lmms { +// Default initialize the static shared pool pointer with no parameters, essentially like passing nullptr. s_instance.expired() will be true at the start. +std::weak_ptr SfzSamplePool::s_instance = std::weak_ptr(); -const SfzSampleBuffer* SfzSamplePool::loadSample(const QString& path) +const std::shared_ptr SfzSamplePool::instance() { - // If the sample has already been loaded before, just return a pointer to it - if (m_samplePool.contains(path)) + if (s_instance.expired()) { - return m_samplePool.at(path).get(); + // Return the shared_ptr to the SfzPlayer, but keep a weak_ptr copy so that we can make more shared_ptr copies to anyone who asks later, but it won't prevent it from being automatically deleted when all the SfzPlayers are destroyed. + std::shared_ptr inst = std::make_shared(); + s_instance = std::weak_ptr(inst); + return inst; + } + else + { + return std::shared_ptr(s_instance); + } +} + + +const int SfzSamplePool::sampleMemoryUsage() const +{ + int total = 0; + for (const auto& [key, value] : m_samplePool) + { + if (!value.expired()) + { + const int sizeOfSampleData = value.lock()->size() * SfzSampleBuffer::NUM_CHANNELS * sizeof(float); + const int sizeOfSampleBufferObject = sizeof(SfzSampleBuffer); + total += sizeOfSampleData + sizeOfSampleBufferObject; + } + } + return total; +} + + +const std::shared_ptr SfzSamplePool::loadSample(const QString& path, bool* sampleInPool) +{ + // If the sample has already been loaded before, return a shared pointer to it + if (m_samplePool.contains(path) && !m_samplePool.at(path).expired()) + { + *sampleInPool = true; + return m_samplePool.at(path).lock(); } else if (auto buffer = SampleBuffer::fromFile(path)) { - m_samplePool.insert({path, std::make_unique(buffer->data(), buffer->size(), buffer->sampleRate())}); - return m_samplePool.at(path).get(); + // Otherwise, load the sample file and create a shared_ptr. Store a weak_ptr in the sample pool so that it can be freely deleted when no more SfzPlayers are using it. + *sampleInPool = false; + auto sampleSharedPtr = std::make_shared(buffer->data(), buffer->size(), buffer->sampleRate()); + m_samplePool.insert({path, std::weak_ptr(sampleSharedPtr)}); + return sampleSharedPtr; } else { + *sampleInPool = false; return nullptr; } } +void SfzSamplePool::clearExpiredWeakPtrs() +{ + std::erase_if(m_samplePool, [](const auto& entry){ + const auto& [key, value] = entry; + return value.expired(); + }); +} + } // namespace lmms diff --git a/plugins/SfzPlayer/SfzSamplePool.h b/plugins/SfzPlayer/SfzSamplePool.h index b3e658f742c..686148c2793 100644 --- a/plugins/SfzPlayer/SfzSamplePool.h +++ b/plugins/SfzPlayer/SfzSamplePool.h @@ -36,13 +36,31 @@ namespace lmms class SfzSamplePool { public: - const SfzSampleBuffer* loadSample(const QString& path); + //! Returns a shared_ptr to the global sample pool instance, shared across all SfzPlayers. + //! If no sample pool has been created yet, or the previous one was deleted because all active SfzPlayer were closed, it will initialize a new one + static const std::shared_ptr instance(); + + //! Attempts to load the sample from the given path. It checks if it has already been loaded into the sample pool, otherwise it will load it from disk. + //! Sets `sampleInPool` to true if the sample was already loaded, false if not. + //! Returns a std::shard_ptr to the sample object if successful. Returns nullptr if it could not load the sample from disk. + const std::shared_ptr loadSample(const QString& path, bool* sampleInPool); //! Returns the number of samples currently loaded in the pool const int sampleCount() const { return m_samplePool.size(); } + //! Returns the total number of bytes used by the sample objects. Technically this might not be the total memory used, depending on padding or extra minor objects I missed, but it should be the bulk of the data. + const int sampleMemoryUsage() const; + + //! Removes any samples pointers from the buffer which no longer exist. Because the pool stores them as weak_ptrs, they may have been deleted when a previous SfzPlayer was deleted, but not removed from the pool. This method goes through and cleans it up so that only active samples remain. + void clearExpiredWeakPtrs(); + private: - std::map> m_samplePool; + //! Stores a list of std::weak_ptrs to the SfzSampleBuffer objects for each loaded sample. Weak pointers are used so that the pool can be shared among multiple instances of the SfzPlayer, each one's regions holding a std::shared_ptr, which when all destroyed, allow the object to be automatically deleted. + std::map> m_samplePool; + + //! Global pointer to the sample pool instance shared across all SfzPlayers. + //! This is a weak_ptr, but the individual SfzPlayers will use shared_ptrs of it, so that when the last SfzPlayer is deleted, the sample pool will also be deleted. + static std::weak_ptr s_instance; }; } // namespace lmms From e9aca61a680632cf9260f69839087252cff7e935 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:55:28 -0400 Subject: [PATCH 124/131] Load samples as notes are played --- plugins/SfzPlayer/SfzPlayer.cpp | 64 +++++++++++++++++++++++------ plugins/SfzPlayer/SfzPlayer.h | 6 ++- plugins/SfzPlayer/SfzPlayerView.cpp | 5 +++ plugins/SfzPlayer/SfzRegion.cpp | 15 ++++++- plugins/SfzPlayer/SfzRegion.h | 6 ++- 5 files changed, 79 insertions(+), 17 deletions(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index cc35d0e63b7..02028b5cc72 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -68,6 +68,7 @@ SfzPlayer::SfzPlayer(InstrumentTrack* instrumentTrack) : Instrument(instrumentTrack, &sfzplayer_plugin_descriptor, nullptr, Flag::IsSingleStreamed) , m_samplePool(SfzSamplePool::instance()) , m_parentTrack(instrumentTrack) + , m_preloadAllSamplesModel(false, this, tr("Preload All Samples")) { auto iph = new InstrumentPlayHandle(this, instrumentTrack); Engine::audioEngine()->addPlayHandle(iph); @@ -76,6 +77,9 @@ SfzPlayer::SfzPlayer(InstrumentTrack* instrumentTrack) // needs to delete the old data afterwards, so here we connect some kind of run-loop so that the main thread gets a chance to check after the audio thread acts connect(lmms::gui::getGUI()->mainWindow(), SIGNAL(periodicUpdate()), this, SLOT(mainThreadUpdateAfterDataSwap())); // I really don't like this. Would it be better to use our own QTimer, not LMMS's gui's? + // If the user enables preloading all samples, load all the samples immediately + connect(&m_preloadAllSamplesModel, &BoolModel::dataChanged, [&](){ if (m_preloadAllSamplesModel.value()) { preloadAllSamples(); }}); + emit dataChanged(); } @@ -138,12 +142,29 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) // Notify the global state to update which switch keys are active, update midi CC values, etc m_sfzGlobalState.processTrigger(trigger); + + bool preloadSamples = m_preloadAllSamplesModel.value(); // Cache this variable so that it isn't acessed every iteration for (auto* region : m_regionManager->findPotentialMatchingRegions(trigger)) { // If the trigger conditions are met, spawn a new sound if (region->triggerConditionsMet(m_sfzGlobalState, trigger)) { + // If samples are loaded when notes are pressed, check if this region has its sample loaded yet. If not, quickly load it before spawning the voice. + if (!preloadSamples && !region->sample()) + { + setStatusInfo(QString("Loading sample %1").arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); + bool sampleInPool = false; + bool successfulLoadSample = region->initializeSample(QFileInfo(m_sfzFilePath).absoluteDir(), *m_samplePool, &sampleInPool); + if (successfulLoadSample) + { + setStatusInfo((sampleInPool ? QString("Loaded sample %1 (already cached)") : QString("Loaded sample %1")).arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); + } + else + { + setStatusInfo(QString("An error occured when loading sample %1").arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); + } + } // Loop through array to find open position bool foundOpenPosition = false; for (size_t i = 0; i <= m_voices.size(); ++i) @@ -275,27 +296,35 @@ void SfzPlayer::loadSfzFile(const QString& filePath, const bool resetCCKnobs) // Don't immediately set m_regionManager, since the audio thread may still be accessing it; instead set the temporary object m_tempRegionManager = new SfzRegionManager(regions); - // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples - // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them - // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: - QDir parentDirectory = QFileInfo(filePath).absoluteDir(); + // Set the flag to let the audio thread know it can swap the data + m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data + m_newSfzDataReady = true; +} + +void SfzPlayer::preloadAllSamples() +{ + // (For some reason, when loading the settings of the "Preload All Samples" toggle, it sets the value, which triggers this function, so we need to check to make sure the regions have actually been initialized before continuing) + if (m_regionManager == nullptr) { return; } // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active - m_sampleLoadingThread = std::thread(&SfzPlayer::sampleLoadingThreadFunction, this, parentDirectory); + m_sampleLoadingThread = std::thread(&SfzPlayer::sampleLoadingThreadFunction, this); } -void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) +void SfzPlayer::sampleLoadingThreadFunction() { + // The samples are stored with relative paths with respect to the sfz file, so first find the parent directory: + QDir parentDirectory = QFileInfo(m_sfzFilePath).absoluteDir(); + int i = 0; // Count the number of regions int samplesLoadedFromDisk = 0; // Count the number of samples which had to be loaded from disk int samplesAlreadyInPool = 0; // Count the number of samples which had previously been loaded into the pool and didn't need to be loaded from disk int samplesFailedToLoad = 0; // Count the number of samples which couldn't be loaded (invalid path, etc) - for (auto* region : m_tempRegionManager->allRegions()) + for (auto* region : m_regionManager->allRegions()) { // Update the GUI info text to notify the user as samples are loaded - setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_tempRegionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); + setStatusInfo(QString("Loading sample %1/%2 %3").arg(i+1).arg(m_regionManager->allRegions().size()).arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); // Initialize the sample into the temporary pool, so that it doesn't disturb the audio thread which may still be using the previous samples. bool sampleInPool = false; bool successfulLoadSample = region->initializeSample(parentDirectory, *m_samplePool, &sampleInPool); @@ -313,15 +342,12 @@ void SfzPlayer::sampleLoadingThreadFunction(const QDir& parentDirectory) } if (samplesFailedToLoad == 0) { - setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.").arg(m_tempRegionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool)); + setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.").arg(m_regionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool)); } else { - setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.\nWARNING: Failed to load %3 samples, see logs for details.").arg(m_tempRegionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool).arg(samplesFailedToLoad)); + setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.\nWARNING: Failed to load %4 samples, see logs for details.").arg(m_tempRegionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool).arg(samplesFailedToLoad)); } - // When the thread is done loading all the samples, set the flag to let the audio thread know it can swap the data - m_bufferCounterWhenDataReady = m_bufferCounter; // Save the current frame counter so the main thread knows when enough buffers have passed that it can delete the old data - m_newSfzDataReady = true; m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? } @@ -361,6 +387,16 @@ void SfzPlayer::mainThreadUpdateAfterDataSwap() m_samplePool->clearExpiredWeakPtrs(); // Update the GUI emit fileLoaded(); + + // Now load the samples + // The SfzParser generates all the SfzRegion objects, but it doesn't load any of the samples + // The sample filenames are stored in the regions as from the `sample` opcode, so we just need to load the files into memory to use them + // If the user had enabled loading all the samples at once up front, do that. Otherwise, they will be loaded whenever they are needed, in SfzPlayer::processTrigger() + if (m_preloadAllSamplesModel.value()) + { + // This is fine to do while the audio thread is running, since the sample pointers in the region objects are atomic + preloadAllSamples(); + } } } @@ -368,11 +404,13 @@ void SfzPlayer::mainThreadUpdateAfterDataSwap() void SfzPlayer::saveSettings(QDomDocument& document, QDomElement& element) { element.setAttribute("sfzfile", m_sfzFilePath); + m_preloadAllSamplesModel.saveSettings(document, element, "preload_all_samples"); } void SfzPlayer::loadSettings(const QDomElement& element) { m_sfzFilePath = element.attribute("sfzfile"); + m_preloadAllSamplesModel.loadSettings(element, "preload_all_samples"); if (!m_sfzFilePath.isEmpty()) { loadSfzFile(m_sfzFilePath, false); // Passing false to leave the user-set midi CC knobs alone, since they were loaded by the InstrumentTrack and shouldn't be reset to the SFZ's defaults. diff --git a/plugins/SfzPlayer/SfzPlayer.h b/plugins/SfzPlayer/SfzPlayer.h index c7a9dfe7486..970f9b8a78c 100644 --- a/plugins/SfzPlayer/SfzPlayer.h +++ b/plugins/SfzPlayer/SfzPlayer.h @@ -104,6 +104,9 @@ class SfzPlayer : public Instrument //! The path to the currently loaded SFZ file QString m_sfzFilePath = ""; + //! Whether to load all the samples at once, or as the notes are played + BoolModel m_preloadAllSamplesModel; + //! Helper function for printing debug info/passing status info to the GUI void setStatusInfo(const QString& text); QString m_statusText = ""; @@ -112,7 +115,7 @@ class SfzPlayer : public Instrument //! Helper thread for loading sample files so that the main thread isn't blocked std::thread m_sampleLoadingThread; //! The function which the sample loading thread uses to load all the samples - void sampleLoadingThreadFunction(const QDir& parentDirectory); + void sampleLoadingThreadFunction(); //! Unfortunately, dealing with multiple threads gets complicated. //! There is the possibility that the audio thread could access the region/sample data while the main thread is loading a new SFZ file and the samples @@ -142,6 +145,7 @@ class SfzPlayer : public Instrument //! A helper function for the main thread to delete the old data and update things like the CC knobs after the audio thread has swapped the data. private slots: void mainThreadUpdateAfterDataSwap(); + void preloadAllSamples(); friend class gui::SfzPlayerView; }; diff --git a/plugins/SfzPlayer/SfzPlayerView.cpp b/plugins/SfzPlayer/SfzPlayerView.cpp index 5d1ca9e86f6..f81fdbb2220 100644 --- a/plugins/SfzPlayer/SfzPlayerView.cpp +++ b/plugins/SfzPlayer/SfzPlayerView.cpp @@ -38,6 +38,7 @@ #include "InstrumentView.h" #include "Knob.h" #include "LcdSpinBox.h" +#include "LedCheckBox.h" #include "PixmapButton.h" #include "SfzPlayer.h" #include "SfzSamplePool.h" @@ -85,6 +86,10 @@ SfzPlayerView::SfzPlayerView(SfzPlayer* instrument, QWidget* parent) layout1->addWidget(m_infoLabelsWidget); + LedCheckBox* preloadAllSamplesCheck = new LedCheckBox("Preload All Samples", this, tr("Preload All Samples"), LedCheckBox::LedColor::Green); + preloadAllSamplesCheck->setModel(&m_instrument->m_preloadAllSamplesModel); + layout1->addWidget(preloadAllSamplesCheck); + auto layout2 = new QHBoxLayout(m_infoLabelsWidget); layout2->setContentsMargins(0, 0, 0, 0); layout2->addWidget(m_generalInfoLabel); diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index 29e572d1e57..946ddfb7506 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -45,6 +45,19 @@ SfzRegion::SfzRegion(SfzOpcodeState opcodeState) } } + +SfzRegion::SfzRegion(const SfzRegion& other) + : SfzOpcodeState(other) +{ + // Since atomic variables cannot be copied, reinitialize it + m_sample.store(other.m_sample.load()); + // Copy all the other member variables as normal + m_basicWaveShape = other.m_basicWaveShape; + m_roundRobinCount = other.m_roundRobinCount; + m_lohiccDefinedCCNumbers = other.m_lohiccDefinedCCNumbers; +} + + bool SfzRegion::triggerConditionsMet(const SfzGlobalState& globalState, const SfzTrigger& trigger) { if (trigger.type() == SfzTrigger::Type::ControlChange) { return false; } // TODO. It is possible for midi CC's to trigger regions, such as using the sustain pedal or on_locc/on_hicc @@ -134,7 +147,7 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path, sampleInPool); // sampleInPool is passed so that we can tell the SfzPlayer if it actually needed to load it from disk or whether it was previously loaded and could be retrieved. - return m_sample != nullptr; + return m_sample.load() != nullptr; } return true; } diff --git a/plugins/SfzPlayer/SfzRegion.h b/plugins/SfzPlayer/SfzRegion.h index ebcec3ba2f1..9763290a380 100644 --- a/plugins/SfzPlayer/SfzRegion.h +++ b/plugins/SfzPlayer/SfzRegion.h @@ -42,6 +42,7 @@ class SfzRegion : public SfzOpcodeState { public: SfzRegion(SfzOpcodeState opcodeState); + SfzRegion(const SfzRegion& other); // A custom copy constructor is needed because atomic member variables have no default copy/move constructor //! Returns true if the trigger event matches all of the requirements defined by the opcodes of this region //! For example, if lokey=24 and hikey=29, and the trigger is a NoteOn event on key 26, then it will return true @@ -57,7 +58,7 @@ class SfzRegion : public SfzOpcodeState bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool, bool* sampleInPool); //! Returns a non-owning raw pointer to the sample object for this region - const SfzSampleBuffer* sample() const { return m_sample.get(); } + const SfzSampleBuffer* sample() const { return m_sample.load().get(); } //! Returns the type of basic wave for this region, if it specified instead of a real sample. //! If a sample was specified, this returns SfzBasicWaves::Shape::Silence by default. const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } @@ -65,7 +66,8 @@ class SfzRegion : public SfzOpcodeState private: //! Shared pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. - std::shared_ptr m_sample; + //! This is an atomic variable, since the user may have enabled the option to load samples as they play the notes, which means the sample pointer needs to be changed while the instrument is playing, which could potentially have issues with the different threads acessing the same pointer at once. + std::atomic> m_sample; //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; From be352466a81e0b8c725f5f0d94f58f20988ead5a Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:11:38 -0400 Subject: [PATCH 125/131] Handle missing samples better --- plugins/SfzPlayer/SfzPlayer.cpp | 3 ++- plugins/SfzPlayer/SfzRegion.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 02028b5cc72..8301d972206 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -163,6 +163,7 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) else { setStatusInfo(QString("An error occured when loading sample %1").arg(QFileInfo(region->m_sampleFile.value_or("N/A")).fileName())); + continue; } } // Loop through array to find open position @@ -346,7 +347,7 @@ void SfzPlayer::sampleLoadingThreadFunction() } else { - setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.\nWARNING: Failed to load %4 samples, see logs for details.").arg(m_tempRegionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool).arg(samplesFailedToLoad)); + setStatusInfo(QString("Initialized %1 regions.\nLoaded %2 samples from disk.\nRetrieved %3 from sample pool.\nWARNING: Failed to load %4 samples, see logs for details.").arg(m_regionManager->allRegions().size()).arg(samplesLoadedFromDisk).arg(samplesAlreadyInPool).arg(samplesFailedToLoad)); } m_currentlyLoadingSamples = false; // TODO this doesn't seem thread safe, since there would be a brief moment in time where this is false but the thread is still active? } diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index 946ddfb7506..b4d6b03fb86 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -144,6 +144,8 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam // If it's not a basic wave, load the real sample file. QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value_or(""))); // TODO QString path = defaultDirectory.absoluteFilePath(m_sampleFile); // TODO + // Check if the file exists before calling the LMMS function to load it, since that will spawn a popup window if it can't load it, which could be hundreds of popups if its a large sfz + if (!QFile::exists(path)) { return false; } // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path, sampleInPool); // sampleInPool is passed so that we can tell the SfzPlayer if it actually needed to load it from disk or whether it was previously loaded and could be retrieved. From 3af7dec50b9a43af6412e1fca1305c6443b92ba9 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:44:54 -0400 Subject: [PATCH 126/131] Fix some builds not supporting std::atomic --- plugins/SfzPlayer/SfzRegion.cpp | 5 +++-- plugins/SfzPlayer/SfzRegion.h | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index b4d6b03fb86..af166a3060d 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -50,7 +50,7 @@ SfzRegion::SfzRegion(const SfzRegion& other) : SfzOpcodeState(other) { // Since atomic variables cannot be copied, reinitialize it - m_sample.store(other.m_sample.load()); + m_samplePointer.store(other.m_samplePointer.load()); // Copy all the other member variables as normal m_basicWaveShape = other.m_basicWaveShape; m_roundRobinCount = other.m_roundRobinCount; @@ -148,8 +148,9 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam if (!QFile::exists(path)) { return false; } // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory m_sample = samplePool.loadSample(path, sampleInPool); // sampleInPool is passed so that we can tell the SfzPlayer if it actually needed to load it from disk or whether it was previously loaded and could be retrieved. + m_samplePointer = m_sample.get(); // Unfortunately, not all the builds support std::atomic, so we use both a shared_ptr (m_sample) and an atomic raw pointer (m_samplePointer). - return m_sample.load() != nullptr; + return m_samplePointer != nullptr; } return true; } diff --git a/plugins/SfzPlayer/SfzRegion.h b/plugins/SfzPlayer/SfzRegion.h index 9763290a380..80578b3d1d1 100644 --- a/plugins/SfzPlayer/SfzRegion.h +++ b/plugins/SfzPlayer/SfzRegion.h @@ -58,7 +58,7 @@ class SfzRegion : public SfzOpcodeState bool initializeSample(const QDir& parentDirectory, SfzSamplePool& samplePool, bool* sampleInPool); //! Returns a non-owning raw pointer to the sample object for this region - const SfzSampleBuffer* sample() const { return m_sample.load().get(); } + const SfzSampleBuffer* sample() const { return m_samplePointer; } //! Returns the type of basic wave for this region, if it specified instead of a real sample. //! If a sample was specified, this returns SfzBasicWaves::Shape::Silence by default. const SfzBasicWaves::Shape basicWaveShape() const { return m_basicWaveShape; } @@ -67,7 +67,8 @@ class SfzRegion : public SfzOpcodeState //! Shared pointer to sample object to be played. The sample file path is defined in the `sample` opcode, but the data needs to be loaded first //! The actual sample objects are stored in a shared pool, SfzSamplePool, so that if multiple of the same sample are loaded, they don't waste memory. //! This is an atomic variable, since the user may have enabled the option to load samples as they play the notes, which means the sample pointer needs to be changed while the instrument is playing, which could potentially have issues with the different threads acessing the same pointer at once. - std::atomic> m_sample; + std::atomic m_samplePointer = nullptr; + std::shared_ptr m_sample; // Unfortunately, not all the builds support std::atomic, so as a workaround, the shared pointer exists so that the sample garbage collection works, but the actual audio processing uses the raw pointer which is atomic. //! However, if a basic wave keyword such as *sine, *saw, *triangle, etc is used, handle it separately (see SfzBasicWaves.h/.cpp) SfzBasicWaves::Shape m_basicWaveShape = SfzBasicWaves::Shape::Silence; From dc4403803efc34bf7adecf7a97394bc47a77688b Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:33:23 -0400 Subject: [PATCH 127/131] Fix issue with path formatting when checking if file exists --- plugins/SfzPlayer/SfzRegion.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/SfzPlayer/SfzRegion.cpp b/plugins/SfzPlayer/SfzRegion.cpp index af166a3060d..3a9a0c169ad 100644 --- a/plugins/SfzPlayer/SfzRegion.cpp +++ b/plugins/SfzPlayer/SfzRegion.cpp @@ -26,6 +26,7 @@ #include "Engine.h" #include "AudioEngine.h" +#include "PathUtil.h" #include @@ -144,6 +145,8 @@ bool SfzRegion::initializeSample(const QDir& parentDirectory, SfzSamplePool& sam // If it's not a basic wave, load the real sample file. QDir defaultDirectory = QDir(parentDirectory.absoluteFilePath(m_default_path.value_or(""))); // TODO QString path = defaultDirectory.absoluteFilePath(m_sampleFile); // TODO + // Format the path properly before checking if it exists; this can cause issues when mixing ".." and "\" and "/" and everything + path = PathUtil::toAbsolute(path); // Check if the file exists before calling the LMMS function to load it, since that will spawn a popup window if it can't load it, which could be hundreds of popups if its a large sfz if (!QFile::exists(path)) { return false; } // The sample pool handles making sure the same sample isn't loaded twice, which would waste memory From 6bd05abc9ed19201bfdf746156ba2e549aa5423f Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:03:43 -0400 Subject: [PATCH 128/131] Use ThreadPool instead of std::thread --- plugins/SfzPlayer/SfzPlayer.cpp | 7 ++++--- plugins/SfzPlayer/SfzPlayer.h | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 8301d972206..1d89073b372 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -35,6 +35,7 @@ #include "ConfigManager.h" #include "SfzPlayerView.h" #include "Song.h" +#include "ThreadPool.h" #include "embed.h" #include "interpolation.h" #include "plugin_export.h" @@ -86,7 +87,7 @@ SfzPlayer::SfzPlayer(InstrumentTrack* instrumentTrack) SfzPlayer::~SfzPlayer() { // Make sure to end the sample loading thread before the plugin exits, or else the program might forcefully terminate - if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } + if (m_sampleLoadingTask.valid()) { m_sampleLoadingTask.get(); } // Deleting the region objects will also destroy their shared_ptrs to the sample objects, which will delete them if no other SfzPlayers are also using them. if (m_regionManager != nullptr) { delete m_regionManager; } // these may be nullptr at first when no SFZ file has been loaded previously @@ -309,8 +310,8 @@ void SfzPlayer::preloadAllSamples() if (m_regionManager == nullptr) { return; } // To prevent the main gui thread from freezing while all the samples are loaded, start up a separate thread to handle loading everything m_currentlyLoadingSamples = true; - if (m_sampleLoadingThread.joinable()) { m_sampleLoadingThread.join(); } // Reset the previous thread if it is active - m_sampleLoadingThread = std::thread(&SfzPlayer::sampleLoadingThreadFunction, this); + if (m_sampleLoadingTask.valid()) { m_sampleLoadingTask.get(); } // Reset the previous thread if it is active + m_sampleLoadingTask = ThreadPool::instance().enqueue(&SfzPlayer::sampleLoadingThreadFunction, this); } void SfzPlayer::sampleLoadingThreadFunction() diff --git a/plugins/SfzPlayer/SfzPlayer.h b/plugins/SfzPlayer/SfzPlayer.h index 970f9b8a78c..14e57eaa818 100644 --- a/plugins/SfzPlayer/SfzPlayer.h +++ b/plugins/SfzPlayer/SfzPlayer.h @@ -38,8 +38,6 @@ #include "SfzControlsConfig.h" #include "SfzRegionManager.h" -#include - namespace lmms { class SfzPlayer : public Instrument @@ -112,8 +110,8 @@ class SfzPlayer : public Instrument QString m_statusText = ""; - //! Helper thread for loading sample files so that the main thread isn't blocked - std::thread m_sampleLoadingThread; + //! Handle for helper thread task for loading sample files so that the main thread isn't blocked. This is done using ThreadPool::instance().enqueue(), which returns std::future, which is stored here + std::future m_sampleLoadingTask; //! The function which the sample loading thread uses to load all the samples void sampleLoadingThreadFunction(); From 02b4f813bb85e346e50237159d38bfda304d0954 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:23:31 -0400 Subject: [PATCH 129/131] Fix bugs when playing notes while loading samples --- plugins/SfzPlayer/SfzPlayer.cpp | 2 ++ plugins/SfzPlayer/SfzRegionPlayState.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/plugins/SfzPlayer/SfzPlayer.cpp b/plugins/SfzPlayer/SfzPlayer.cpp index 1d89073b372..d89b52a6aa8 100644 --- a/plugins/SfzPlayer/SfzPlayer.cpp +++ b/plugins/SfzPlayer/SfzPlayer.cpp @@ -141,6 +141,8 @@ void SfzPlayer::processTrigger(const SfzTrigger& trigger) { if (m_regionManager == nullptr) { return; } // Before any SFZ file is loaded, m_regionManager is nullptr, so there's no regions to trigger. + if (m_currentlyLoadingSamples) { return; } // There are some issues with setStatusInfo being called from multiple threads at once, so just to be safe, don't handle any note events while the samples thread is running. + // Notify the global state to update which switch keys are active, update midi CC values, etc m_sfzGlobalState.processTrigger(trigger); diff --git a/plugins/SfzPlayer/SfzRegionPlayState.cpp b/plugins/SfzPlayer/SfzRegionPlayState.cpp index c7460d7605a..04b7343e477 100644 --- a/plugins/SfzPlayer/SfzRegionPlayState.cpp +++ b/plugins/SfzPlayer/SfzRegionPlayState.cpp @@ -212,6 +212,10 @@ bool SfzRegionPlayState::play(SampleFrame* buffer, const f_cnt_t frames) // If the initial m_frameCount is negative, that means the note hasn't started yet if (m_frameCount < -static_cast(frames)) { m_frameCount += frames; return false; } // If the note doesn't start in this buffer, don't play anything + // If no sample has been loaded yet (and no basic wave was specified), don't play anything, and end the note because no sound will ever come. + // This caused an odd bug where spamming keys which had NoteOff samples while the samples were being loaded would make the voices last forever (playing silence, but filling up the active voice array). + if (m_sampleObject == nullptr && m_region->basicWaveShape() == SfzBasicWaves::Shape::Silence) { m_active = false; return false; } + // Helper variable const float normalizedVelocity = m_trigger.velocity().value() / 127.0f; From a69ace0ef224123705c5af91a5b2fe6e9363ce20 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:50:11 -0400 Subject: [PATCH 130/131] Fix build error --- plugins/SfzPlayer/SfzPlayer.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/SfzPlayer/SfzPlayer.h b/plugins/SfzPlayer/SfzPlayer.h index 14e57eaa818..787a788ce36 100644 --- a/plugins/SfzPlayer/SfzPlayer.h +++ b/plugins/SfzPlayer/SfzPlayer.h @@ -38,6 +38,8 @@ #include "SfzControlsConfig.h" #include "SfzRegionManager.h" +#include + namespace lmms { class SfzPlayer : public Instrument From a6879171bdcc6d897c933b10a00dbae107305f98 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:35:16 -0400 Subject: [PATCH 131/131] Make ThreadPool LMMS_EXPORT --- include/ThreadPool.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/ThreadPool.h b/include/ThreadPool.h index ad4d798505b..18ef35f5acb 100644 --- a/include/ThreadPool.h +++ b/include/ThreadPool.h @@ -25,6 +25,8 @@ #ifndef LMMS_THREAD_POOL_H #define LMMS_THREAD_POOL_H +#include "lmms_export.h" + #include #include #include @@ -38,7 +40,7 @@ namespace lmms { //! A thread pool that can be used for asynchronous processing. -class ThreadPool +class LMMS_EXPORT ThreadPool { public: //! Destroys the `ThreadPool` object.