mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-17 11:12:33 +02:00
add spectrum to graphic eq mode
This commit is contained in:
@@ -100,6 +100,11 @@ class AstraScopeModule : Module() {
|
||||
Prop("analysisFrameMs") { view, value: Double -> view.analysisFrameMs = value }
|
||||
Prop("smoothing") { view, value: Double -> view.smoothing = value.toFloat() }
|
||||
Prop("pointCount") { view, value: Int -> view.pointCount = value }
|
||||
Prop("frequencyAnchors") { view, value: List<Double>? ->
|
||||
view.frequencyAnchors = value?.let { frequencies ->
|
||||
FloatArray(frequencies.size) { index -> frequencies[index].toFloat() }
|
||||
}
|
||||
}
|
||||
Prop("dbMin") { view, value: Double -> view.dbMin = value.toFloat() }
|
||||
Prop("dbMax") { view, value: Double -> view.dbMax = value.toFloat() }
|
||||
Prop("tiltDbPerOctave") { view, value: Double -> view.tiltDbPerOctave = value.toFloat() }
|
||||
|
||||
+70
-4
@@ -1,5 +1,6 @@
|
||||
package expo.modules.astrascope
|
||||
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -83,6 +84,7 @@ internal object AstraScopeProjection {
|
||||
dbMin: Float,
|
||||
dbMax: Float,
|
||||
tiltDbPerOctave: Float,
|
||||
frequencyAnchors: FloatArray? = null,
|
||||
sampleRate: Float = 48_000f
|
||||
) {
|
||||
val bins = min(rawCount, raw.capacity())
|
||||
@@ -97,12 +99,13 @@ internal object AstraScopeProjection {
|
||||
val maxFrequency = max(minFrequency + 1.0, min(MAX_FREQUENCY, nyquist))
|
||||
val binWidth = nyquist / bins.toDouble()
|
||||
val range = max(1f, dbMax - dbMin)
|
||||
val anchors = validFrequencyAnchors(frequencyAnchors, minFrequency, maxFrequency)
|
||||
|
||||
for (point in 0 until points) {
|
||||
val t0 = point.toDouble() / (points - 1).toDouble()
|
||||
val t1 = min(1.0, (point + 1).toDouble() / (points - 1).toDouble())
|
||||
val frequency0 = frequencyAt(t0, minFrequency, maxFrequency)
|
||||
val frequency1 = frequencyAt(t1, minFrequency, maxFrequency)
|
||||
val frequency0 = frequencyAt(t0, minFrequency, maxFrequency, anchors)
|
||||
val frequency1 = frequencyAt(t1, minFrequency, maxFrequency, anchors)
|
||||
val centerFrequency = (frequency0 + frequency1) * 0.5
|
||||
val bin0 = frequency0 / binWidth
|
||||
val bin1 = frequency1 / binWidth
|
||||
@@ -119,6 +122,20 @@ internal object AstraScopeProjection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps normalized x to frequency. With anchors, each frequency is pinned to
|
||||
* the center of an equal-width column and the intervals remain logarithmic.
|
||||
*/
|
||||
internal fun spectrumFrequencyAt(
|
||||
t: Double,
|
||||
minFrequency: Double = MIN_FREQUENCY,
|
||||
maxFrequency: Double = MAX_FREQUENCY,
|
||||
frequencyAnchors: FloatArray? = null
|
||||
): Double {
|
||||
val anchors = validFrequencyAnchors(frequencyAnchors, minFrequency, maxFrequency)
|
||||
return frequencyAt(t, minFrequency, maxFrequency, anchors)
|
||||
}
|
||||
|
||||
/** Returns the largest absolute value left after one decay step. */
|
||||
fun decay(values: FloatArray, count: Int, factor: Float = DECAY_PER_FRAME): Float {
|
||||
var peak = 0f
|
||||
@@ -131,8 +148,57 @@ internal object AstraScopeProjection {
|
||||
return peak
|
||||
}
|
||||
|
||||
private fun frequencyAt(t: Double, minFrequency: Double, maxFrequency: Double): Double =
|
||||
minFrequency * (maxFrequency / minFrequency).pow(t)
|
||||
private fun frequencyAt(
|
||||
t: Double,
|
||||
minFrequency: Double,
|
||||
maxFrequency: Double,
|
||||
anchors: FloatArray?
|
||||
): Double {
|
||||
val position = t.coerceIn(0.0, 1.0)
|
||||
if (anchors == null) return logLerp(minFrequency, maxFrequency, position)
|
||||
|
||||
val count = anchors.size
|
||||
val firstCenter = 0.5 / count
|
||||
val lastCenter = (count - 0.5) / count
|
||||
if (position <= firstCenter) {
|
||||
return logLerp(minFrequency, anchors.first().toDouble(), position / firstCenter)
|
||||
}
|
||||
if (position >= lastCenter) {
|
||||
return logLerp(
|
||||
anchors.last().toDouble(),
|
||||
maxFrequency,
|
||||
(position - lastCenter) / (1.0 - lastCenter)
|
||||
)
|
||||
}
|
||||
|
||||
val lower = floor(position * count - 0.5).toInt().coerceIn(0, count - 2)
|
||||
val lowerCenter = (lower + 0.5) / count
|
||||
val upperCenter = (lower + 1.5) / count
|
||||
return logLerp(
|
||||
anchors[lower].toDouble(),
|
||||
anchors[lower + 1].toDouble(),
|
||||
(position - lowerCenter) / (upperCenter - lowerCenter)
|
||||
)
|
||||
}
|
||||
|
||||
private fun validFrequencyAnchors(
|
||||
anchors: FloatArray?,
|
||||
minFrequency: Double,
|
||||
maxFrequency: Double
|
||||
): FloatArray? {
|
||||
val frequencies = anchors ?: return null
|
||||
if (frequencies.isEmpty()) return null
|
||||
var previous = minFrequency
|
||||
for (anchor in frequencies) {
|
||||
val frequency = anchor.toDouble()
|
||||
if (!frequency.isFinite() || frequency <= previous || frequency >= maxFrequency) return null
|
||||
previous = frequency
|
||||
}
|
||||
return frequencies
|
||||
}
|
||||
|
||||
private fun logLerp(start: Double, end: Double, t: Double): Double =
|
||||
start * (end / start).pow(t.coerceIn(0.0, 1.0))
|
||||
|
||||
private fun safeRefreshRate(refreshRate: Float): Float =
|
||||
if (refreshRate.isFinite() && refreshRate >= 30f) refreshRate else 60f
|
||||
|
||||
@@ -64,6 +64,7 @@ internal class AstraScopeView(
|
||||
var analysisFrameMs = 32.0
|
||||
var smoothing = 0.92f
|
||||
var pointCount = 120
|
||||
var frequencyAnchors: FloatArray? = null
|
||||
var dbMin = -90f
|
||||
var dbMax = -10f
|
||||
var tiltDbPerOctave = 3.5f
|
||||
@@ -332,7 +333,8 @@ internal class AstraScopeView(
|
||||
renderedPointCount,
|
||||
dbMin,
|
||||
dbMax,
|
||||
tiltDbPerOctave
|
||||
tiltDbPerOctave,
|
||||
frequencyAnchors
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
+39
@@ -22,6 +22,45 @@ class AstraScopeProjectionTest {
|
||||
for (index in 1 until out.size) assertTrue(out[index] >= out[index - 1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun graphicEqFrequenciesAlignToEvenColumnCenters() {
|
||||
val anchors = floatArrayOf(60f, 250f, 1_000f, 4_000f, 12_000f)
|
||||
|
||||
assertEquals(20.0, AstraScopeProjection.spectrumFrequencyAt(0.0, frequencyAnchors = anchors), 0.001)
|
||||
anchors.forEachIndexed { index, frequency ->
|
||||
val center = (index + 0.5) / anchors.size
|
||||
assertEquals(
|
||||
frequency.toDouble(),
|
||||
AstraScopeProjection.spectrumFrequencyAt(center, frequencyAnchors = anchors),
|
||||
0.001
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
20_000.0,
|
||||
AstraScopeProjection.spectrumFrequencyAt(1.0, frequencyAnchors = anchors),
|
||||
0.001
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingOrInvalidFrequencyAnchorsFallBackToLogProjection() {
|
||||
val expectedMidpoint = kotlin.math.sqrt(20.0 * 20_000.0)
|
||||
val nonIncreasing = floatArrayOf(60f, 1_000f, 250f)
|
||||
val outOfRange = floatArrayOf(60f, 25_000f)
|
||||
|
||||
assertEquals(expectedMidpoint, AstraScopeProjection.spectrumFrequencyAt(0.5), 0.001)
|
||||
assertEquals(
|
||||
expectedMidpoint,
|
||||
AstraScopeProjection.spectrumFrequencyAt(0.5, frequencyAnchors = nonIncreasing),
|
||||
0.001
|
||||
)
|
||||
assertEquals(
|
||||
expectedMidpoint,
|
||||
AstraScopeProjection.spectrumFrequencyAt(0.5, frequencyAnchors = outOfRange),
|
||||
0.001
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clampRejectsInvalidAndOutOfRangeValues() {
|
||||
assertEquals(0f, AstraScopeProjection.clamp01(Float.NaN), 0f)
|
||||
|
||||
@@ -87,6 +87,8 @@ export interface AstraScopeViewProps extends ViewProps {
|
||||
analysisFrameMs?: number;
|
||||
smoothing?: number;
|
||||
pointCount?: number;
|
||||
/** Optional frequencies pinned to evenly spaced horizontal centers. */
|
||||
frequencyAnchors?: number[];
|
||||
dbMin?: number;
|
||||
dbMax?: number;
|
||||
tiltDbPerOctave?: number;
|
||||
|
||||
Reference in New Issue
Block a user