-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Auto-quit rework #8070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Auto-quit rework #8070
Conversation
|
To clarify, by track channels do you mean |
|
@sakertooth The multi-channel plugins PR lays the groundwork for users to be able to increase the number of track channels within an instrument / mixer channel from 2 (a single stereo pair) to a maximum of 256 (arbitrarily chosen), similar to REAPER. This PR also lays some of that groundwork, but still keeping the number of track channels fixed at 2. |
|
I should also note that I'm unsure of the proper terminology to use, so I may change some things. According to ChatGPT, a "bus" is not the right term for a single group of track channels or the overall collection of channels, since a "bus" usually refers to a "routing destination or summing path". I'm thinking "(track) channel group" might be a good channel-count-agnostic alternative to "stereo pair" or "track channel bus". And as for the class |
So if I understand correctly, a "track channel" is just an audio channel, like a left and right channel?
I don't think we should keep the number of track channels fixed at 2 in the code:
This is another reason why I suggest also using a planar format. As a matter of fact, I would probably recommend that
Those are my current thoughts. Feel free to correct me on anything you believe I am misunderstanding. I really do want to understand your perspective and work. |
Yes, though it refers to a single audio channel not two channels (left and right) together. I'm using the term "track channel pair" to refer to two track channels that form a single stereo channel.
I think you're misunderstanding me. When I said it's "fixed at 2" channels, I mean we're still using 1 pair of channels and there's no way to change that at runtime: SampleFrame* trackChannelPair = getTrackChannelPair();
auto bus = AudioBus{&trackChannelPair, 1, frames};In the future, it might look something more like this: SampleFrame** trackChannelPairs = getTrackChannelPairs();
auto bus = AudioBus{trackChannelPairs, numTrackChannelPairs, frames};As you can see, Btw, I'm considering making
That would be a lot more work and beyond the scope of this PR. It would probably also cause performance issues until we fully convert over to planar. |
Glad we are on the same page. I understood that part.
Maybe I was not clear on my end. So in an ideal scenario (disregard reality for a minute), would you agree that I don't see a strong reason for having this "track channel" and "track channel pair" distinction. IMO, design your class interfaces for what you actually want, and not for what you have to implement because of certain requirements of the problem at hand. We don't want to have to change each call site when we later want to make
I understand your concern, PS: Is Edit: I also actually think it's more of a performance hazard not using planar here though. It's not like keeping things interleaved is going to be optimal for performance. My guess is that you are striding inside your buffers to work with individual channels anyways (prevents SIMD optimizations). That's still a performance cost/downgrade, and a planar format is quite frankly the easiest, most intuitive way to think of The only potential performance cost I can imagine using planar for Edit 2:
I take that back. Converting interleaved to planar will always result in some striding somewhere, but at least it is just copies and not any more complicated processing you eventually want to do for individual channels. Then again, we would need to profile to see which is better. |
|
I'll try and review this at some point and maybe formalize any concerns I have a bit more pragmatically. |
sakertooth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we aren't using planar yet and do not want to convert to/from it on the fly, I don't think we are in the position to have something like AudioBus as of right now, especially since there are disagreements that will require more discussion/work before we do something like that. Is AudioBus truly a requirement for your auto quit rework?
|
@sakertooth Sorry for the late response.
Yes, eventually
Yeah, it's starting to look like we should design this with first class support for dynamically-sized groups of channels, and squashing all the track channels into a single array of What do you think of something like this? inline constexpr track_ch_t MaxTrackChannelsPerGroup = 32;
inline constexpr unsigned int MaxTrackChannelGroups = 256;
class AudioBus
{
public:
// ...
class ChannelGroup
{
public:
// getters/setters here
private:
std::unique_ptr<float[]> m_buffer; // interleaved (at least for now)
track_ch_t m_channels; // # of channels in `m_buffer` (`MaxTrackChannelsPerGroup` maximum) - currently only 2 is used
std::bitset<MaxTrackChannelsPerGroup> m_quietChannels;
};
auto channelGroup(unsigned int index) const -> const ChannelGroup& { return m_groups.at(index); }
private:
ArrayVector<ChannelGroup, MaxTrackChannelGroups> m_groups;
f_cnt_t m_frames;
bool m_silenceTrackingEnabled = false;
};As you can see, this uses groups of a dynamic number of channels rather than groups of exactly 2 channels each.
Yes, it's a collection of audio buffers which keeps track of silent channels. I'm definitely open to changing the name.
When operating on individual channels like this class requires, it's true that interleaved buffers do not seem ideal. Though if both channels are being operated on at once (which is by far the most common situation, since L is usually mapped to L, R to R, etc.), there is no striding where we skip over every N samples, so it shouldn't be less performant. I'd imagine the cost of frequent conversions between interleaved and planar to be far more than some striding when iterating over an interleaved buffer, though I haven't measured this. I'd be willing to attempt that conversion from interleaved to planar in this PR, though it will probably be a lot of work, and we should probably figure out the channel grouping stuff first.
I can attempt to convert between planar and interleaved on the fly, though even if it doesn't work out, I don't think using interleaved should block this PR from being merged since it's what we're using already and doesn't prevent us from switching to planar in the future any more than adding a new instrument/effect plugin with interleaved processing does.
Yes, The current auto-quit system is probably usable even if multi-channel plugins and audio routing are added, though it's not suited for it and would probably be less efficient than it could be. For example, when auto-quit is enabled and plugins are put to sleep, under the current auto-quit system, all sleeping plugins would need to zero their output buffers each period rather than do nothing because some of the assumptions auto-quit relies on do not hold up in a world with pin connector routing. Under this PR, silence is tracked so they could avoid a lot of writes to buffers that are already known to be silent. Auto-quit works precisely when and where it needs to rather than applying globally across the entire effects chain. |
|
Interestingly, it looks like VST3 has an
|
inline constexpr track_ch_t MaxTrackChannelsPerGroup = 32;
inline constexpr unsigned int MaxTrackChannelGroups = 256;
class AudioBus
{
public:
// ...
class ChannelGroup
{
public:
// getters/setters here
private:
std::unique_ptr<float[]> m_buffer; // interleaved (at least for now)
track_ch_t m_channels; // # of channels in `m_buffer` (`MaxTrackChannelsPerGroup` maximum) - currently only 2 is used
std::bitset<MaxTrackChannelsPerGroup> m_quietChannels;
};
auto channelGroup(unsigned int index) const -> const ChannelGroup& { return m_groups.at(index); }
private:
ArrayVector<ChannelGroup, MaxTrackChannelGroups> m_groups;
f_cnt_t m_frames;
bool m_silenceTrackingEnabled = false;
};I think I am slowly understanding this a bit better. Your That being said, I think my stance on this being named For the Though, I am confused how we would handle situations (for example) where one of the |
Conflicts: - include/Effect.h
|
@sakertooth In 51033a8, I converted This was less work than I was expecting, and it's nice to start transitioning away from Some other additions:
To summarize how it functions now, |
sakertooth
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to review as much as I could but started skimming near the end. Sorry if I mentioned there were unused functions, my editor didn't find all the references for some reason (so if I said something was unused and it was, you can ignore that).
| class LMMS_EXPORT AudioBus | ||
| { | ||
| public: | ||
| class LMMS_EXPORT BusData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the ChannelGroup you proposed a few weeks back? If so, I think I preferred that name better. I also asked ChatGPT and it also come up with ChannelGroup 🤣 (also came up with AudioChannelGroup, BusChannelGroup, etc, ChannelGroup is nice though). It said it didn't like BusData because it didn't really bring forward the concept of a group of channels in the name, which I agree with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's the ChannelGroup class I proposed before.
I'll have to think it over some more. Naming stuff has been the hardest part of this PR haha.
| class LMMS_EXPORT AudioBus | ||
| { | ||
| public: | ||
| class LMMS_EXPORT BusData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the realization that BusData is really just an audio buffer... which overlaps with SampleBuffer a bit. I want a general AudioBuffer class and to also phase out SampleBuffer or transition it to work with an arbitrary number of channels and layout rather than just stereo and interleaved. That way, we can also simplify AudioBus a bit and remove the nested BusData class.
Though, you might prefer working with float* instead of AudioBuffer... not sure why but you might find that simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a look at SampleBuffer and see if we might be able to consolidate the designs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I came up with a good abstraction for this I think in #8178. SampleBuffer is really meant to be a SharedAudioBuffer and is immutable since no one needs to write to it. It will use the Flyweight pattern, allowing value type semantics and simplifying the implementation to share these buffers with a cache.
AudioBuffer however can be a regular buffer of audio that can be modified and is meant for our DSP modules.
I am transitioning SampleBuffer to SharedAudioBuffer in 8178 (just without the caching/full Flyweight pattern). This should be a bit clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also considering just making SharedAudioBuffer an alias for std::shared_ptr<const AudioBuffer> and not overthink anything lol.
| volatile bool m_bufferUsage; | ||
|
|
||
| SampleFrame* const m_buffer; | ||
| AudioBus m_busses; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_bus feels more natural to me, but if you did because "technically AudioBus is a bus of busses", then renaming AudioBus to AudioBusGroup and using m_busGroup instead could work.
|
|
||
| //! Copies planar buffers to interleaved buffers | ||
| template<class T, proc_ch_t inputs, proc_ch_t outputs> | ||
| constexpr void copy(InterleavedBufferView<std::remove_const_t<T>, outputs> dst, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would probably rename these functions since copy is a bit vauge, maybe convertPlanarToInterleaved. Also curious if this belongs in MixHelpers.
| } | ||
|
|
||
| //! Verifies that the `silenceChannels` method works as intended | ||
| void SilenceChannels() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test names are somewhat odd (doesn't explain what is being tested exactly, though the docs do). Usually the test name would be quite descriptive and even longer than usual if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're named after the methods they test, so I think it's pretty clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually the test names would describe how you are testing the method as well. But now I would have to go through the code to see how you are testing this method.
Just having the method doesn't tell me what kind of logic about the method you are testing here. You unit test individual features of the class, not the methods.
Each unit test should also test one thing at a time.
Do not worry about long test names, those are expected.
Unit tests also effectively serve as documentation, explaining the design requirements of the class in each unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend looking into these best practices to get a feel for what kind of name I am looking for. It goes over naming best practices.
|
Late addition but
Yeah. This is also how Ardour does it (Audio/MIDI Busses). |
This is another PR coming out of the work done in #7459, and probably the last one needed before that PR can be merged.
It reworks the auto-quit feature by introducing a new
AudioBusclass which keeps track of which track channels are currently silent as audio flows through the effects chain.When track channels going into an effect's input are not marked as quiet, it is assumed a signal is present and the plugin needs to wake up if it is asleep due to auto-quit. After a plugin processes a buffer, the silence status is updated.
When the auto-quit setting is disabled (that is, when effects are always kept running), effects are always assumed to have input noise (a non-quiet signal present at the plugin inputs), which should result in the same behavior as before.
Benefits:
This new system works so long as the silence flags for each channel remain valid at each point along the effect chain. Modifying the buffers without an accompanying update of the silence flags could violate assumptions. Through unit tests, the correct functioning of
AudioBusitself can be validated, but its usage inAudioBusHandle,Mixer, and a few other places where track channels are handled will need to be done with care.