fmchirp.kt
src/jvmMain/kotlin/afield/graphics/vulkan/util/fmchirp.kt · 1884 lines
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package afield.graphics.vulkan.util
FM chirp curves: an eight-parameter family of 0..1 remapping curves, packed as half floats, that evaluate the same way in Kotlin and in GLSL.
FMChirpCurves holds the parameter type, evaluation, sampling, measures such as roughness and foldover, and least-squares fitting to target points. FM_CHIRP_GLSL is the matching shader source, registered as the fmchirp shader template by registerFMChirpShaderTemplate; registerFMChirpVisualizers gives the parameter type an HTML summary when it is printed in a box.
import afield.runtime.lifecycle.RunsAt
import afield.runtime.lifecycle.newRoot
import afield.ui.visualization.registerValuePrinter
import afield.math.optimization.LeastSquaresOptions
import afield.math.optimization.LeastSquaresResult
import afield.math.optimization.leastSquares
import afield.runtime.lifecycle.templatesRegistration
import afield.math.linalg.Vector4f
import afield.math.linalg.Vector8h
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.sin
import java.util.Locale
import afield.ui.visualization.Align
import afield.ui.output.FieldMarkdown
import afield.ui.visualization.facts
val FM_CHIRP_GLSL
body — 357 lines, from line 32
val FM_CHIRP_GLSL = """
// ============================================================================
// FM Chirp Curve8
// ============================================================================
//
// 8 half-float parameter layout:
//
// a = freq
// b = mod
// c = modPhase
// d = phase
// f = chirp
// g = modChirp
// n = inputPow
// n2 = outputPow
//
// Packed as uvec4:
//
// h.x = packHalf2x16(vec2(a, b))
// h.y = packHalf2x16(vec2(c, d))
// h.z = packHalf2x16(vec2(f, g))
// h.w = packHalf2x16(vec2(n, n2))
//
// q(x) = sin(
// TAU * (a*x + d + f*x*x)
// + sin(TAU * (b*x + c + g*x*x))
// )
//
// Direct01 mode:
//
// y = pow(0.5 + 0.5*q(pow(x, n)), n2)
//
// Residual mode:
//
// y = x + gain*signedPow(q(pow(x,n)), n2)
//
// Anchored residual mode:
//
// y = x + gain*x*(1-x)*signedPow(q(pow(x,n)), n2)
//
// ============================================================================
const float FM_CURVE_TAU = 6.28318530717958647692;
const float FM_CURVE_EPS = 1e-8;
const float FM_CURVE_MIN_EXPONENT = 1e-4;
const int FM_CURVE_MODE_DIRECT_01 = 0;
const int FM_CURVE_MODE_RESIDUAL = 1;
const int FM_CURVE_MODE_ANCHORED_RESIDUAL = 2;
struct FMChirpCurve8 {
float freq;
float mod;
float modPhase;
float phase;
float chirp;
float modChirp;
float inputPow;
float outputPow;
};
float fmSafePow01(float x, float exponent) {
x = clamp(x, 0.0, 1.0);
exponent = max(exponent, FM_CURVE_MIN_EXPONENT);
if (x <= 0.0) {
return 0.0;
}
if (x >= 1.0) {
return 1.0;
}
return exp2(log2(max(x, FM_CURVE_EPS)) * exponent);
}
float fmSafeSignedPow(float x, float exponent) {
float ax = abs(x);
exponent = max(exponent, FM_CURVE_MIN_EXPONENT);
if (ax <= 0.0) {
return 0.0;
}
float y = exp2(log2(max(ax, FM_CURVE_EPS)) * exponent);
return sign(x) * y;
}
FMChirpCurve8 fmCurve8Make(
float freq,
float mod,
float modPhase,
float phase,
float chirp,
float modChirp,
float inputPow,
float outputPow
) {
FMChirpCurve8 p;
p.freq = freq;
p.mod = mod;
p.modPhase = modPhase;
p.phase = phase;
p.chirp = chirp;
p.modChirp = modChirp;
p.inputPow = inputPow;
p.outputPow = outputPow;
return p;
}
uvec4 fmCurve8PackToUVec4(FMChirpCurve8 p) {
return uvec4(
packHalf2x16(vec2(p.freq, p.mod)),
packHalf2x16(vec2(p.modPhase, p.phase)),
packHalf2x16(vec2(p.chirp, p.modChirp)),
packHalf2x16(vec2(p.inputPow, p.outputPow))
);
}
FMChirpCurve8 fmCurve8UnpackFromUVec4(uvec4 h) {
vec2 ab = unpackHalf2x16(h.x);
vec2 cd = unpackHalf2x16(h.y);
vec2 fg = unpackHalf2x16(h.z);
vec2 nn = unpackHalf2x16(h.w);
FMChirpCurve8 p;
p.freq = ab.x;
p.mod = ab.y;
p.modPhase = cd.x;
p.phase = cd.y;
p.chirp = fg.x;
p.modChirp = fg.y;
p.inputPow = nn.x;
p.outputPow = nn.y;
return p;
}
vec4 fmCurve8PackToOpaqueVec4(FMChirpCurve8 p) {
uvec4 u = fmCurve8PackToUVec4(p);
return vec4(
uintBitsToFloat(u.x),
uintBitsToFloat(u.y),
uintBitsToFloat(u.z),
uintBitsToFloat(u.w)
);
}
FMChirpCurve8 fmCurve8UnpackFromOpaqueVec4(vec4 v) {
uvec4 u = uvec4(
floatBitsToUint(v.x),
floatBitsToUint(v.y),
floatBitsToUint(v.z),
floatBitsToUint(v.w)
);
return fmCurve8UnpackFromUVec4(u);
}
float fmCurve8QAtWarpedX(float xWarped, FMChirpCurve8 p) {
float x = xWarped;
float xx = x * x;
float modPhaseValue =
FM_CURVE_TAU * (
p.mod * x +
p.modPhase +
p.modChirp * xx
);
float phaseValue =
FM_CURVE_TAU * (
p.freq * x +
p.phase +
p.chirp * xx
);
return sin(phaseValue + sin(modPhaseValue));
}
float fmCurve8Q(float x, FMChirpCurve8 p) {
float xWarped =
fmSafeSignedPow(x, p.inputPow);
return fmCurve8QAtWarpedX(xWarped, p);
}
float fmCurve8Direct01(float x, FMChirpCurve8 p) {
float q = fmCurve8Q(x, p);
float y = 0.5 + 0.5 * q;
return fmSafePow01(y, p.outputPow);
}
float fmCurve8Residual(
float x,
FMChirpCurve8 p,
float gain,
bool shapeResidualWithPow
) {
float q = fmCurve8Q(x, p);
if (shapeResidualWithPow) {
q = fmSafeSignedPow(q, p.outputPow);
}
return x + gain * q;
}
float fmCurve8AnchoredResidual(
float x,
FMChirpCurve8 p,
float gain,
bool shapeResidualWithPow
) {
float q = fmCurve8Q(x, p);
if (shapeResidualWithPow) {
q = fmSafeSignedPow(q, p.outputPow);
}
return x + gain * x * (1.0 - x) * q;
}
float fmCurve8Eval(
float x,
FMChirpCurve8 p,
int mode,
float gain
) {
if (mode == FM_CURVE_MODE_DIRECT_01) {
return fmCurve8Direct01(x, p);
}
if (mode == FM_CURVE_MODE_ANCHORED_RESIDUAL) {
return fmCurve8AnchoredResidual(
x,
p,
gain,
true
);
}
return fmCurve8Residual(
x,
p,
gain,
true
);
}
float fmCurve8EvalPackedUVec4(
float x,
uvec4 packed,
int mode,
float gain
) {
FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed);
return fmCurve8Eval(x, p, mode, gain);
}
float fmCurve8ResidualPackedUVec4(
float x,
uvec4 packed,
float gain
) {
FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed);
return fmCurve8Residual(
x,
p,
gain,
true
);
}
float fmCurve8Direct01PackedUVec4(
float x,
uvec4 packed
) {
FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed);
return fmCurve8Direct01(x, p);
}
float fmCurve8AnchoredResidualPackedUVec4(
float x,
uvec4 packed,
float gain
) {
FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed);
return fmCurve8AnchoredResidual(
x,
p,
gain,
true
);
}
float fmCurve8EvalOpaqueVec4(
float x,
vec4 packedOpaque,
int mode,
float gain
) {
FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque);
return fmCurve8Eval(x, p, mode, gain);
}
float fmCurve8ResidualOpaqueVec4(
float x,
vec4 packedOpaque,
float gain
) {
FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque);
return fmCurve8Residual(
x,
p,
gain,
true
);
}
float fmCurve8Direct01OpaqueVec4(
float x,
vec4 packedOpaque
) {
FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque);
return fmCurve8Direct01(x, p);
}
float fmCurve8AnchoredResidualOpaqueVec4(
float x,
vec4 packedOpaque,
float gain
) {
FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque);
return fmCurve8AnchoredResidual(
x,
p,
gain,
true
);
}
""".trimIndent()
fun registerFMChirpShaderTemplate()
@RunsAt(templatesRegistration)
fun registerFMChirpShaderTemplate() {
registerGlobalShaderTemplate("fmchirp", FM_CHIRP_GLSL)
}
fun registerFMChirpVisualizers()
@RunsAt(newRoot)
fun registerFMChirpVisualizers() {
registerValuePrinter<FMChirpCurves.Params> { it.summaryHtml() }
}
Example GLSL
version 450
FM Chirp Curve8
8 half-float parameter layout:
a = freq b = mod c = modPhase d = phase f = chirp g = modChirp n = inputPow n2 = outputPow
Packed as uvec4:
h.x = packHalf2x16(vec2(a, b)) h.y = packHalf2x16(vec2(c, d)) h.z = packHalf2x16(vec2(f, g)) h.w = packHalf2x16(vec2(n, n2))
q(x) = sin( TAU * (ax + d + fxx) + sin(TAU * (bx + c + gxx)) )
Direct01 mode:
y = pow(0.5 + 0.5*q(pow(x, n)), n2)
Residual mode:
y = x + gain*signedPow(q(pow(x,n)), n2)
Anchored residual mode:
y = x + gainx(1-x)*signedPow(q(pow(x,n)), n2)
const float FM_CURVE_TAU = 6.28318530717958647692; const float FM_CURVE_EPS = 1e-8; const float FM_CURVE_MIN_EXPONENT = 1e-4;
For fmCurve8Eval(...) const int FM_CURVE_MODE_DIRECT_01 = 0; const int FM_CURVE_MODE_RESIDUAL = 1; const int FM_CURVE_MODE_ANCHORED_RESIDUAL = 2;
struct FMChirpCurve8 { float freq; // a float mod; // b float modPhase; // c float phase; // d float chirp; // f float modChirp; // g float inputPow; // n float outputPow; // n2 };
Safe power helpers
float fmSafePow01(float x, float exponent) { x = clamp(x, 0.0, 1.0); exponent = max(exponent, FM_CURVE_MIN_EXPONENT);
if (x <= 0.0) { return 0.0; }
if (x >= 1.0) { return 1.0; }
return exp2(log2(max(x, FM_CURVE_EPS)) * exponent); }
float fmSafeSignedPow(float x, float exponent) { float ax = abs(x); exponent = max(exponent, FM_CURVE_MIN_EXPONENT);
if (ax <= 0.0) { return 0.0; }
float y = exp2(log2(max(ax, FM_CURVE_EPS)) * exponent);
return sign(x) * y; }
Construction / packing
FMChirpCurve8 fmCurve8Make( float freq, float mod, float modPhase, float phase, float chirp, float modChirp, float inputPow, float outputPow ) { FMChirpCurve8 p;
p.freq = freq; p.mod = mod; p.modPhase = modPhase; p.phase = phase; p.chirp = chirp; p.modChirp = modChirp; p.inputPow = inputPow; p.outputPow = outputPow;
return p; }
uvec4 fmCurve8PackToUVec4(FMChirpCurve8 p) { return uvec4( packHalf2x16(vec2(p.freq, p.mod)), packHalf2x16(vec2(p.modPhase, p.phase)), packHalf2x16(vec2(p.chirp, p.modChirp)), packHalf2x16(vec2(p.inputPow, p.outputPow)) ); }
FMChirpCurve8 fmCurve8UnpackFromUVec4(uvec4 h) { vec2 ab = unpackHalf2x16(h.x); vec2 cd = unpackHalf2x16(h.y); vec2 fg = unpackHalf2x16(h.z); vec2 nn = unpackHalf2x16(h.w);
FMChirpCurve8 p;
p.freq = ab.x; p.mod = ab.y;
p.modPhase = cd.x; p.phase = cd.y;
p.chirp = fg.x; p.modChirp = fg.y;
p.inputPow = nn.x; p.outputPow = nn.y;
return p; }
Only use these vec4 bitcast versions if the vec4 is an opaque bit container. Do not pass these floats through interpolation, filtering, color attachments, normalized formats, or anything that may canonicalize NaNs.
vec4 fmCurve8PackToOpaqueVec4(FMChirpCurve8 p) { uvec4 u = fmCurve8PackToUVec4(p);
return vec4( uintBitsToFloat(u.x), uintBitsToFloat(u.y), uintBitsToFloat(u.z), uintBitsToFloat(u.w) ); }
FMChirpCurve8 fmCurve8UnpackFromOpaqueVec4(vec4 v) { uvec4 u = uvec4( floatBitsToUint(v.x), floatBitsToUint(v.y), floatBitsToUint(v.z), floatBitsToUint(v.w) );
return fmCurve8UnpackFromUVec4(u); }
Evaluation
float fmCurve8QAtWarpedX(float xWarped, FMChirpCurve8 p) { float x = xWarped; float xx = x * x;
float modPhaseValue = FM_CURVE_TAU * ( p.mod * x + p.modPhase + p.modChirp * xx );
float phaseValue = FM_CURVE_TAU * ( p.freq * x + p.phase + p.chirp * xx );
return sin(phaseValue + sin(modPhaseValue)); }
float fmCurve8Q(float x, FMChirpCurve8 p) { float xWarped = fmSafeSignedPow(x, p.inputPow);
return fmCurve8QAtWarpedX(xWarped, p); }
Bounded [0,1] version. Note: zero params produce flat 0.5, not identity. float fmCurve8Direct01(float x, FMChirpCurve8 p) { float q = fmCurve8Q(x, p);
float y = 0.5 + 0.5 * q;
return fmSafePow01(y, p.outputPow); }
Free residual version. Zero params produce identity. This is probably the best default shader-art remap primitive. float fmCurve8Residual( float x, FMChirpCurve8 p, float gain, bool shapeResidualWithPow ) { float q = fmCurve8Q(x, p);
if (shapeResidualWithPow) { q = fmSafeSignedPow(q, p.outputPow); }
return x + gain * q; }
Endpoint-anchored version. Zero params produce identity and y(0)=0, y(1)=1. float fmCurve8AnchoredResidual( float x, FMChirpCurve8 p, float gain, bool shapeResidualWithPow ) { float q = fmCurve8Q(x, p);
if (shapeResidualWithPow) { q = fmSafeSignedPow(q, p.outputPow); }
return x + gain * x * (1.0 - x) * q; }
float fmCurve8Eval( float x, FMChirpCurve8 p, int mode, float gain ) { if (mode == FM_CURVE_MODE_DIRECT_01) { return fmCurve8Direct01(x, p); }
if (mode == FM_CURVE_MODE_ANCHORED_RESIDUAL) { return fmCurve8AnchoredResidual( x, p, gain, true ); }
return fmCurve8Residual( x, p, gain, true ); }
Convenience overloads for packed data
float fmCurve8EvalPackedUVec4( float x, uvec4 packed, int mode, float gain ) { FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed); return fmCurve8Eval(x, p, mode, gain); }
float fmCurve8ResidualPackedUVec4( float x, uvec4 packed, float gain ) { FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed);
return fmCurve8Residual( x, p, gain, true ); }
float fmCurve8Direct01PackedUVec4( float x, uvec4 packed ) { FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed); return fmCurve8Direct01(x, p); }
float fmCurve8AnchoredResidualPackedUVec4( float x, uvec4 packed, float gain ) { FMChirpCurve8 p = fmCurve8UnpackFromUVec4(packed);
return fmCurve8AnchoredResidual( x, p, gain, true ); }
float fmCurve8EvalOpaqueVec4( float x, vec4 packedOpaque, int mode, float gain ) { FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque); return fmCurve8Eval(x, p, mode, gain); }
float fmCurve8ResidualOpaqueVec4( float x, vec4 packedOpaque, float gain ) { FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque);
return fmCurve8Residual( x, p, gain, true ); }
float fmCurve8Direct01OpaqueVec4( float x, vec4 packedOpaque ) { FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque); return fmCurve8Direct01(x, p); }
float fmCurve8AnchoredResidualOpaqueVec4( float x, vec4 packedOpaque, float gain ) { FMChirpCurve8 p = fmCurve8UnpackFromOpaqueVec4(packedOpaque);
return fmCurve8AnchoredResidual( x, p, gain, true ); }
Example usage
layout(std430, binding = 0) readonly buffer CurveBuffer { uvec4 curves[]; };
void main() { uint id = gl_GlobalInvocationID.x;
uvec4 packed = curves[id];
float x = 0.37;
float y = fmCurve8ResidualPackedUVec4(
x,
packed,
1.0 // gain
);
}
object FMChirpCurves
object FMChirpCurves {
private const val PARAMETER_COUNT = 8
private const val EPS = 1e-8f
private const val MIN_EXPONENT = 1e-4f
private const val LOG2 = 0.6931471805599453
private const val TAU = (2.0 * PI)
enum class Mode
enum class Mode {
y = pow(0.5 + 0.5*q(pow(x,n)), n2)
Bounded in [0,1], but zero params produce flat 0.5, not identity.
Direct01,
y = x + gain*signedPow(q(pow(x,n)), n2)
Free residual remap. Zero params produce identity. This is deliberately not endpoint anchored, so low-x and high-x targets still have real authority for art-directed fitting.
Residual,
y = x + gainx(1-x)*signedPow(q(pow(x,n)), n2)
Endpoint anchored. Zero params produce identity.
AnchoredResidual
}
data class Range(val min: Float, val max: Float)
data class FitOptions
data class FitOptions(
val leastSquares: LeastSquaresOptions = LeastSquaresOptions(maxIterations = 60),
val regularizationWeight: Float = 0.005f,
val parameterScales: FloatArray = floatArrayOf(8f, 8f, 1f, 1f, 8f, 8f, 2f, 2f),
val maxFreq: Float = 64f,
val maxChirp: Float = 64f,
val maxPow: Float = 8f
) {
init
init {
require(regularizationWeight >= 0f) { "regularizationWeight must be >= 0" }
require(parameterScales.size == PARAMETER_COUNT) { "parameterScales must have $PARAMETER_COUNT entries" }
require(parameterScales.all { it > 0f && it.isFinite() }) { "parameterScales must be positive finite values" }
}
}
data class FitStep
data class FitStep(
val modifier: CurveModifier,
val before: Params,
val after: Params,
val result: LeastSquaresResult
)
data class FitTrace
data class FitTrace(
val start: Params,
val result: Params,
val steps: List<FitStep>
)
fun interface CurveModifier
fun residuals(params: Params, base: Params): DoubleArray
data class Params
data class Params(
/** Carrier frequency over x in [0,1]. Corresponds to `a`. */
val freq: Float = 0f,
/** Modulator frequency over x in [0,1]. Corresponds to `b`. */
val mod: Float = 0f,
/** Modulator phase. Corresponds to `c`. */
val modPhase: Float = 0f,
/** Carrier phase. Corresponds to `d`. */
val phase: Float = 0f,
/** Carrier quadratic chirp. Corresponds to `f`. */
val chirp: Float = 0f,
/** Modulator quadratic chirp. Corresponds to `g`. */
val modChirp: Float = 0f,
/** Input shaping exponent. */
val inputPow: Float = 1f,
/** Output shaping exponent. */
val outputPow: Float = 1f
) {
fun toVector8h(): Vector8h
fun toVector8h(): Vector8h =
Vector8h(freq, mod, modPhase, phase, chirp, modChirp, inputPow, outputPow)
fun toVector(): DoubleArray
fun toVector(): DoubleArray =
doubleArrayOf(
freq.toDouble(),
mod.toDouble(),
modPhase.toDouble(),
phase.toDouble(),
chirp.toDouble(),
modChirp.toDouble(),
inputPow.toDouble(),
outputPow.toDouble()
)
fun safeInputPow(): Float
fun safeInputPow(): Float =
max(inputPow, MIN_EXPONENT)
fun safeOutputPow(): Float
fun safeOutputPow(): Float =
max(outputPow, MIN_EXPONENT)
fun withInputPow(pow: Float): Params
fun withInputPow(pow: Float): Params =
copy(inputPow = max(pow, MIN_EXPONENT))
fun withOutputPow(pow: Float): Params
fun withOutputPow(pow: Float): Params =
copy(outputPow = max(pow, MIN_EXPONENT))
fun withPow(input: Float, output: Float): Params
fun withPow(input: Float, output: Float): Params =
withInputPow(input).withOutputPow(output)
fun withWrappedPhases(): Params
fun withWrappedPhases(): Params =
copy(
modPhase = wrap01(modPhase),
phase = wrap01(phase)
)
fun clampedForHalf
fun clampedForHalf(
maxFreq: Float = 64f,
maxChirp: Float = 64f,
maxPow: Float = 8f
): Params {
return copy(
freq = freq.coerceIn(-maxFreq, maxFreq),
mod = mod.coerceIn(-maxFreq, maxFreq),
modPhase = modPhase.coerceIn(-maxFreq, maxFreq),
phase = phase.coerceIn(-maxFreq, maxFreq),
chirp = chirp.coerceIn(-maxChirp, maxChirp),
modChirp = modChirp.coerceIn(-maxChirp, maxChirp),
inputPow = inputPow.coerceIn(MIN_EXPONENT, maxPow),
outputPow = outputPow.coerceIn(MIN_EXPONENT, maxPow)
)
}
fun qRawAtWarpedX(xWarped: Float): Float
Raw q:
q(x) = sin( TAU(freqx + phase + chirpxx) + sin(TAU(modx + modPhase + modChirpxx)) )
Input x is already input-power warped here.
fun qRawAtWarpedX(xWarped: Float): Float {
val x = xWarped
val xx = x * x
val modPhaseValue =
TAU * (
mod * x +
modPhase +
modChirp * xx
)
val phaseValue =
TAU * (
freq * x +
phase +
chirp * xx
)
return sin(phaseValue + sin(modPhaseValue)).toFloat()
}
fun qRaw(x: Float): Float
fun qRaw(x: Float): Float {
val xWarped = signedPow(x, safeInputPow())
return qRawAtWarpedX(xWarped)
}
fun evalDirect01(x: Float): Float
Direct bounded transfer:
y = pow(0.5 + 0.5*q(pow(x,n)), n2)
This stays in [0,1]. It does not preserve endpoint identity unless the phases/frequencies happen to make that true.
fun evalDirect01(x: Float): Float {
val q = qRaw(x)
val y = 0.5f + 0.5f * q
return safePow01(y, safeOutputPow())
}
fun evalResidual
Free residual transfer:
y = x + gain*signedPow(q(pow(x,n)), n2)
This intentionally permits endpoint movement. Use evalAnchoredResidual when the curve must keep y(0)=0 and y(1)=1.
fun evalResidual(
x: Float,
gain: Float = 1f,
shapeResidualWithPow: Boolean = true
): Float {
val q = qRaw(x)
val shapedQ =
if (shapeResidualWithPow) {
safeSignedPow(q, safeOutputPow())
} else {
q
}
return x + gain * shapedQ
}
fun evalAnchoredResidual
Endpoint-anchored residual transfer:
y = x + gainx(1-x)*signedPow(q(pow(x,n)), n2)
fun evalAnchoredResidual(
x: Float,
gain: Float = 1f,
shapeResidualWithPow: Boolean = true
): Float {
val q = qRaw(x)
val shapedQ =
if (shapeResidualWithPow) {
safeSignedPow(q, safeOutputPow())
} else {
q
}
return x + gain * x * (1f - x) * shapedQ
}
fun eval
fun eval(
x: Float,
mode: Mode = Mode.Residual,
gain: Float = 1f
): Float {
return when (mode) {
Mode.Direct01 -> evalDirect01(x)
Mode.Residual -> evalResidual(x, gain)
Mode.AnchoredResidual -> evalAnchoredResidual(x, gain)
}
}
fun sample
fun sample(
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f
): FloatArray {
require(count >= 2) { "count must be >= 2" }
return FloatArray(count) { i ->
val x = i.toFloat() / (count - 1).toFloat()
eval(x, mode, gain)
}
}
fun range
fun range(
count: Int = 512,
mode: Mode = Mode.Residual,
gain: Float = 1f
): Range {
val s = sample(count, mode, gain)
var lo = Float.POSITIVE_INFINITY
var hi = Float.NEGATIVE_INFINITY
for (v in s) {
if (v < lo) lo = v
if (v > hi) hi = v
}
return Range(lo, hi)
}
fun roughness
Useful cheap measure for sorting generated curves. Larger values generally mean more folds / wiggles / local acceleration.
fun roughness(
count: Int = 512,
mode: Mode = Mode.Residual,
gain: Float = 1f
): Float {
val s = sample(count, mode, gain)
var total = 0f
for (i in 1 until s.lastIndex) {
val secondDifference = s[i - 1] - 2f * s[i] + s[i + 1]
total += abs(secondDifference)
}
return total
}
fun foldover
fun foldover(
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f,
xMin: Float = -0.2f,
xMax: Float = 1.2f
): Float {
require(count >= 2) { "count must be >= 2" }
var total = 0f
var px = xMin
var py = eval(px, mode, gain)
for (i in 1 until count) {
val t = i.toFloat() / (count - 1).toFloat()
val x = xMin + t * (xMax - xMin)
val y = eval(x, mode, gain)
val dx = x - px
if (dx > 0f && y < py) {
total += (py - y) / dx * dx
}
px = x
py = y
}
return total
}
fun area
fun area(
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f,
xMin: Float = 0f,
xMax: Float = 1f
): Float {
require(count >= 2) { "count must be >= 2" }
var total = 0f
var px = xMin
var py = eval(px, mode, gain)
for (i in 1 until count) {
val t = i.toFloat() / (count - 1).toFloat()
val x = xMin + t * (xMax - xMin)
val y = eval(x, mode, gain)
total += (x - px) * (py + y) * 0.5f
px = x
py = y
}
return total
}
fun fit
fun fit(
modifier: CurveModifier,
options: FitOptions = FitOptions()
): FitStep {
val base = this
val baseVector = base.toVector()
val result = leastSquares(
initial = baseVector,
options = options.leastSquares
) { vector ->
val candidate = fromVector(vector).clampedForHalf(
maxFreq = options.maxFreq,
maxChirp = options.maxChirp,
maxPow = options.maxPow
)
val candidateVector = candidate.toVector()
val modifierResiduals = modifier.residuals(candidate, base)
val reg = DoubleArray(PARAMETER_COUNT) { i ->
((candidateVector[i] - baseVector[i]) / options.parameterScales[i].toDouble()) *
options.regularizationWeight.toDouble()
}
modifierResiduals + reg
}
val after = fromVector(result.x).clampedForHalf(
maxFreq = options.maxFreq,
maxChirp = options.maxChirp,
maxPow = options.maxPow
)
return FitStep(modifier = modifier, before = base, after = after, result = result)
}
fun shape
fun shape(
vararg modifiers: CurveModifier,
options: FitOptions = FitOptions()
): Params =
shapeWithTrace(*modifiers, options = options).result
fun shapeWithTrace
fun shapeWithTrace(
vararg modifiers: CurveModifier,
options: FitOptions = FitOptions()
): FitTrace {
var current = this
val steps = mutableListOf<FitStep>()
for (modifier in modifiers) {
val step = current.fit(modifier, options)
steps += step
current = step.after
}
return FitTrace(start = this, result = current, steps = steps)
}
fun summaryHtml(): String
fun summaryHtml(): String {
val cells = listOf(
"freq" to freq,
"mod" to mod,
"modPhase" to modPhase,
"phase" to phase,
"chirp" to chirp,
"modChirp" to modChirp,
"inputPow" to inputPow,
"outputPow" to outputPow
)
// The numbers are pre-formatted by `fmt`, so they arrive as strings and the column is
// right-aligned explicitly rather than by Table's numeric inference.
val params = facts(cells.map { (label, value) -> label to fmt(value) })
.align("value", Align.RIGHT)
return FieldMarkdown.card(
"""
FMChirp {muted:Params}
${params.toHTML()}
"""
)
}
fun curveSvg
body — 187 lines, from line 1248
fun curveSvg(
width: Int = 560,
height: Int = 260,
samples: Int = 320,
mode: Mode = Mode.Residual,
gain: Float = 1f
): String {
val w = width.coerceAtLeast(240)
val h = height.coerceAtLeast(160)
val n = samples.coerceAtLeast(16)
val left = 42f
val right = 14f
val top = 16f
val bottom = 30f
val plotW = (w - left - right).coerceAtLeast(1f)
val plotH = (h - top - bottom).coerceAtLeast(1f)
val xMin = -0.2f
val xMax = 1.2f
val yMin = -0.2f
val yMax = 1.2f
val grid = floatArrayOf(0f, 0.2f, 0.4f, 0.5f, 0.6f, 0.8f, 1f)
fun sx(x: Float): Float = left + (x - xMin) / (xMax - xMin) * plotW
fun sy(y: Float): Float = top + (yMax - y) / (yMax - yMin) * plotH
fun p(v: Float): String = fmt(v)
val sampledYs = FloatArray(n) { i ->
val t = i.toFloat() / (n - 1).toFloat()
val x = xMin + t * (xMax - xMin)
eval(x, mode, gain)
}
val gridLines = buildString {
for (g in grid) {
val x = sx(g)
append("""<line x1="${p(x)}" y1="${p(top)}" x2="${p(x)}" y2="${p(top + plotH)}" stroke="#242424" stroke-width="1"/>""")
append("""<text x="${p(x)}" y="${p(h - 9f)}" fill="#777" font-size="10" text-anchor="middle">$g</text>""")
val y = sy(g)
append("""<line x1="${p(left)}" y1="${p(y)}" x2="${p(left + plotW)}" y2="${p(y)}" stroke="#242424" stroke-width="1"/>""")
append("""<text x="${p(left - 8f)}" y="${p(y + 3f)}" fill="#777" font-size="10" text-anchor="end">$g</text>""")
}
}
val path = buildString {
for (i in 0 until n) {
val t = i.toFloat() / (n - 1).toFloat()
val x = xMin + t * (xMax - xMin)
val y = sampledYs[i]
if (!y.isFinite()) continue
if (isEmpty()) append("M") else append(" L")
append(p(sx(x)))
append(' ')
append(p(sy(y.coerceIn(yMin, yMax))))
}
}
val zeroY = sy(0f)
val oneY = sy(1f)
val zeroX = sx(0f)
val oneX = sx(1f)
val plottedValues = sampledYs.filter { it.isFinite() }
val rangeMin = plottedValues.minOrNull() ?: Float.NaN
val rangeMax = plottedValues.maxOrNull() ?: Float.NaN
val hoverValues = sampledYs.joinToString(prefix = "[", postfix = "]") { y ->
if (y.isFinite()) fmt(y) else "null"
}
return """
<div style="display:inline-block;background:#050505;color:#e8e8e8;border:1px solid #303030;border-radius:4px;padding:8px;font-family:Iosevka, ui-monospace, SFMono-Regular, Menlo, monospace;">
<svg xmlns="http://www.w3.org/2000/svg" width="$w" height="$h" viewBox="0 0 $w $h" data-fmchirp-curve="true" data-values='$hoverValues' data-left="${p(left)}" data-top="${p(top)}" data-plot-w="${p(plotW)}" data-plot-h="${p(plotH)}" data-x-min="${p(xMin)}" data-x-max="${p(xMax)}" data-y-min="${p(yMin)}" data-y-max="${p(yMax)}" style="display:block;overflow:visible;background:#050505;">
<rect x="0" y="0" width="$w" height="$h" fill="#050505"/>
<rect x="${p(left)}" y="${p(top)}" width="${p(plotW)}" height="${p(plotH)}" fill="#080808" stroke="#343434" stroke-width="1"/>
$gridLines
<line x1="${p(zeroX)}" y1="${p(top)}" x2="${p(zeroX)}" y2="${p(top + plotH)}" stroke="#6f6f6f" stroke-width="1.2"/>
<line x1="${p(oneX)}" y1="${p(top)}" x2="${p(oneX)}" y2="${p(top + plotH)}" stroke="#4a4a4a" stroke-width="1"/>
<line x1="${p(left)}" y1="${p(zeroY)}" x2="${p(left + plotW)}" y2="${p(zeroY)}" stroke="#6f6f6f" stroke-width="1.2"/>
<line x1="${p(left)}" y1="${p(oneY)}" x2="${p(left + plotW)}" y2="${p(oneY)}" stroke="#4a4a4a" stroke-width="1"/>
<path d="$path" fill="none" stroke="#f5f5f5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<text x="${p(left)}" y="12" fill="#f2f2f2" font-size="11">FMChirp residual</text>
<text x="${p(left + plotW)}" y="12" fill="#777" font-size="10" text-anchor="end">x -0.2..1.2 y ${fmt(rangeMin)}..${fmt(rangeMax)}</text>
<g data-role="hover" visibility="hidden">
<line data-role="hover-x" x1="${p(left)}" y1="${p(top)}" x2="${p(left)}" y2="${p(top + plotH)}" stroke="#48c9ff" stroke-width="1" stroke-opacity="0.95" stroke-dasharray="3 4"/>
<line data-role="hover-y" x1="${p(left)}" y1="${p(top)}" x2="${p(left + plotW)}" y2="${p(top)}" stroke="#48c9ff" stroke-width="1" stroke-opacity="0.72" stroke-dasharray="3 4"/>
<circle data-role="hover-point" cx="${p(left)}" cy="${p(top)}" r="4" fill="#050505" stroke="#48c9ff" stroke-width="1.8"/>
<g data-role="hover-label" transform="translate(${p(left + 8f)} ${p(top + 8f)})">
<rect width="122" height="34" rx="3" fill="#061014" stroke="#48c9ff" stroke-width="1"/>
<text fill="#d9f6ff" font-size="10">
<tspan data-role="hover-label-x" x="7" y="13">x</tspan>
<tspan data-role="hover-label-y" x="7" y="27">y</tspan>
</text>
</g>
</g>
<rect data-role="hover-overlay" x="${p(left)}" y="${p(top)}" width="${p(plotW)}" height="${p(plotH)}" fill="transparent" style="cursor:crosshair;pointer-events:all;"/>
</svg>
<script>
(() => {
const script = document.currentScript;
const root = (script && script.parentElement) || document;
const curves = root.querySelectorAll('svg[data-fmchirp-curve]');
function clamp(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
function formatValue(v) {
if (!Number.isFinite(v)) return String(v);
const a = Math.abs(v);
if ((a >= 1000) || (a > 0 && a < 0.001)) return v.toExponential(3);
let s = v.toFixed(4);
while (s.indexOf('.') !== -1 && s.endsWith('0')) s = s.slice(0, -1);
if (s.endsWith('.')) s = s.slice(0, -1);
return s;
}
function valueAt(values, x, xMin, xMax) {
if (values.length === 1) return values[0];
const t = (x - xMin) / (xMax - xMin) * (values.length - 1);
const i = clamp(Math.floor(t), 0, values.length - 2);
const f = clamp(t - i, 0, 1);
const a = values[i];
const b = values[i + 1];
if (Number.isFinite(a) && Number.isFinite(b)) return a + (b - a) * f;
if (Number.isFinite(a)) return a;
if (Number.isFinite(b)) return b;
return NaN;
}
curves.forEach((svg) => {
if (svg.dataset.fmchirpHoverReady === '1') return;
svg.dataset.fmchirpHoverReady = '1';
const values = JSON.parse(svg.getAttribute('data-values') || '[]');
const hover = svg.querySelector('[data-role="hover"]');
const overlay = svg.querySelector('[data-role="hover-overlay"]');
const xLine = svg.querySelector('[data-role="hover-x"]');
const yLine = svg.querySelector('[data-role="hover-y"]');
const point = svg.querySelector('[data-role="hover-point"]');
const label = svg.querySelector('[data-role="hover-label"]');
const labelX = svg.querySelector('[data-role="hover-label-x"]');
const labelY = svg.querySelector('[data-role="hover-label-y"]');
if (!values.length || !hover || !overlay || !xLine || !yLine || !point || !label || !labelX || !labelY) return;
const left = parseFloat(svg.dataset.left || '0');
const top = parseFloat(svg.dataset.top || '0');
const plotW = parseFloat(svg.dataset.plotW || '1');
const plotH = parseFloat(svg.dataset.plotH || '1');
const xMin = parseFloat(svg.dataset.xMin || '0');
const xMax = parseFloat(svg.dataset.xMax || '1');
const yMin = parseFloat(svg.dataset.yMin || '0');
const yMax = parseFloat(svg.dataset.yMax || '1');
const labelW = 122;
const labelH = 34;
overlay.addEventListener('mousemove', (event) => {
const bounds = overlay.getBoundingClientRect();
if (!bounds.width) return;
const u = clamp((event.clientX - bounds.left) / bounds.width, 0, 1);
const x = xMin + u * (xMax - xMin);
const y = valueAt(values, x, xMin, xMax);
const px = left + u * plotW;
const py = top + (yMax - clamp(y, yMin, yMax)) / (yMax - yMin) * plotH;
const lx = clamp(px + 10, left + 4, left + plotW - labelW - 4);
const ly = clamp(py - labelH - 8, top + 4, top + plotH - labelH - 4);
hover.setAttribute('visibility', 'visible');
xLine.setAttribute('x1', String(px));
xLine.setAttribute('x2', String(px));
yLine.setAttribute('y1', String(py));
yLine.setAttribute('y2', String(py));
point.setAttribute('cx', String(px));
point.setAttribute('cy', String(py));
label.setAttribute('transform', 'translate(' + lx + ' ' + ly + ')');
labelX.textContent = 'x ' + formatValue(x);
labelY.textContent = 'y ' + formatValue(y);
});
overlay.addEventListener('mouseleave', () => {
hover.setAttribute('visibility', 'hidden');
});
});
})();
</script>
</div>
""".trimIndent()
}
}
fun fromVector8h(v: Vector8h): Params
fun fromVector8h(v: Vector8h): Params {
return Params(
freq = v.x(),
mod = v.y(),
modPhase = v.z(),
phase = v.w(),
chirp = v.a(),
modChirp = v.b(),
inputPow = v.c(),
outputPow = v.d()
)
}
fun make
fun make(
freq: Float = 0f,
mod: Float = 0f,
modPhase: Float = 0f,
phase: Float = 0f,
chirp: Float = 0f,
modChirp: Float = 0f,
inputPow: Float = 1f,
outputPow: Float = 1f
): Params {
return Params(
freq = freq,
mod = mod,
modPhase = modPhase,
phase = phase,
chirp = chirp,
modChirp = modChirp,
inputPow = max(inputPow, MIN_EXPONENT),
outputPow = max(outputPow, MIN_EXPONENT)
)
}
fun vector8h
fun vector8h(
freq: Float = 0f,
mod: Float = 0f,
modPhase: Float = 0f,
phase: Float = 0f,
chirp: Float = 0f,
modChirp: Float = 0f,
inputPow: Float = 1f,
outputPow: Float = 1f
): Vector8h {
return make(
freq = freq,
mod = mod,
modPhase = modPhase,
phase = phase,
chirp = chirp,
modChirp = modChirp,
inputPow = inputPow,
outputPow = outputPow
).toVector8h()
}
fun fromVector(values: DoubleArray): Params
fun fromVector(values: DoubleArray): Params {
require(values.size == PARAMETER_COUNT) { "FMChirp parameter vector must have $PARAMETER_COUNT entries" }
return make(
freq = values[0].toFloat(),
mod = values[1].toFloat(),
modPhase = values[2].toFloat(),
phase = values[3].toFloat(),
chirp = values[4].toFloat(),
modChirp = values[5].toFloat(),
inputPow = values[6].toFloat(),
outputPow = values[7].toFloat()
)
}
object Modifiers
fun hit
fun hit(
x: Float,
y: Float,
weight: Float = 20f,
mode: Mode = Mode.Residual,
gain: Float = 1f
): CurveModifier = CurveModifier { params, _ ->
doubleArrayOf((params.eval(x, mode, gain) - y).toDouble() * weight.toDouble())
}
fun area
fun area(
target: Float,
weight: Float = 1f,
count: Int = 128,
mode: Mode = Mode.Residual,
gain: Float = 1f,
xMin: Float = 0f,
xMax: Float = 1f
): CurveModifier = CurveModifier { params, _ ->
doubleArrayOf(
(params.area(count, mode, gain, xMin, xMax) - target).toDouble() *
weight.toDouble()
)
}
fun areaDelta
fun areaDelta(
delta: Float,
weight: Float = 1f,
count: Int = 128,
mode: Mode = Mode.Residual,
gain: Float = 1f,
xMin: Float = 0f,
xMax: Float = 1f
): CurveModifier = CurveModifier { params, base ->
val target = base.area(count, mode, gain, xMin, xMax) + delta
doubleArrayOf(
(params.area(count, mode, gain, xMin, xMax) - target).toDouble() *
weight.toDouble()
)
}
fun roughness
fun roughness(
target: Float,
weight: Float = 1f,
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f
): CurveModifier = CurveModifier { params, _ ->
doubleArrayOf((params.roughness(count, mode, gain) - target).toDouble() * weight.toDouble())
}
fun roughnessDelta
fun roughnessDelta(
delta: Float,
weight: Float = 1f,
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f
): CurveModifier = CurveModifier { params, base ->
val target = base.roughness(count, mode, gain) + delta
doubleArrayOf((params.roughness(count, mode, gain) - target).toDouble() * weight.toDouble())
}
fun foldover
fun foldover(
target: Float,
weight: Float = 1f,
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f,
xMin: Float = -0.2f,
xMax: Float = 1.2f
): CurveModifier = CurveModifier { params, _ ->
doubleArrayOf(
(params.foldover(count, mode, gain, xMin, xMax) - target).toDouble() *
weight.toDouble()
)
}
fun foldoverDelta
fun foldoverDelta(
delta: Float,
weight: Float = 1f,
count: Int = 256,
mode: Mode = Mode.Residual,
gain: Float = 1f,
xMin: Float = -0.2f,
xMax: Float = 1.2f
): CurveModifier = CurveModifier { params, base ->
val target = base.foldover(count, mode, gain, xMin, xMax) + delta
doubleArrayOf(
(params.foldover(count, mode, gain, xMin, xMax) - target).toDouble() *
weight.toDouble()
)
}
}
object Presets
object Presets {
fun neutral(): Params = Params()
Direct01 mode: flat 0.5. Residual mode: identity.
fun lazyBend(): Params
fun lazyBend(): Params =
make(
freq = 0.45f,
mod = 0.10f,
modPhase = 0.00f,
phase = 0.00f,
chirp = 0.00f,
modChirp = 0.00f,
inputPow = 1.0f,
outputPow = 1.0f
)
fun softThreshold(): Params
fun softThreshold(): Params =
make(
freq = 0.80f,
mod = 0.35f,
modPhase = 0.15f,
phase = -0.20f,
chirp = 0.85f,
modChirp = -0.35f,
inputPow = 1.15f,
outputPow = 0.85f
)
fun humpShoulder(): Params
fun humpShoulder(): Params =
make(
freq = 0.15f,
mod = 0.85f,
modPhase = 0.20f,
phase = 0.05f,
chirp = -0.55f,
modChirp = 0.35f,
inputPow = 1.0f,
outputPow = 1.0f
)
fun delayedRise(): Params
fun delayedRise(): Params =
make(
freq = -0.25f,
mod = 0.60f,
modPhase = 0.30f,
phase = -0.10f,
chirp = 0.75f,
modChirp = 0.20f,
inputPow = 1.65f,
outputPow = 1.0f
)
fun doubleHump(): Params
fun doubleHump(): Params =
make(
freq = 1.15f,
mod = 1.40f,
modPhase = 0.00f,
phase = 0.00f,
chirp = -0.75f,
modChirp = 2.25f,
inputPow = 1.0f,
outputPow = 1.0f
)
fun parameterFoldback(): Params
fun parameterFoldback(): Params =
make(
freq = 1.60f,
mod = 1.80f,
modPhase = 0.10f,
phase = -0.25f,
chirp = -1.20f,
modChirp = 2.75f,
inputPow = 1.0f,
outputPow = 0.75f
)
fun brokenTransfer(): Params
fun brokenTransfer(): Params =
make(
freq = 2.20f,
mod = 1.10f,
modPhase = 0.25f,
phase = 0.00f,
chirp = -1.60f,
modChirp = 3.10f,
inputPow = 0.85f,
outputPow = 1.20f
)
fun smallTurbulence(): Params
fun smallTurbulence(): Params =
make(
freq = 2.80f,
mod = 5.00f,
modPhase = 0.07f,
phase = 0.00f,
chirp = -1.90f,
modChirp = 7.00f,
inputPow = 1.0f,
outputPow = 1.0f
)
fun aggressiveTurbulence(): Params
fun aggressiveTurbulence(): Params =
make(
freq = 4.20f,
mod = 6.00f,
modPhase = 0.00f,
phase = 0.10f,
chirp = -3.25f,
modChirp = 9.00f,
inputPow = 1.0f,
outputPow = 0.85f
)
}
object HalfPacking
Optional CPU-side half packing helpers. Useful for tests/debugging against GLSL packHalf2x16.
fun packHalf2x16(x: Float, y: Float): Int
fun packHalf2x16(x: Float, y: Float): Int {
val lo = floatToHalfBits(x)
val hi = floatToHalfBits(y)
return lo or (hi shl 16)
}
fun packToUvec4Bits(p: Params): IntArray
fun packToUvec4Bits(p: Params): IntArray {
return intArrayOf(
packHalf2x16(p.freq, p.mod),
packHalf2x16(p.modPhase, p.phase),
packHalf2x16(p.chirp, p.modChirp),
packHalf2x16(p.inputPow, p.outputPow)
)
}
fun packToOpaqueFloatVec4(p: Params): Vector4f
Only use this if you intentionally want opaque bit-pattern floats, e.g. to match uintBitsToFloat(packHalf2x16(...)).
Do not treat these as numeric floats.
fun packToOpaqueFloatVec4(p: Params): Vector4f {
val u = packToUvec4Bits(p)
return Vector4f(
java.lang.Float.intBitsToFloat(u[0]),
java.lang.Float.intBitsToFloat(u[1]),
java.lang.Float.intBitsToFloat(u[2]),
java.lang.Float.intBitsToFloat(u[3])
)
}
fun floatToHalfBits(value: Float): Int
body — 66 lines, from line 1754
fun floatToHalfBits(value: Float): Int {
val f = java.lang.Float.floatToRawIntBits(value)
val sign = (f ushr 16) and 0x8000
val exp = (f ushr 23) and 0xff
val mant = f and 0x7fffff
if (exp == 0xff) {
return if (mant == 0) {
sign or 0x7c00
} else {
sign or 0x7e00
}
}
val halfExpUnbiased = exp - 127 + 15
if (halfExpUnbiased >= 0x1f) {
return sign or 0x7c00
}
if (halfExpUnbiased <= 0) {
if (halfExpUnbiased < -10) {
return sign
}
val mantWithHiddenBit = mant or 0x800000
val shift = 14 - halfExpUnbiased
var halfMant = mantWithHiddenBit ushr shift
val roundBit = (mantWithHiddenBit ushr (shift - 1)) and 1
val stickyMask = (1 shl (shift - 1)) - 1
val sticky = mantWithHiddenBit and stickyMask
if (roundBit == 1 && (sticky != 0 || (halfMant and 1) != 0)) {
halfMant += 1
}
return sign or halfMant
}
var halfExp = halfExpUnbiased shl 10
var halfMant = mant ushr 13
val roundBits = mant and 0x1fff
if (
roundBits > 0x1000 ||
(roundBits == 0x1000 && (halfMant and 1) != 0)
) {
halfMant += 1
if (halfMant == 0x400) {
halfMant = 0
halfExp += 0x400
if (halfExp >= 0x7c00) {
return sign or 0x7c00
}
}
}
return sign or halfExp or halfMant
}
}
fun safePow01(x: Float, exponent: Float): Float
fun safePow01(x: Float, exponent: Float): Float {
val cx = x.coerceIn(0f, 1f)
val e = max(exponent, MIN_EXPONENT)
if (cx <= 0f) return 0f
if (cx >= 1f) return 1f
return exp2(log2(cx) * e)
}
fun safeSignedPow(x: Float, exponent: Float): Float
fun safeSignedPow(x: Float, exponent: Float): Float {
val ax = abs(x)
val e = max(exponent, MIN_EXPONENT)
if (ax <= 0f) return 0f
val y = exp2(log2(ax) * e)
return when {
x < 0f -> -y
x > 0f -> y
else -> 0f
}
}
fun signedPow(x: Float, exponent: Float): Float
fun signedPow(x: Float, exponent: Float): Float {
val ax = abs(x)
val e = max(exponent, MIN_EXPONENT)
if (ax <= 0f) return 0f
val y = exp2(log2(ax) * e)
return if (x < 0f) -y else y
}
fun exp2(x: Float): Float
fun exp2(x: Float): Float =
exp(LOG2 * x.toDouble()).toFloat()
fun log2(x: Float): Float
fun log2(x: Float): Float =
(ln(x.toDouble()) / LOG2).toFloat()
fun wrap01(x: Float): Float
fun wrap01(x: Float): Float {
val f = floor(x.toDouble()).toFloat()
return x - f
}
private fun fmt(value: Float): String =
when {
value.isNaN() -> "NaN"
value == Float.POSITIVE_INFINITY -> "Inf"
value == Float.NEGATIVE_INFINITY -> "-Inf"
abs(value) >= 1000f || abs(value) < 0.001f && value != 0f ->
String.format(Locale.US, "%.3e", value)
else -> String.format(Locale.US, "%.4f", value).trimEnd('0').trimEnd('.')
}
}
val FMChirpCurves.Params.h: String
val FMChirpCurves.Params.h: String
get() = summaryHtml()
val FMChirpCurves.Params.h2: String
val FMChirpCurves.Params.h2: String
get() = curveSvg()