33#include "../Core/AudioBuffer.h"
34#include "../Core/AudioSpec.h"
35#include "../Core/DenormalGuard.h"
36#include "../Core/DspMath.h"
37#include "../Core/StateBlob.h"
78 if (!std::isfinite(bufferSeconds)) bufferSeconds = 4.0;
79 prepared_.store(
false, std::memory_order_relaxed);
84 while (size <
static_cast<int>(sampleRate_ * std::clamp(bufferSeconds, 0.5, 16.0)))
87 for (
int ch = 0; ch < 2; ++ch)
88 ring_[ch].assign(
static_cast<size_t>(size), T(0));
90 window_.resize(kWindowSize);
91 for (
int i = 0; i < kWindowSize; ++i)
92 window_[
static_cast<size_t>(i)] =
static_cast<T
>(
93 0.5 - 0.5 * std::cos(2.0 * 3.14159265358979 * i / (kWindowSize - 1)));
95 prepared_.store(
true, std::memory_order_relaxed);
102 if (!prepared_.load(std::memory_order_relaxed))
return;
103 for (
auto& r : ring_) std::fill(r.begin(), r.end(), T(0));
104 for (
auto& g : grains_) g.active =
false;
108 currentMix_ = mix_.load(std::memory_order_relaxed);
117 if (!std::isfinite(ms))
return;
118 grainMs_.store(std::clamp(ms, T(10), T(500)), std::memory_order_relaxed);
125 if (!std::isfinite(perSecond))
return;
126 density_.store(std::clamp(perSecond, T(1), T(200)), std::memory_order_relaxed);
133 if (!std::isfinite(amount))
return;
134 jitter_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
141 if (!std::isfinite(semitones))
return;
142 pitchSt_.store(std::clamp(semitones, T(-24), T(24)), std::memory_order_relaxed);
149 if (!std::isfinite(semitones))
return;
150 pitchJitterSt_.store(std::clamp(semitones, T(0), T(12)), std::memory_order_relaxed);
157 if (!std::isfinite(amount))
return;
158 spread_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
164 freeze_.store(frozen, std::memory_order_relaxed);
171 if (!std::isfinite(mix))
return;
172 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
175 [[nodiscard]] T
getGrainSize() const noexcept {
return grainMs_.load(std::memory_order_relaxed); }
176 [[nodiscard]] T
getDensity() const noexcept {
return density_.load(std::memory_order_relaxed); }
177 [[nodiscard]] T
getJitter() const noexcept {
return jitter_.load(std::memory_order_relaxed); }
178 [[nodiscard]] T
getPitch() const noexcept {
return pitchSt_.load(std::memory_order_relaxed); }
179 [[nodiscard]] T
getPitchJitter() const noexcept {
return pitchJitterSt_.load(std::memory_order_relaxed); }
180 [[nodiscard]] T
getSpread() const noexcept {
return spread_.load(std::memory_order_relaxed); }
181 [[nodiscard]]
bool getFreeze() const noexcept {
return freeze_.load(std::memory_order_relaxed); }
182 [[nodiscard]] T
getMix() const noexcept {
return mix_.load(std::memory_order_relaxed); }
185 [[nodiscard]]
static constexpr int getLatency() noexcept {
return 0; }
188 [[nodiscard]] std::vector<uint8_t>
getState()
const
193 w.
write(
"grainMs",
static_cast<float>(grainMs_.load(std::memory_order_relaxed)));
194 w.
write(
"density",
static_cast<float>(density_.load(std::memory_order_relaxed)));
195 w.
write(
"jitter",
static_cast<float>(jitter_.load(std::memory_order_relaxed)));
196 w.
write(
"pitch",
static_cast<float>(pitchSt_.load(std::memory_order_relaxed)));
197 w.
write(
"pitchJitter",
static_cast<float>(pitchJitterSt_.load(std::memory_order_relaxed)));
198 w.
write(
"spread",
static_cast<float>(spread_.load(std::memory_order_relaxed)));
199 w.
write(
"freeze", freeze_.load(std::memory_order_relaxed));
200 w.
write(
"mix",
static_cast<float>(mix_.load(std::memory_order_relaxed)));
226 if (!prepared_.load(std::memory_order_relaxed))
return;
229 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
230 const int nS = buffer.getNumSamples();
231 if (nCh == 0 || nS == 0)
return;
233 const bool frozen = freeze_.load(std::memory_order_relaxed);
238 const T mixTarget = mix_.load(std::memory_order_relaxed);
239 const T mixStart = currentMix_;
240 const T mixStep = (mixTarget - mixStart) /
static_cast<T
>(nS);
241 const double spawnPerSample =
242 static_cast<double>(density_.load(std::memory_order_relaxed)) / sampleRate_;
244 for (
int i = 0; i < nS; ++i)
249 ring_[0][
static_cast<size_t>(writePos_)] = buffer.getChannel(0)[i];
250 ring_[1][
static_cast<size_t>(writePos_)] =
251 buffer.getChannel(nCh > 1 ? 1 : 0)[i];
252 writePos_ = (writePos_ + 1) & ringMask_;
256 spawnAcc_ += spawnPerSample;
257 while (spawnAcc_ >= 1.0)
264 T outL = T(0), outR = T(0);
265 for (
auto& g : grains_)
267 if (!g.active)
continue;
269 const auto idx =
static_cast<int64_t
>(g.pos);
270 const T frac =
static_cast<T
>(g.pos -
static_cast<double>(idx));
271 const int i0 =
static_cast<int>(idx) & ringMask_;
272 const int i1 = (i0 + 1) & ringMask_;
274 const double wPos = g.phase * (kWindowSize - 1);
275 const auto wIdx =
static_cast<int>(wPos);
276 const T wFrac =
static_cast<T
>(wPos - wIdx);
277 const T w = window_[
static_cast<size_t>(wIdx)]
278 + (window_[
static_cast<size_t>(std::min(wIdx + 1, kWindowSize - 1))]
279 - window_[
static_cast<size_t>(wIdx)]) * wFrac;
281 const T sL = ring_[0][
static_cast<size_t>(i0)]
282 + (ring_[0][
static_cast<size_t>(i1)] - ring_[0][
static_cast<size_t>(i0)]) * frac;
283 const T sR = ring_[1][
static_cast<size_t>(i0)]
284 + (ring_[1][
static_cast<size_t>(i1)] - ring_[1][
static_cast<size_t>(i0)]) * frac;
286 outL += sL * w * g.gainL;
287 outR += sR * w * g.gainR;
290 g.phase += g.phaseInc;
295 const T mixVal = mixStart + mixStep *
static_cast<T
>(i);
296 const T dryL = buffer.getChannel(0)[i];
297 buffer.getChannel(0)[i] = dryL + (outL - dryL) * mixVal;
300 const T dryR = buffer.getChannel(1)[i];
301 buffer.getChannel(1)[i] = dryR + (outR - dryR) * mixVal;
304 currentMix_ = mixTarget;
308 static constexpr int kWindowSize = 2048;
316 double phaseInc = 0.0;
317 T gainL = T(0), gainR = T(0);
320 [[nodiscard]]
double frand() noexcept
322 rng_ = rng_ * 1664525u + 1013904223u;
323 return static_cast<double>(rng_ >> 8) / 16777216.0;
326 void spawnGrain() noexcept
328 for (
auto& g : grains_)
330 if (g.active)
continue;
332 const double sizeMs =
static_cast<double>(grainMs_.load(std::memory_order_relaxed));
333 const double lenSamples = sizeMs * 0.001 * sampleRate_;
334 const double jit =
static_cast<double>(jitter_.load(std::memory_order_relaxed));
335 const double st =
static_cast<double>(pitchSt_.load(std::memory_order_relaxed))
336 + (frand() * 2.0 - 1.0)
337 *
static_cast<double>(pitchJitterSt_.load(std::memory_order_relaxed));
338 const double rate = std::exp2(st / 12.0);
342 const double maxSpan = lenSamples * std::max(rate, 1.0) + 64.0;
343 const double jitterSpan = jit * 0.5 *
static_cast<double>(ringMask_ + 1 - maxSpan - 64.0);
344 const double back = maxSpan + frand() * std::max(jitterSpan, 0.0);
345 g.pos =
static_cast<double>(writePos_) - back;
346 while (g.pos < 0.0) g.pos +=
static_cast<double>(ringMask_ + 1);
350 g.phaseInc = 1.0 / lenSamples;
352 const double spreadAmt =
static_cast<double>(spread_.load(std::memory_order_relaxed));
353 const double pan = 0.5 + (frand() - 0.5) * spreadAmt;
354 const double a = pan * 1.5707963267948966;
356 const double overlap = std::max(1.0,
357 static_cast<double>(density_.load(std::memory_order_relaxed))
359 const auto norm =
static_cast<T
>(1.0 / std::sqrt(overlap));
360 g.gainL =
static_cast<T
>(std::cos(a)) * norm;
361 g.gainR =
static_cast<T
>(std::sin(a)) * norm;
368 double sampleRate_ = 48000.0;
369 int numChannels_ = 0;
370 std::atomic<bool> prepared_ {
false };
372 std::array<std::vector<T>, 2> ring_;
376 std::vector<T> window_;
377 std::array<Grain, kMaxGrains> grains_;
378 double spawnAcc_ = 0.0;
379 uint32_t rng_ = 0x9E3779B9u;
380 T currentMix_ = T(1);
382 std::atomic<T> grainMs_ { T(80) };
383 std::atomic<T> density_ { T(25) };
384 std::atomic<T> jitter_ { T(0.3) };
385 std::atomic<T> pitchSt_ { T(0) };
386 std::atomic<T> pitchJitterSt_ { T(0) };
387 std::atomic<T> spread_ { T(0.5) };
388 std::atomic<bool> freeze_ {
false };
389 std::atomic<T> mix_ { T(1) };
Non-owning view over audio channel data.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Granular clouds and spectral-freeze textures from live input.
void setPitch(T semitones) noexcept
Per-grain pitch shift in semitones [-24, +24] (default 0). Non-finite values are ignored.
void prepare(const AudioSpec &spec, double bufferSeconds=4.0)
Allocates the capture ring and grain pool.
void setDensity(T perSecond) noexcept
Grains per second [1, 200] (default 25). Non-finite values are ignored.
T getSpread() const noexcept
void setGrainSize(T ms) noexcept
Grain duration in milliseconds [10, 500] (default 80). Non-finite values are ignored.
void setSpread(T amount) noexcept
Stereo spread of grain panning [0, 1] (default 0.5). Non-finite values are ignored.
T getJitter() const noexcept
void setFreeze(bool frozen) noexcept
Freezes capture: grains keep playing the held history.
void reset() noexcept
Kills all grains and clears the capture ring. RT-safe.
void setMix(T mix) noexcept
Dry/wet mix [0, 1] (default 1); smoothed linearly over one block. Non-finite values are ignored.
T getGrainSize() const noexcept
void processBlock(AudioBufferView< T > buffer) noexcept
Processes a block in-place (1 or 2 channels). Pass-through until prepare() succeeds.
void setPitchJitter(T semitones) noexcept
Random pitch spread per grain in semitones [0, 12] (default 0). Non-finite values are ignored.
T getPitchJitter() const noexcept
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
static constexpr int kMaxGrains
T getMix() const noexcept
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
T getPitch() const noexcept
void setJitter(T amount) noexcept
Position jitter [0, 1]: how far back grains may start (default 0.3). Non-finite values are ignored.
bool getFreeze() const noexcept
static constexpr int getLatency() noexcept
Zero: the cloud is parallel to the dry path.
T getDensity() const noexcept
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
bool isValid() const noexcept
uint32_t processorId() const noexcept
Serializes key/value parameters into a versioned blob.
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
void write(const char *key, float value)
Writes a float parameter.
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Describes the audio environment for a DSP processor.
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
double sampleRate
Sample rate in Hz.