This commit is contained in:
Boof2015
2026-06-19 13:19:37 -04:00
parent 978d001db9
commit bc687dfe87
145 changed files with 4685 additions and 197 deletions
@@ -29,5 +29,52 @@ class AstraScopeModule : Module() {
Function("getOscilloscopeFrame") { out: Float32Array ->
ScopeBridge.nativeFillOscilloscope(out.toDirectBuffer(), out.length)
}
// --- M4: post-EQ spectrum (EQ screen overlay) ---
// Gate the post-EQ tap (true only while the EQ screen is open).
Function("setActivePostEq") { active: Boolean ->
ScopeBridge.postEqActive = active
}
Function("getSpectrumFramePostEq") { out: Float32Array ->
ScopeBridge.nativeFillSpectrumPostEq(out.toDirectBuffer(), out.length)
}
// --- M4: EQ params + per-track gain (consumed by the kotlin-audio processors) ---
Function("setEqEnabled") { enabled: Boolean ->
EqBridge.enabled = enabled
EqBridge.revision += 1
}
Function("setEqPreamp") { linear: Double ->
EqBridge.preampLinear = linear.toFloat()
EqBridge.revision += 1
}
// Flat band params: 5 floats per band [typeOrdinal, frequency, gain, Q, enabled?1:0].
Function("setEqBands") { params: FloatArray ->
EqBridge.bands = params
EqBridge.revision += 1
}
Function("setNormalizationGain") { linear: Double ->
GainBridge.linearGain = linear.toFloat()
}
// Register a queued track's gain by URL so the player can switch to it natively at
// the exact media-item transition (no JS round-trip on track change).
Function("setTrackGain") { url: String, linear: Double ->
GainBridge.putGain(url, linear.toFloat())
}
// Make the registered gain for this URL active now (used for the current track on
// mount / settings change, where no transition fires).
Function("activateTrackGain") { url: String ->
GainBridge.activateFor(url)
}
Function("clearTrackGains") {
GainBridge.clearGains()
}
}
}
@@ -0,0 +1,29 @@
package expo.modules.astrascope
/**
* Holds the current parametric-EQ configuration, set from JS (raw band params, NOT
* biquad coefficients). The vendored kotlin-audio `EqAudioProcessor` reads these
* fields lock-free on the ExoPlayer audio thread and recomputes coefficients at the
* real stream sample rate whenever [revision] changes.
*
* Single writer (JS thread via [AstraScopeModule]); single reader (audio thread).
* `bands` is published before `revision` is bumped, so a reader that observes a new
* revision also observes the matching band array.
*/
object EqBridge {
/** Master bypass. When false the processor is passthrough. */
@Volatile
var enabled: Boolean = false
/** EQ preamp as a linear amplitude (1 = unity). */
@Volatile
var preampLinear: Float = 1f
/** 5 floats per band: [typeOrdinal, frequency, gain, Q, enabled?1:0]. */
@Volatile
var bands: FloatArray = FloatArray(0)
/** Bumped on every enabled/preamp/bands change so the processor recomputes. */
@Volatile
var revision: Int = 0
}
@@ -0,0 +1,37 @@
package expo.modules.astrascope
import java.util.concurrent.ConcurrentHashMap
/**
* Per-track normalization / ReplayGain gain, read lock-free by the vendored
* kotlin-audio `NormalizationGainProcessor` on the audio thread. Applied BEFORE the
* scope taps so the scopes see normalized levels.
*
* JS pre-registers each queued track's gain by URL ([putGain]); the player then swaps
* the active gain to the matching one natively at the real media-item transition
* ([activateFor], called from BaseAudioPlayer.onMediaItemTransition). That lands the
* gain at the actual audio boundary instead of after a JS round-trip on track change.
*/
object GainBridge {
/** Active linear amplitude multiplier (1 = unity). Read on the audio thread. */
@Volatile
var linearGain: Float = 1f
// url -> linear gain, seeded from JS ahead of playback.
private val gains = ConcurrentHashMap<String, Float>()
/** Register (or update) the gain for a track URL. */
fun putGain(url: String, gain: Float) {
gains[url] = gain
}
/** Make the gain registered for [url] active (unity if unknown/null). */
fun activateFor(url: String?) {
linearGain = if (url != null) gains[url] ?: 1f else 1f
}
/** Drop all registered gains (e.g. on full queue reset). */
fun clearGains() {
gains.clear()
}
}
@@ -21,6 +21,14 @@ object ScopeBridge {
@Volatile
var active: Boolean = false
/**
* Gates the POST-EQ tap specifically (true only while the EQ screen is open).
* The post-EQ tap runs when [active] && [postEqActive], so the second downmix
* costs nothing unless the EQ overlay is actually being viewed.
*/
@Volatile
var postEqActive: Boolean = false
/** Audio thread. Tell the analyzer the stream's sample rate / channels. */
external fun nativeConfigure(sampleRate: Int, channelCount: Int)
@@ -41,4 +49,13 @@ object ScopeBridge {
* writes straight into JS memory.
*/
external fun nativeFillOscilloscope(buffer: java.nio.ByteBuffer, capacityFloats: Int): Int
/** Audio thread. Push POST-EQ interleaved float PCM (the M4 second tap). */
external fun nativePushFramesPostEq(frames: FloatArray, frameCount: Int, channelCount: Int)
/**
* 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
}