Auto-quit rework (#8070)
This reworks the auto-quit feature by introducing a new AudioBuffer class which keeps track of which 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: - The auto-quit system now closely follows how it is supposed to function by only waking plugins which have non-zero input rather than waking all plugins at once whenever an instrument plays a note or a sample track plays. This granularity better fits multi-channel plugins and pin connector routing where not all plugin inputs are connected to the same track channels. This means a sleeping plugin whose inputs are connected to channels 3/4 would not need to wake up if a signal is only present on channels 1/2. - Silencing channels that are already known to be silent is a no-op - Calculating the absolute peak sample value for a channel already known to be silent is a no-op - The silence flags also could be useful for other purposes, such as adding visual indicators to represent how audio signals flow in and out of each plugin - With a little more work, auto-quit could be enabled/disabled for plugins on an individual basis - With a little more work, auto-quit could be implemented for instrument plugins - AudioBuffer can be used with SharedMemory - AudioBuffer could be used in plugins for their buffers 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 AudioBuffer itself can be validated, but its usage in AudioBusHandle, Mixer, and a few other places where track channels are handled will need to be done with care. --------- Co-authored-by: Sotonye Atemie <sakertooth@gmail.com>
This commit is contained in:
421
include/AudioBuffer.h
Normal file
421
include/AudioBuffer.h
Normal file
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* AudioBuffer.h
|
||||
*
|
||||
* Copyright (c) 2026 Dalton Messmer <messmer.dalton/at/gmail.com>
|
||||
*
|
||||
* 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_AUDIO_BUFFER_H
|
||||
#define LMMS_AUDIO_BUFFER_H
|
||||
|
||||
#include <bitset>
|
||||
#include <memory_resource>
|
||||
|
||||
#include "AudioBufferView.h"
|
||||
#include "ArrayVector.h"
|
||||
#include "LmmsTypes.h"
|
||||
#include "lmms_constants.h"
|
||||
#include "lmms_export.h"
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
/**
|
||||
* An owning collection of audio channels for an instrument track, mixer channel, or audio processor.
|
||||
*
|
||||
* Features:
|
||||
* - Up to `MaxChannelsPerAudioBuffer` total channels
|
||||
* - Audio data in planar format (plus a temporary interleaved buffer for conversions until we use planar only)
|
||||
* - All planar buffers are sourced from the same large buffer for better cache locality
|
||||
* - Custom allocator support
|
||||
* - Silence tracking for each channel (NOTE: requires careful use so that non-silent data is not written to a
|
||||
* channel marked silent without updating that channel's silence flag afterward)
|
||||
* - Methods for sanitizing, silencing, and calculating the absolute peak value of channels, and doing so more
|
||||
* efficiently using the data from silence tracking
|
||||
* - Can organize channels into arbitrary groups. For example, you could have 6 total channels divided into 2 groups
|
||||
* where the 1st group contains 2 channels (stereo) and the 2nd contains 4 channels (quadraphonic).
|
||||
* - Extensive unit testing - @ref AudioBufferTest.cpp
|
||||
*
|
||||
* Audio data layout explanation:
|
||||
* - All planar audio data for all channels in an AudioBuffer is sourced from the same large contiguous
|
||||
* buffer called the source buffer (m_sourceBuffer).
|
||||
* - The source buffer consists of the buffer for 1st channel followed by the buffer for the 2nd channel, and so on
|
||||
* for all channels. In total, the number of elements is `channels * frames`.
|
||||
* - A separate vector of non-owning pointers to channel buffers is also maintained. In this vector, each index
|
||||
* corresponds to a channel, providing a mapping from the channel index to a pointer to the start of that
|
||||
* channel's buffer within the source buffer. This is called the access buffer (m_accessBuffer).
|
||||
* - The purpose of the access buffer is to provide channel-wise access to buffers within the source buffer, so
|
||||
* it's `m_accessBuffer[channelIdx][frameIdx]` instead of `m_sourceBuffer[channelIdx * frames + frameIdx]`.
|
||||
* This is very important since many APIs dealing with planar audio expect it in this `float**` 2D array form.
|
||||
* - Groups have no effect on the audio data layout in the source/access buffers and are merely a layer built on top.
|
||||
* Conveniently, if you take `m_accessBuffer` and offset it by `channelIndex`, you get another `float**`
|
||||
* starting at that channel. This what the `float**` buffer stored in each ChannelGroup is.
|
||||
*
|
||||
* Naming notes:
|
||||
* - When this class is used in an instrument track or mixer channel, its channels could be referred to
|
||||
* as "track channels" or "internal channels", since they are equivalent to the "track channels" used
|
||||
* in other DAWs such as REAPER.
|
||||
* - When this class is used in an audio processor or audio plugin, its channels could be referred to
|
||||
* as "processor channels" or "plugin channels".
|
||||
*/
|
||||
class LMMS_EXPORT AudioBuffer
|
||||
{
|
||||
public:
|
||||
using ChannelFlags = std::bitset<MaxChannelsPerAudioBuffer>;
|
||||
|
||||
//! Non-owning collection of audio channels + metadata
|
||||
class ChannelGroup
|
||||
{
|
||||
public:
|
||||
ChannelGroup() = default;
|
||||
ChannelGroup(float** buffers, ch_cnt_t channels)
|
||||
: m_buffers{buffers}
|
||||
, m_channels{channels}
|
||||
{}
|
||||
|
||||
auto buffers() const -> const float* const* { return m_buffers; }
|
||||
auto buffers() -> float** { return m_buffers; }
|
||||
|
||||
auto buffer(ch_cnt_t channel) const -> const float*
|
||||
{
|
||||
assert(channel < m_channels);
|
||||
return m_buffers[channel];
|
||||
}
|
||||
|
||||
auto buffer(ch_cnt_t channel) -> float*
|
||||
{
|
||||
assert(channel < m_channels);
|
||||
return m_buffers[channel];
|
||||
}
|
||||
|
||||
auto channels() const -> ch_cnt_t { return m_channels; }
|
||||
|
||||
void setBuffers(float** newBuffers) { m_buffers = newBuffers; }
|
||||
void setChannels(ch_cnt_t channels) { m_channels = channels; }
|
||||
|
||||
// TODO: Future additions: Group names, type (main/aux), speaker arrangements (for surround sound), ...
|
||||
|
||||
private:
|
||||
/**
|
||||
* Provides access to individual channel buffers.
|
||||
* [channel index][frame index]
|
||||
*/
|
||||
float** m_buffers = nullptr;
|
||||
|
||||
//! Number of channels in `m_buffers` - currently only 2 is used
|
||||
ch_cnt_t m_channels = 0;
|
||||
};
|
||||
|
||||
AudioBuffer() = delete;
|
||||
|
||||
AudioBuffer(const AudioBuffer&) = delete;
|
||||
AudioBuffer(AudioBuffer&&) noexcept = default;
|
||||
auto operator=(const AudioBuffer&) -> AudioBuffer& = delete;
|
||||
auto operator=(AudioBuffer&&) noexcept -> AudioBuffer& = default;
|
||||
|
||||
/**
|
||||
* Creates AudioBuffer with a 1st (main) channel group.
|
||||
*
|
||||
* Silence tracking is enabled or disabled depending on the auto-quit setting.
|
||||
*
|
||||
* @param frames frame count for each channel
|
||||
* @param channels channel count for the 1st group, or zero to skip adding the 1st group
|
||||
* @param resource memory resource for all buffers
|
||||
*/
|
||||
explicit AudioBuffer(f_cnt_t frames, ch_cnt_t channels = DEFAULT_CHANNELS,
|
||||
std::pmr::memory_resource* resource = std::pmr::get_default_resource());
|
||||
|
||||
/**
|
||||
* Creates AudioBuffer with groups defined.
|
||||
*
|
||||
* Silence tracking is enabled or disabled depending on the auto-quit setting.
|
||||
*
|
||||
* @param frames frame count for each channel
|
||||
* @param channels total channel count
|
||||
* @param groups group count
|
||||
* @param resource memory resource for all buffers
|
||||
* @param groupVisitor see @ref setGroups
|
||||
*/
|
||||
template<class F>
|
||||
AudioBuffer(f_cnt_t frames, ch_cnt_t channels, group_cnt_t groups,
|
||||
std::pmr::memory_resource* resource, F&& groupVisitor)
|
||||
: AudioBuffer{frames, channels, resource}
|
||||
{
|
||||
setGroups(groups, std::forward<F>(groupVisitor));
|
||||
}
|
||||
|
||||
//! The presence of the temporary interleaved buffer is opt-in. Call this to create it.
|
||||
void allocateInterleavedBuffer();
|
||||
|
||||
auto hasInterleavedBuffer() const -> bool { return !m_interleavedBuffer.empty(); }
|
||||
|
||||
/**
|
||||
* @returns the number of bytes needed to allocate buffers with given frame and channel counts.
|
||||
* Useful for preallocating a buffer for a shared memory resource.
|
||||
*/
|
||||
static auto allocationSize(f_cnt_t frames, ch_cnt_t channels,
|
||||
bool withInterleavedBuffer = false) -> std::size_t;
|
||||
|
||||
//! @returns current number of channel groups
|
||||
auto groupCount() const -> group_cnt_t { return static_cast<group_cnt_t>(m_groups.size()); }
|
||||
|
||||
auto group(group_cnt_t index) const -> const ChannelGroup& { return m_groups[index]; }
|
||||
auto group(group_cnt_t index) -> ChannelGroup& { return m_groups[index]; }
|
||||
|
||||
//! @returns the buffers for all channel groups
|
||||
auto allBuffers() const -> PlanarBufferView<const float>
|
||||
{
|
||||
return {m_accessBuffer.data(), totalChannels(), m_frames};
|
||||
}
|
||||
|
||||
//! @returns the buffers for all channel groups
|
||||
auto allBuffers() -> PlanarBufferView<float>
|
||||
{
|
||||
return {m_accessBuffer.data(), totalChannels(), m_frames};
|
||||
}
|
||||
|
||||
//! @returns the buffers of the given channel group
|
||||
auto groupBuffers(group_cnt_t index) const -> PlanarBufferView<const float>
|
||||
{
|
||||
assert(index < groupCount());
|
||||
const ChannelGroup& g = m_groups[index];
|
||||
return {g.buffers(), g.channels(), m_frames};
|
||||
}
|
||||
|
||||
//! @returns the buffers of the given channel group
|
||||
auto groupBuffers(group_cnt_t index) -> PlanarBufferView<float>
|
||||
{
|
||||
assert(index < groupCount());
|
||||
ChannelGroup& g = m_groups[index];
|
||||
return {g.buffers(), g.channels(), m_frames};
|
||||
}
|
||||
|
||||
//! @returns the buffer for the given channel
|
||||
auto buffer(ch_cnt_t channel) const -> std::span<const float>
|
||||
{
|
||||
return {m_accessBuffer[channel], m_frames};
|
||||
}
|
||||
|
||||
//! @returns the buffer for the given channel
|
||||
auto buffer(ch_cnt_t channel) -> std::span<float>
|
||||
{
|
||||
return {m_accessBuffer[channel], m_frames};
|
||||
}
|
||||
|
||||
//! @returns the total channel count (never exceeds MaxChannelsPerAudioBuffer)
|
||||
auto totalChannels() const -> ch_cnt_t { return static_cast<ch_cnt_t>(m_accessBuffer.size()); }
|
||||
|
||||
//! @returns the frame count for each channel buffer
|
||||
auto frames() const -> f_cnt_t { return m_frames; }
|
||||
|
||||
//! @returns scratch buffer for conversions between interleaved and planar TODO: Remove once using planar only
|
||||
auto interleavedBuffer() const -> InterleavedBufferView<const float, 2>
|
||||
{
|
||||
assert(hasInterleavedBuffer());
|
||||
return {m_interleavedBuffer.data(), m_frames};
|
||||
}
|
||||
|
||||
//! @returns scratch buffer for conversions between interleaved and planar TODO: Remove once using planar only
|
||||
auto interleavedBuffer() -> InterleavedBufferView<float, 2>
|
||||
{
|
||||
assert(hasInterleavedBuffer());
|
||||
return {m_interleavedBuffer.data(), m_frames};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds a new channel group at the end of the list.
|
||||
*
|
||||
* If the memory resource is `SharedMemoryResource`, all buffers (source, channels,
|
||||
* and interleaved) will be reallocated. The number of bytes allocated will be
|
||||
* `allocationSize(frames(), totalChannels() + channels, hasInterleavedBuffer())`.
|
||||
*
|
||||
* @param channels how many channels the new group should have
|
||||
* @returns the newly created group, or nullptr upon failure
|
||||
*/
|
||||
auto addGroup(ch_cnt_t channels) -> ChannelGroup*;
|
||||
|
||||
/**
|
||||
* @brief Changes the channel grouping without changing the channel count.
|
||||
* Does not reallocate any buffers.
|
||||
*
|
||||
* @param groups the new group count
|
||||
* @param groupVisitor called for each new group, passed the index and group reference, and is
|
||||
* expected to return the channel count for that group. The visitor may
|
||||
* also set the group's metadata.
|
||||
*/
|
||||
template<class F>
|
||||
void setGroups(group_cnt_t groups, F&& groupVisitor)
|
||||
{
|
||||
static_assert(std::is_invocable_r_v<ch_cnt_t, F, group_cnt_t, ChannelGroup&>,
|
||||
"groupVisitor is passed the group index + group reference and must return the group's channel count");
|
||||
|
||||
m_groups.clear();
|
||||
ch_cnt_t ch = 0;
|
||||
for (group_cnt_t idx = 0; idx < groups; ++idx)
|
||||
{
|
||||
auto& group = m_groups.emplace_back();
|
||||
|
||||
const auto channels = groupVisitor(idx, group);
|
||||
if (channels == 0) { throw std::runtime_error{"group cannot have zero channels"}; }
|
||||
|
||||
group.setBuffers(&m_accessBuffer[ch]);
|
||||
group.setChannels(channels);
|
||||
|
||||
ch += channels;
|
||||
if (ch > this->totalChannels())
|
||||
{
|
||||
throw std::runtime_error{"sum of group channel counts exceeds total channels"};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels which are known to be quiet, AKA the silence status.
|
||||
* 1 = channel is known to be silent
|
||||
* 0 = channel is assumed to be non-silent (or, when silence tracking
|
||||
* is enabled, *known* to be non-silent)
|
||||
*
|
||||
* NOTE: If any channel buffers are used and their data modified outside of this class,
|
||||
* their silence flags will be invalidated until `updateSilenceFlags()` is called.
|
||||
* Therefore, calling code must be careful to always keep the silence flags up-to-date.
|
||||
*/
|
||||
auto silenceFlags() const -> const ChannelFlags& { return m_silenceFlags; }
|
||||
|
||||
//! Forcibly pessimizes silence tracking for a specific channel
|
||||
void assumeNonSilent(ch_cnt_t channel) { m_silenceFlags[channel] = false; }
|
||||
|
||||
/**
|
||||
* When silence tracking is enabled, channels will be checked for silence whenever their data may
|
||||
* have changed, so it'll always be known whether they are silent or non-silent. There is a performance cost
|
||||
* to this, but it is likely worth it since this information allows many effects to be put to sleep
|
||||
* when their inputs are silent ("auto-quit"). When a channel is known to be silent, it also
|
||||
* enables optimizations in buffer sanitization, buffer zeroing, and finding the absolute peak sample value.
|
||||
*
|
||||
* When silence tracking is disabled, channels are not checked for silence, so a silence flag may be
|
||||
* unset despite the channel being silent. Non-silence must be assumed whenever the silence status is not
|
||||
* known, so the optimizations which silent buffers allow will not be possible as often.
|
||||
*/
|
||||
void enableSilenceTracking(bool enabled);
|
||||
auto silenceTrackingEnabled() const -> bool { return m_silenceTrackingEnabled; }
|
||||
|
||||
//! Mixes the silence flags of the other `AudioBuffer` with this `AudioBuffer`
|
||||
void mixSilenceFlags(const AudioBuffer& other);
|
||||
|
||||
/**
|
||||
* Checks whether any of the selected channels are non-silent (has a signal).
|
||||
*
|
||||
* If silence tracking is disabled, all channels that aren't marked
|
||||
* as silent are assumed to be non-silent.
|
||||
*
|
||||
* A processor could check for a signal present at any of its inputs by
|
||||
* calling this method selecting all of the track channels that are routed
|
||||
* to at least one of its inputs.
|
||||
*
|
||||
* @param channels channels to check for a signal; 1 = selected, 0 = ignore
|
||||
*/
|
||||
auto hasSignal(const ChannelFlags& channels) const -> bool;
|
||||
|
||||
//! Checks whether any channel is non-silent (has a signal). @see hasSignal
|
||||
auto hasAnySignal() const -> bool;
|
||||
|
||||
/**
|
||||
* @brief Sanitizes specified channels of any Inf/NaN values if "nanhandler" setting is enabled
|
||||
*
|
||||
* @param channels channels to sanitize; 1 = selected, 0 = skip
|
||||
* @param upperBound any channel indexes at or above this are skipped
|
||||
*/
|
||||
void sanitize(const ChannelFlags& channels, ch_cnt_t upperBound = MaxChannelsPerAudioBuffer);
|
||||
|
||||
//! Sanitizes all channels. @see sanitize
|
||||
void sanitizeAll();
|
||||
|
||||
/**
|
||||
* @brief Updates the silence status of the given channels, up to the upperBound index.
|
||||
*
|
||||
* @param channels channels to update; 1 = selected, 0 = skip
|
||||
* @param upperBound any channel indexes at or above this are skipped
|
||||
* @returns true if all selected channels were silent
|
||||
*/
|
||||
auto updateSilenceFlags(const ChannelFlags& channels, ch_cnt_t upperBound = MaxChannelsPerAudioBuffer) -> bool;
|
||||
|
||||
//! Updates the silence status of all channels. @see updateSilenceFlags
|
||||
auto updateAllSilenceFlags() -> bool;
|
||||
|
||||
/**
|
||||
* @brief Silences (zeroes) the given channels
|
||||
*
|
||||
* @param channels channels to silence; 1 = selected, 0 = skip
|
||||
* @param upperBound any channel indexes at or above this are skipped
|
||||
*/
|
||||
void silenceChannels(const ChannelFlags& channels, ch_cnt_t upperBound = MaxChannelsPerAudioBuffer);
|
||||
|
||||
//! Silences (zeroes) all channels. @see silenceChannels
|
||||
void silenceAllChannels();
|
||||
|
||||
//! @returns absolute peak sample value for the given channel
|
||||
auto absPeakValue(ch_cnt_t channel) const -> float;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Large buffer that all channel buffers are sourced from.
|
||||
*
|
||||
* [channel index]
|
||||
*/
|
||||
std::pmr::vector<float> m_sourceBuffer;
|
||||
|
||||
/**
|
||||
* Provides access to individual channel buffers within the source buffer.
|
||||
*
|
||||
* [channel index][frame index]
|
||||
*/
|
||||
std::pmr::vector<float*> m_accessBuffer;
|
||||
|
||||
/**
|
||||
* Interleaved scratch buffer for conversions between interleaved and planar.
|
||||
*
|
||||
* TODO: Remove once using planar only
|
||||
*/
|
||||
std::pmr::vector<float> m_interleavedBuffer;
|
||||
|
||||
//! Divides channels into arbitrary groups
|
||||
ArrayVector<ChannelGroup, MaxGroupsPerAudioBuffer> m_groups;
|
||||
|
||||
//! Frame count for every channel buffer
|
||||
f_cnt_t m_frames = 0;
|
||||
|
||||
/**
|
||||
* Stores which channels are known to be quiet, AKA the silence status.
|
||||
*
|
||||
* This must always be kept in sync with the buffer data when enabled - at minimum
|
||||
* avoiding any false positives where a channel is marked as "silent" when it isn't.
|
||||
* Any channel bits at or above `totalChannels()` must always be marked silent.
|
||||
*
|
||||
* 1 = channel is known to be silent
|
||||
* 0 = channel is assumed to be non-silent (or, when silence tracking
|
||||
* is enabled, *known* to be non-silent)
|
||||
*/
|
||||
ChannelFlags m_silenceFlags;
|
||||
|
||||
bool m_silenceTrackingEnabled = false;
|
||||
};
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif // LMMS_AUDIO_BUFFER_H
|
||||
@@ -39,20 +39,20 @@ namespace lmms
|
||||
{
|
||||
|
||||
//! Use when the number of channels is not known at compile time
|
||||
inline constexpr auto DynamicChannelCount = static_cast<proc_ch_t>(-1);
|
||||
inline constexpr auto DynamicChannelCount = static_cast<ch_cnt_t>(-1);
|
||||
|
||||
|
||||
namespace detail {
|
||||
|
||||
// For buffer views with static channel count
|
||||
template<typename T, proc_ch_t channelCount>
|
||||
template<typename T, ch_cnt_t channelCount>
|
||||
class BufferViewData
|
||||
{
|
||||
public:
|
||||
constexpr BufferViewData() = default;
|
||||
constexpr BufferViewData(const BufferViewData&) = default;
|
||||
|
||||
constexpr BufferViewData(T* data, [[maybe_unused]] proc_ch_t channels, f_cnt_t frames) noexcept
|
||||
constexpr BufferViewData(T* data, [[maybe_unused]] ch_cnt_t channels, f_cnt_t frames) noexcept
|
||||
: m_data{data}
|
||||
, m_frames{frames}
|
||||
{
|
||||
@@ -66,7 +66,7 @@ public:
|
||||
}
|
||||
|
||||
constexpr auto data() const noexcept -> T* { return m_data; }
|
||||
static constexpr auto channels() noexcept -> proc_ch_t { return channelCount; }
|
||||
static constexpr auto channels() noexcept -> ch_cnt_t { return channelCount; }
|
||||
constexpr auto frames() const noexcept -> f_cnt_t { return m_frames; }
|
||||
|
||||
protected:
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
constexpr BufferViewData() = default;
|
||||
constexpr BufferViewData(const BufferViewData&) = default;
|
||||
|
||||
constexpr BufferViewData(T* data, proc_ch_t channels, f_cnt_t frames) noexcept
|
||||
constexpr BufferViewData(T* data, ch_cnt_t channels, f_cnt_t frames) noexcept
|
||||
: m_data{data}
|
||||
, m_channels{channels}
|
||||
, m_frames{frames}
|
||||
@@ -91,17 +91,17 @@ public:
|
||||
}
|
||||
|
||||
constexpr auto data() const noexcept -> T* { return m_data; }
|
||||
constexpr auto channels() const noexcept -> proc_ch_t { return m_channels; }
|
||||
constexpr auto channels() const noexcept -> ch_cnt_t { return m_channels; }
|
||||
constexpr auto frames() const noexcept -> f_cnt_t { return m_frames; }
|
||||
|
||||
protected:
|
||||
T* m_data = nullptr;
|
||||
proc_ch_t m_channels = 0;
|
||||
ch_cnt_t m_channels = 0;
|
||||
f_cnt_t m_frames = 0;
|
||||
};
|
||||
|
||||
// For interleaved frame iterators with static channel count
|
||||
template<typename T, proc_ch_t channelCount>
|
||||
template<typename T, ch_cnt_t channelCount>
|
||||
class InterleavedFrameIteratorData
|
||||
{
|
||||
public:
|
||||
@@ -113,7 +113,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static constexpr auto channels() noexcept -> proc_ch_t { return channelCount; }
|
||||
static constexpr auto channels() noexcept -> ch_cnt_t { return channelCount; }
|
||||
|
||||
protected:
|
||||
T* m_data = nullptr;
|
||||
@@ -127,21 +127,21 @@ public:
|
||||
constexpr InterleavedFrameIteratorData() = default;
|
||||
constexpr InterleavedFrameIteratorData(const InterleavedFrameIteratorData&) = default;
|
||||
|
||||
constexpr InterleavedFrameIteratorData(T* data, proc_ch_t channels) noexcept
|
||||
constexpr InterleavedFrameIteratorData(T* data, ch_cnt_t channels) noexcept
|
||||
: m_data{data}
|
||||
, m_channels{channels}
|
||||
{
|
||||
}
|
||||
|
||||
constexpr auto channels() const noexcept -> proc_ch_t { return m_channels; }
|
||||
constexpr auto channels() const noexcept -> ch_cnt_t { return m_channels; }
|
||||
|
||||
protected:
|
||||
T* m_data = nullptr;
|
||||
proc_ch_t m_channels = 0;
|
||||
ch_cnt_t m_channels = 0;
|
||||
};
|
||||
|
||||
// Allows for iterating over the frames of `InterleavedBufferView`
|
||||
template<typename T, proc_ch_t channelCount = DynamicChannelCount>
|
||||
template<typename T, ch_cnt_t channelCount = DynamicChannelCount>
|
||||
class InterleavedFrameIterator : public InterleavedFrameIteratorData<T, channelCount>
|
||||
{
|
||||
using Base = InterleavedFrameIteratorData<T, channelCount>;
|
||||
@@ -286,7 +286,7 @@ concept SampleType = detail::OneOf<std::remove_const_t<T>,
|
||||
*
|
||||
* TODO C++23: Use std::mdspan?
|
||||
*/
|
||||
template<SampleType T, proc_ch_t channelCount = DynamicChannelCount>
|
||||
template<SampleType T, ch_cnt_t channelCount = DynamicChannelCount>
|
||||
class InterleavedBufferView : public detail::BufferViewData<T, channelCount>
|
||||
{
|
||||
using Base = detail::BufferViewData<T, channelCount>;
|
||||
@@ -312,24 +312,24 @@ public:
|
||||
}
|
||||
|
||||
//! Construct dynamic channel count from static
|
||||
template<proc_ch_t otherChannels>
|
||||
template<ch_cnt_t otherChannels>
|
||||
requires (channelCount == DynamicChannelCount && otherChannels != DynamicChannelCount)
|
||||
constexpr InterleavedBufferView(InterleavedBufferView<T, otherChannels> other) noexcept
|
||||
: Base{other.data(), otherChannels, other.frames()}
|
||||
{
|
||||
}
|
||||
|
||||
//! Construct from std::span<SampleFrame>
|
||||
InterleavedBufferView(std::span<SampleFrame> buffer) noexcept
|
||||
//! Construct from SampleFrame*
|
||||
InterleavedBufferView(SampleFrame* data, f_cnt_t frames) noexcept
|
||||
requires (std::is_same_v<std::remove_const_t<T>, float> && channelCount == 2)
|
||||
: Base{reinterpret_cast<float*>(buffer.data()), buffer.size()}
|
||||
: Base{reinterpret_cast<float*>(data), frames}
|
||||
{
|
||||
}
|
||||
|
||||
//! Construct from std::span<const SampleFrame>
|
||||
InterleavedBufferView(std::span<const SampleFrame> buffer) noexcept
|
||||
//! Construct from const SampleFrame*
|
||||
InterleavedBufferView(const SampleFrame* data, f_cnt_t frames) noexcept
|
||||
requires (std::is_same_v<T, const float> && channelCount == 2)
|
||||
: Base{reinterpret_cast<const float*>(buffer.data()), buffer.size()}
|
||||
: Base{reinterpret_cast<const float*>(data), frames}
|
||||
{
|
||||
}
|
||||
|
||||
@@ -437,13 +437,13 @@ public:
|
||||
return reinterpret_cast<const SampleFrame*>(this->m_data)[index];
|
||||
}
|
||||
|
||||
auto toSampleFrames() noexcept -> std::span<SampleFrame>
|
||||
auto asSampleFrames() noexcept -> std::span<SampleFrame>
|
||||
requires (std::is_same_v<T, float> && channelCount == 2)
|
||||
{
|
||||
return {reinterpret_cast<SampleFrame*>(this->m_data), this->m_frames};
|
||||
}
|
||||
|
||||
auto toSampleFrames() const noexcept -> std::span<const SampleFrame>
|
||||
auto asSampleFrames() const noexcept -> std::span<const SampleFrame>
|
||||
requires (std::is_same_v<T, const float> && channelCount == 2)
|
||||
{
|
||||
return {reinterpret_cast<const SampleFrame*>(this->m_data), this->m_frames};
|
||||
@@ -457,6 +457,10 @@ public:
|
||||
static_assert(sizeof(InterleavedBufferView<float>) > sizeof(InterleavedBufferView<float, 2>));
|
||||
static_assert(sizeof(InterleavedBufferView<float, 2>) == sizeof(void*) + sizeof(f_cnt_t));
|
||||
|
||||
// Deduction guides
|
||||
InterleavedBufferView(const SampleFrame*, f_cnt_t) -> InterleavedBufferView<const float, 2>;
|
||||
InterleavedBufferView(SampleFrame*, f_cnt_t) -> InterleavedBufferView<float, 2>;
|
||||
|
||||
|
||||
/**
|
||||
* Non-owning view for multi-channel non-interleaved audio data
|
||||
@@ -466,7 +470,7 @@ static_assert(sizeof(InterleavedBufferView<float, 2>) == sizeof(void*) + sizeof(
|
||||
*
|
||||
* TODO C++23: Use std::mdspan?
|
||||
*/
|
||||
template<SampleType T, proc_ch_t channelCount = DynamicChannelCount>
|
||||
template<SampleType T, ch_cnt_t channelCount = DynamicChannelCount>
|
||||
class PlanarBufferView : public detail::BufferViewData<T* const, channelCount>
|
||||
{
|
||||
using Base = detail::BufferViewData<T* const, channelCount>;
|
||||
@@ -489,7 +493,7 @@ public:
|
||||
}
|
||||
|
||||
//! Construct dynamic channel count from static
|
||||
template<proc_ch_t otherChannels>
|
||||
template<ch_cnt_t otherChannels>
|
||||
requires (channelCount == DynamicChannelCount && otherChannels != DynamicChannelCount)
|
||||
constexpr PlanarBufferView(PlanarBufferView<T, otherChannels> other) noexcept
|
||||
: Base{other.data(), otherChannels, other.frames()}
|
||||
@@ -502,13 +506,13 @@ public:
|
||||
}
|
||||
|
||||
//! @return the buffer of the given channel
|
||||
constexpr auto buffer(proc_ch_t channel) const noexcept -> std::span<T>
|
||||
constexpr auto buffer(ch_cnt_t channel) const noexcept -> std::span<T>
|
||||
{
|
||||
return {bufferPtr(channel), this->m_frames};
|
||||
}
|
||||
|
||||
//! @return the buffer of the given channel
|
||||
template<proc_ch_t channel> requires (channelCount != DynamicChannelCount)
|
||||
template<ch_cnt_t channel> requires (channelCount != DynamicChannelCount)
|
||||
constexpr auto buffer() const noexcept -> std::span<T>
|
||||
{
|
||||
return {bufferPtr<channel>(), this->m_frames};
|
||||
@@ -518,7 +522,7 @@ public:
|
||||
* @return pointer to the buffer of the given channel.
|
||||
* The size of the buffer is `frames()`.
|
||||
*/
|
||||
constexpr auto bufferPtr(proc_ch_t channel) const noexcept -> T*
|
||||
constexpr auto bufferPtr(ch_cnt_t channel) const noexcept -> T*
|
||||
{
|
||||
assert(channel < Base::channels());
|
||||
assert(this->m_data != nullptr);
|
||||
@@ -529,7 +533,7 @@ public:
|
||||
* @return pointer to the buffer of the given channel.
|
||||
* The size of the buffer is `frames()`.
|
||||
*/
|
||||
template<proc_ch_t channel> requires (channelCount != DynamicChannelCount)
|
||||
template<ch_cnt_t channel> requires (channelCount != DynamicChannelCount)
|
||||
constexpr auto bufferPtr() const noexcept -> T*
|
||||
{
|
||||
static_assert(channel < channelCount);
|
||||
@@ -541,7 +545,7 @@ public:
|
||||
* @return pointer to the buffer of a given channel.
|
||||
* The size of the buffer is `frames()`.
|
||||
*/
|
||||
constexpr auto operator[](proc_ch_t channel) const noexcept -> T*
|
||||
constexpr auto operator[](ch_cnt_t channel) const noexcept -> T*
|
||||
{
|
||||
return bufferPtr(channel);
|
||||
}
|
||||
@@ -556,10 +560,55 @@ static_assert(sizeof(PlanarBufferView<float, 2>) == sizeof(void**) + sizeof(f_cn
|
||||
|
||||
|
||||
//! Concept for any audio buffer view, interleaved or planar
|
||||
template<class T, typename U, proc_ch_t channels = DynamicChannelCount>
|
||||
template<class T, typename U, ch_cnt_t channels = DynamicChannelCount>
|
||||
concept AudioBufferView = SampleType<U> && (std::convertible_to<T, InterleavedBufferView<U, channels>>
|
||||
|| std::convertible_to<T, PlanarBufferView<U, channels>>);
|
||||
|
||||
|
||||
//! Converts planar buffers to interleaved buffers
|
||||
template<class T, ch_cnt_t inputs, ch_cnt_t outputs>
|
||||
constexpr void toInterleaved(PlanarBufferView<T, inputs> src,
|
||||
InterleavedBufferView<std::remove_const_t<T>, outputs> dst)
|
||||
{
|
||||
assert(src.frames() == dst.frames());
|
||||
if constexpr (inputs == DynamicChannelCount || outputs == DynamicChannelCount)
|
||||
{
|
||||
assert(src.channels() == dst.channels());
|
||||
}
|
||||
else { static_assert(inputs == outputs); }
|
||||
|
||||
for (f_cnt_t frame = 0; frame < dst.frames(); ++frame)
|
||||
{
|
||||
auto* framePtr = dst.framePtr(frame);
|
||||
for (ch_cnt_t channel = 0; channel < dst.channels(); ++channel)
|
||||
{
|
||||
framePtr[channel] = src.bufferPtr(channel)[frame];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! Converts interleaved buffers to planar buffers
|
||||
template<class T, ch_cnt_t inputs, ch_cnt_t outputs>
|
||||
constexpr void toPlanar(InterleavedBufferView<T, inputs> src,
|
||||
PlanarBufferView<std::remove_const_t<T>, outputs> dst)
|
||||
{
|
||||
assert(src.frames() == dst.frames());
|
||||
if constexpr (inputs == DynamicChannelCount || outputs == DynamicChannelCount)
|
||||
{
|
||||
assert(src.channels() == dst.channels());
|
||||
}
|
||||
else { static_assert(inputs == outputs); }
|
||||
|
||||
for (ch_cnt_t channel = 0; channel < dst.channels(); ++channel)
|
||||
{
|
||||
auto* channelPtr = dst.bufferPtr(channel);
|
||||
for (f_cnt_t frame = 0; frame < dst.frames(); ++frame)
|
||||
{
|
||||
channelPtr[frame] = src.framePtr(frame)[channel];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif // LMMS_AUDIO_BUFFER_VIEW_H
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QString>
|
||||
#include <QMutex>
|
||||
|
||||
#include "AudioBuffer.h"
|
||||
#include "PlayHandle.h"
|
||||
|
||||
namespace lmms
|
||||
@@ -58,8 +59,6 @@ public:
|
||||
BoolModel* mutedModel = nullptr);
|
||||
virtual ~AudioBusHandle();
|
||||
|
||||
SampleFrame* buffer() { return m_buffer; }
|
||||
|
||||
// indicate whether JACK & Co should provide output-buffer at ext. port
|
||||
bool extOutputEnabled() const { return m_extOutputEnabled; }
|
||||
void setExtOutputEnabled(bool enabled);
|
||||
@@ -85,7 +84,7 @@ public:
|
||||
private:
|
||||
volatile bool m_bufferUsage;
|
||||
|
||||
SampleFrame* const m_buffer;
|
||||
AudioBuffer m_buffer;
|
||||
|
||||
bool m_extOutputEnabled;
|
||||
mix_ch_t m_nextMixerChannel;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
class AudioBuffer;
|
||||
class EffectChain;
|
||||
class EffectControls;
|
||||
|
||||
@@ -65,7 +66,7 @@ public:
|
||||
}
|
||||
|
||||
//! Returns true if audio was processed and should continue being processed
|
||||
bool processAudioBuffer(SampleFrame* buf, const fpp_t frames);
|
||||
bool processAudioBuffer(AudioBuffer& inOut);
|
||||
|
||||
inline bool isOkay() const
|
||||
{
|
||||
@@ -77,22 +78,10 @@ public:
|
||||
m_okay = _state;
|
||||
}
|
||||
|
||||
|
||||
inline bool isRunning() const
|
||||
//! "Awake" means the effect has not been put to sleep by auto-quit
|
||||
bool isAwake() const
|
||||
{
|
||||
return m_running;
|
||||
}
|
||||
|
||||
void startRunning()
|
||||
{
|
||||
m_quietBufferCount = 0;
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
void stopRunning()
|
||||
{
|
||||
m_quietBufferCount = 0;
|
||||
m_running = false;
|
||||
return m_awake;
|
||||
}
|
||||
|
||||
inline bool isEnabled() const
|
||||
@@ -125,7 +114,12 @@ public:
|
||||
{
|
||||
m_noRun = _state;
|
||||
}
|
||||
|
||||
|
||||
bool isProcessingAudio() const
|
||||
{
|
||||
return isEnabled() && isAwake() && isOkay() && !dontRun();
|
||||
}
|
||||
|
||||
inline TempoSyncKnobModel* autoQuitModel()
|
||||
{
|
||||
return &m_autoQuitModel;
|
||||
@@ -162,21 +156,32 @@ protected:
|
||||
};
|
||||
|
||||
/**
|
||||
* The main audio processing method that runs when plugin is not asleep
|
||||
* The main audio processing method that runs when plugin is awake and running
|
||||
*/
|
||||
virtual ProcessStatus processImpl(SampleFrame* buf, const fpp_t frames) = 0;
|
||||
|
||||
/**
|
||||
* Optional method that runs when plugin is sleeping (not enabled,
|
||||
* not running, not in the Okay state, or in the Don't Run state)
|
||||
* Optional method that runs instead of `processImpl` when an effect
|
||||
* is awake but not running.
|
||||
*/
|
||||
virtual void processBypassedImpl() {}
|
||||
|
||||
|
||||
gui::PluginView* instantiateView( QWidget * ) override;
|
||||
|
||||
virtual void onEnabledChanged() {}
|
||||
void goToSleep()
|
||||
{
|
||||
m_quietBufferCount = 0;
|
||||
m_awake = false;
|
||||
}
|
||||
|
||||
void wakeUp()
|
||||
{
|
||||
m_quietBufferCount = 0;
|
||||
m_awake = true;
|
||||
}
|
||||
|
||||
virtual void onEnabledChanged() {}
|
||||
|
||||
private:
|
||||
/**
|
||||
@@ -184,14 +189,14 @@ private:
|
||||
* after "decay" ms of the output buffer remaining below the silence threshold, the effect is
|
||||
* turned off and won't be processed again until it receives new audio input.
|
||||
*/
|
||||
void handleAutoQuit(std::span<const SampleFrame> output);
|
||||
void handleAutoQuit(bool silentOutput);
|
||||
|
||||
|
||||
EffectChain * m_parent;
|
||||
|
||||
bool m_okay;
|
||||
bool m_noRun;
|
||||
bool m_running;
|
||||
bool m_awake;
|
||||
|
||||
//! The number of consecutive periods where output buffers remain below the silence threshold
|
||||
f_cnt_t m_quietBufferCount = 0;
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
class AudioBuffer;
|
||||
class Effect;
|
||||
class SampleFrame;
|
||||
|
||||
namespace gui
|
||||
{
|
||||
@@ -63,8 +63,7 @@ public:
|
||||
void removeEffect( Effect * _effect );
|
||||
void moveDown( Effect * _effect );
|
||||
void moveUp( Effect * _effect );
|
||||
bool processAudioBuffer( SampleFrame* _buf, const fpp_t _frames, bool hasInputNoise );
|
||||
void startRunning();
|
||||
bool processAudioBuffer(AudioBuffer& buffer);
|
||||
|
||||
void clear();
|
||||
|
||||
|
||||
@@ -43,12 +43,11 @@ using int_sample_t = std::int16_t; // 16-bit-int-sample
|
||||
using sample_rate_t = std::uint32_t; // sample-rate
|
||||
using fpp_t = std::size_t; // frames per period (0-16384)
|
||||
using f_cnt_t = std::size_t; // standard frame-count
|
||||
using ch_cnt_t = std::uint8_t; // channel-count (0-DEFAULT_CHANNELS)
|
||||
using ch_cnt_t = std::uint8_t; // audio channel index/count (0-MaxChannelsPerAudioBuffer)
|
||||
using bpm_t = std::uint16_t; // tempo (MIN_BPM to MAX_BPM)
|
||||
using bitrate_t = std::uint16_t; // bitrate in kbps
|
||||
using mix_ch_t = std::uint16_t; // Mixer-channel (0 to MAX_CHANNEL)
|
||||
using track_ch_t = std::uint16_t; // track channel index/count (0-256)
|
||||
using proc_ch_t = std::uint16_t; // audio processor channel index/count
|
||||
using group_cnt_t = std::uint8_t; // channel group index/count (0-MaxGroupsPerAudioBuffer)
|
||||
|
||||
using jo_id_t = std::uint32_t; // (unique) ID of a journalling object
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#ifndef LMMS_MIX_HELPERS_H
|
||||
#define LMMS_MIX_HELPERS_H
|
||||
|
||||
#include "LmmsTypes.h"
|
||||
#include "AudioBufferView.h"
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
@@ -38,15 +38,28 @@ namespace MixHelpers
|
||||
|
||||
bool isSilent( const SampleFrame* src, int frames );
|
||||
|
||||
bool isSilent(std::span<sample_t> buffer);
|
||||
|
||||
bool useNaNHandler();
|
||||
|
||||
void setNaNHandler( bool use );
|
||||
|
||||
bool sanitize( SampleFrame* src, int frames );
|
||||
/**
|
||||
* @brief Sanitizes a buffer of infs/NaNs, zeroing the entire buffer if
|
||||
* any is detected.
|
||||
*
|
||||
* Only performs sanitization when the NaN handler is active.
|
||||
*
|
||||
* @returns true if inf or NaN was detected
|
||||
*/
|
||||
bool sanitize(std::span<sample_t> buffer);
|
||||
|
||||
/*! \brief Add samples from src to dst */
|
||||
void add( SampleFrame* dst, const SampleFrame* src, int frames );
|
||||
|
||||
/*! \brief Add samples from src to dst */
|
||||
void add(PlanarBufferView<sample_t> dst, PlanarBufferView<const sample_t> src);
|
||||
|
||||
/*! \brief Multiply samples from `dst` by `coeff` */
|
||||
void multiply(SampleFrame* dst, float coeff, int frames);
|
||||
|
||||
|
||||
@@ -25,9 +25,10 @@
|
||||
#ifndef LMMS_MIXER_H
|
||||
#define LMMS_MIXER_H
|
||||
|
||||
#include "Model.h"
|
||||
#include "AudioBuffer.h"
|
||||
#include "EffectChain.h"
|
||||
#include "JournallingObject.h"
|
||||
#include "Model.h"
|
||||
#include "ThreadableJob.h"
|
||||
|
||||
#include <atomic>
|
||||
@@ -43,56 +44,54 @@ using MixerRouteVector = std::vector<MixerRoute*>;
|
||||
|
||||
class MixerChannel : public ThreadableJob
|
||||
{
|
||||
public:
|
||||
MixerChannel( int idx, Model * _parent );
|
||||
virtual ~MixerChannel();
|
||||
public:
|
||||
MixerChannel(int idx, Model* _parent);
|
||||
virtual ~MixerChannel();
|
||||
|
||||
EffectChain m_fxChain;
|
||||
EffectChain m_fxChain;
|
||||
|
||||
// set to true when input fed from mixToChannel or child channel
|
||||
bool m_hasInput;
|
||||
// set to true if any effect in the channel is enabled and running
|
||||
bool m_stillRunning;
|
||||
// set to true if any effect in the channel is enabled and running
|
||||
bool m_stillRunning;
|
||||
|
||||
float m_peakLeft;
|
||||
float m_peakRight;
|
||||
SampleFrame* m_buffer;
|
||||
bool m_muteBeforeSolo;
|
||||
BoolModel m_muteModel;
|
||||
BoolModel m_soloModel;
|
||||
FloatModel m_volumeModel;
|
||||
QString m_name;
|
||||
QMutex m_lock;
|
||||
bool m_queued; // are we queued up for rendering yet?
|
||||
bool m_muted; // are we muted? updated per period so we don't have to call m_muteModel.value() twice
|
||||
float m_peakLeft;
|
||||
float m_peakRight;
|
||||
AudioBuffer m_buffer;
|
||||
bool m_muteBeforeSolo;
|
||||
BoolModel m_muteModel;
|
||||
BoolModel m_soloModel;
|
||||
FloatModel m_volumeModel;
|
||||
QString m_name;
|
||||
QMutex m_lock;
|
||||
bool m_queued; // are we queued up for rendering yet?
|
||||
bool m_muted; // are we muted? updated per period so we don't have to call m_muteModel.value() twice
|
||||
|
||||
// pointers to other channels that this one sends to
|
||||
MixerRouteVector m_sends;
|
||||
// pointers to other channels that this one sends to
|
||||
MixerRouteVector m_sends;
|
||||
|
||||
// pointers to other channels that send to this one
|
||||
MixerRouteVector m_receives;
|
||||
// pointers to other channels that send to this one
|
||||
MixerRouteVector m_receives;
|
||||
|
||||
int index() const { return m_channelIndex; }
|
||||
void setIndex(int index) { m_channelIndex = index; }
|
||||
int index() const { return m_channelIndex; }
|
||||
void setIndex(int index) { m_channelIndex = index; }
|
||||
|
||||
bool isMaster() { return m_channelIndex == 0; }
|
||||
bool isMaster() { return m_channelIndex == 0; }
|
||||
|
||||
bool requiresProcessing() const override { return true; }
|
||||
void unmuteForSolo();
|
||||
void unmuteSenderForSolo();
|
||||
void unmuteReceiverForSolo();
|
||||
bool requiresProcessing() const override { return true; }
|
||||
void unmuteForSolo();
|
||||
void unmuteSenderForSolo();
|
||||
void unmuteReceiverForSolo();
|
||||
|
||||
auto color() const -> const std::optional<QColor>& { return m_color; }
|
||||
void setColor(const std::optional<QColor>& color) { m_color = color; }
|
||||
auto color() const -> const std::optional<QColor>& { return m_color; }
|
||||
void setColor(const std::optional<QColor>& color) { m_color = color; }
|
||||
|
||||
std::atomic_size_t m_dependenciesMet;
|
||||
void incrementDeps();
|
||||
void processed();
|
||||
|
||||
private:
|
||||
void doProcessing() override;
|
||||
int m_channelIndex;
|
||||
std::optional<QColor> m_color;
|
||||
std::atomic_size_t m_dependenciesMet;
|
||||
void incrementDeps();
|
||||
void processed();
|
||||
|
||||
private:
|
||||
void doProcessing() override;
|
||||
int m_channelIndex;
|
||||
std::optional<QColor> m_color;
|
||||
};
|
||||
|
||||
class MixerRoute : public QObject
|
||||
@@ -143,7 +142,7 @@ public:
|
||||
Mixer();
|
||||
~Mixer() override;
|
||||
|
||||
void mixToChannel( const SampleFrame* _buf, mix_ch_t _ch );
|
||||
void mixToChannel(const AudioBuffer& buffer, mix_ch_t dest);
|
||||
|
||||
void prepareMasterMix();
|
||||
void masterMix( SampleFrame* _buf );
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* SharedMemory.h
|
||||
*
|
||||
* Copyright (c) 2022 Dominic Clark <mrdomclark/at/gmail.com>
|
||||
* Copyright (c) 2025-2026 Dalton Messmer <messmer.dalton/at/gmail.com>
|
||||
*
|
||||
* This file is part of LMMS - https://lmms.io
|
||||
*
|
||||
@@ -26,6 +27,8 @@
|
||||
#define LMMS_SHARED_MEMORY_H
|
||||
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
@@ -42,9 +45,9 @@ class SharedMemoryData
|
||||
{
|
||||
public:
|
||||
SharedMemoryData() noexcept;
|
||||
SharedMemoryData(std::string&& key, bool readOnly);
|
||||
SharedMemoryData(std::string&& key, std::size_t size, bool readOnly);
|
||||
SharedMemoryData(std::size_t size, bool readOnly);
|
||||
SharedMemoryData(std::string&& key, bool readOnly, bool isArray);
|
||||
SharedMemoryData(std::string&& key, std::size_t size, bool readOnly, bool isArray);
|
||||
SharedMemoryData(std::size_t size, bool readOnly, bool isArray);
|
||||
~SharedMemoryData();
|
||||
|
||||
SharedMemoryData(SharedMemoryData&& other) noexcept;
|
||||
@@ -65,7 +68,7 @@ public:
|
||||
|
||||
const std::string& key() const noexcept { return m_key; }
|
||||
void* get() const noexcept { return m_ptr; }
|
||||
std::size_t size_bytes() const noexcept;
|
||||
std::size_t arraySize() const noexcept;
|
||||
|
||||
private:
|
||||
std::string m_key;
|
||||
@@ -76,30 +79,96 @@ private:
|
||||
} // namespace detail
|
||||
|
||||
|
||||
//! Similar to std::pmr::monotonic_buffer_resource, but the initial buffer can be replaced
|
||||
class SharedMemoryResource final : public std::pmr::memory_resource
|
||||
{
|
||||
public:
|
||||
SharedMemoryResource() = default;
|
||||
SharedMemoryResource(void* buffer, std::size_t bufferSize) noexcept
|
||||
: m_buffer{buffer}
|
||||
, m_availableBytes{bufferSize}
|
||||
, m_initialBuffer{buffer}
|
||||
, m_initialBufferSize{bufferSize}
|
||||
{}
|
||||
|
||||
SharedMemoryResource(const SharedMemoryResource&) = delete;
|
||||
auto operator=(const SharedMemoryResource&) -> SharedMemoryResource& = delete;
|
||||
SharedMemoryResource(SharedMemoryResource&&) = default;
|
||||
auto operator=(SharedMemoryResource&&) -> SharedMemoryResource& = default;
|
||||
|
||||
//! Returns the buffer back to its initial state
|
||||
void reset() noexcept
|
||||
{
|
||||
m_buffer = m_initialBuffer;
|
||||
m_availableBytes = m_initialBufferSize;
|
||||
}
|
||||
|
||||
//! @returns the number of bytes that can still be allocated
|
||||
auto availableBytes() const noexcept -> std::size_t { return m_availableBytes; }
|
||||
|
||||
template<typename T>
|
||||
friend class SharedMemory;
|
||||
|
||||
private:
|
||||
//! Replaces the initial buffer
|
||||
void reset(void* newBuffer, std::size_t newBufferSize) noexcept
|
||||
{
|
||||
m_buffer = newBuffer;
|
||||
m_availableBytes = newBufferSize;
|
||||
m_initialBuffer = newBuffer;
|
||||
m_initialBufferSize = newBufferSize;
|
||||
}
|
||||
|
||||
void* do_allocate(std::size_t bytes, std::size_t alignment) override
|
||||
{
|
||||
void* p = std::align(alignment, bytes, m_buffer, m_availableBytes);
|
||||
if (!p) { throw std::bad_alloc{}; }
|
||||
|
||||
m_buffer = static_cast<char*>(m_buffer) + bytes;
|
||||
m_availableBytes -= bytes;
|
||||
return p;
|
||||
}
|
||||
void do_deallocate(void*, std::size_t, std::size_t) override {} // no-op
|
||||
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override
|
||||
{
|
||||
return this == &other;
|
||||
}
|
||||
|
||||
private:
|
||||
void* m_buffer = nullptr;
|
||||
std::size_t m_availableBytes = 0;
|
||||
void* m_initialBuffer = nullptr;
|
||||
std::size_t m_initialBufferSize = 0;
|
||||
};
|
||||
|
||||
|
||||
template<typename T>
|
||||
class SharedMemory
|
||||
{
|
||||
// This is stricter than necessary, but keeps things easy for now
|
||||
static_assert(std::is_trivial_v<T>, "objects held in shared memory must be trivial");
|
||||
static_assert(sizeof(T) > 0);
|
||||
|
||||
public:
|
||||
SharedMemory() = default;
|
||||
SharedMemory(const SharedMemory&) = delete;
|
||||
SharedMemory& operator=(const SharedMemory&) = delete;
|
||||
SharedMemory(SharedMemory&&) = default;
|
||||
SharedMemory& operator=(SharedMemory&&) = default;
|
||||
|
||||
void attach(std::string key)
|
||||
{
|
||||
m_data = detail::SharedMemoryData{std::move(key), std::is_const_v<T>};
|
||||
m_data = detail::SharedMemoryData{std::move(key), std::is_const_v<T>, false};
|
||||
}
|
||||
|
||||
void create(std::string key)
|
||||
{
|
||||
m_data = detail::SharedMemoryData{std::move(key), sizeof(T), std::is_const_v<T>};
|
||||
m_data = detail::SharedMemoryData{std::move(key), sizeof(T), std::is_const_v<T>, false};
|
||||
}
|
||||
|
||||
void create()
|
||||
{
|
||||
m_data = detail::SharedMemoryData{sizeof(T), std::is_const_v<T>};
|
||||
m_data = detail::SharedMemoryData{sizeof(T), std::is_const_v<T>, false};
|
||||
}
|
||||
|
||||
void detach() noexcept
|
||||
@@ -126,43 +195,53 @@ class SharedMemory<T[]>
|
||||
{
|
||||
// This is stricter than necessary, but keeps things easy for now
|
||||
static_assert(std::is_trivial_v<T>, "objects held in shared memory must be trivial");
|
||||
static_assert(sizeof(T) > 0);
|
||||
|
||||
public:
|
||||
SharedMemory() = default;
|
||||
SharedMemory(const SharedMemory&) = delete;
|
||||
SharedMemory& operator=(const SharedMemory&) = delete;
|
||||
SharedMemory(SharedMemory&&) = default;
|
||||
SharedMemory& operator=(SharedMemory&&) = default;
|
||||
|
||||
void attach(std::string key)
|
||||
{
|
||||
m_data = detail::SharedMemoryData{std::move(key), std::is_const_v<T>};
|
||||
m_data = detail::SharedMemoryData{std::move(key), std::is_const_v<T>, true};
|
||||
m_resource.reset(m_data.get(), size_bytes());
|
||||
}
|
||||
|
||||
void create(std::string key, std::size_t size)
|
||||
{
|
||||
m_data = detail::SharedMemoryData{std::move(key), size * sizeof(T), std::is_const_v<T>};
|
||||
m_data = detail::SharedMemoryData{std::move(key), size * sizeof(T), std::is_const_v<T>, true};
|
||||
m_resource.reset(m_data.get(), size_bytes());
|
||||
}
|
||||
|
||||
void create(std::size_t size)
|
||||
{
|
||||
m_data = detail::SharedMemoryData{size * sizeof(T), std::is_const_v<T>};
|
||||
m_data = detail::SharedMemoryData{size * sizeof(T), std::is_const_v<T>, true};
|
||||
m_resource.reset(m_data.get(), size_bytes());
|
||||
}
|
||||
|
||||
void detach() noexcept
|
||||
{
|
||||
m_data = detail::SharedMemoryData{};
|
||||
m_resource.reset(nullptr, 0);
|
||||
}
|
||||
|
||||
const std::string& key() const noexcept { return m_data.key(); }
|
||||
T* get() const noexcept { return static_cast<T*>(m_data.get()); }
|
||||
|
||||
std::size_t size() const noexcept { return m_data.size_bytes() / sizeof(T); }
|
||||
std::size_t size_bytes() const noexcept { return m_data.size_bytes(); }
|
||||
std::size_t size() const noexcept { return m_data.arraySize() / sizeof(T); }
|
||||
std::size_t size_bytes() const noexcept { return m_data.arraySize(); }
|
||||
|
||||
T& operator[](std::size_t index) const noexcept { return get()[index]; }
|
||||
explicit operator bool() const noexcept { return get() != nullptr; }
|
||||
|
||||
SharedMemoryResource* resource() noexcept { return &m_resource; }
|
||||
|
||||
private:
|
||||
detail::SharedMemoryData m_data;
|
||||
SharedMemoryResource m_resource;
|
||||
};
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
@@ -34,7 +34,10 @@ namespace lmms
|
||||
// using this directly
|
||||
inline constexpr float F_EPSILON = 1.0e-10f; // 10^-10
|
||||
|
||||
inline constexpr ch_cnt_t DEFAULT_CHANNELS = 2;
|
||||
// Channel counts
|
||||
inline constexpr auto DEFAULT_CHANNELS = ch_cnt_t{2};
|
||||
inline constexpr auto MaxChannelsPerAudioBuffer = ch_cnt_t{128};
|
||||
inline constexpr auto MaxGroupsPerAudioBuffer = group_cnt_t{MaxChannelsPerAudioBuffer / 2};
|
||||
|
||||
// Microtuner
|
||||
inline constexpr unsigned MaxScaleCount = 10; //!< number of scales per project
|
||||
|
||||
Reference in New Issue
Block a user