Compare commits
2 Commits
3a180eb983
...
c4f4cf34c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4f4cf34c0 | ||
|
|
58639d07cf |
@@ -22,8 +22,8 @@ android {
|
||||
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 18
|
||||
versionName = "0.6.0"
|
||||
versionCode = 20
|
||||
versionName = "0.7.1"
|
||||
|
||||
// We provide our own instrumentation runner if/when tests are added.
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -1225,7 +1225,10 @@ class AudioEngine(
|
||||
private const val LIVE_POLYPHONY = 8
|
||||
private const val AUDITION_POLYPHONY = 8
|
||||
private const val BUS_POLYPHONY = 8 // per-mixer-channel live audition
|
||||
private const val MASTER_GAIN = 0.35f
|
||||
// Master make-up before the soft-clip limiter. Raised from 0.35 → 0.5 (~+3 dB)
|
||||
// so playback sits closer to other media apps (it had noticeably more headroom
|
||||
// than typical music playback); the soft-knee limiter below still catches peaks.
|
||||
private const val MASTER_GAIN = 0.5f
|
||||
private const val LIVE_GAIN = 0.5f
|
||||
private const val SOFT_KNEE = 0.6f // limiter is transparent below this, saturates above
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.max
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.math.tanh
|
||||
|
||||
/**
|
||||
@@ -39,6 +41,7 @@ interface AudioEffect {
|
||||
ToolboxType.FILTER -> BiquadFilterEffect(sampleRate)
|
||||
ToolboxType.BITCRUSHER -> Bitcrusher()
|
||||
ToolboxType.EQ10 -> GraphicEq(sampleRate)
|
||||
ToolboxType.MIX_TIGHTENER -> MixTightener(sampleRate)
|
||||
else -> null // arpeggiator / transposer / LFO are handled by the sequencer
|
||||
}
|
||||
}
|
||||
@@ -322,3 +325,197 @@ private class GraphicEq(private val sampleRate: Int) : AudioEffect {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reusable RBJ biquad (transposed direct form II, double state) for the Mix
|
||||
// Tightener. Coefficients are set at block rate; process() runs per sample and
|
||||
// self-recovers to silence if state ever goes non-finite.
|
||||
// ---------------------------------------------------------------------------
|
||||
private class Bq {
|
||||
private var b0 = 1.0; private var b1 = 0.0; private var b2 = 0.0
|
||||
private var a1 = 0.0; private var a2 = 0.0
|
||||
private var z1 = 0.0; private var z2 = 0.0
|
||||
|
||||
fun identity() { b0 = 1.0; b1 = 0.0; b2 = 0.0; a1 = 0.0; a2 = 0.0 }
|
||||
|
||||
private fun setRaw(nb0: Double, nb1: Double, nb2: Double, a0: Double, na1: Double, na2: Double) {
|
||||
b0 = nb0 / a0; b1 = nb1 / a0; b2 = nb2 / a0; a1 = na1 / a0; a2 = na2 / a0
|
||||
}
|
||||
|
||||
fun peak(sr: Int, freq: Float, gainDb: Float, q: Float) {
|
||||
val f = freq.coerceIn(20f, sr * 0.45f)
|
||||
val a = 10.0.pow(gainDb / 40.0)
|
||||
val w0 = 2.0 * PI * f / sr; val cw = cos(w0); val alpha = sin(w0) / (2.0 * q.coerceAtLeast(0.1f))
|
||||
setRaw(1 + alpha * a, -2 * cw, 1 - alpha * a, 1 + alpha / a, -2 * cw, 1 - alpha / a)
|
||||
}
|
||||
|
||||
fun lowShelf(sr: Int, freq: Float, gainDb: Float) {
|
||||
val f = freq.coerceIn(20f, sr * 0.45f)
|
||||
val a = 10.0.pow(gainDb / 40.0); val w0 = 2.0 * PI * f / sr; val cw = cos(w0)
|
||||
val alpha = sin(w0) / 2.0 * sqrt(2.0); val ta = 2.0 * sqrt(a) * alpha
|
||||
setRaw(
|
||||
a * ((a + 1) - (a - 1) * cw + ta), 2 * a * ((a - 1) - (a + 1) * cw),
|
||||
a * ((a + 1) - (a - 1) * cw - ta), (a + 1) + (a - 1) * cw + ta,
|
||||
-2 * ((a - 1) + (a + 1) * cw), (a + 1) + (a - 1) * cw - ta,
|
||||
)
|
||||
}
|
||||
|
||||
fun highShelf(sr: Int, freq: Float, gainDb: Float) {
|
||||
val f = freq.coerceIn(20f, sr * 0.45f)
|
||||
val a = 10.0.pow(gainDb / 40.0); val w0 = 2.0 * PI * f / sr; val cw = cos(w0)
|
||||
val alpha = sin(w0) / 2.0 * sqrt(2.0); val ta = 2.0 * sqrt(a) * alpha
|
||||
setRaw(
|
||||
a * ((a + 1) + (a - 1) * cw + ta), -2 * a * ((a - 1) + (a + 1) * cw),
|
||||
a * ((a + 1) + (a - 1) * cw - ta), (a + 1) - (a - 1) * cw + ta,
|
||||
2 * ((a - 1) - (a + 1) * cw), (a + 1) - (a - 1) * cw - ta,
|
||||
)
|
||||
}
|
||||
|
||||
fun lowPass(sr: Int, freq: Float, q: Float) {
|
||||
val f = freq.coerceIn(20f, sr * 0.45f)
|
||||
val w0 = 2.0 * PI * f / sr; val cw = cos(w0); val alpha = sin(w0) / (2.0 * q)
|
||||
setRaw((1 - cw) / 2, 1 - cw, (1 - cw) / 2, 1 + alpha, -2 * cw, 1 - alpha)
|
||||
}
|
||||
|
||||
fun highPass(sr: Int, freq: Float, q: Float) {
|
||||
val f = freq.coerceIn(20f, sr * 0.45f)
|
||||
val w0 = 2.0 * PI * f / sr; val cw = cos(w0); val alpha = sin(w0) / (2.0 * q)
|
||||
setRaw((1 + cw) / 2, -(1 + cw), (1 + cw) / 2, 1 + alpha, -2 * cw, 1 - alpha)
|
||||
}
|
||||
|
||||
fun reset() { z1 = 0.0; z2 = 0.0 }
|
||||
|
||||
fun process(x: Float): Float {
|
||||
val inp = x.toDouble()
|
||||
var out = b0 * inp + z1
|
||||
z1 = b1 * inp - a1 * out + z2
|
||||
z2 = b2 * inp - a2 * out
|
||||
if (!out.isFinite()) { z1 = 0.0; z2 = 0.0; out = 0.0 }
|
||||
return out.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mix Tightener: a preset-driven "channel strip in a box" (parametric EQ →
|
||||
// 3-band multiband compressor → peak limiter) behind ONE user control, Strength,
|
||||
// which morphs the whole chain from transparent (0) to the full preset (1). Every
|
||||
// per-band value is a hidden slot param the genre/instrument presets set; only
|
||||
// Strength is exposed in the editor.
|
||||
//
|
||||
// Performance: crossover/EQ are biquads; the expensive per-band gain computation
|
||||
// (log/pow) runs at control rate (every CTRL samples) while the envelope follower
|
||||
// runs per sample. Idle/gentle settings short-circuit: Strength≈0 bypasses the
|
||||
// whole effect, flat EQ bands and ratio≈1 comp bands are skipped.
|
||||
// ---------------------------------------------------------------------------
|
||||
private class MixTightener(private val sampleRate: Int) : AudioEffect {
|
||||
// Parametric EQ: low shelf, mid peak, high shelf.
|
||||
private val eqLo = Bq(); private val eqMid = Bq(); private val eqHi = Bq()
|
||||
private var eqActive = false
|
||||
// Linkwitz-Riley-ish 3-way crossover (Q≈0.5) into low / mid / high.
|
||||
private val xLoLP = Bq(); private val xLoHP = Bq(); private val xHiLP = Bq(); private val xHiHP = Bq()
|
||||
private var compActive = false
|
||||
// Per-band compressor state (0=low,1=mid,2=high).
|
||||
private val env = FloatArray(3)
|
||||
private val gain = FloatArray(3) { 1f } // per-sample smoothed applied gain
|
||||
private val target = FloatArray(3) { 1f } // control-rate gain target
|
||||
private val atkCoef = FloatArray(3); private val relCoef = FloatArray(3)
|
||||
private val thrDb = FloatArray(3); private val ratio = FloatArray(3); private val makeDb = FloatArray(3)
|
||||
private var ctrl = 0
|
||||
// Limiter + output.
|
||||
private var limCeil = 1f; private var limRelCoef = 0f; private var limEnv = 0f
|
||||
private var outGain = 1f
|
||||
private var strength = 1f
|
||||
private var compScale = 1f // master × per-component "Comp" amount, applied to the gain reduction
|
||||
|
||||
private fun dbToLin(db: Float): Float = 10.0.pow(db / 20.0).toFloat()
|
||||
private fun coefFromMs(ms: Float): Float {
|
||||
val t = (ms.coerceAtLeast(0.1f)) * 0.001f * sampleRate
|
||||
return (1.0 - exp(-1.0 / t)).toFloat()
|
||||
}
|
||||
|
||||
override fun update(slot: ToolboxSlot, tempoBpm: Float) {
|
||||
strength = slot.float("strength", 1f).coerceIn(0f, 1f)
|
||||
// Per-component amounts (default 1) let each stage be trimmed independently;
|
||||
// the effective scale for a stage is master strength × that stage's amount.
|
||||
val eqS = strength * slot.float("eqAmt", 1f).coerceIn(0f, 1f)
|
||||
val compAmt = slot.float("compAmt", 1f).coerceIn(0f, 1f)
|
||||
compScale = strength * compAmt
|
||||
val limS = strength * slot.float("limAmt", 1f).coerceIn(0f, 1f)
|
||||
|
||||
// EQ — gains scaled by the EQ amount so 0 is flat.
|
||||
val loG = slot.float("eqLoGain", 0f).coerceIn(-18f, 18f) * eqS
|
||||
val midG = slot.float("eqMidGain", 0f).coerceIn(-18f, 18f) * eqS
|
||||
val hiG = slot.float("eqHiGain", 0f).coerceIn(-18f, 18f) * eqS
|
||||
if (abs(loG) > 0.1f) eqLo.lowShelf(sampleRate, slot.float("eqLoFreq", 120f), loG) else eqLo.identity()
|
||||
if (abs(midG) > 0.1f) eqMid.peak(sampleRate, slot.float("eqMidFreq", 1000f), midG,
|
||||
slot.float("eqMidQ", 1.0f).coerceIn(0.2f, 8f)) else eqMid.identity()
|
||||
if (abs(hiG) > 0.1f) eqHi.highShelf(sampleRate, slot.float("eqHiFreq", 8000f), hiG) else eqHi.identity()
|
||||
eqActive = abs(loG) > 0.1f || abs(midG) > 0.1f || abs(hiG) > 0.1f
|
||||
|
||||
// Crossovers.
|
||||
val xl = slot.float("xLo", 200f).coerceIn(40f, sampleRate * 0.4f)
|
||||
val xh = slot.float("xHi", 2500f).coerceIn(xl + 50f, sampleRate * 0.45f)
|
||||
xLoLP.lowPass(sampleRate, xl, 0.5f); xLoHP.highPass(sampleRate, xl, 0.5f)
|
||||
xHiLP.lowPass(sampleRate, xh, 0.5f); xHiHP.highPass(sampleRate, xh, 0.5f)
|
||||
|
||||
// Compressor bands.
|
||||
thrDb[0] = slot.float("cLoThr", -24f); ratio[0] = slot.float("cLoRatio", 2f).coerceIn(1f, 20f)
|
||||
makeDb[0] = slot.float("cLoMk", 0f); atkCoef[0] = coefFromMs(slot.float("cLoAtk", 10f)); relCoef[0] = coefFromMs(slot.float("cLoRel", 120f))
|
||||
thrDb[1] = slot.float("cMidThr", -22f); ratio[1] = slot.float("cMidRatio", 2f).coerceIn(1f, 20f)
|
||||
makeDb[1] = slot.float("cMidMk", 0f); atkCoef[1] = coefFromMs(slot.float("cMidAtk", 15f)); relCoef[1] = coefFromMs(slot.float("cMidRel", 140f))
|
||||
thrDb[2] = slot.float("cHiThr", -24f); ratio[2] = slot.float("cHiRatio", 2f).coerceIn(1f, 20f)
|
||||
makeDb[2] = slot.float("cHiMk", 0f); atkCoef[2] = coefFromMs(slot.float("cHiAtk", 5f)); relCoef[2] = coefFromMs(slot.float("cHiRel", 90f))
|
||||
compActive = compAmt > 0.001f && (ratio.any { it > 1.01f } || makeDb.any { abs(it) > 0.1f })
|
||||
|
||||
// Limiter: at amount 0 the ceiling is 1.0 (no limiting) and output gain is unity,
|
||||
// at 1 they're the preset values.
|
||||
limCeil = 1f + (dbToLin(slot.float("limCeil", -0.5f).coerceIn(-24f, 0f)) - 1f) * limS
|
||||
limRelCoef = coefFromMs(slot.float("limRel", 100f))
|
||||
outGain = 1f + (dbToLin(slot.float("outGain", 0f).coerceIn(-12f, 18f)) - 1f) * limS
|
||||
}
|
||||
|
||||
private fun compBand(i: Int, band: Float): Float {
|
||||
val r = abs(band)
|
||||
val e = env[i]
|
||||
env[i] = if (r > e) e + atkCoef[i] * (r - e) else e + relCoef[i] * (r - e)
|
||||
gain[i] += GAIN_SMOOTH * (target[i] - gain[i]) // click-free gain ramp
|
||||
return band * gain[i]
|
||||
}
|
||||
|
||||
override fun process(x: Float): Float {
|
||||
if (strength < 0.001f) return x
|
||||
var s = x
|
||||
if (eqActive) { s = eqLo.process(s); s = eqMid.process(s); s = eqHi.process(s) }
|
||||
|
||||
if (compActive) {
|
||||
val low = xLoLP.process(s)
|
||||
val hp = xLoHP.process(s)
|
||||
val mid = xHiLP.process(hp)
|
||||
val high = xHiHP.process(hp)
|
||||
if (ctrl <= 0) {
|
||||
ctrl = CTRL
|
||||
for (i in 0 until 3) {
|
||||
val envDb = 20f * log10(env[i] + 1e-9f)
|
||||
val over = envDb - thrDb[i]
|
||||
val grDb = if (over > 0f) -over * (1f - 1f / ratio[i]) else 0f
|
||||
target[i] = dbToLin(((grDb + makeDb[i]) * compScale).coerceIn(-40f, 24f))
|
||||
}
|
||||
}
|
||||
ctrl--
|
||||
s = compBand(0, low) + compBand(1, mid) + compBand(2, high)
|
||||
}
|
||||
|
||||
// Peak limiter: instant attack, smoothed release; a plain divide (no log/pow).
|
||||
val r = abs(s)
|
||||
limEnv = if (r > limEnv) r else limEnv + limRelCoef * (r - limEnv)
|
||||
if (limEnv > limCeil && limEnv > 1e-6f) s *= limCeil / limEnv
|
||||
s *= outGain
|
||||
// Safety: never leave the effect hotter than full scale.
|
||||
return if (s > 1f) 1f else if (s < -1f) -1f else if (!s.isFinite()) 0f else s
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CTRL = 16 // samples between (expensive) gain recomputes
|
||||
const val GAIN_SMOOTH = 0.05f // per-sample gain ramp toward the control-rate target
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package space.rcmd.android.sizzle.io
|
||||
|
||||
import space.rcmd.android.sizzle.audio.SampleStore
|
||||
import space.rcmd.android.sizzle.model.AmbiencePresets
|
||||
import space.rcmd.android.sizzle.model.MixTightenerPresets
|
||||
import space.rcmd.android.sizzle.model.NesSynthPresets
|
||||
import space.rcmd.android.sizzle.model.Project
|
||||
import space.rcmd.android.sizzle.model.SamplerPads
|
||||
@@ -51,6 +52,9 @@ class PresetLibrary(private val baseDir: File) {
|
||||
seedType(ToolboxType.NES_SYNTH, NesSynthPresets.FACTORY.map { it.name }) { slot, name ->
|
||||
NesSynthPresets.apply(slot, NesSynthPresets.FACTORY.first { it.name == name })
|
||||
}
|
||||
seedType(ToolboxType.MIX_TIGHTENER, MixTightenerPresets.FACTORY.map { it.name }) { slot, name ->
|
||||
MixTightenerPresets.apply(slot, MixTightenerPresets.FACTORY.first { it.name == name })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package space.rcmd.android.sizzle.model
|
||||
|
||||
/**
|
||||
* Factory presets for the [ToolboxType.MIX_TIGHTENER] effect — genre- and
|
||||
* instrument-targeted "channel strip in a box" settings. Each preset is a sparse
|
||||
* map of the effect's hidden per-band parameters (EQ / crossover / 3-band
|
||||
* compressor / limiter / output); anything omitted falls back to the DSP's
|
||||
* defaults in `MixTightener`. Only the single **Strength** control is exposed in
|
||||
* the editor — these presets are the "under the hood" tuning it morphs toward.
|
||||
*
|
||||
* Parameter keys (all optional, dB / Hz / ms):
|
||||
* strength 0..1 morph amount (default 1)
|
||||
* eqLoFreq eqLoGain low shelf
|
||||
* eqMidFreq eqMidGain eqMidQ mid peak
|
||||
* eqHiFreq eqHiGain high shelf
|
||||
* xLo xHi band crossover frequencies
|
||||
* cLoThr cLoRatio cLoAtk cLoRel cLoMk low-band compressor
|
||||
* cMidThr cMidRatio cMidAtk cMidRel cMidMk mid-band compressor
|
||||
* cHiThr cHiRatio cHiAtk cHiRel cHiMk high-band compressor
|
||||
* limCeil limRel peak limiter
|
||||
* outGain make-up at the output
|
||||
*/
|
||||
object MixTightenerPresets {
|
||||
|
||||
class Preset(val name: String, val params: Map<String, String>)
|
||||
|
||||
/** Apply [preset]'s overrides onto an already-[ToolboxSlot.fill]ed slot. */
|
||||
fun apply(slot: ToolboxSlot, preset: Preset) {
|
||||
preset.params.forEach { (k, v) -> slot.set(k, v) }
|
||||
}
|
||||
|
||||
val FACTORY: List<Preset> = listOf(
|
||||
// ---------------- Drums & percussion ----------------
|
||||
Preset("808 Crisp", mapOf(
|
||||
"eqLoFreq" to "70", "eqLoGain" to "4", "eqMidFreq" to "450", "eqMidGain" to "-2",
|
||||
"eqHiFreq" to "9000", "eqHiGain" to "4", "xLo" to "120", "xHi" to "3000",
|
||||
"cLoThr" to "-22", "cLoRatio" to "4", "cLoAtk" to "8", "cLoRel" to "140", "cLoMk" to "3",
|
||||
"cMidThr" to "-20", "cMidRatio" to "2.5", "cMidMk" to "1",
|
||||
"cHiThr" to "-24", "cHiRatio" to "3", "cHiAtk" to "3", "cHiRel" to "80", "cHiMk" to "2",
|
||||
"limCeil" to "-0.3", "limRel" to "80", "outGain" to "2",
|
||||
)),
|
||||
Preset("Boom-Bap Kit", mapOf(
|
||||
"eqLoFreq" to "90", "eqLoGain" to "2", "eqHiFreq" to "10000", "eqHiGain" to "1.5",
|
||||
"xLo" to "160", "xHi" to "2500",
|
||||
"cLoThr" to "-20", "cLoRatio" to "3", "cLoAtk" to "12", "cLoRel" to "160", "cLoMk" to "2",
|
||||
"cMidThr" to "-18", "cMidRatio" to "2.5", "cMidMk" to "1",
|
||||
"limCeil" to "-0.5", "outGain" to "1.5",
|
||||
)),
|
||||
Preset("Trap Hats", mapOf(
|
||||
"eqMidFreq" to "4000", "eqMidGain" to "1", "eqHiFreq" to "11000", "eqHiGain" to "5", "xHi" to "4000",
|
||||
"cLoRatio" to "1.5", "cMidRatio" to "2",
|
||||
"cHiThr" to "-26", "cHiRatio" to "4", "cHiAtk" to "2", "cHiRel" to "60", "cHiMk" to "2",
|
||||
"limCeil" to "-0.3", "outGain" to "2",
|
||||
)),
|
||||
Preset("Industrial Drums", mapOf(
|
||||
"eqLoFreq" to "80", "eqLoGain" to "3", "eqMidFreq" to "2200", "eqMidGain" to "3", "eqMidQ" to "1.2",
|
||||
"eqHiFreq" to "8000", "eqHiGain" to "2", "xLo" to "150", "xHi" to "2000",
|
||||
"cLoThr" to "-18", "cLoRatio" to "5", "cLoAtk" to "6", "cLoRel" to "120", "cLoMk" to "3",
|
||||
"cMidThr" to "-16", "cMidRatio" to "4", "cMidAtk" to "8", "cMidRel" to "120", "cMidMk" to "3",
|
||||
"cHiThr" to "-20", "cHiRatio" to "3", "cHiMk" to "2",
|
||||
"limCeil" to "-0.2", "limRel" to "60", "outGain" to "3",
|
||||
)),
|
||||
Preset("Techno Kick", mapOf(
|
||||
"eqLoFreq" to "60", "eqLoGain" to "3", "eqMidFreq" to "700", "eqMidGain" to "-2",
|
||||
"xLo" to "120", "xHi" to "3000",
|
||||
"cLoThr" to "-16", "cLoRatio" to "6", "cLoAtk" to "4", "cLoRel" to "100", "cLoMk" to "3",
|
||||
"cMidRatio" to "2", "cHiRatio" to "1.5",
|
||||
"limCeil" to "-0.2", "outGain" to "2.5",
|
||||
)),
|
||||
Preset("House Groove", mapOf(
|
||||
"strength" to "0.85",
|
||||
"eqLoFreq" to "90", "eqLoGain" to "1.5", "eqHiFreq" to "9000", "eqHiGain" to "1.5",
|
||||
"xLo" to "180", "xHi" to "2600",
|
||||
"cLoThr" to "-22", "cLoRatio" to "2.5", "cLoMk" to "1.5",
|
||||
"cMidThr" to "-20", "cMidRatio" to "2", "cMidMk" to "1",
|
||||
"cHiThr" to "-22", "cHiRatio" to "2", "cHiMk" to "1",
|
||||
"limCeil" to "-0.5", "outGain" to "1.5",
|
||||
)),
|
||||
Preset("DnB Breaks", mapOf(
|
||||
"eqLoFreq" to "80", "eqLoGain" to "2", "eqMidFreq" to "2500", "eqMidGain" to "1.5",
|
||||
"eqHiFreq" to "9000", "eqHiGain" to "2", "xLo" to "150", "xHi" to "2500",
|
||||
"cLoThr" to "-18", "cLoRatio" to "4", "cLoAtk" to "3", "cLoRel" to "90", "cLoMk" to "2",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidAtk" to "4", "cMidRel" to "90", "cMidMk" to "2",
|
||||
"cHiThr" to "-22", "cHiRatio" to "3", "cHiAtk" to "2", "cHiRel" to "60", "cHiMk" to "2",
|
||||
"limCeil" to "-0.3", "limRel" to "50", "outGain" to "2",
|
||||
)),
|
||||
Preset("Lo-Fi Kit", mapOf(
|
||||
"strength" to "0.8",
|
||||
"eqLoFreq" to "100", "eqLoGain" to "2", "eqMidFreq" to "500", "eqMidGain" to "1",
|
||||
"eqHiFreq" to "6000", "eqHiGain" to "-4", "xLo" to "200", "xHi" to "2200",
|
||||
"cLoThr" to "-22", "cLoRatio" to "2.5", "cLoMk" to "1", "cMidRatio" to "2", "cHiRatio" to "1.5",
|
||||
"limCeil" to "-1", "outGain" to "1",
|
||||
)),
|
||||
Preset("Snare Crack", mapOf(
|
||||
"eqLoFreq" to "200", "eqLoGain" to "-2", "eqMidFreq" to "2000", "eqMidGain" to "3", "eqMidQ" to "1.5",
|
||||
"eqHiFreq" to "8000", "eqHiGain" to "3", "xLo" to "250", "xHi" to "1800",
|
||||
"cLoRatio" to "1.5",
|
||||
"cMidThr" to "-18", "cMidRatio" to "4", "cMidAtk" to "3", "cMidRel" to "100", "cMidMk" to "2",
|
||||
"cHiThr" to "-22", "cHiRatio" to "3", "cHiAtk" to "2", "cHiRel" to "70", "cHiMk" to "2",
|
||||
"limCeil" to "-0.3", "outGain" to "2",
|
||||
)),
|
||||
Preset("Clap Snap", mapOf(
|
||||
"eqMidFreq" to "1500", "eqMidGain" to "2", "eqHiFreq" to "9000", "eqHiGain" to "4", "xHi" to "3000",
|
||||
"cLoRatio" to "1.5",
|
||||
"cMidRatio" to "3", "cMidAtk" to "3", "cMidMk" to "1",
|
||||
"cHiRatio" to "3", "cHiAtk" to "2", "cHiMk" to "2", "outGain" to "2",
|
||||
)),
|
||||
Preset("Cymbal Sheen", mapOf(
|
||||
"strength" to "0.75",
|
||||
"eqMidFreq" to "3000", "eqMidGain" to "-1", "eqHiFreq" to "12000", "eqHiGain" to "4", "xHi" to "5000",
|
||||
"cHiThr" to "-26", "cHiRatio" to "2.5", "cHiAtk" to "5", "cHiRel" to "120", "cHiMk" to "1",
|
||||
"limCeil" to "-0.5", "outGain" to "1.5",
|
||||
)),
|
||||
Preset("Percussion Tight", mapOf(
|
||||
"eqMidFreq" to "2500", "eqMidGain" to "1.5", "eqHiFreq" to "8000", "eqHiGain" to "2",
|
||||
"xLo" to "200", "xHi" to "2500",
|
||||
"cLoRatio" to "2", "cLoMk" to "1",
|
||||
"cMidRatio" to "3", "cMidAtk" to "4", "cMidMk" to "1.5",
|
||||
"cHiRatio" to "2.5", "cHiMk" to "1", "outGain" to "1.5",
|
||||
)),
|
||||
|
||||
// ---------------- Bass ----------------
|
||||
Preset("EBM Bass", mapOf(
|
||||
"eqLoFreq" to "80", "eqLoGain" to "3", "eqMidFreq" to "800", "eqMidGain" to "2", "eqMidQ" to "1.2",
|
||||
"eqHiFreq" to "9000", "eqHiGain" to "-2", "xLo" to "150", "xHi" to "1500",
|
||||
"cLoThr" to "-18", "cLoRatio" to "4", "cLoAtk" to "10", "cLoRel" to "130", "cLoMk" to "3",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidAtk" to "12", "cMidRel" to "130", "cMidMk" to "2",
|
||||
"cHiRatio" to "1.5", "limCeil" to "-0.3", "outGain" to "2.5",
|
||||
)),
|
||||
Preset("Reese Bass", mapOf(
|
||||
"eqLoFreq" to "70", "eqLoGain" to "2", "eqMidFreq" to "500", "eqMidGain" to "2",
|
||||
"xLo" to "140", "xHi" to "1200",
|
||||
"cLoThr" to "-16", "cLoRatio" to "5", "cLoAtk" to "8", "cLoRel" to "120", "cLoMk" to "3",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidMk" to "2", "cHiRatio" to "1.5",
|
||||
"limCeil" to "-0.3", "outGain" to "2",
|
||||
)),
|
||||
Preset("Acid 303", mapOf(
|
||||
"eqLoFreq" to "90", "eqLoGain" to "1", "eqMidFreq" to "1200", "eqMidGain" to "3", "eqMidQ" to "1.5",
|
||||
"xLo" to "160", "xHi" to "1800",
|
||||
"cLoRatio" to "3", "cLoMk" to "2",
|
||||
"cMidThr" to "-18", "cMidRatio" to "4", "cMidAtk" to "6", "cMidRel" to "110", "cMidMk" to "2",
|
||||
"cHiRatio" to "2", "limCeil" to "-0.4", "outGain" to "2",
|
||||
)),
|
||||
Preset("Sub Bass Tame", mapOf(
|
||||
"strength" to "0.9",
|
||||
"eqLoFreq" to "55", "eqLoGain" to "2", "eqMidFreq" to "900", "eqMidGain" to "-2",
|
||||
"xLo" to "120", "xHi" to "2000",
|
||||
"cLoThr" to "-14", "cLoRatio" to "6", "cLoAtk" to "12", "cLoRel" to "150", "cLoMk" to "3",
|
||||
"cMidRatio" to "2", "cHiRatio" to "1.2", "limCeil" to "-0.5", "outGain" to "2",
|
||||
)),
|
||||
Preset("FM Bass Punch", mapOf(
|
||||
"eqLoFreq" to "90", "eqLoGain" to "2", "eqMidFreq" to "1000", "eqMidGain" to "1.5",
|
||||
"xLo" to "160", "xHi" to "2200",
|
||||
"cLoThr" to "-18", "cLoRatio" to "4", "cLoAtk" to "6", "cLoRel" to "110", "cLoMk" to "2",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidMk" to "1.5", "cHiRatio" to "2",
|
||||
"limCeil" to "-0.3", "outGain" to "2",
|
||||
)),
|
||||
Preset("Goth Bass", mapOf(
|
||||
"strength" to "0.9",
|
||||
"eqLoFreq" to "75", "eqLoGain" to "3", "eqMidFreq" to "600", "eqMidGain" to "-1",
|
||||
"eqHiFreq" to "8000", "eqHiGain" to "-2", "xLo" to "140", "xHi" to "1600",
|
||||
"cLoThr" to "-18", "cLoRatio" to "4", "cLoAtk" to "10", "cLoRel" to "140", "cLoMk" to "3",
|
||||
"cMidRatio" to "2.5", "cMidMk" to "1", "cHiRatio" to "1.5", "limCeil" to "-0.5", "outGain" to "2",
|
||||
)),
|
||||
Preset("Synthwave Bass", mapOf(
|
||||
"eqLoFreq" to "80", "eqLoGain" to "2.5", "eqMidFreq" to "900", "eqMidGain" to "1",
|
||||
"eqHiFreq" to "7000", "eqHiGain" to "1", "xLo" to "150", "xHi" to "2000",
|
||||
"cLoThr" to "-20", "cLoRatio" to "3", "cLoAtk" to "10", "cLoRel" to "130", "cLoMk" to "2",
|
||||
"cMidRatio" to "2.5", "cMidMk" to "1", "cHiRatio" to "2",
|
||||
"limCeil" to "-0.5", "outGain" to "1.8",
|
||||
)),
|
||||
|
||||
// ---------------- Synths, leads & pads ----------------
|
||||
Preset("Synthwave Lead", mapOf(
|
||||
"strength" to "0.85",
|
||||
"eqLoFreq" to "200", "eqLoGain" to "-1", "eqMidFreq" to "2000", "eqMidGain" to "1.5",
|
||||
"eqHiFreq" to "9000", "eqHiGain" to "4", "xLo" to "300", "xHi" to "3000",
|
||||
"cLoRatio" to "1.5",
|
||||
"cMidThr" to "-20", "cMidRatio" to "2.5", "cMidAtk" to "12", "cMidRel" to "150", "cMidMk" to "1.5",
|
||||
"cHiThr" to "-22", "cHiRatio" to "2", "cHiMk" to "1.5", "limCeil" to "-0.5", "outGain" to "2",
|
||||
)),
|
||||
Preset("Cutting Lead", mapOf(
|
||||
"eqLoFreq" to "200", "eqLoGain" to "-2", "eqMidFreq" to "3000", "eqMidGain" to "3", "eqMidQ" to "1.2",
|
||||
"eqHiFreq" to "8000", "eqHiGain" to "2", "xLo" to "300", "xHi" to "3500",
|
||||
"cLoRatio" to "1.5",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidAtk" to "8", "cMidRel" to "130", "cMidMk" to "2",
|
||||
"cHiRatio" to "2", "cHiMk" to "1", "limCeil" to "-0.4", "outGain" to "2",
|
||||
)),
|
||||
Preset("Darkwave Pad", mapOf(
|
||||
"strength" to "0.7",
|
||||
"eqLoFreq" to "150", "eqLoGain" to "1", "eqMidFreq" to "1000", "eqMidGain" to "0.5",
|
||||
"eqHiFreq" to "8000", "eqHiGain" to "-2", "xLo" to "250", "xHi" to "2500",
|
||||
"cLoRatio" to "2", "cLoMk" to "1",
|
||||
"cMidThr" to "-24", "cMidRatio" to "2", "cMidAtk" to "20", "cMidRel" to "200", "cMidMk" to "1",
|
||||
"cHiRatio" to "2", "limCeil" to "-1", "outGain" to "1",
|
||||
)),
|
||||
Preset("Ambient Wash", mapOf(
|
||||
"strength" to "0.6",
|
||||
"eqLoFreq" to "120", "eqLoGain" to "1", "eqHiFreq" to "10000", "eqHiGain" to "1",
|
||||
"xLo" to "300", "xHi" to "3000",
|
||||
"cMidThr" to "-26", "cMidRatio" to "1.8", "cMidAtk" to "30", "cMidRel" to "250", "cMidMk" to "0.5",
|
||||
"limCeil" to "-1.5", "outGain" to "0.5",
|
||||
)),
|
||||
Preset("Pluck Tight", mapOf(
|
||||
"eqMidFreq" to "2500", "eqMidGain" to "2", "eqHiFreq" to "9000", "eqHiGain" to "3",
|
||||
"xLo" to "300", "xHi" to "3000", "cLoRatio" to "1.5",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidAtk" to "3", "cMidRel" to "100", "cMidMk" to "1.5",
|
||||
"cHiThr" to "-22", "cHiRatio" to "2.5", "cHiAtk" to "2", "cHiRel" to "70", "cHiMk" to "1.5",
|
||||
"outGain" to "1.5",
|
||||
)),
|
||||
Preset("Arp Sparkle", mapOf(
|
||||
"strength" to "0.8",
|
||||
"eqMidFreq" to "3000", "eqMidGain" to "1", "eqHiFreq" to "11000", "eqHiGain" to "4", "xHi" to "4000",
|
||||
"cMidRatio" to "2", "cMidMk" to "1",
|
||||
"cHiThr" to "-24", "cHiRatio" to "2.5", "cHiAtk" to "4", "cHiMk" to "1.5",
|
||||
"limCeil" to "-0.5", "outGain" to "1.5",
|
||||
)),
|
||||
Preset("Chip Lead", mapOf(
|
||||
"eqLoFreq" to "150", "eqLoGain" to "-3", "eqMidFreq" to "2500", "eqMidGain" to "2.5", "eqMidQ" to "1.2",
|
||||
"eqHiFreq" to "8000", "eqHiGain" to "2", "xLo" to "350", "xHi" to "3000",
|
||||
"cLoRatio" to "1.2",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidMk" to "2", "cHiRatio" to "2", "cHiMk" to "1",
|
||||
"limCeil" to "-0.4", "outGain" to "2",
|
||||
)),
|
||||
Preset("Bell Clarity", mapOf(
|
||||
"strength" to "0.75",
|
||||
"eqMidFreq" to "1800", "eqMidGain" to "1.5", "eqHiFreq" to "10000", "eqHiGain" to "3", "xHi" to "4000",
|
||||
"cMidRatio" to "2", "cMidAtk" to "6", "cMidMk" to "1",
|
||||
"cHiRatio" to "2", "cHiAtk" to "4", "cHiMk" to "1", "limCeil" to "-0.5", "outGain" to "1",
|
||||
)),
|
||||
Preset("Keys Clarity", mapOf(
|
||||
"strength" to "0.8",
|
||||
"eqLoFreq" to "200", "eqLoGain" to "-1", "eqMidFreq" to "2500", "eqMidGain" to "1.5",
|
||||
"eqHiFreq" to "9000", "eqHiGain" to "2", "xLo" to "250", "xHi" to "3000",
|
||||
"cLoRatio" to "1.8",
|
||||
"cMidThr" to "-22", "cMidRatio" to "2.5", "cMidAtk" to "10", "cMidRel" to "150", "cMidMk" to "1",
|
||||
"cHiRatio" to "2", "cHiMk" to "1", "limCeil" to "-0.5", "outGain" to "1.5",
|
||||
)),
|
||||
|
||||
// ---------------- Bus / master ----------------
|
||||
Preset("Mix Glue", mapOf(
|
||||
"strength" to "0.7",
|
||||
"eqLoFreq" to "100", "eqLoGain" to "0.5", "eqHiFreq" to "10000", "eqHiGain" to "1",
|
||||
"xLo" to "200", "xHi" to "2500",
|
||||
"cLoThr" to "-22", "cLoRatio" to "2", "cLoAtk" to "20", "cLoRel" to "200", "cLoMk" to "1",
|
||||
"cMidThr" to "-22", "cMidRatio" to "2", "cMidAtk" to "25", "cMidRel" to "200", "cMidMk" to "1",
|
||||
"cHiThr" to "-22", "cHiRatio" to "2", "cHiAtk" to "15", "cHiRel" to "180", "cHiMk" to "1",
|
||||
"limCeil" to "-0.8", "outGain" to "1",
|
||||
)),
|
||||
Preset("Loudness Max", mapOf(
|
||||
"eqLoFreq" to "90", "eqLoGain" to "1", "eqHiFreq" to "9000", "eqHiGain" to "2",
|
||||
"xLo" to "180", "xHi" to "2800",
|
||||
"cLoThr" to "-20", "cLoRatio" to "3", "cLoMk" to "2",
|
||||
"cMidThr" to "-18", "cMidRatio" to "3", "cMidMk" to "2",
|
||||
"cHiThr" to "-20", "cHiRatio" to "3", "cHiMk" to "2",
|
||||
"limCeil" to "-0.1", "limRel" to "40", "outGain" to "4",
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -216,6 +216,16 @@ enum class ToolboxType(
|
||||
ParamSpec("duckattack", "Duck Attack (ms)", 10f, 0.5f, 100f),
|
||||
ParamSpec("duckrelease", "Duck Release (ms)", 200f, 10f, 2000f),
|
||||
),
|
||||
),
|
||||
MIX_TIGHTENER(
|
||||
ToolboxKind.EFFECT, "Mix Tightener",
|
||||
// A preset-driven "channel strip in a box": parametric EQ → 3-band multiband
|
||||
// compressor → peak limiter, all behind ONE visible control — Strength — which
|
||||
// morphs the whole chain from transparent (0) to the full preset (1). Every
|
||||
// per-band value is a hidden slot param set by the genre/instrument presets
|
||||
// (see MixTightenerPresets); only Strength is user-facing. Rendered by
|
||||
// MixTightener in Effects.kt, edited by ui/toolbox/MixTightenerEditor.
|
||||
listOf(ParamSpec("strength", "Strength", 1f, 0f, 1f)),
|
||||
);
|
||||
|
||||
val isInstrument: Boolean get() = kind == ToolboxKind.INSTRUMENT
|
||||
|
||||
@@ -10,6 +10,8 @@ import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
@@ -180,16 +182,49 @@ fun <T> RetroDropdown(
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {
|
||||
options.forEach { opt ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(optionLabel(opt), fontFamily = FontFamily.Monospace) },
|
||||
onClick = { onSelect(opt); open = false },
|
||||
)
|
||||
if (options.size <= DROPDOWN_MAX_ROWS) {
|
||||
// Short lists: the ordinary one-per-row menu.
|
||||
options.forEach { opt ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(optionLabel(opt), fontFamily = FontFamily.Monospace) },
|
||||
onClick = { onSelect(opt); open = false },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Long lists (e.g. 30+ presets): lay out in balanced columns so the menu
|
||||
// grows sideways instead of into a tall vertical scroll. Column count is
|
||||
// the fewest that keeps each column within DROPDOWN_MAX_ROWS.
|
||||
val cols = (options.size + DROPDOWN_MAX_ROWS - 1) / DROPDOWN_MAX_ROWS
|
||||
val perCol = (options.size + cols - 1) / cols
|
||||
Row(Modifier.padding(horizontal = 4.dp)) {
|
||||
for (col in 0 until cols) {
|
||||
Column {
|
||||
for (r in 0 until perCol) {
|
||||
val idx = col * perCol + r
|
||||
if (idx >= options.size) break
|
||||
val opt = options[idx]
|
||||
Text(
|
||||
optionLabel(opt),
|
||||
color = if (opt == selected) c.accent else c.text,
|
||||
fontFamily = FontFamily.Monospace, fontSize = 13.sp,
|
||||
maxLines = 1, softWrap = false, overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clickable { onSelect(opt); open = false }
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Above this many options a [RetroDropdown] menu switches to a multi-column layout
|
||||
* so it never becomes a long vertical scroll. */
|
||||
private const val DROPDOWN_MAX_ROWS = 12
|
||||
|
||||
/** A dim, spaced section header, e.g. "── TRANSPORT ──". */
|
||||
@Composable
|
||||
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package space.rcmd.android.sizzle.ui.toolbox
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import space.rcmd.android.sizzle.model.ToolboxSlot
|
||||
import space.rcmd.android.sizzle.ui.AppViewModel
|
||||
import space.rcmd.android.sizzle.ui.theme.LocalRetro
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Editor for the [space.rcmd.android.sizzle.model.ToolboxType.MIX_TIGHTENER]
|
||||
* effect. The preset bar above (rendered by [ToolboxScreen]) chooses a genre/
|
||||
* instrument voicing; this screen exposes four faders:
|
||||
* - **Strength** — the master amount for the whole EQ → compressor → limiter chain.
|
||||
* - **EQ / Comp / Limit** — per-stage trims. Each stage's effective amount is
|
||||
* master Strength × that stage's fader, so you can (say) keep the tone shaping
|
||||
* but back off the compression, or push the limiter for more loudness.
|
||||
* At the bottom every fader is transparent; at the top it's the full preset.
|
||||
*/
|
||||
@Composable
|
||||
fun MixTightenerEditor(vm: AppViewModel, slot: ToolboxSlot) {
|
||||
val c = LocalRetro.current
|
||||
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so the faders refresh
|
||||
val mono = FontFamily.Monospace
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(
|
||||
"Preset-driven EQ → 3-band compressor → limiter. Pick a preset, then set the " +
|
||||
"master Strength and trim each stage:",
|
||||
color = c.textDim, fontFamily = mono, fontSize = 11.sp,
|
||||
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(268.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Fader(vm, slot, "strength", "STRENGTH", 1f, Modifier.weight(1f))
|
||||
Fader(vm, slot, "eqAmt", "EQ", 1f, Modifier.weight(1f))
|
||||
Fader(vm, slot, "compAmt", "COMP", 1f, Modifier.weight(1f))
|
||||
Fader(vm, slot, "limAmt", "LIMIT", 1f, Modifier.weight(1f))
|
||||
}
|
||||
Text(
|
||||
"Bottom = off · Top = full. Each stage = Strength × its fader.",
|
||||
color = c.textDim, fontFamily = mono, fontSize = 10.sp,
|
||||
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** One labelled vertical fader writing [key] on the slot. */
|
||||
@Composable
|
||||
private fun RowScope.Fader(
|
||||
vm: AppViewModel,
|
||||
slot: ToolboxSlot,
|
||||
key: String,
|
||||
label: String,
|
||||
default: Float,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val c = LocalRetro.current
|
||||
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so this fader refreshes on write
|
||||
val mono = FontFamily.Monospace
|
||||
val value = slot.float(key, default).coerceIn(0f, 1f)
|
||||
Column(
|
||||
modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(label, color = c.accent, fontFamily = mono, fontSize = 11.sp, maxLines = 1)
|
||||
Text("${(value * 100).roundToInt()}%", color = c.text, fontFamily = mono, fontSize = 12.sp)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.border(1.dp, c.grid, RectangleShape)
|
||||
.background(c.background)
|
||||
.pointerInput(slot.index, key) {
|
||||
detectTapGestures { off -> setAmt(vm, slot, key, 1f - off.y / size.height) }
|
||||
}
|
||||
.pointerInput(slot.index, key) {
|
||||
detectDragGestures { change, _ ->
|
||||
change.consume()
|
||||
setAmt(vm, slot, key, 1f - change.position.y / size.height)
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
FaderFill(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FaderFill(value: Float) {
|
||||
val c = LocalRetro.current
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(value.coerceIn(0.001f, 1f))
|
||||
.background(c.accent.copy(alpha = 0.30f)),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth().height(3.dp).background(c.accent)) // thumb
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAmt(vm: AppViewModel, slot: ToolboxSlot, key: String, fraction: Float) {
|
||||
slot.set(key, fraction.coerceIn(0f, 1f))
|
||||
vm.bumpForToolbar()
|
||||
}
|
||||
@@ -303,6 +303,7 @@ private fun ParamEditor(vm: AppViewModel, slot: ToolboxSlot, onClose: () -> Unit
|
||||
// auto-generated parameter list, laid out two-per-row to fit more.
|
||||
when (type) {
|
||||
ToolboxType.NES_SYNTH -> NesSynthEditor(vm, slot)
|
||||
ToolboxType.MIX_TIGHTENER -> MixTightenerEditor(vm, slot)
|
||||
ToolboxType.SAMPLER -> SamplerEditor(vm, slot)
|
||||
ToolboxType.SOUNDFONT -> SoundFontEditor(vm, slot)
|
||||
ToolboxType.DELAY -> DelayEditor(vm, slot)
|
||||
|
||||
Reference in New Issue
Block a user