mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 13:20:54 +02:00
performance improvements + oscilloscope
This commit is contained in:
@@ -3,6 +3,14 @@
|
||||
#include <cmath>
|
||||
|
||||
namespace Visualizer {
|
||||
namespace {
|
||||
|
||||
float safeFilterFrequency(float frequency, float sampleRate) {
|
||||
const float nyquistSafe = std::max(20.0f, sampleRate * 0.45f);
|
||||
return std::clamp(frequency, 20.0f, nyquistSafe);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Oscilloscope::Oscilloscope()
|
||||
: sampleRate_(48000.0f)
|
||||
@@ -29,6 +37,10 @@ Oscilloscope::Oscilloscope()
|
||||
// Initialize analysis and render buffers
|
||||
displayBuffer_.resize(OSCILLOSCOPE_BUFFER_SIZE, 0.0f);
|
||||
visualBuffer_.resize(OSCILLOSCOPE_BUFFER_SIZE, 0.0f);
|
||||
pitchAnalysisBuffer_.resize(2048, 0.0f);
|
||||
pitchWindowedBuffer_.resize(2048, 0.0f);
|
||||
pitchMagnitudes_.resize(1024, 0.0f);
|
||||
pitchFft_ = std::make_unique<DSP::FFT>(2048);
|
||||
|
||||
// Initialize display filters (high shelf + cascaded lowpass for steep rolloff)
|
||||
displayShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f);
|
||||
@@ -42,20 +54,23 @@ Oscilloscope::Oscilloscope()
|
||||
|
||||
void Oscilloscope::setSampleRate(float sampleRate) {
|
||||
sampleRate_ = sampleRate;
|
||||
const float shelfFrequency = safeFilterFrequency(400.0f, sampleRate_);
|
||||
const float lowpassFrequency = safeFilterFrequency(18000.0f, sampleRate_);
|
||||
|
||||
// Redesign filter with new sample rate (10% bandwidth)
|
||||
float bandwidth = lastFilterPitch_ * 0.1f;
|
||||
bandpassFilter_.designBandpass(lastFilterPitch_, bandwidth, sampleRate_, 60.0f);
|
||||
// Update high shelf for new sample rate
|
||||
pitchAnalysisShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f);
|
||||
pitchAnalysisShelf_.setHighShelf(shelfFrequency, sampleRate_, -3.0f, 0.71f);
|
||||
|
||||
// Update display filters
|
||||
displayShelf_.setHighShelf(400.0f, sampleRate_, -3.0f, 0.71f);
|
||||
displayLowpass1_.setLowpass(18000.0f, sampleRate_, 0.707f);
|
||||
displayLowpass2_.setLowpass(18000.0f, sampleRate_, 0.707f);
|
||||
displayShelf_.setHighShelf(shelfFrequency, sampleRate_, -3.0f, 0.71f);
|
||||
displayLowpass1_.setLowpass(lowpassFrequency, sampleRate_, 0.707f);
|
||||
displayLowpass2_.setLowpass(lowpassFrequency, sampleRate_, 0.707f);
|
||||
|
||||
// Update pitch detection lowpass
|
||||
pitchLowpass1_.setLowpass(18000.0f, sampleRate_, 0.707f);
|
||||
pitchLowpass2_.setLowpass(18000.0f, sampleRate_, 0.707f);
|
||||
pitchLowpass1_.setLowpass(lowpassFrequency, sampleRate_, 0.707f);
|
||||
pitchLowpass2_.setLowpass(lowpassFrequency, sampleRate_, 0.707f);
|
||||
}
|
||||
|
||||
void Oscilloscope::setPitchLock(bool enabled) {
|
||||
@@ -150,27 +165,26 @@ OscilloscopeResult Oscilloscope::process() {
|
||||
// Detect pitch from recent samples in circular buffer
|
||||
// Use RAW buffer for pitch detection (filtered buffer may attenuate the fundamental)
|
||||
// Use last 2048 samples for pitch detection
|
||||
std::vector<float> recentSamples(2048);
|
||||
for (size_t i = 0; i < 2048; i++) {
|
||||
size_t idx = (writePos_ + OSCILLOSCOPE_BUFFER_SIZE - 2048 + i) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
recentSamples[i] = displayBuffer_[idx]; // Use RAW samples, not filtered
|
||||
pitchAnalysisBuffer_[i] = displayBuffer_[idx]; // Use RAW samples, not filtered
|
||||
}
|
||||
|
||||
// Apply high shelf filter to reduce HF interference with pitch detection
|
||||
pitchAnalysisShelf_.reset();
|
||||
for (size_t i = 0; i < 2048; i++) {
|
||||
recentSamples[i] = pitchAnalysisShelf_.process(recentSamples[i]);
|
||||
pitchAnalysisBuffer_[i] = pitchAnalysisShelf_.process(pitchAnalysisBuffer_[i]);
|
||||
}
|
||||
|
||||
// Apply cascaded lowpass for steep HF rejection
|
||||
pitchLowpass1_.reset();
|
||||
pitchLowpass2_.reset();
|
||||
for (size_t i = 0; i < 2048; i++) {
|
||||
recentSamples[i] = pitchLowpass1_.process(recentSamples[i]);
|
||||
recentSamples[i] = pitchLowpass2_.process(recentSamples[i]);
|
||||
pitchAnalysisBuffer_[i] = pitchLowpass1_.process(pitchAnalysisBuffer_[i]);
|
||||
pitchAnalysisBuffer_[i] = pitchLowpass2_.process(pitchAnalysisBuffer_[i]);
|
||||
}
|
||||
|
||||
float newPitch = DSP::detectPitchFFT(recentSamples.data(), 2048, sampleRate_, 40.0f, 1000.0f);
|
||||
float newPitch = detectPitchFFTReused(pitchAnalysisBuffer_.data(), 2048, 40.0f, 1000.0f);
|
||||
if (newPitch > 0.0f) {
|
||||
pitchSamplesProcessed_++;
|
||||
|
||||
@@ -263,44 +277,97 @@ void Oscilloscope::getSamples(float* output, size_t startPos, size_t count) cons
|
||||
// This preserves the high-precision trigger position from zero-crossing detection
|
||||
void Oscilloscope::getSamplesInterpolated(float* output, float startPos, size_t count) const {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float pos = startPos + static_cast<float>(i);
|
||||
output[i] = sampleInterpolated(startPos + static_cast<float>(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap position to buffer bounds
|
||||
while (pos < 0) pos += OSCILLOSCOPE_BUFFER_SIZE;
|
||||
while (pos >= OSCILLOSCOPE_BUFFER_SIZE) pos -= OSCILLOSCOPE_BUFFER_SIZE;
|
||||
void Oscilloscope::getSamplesInterpolated(float* output, float startPos, size_t count, float step) const {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
output[i] = sampleInterpolated(startPos + static_cast<float>(i) * step);
|
||||
}
|
||||
}
|
||||
|
||||
size_t idx = static_cast<size_t>(pos) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
float frac = pos - std::floor(pos);
|
||||
float Oscilloscope::sampleInterpolated(float pos) const {
|
||||
// Wrap position to buffer bounds
|
||||
while (pos < 0) pos += OSCILLOSCOPE_BUFFER_SIZE;
|
||||
while (pos >= OSCILLOSCOPE_BUFFER_SIZE) pos -= OSCILLOSCOPE_BUFFER_SIZE;
|
||||
|
||||
if (frac < 0.0001f) {
|
||||
// No interpolation needed - exact sample position
|
||||
output[i] = visualBuffer_[idx];
|
||||
} else {
|
||||
// Cubic (Catmull-Rom) interpolation for smooth sub-sample rendering
|
||||
// This eliminates pixel-level ghosting/jitter from truncated trigger positions
|
||||
size_t i0 = (idx + OSCILLOSCOPE_BUFFER_SIZE - 1) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
size_t i1 = idx;
|
||||
size_t i2 = (idx + 1) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
size_t i3 = (idx + 2) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
size_t idx = static_cast<size_t>(pos) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
float frac = pos - std::floor(pos);
|
||||
|
||||
float y0 = visualBuffer_[i0];
|
||||
float y1 = visualBuffer_[i1];
|
||||
float y2 = visualBuffer_[i2];
|
||||
float y3 = visualBuffer_[i3];
|
||||
if (frac < 0.0001f) {
|
||||
// No interpolation needed - exact sample position
|
||||
return visualBuffer_[idx];
|
||||
}
|
||||
|
||||
// Catmull-Rom spline coefficients
|
||||
float t = frac;
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
// Cubic (Catmull-Rom) interpolation for smooth sub-sample rendering.
|
||||
size_t i0 = (idx + OSCILLOSCOPE_BUFFER_SIZE - 1) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
size_t i1 = idx;
|
||||
size_t i2 = (idx + 1) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
size_t i3 = (idx + 2) % OSCILLOSCOPE_BUFFER_SIZE;
|
||||
|
||||
output[i] = 0.5f * (
|
||||
(2.0f * y1) +
|
||||
(-y0 + y2) * t +
|
||||
(2.0f * y0 - 5.0f * y1 + 4.0f * y2 - y3) * t2 +
|
||||
(-y0 + 3.0f * y1 - 3.0f * y2 + y3) * t3
|
||||
);
|
||||
float y0 = visualBuffer_[i0];
|
||||
float y1 = visualBuffer_[i1];
|
||||
float y2 = visualBuffer_[i2];
|
||||
float y3 = visualBuffer_[i3];
|
||||
|
||||
float t = frac;
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
|
||||
return 0.5f * (
|
||||
(2.0f * y1) +
|
||||
(-y0 + y2) * t +
|
||||
(2.0f * y0 - 5.0f * y1 + 4.0f * y2 - y3) * t2 +
|
||||
(-y0 + 3.0f * y1 - 3.0f * y2 + y3) * t3
|
||||
);
|
||||
}
|
||||
|
||||
float Oscilloscope::detectPitchFFTReused(const float* data, size_t length, float minFreq, float maxFreq) {
|
||||
const size_t fftSize = 2048;
|
||||
if (length < fftSize || !pitchFft_) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < fftSize; i++) {
|
||||
float win = 0.5f * (1.0f - cosf(2.0f * static_cast<float>(M_PI) * i / fftSize));
|
||||
pitchWindowedBuffer_[i] = data[i] * win;
|
||||
}
|
||||
|
||||
pitchFft_->forward(pitchWindowedBuffer_.data(), pitchMagnitudes_.data());
|
||||
|
||||
int minBin = std::max(1, static_cast<int>(minFreq * fftSize / sampleRate_));
|
||||
int maxBin = std::min(static_cast<int>(fftSize / 2 - 1), static_cast<int>(maxFreq * fftSize / sampleRate_));
|
||||
if (minBin >= maxBin) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float peakMag = 0.0f;
|
||||
int peakBin = minBin;
|
||||
for (int i = minBin; i <= maxBin; i++) {
|
||||
if (pitchMagnitudes_[i] > peakMag) {
|
||||
peakMag = pitchMagnitudes_[i];
|
||||
peakBin = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (peakMag < 1e-6f) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (peakBin > 0 && peakBin < static_cast<int>(fftSize / 2) - 1) {
|
||||
float y1 = pitchMagnitudes_[peakBin - 1];
|
||||
float y2 = pitchMagnitudes_[peakBin];
|
||||
float y3 = pitchMagnitudes_[peakBin + 1];
|
||||
float denom = y1 - 2.0f * y2 + y3;
|
||||
if (std::abs(denom) > 1e-9f) {
|
||||
float offset = 0.5f * (y1 - y3) / denom;
|
||||
offset = std::clamp(offset, -0.5f, 0.5f);
|
||||
return (static_cast<float>(peakBin) + offset) * sampleRate_ / static_cast<float>(fftSize);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<float>(peakBin) * sampleRate_ / static_cast<float>(fftSize);
|
||||
}
|
||||
|
||||
void Oscilloscope::reset() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "dsp_utils.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
|
||||
// Get samples with sub-sample interpolation (preserves trigger precision)
|
||||
void getSamplesInterpolated(float* output, float startPos, size_t count) const;
|
||||
void getSamplesInterpolated(float* output, float startPos, size_t count, float step) const;
|
||||
|
||||
// Reset state
|
||||
void reset();
|
||||
@@ -76,10 +78,16 @@ private:
|
||||
float lastTrigger_;
|
||||
float smoothedPitch_;
|
||||
int pitchSamplesProcessed_; // Track samples for adaptive smoothing
|
||||
std::vector<float> pitchAnalysisBuffer_;
|
||||
std::vector<float> pitchWindowedBuffer_;
|
||||
std::vector<float> pitchMagnitudes_;
|
||||
std::unique_ptr<DSP::FFT> pitchFft_;
|
||||
|
||||
// Internal helpers
|
||||
void updateFiltered();
|
||||
float findTriggerBackwards(size_t target, size_t range);
|
||||
float sampleInterpolated(float pos) const;
|
||||
float detectPitchFFTReused(const float* data, size_t length, float minFreq, float maxFreq);
|
||||
};
|
||||
|
||||
} // namespace Visualizer
|
||||
|
||||
@@ -57,4 +57,20 @@ Java_expo_modules_astrascope_ScopeBridge_nativeFillSpectrum(
|
||||
return static_cast<jint>(n);
|
||||
}
|
||||
|
||||
// Fills a direct ByteBuffer (over the JS Float32Array's memory) with render-ready
|
||||
// points from the latest triggered oscilloscope window. Zero-copy.
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_expo_modules_astrascope_ScopeBridge_nativeFillOscilloscope(
|
||||
JNIEnv* env, jobject /*thiz*/, jobject buffer, jint capacityFloats) {
|
||||
if (buffer == nullptr || capacityFloats <= 0) {
|
||||
return 0;
|
||||
}
|
||||
auto* dst = static_cast<float*>(env->GetDirectBufferAddress(buffer));
|
||||
if (dst == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
const size_t n = driver().fillOscilloscope(dst, static_cast<size_t>(capacityFloats));
|
||||
return static_cast<jint>(n);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
// read only the most recent fftSize samples, so a slow consumer simply sees the
|
||||
// latest window (correct for a rolling spectrum).
|
||||
|
||||
#include "oscilloscope.h"
|
||||
#include "spectrum.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
@@ -77,11 +81,14 @@ class ScopeDriver {
|
||||
|
||||
const size_t fftSize = spectrum_.getFFTSize();
|
||||
const size_t w = writePos_.load(std::memory_order_acquire);
|
||||
const size_t sampleRate = sr > 0 ? static_cast<size_t>(sr) : static_cast<size_t>(48000);
|
||||
const size_t delaySamples = scopeOutputDelaySamples(sampleRate);
|
||||
const size_t readHead = w > delaySamples ? w - delaySamples : 0;
|
||||
|
||||
const std::vector<float>* mags;
|
||||
if (w >= fftSize) {
|
||||
if (readHead >= fftSize) {
|
||||
scratch_.resize(fftSize);
|
||||
const size_t start = w - fftSize;
|
||||
const size_t start = readHead - fftSize;
|
||||
for (size_t i = 0; i < fftSize; ++i) {
|
||||
scratch_[i] = ring_[(start + i) & kMask];
|
||||
}
|
||||
@@ -96,19 +103,169 @@ class ScopeDriver {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Render thread (single consumer). Unlike the spectrum (which snapshots the
|
||||
// latest window), the oscilloscope needs CONTINUOUS samples for a stable
|
||||
// pitch-locked trigger. We drain a bounded recent slice into its internal
|
||||
// circular buffer, then return render-ready points from the triggered window.
|
||||
size_t fillOscilloscope(float* out, size_t cap) {
|
||||
if (out == nullptr || cap == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int sr = pendingSampleRate_.load(std::memory_order_acquire);
|
||||
if (sr != oscAppliedSampleRate_) {
|
||||
osc_.setSampleRate(static_cast<float>(sr));
|
||||
oscDisplaySamples_ = normalizedDisplaySamples(sr);
|
||||
osc_.setDisplaySamples(static_cast<int>(oscDisplaySamples_));
|
||||
oscAppliedSampleRate_ = sr;
|
||||
oscLastDrainTime_ = {};
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
}
|
||||
|
||||
const size_t w = writePos_.load(std::memory_order_acquire);
|
||||
|
||||
if (w < kOscWarmupSamples) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t sampleRate = sr > 0 ? static_cast<size_t>(sr) : static_cast<size_t>(48000);
|
||||
const size_t outputDelaySamples = scopeOutputDelaySamples(sampleRate);
|
||||
size_t available = w - oscReadPos_;
|
||||
const size_t staleResetSamples = std::max(kSize, sampleRate / 4);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
if (available > kSize || available >= staleResetSamples) {
|
||||
osc_.reset();
|
||||
oscSamplesSeen_ = 0;
|
||||
oscLastDrainTime_ = now;
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
const size_t retained = std::min({w, kSize, kOscRetainedBacklogSamples});
|
||||
oscReadPos_ = w - retained;
|
||||
available = retained;
|
||||
} else if (available > kOscRetainedBacklogSamples) {
|
||||
oscReadPos_ = w - kOscRetainedBacklogSamples;
|
||||
available = kOscRetainedBacklogSamples;
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
}
|
||||
|
||||
const size_t drainable = available > outputDelaySamples ? available - outputDelaySamples : 0;
|
||||
size_t drainBudget = oscDrainBudget(now, sampleRate, drainable);
|
||||
const size_t warmup = std::max(kOscWarmupSamples, oscDisplaySamples_);
|
||||
if (oscSamplesSeen_ < warmup) {
|
||||
drainBudget = std::max(drainBudget, std::min(drainable, warmup - oscSamplesSeen_));
|
||||
}
|
||||
|
||||
const size_t drainEnd = oscReadPos_ + std::min(drainable, drainBudget);
|
||||
while (oscReadPos_ < drainEnd) {
|
||||
const size_t idx = oscReadPos_ & kMask;
|
||||
const size_t chunk = std::min(drainEnd - oscReadPos_, kSize - idx);
|
||||
osc_.pushSamples(&ring_[idx], chunk);
|
||||
oscReadPos_ += chunk;
|
||||
oscSamplesSeen_ += chunk;
|
||||
}
|
||||
|
||||
if (oscSamplesSeen_ < warmup) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Visualizer::OscilloscopeResult r = osc_.process();
|
||||
if (r.samplesToShow <= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size_t count = std::min(cap, static_cast<size_t>(r.samplesToShow));
|
||||
if (count < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const float step = static_cast<float>(r.samplesToShow - 1) /
|
||||
static_cast<float>(count - 1);
|
||||
osc_.getSamplesInterpolated(out, r.triggerIndex, count, step);
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t binCount() const { return spectrum_.getFFTSize() / 2; }
|
||||
|
||||
void reset() { spectrum_.reset(); }
|
||||
void reset() {
|
||||
spectrum_.reset();
|
||||
osc_.reset();
|
||||
oscReadPos_ = writePos_.load(std::memory_order_acquire);
|
||||
oscSamplesSeen_ = 0;
|
||||
oscLastDrainTime_ = {};
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
}
|
||||
|
||||
private:
|
||||
ScopeDriver() : spectrum_(kFftSize) {
|
||||
spectrum_.setSmoothing(0.9f);
|
||||
spectrum_.setSmoothing(0.92f);
|
||||
ring_.assign(kSize, 0.0f);
|
||||
}
|
||||
|
||||
static constexpr size_t kFftSize = 2048; // -> 1024 dB bins
|
||||
static constexpr size_t kSize = 8192; // ring capacity (power of two)
|
||||
static constexpr size_t kSize = 16384; // ring capacity (power of two)
|
||||
static constexpr size_t kMask = kSize - 1;
|
||||
static constexpr size_t kOscWarmupSamples = 4096;
|
||||
static constexpr size_t kOscRetainedBacklogSamples = kSize;
|
||||
static constexpr size_t kOscMinFrameDrainSamples = 128;
|
||||
static constexpr size_t kOscMaxFrameDrainSamples = 2048;
|
||||
static constexpr double kScopeOutputDelaySeconds = 0.12;
|
||||
|
||||
static size_t normalizedDisplaySamples(int sampleRate) {
|
||||
constexpr double base = 2048.0;
|
||||
constexpr double rateMin = 44100.0;
|
||||
constexpr double rateMax = 48000.0;
|
||||
const double safeRate = sampleRate > 0 ? static_cast<double>(sampleRate) : rateMax;
|
||||
|
||||
double samples = base;
|
||||
if (safeRate < rateMin) {
|
||||
samples = base * (safeRate / rateMin);
|
||||
} else if (safeRate > rateMax) {
|
||||
samples = base * (safeRate / rateMax);
|
||||
}
|
||||
|
||||
return static_cast<size_t>(std::clamp(std::round(samples), 64.0, 32767.0));
|
||||
}
|
||||
|
||||
static size_t scopeOutputDelaySamples(size_t sampleRate) {
|
||||
const double samples = static_cast<double>(sampleRate) * kScopeOutputDelaySeconds;
|
||||
return static_cast<size_t>(std::clamp(
|
||||
std::round(samples),
|
||||
0.0,
|
||||
static_cast<double>(kOscRetainedBacklogSamples / 2)));
|
||||
}
|
||||
|
||||
size_t oscDrainBudget(
|
||||
std::chrono::steady_clock::time_point now,
|
||||
size_t sampleRate,
|
||||
size_t available) {
|
||||
if (available == 0) {
|
||||
oscLastDrainTime_ = now;
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double elapsedSeconds = 1.0 / 60.0;
|
||||
if (oscLastDrainTime_.time_since_epoch().count() != 0) {
|
||||
elapsedSeconds = std::chrono::duration<double>(now - oscLastDrainTime_).count();
|
||||
elapsedSeconds = std::clamp(elapsedSeconds, 0.0, 0.1);
|
||||
}
|
||||
oscLastDrainTime_ = now;
|
||||
|
||||
double desired = elapsedSeconds * static_cast<double>(sampleRate) + oscDrainCarrySamples_;
|
||||
size_t budget = static_cast<size_t>(std::floor(desired));
|
||||
oscDrainCarrySamples_ = desired - static_cast<double>(budget);
|
||||
|
||||
if (budget < kOscMinFrameDrainSamples) {
|
||||
budget = std::min(kOscMinFrameDrainSamples, available);
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
}
|
||||
|
||||
const size_t drain = std::min({available, budget, kOscMaxFrameDrainSamples});
|
||||
if (drain >= available) {
|
||||
oscDrainCarrySamples_ = 0.0;
|
||||
}
|
||||
return drain;
|
||||
}
|
||||
|
||||
// Shared SPSC state.
|
||||
std::vector<float> ring_;
|
||||
@@ -119,6 +276,15 @@ class ScopeDriver {
|
||||
std::vector<float> scratch_;
|
||||
Visualizer::Spectrum spectrum_;
|
||||
int appliedSampleRate_{0};
|
||||
|
||||
Visualizer::Oscilloscope osc_;
|
||||
size_t oscReadPos_{0};
|
||||
size_t oscSamplesSeen_{0};
|
||||
size_t oscDisplaySamples_{2048};
|
||||
std::chrono::steady_clock::time_point oscLastDrainTime_{};
|
||||
double oscDrainCarrySamples_{0.0};
|
||||
int oscAppliedSampleRate_{0};
|
||||
|
||||
};
|
||||
|
||||
} // namespace astra
|
||||
|
||||
Reference in New Issue
Block a user