Skip to content

Commit 3791e59

Browse files
committed
feat: add spectrum extension wet only monitor
1 parent 48f0426 commit 3791e59

10 files changed

Lines changed: 100 additions & 17 deletions

File tree

app/src/main/cpp/libjamesdsp-wrapper/JamesDspWrapper.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ Java_me_timschneeberger_rootlessjamesdsp_interop_JamesDspWrapper_setSpectrumExte
963963
jfloat strengthLinear,
964964
jint referenceFreq,
965965
jfloat wetMix,
966+
jboolean wetOnlyMonitor,
966967
jfloat postGainDb,
967968
jboolean safetyEnabled,
968969
jfloat hpQ,
@@ -1015,6 +1016,7 @@ Java_me_timschneeberger_rootlessjamesdsp_interop_JamesDspWrapper_setSpectrumExte
10151016
safeStrength,
10161017
safeReferenceFreq,
10171018
safeWetMix,
1019+
wetOnlyMonitor ? 1 : 0,
10181020
safePostGainDb,
10191021
safetyEnabled ? 1 : 0,
10201022
safeHpQ,

app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/PreferenceGroupFragment.kt

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.preference.Preference.SummaryProvider
1515
import androidx.preference.PreferenceFragmentCompat
1616
import androidx.preference.ListPreference
1717
import androidx.preference.PreferenceScreen
18+
import androidx.preference.SwitchPreferenceCompat
1819
import androidx.recyclerview.widget.RecyclerView
1920
import me.timschneeberger.rootlessjamesdsp.R
2021
import me.timschneeberger.rootlessjamesdsp.activity.GraphicEqualizerActivity
@@ -248,19 +249,53 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
248249
val unitPref = findPreference<ListPreference>(getString(R.string.key_spectrum_ext_strength_unit))
249250
val strengthPercentPref = findPreference<MaterialSeekbarPreference>(getString(R.string.key_spectrum_ext_strength_percent))
250251
val strengthDbPref = findPreference<MaterialSeekbarPreference>(getString(R.string.key_spectrum_ext_strength_db))
252+
val allowBoostPref = findPreference<SwitchPreferenceCompat>(getString(R.string.key_spectrum_ext_allow_boost))
251253
val harmonicsPref = findPreference<EditTextPreference>(getString(R.string.key_spectrum_ext_harmonics))
252254

253255
bindStrengthUnitPreferences(
254256
unitPref = unitPref,
255257
strengthPercentPref = strengthPercentPref,
256258
strengthDbPref = strengthDbPref,
257-
maxDb = 0.0f,
259+
maxDbProvider = {
260+
if (allowBoostPref?.isChecked == true) {
261+
SPECTRUM_STRENGTH_BOOST_DB_MAX
262+
} else {
263+
SPECTRUM_STRENGTH_DB_DEFAULT_MAX
264+
}
265+
},
258266
minPercent = 0.0f,
259-
maxPercent = 100.0f,
267+
maxPercentProvider = {
268+
if (allowBoostPref?.isChecked == true) {
269+
strengthPercentFromDb(SPECTRUM_STRENGTH_BOOST_DB_MAX)
270+
} else {
271+
100.0f
272+
}
273+
},
260274
minLinear = 0.0f,
261275
mapMinDbToZero = true
262276
)
263277

278+
fun applyBoostUi(isEnabled: Boolean) {
279+
val maxDb = if (isEnabled) SPECTRUM_STRENGTH_BOOST_DB_MAX else SPECTRUM_STRENGTH_DB_DEFAULT_MAX
280+
val maxPercent = if (isEnabled) strengthPercentFromDb(SPECTRUM_STRENGTH_BOOST_DB_MAX) else 100.0f
281+
282+
strengthDbPref?.setMax(maxDb)
283+
strengthPercentPref?.setMax(maxPercent)
284+
285+
if ((strengthDbPref?.getValue() ?: maxDb) > maxDb) {
286+
strengthDbPref?.setValue(maxDb)
287+
}
288+
if ((strengthPercentPref?.getValue() ?: maxPercent) > maxPercent) {
289+
strengthPercentPref?.setValue(maxPercent)
290+
}
291+
}
292+
293+
applyBoostUi(allowBoostPref?.isChecked == true)
294+
allowBoostPref?.setOnPreferenceChangeListener { _, newValue ->
295+
applyBoostUi(newValue as Boolean)
296+
true
297+
}
298+
264299
harmonicsPref?.setOnPreferenceChangeListener { _, newValue ->
265300
val harmonicsRaw = (newValue as? String)?.trim().orEmpty()
266301
if (isValidSemicolonDelimitedHarmonics(harmonicsRaw)) {
@@ -279,9 +314,9 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
279314
unitPref = unitPref,
280315
strengthPercentPref = strengthPercentPref,
281316
strengthDbPref = strengthDbPref,
282-
maxDb = CLARITY_STRENGTH_DB_MAX,
317+
maxDbProvider = { CLARITY_STRENGTH_DB_MAX },
283318
minPercent = 0.0f,
284-
maxPercent = 800.0f,
319+
maxPercentProvider = { 800.0f },
285320
minLinear = 0.0f,
286321
mapMinDbToZero = true
287322
)
@@ -322,39 +357,40 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
322357
unitPref: ListPreference?,
323358
strengthPercentPref: MaterialSeekbarPreference?,
324359
strengthDbPref: MaterialSeekbarPreference?,
325-
maxDb: Float,
360+
maxDbProvider: () -> Float,
326361
minPercent: Float,
327-
maxPercent: Float,
362+
maxPercentProvider: () -> Float,
328363
minLinear: Float = 0.01f,
329364
mapMinDbToZero: Boolean = false,
330365
) {
331-
val maxLinear = dbToLinear(maxDb)
332366
val minDb = MIN_STRENGTH_DB
333367
val percentUnit = requireContext().getString(R.string.strength_unit_value_percent)
334368
val dbUnit = requireContext().getString(R.string.strength_unit_value_db)
335369
var internalUpdate = false
336370

337371
fun percentToLinear(percent: Float): Float {
372+
val maxLinear = dbToLinear(maxDbProvider())
338373
return clampLinear(percent / 100.0f, minLinear, maxLinear)
339374
}
340375

341376
fun dbToLinearMapped(db: Float): Float {
342377
if (mapMinDbToZero && db <= minDb) {
343378
return 0.0f
344379
}
380+
val maxLinear = dbToLinear(maxDbProvider())
345381
return clampLinear(dbToLinear(db), minLinear, maxLinear)
346382
}
347383

348384
fun linearToDbMapped(linear: Float): Float {
349385
if (mapMinDbToZero && linear <= 0.0f) {
350386
return minDb
351387
}
352-
return linearToDb(linear).coerceIn(minDb, maxDb)
388+
return linearToDb(linear).coerceIn(minDb, maxDbProvider())
353389
}
354390

355391
strengthPercentPref?.setOnPreferenceChangeListener { _, newValue ->
356392
if (internalUpdate) return@setOnPreferenceChangeListener true
357-
val percent = (newValue as Float).coerceIn(minPercent, maxPercent)
393+
val percent = (newValue as Float).coerceIn(minPercent, maxPercentProvider())
358394
val linear = percentToLinear(percent)
359395
val db = linearToDbMapped(linear)
360396
internalUpdate = true
@@ -365,9 +401,9 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
365401

366402
strengthDbPref?.setOnPreferenceChangeListener { _, newValue ->
367403
if (internalUpdate) return@setOnPreferenceChangeListener true
368-
val db = (newValue as Float).coerceIn(minDb, maxDb)
404+
val db = (newValue as Float).coerceIn(minDb, maxDbProvider())
369405
val linear = dbToLinearMapped(db)
370-
val percent = (linear * 100.0f).coerceIn(minPercent, maxPercent)
406+
val percent = (linear * 100.0f).coerceIn(minPercent, maxPercentProvider())
371407
internalUpdate = true
372408
strengthPercentPref?.setValue(percent)
373409
internalUpdate = false
@@ -384,7 +420,7 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
384420
} else if (newUnit == percentUnit) {
385421
val db = strengthDbPref?.getValue() ?: minDb
386422
val linear = dbToLinearMapped(db)
387-
strengthPercentPref?.setValue((linear * 100.0f).coerceIn(minPercent, maxPercent))
423+
strengthPercentPref?.setValue((linear * 100.0f).coerceIn(minPercent, maxPercentProvider()))
388424
}
389425
internalUpdate = false
390426
setStrengthVisibility(newUnit, strengthPercentPref, strengthDbPref)
@@ -413,6 +449,8 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
413449

414450
private fun dbToLinear(db: Float): Float = 10.0.pow((db / 20.0f).toDouble()).toFloat()
415451

452+
private fun strengthPercentFromDb(db: Float): Float = dbToLinear(db) * 100.0f
453+
416454
/**
417455
* Domain-specific clamping for linear gain values used by [linearToDb] and [dbToLinear].
418456
* Inputs and bounds are linear-domain amplitudes and must stay positive for log conversion.
@@ -460,6 +498,8 @@ class PreferenceGroupFragment : PreferenceFragmentCompat(), KoinComponent {
460498
private const val CROSSFEED_MODE_DEFAULT_VALUE = "5"
461499
private const val CUSTOM_CROSSFEED_MODE_VALUE = "99"
462500
private const val MIN_STRENGTH_DB = -40.0f
501+
private const val SPECTRUM_STRENGTH_DB_DEFAULT_MAX = 0.0f
502+
private const val SPECTRUM_STRENGTH_BOOST_DB_MAX = 12.0f
463503
private const val CLARITY_STRENGTH_LINEAR_MAX = 8.0f
464504
private val CLARITY_STRENGTH_DB_MAX = (20.0 * log10(CLARITY_STRENGTH_LINEAR_MAX.toDouble())).toFloat()
465505
// Semicolon-separated decimal numbers used by Spectrum Extension harmonics list.

app/src/main/java/me/timschneeberger/rootlessjamesdsp/interop/JamesDspBaseEngine.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
3232
private const val SPECTRUM_STRENGTH_UNIT_PERCENT = "percent"
3333
private const val SPECTRUM_STRENGTH_UNIT_DB = "db"
3434
private const val SPECTRUM_STRENGTH_DB_MIN = -40.0f
35-
private const val SPECTRUM_STRENGTH_DB_MAX = 0.0f
35+
private const val SPECTRUM_STRENGTH_DB_DEFAULT_MAX = 0.0f
36+
private const val SPECTRUM_STRENGTH_DB_BOOST_MAX = 12.0f
3637
private const val SPECTRUM_STRENGTH_PERCENT_MIN = 0.0f
3738
private const val SPECTRUM_STRENGTH_PERCENT_MAX = 100.0f
3839
private const val SPECTRUM_HARMONICS_DEFAULT_RAW = "0.02;0;0.02;0;0.02;0;0.02;0;0.02;0"
3940
private const val MAX_EQ_INTERPOLATION_MODE = 1
4041
private val DEFAULT_SPECTRUM_HARMONICS = doubleArrayOf(0.02, 0.0, 0.02, 0.0, 0.02, 0.0, 0.02, 0.0, 0.02, 0.0)
42+
private val SPECTRUM_STRENGTH_PERCENT_BOOST_MAX = 10.0.pow((SPECTRUM_STRENGTH_DB_BOOST_MAX / 20.0f).toDouble()).toFloat() * SPECTRUM_STRENGTH_PERCENT_MAX
4143
}
4244

4345
abstract var enabled: Boolean
@@ -128,8 +130,10 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
128130
val spectrumStrengthUnit = cache.get(R.string.key_spectrum_ext_strength_unit, SPECTRUM_STRENGTH_UNIT_PERCENT)
129131
val spectrumStrengthPercent = cache.get(R.string.key_spectrum_ext_strength_percent, 10f)
130132
val spectrumStrengthDb = cache.get(R.string.key_spectrum_ext_strength_db, -20f)
133+
val spectrumAllowBoost = cache.get(R.string.key_spectrum_ext_allow_boost, false)
131134
val spectrumRefFreq = cache.get(R.string.key_spectrum_ext_ref_freq, 7600f).toInt()
132135
val spectrumWetMix = cache.get(R.string.key_spectrum_ext_wet_mix, 100f)
136+
val spectrumWetOnlyMonitor = cache.get(R.string.key_spectrum_ext_wet_only_monitor, false)
133137
val spectrumPostGain = cache.get(R.string.key_spectrum_ext_post_gain, 0f)
134138
val spectrumSafety = cache.get(R.string.key_spectrum_ext_safety, false)
135139
val spectrumHpQ = cache.get(R.string.key_spectrum_ext_hp_q, 0.717f)
@@ -246,8 +250,10 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
246250
spectrumStrengthUnit,
247251
spectrumStrengthPercent,
248252
spectrumStrengthDb,
253+
spectrumAllowBoost,
249254
spectrumRefFreq,
250255
spectrumWetMix,
256+
spectrumWetOnlyMonitor,
251257
spectrumPostGain,
252258
spectrumSafety,
253259
spectrumHpQ,
@@ -491,17 +497,21 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
491497
strengthUnit: String,
492498
strengthPercent: Float,
493499
strengthDb: Float,
500+
allowBoost: Boolean,
494501
referenceFreq: Int,
495502
wetMixPercent: Float,
503+
wetOnlyMonitor: Boolean,
496504
postGainDb: Float,
497505
safetyEnabled: Boolean,
498506
hpQ: Float,
499507
lpQ: Float,
500508
lpCutoffOffsetHz: Int,
501509
harmonicsRaw: String,
502510
): Boolean {
511+
val maxDb = if (allowBoost) SPECTRUM_STRENGTH_DB_BOOST_MAX else SPECTRUM_STRENGTH_DB_DEFAULT_MAX
512+
val maxPercent = if (allowBoost) SPECTRUM_STRENGTH_PERCENT_BOOST_MAX else SPECTRUM_STRENGTH_PERCENT_MAX
503513
val uiStrength = if (strengthUnit == SPECTRUM_STRENGTH_UNIT_DB) {
504-
val clampedDb = strengthDb.coerceIn(SPECTRUM_STRENGTH_DB_MIN, SPECTRUM_STRENGTH_DB_MAX)
514+
val clampedDb = strengthDb.coerceIn(SPECTRUM_STRENGTH_DB_MIN, maxDb)
505515
// Map the minimum db value to full-off for stable round-tripping with percent mode.
506516
if (clampedDb <= SPECTRUM_STRENGTH_DB_MIN) {
507517
0.0f
@@ -510,7 +520,7 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
510520
}
511521
} else {
512522
strengthPercent
513-
}.coerceIn(SPECTRUM_STRENGTH_PERCENT_MIN, SPECTRUM_STRENGTH_PERCENT_MAX)
523+
}.coerceIn(SPECTRUM_STRENGTH_PERCENT_MIN, maxPercent)
514524

515525
// Stock ViPER mapping: param65550 = trunc(strength * 5.6), core uses /100.
516526
// The local JNI path applies Spectrum params as a single native call instead of discrete
@@ -524,6 +534,7 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
524534
exciter,
525535
referenceFreq,
526536
wetMixPercent.coerceIn(0f, 100f) / 100.0f,
537+
wetOnlyMonitor,
527538
postGainDb.coerceIn(-24f, 24f),
528539
safetyEnabled,
529540
hpQ.coerceIn(0.1f, 3.0f),
@@ -791,6 +802,7 @@ abstract class JamesDspBaseEngine(val context: Context, val callbacks: JamesDspW
791802
strengthLinear: Float,
792803
referenceFreq: Int,
793804
wetMix: Float,
805+
wetOnlyMonitor: Boolean,
794806
postGainDb: Float,
795807
safetyEnabled: Boolean,
796808
hpQ: Float,

app/src/main/java/me/timschneeberger/rootlessjamesdsp/interop/JamesDspLocalEngine.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class JamesDspLocalEngine(context: Context, callbacks: JamesDspWrapper.JamesDspC
221221
strengthLinear: Float,
222222
referenceFreq: Int,
223223
wetMix: Float,
224+
wetOnlyMonitor: Boolean,
224225
postGainDb: Float,
225226
safetyEnabled: Boolean,
226227
hpQ: Float,
@@ -236,6 +237,7 @@ class JamesDspLocalEngine(context: Context, callbacks: JamesDspWrapper.JamesDspC
236237
strengthLinear,
237238
referenceFreq,
238239
wetMix,
240+
wetOnlyMonitor,
239241
postGainDb,
240242
safetyEnabled,
241243
hpQ,

app/src/main/java/me/timschneeberger/rootlessjamesdsp/interop/JamesDspRemoteEngine.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ class JamesDspRemoteEngine(
478478
strengthLinear: Float,
479479
referenceFreq: Int,
480480
wetMix: Float,
481+
wetOnlyMonitor: Boolean,
481482
postGainDb: Float,
482483
safetyEnabled: Boolean,
483484
hpQ: Float,
@@ -503,6 +504,9 @@ class JamesDspRemoteEngine(
503504

504505
if (enable) {
505506
val fallbackToLegacy = {
507+
if (wetOnlyMonitor) {
508+
Timber.w("Spectrum wet-only monitor is unavailable in legacy protocol mode.")
509+
}
506510
val referenceResult = effect.setParameter(PARAM_SPECTRUM_EXTENSION_BARK, referenceFreq)
507511
if (referenceResult != AudioEffect.SUCCESS) {
508512
markUnsupported(referenceResult, true)
@@ -533,7 +537,7 @@ class JamesDspRemoteEngine(
533537
hpQ,
534538
lpQ,
535539
lpCutoffOffsetHz.toFloat()
536-
) + safeHarmonics.map { it.toFloat() }
540+
) + safeHarmonics.map { it.toFloat() } + floatArrayOf(if (wetOnlyMonitor) 1.0f else 0.0f)
537541
val payloadResult = effect.setParameterFloatArray(PARAM_SPECTRUM_EXTENSION, payload)
538542

539543
if (payloadResult == AudioEffect.SUCCESS) {

app/src/main/java/me/timschneeberger/rootlessjamesdsp/interop/JamesDspWrapper.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ object JamesDspWrapper {
6767
strengthLinear: Float,
6868
referenceFreq: Int,
6969
wetMix: Float,
70+
wetOnlyMonitor: Boolean,
7071
postGainDb: Float,
7172
safetyEnabled: Boolean,
7273
hpQ: Float,

app/src/main/res/values/keys.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@
110110
<string name="key_spectrum_ext_strength_unit" translatable="false">spectrum_ext_strength_unit</string>
111111
<string name="key_spectrum_ext_strength_percent" translatable="false">spectrum_ext_strength_percent</string>
112112
<string name="key_spectrum_ext_strength_db" translatable="false">spectrum_ext_strength_db</string>
113+
<string name="key_spectrum_ext_allow_boost" translatable="false">spectrum_ext_allow_boost</string>
113114
<string name="key_spectrum_ext_ref_freq" translatable="false">spectrum_ext_ref_freq</string>
114115
<string name="key_spectrum_ext_wet_mix" translatable="false">spectrum_ext_wet_mix</string>
116+
<string name="key_spectrum_ext_wet_only_monitor" translatable="false">spectrum_ext_wet_only_monitor</string>
115117
<string name="key_spectrum_ext_post_gain" translatable="false">spectrum_ext_post_gain</string>
116118
<string name="key_spectrum_ext_safety" translatable="false">spectrum_ext_safety</string>
117119
<string name="key_spectrum_ext_hp_q" translatable="false">spectrum_ext_hp_q</string>

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,14 @@
147147
<string name="spectrum_ext_enable">Spectrum extension</string>
148148
<string name="spectrum_ext_strength">Strength</string>
149149
<string name="spectrum_ext_strength_unit">Strength unit</string>
150+
<string name="spectrum_ext_allow_boost">Allow strength above 0 dB</string>
151+
<string name="spectrum_ext_allow_boost_summary">Experimental. Expands the strength range above stock limits.</string>
150152
<string name="strength_unit_percent">Percent</string>
151153
<string name="strength_unit_db">dB</string>
152154
<string name="spectrum_ext_ref_freq">Reference frequency</string>
153155
<string name="spectrum_ext_wet_mix">Wet mix</string>
156+
<string name="spectrum_ext_wet_only_monitor">Monitor added signal only</string>
157+
<string name="spectrum_ext_wet_only_monitor_summary">Mutes the original signal and outputs only Spectrum Extension content.</string>
154158
<string name="spectrum_ext_post_gain">Post gain</string>
155159
<string name="spectrum_ext_safety">Safety limiter</string>
156160
<string name="spectrum_ext_safety_summary">Disabled by default. Enable to reduce overload and clipping.</string>

app/src/main/res/xml/dsp_spectrum_ext_preferences.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242
app:showSeekBarValue="true"
4343
app:iconSpaceReserved="false" />
4444

45+
<SwitchPreferenceCompat
46+
android:key="@string/key_spectrum_ext_allow_boost"
47+
android:title="@string/spectrum_ext_allow_boost"
48+
android:summary="@string/spectrum_ext_allow_boost_summary"
49+
android:defaultValue="false"
50+
app:allowDividerAbove="false"
51+
app:iconSpaceReserved="false" />
52+
4553
<me.timschneeberger.rootlessjamesdsp.preference.MaterialSeekbarPreference
4654
android:key="@string/key_spectrum_ext_ref_freq"
4755
android:title="@string/spectrum_ext_ref_freq"
@@ -66,6 +74,14 @@
6674
app:showSeekBarValue="true"
6775
app:iconSpaceReserved="false" />
6876

77+
<SwitchPreferenceCompat
78+
android:key="@string/key_spectrum_ext_wet_only_monitor"
79+
android:title="@string/spectrum_ext_wet_only_monitor"
80+
android:summary="@string/spectrum_ext_wet_only_monitor_summary"
81+
android:defaultValue="false"
82+
app:allowDividerAbove="false"
83+
app:iconSpaceReserved="false" />
84+
6985
<me.timschneeberger.rootlessjamesdsp.preference.MaterialSeekbarPreference
7086
android:key="@string/key_spectrum_ext_post_gain"
7187
android:title="@string/spectrum_ext_post_gain"

0 commit comments

Comments
 (0)