diff --git a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt index ebe3301..8406786 100644 --- a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt +++ b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/AstraScopeModule.kt @@ -21,8 +21,8 @@ class AstraScopeModule : Module() { } // Fill `out` with the latest dB spectrum; returns the number of bins written. - Function("getSpectrumFrame") { out: Float32Array -> - ScopeBridge.nativeFillSpectrum(out.toDirectBuffer(), out.length) + Function("getSpectrumFrame") { out: Float32Array, smoothing: Double -> + ScopeBridge.nativeFillSpectrum(out.toDirectBuffer(), out.length, smoothing.toFloat()) } // Fill `out` with render-ready oscilloscope points (~[-1,1]). @@ -36,8 +36,8 @@ class AstraScopeModule : Module() { ScopeBridge.postEqActive = active } - Function("getSpectrumFramePostEq") { out: Float32Array -> - ScopeBridge.nativeFillSpectrumPostEq(out.toDirectBuffer(), out.length) + Function("getSpectrumFramePostEq") { out: Float32Array, smoothing: Double -> + ScopeBridge.nativeFillSpectrumPostEq(out.toDirectBuffer(), out.length, smoothing.toFloat()) } // --- M4: EQ params + per-track gain (consumed by the kotlin-audio processors) --- diff --git a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt index aadccb9..cc3775c 100644 --- a/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt +++ b/modules/astra-scope/android/src/main/java/expo/modules/astrascope/ScopeBridge.kt @@ -40,7 +40,11 @@ object ScopeBridge { * memory) with the latest dB spectrum, up to `capacityFloats` floats. * Returns the number of bins written. Zero-copy: writes straight into JS memory. */ - external fun nativeFillSpectrum(buffer: java.nio.ByteBuffer, capacityFloats: Int): Int + external fun nativeFillSpectrum( + buffer: java.nio.ByteBuffer, + capacityFloats: Int, + smoothing: Float + ): Int /** * Render thread. Fill `buffer` (a direct ByteBuffer over the JS Float32Array's @@ -57,5 +61,9 @@ object ScopeBridge { * Render thread. Fill `buffer` with the latest POST-EQ dB spectrum (feeds the * EQ screen's response-curve overlay). Returns bins written. Zero-copy. */ - external fun nativeFillSpectrumPostEq(buffer: java.nio.ByteBuffer, capacityFloats: Int): Int + external fun nativeFillSpectrumPostEq( + buffer: java.nio.ByteBuffer, + capacityFloats: Int, + smoothing: Float + ): Int } diff --git a/modules/astra-scope/cpp/scope_jni.cpp b/modules/astra-scope/cpp/scope_jni.cpp index 33f6d8a..59b3895 100644 --- a/modules/astra-scope/cpp/scope_jni.cpp +++ b/modules/astra-scope/cpp/scope_jni.cpp @@ -45,7 +45,8 @@ Java_expo_modules_astrascope_ScopeBridge_nativePushFrames( // Zero-copy: writes straight into the JS-owned ArrayBuffer. JNIEXPORT jint JNICALL Java_expo_modules_astrascope_ScopeBridge_nativeFillSpectrum( - JNIEnv* env, jobject /*thiz*/, jobject buffer, jint capacityFloats) { + JNIEnv* env, jobject /*thiz*/, jobject buffer, jint capacityFloats, + jfloat smoothing) { if (buffer == nullptr || capacityFloats <= 0) { return 0; } @@ -53,7 +54,8 @@ Java_expo_modules_astrascope_ScopeBridge_nativeFillSpectrum( if (dst == nullptr) { return 0; } - const size_t n = driver().fillSpectrum(dst, static_cast(capacityFloats)); + const size_t n = driver().fillSpectrum( + dst, static_cast(capacityFloats), static_cast(smoothing)); return static_cast(n); } @@ -95,7 +97,8 @@ Java_expo_modules_astrascope_ScopeBridge_nativePushFramesPostEq( JNIEXPORT jint JNICALL Java_expo_modules_astrascope_ScopeBridge_nativeFillSpectrumPostEq( - JNIEnv* env, jobject /*thiz*/, jobject buffer, jint capacityFloats) { + JNIEnv* env, jobject /*thiz*/, jobject buffer, jint capacityFloats, + jfloat smoothing) { if (buffer == nullptr || capacityFloats <= 0) { return 0; } @@ -103,7 +106,8 @@ Java_expo_modules_astrascope_ScopeBridge_nativeFillSpectrumPostEq( if (dst == nullptr) { return 0; } - const size_t n = driver().fillSpectrumPostEq(dst, static_cast(capacityFloats)); + const size_t n = driver().fillSpectrumPostEq( + dst, static_cast(capacityFloats), static_cast(smoothing)); return static_cast(n); } diff --git a/modules/astra-scope/cpp/scope_ring.h b/modules/astra-scope/cpp/scope_ring.h index b20a1f3..4e821d7 100644 --- a/modules/astra-scope/cpp/scope_ring.h +++ b/modules/astra-scope/cpp/scope_ring.h @@ -68,11 +68,13 @@ class ScopeDriver { // Render thread (single consumer). Snapshot the most recent fftSize mono // samples, run the FFT, copy up to `cap` dB magnitudes into `out`. // Returns the number of bins written. - size_t fillSpectrum(float* out, size_t cap) { + size_t fillSpectrum(float* out, size_t cap, float smoothing) { if (out == nullptr || cap == 0) { return 0; } + spectrum_.setSmoothing(smoothing); + const int sr = pendingSampleRate_.load(std::memory_order_acquire); if (sr != appliedSampleRate_) { spectrum_.setSampleRate(static_cast(sr)); @@ -209,11 +211,13 @@ class ScopeDriver { } // Render thread. Latest post-EQ spectrum window -> `out` (dB magnitudes). - size_t fillSpectrumPostEq(float* out, size_t cap) { + size_t fillSpectrumPostEq(float* out, size_t cap, float smoothing) { if (out == nullptr || cap == 0) { return 0; } + postEqSpectrum_.setSmoothing(smoothing); + const int sr = pendingSampleRate_.load(std::memory_order_acquire); if (sr != postEqAppliedSampleRate_) { postEqSpectrum_.setSampleRate(static_cast(sr)); diff --git a/modules/astra-scope/index.ts b/modules/astra-scope/index.ts index 18b2aea..0999091 100644 --- a/modules/astra-scope/index.ts +++ b/modules/astra-scope/index.ts @@ -15,10 +15,11 @@ declare class AstraScopeModuleType extends NativeModule { setActive(active: boolean): void; /** * Fill `out` (length should be {@link SPECTRUM_BINS}) with the latest dB - * spectrum magnitudes in place; returns the number of bins written. Call once - * per render frame from the JS thread. + * spectrum magnitudes in place; returns the number of bins written. `smoothing` + * is the previous-frame retention in [0, 0.99]. Call once per render frame + * from the JS thread. */ - getSpectrumFrame(out: Float32Array): number; + getSpectrumFrame(out: Float32Array, smoothing: number): number; /** * Fill `out` with render-ready, evenly spaced points from the latest * pitch-locked oscilloscope window. Values are interpolated visual samples in @@ -31,7 +32,7 @@ declare class AstraScopeModuleType extends NativeModule { * Like {@link getSpectrumFrame} but for the POST-EQ tap (ring #2) — feeds the * EQ screen's spectrum behind the response curve. Returns bins written. */ - getSpectrumFramePostEq(out: Float32Array): number; + getSpectrumFramePostEq(out: Float32Array, smoothing: number): number; // --- M4 EQ + per-track gain (params pushed from JS; biquad coeffs computed // natively at the real stream sample rate) --- diff --git a/src/components/SpectrumCurve.tsx b/src/components/SpectrumCurve.tsx index c5a86fc..4e79383 100644 --- a/src/components/SpectrumCurve.tsx +++ b/src/components/SpectrumCurve.tsx @@ -34,6 +34,8 @@ interface SpectrumCurveProps { frameMs?: number; /** Native pull cadence. Defaults to frameMs; 0 advances analysis every display frame. */ analysisFrameMs?: number; + /** Previous native spectrum-frame retention in [0, 0.99]. */ + smoothing?: number; dbMin?: number; dbMax?: number; tiltDbPerOctave?: number; @@ -54,6 +56,7 @@ type SkiaViewApiShape = { const DEFAULT_POINTS = 120; const MINI_FRAME_MS = 32; +const DEFAULT_SMOOTHING = 0.92; const DISPLAY_DB_MIN = -90; const DISPLAY_DB_MAX = -10; const SPECTRUM_SAMPLE_RATE = 48000; @@ -376,6 +379,7 @@ export function SpectrumCurve({ pointCount, frameMs = MINI_FRAME_MS, analysisFrameMs, + smoothing = DEFAULT_SMOOTHING, dbMin = DISPLAY_DB_MIN, dbMax = DISPLAY_DB_MAX, tiltDbPerOctave = TILT_DB_PER_OCT, @@ -567,8 +571,8 @@ export function SpectrumCurve({ lastAnalysis = t; const got = source === 'post' - ? AstraScope.getSpectrumFramePostEq(spectrumBins) - : AstraScope.getSpectrumFrame(spectrumBins); + ? AstraScope.getSpectrumFramePostEq(spectrumBins, smoothing) + : AstraScope.getSpectrumFrame(spectrumBins, smoothing); if (got > 0) { writeSpectrumPoints(spectrumBins, renderValues, pointOptions); hasNewFrame = true; @@ -601,6 +605,7 @@ export function SpectrumCurve({ reduceMotion, resolvedPointCount, source, + smoothing, tiltDbPerOctave, width, ]); diff --git a/src/components/Visualizer.tsx b/src/components/Visualizer.tsx index be90716..dfadd38 100644 --- a/src/components/Visualizer.tsx +++ b/src/components/Visualizer.tsx @@ -25,6 +25,7 @@ interface VisualizerProps { showChrome?: boolean; mode?: Mode; edgeFade?: boolean; + spectrumSmoothing?: number; /** Freeze the live scopes without unmounting (e.g. while occluded by an overlay). */ paused?: boolean; } @@ -41,6 +42,7 @@ export function Visualizer({ showChrome = true, mode: controlledMode, edgeFade = false, + spectrumSmoothing, paused = false, }: VisualizerProps) { const styles = useStyles(); @@ -72,6 +74,7 @@ export function Visualizer({ @@ -1457,6 +1459,7 @@ function ScopeRail({ width, height, mode, paused, revealed, onSwap }: ScopeRailP showChrome={false} mode={mode} edgeFade + spectrumSmoothing={NOW_PLAYING_SPECTRUM_SMOOTHING} paused={paused} /> diff --git a/src/components/player/ScopeRack.tsx b/src/components/player/ScopeRack.tsx index d154793..169c4d8 100644 --- a/src/components/player/ScopeRack.tsx +++ b/src/components/player/ScopeRack.tsx @@ -35,6 +35,7 @@ interface ScopeRackProps { /** Strip span (the rail's width): wider than the card, overflowing it. */ stripWidth: number; artworkUri: string | null; + spectrumSmoothing?: number; /** Freeze the scopes without unmounting (overlay closed / queue open). */ paused?: boolean; } @@ -45,7 +46,13 @@ interface ScopeRackProps { * backdrop dissolves past the former card frame, while the strips span the * full rail width and independently fade at their ends. */ -export function ScopeRack({ size, stripWidth, artworkUri, paused = false }: ScopeRackProps) { +export function ScopeRack({ + size, + stripWidth, + artworkUri, + spectrumSmoothing, + paused = false, +}: ScopeRackProps) { const styles = useStyles(); const artwork = useImage(artworkUri); const active = useScopeActive() && !paused; @@ -114,6 +121,7 @@ export function ScopeRack({ size, stripWidth, artworkUri, paused = false }: Scop