Add Mix Tightener effect, multi-column preset menus, louder master (v0.7.0)
Mix Tightener — a preset-driven "channel strip in a box" (Neutron-style): 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 the presets set; the editor shows only a vertical Strength fader. * DSP (Effects.kt): reusable RBJ biquad; LR-style 3-way crossover; per-band peak-follower compressors; fast peak limiter + output make-up. Performance: the expensive per-band gain computation (log/pow) runs at control rate while the envelope follower runs per sample; Strength≈0 bypasses the whole effect, and flat EQ bands / ratio≈1 comp bands are skipped. Output is clamped and NaN-guarded so a channel FX can never poison the master. * 30 factory presets (MixTightenerPresets) across drums (808 Crisp, Industrial Drums, Trap Hats, Techno Kick, DnB Breaks, …), bass (EBM Bass, Reese, Acid 303, Sub Bass Tame, Goth Bass, Synthwave Bass, …), synths/leads/pads (Synthwave Lead, Cutting Lead, Darkwave Pad, Arp Sparkle, …) and bus glue (Mix Glue, Loudness Max), seeded via PresetLibrary. Global: RetroDropdown now lays long option lists (>12) into balanced columns so preset menus grow sideways instead of into a tall vertical scroll — applies to every dropdown in the app. Master loudness: raise MASTER_GAIN 0.35 → 0.5 (~+3 dB) so playback sits closer to other media apps; the soft-knee limiter still catches peaks. Builds clean (staging + release). On-device performance profiling and install are pending — the device dropped off wireless adb mid-session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,8 @@ android {
|
|||||||
// (android.media.midi) and AAudio low-latency audio we rely on.
|
// (android.media.midi) and AAudio low-latency audio we rely on.
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 18
|
versionCode = 19
|
||||||
versionName = "0.6.0"
|
versionName = "0.7.0"
|
||||||
|
|
||||||
// We provide our own instrumentation runner if/when tests are added.
|
// We provide our own instrumentation runner if/when tests are added.
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|||||||
@@ -1225,7 +1225,10 @@ class AudioEngine(
|
|||||||
private const val LIVE_POLYPHONY = 8
|
private const val LIVE_POLYPHONY = 8
|
||||||
private const val AUDITION_POLYPHONY = 8
|
private const val AUDITION_POLYPHONY = 8
|
||||||
private const val BUS_POLYPHONY = 8 // per-mixer-channel live audition
|
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 LIVE_GAIN = 0.5f
|
||||||
private const val SOFT_KNEE = 0.6f // limiter is transparent below this, saturates above
|
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.abs
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.exp
|
import kotlin.math.exp
|
||||||
|
import kotlin.math.log10
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
import kotlin.math.tanh
|
import kotlin.math.tanh
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +41,7 @@ interface AudioEffect {
|
|||||||
ToolboxType.FILTER -> BiquadFilterEffect(sampleRate)
|
ToolboxType.FILTER -> BiquadFilterEffect(sampleRate)
|
||||||
ToolboxType.BITCRUSHER -> Bitcrusher()
|
ToolboxType.BITCRUSHER -> Bitcrusher()
|
||||||
ToolboxType.EQ10 -> GraphicEq(sampleRate)
|
ToolboxType.EQ10 -> GraphicEq(sampleRate)
|
||||||
|
ToolboxType.MIX_TIGHTENER -> MixTightener(sampleRate)
|
||||||
else -> null // arpeggiator / transposer / LFO are handled by the sequencer
|
else -> null // arpeggiator / transposer / LFO are handled by the sequencer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,3 +325,189 @@ 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 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)
|
||||||
|
|
||||||
|
// EQ — gains scaled by strength so 0 is flat.
|
||||||
|
val loG = slot.float("eqLoGain", 0f).coerceIn(-18f, 18f) * strength
|
||||||
|
val midG = slot.float("eqMidGain", 0f).coerceIn(-18f, 18f) * strength
|
||||||
|
val hiG = slot.float("eqHiGain", 0f).coerceIn(-18f, 18f) * strength
|
||||||
|
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 = ratio.any { it > 1.01f } || makeDb.any { abs(it) > 0.1f }
|
||||||
|
|
||||||
|
// Limiter: at strength 0 the ceiling is 1.0 (no limiting), at 1 it's the preset.
|
||||||
|
limCeil = 1f + (dbToLin(slot.float("limCeil", -0.5f).coerceIn(-24f, 0f)) - 1f) * strength
|
||||||
|
limRelCoef = coefFromMs(slot.float("limRel", 100f))
|
||||||
|
outGain = 1f + (dbToLin(slot.float("outGain", 0f).coerceIn(-12f, 18f)) - 1f) * strength
|
||||||
|
}
|
||||||
|
|
||||||
|
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]) * strength).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.audio.SampleStore
|
||||||
import space.rcmd.android.sizzle.model.AmbiencePresets
|
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.NesSynthPresets
|
||||||
import space.rcmd.android.sizzle.model.Project
|
import space.rcmd.android.sizzle.model.Project
|
||||||
import space.rcmd.android.sizzle.model.SamplerPads
|
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 ->
|
seedType(ToolboxType.NES_SYNTH, NesSynthPresets.FACTORY.map { it.name }) { slot, name ->
|
||||||
NesSynthPresets.apply(slot, NesSynthPresets.FACTORY.first { it.name == 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("duckattack", "Duck Attack (ms)", 10f, 0.5f, 100f),
|
||||||
ParamSpec("duckrelease", "Duck Release (ms)", 200f, 10f, 2000f),
|
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
|
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.awaitFirstDown
|
||||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||||
import androidx.compose.foundation.layout.Box
|
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.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
@@ -180,16 +182,49 @@ fun <T> RetroDropdown(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {
|
DropdownMenu(expanded = open, onDismissRequest = { open = false }) {
|
||||||
options.forEach { opt ->
|
if (options.size <= DROPDOWN_MAX_ROWS) {
|
||||||
DropdownMenuItem(
|
// Short lists: the ordinary one-per-row menu.
|
||||||
text = { Text(optionLabel(opt), fontFamily = FontFamily.Monospace) },
|
options.forEach { opt ->
|
||||||
onClick = { onSelect(opt); open = false },
|
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 ──". */
|
/** A dim, spaced section header, e.g. "── TRANSPORT ──". */
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
|
fun SectionLabel(text: String, modifier: Modifier = Modifier) {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// 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.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
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. Deliberately minimal: the preset bar above (rendered by the parent
|
||||||
|
* [ToolboxScreen]) chooses a genre/instrument voicing, and this screen exposes the
|
||||||
|
* single **Strength** fader that morphs the whole EQ → multiband-compressor →
|
||||||
|
* limiter chain from transparent (bottom) to the full preset (top). All the
|
||||||
|
* per-band tuning lives in the preset — there's intentionally nothing else to
|
||||||
|
* fiddle with here.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MixTightenerEditor(vm: AppViewModel, slot: ToolboxSlot) {
|
||||||
|
val c = LocalRetro.current
|
||||||
|
@Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so the fader refreshes
|
||||||
|
val mono = FontFamily.Monospace
|
||||||
|
val strength = slot.float("strength", 1f).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
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 for the " +
|
||||||
|
"instrument on this channel, then set how hard it works:",
|
||||||
|
color = c.textDim, fontFamily = mono, fontSize = 11.sp,
|
||||||
|
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Text("STRENGTH", color = c.accent, fontFamily = mono, fontSize = 13.sp)
|
||||||
|
Text("${(strength * 100).roundToInt()}%", color = c.text, fontFamily = mono, fontSize = 22.sp)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.height(260.dp)
|
||||||
|
.width(96.dp)
|
||||||
|
.border(1.dp, c.grid, RectangleShape)
|
||||||
|
.background(c.background)
|
||||||
|
.pointerInput(slot.index) {
|
||||||
|
detectTapGestures { off -> setStrength(vm, slot, 1f - off.y / size.height) }
|
||||||
|
}
|
||||||
|
.pointerInput(slot.index) {
|
||||||
|
detectDragGestures { change, _ ->
|
||||||
|
change.consume()
|
||||||
|
setStrength(vm, slot, 1f - change.position.y / size.height)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(strength.coerceIn(0.001f, 1f))
|
||||||
|
.background(c.accent.copy(alpha = 0.30f)),
|
||||||
|
contentAlignment = Alignment.TopCenter,
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxWidth().height(3.dp).background(c.accent)) // thumb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"Bottom = transparent · Top = full preset",
|
||||||
|
color = c.textDim, fontFamily = mono, fontSize = 10.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStrength(vm: AppViewModel, slot: ToolboxSlot, fraction: Float) {
|
||||||
|
slot.set("strength", 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.
|
// auto-generated parameter list, laid out two-per-row to fit more.
|
||||||
when (type) {
|
when (type) {
|
||||||
ToolboxType.NES_SYNTH -> NesSynthEditor(vm, slot)
|
ToolboxType.NES_SYNTH -> NesSynthEditor(vm, slot)
|
||||||
|
ToolboxType.MIX_TIGHTENER -> MixTightenerEditor(vm, slot)
|
||||||
ToolboxType.SAMPLER -> SamplerEditor(vm, slot)
|
ToolboxType.SAMPLER -> SamplerEditor(vm, slot)
|
||||||
ToolboxType.SOUNDFONT -> SoundFontEditor(vm, slot)
|
ToolboxType.SOUNDFONT -> SoundFontEditor(vm, slot)
|
||||||
ToolboxType.DELAY -> DelayEditor(vm, slot)
|
ToolboxType.DELAY -> DelayEditor(vm, slot)
|
||||||
|
|||||||
Reference in New Issue
Block a user