Geometry Nodes: Reuse audio readers for sound frequency sampling

Reuse AUDASPACE readers between uncached Sample Sound Frequencies windows
instead of creating a new reader for every window.

Readers are acquired through a small cache associated with the current
`bSound` runtime generation. An RAII lease automatically returns readers after
use, while failed seek or read operations discard potentially invalid readers.

The cache retains at most two idle readers. When all cached readers are in use,
a new reader is created outside the cache lock instead of making concurrent
evaluations wait.

Cached samplers and their reader cache remain alive for evaluations already in
progress when a sound is reloaded or freed.
This commit is contained in:
Tibo Stans 2026-08-10 18:48:26 +02:00 • committed by Jacques Lucke
parent ef73004e74
commit e4218ae71a
9 changed files with 462 additions and 37 deletions

View file

@ -4,6 +4,7 @@
#pragma once
#include <memory>
#include <optional>
#include "BLI_array.hh"
@ -14,6 +15,10 @@
namespace blender::bke {
#if defined(WITH_AUDASPACE)
class SoundReaderCache;
#endif
/**
* This class allows efficiently sampling an arbitrary frequency range at an arbitrary point in
* time. This is achieved by caching the result of the fourier transform for various windows and
@ -79,8 +84,10 @@ class bSoundFrequencySampler {
mutable std::optional<Array<float, 0>> cumulative_amplitudes;
};
AUD_Sound sound_;
Key key_;
#if defined(WITH_AUDASPACE)
std::shared_ptr<SoundReaderCache> reader_cache_;
#endif
/** Derived from the sound. */
int samples_per_second_;
/**
@ -92,15 +99,15 @@ class bSoundFrequencySampler {
int window_cache_stride_;
Array<WindowCache> window_caches_;
#if defined(WITH_AUDASPACE)
/** Cached weights of the selected window function. */
const WindowWeights &window_weights_;
#endif
public:
/** Construct a new sampler, prefer using #get_cached instead. */
bSoundFrequencySampler(AUD_Sound sound, const Key &key);
/** Access a reusable frequency sampler for the given sound. */
static const bSoundFrequencySampler *get_cached(const bSound &sound, const Key &key);
static std::shared_ptr<const bSoundFrequencySampler> get_cached(const bSound &sound,
const Key &key);
/** Sample the amplitude a the given time and frequency range. */
float sample(float time,
@ -110,6 +117,12 @@ class bSoundFrequencySampler {
InterpolationMethod frequency_interpolation) const;
private:
#if defined(WITH_AUDASPACE)
/** Construct a sampler sharing the reader cache of its runtime generation. */
bSoundFrequencySampler(AUD_Sound sound,
std::shared_ptr<SoundReaderCache> reader_cache,
const Key &key);
#endif
float sample_frequency_range_in_window(int window_i,
float low,
float high,

View file

@ -269,6 +269,7 @@ set(SRC
intern/shrinkwrap.cc
intern/softbody.cc
intern/sound.cc
intern/sound_reader_cache.cc
intern/speaker.cc
intern/studiolight.cc
intern/subdiv.cc
@ -563,6 +564,7 @@ set(SRC
intern/pbvh_intern.hh
intern/pbvh_pixels_copy.hh
intern/pbvh_uv_islands.hh
intern/sound_reader_cache.hh
intern/subdiv_converter.hh
intern/subdiv_inline.hh
intern/tracking_private.hh
@ -781,6 +783,7 @@ if(WITH_GTESTS)
intern/node_socket_value_iter_test.cc
intern/path_templates_test.cc
intern/scene_test.cc
intern/sound_reader_cache_test.cc
intern/subdiv_ccg_test.cc
intern/tracking_test.cc
intern/volume_test.cc

View file

@ -15,6 +15,7 @@
#include <numeric>
#include <optional>
#include <thread>
#include <utility>
#ifdef WITH_FFTW3
# include <fftw3.h>
@ -48,6 +49,7 @@
#ifdef WITH_AUDASPACE
# include "BLI_set.hh"
# include "BLI_vector.hh"
# include <Exception.h>
# include <IReader.h>
@ -91,6 +93,8 @@
#include "BKE_sound.hh"
#include "BKE_sound_sample.hh"
#include "sound_reader_cache.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
@ -119,6 +123,14 @@ ENUM_OPERATORS(SoundTags);
using bSoundFrequencySamplerMap =
ConcurrentMap<bSoundFrequencySampler::Key, std::shared_ptr<bSoundFrequencySampler>>;
#ifdef WITH_AUDASPACE
struct SoundRuntimeGeneration {
AUD_Sound handle;
bSoundFrequencySamplerMap samplers;
std::shared_ptr<SoundReaderCache> reader_cache;
};
#endif
struct SoundRuntime {
AUD_Sound handle;
AUD_Sound cache;
@ -133,8 +145,10 @@ struct SoundRuntime {
Vector<float> *waveform = nullptr;
SoundTags tags = SoundTags::None;
/** Caches frequency samplers for this sound. */
bSoundFrequencySamplerMap samplers;
#ifdef WITH_AUDASPACE
std::mutex generation_mutex;
std::shared_ptr<SoundRuntimeGeneration> generation;
#endif
};
} // namespace bke
@ -147,6 +161,28 @@ static void sound_init_runtime(bSound *sound)
BLI_spin_init(&sound->runtime->spinlock);
}
#ifdef WITH_AUDASPACE
static void sound_runtime_generation_replace(
bSound *sound, std::shared_ptr<bke::SoundRuntimeGeneration> generation)
{
bke::SoundRuntime *runtime = sound->runtime;
std::shared_ptr<bke::SoundRuntimeGeneration> old_generation;
{
std::lock_guard lock{runtime->generation_mutex};
old_generation = std::move(runtime->generation);
runtime->generation = std::move(generation);
}
}
static std::shared_ptr<bke::SoundRuntimeGeneration> sound_runtime_generation_get(
const bSound &sound)
{
bke::SoundRuntime *runtime = sound.runtime;
std::lock_guard lock{runtime->generation_mutex};
return runtime->generation;
}
#endif
static void sound_free_waveform(bSound *sound)
{
bke::SoundRuntime *runtime = sound->runtime;
@ -363,6 +399,7 @@ static void sound_free_audio(bSound *sound)
{
#ifdef WITH_AUDASPACE
bke::SoundRuntime *runtime = sound->runtime;
sound_runtime_generation_replace(sound, nullptr);
runtime->handle.reset();
runtime->playback_handle.reset();
runtime->cache.reset();
@ -689,6 +726,7 @@ void BKE_sound_refresh_callback_bmain(Main *bmain)
static void sound_load_audio(Main *bmain, bSound *sound, bool free_waveform)
{
bke::SoundRuntime *runtime = sound->runtime;
sound_runtime_generation_replace(sound, nullptr);
runtime->cache.reset();
runtime->handle.reset();
runtime->playback_handle.reset();
@ -738,6 +776,11 @@ static void sound_load_audio(Main *bmain, bSound *sound, bool free_waveform)
else {
runtime->playback_handle = runtime->handle;
}
auto generation = std::make_shared<bke::SoundRuntimeGeneration>();
generation->handle = runtime->handle;
generation->reader_cache = std::make_shared<bke::SoundReaderCache>(generation->handle);
sound_runtime_generation_replace(sound, std::move(generation));
}
void BKE_sound_load(Main *bmain, bSound *sound)
@ -2147,25 +2190,29 @@ const Vector<float> *BKE_sound_runtime_get_waveform(const bSound *sound)
namespace bke {
const bSoundFrequencySampler *bSoundFrequencySampler::get_cached(const bSound &sound,
const Key &key)
std::shared_ptr<const bSoundFrequencySampler> bSoundFrequencySampler::get_cached(
const bSound &sound, const Key &key)
{
#ifdef WITH_AUDASPACE
const std::shared_ptr<SoundRuntimeGeneration> generation = sound_runtime_generation_get(sound);
if (!generation) {
return nullptr;
}
{
/* Fast common case when the sampler has been created already. */
bSoundFrequencySamplerMap::ConstAccessor accessor;
if (sound.runtime->samplers.lookup(accessor, key)) {
return accessor->second.get();
if (generation->samplers.lookup(accessor, key)) {
return accessor->second;
}
}
AUD_Sound sound_handle = sound.runtime->handle;
AUD_Sound sound_handle = generation->handle;
if (!sound_handle) {
/* Maybe try to load the sound in this case instead of relying on cache. */
return nullptr;
}
/* Slower case when the sampler is newly created. */
bSoundFrequencySamplerMap::MutableAccessor accessor;
if (sound.runtime->samplers.add(accessor, key)) {
if (generation->samplers.add(accessor, key)) {
if (key.channel.has_value()) {
const SoundInfo info = sound_info_get(sound_handle);
const int channel = *key.channel;
@ -2173,15 +2220,17 @@ const bSoundFrequencySampler *bSoundFrequencySampler::get_cached(const bSound &s
return nullptr;
}
}
accessor->second = std::make_shared<bSoundFrequencySampler>(sound_handle, key);
accessor->second = std::shared_ptr<bSoundFrequencySampler>(
new bSoundFrequencySampler(sound_handle, generation->reader_cache, key));
}
return accessor->second.get();
return accessor->second;
#else
UNUSED_VARS(sound, key);
return nullptr;
#endif
}
#ifdef WITH_AUDASPACE
static bSoundFrequencySampler::WindowWeights compute_window_function_weights(
const bSoundFrequencySampler::WindowFunction window, const int size)
{
@ -2229,14 +2278,17 @@ static const bSoundFrequencySampler::WindowWeights &get_window_function_weights(
compute_window_function_weights(window, size));
});
}
#endif
bSoundFrequencySampler::bSoundFrequencySampler(AUD_Sound sound, const Key &key)
: sound_(sound),
key_(key),
#ifdef WITH_AUDASPACE
bSoundFrequencySampler::bSoundFrequencySampler(AUD_Sound sound,
std::shared_ptr<SoundReaderCache> reader_cache,
const Key &key)
: key_(key),
reader_cache_(std::move(reader_cache)),
window_weights_(get_window_function_weights(key.window_function, key.fft_size))
{
#ifdef WITH_AUDASPACE
const SoundInfo info = bke::sound_info_get(sound_);
const SoundInfo info = bke::sound_info_get(sound);
samples_per_second_ = info.specs.samplerate;
/* This could be a parameter but a single fixed value seems fine for now and makes caching much
* simpler. */
@ -2244,11 +2296,8 @@ bSoundFrequencySampler::bSoundFrequencySampler(AUD_Sound sound, const Key &key)
const int window_caches_num = std::ceil(info.length * info.specs.samplerate /
window_cache_stride_);
window_caches_.reinitialize(window_caches_num);
#else
UNUSED_VARS(sound, key);
BLI_assert_unreachable();
#endif
}
#endif
std::optional<Array<float>> bSoundFrequencySampler::compute_fft(const int start_sample) const
{
@ -2261,17 +2310,26 @@ std::optional<Array<float>> bSoundFrequencySampler::compute_fft(const int start_
* because the #read function may sometimes give invalid data for the first samples. */
const int warmup_samples = std::min(2000, start_sample);
/* Prepare the reader. */
std::shared_ptr<aud::IReader> reader = sound_->createReader();
const aud::Specs specs = reader->getSpecs();
const int channels_num = specs.channels;
/* Read the raw samples from the audio stream. */
Array<float> read_buffer_extra((key_.fft_size + warmup_samples) * channels_num);
int channels_num;
Array<float> read_buffer_extra;
bool is_end_of_stream = false;
int length = key_.fft_size + warmup_samples;
reader->seek(std::max(start_sample - warmup_samples, 0));
reader->read(length, is_end_of_stream, read_buffer_extra.data());
{
SoundReaderLease reader = reader_cache_->acquire();
if (!reader) {
return std::nullopt;
}
channels_num = reader->getSpecs().channels;
read_buffer_extra.reinitialize((key_.fft_size + warmup_samples) * channels_num);
try {
reader->seek(std::max(start_sample - warmup_samples, 0));
reader->read(length, is_end_of_stream, read_buffer_extra.data());
}
catch (...) {
reader.discard();
throw;
}
}
const Span<float> read_buffer = read_buffer_extra.as_span().drop_front(warmup_samples *
channels_num);
const int read_length = read_buffer.size() / channels_num;

View file

@ -0,0 +1,121 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*/
#include "sound_reader_cache.hh"
#if defined(WITH_AUDASPACE)
# include <utility>
# include "BLI_mutex.hh"
# include "BLI_vector.hh"
# include <IReader.h>
# include <ISound.h>
namespace blender::bke {
struct SoundReaderCacheState {
static constexpr int max_idle_readers = 2;
explicit SoundReaderCacheState(std::shared_ptr<aud::ISound> sound) : sound(std::move(sound)) {}
std::shared_ptr<aud::ISound> sound;
Mutex mutex;
Vector<std::shared_ptr<aud::IReader>, max_idle_readers> idle_readers;
void release(std::shared_ptr<aud::IReader> reader) noexcept
{
if (!reader) {
return;
}
std::lock_guard lock{this->mutex};
if (this->idle_readers.size() < max_idle_readers) {
this->idle_readers.append(std::move(reader));
}
}
};
SoundReaderLease::SoundReaderLease(std::shared_ptr<SoundReaderCacheState> state,
std::shared_ptr<aud::IReader> reader)
: state_(std::move(state)), reader_(std::move(reader))
{
}
SoundReaderLease::~SoundReaderLease()
{
this->release();
}
SoundReaderLease::SoundReaderLease(SoundReaderLease &&other) noexcept
: state_(std::move(other.state_)), reader_(std::move(other.reader_))
{
}
SoundReaderLease &SoundReaderLease::operator=(SoundReaderLease &&other) noexcept
{
if (this != &other) {
this->release();
this->state_ = std::move(other.state_);
this->reader_ = std::move(other.reader_);
}
return *this;
}
SoundReaderLease::operator bool() const
{
return bool(this->reader_);
}
aud::IReader *SoundReaderLease::operator->() const
{
return this->reader_.get();
}
void SoundReaderLease::discard() noexcept
{
this->reader_.reset();
this->state_.reset();
}
void SoundReaderLease::release() noexcept
{
if (this->state_) {
this->state_->release(std::move(this->reader_));
this->state_.reset();
}
}
SoundReaderCache::SoundReaderCache(std::shared_ptr<aud::ISound> sound)
: state_(std::make_shared<SoundReaderCacheState>(std::move(sound)))
{
}
SoundReaderLease SoundReaderCache::acquire()
{
std::shared_ptr<SoundReaderCacheState> state = this->state_;
std::shared_ptr<aud::IReader> reader;
{
std::lock_guard lock{state->mutex};
if (!state->idle_readers.is_empty()) {
reader = state->idle_readers.pop_last();
}
}
if (!reader && state->sound) {
reader = state->sound->createReader();
}
if (!reader) {
return {};
}
return SoundReaderLease(std::move(state), std::move(reader));
}
} // namespace blender::bke
#endif

View file

@ -0,0 +1,76 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#if defined(WITH_AUDASPACE)
# include <memory>
# include "BLI_utility_mixins.hh"
namespace aud {
class IReader;
class ISound;
} // namespace aud
namespace blender::bke {
struct SoundReaderCacheState;
class SoundReaderCache;
/** Exclusive access to a sound reader. The reader returns to its cache on destruction. */
class SoundReaderLease : NonCopyable {
private:
/**
* Keep the shared cache state alive so a lease can return its reader after the outer cache has
* been replaced or destroyed.
*/
std::shared_ptr<SoundReaderCacheState> state_;
std::shared_ptr<aud::IReader> reader_;
SoundReaderLease(std::shared_ptr<SoundReaderCacheState> state,
std::shared_ptr<aud::IReader> reader);
void release() noexcept;
friend SoundReaderCache;
public:
SoundReaderLease() = default;
~SoundReaderLease();
SoundReaderLease(SoundReaderLease &&other) noexcept;
SoundReaderLease &operator=(SoundReaderLease &&other) noexcept;
explicit operator bool() const;
aud::IReader *operator->() const;
/**
* Destroy the reader instead of returning it to the cache.
*
* This is used when a failed reader operation may have left its state invalid.
*/
void discard() noexcept;
};
/**
* Runtime-only reusable reader state associated with a #bSound runtime.
*
* Active readers belong to leases, not the idle-reader vector.
*/
class SoundReaderCache : NonCopyable {
private:
std::shared_ptr<SoundReaderCacheState> state_;
public:
explicit SoundReaderCache(std::shared_ptr<aud::ISound> sound);
/** Acquire a reader. Returns an empty lease when reader creation returns null. */
SoundReaderLease acquire();
};
} // namespace blender::bke
#endif

View file

@ -0,0 +1,147 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "sound_reader_cache.hh"
#include "testing/testing.h"
#if defined(WITH_AUDASPACE)
# include <memory>
# include <utility>
# include <IReader.h>
# include <ISound.h>
# include <util/Buffer.h>
# include <util/StreamBuffer.h>
namespace blender::bke::tests {
struct ReaderControl {
int readers_created = 0;
int live_readers = 0;
};
class CountingSound : public aud::ISound {
private:
std::shared_ptr<aud::ISound> sound;
public:
std::shared_ptr<ReaderControl> control = std::make_shared<ReaderControl>();
explicit CountingSound(std::shared_ptr<aud::ISound> sound) : sound(std::move(sound)) {}
std::shared_ptr<aud::IReader> createReader() override
{
this->control->readers_created++;
std::shared_ptr<aud::IReader> reader = this->sound->createReader();
this->control->live_readers++;
aud::IReader *reader_pointer = reader.get();
return std::shared_ptr<aud::IReader>(
reader_pointer,
[reader = std::move(reader), control = this->control](aud::IReader *) mutable {
reader.reset();
control->live_readers--;
});
}
};
static std::shared_ptr<CountingSound> create_test_sound()
{
auto buffer = std::make_shared<aud::Buffer>(sizeof(aud::sample_t));
auto stream = std::make_shared<aud::StreamBuffer>(
buffer, aud::Specs{aud::RATE_48000, aud::CHANNELS_MONO});
return std::make_shared<CountingSound>(std::move(stream));
}
TEST(sound_reader_cache, ReusesReaderAndLimitsIdleReaders)
{
const std::shared_ptr<CountingSound> sound = create_test_sound();
SoundReaderCache cache(sound);
{
SoundReaderLease lease = cache.acquire();
ASSERT_TRUE(lease);
}
{
SoundReaderLease lease = cache.acquire();
ASSERT_TRUE(lease);
}
EXPECT_EQ(sound->control->readers_created, 1);
SoundReaderLease first = cache.acquire();
SoundReaderLease second = cache.acquire();
SoundReaderLease third = cache.acquire();
ASSERT_TRUE(first);
ASSERT_TRUE(second);
ASSERT_TRUE(third);
EXPECT_EQ(sound->control->readers_created, 3);
first = {};
second = {};
third = {};
EXPECT_EQ(sound->control->live_readers, 2);
SoundReaderLease reused_first = cache.acquire();
SoundReaderLease reused_second = cache.acquire();
ASSERT_TRUE(reused_first);
ASSERT_TRUE(reused_second);
EXPECT_EQ(sound->control->readers_created, 3);
}
TEST(sound_reader_cache, DiscardDestroysReader)
{
const std::shared_ptr<CountingSound> sound = create_test_sound();
SoundReaderCache cache(sound);
{
SoundReaderLease lease = cache.acquire();
ASSERT_TRUE(lease);
lease.discard();
EXPECT_FALSE(lease);
}
EXPECT_EQ(sound->control->live_readers, 0);
SoundReaderLease replacement = cache.acquire();
ASSERT_TRUE(replacement);
EXPECT_EQ(sound->control->readers_created, 2);
}
TEST(sound_reader_cache, MoveOperationsReturnEachReaderOnce)
{
const std::shared_ptr<CountingSound> sound = create_test_sound();
SoundReaderCache cache(sound);
SoundReaderLease first = cache.acquire();
ASSERT_TRUE(first);
SoundReaderLease moved(std::move(first));
EXPECT_FALSE(first);
SoundReaderLease second = cache.acquire();
ASSERT_TRUE(second);
moved = std::move(second);
EXPECT_FALSE(second);
moved = {};
SoundReaderLease reused_first = cache.acquire();
SoundReaderLease reused_second = cache.acquire();
ASSERT_TRUE(reused_first);
ASSERT_TRUE(reused_second);
EXPECT_EQ(sound->control->readers_created, 2);
}
TEST(sound_reader_cache, LeaseOutlivesCache)
{
const std::shared_ptr<CountingSound> sound = create_test_sound();
auto cache = std::make_shared<SoundReaderCache>(sound);
SoundReaderLease lease = cache->acquire();
ASSERT_TRUE(lease);
cache.reset();
EXPECT_EQ(sound->control->live_readers, 1);
lease = {};
EXPECT_EQ(sound->control->live_readers, 0);
}
} // namespace blender::bke::tests
#endif

View file

@ -178,8 +178,8 @@ class SampleSoundFunction : public mf::MultiFunction {
key.window_function = window_function_;
key.fft_size = fft_size_;
key.channel = *all_channels_value ? std::nullopt : channel_value;
const bke::bSoundFrequencySampler *sampler = bke::bSoundFrequencySampler::get_cached(sound_,
key);
const std::shared_ptr<const bke::bSoundFrequencySampler> sampler =
bke::bSoundFrequencySampler::get_cached(sound_, key);
if (!sampler) {
index_mask::masked_fill(amplitudes, 0.0f, mask);
return;
@ -227,8 +227,8 @@ class SampleSoundFunction : public mf::MultiFunction {
key.window_function = window_function_;
key.fft_size = fft_size_;
key.channel = channel;
const bke::bSoundFrequencySampler *sampler = bke::bSoundFrequencySampler::get_cached(sound_,
key);
const std::shared_ptr<const bke::bSoundFrequencySampler> sampler =
bke::bSoundFrequencySampler::get_cached(sound_, key);
if (!sampler) {
amplitudes.fill_indices(indices, 0.0f);
return;

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab0828ab72d48aa15bd9290b98bfc07dd243713b6fbf7887f51af119b0879988
size 103687

View file

@ -1281,6 +1281,10 @@ if(TEST_SRC_DIR_EXISTS)
vector
)
if(WITH_AUDASPACE AND WITH_FFTW3)
list(APPEND geo_node_tests sound)
endif()
if(WITH_GMP)
list(APPEND geo_node_tests mesh/boolean)
endif()