Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions include/AudioBufferView.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,17 +319,17 @@ class InterleavedBufferView : public detail::BufferViewData<T, channelCount>
{
}

//! 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}
{
}

Expand Down Expand Up @@ -437,13 +437,13 @@ class InterleavedBufferView : public detail::BufferViewData<T, channelCount>
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};
Expand All @@ -457,6 +457,10 @@ class InterleavedBufferView : public detail::BufferViewData<T, channelCount>
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
Expand Down
284 changes: 284 additions & 0 deletions include/AudioBus.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/*
* AudioBus.h
*
* Copyright (c) 2025 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_BUS_H
#define LMMS_AUDIO_BUS_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
{

/**
* A collection of track channels for an instrument or effect chain
* which keeps track of signal flow.
*/
class LMMS_EXPORT AudioBus
{
public:
using ChannelFlags = std::bitset<MaxTrackChannels>;

class LMMS_EXPORT BusData
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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

Copy link
Contributor

@sakertooth sakertooth Dec 22, 2025

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.

Copy link
Contributor

@sakertooth sakertooth Dec 22, 2025

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.

{
public:
BusData(std::pmr::polymorphic_allocator<>& alloc,
ch_cnt_t channels, f_cnt_t frames, track_ch_t startingChannel);

BusData(const BusData&) = delete;
BusData(BusData&&) noexcept = default;
auto operator=(const BusData&) -> BusData& = delete;
auto operator=(BusData&&) noexcept -> BusData& = default;

auto channelBuffers() const -> const float* const* { return m_channelBuffers; }
auto channelBuffers() -> float** { return m_channelBuffers; }

auto channelBuffer(ch_cnt_t channel) const -> const float*
{
assert(channel < m_channels);
return m_channelBuffers[channel];
}

auto channelBuffer(ch_cnt_t channel) -> float*
{
assert(channel < m_channels);
return m_channelBuffers[channel];
}

auto interleavedBuffer() const -> const float* { return m_interleavedBuffer; }
auto interleavedBuffer() -> float* { return m_interleavedBuffer; }

auto channels() const -> ch_cnt_t { return m_channels; }

auto startingChannel() const -> track_ch_t { return m_startingChannel; }

friend class AudioBus;

private:
//! Large buffer that all channel buffers are sourced from
float* m_sourceBuffer = nullptr;

//! Provides access to individual channel buffers within the source buffer
float** m_channelBuffers = nullptr;

//! Interleaved scratch buffer for conversions between interleaved and planar TODO: Remove once using planar only
float* m_interleavedBuffer = nullptr;

//! Number of channels in `m_channelBuffers` (`MaxChannelsPerBus` maximum) - currently only 2 is used
ch_cnt_t m_channels = 0;

//! Maps channel #0 of this bus to its track channel # within AudioBus (for performance)
track_ch_t m_startingChannel = 0;
};

AudioBus() = default;
~AudioBus();

AudioBus(const AudioBus&) = delete;
AudioBus(AudioBus&&) noexcept = default;
auto operator=(const AudioBus&) -> AudioBus& = delete;
auto operator=(AudioBus&&) noexcept -> AudioBus& = default;

//! Single bus with `frames` frames, `channels` channels, and all buffers allocated with `bufferResource`
explicit AudioBus(f_cnt_t frames, ch_cnt_t channels = DEFAULT_CHANNELS,
std::pmr::memory_resource* bufferResource = std::pmr::get_default_resource());

auto busCount() const -> bus_cnt_t { return static_cast<bus_cnt_t>(m_busses.size()); }

//! @returns the buffers of the given bus
auto buffers(bus_cnt_t busIndex) const -> PlanarBufferView<const float>
{
assert(busIndex < busCount());
const BusData& b = m_busses[busIndex];
return {b.channelBuffers(), b.channels(), m_frames};
}

//! @returns the buffers of the given bus
auto buffers(bus_cnt_t busIndex) -> PlanarBufferView<float>
{
assert(busIndex < busCount());
BusData& b = m_busses[busIndex];
return {b.channelBuffers(), b.channels(), m_frames};
}

//! @returns planar channel buffers for the given bus
auto operator[](bus_cnt_t busIndex) const -> const float* const*
{
return m_busses[busIndex].channelBuffers();
}

//! @returns planar channel buffers for the given bus
auto operator[](bus_cnt_t busIndex) -> float**
{
return m_busses[busIndex].channelBuffers();
}

//! @returns sum of all bus channel counts
auto totalChannels() const -> track_ch_t { return m_totalChannels; }

//! @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(bus_cnt_t busIndex) const -> InterleavedBufferView<const float, 2>
{
assert(m_busses[busIndex].channels() == 2);
return {m_busses[busIndex].interleavedBuffer(), m_frames};
}

//! @returns scratch buffer for conversions between interleaved and planar TODO: Remove once using planar only
auto interleavedBuffer(bus_cnt_t busIndex) -> InterleavedBufferView<float, 2>
{
assert(m_busses[busIndex].channels() == 2);
return {m_busses[busIndex].interleavedBuffer(), m_frames};
}

/**
* @brief Adds a new bus at the end of the list
* @returns the newly created bus, or nullptr upon failure
*/
auto addBus(ch_cnt_t channels) -> BusData*;

/**
* Track channels which are known to be quiet, AKA the silence status.
* 1 = track channel is known to be silent
* 0 = track channel is assumed to be non-silent (or, when silence tracking
* is enabled, known to be non-silent)
*
* NOTE: If any track 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; }

#ifdef LMMS_TESTING
auto silenceFlags() -> ChannelFlags& { return m_silenceFlags; }
#endif

/**
* When silence tracking is enabled, track 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 track 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, track 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 status of the other `AudioBus` with this `AudioBus`
void mixQuietChannels(const AudioBus& other);

/**
* Determines whether a processor has input noise given
* which track channels are routed to the processor's inputs.
*
* For `usedChannels`:
* 0 = track channel is not routed to any processor inputs
* 1 = track channel is routed to at least one processor input
*
* If the processor is sleeping and has input noise, it should wake up.
* If silence tracking is disabled, all channels are assumed to have input noise.
*/
auto hasInputNoise(const ChannelFlags& usedChannels) const -> bool;

//! Determines whether there is input noise on any channel. @see hasInputNoise
auto hasAnyInputNoise() const -> bool;

/**
* @brief Sanitizes specified track channels of any Inf/NaN values if "nanhandler" setting is enabled
*
* @param channels track channels to sanitize; 1 = selected, 0 = skip
* @param upperBound any track channel indexes at or above this are skipped
*/
void sanitize(const ChannelFlags& channels, track_ch_t upperBound = MaxTrackChannels);

//! Sanitizes all channels. @see sanitize
void sanitizeAll();

/**
* @brief Updates the silence status of the given channels, up to the upperBound index.
*
* @param channels track channels to update; 1 = selected, 0 = skip
* @param upperBound any track channel indexes at or above this are skipped
* @returns true if all updated channels were silent
*/
auto updateSilenceFlags(const ChannelFlags& channels, track_ch_t upperBound = MaxTrackChannels) -> bool;

//! Updates the silence status of all channels. @see updateSilenceFlags
auto updateAllSilenceFlags() -> bool;

/**
* @brief Silences (zeroes) the given channels
*
* @param channels track channels to silence; 1 = selected, 0 = skip
* @param upperBound any track channel indexes at or above this are skipped
*/
void silenceChannels(const ChannelFlags& channels, track_ch_t upperBound = MaxTrackChannels);

//! Silences (zeroes) all channels. @see silenceChannels
void silenceAllChannels();

//! @returns absolute peak sample value for the given channel
auto absPeakValue(bus_cnt_t busIndex, ch_cnt_t busChannel) const -> float;

private:
ArrayVector<BusData, MaxBussesPerTrack> m_busses;

//! Caches the sum of `m_busses[idx].channels()` - must never exceed MaxTrackChannels
track_ch_t m_totalChannels = 0;

const f_cnt_t m_frames = 0;

//! Allocator used by all buffers
std::pmr::polymorphic_allocator<> m_alloc;

/**
* Stores which track 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 `m_totalChannels` must always be marked silent.
*
* 1 = track channel is known to be silent
* 0 = track 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_BUS_H
5 changes: 2 additions & 3 deletions include/AudioBusHandle.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <QString>
#include <QMutex>

#include "AudioBus.h"
#include "PlayHandle.h"

namespace lmms
Expand Down Expand Up @@ -58,8 +59,6 @@ class AudioBusHandle : public ThreadableJob
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);
Expand All @@ -85,7 +84,7 @@ class AudioBusHandle : public ThreadableJob
private:
volatile bool m_bufferUsage;

SampleFrame* const m_buffer;
AudioBus m_busses;
Copy link
Contributor

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.


bool m_extOutputEnabled;
mix_ch_t m_nextMixerChannel;
Expand Down
Loading
Loading