Add Native SFZ Player#8391
Conversation
|
In case people immediately want an executable without taking the hassle to build it: https://lmms.io/download/pull-request/8391 My feedback: First, specifically the plugin window's built-in keyboard doesn't seem to play all the velocity samples, as I've tested with VCSL Keys and tried to click the keys from top to bottom area. Velocity samples themselves work fine, however. (e.g. in piano rolls) Second, even though velocity-tracked cutoff is mentioned in the supported opcode list, it doesn't seem to be implemented, as I've tested with Metal-GTX. |
|
Thank you for testing!
Yes, this is an issue with LMMS's instrument window piano widget; playing at the very bottom only gives half the maximum velocity. You can sort of fix this by changing the
Thanks for catching this! It turns out I was incorrectly adding instead of multiplying when applying I tried it out, and wow, the extra expression you get from the velocity is awesome. 2026-05-16.19-16-05.mp4I hope you enjoy! |
Alright, I made the issue #8392. I did make sure to search for previous issue threads about it and I couldn't seem to find any. |
|
this is peak. I am so excited to get this in the future versions |
|
The SFZ player now supports pitch bending, microtuning, and per-note panning! 2026-05-27.17-36-56.mp4 |
|
So I just tested this and here are my findings :
I tried uploading videos but couldn't, because of the horrendous upload speed of GitHub, so all you get is audio About the CC64 knob tested with a piano (but that also counts for anything that uses it) : Things that should be added either here or in a follow-up PR (but most likely here because we're in a feature-freeze) :
|
|
Thank you for testing!
(After further correspondence on the discord, it appears this issue was due to the pitch of the notes drifting up and down ever so slightly. This turned out to be due to My understanding is that when playing a simple interval/power chord with two notes, the harmonics often line up. But when the pitch of a note starts to drift, it creates a warble/beat effect, making the total amplitude of the audio pulse up and down, in proportion to the frequency difference of the notes. When using a non-linear effect such as a guitar amp which heavily relies on the instantaneous amplitude of the signal, this pulsing has a much more pronounced effect, completely altering the resulting sound.) As for the frequencies around 50-200 Hz, I am able to reproduce them, but they also seem to appear when loading the raw sample files into sample tracks and playing them, which makes me think it might be an issue outside of the SFZ player. I'm not sure.
Unfortunately, LMMS handles the midi events, and when CC64 > half, it does not send NoteOff events. This would be very straightforward to change; perhaps an option could be added in the midi tab of instruments to toggle the behavior. It might be out of scope for this PR, though.
This sounds like a good option to have. I am still thinking of how to best implement this; it would likely require a bit of refactoring of how samples are currently loaded.
This was also on my mind. I will have to experiment whether a |
|
The SFZ Player now caches samples between instances in a shared sample pool! 2026-05-31.16-26-06.mp4 |
|
The SFZ player now supports loading samples one by one when you play the notes, instead of loading all of them at once when you open the instrument. 2026-06-03.18-01-10.mp4This feature is currently the default. (Is that good or bad? Let me know!) You can forcefully load all the samples by enabling the "Preload All Samples" option. This setting is saved with the instrument, so it will remember your choice the next time you open the project. Also, when loading an SFZ which has invalid sample paths (such as trying to load an include file instead of the real instrument), instead of showing a hundred popup windows or crashing LMMS, it now gracefully handles it and simply prints a warning message. |
| std::atomic<const SfzSampleBuffer*> m_samplePointer = nullptr; | ||
| std::shared_ptr<const SfzSampleBuffer> m_sample; // Unfortunately, not all the builds support std::atomic<std::shared_ptr>, 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. |
There was a problem hiding this comment.
So when the GUI thread wants to update the data, the GUI thread just updates the std::shared_ptr from under it since the audio thread uses a raw pointer reference?
There was a problem hiding this comment.
Yes, the main thread is able to directly update the std::shared_ptr without locking or anything since the audio thread doesn't access it (it only uses the atomic raw pointer from SfzRegion::sample()). The only real purpose of the shared_ptr is so that the sample gets automatically deleted from the sample pool when all the active SFZ players using it are closed.
There was a problem hiding this comment.
I don't think that thread safe, in the event that the audio thread is using the structure while it is being updated.
There was a problem hiding this comment.
Perhaps I am misunderstanding; The audio thread never accesses the shared pointer, so the main thread can update it whenever it wants without any risk, I believe(?)
There was a problem hiding this comment.
Hmm, its either a thread-safety issue, or a memory leak. I thought about it some more and this might just be a memory leak..
The shared pointer holds a pointer to the heap allocated object. In your code, you also have a raw atomic pointer pointing to the same object (via std::shared_ptr::get). I'm assuming you aren't actually modifying that object (if you were, its most likely not thread-safe), but just replacing the shared pointer with a new structure. In that case, that means the audio thread could still be using the old structure, and it would have the only reference to it. It would have to delete it (not real-time safe), or just fetch the new pointer from the atomic variable after its done using the old one (memory leak).
Edit: This might've been slightly inaccurate. I dont think theres a memory leak here, because the deletion does happen as you are replacing the structure on the GUI thread. But if that deletion happens while the audio thread is using it, that's a use-after-free bug and will most likely crash the application.
Me personally, I would just use the requestChangesGuard that we have. The proper thread-safe and real-time solution would need deferred reclamation as well (via some lock-free queue probably), not just atomic updates.
PS: I also call the GUI thread the main thread sometimes.
There was a problem hiding this comment.
Wait... scratch that.. when you replace the shared pointer on the main thread, it will delete the structure (since the main thread is the only one with the shared reference, the ref count will drop to zero and it will call delete on whatever pointer it is storing). Deleting that structure while the audio thread still has the raw pointer reference is not thread-safe.
So I think best case scenario, you dont crash and its deleted safely (the audio thread was in a convenient time window as you were deleting it), worst case scenario is that you have a segmentation fault, as far as I understand it.
There was a problem hiding this comment.
I see what you mean now. You're right, if the sample is deleted while the audio thread is processing, it could be using the old raw pointer which now points to a deleted object.
Currently, the only time the shared pointer is modified is when the sample is initially loaded, when it changes from nullptr to the sample pointer. Once the sample is initialized, it is never changed.
(If the user decides to load a new SFZ file, it goes through a separate system which creates a whole new SfzRegionManager which contains a vector of SfzRegions, and that gets swapped with the old one when the audio thread is ready and a couple of extra buffers have passed. But in that case, the audio thread is guaranteed to not be currently accessing it, so by the time the SfzRegion is deleted and the shared pointer is deleted, the atomic raw pointer is no longer being used.)
There was a problem hiding this comment.
(If the user decides to load a new SFZ file, it goes through a separate system which creates a whole new SfzRegionManager which contains a vector of SfzRegions, and that gets swapped with the old one when the audio thread is ready and a couple of extra buffers have passed. But in that case, the audio thread is guaranteed to not be currently accessing it, so by the time the SfzRegion is deleted and the shared pointer is deleted, the atomic raw pointer is no longer being used.)
Each note that is played with the instrument can run on any number of audio threads, not just one. This might work if there was only one audio thread, but I believe you can have multiple audio threads processing each individual note that is currently being played, and any of them may have a reference. The deletion should only happen when no audio threads are currently using it.
I believe @rubiefawn handled this in the Lb302 refactor. They might have some thoughts to share about this here.
Edit: I looked at the code... and I think you intended to handle the problem I just mentioned (correct me if I'm wrong)... but I cant verify if its entirely correct.
There was a problem hiding this comment.
I believe you can have multiple audio threads processing each individual note that is currently being played
That may be true for NotePlayHandle-based instruments, but here the instrument is single-streamed, so I think it should be fine. Unless the InstrumentPlayHandle can move process multiple buffers at once on different threads (but I would doubt that because things like filters can't be processed on multiple buffers at once without have separate internal states idk).
This PR adds a native SFZ instrument loader.
2026-05-15.18-10-15.mp4
It also supports pitch bending, microtuning, and per-note panning:
2026-05-27.17-36-56.mp4
What is SFZ?
SFZ is not SoundFont. SFZ is a modern, text-based, sampled-instrument format which provides incredible amounts of control to the creator and user. By its text-based nature, it is fundamentally open-source.
Countless high-quality SFZ instruments have been created by individuals. Sonatina Symphonic Orchestra is a large collection of orchestral SFZs, from strings, to brass, to wind, to percussion. Metal GTX is a famous electric guitar SFZ, one of the most realistic ones you can get for free. This PR allows you to load these directly into LMMS, without any third-party plugins.
Please check out the information at sfzformat.com if you are unfamiliar with the SFZ format.
Nonetheless, here is a brief overview:
An SFZ file essentially defines which samples are played by which keys (or ranges of keys), in what situations they are played (whether there are conditions on velocity, switch keys, current CC values, etc), and how they are played (what kind of envelopes/lfos, filters, etc).
NOTE: The samples are NOT stored in the .sfz file. The .sfz file contains paths to the samples, which are often stored in a separate folder in .wav or .flac. When you download an SFZ instrument, you usually download a .zip folder which contains both the .sfz file and the audio files.
Example sfz file:
This instrument has two samples, one for low velocities, one for high. These individual settings are call “opcodes”, and there are many many many opcodes supported by various SFZ players (this PR only contains a small subset). There are also headers, which group the opcodes together. Only the
<region>header is used here, but other headers for different purposes exist such as<group>,<global>,<control>, etc.Opcodes
There are many opcodes used by SFZs, too many to fit into this initial PR. However, I have implemented the vast majority of opcodes used by SFZs, so most of them should load fine.
Supported Opcodes
GUI
The GUI is work in progress! This PR is intended to give the core support for loading SFZ files, while the visuals are secondary. In the future, a real GUI is planned.
Currently, only some basic information is shown about the currently loaded SFZ file, the last played sample, and the details of the switch keys. The midi CC knobs which the instrument uses are also shown in the GUI for easy access (they control the models in the instrument track’s midi CC rack).
Testing
Please feel free to test it on whatever SFZ’s you find! If the SFZ makes heavy use of ARIA extenions/extra opcodes, it may not function as well, but it might still work. The SFZ player will print an enormous amount of debug info into the terminal, which is helpful when determining what opcodes still need to be implemented or tracking down bugs.