Skip to content
9 changes: 8 additions & 1 deletion ControlPad/EventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
{
if (IsPressed && !IsPressedOld)
{
AudioController.MuteProcess(buttonAction.ActionProperty, !AudioController.IsProcessMute(buttonAction.ActionProperty));

Check warning on line 105 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'processName' in 'void AudioController.MuteProcess(string processName, bool mute)'.

Check warning on line 105 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'processName' in 'bool AudioController.IsProcessMute(string processName)'.
}
break;
}
Expand All @@ -118,7 +118,7 @@
{
if (IsPressed && !IsPressedOld)
{
AudioController.MuteMic(buttonAction.ActionProperty, !AudioController.IsMicMute(buttonAction.ActionProperty));

Check warning on line 121 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'micName' in 'void AudioController.MuteMic(string micName, bool mute)'.

Check warning on line 121 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'micName' in 'bool AudioController.IsMicMute(string micName)'.
}
break;
}
Expand All @@ -126,7 +126,7 @@
{
if (IsPressed && !IsPressedOld)
{
string path = buttonAction.ActionProperty;

Check warning on line 129 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
try
{
Process.Start(new ProcessStartInfo
Expand Down Expand Up @@ -183,7 +183,14 @@

private float SliderToFloat(int value, int mode = 0)
{
return SliderValueConverter.SliderToFloat(value, Settings.TranslationExponent);
value -= 1;
float normalized = Math.Clamp((float)value / 1022.0f, 0f, 1f);

// Clamp to zero when slider is at or near the bottom to ensure complete silence
if (normalized < 0.005f)
return 0f;

return SliderTranslationCurve.Apply(normalized);
}
}
}
72 changes: 71 additions & 1 deletion ControlPad/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public static class Settings
private static bool _startMinimized = false;
private static bool _minimizeToSystemTray = true;
private static double _translationExponent = 1d;
private static string _translationCurvePreset = "linear";
private static double _translationCurveX1 = 0d;
private static double _translationCurveY1 = 0d;
private static double _translationCurveX2 = 1d;
private static double _translationCurveY2 = 1d;
private static int _selectedThemeIndex = 0;
private static int _selectedBackgroundIndex = 3;
private static int _sliderDeadZone = 4;
Expand Down Expand Up @@ -115,13 +120,68 @@ public static double TranslationExponent
}
}

public static string TranslationCurvePreset
{
get => _translationCurvePreset;
set
{
_translationCurvePreset = SliderTranslationCurve.IsSupportedPreset(value) ? value : "linear";
Save();
}
}

public static double TranslationCurveX1
{
get => _translationCurveX1;
set
{
_translationCurveX1 = Math.Clamp(value, 0d, 1d);
Save();
}
}

public static double TranslationCurveY1
{
get => _translationCurveY1;
set
{
_translationCurveY1 = Math.Clamp(value, 0d, 1d);
Save();
}
}

public static double TranslationCurveX2
{
get => _translationCurveX2;
set
{
_translationCurveX2 = Math.Clamp(value, 0d, 1d);
Save();
}
}

public static double TranslationCurveY2
{
get => _translationCurveY2;
set
{
_translationCurveY2 = Math.Clamp(value, 0d, 1d);
Save();
}
}

private class Data
{
public bool TrayIconMessageShown { get; set; } = false;
public bool StartWithWindows { get; set; } = false;
public bool StartMinimized { get; set; } = true;
public bool MinimizeToSystemTray { get; set; } = true;
public double TranslationExponent { get; set; } = 1d;
public string TranslationCurvePreset { get; set; } = "linear";
public double TranslationCurveX1 { get; set; } = 0d;
public double TranslationCurveY1 { get; set; } = 0d;
public double TranslationCurveX2 { get; set; } = 1d;
public double TranslationCurveY2 { get; set; } = 1d;
public int SelectedThemeIndex { get; set; } = 0;
public int SelectedBackgroundIndex { get; set; } = 3;
public int SliderDeadZone { get; set; } = 4;
Expand All @@ -148,6 +208,11 @@ public static void Load()
_selectedBackgroundIndex = data.SelectedBackgroundIndex;
_sliderDeadZone = data.SliderDeadZone;
_translationExponent = data.TranslationExponent;
_translationCurvePreset = SliderTranslationCurve.IsSupportedPreset(data.TranslationCurvePreset) ? data.TranslationCurvePreset : "linear";
_translationCurveX1 = Math.Clamp(data.TranslationCurveX1, 0d, 1d);
_translationCurveY1 = Math.Clamp(data.TranslationCurveY1, 0d, 1d);
_translationCurveX2 = Math.Clamp(data.TranslationCurveX2, 0d, 1d);
_translationCurveY2 = Math.Clamp(data.TranslationCurveY2, 0d, 1d);
_unmuteOnSliderChange = data.UnmuteOnSliderChange;
}
catch
Expand All @@ -170,6 +235,11 @@ private static void Save()
SelectedBackgroundIndex = _selectedBackgroundIndex,
SliderDeadZone = _sliderDeadZone,
TranslationExponent = _translationExponent,
TranslationCurvePreset = _translationCurvePreset,
TranslationCurveX1 = _translationCurveX1,
TranslationCurveY1 = _translationCurveY1,
TranslationCurveX2 = _translationCurveX2,
TranslationCurveY2 = _translationCurveY2,
UnmuteOnSliderChange = _unmuteOnSliderChange,
};

Expand All @@ -183,4 +253,4 @@ private static void Save()
}
}
}
}
}
104 changes: 104 additions & 0 deletions ControlPad/SliderTranslationCurve.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace ControlPad
{
public static class SliderTranslationCurve
{
private const double Epsilon = 1e-6;
private const int MaxBisectionIterations = 20;
private static string? _lastPreset;
private static double _lastX1;
private static double _lastY1;
private static double _lastX2;
private static double _lastY2;
private static (double x1, double y1, double x2, double y2) _cachedControlPoints = (0d, 0d, 1d, 1d);

public static bool IsSupportedPreset(string? preset)
{
return preset is "ease" or "linear" or "ease-in" or "ease-out" or "ease-in-out" or "custom";
}

public static (double x1, double y1, double x2, double y2) GetPresetControlPoints(string preset)
{
return preset switch
{
"ease" => (0.25d, 0.1d, 0.25d, 1d),
"linear" => (0d, 0d, 1d, 1d),
"ease-in" => (0.42d, 0d, 1d, 1d),
"ease-out" => (0d, 0d, 0.58d, 1d),
"ease-in-out" => (0.42d, 0d, 0.58d, 1d),
_ => (0d, 0d, 1d, 1d),
};
}

public static float Apply(float input)
{
double t = Math.Clamp(input, 0f, 1f);
var controlPoints = GetControlPoints();

return (float)Evaluate(controlPoints.Item1, controlPoints.Item2, controlPoints.Item3, controlPoints.Item4, t);
}

private static (double x1, double y1, double x2, double y2) GetControlPoints()
{
string preset = Settings.TranslationCurvePreset;
double x1 = Settings.TranslationCurveX1;
double y1 = Settings.TranslationCurveY1;
double x2 = Settings.TranslationCurveX2;
double y2 = Settings.TranslationCurveY2;

if (_lastPreset == preset && _lastX1 == x1 && _lastY1 == y1 && _lastX2 == x2 && _lastY2 == y2)
return _cachedControlPoints;

_cachedControlPoints = preset == "custom"
? (x1, y1, x2, y2)
: GetPresetControlPoints(preset);

_lastPreset = preset;
_lastX1 = x1;
_lastY1 = y1;
_lastX2 = x2;
_lastY2 = y2;
return _cachedControlPoints;
}

private static double Evaluate(double x1, double y1, double x2, double y2, double xTarget)
{
x1 = Math.Clamp(x1, 0d, 1d);
x2 = Math.Clamp(x2, 0d, 1d);
y1 = Math.Clamp(y1, 0d, 1d);
y2 = Math.Clamp(y2, 0d, 1d);
xTarget = Math.Clamp(xTarget, 0d, 1d);

if (xTarget <= 0d)
return 0d;
if (xTarget >= 1d)
return 1d;

double lower = 0d;
double upper = 1d;
double t = xTarget;

for (int i = 0; i < MaxBisectionIterations; i++)
{
t = (lower + upper) * 0.5d;
double x = Bezier(t, 0d, x1, x2, 1d);
if (Math.Abs(x - xTarget) < Epsilon)
break;
if (x < xTarget)
lower = t;
else
upper = t;
}

return Bezier(t, 0d, y1, y2, 1d);
}

private static double Bezier(double t, double p0, double p1, double p2, double p3)
{
double oneMinusT = 1d - t;
return oneMinusT * oneMinusT * oneMinusT * p0
+ 3d * oneMinusT * oneMinusT * t * p1
+ 3d * oneMinusT * t * t * p2
+ t * t * t * p3;
}
}
}
71 changes: 57 additions & 14 deletions ControlPad/UI Elements/SettingsUserControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,31 +130,74 @@
Unchecked="cb_UnmuteOnSliderChange_Checked"/>
</Grid>

<!-- Translation Exponent -->
<!-- Translation Curve -->
<Grid Margin="0,8,0,0" Grid.Row="5">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>

<StackPanel Orientation="Vertical" Margin="0,0,16,0">
<Label Content="Translation _exponent"
Padding="0"
Target="{Binding ElementName=ThemeComboBox}"/>
<Label Content="Exponent controlling the mapping curve from slider to&#x0a;sound bar."
<Label Content="Translation _curve"
Padding="0"
Target="{Binding ElementName=TranslationCurvePresetComboBox}"/>
<Label Content="Curve controlling mapping from slider to sound level."
Padding="0"
Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</StackPanel>

<ui:NumberBox x:Name="nb_TranslationExponent"
PlaceholderText="1.0"
Grid.ColumnSpan="2"
Margin="367,0,0,0"
Value="1.0"
SmallChange="0.1"
Minimum="0.1"
ValidationMode="InvalidInputOverwritten" ValueChanged="nb_TranslationExponent_ValueChanged"
/>
<StackPanel Grid.Column="1" Orientation="Vertical" MinWidth="180">
<ComboBox x:Name="TranslationCurvePresetComboBox"
SelectedIndex="1"
VerticalAlignment="Center"
SelectionChanged="TranslationCurvePresetComboBox_SelectionChanged">
<ComboBoxItem Content="ease"/>
<ComboBoxItem Content="linear"/>
<ComboBoxItem Content="ease-in"/>
<ComboBoxItem Content="ease-out"/>
<ComboBoxItem Content="ease-in-out"/>
<ComboBoxItem Content="custom"/>
</ComboBox>

<UniformGrid x:Name="CustomCurveGrid" Columns="2" Rows="2" Margin="0,8,0,0">
<ui:NumberBox x:Name="nb_CurveX1"
PlaceholderText="x1"
Value="0.0"
Minimum="0"
Maximum="1"
SmallChange="0.01"
Margin="0,0,6,6"
ValidationMode="InvalidInputOverwritten"
ValueChanged="nb_CustomCurve_ValueChanged"/>
<ui:NumberBox x:Name="nb_CurveY1"
PlaceholderText="y1"
Value="0.0"
Minimum="0"
Maximum="1"
SmallChange="0.01"
Margin="6,0,0,6"
ValidationMode="InvalidInputOverwritten"
ValueChanged="nb_CustomCurve_ValueChanged"/>
<ui:NumberBox x:Name="nb_CurveX2"
PlaceholderText="x2"
Value="1.0"
Minimum="0"
Maximum="1"
SmallChange="0.01"
Margin="0,0,6,0"
ValidationMode="InvalidInputOverwritten"
ValueChanged="nb_CustomCurve_ValueChanged"/>
<ui:NumberBox x:Name="nb_CurveY2"
PlaceholderText="y2"
Value="1.0"
Minimum="0"
Maximum="1"
SmallChange="0.01"
Margin="6,0,0,0"
ValidationMode="InvalidInputOverwritten"
ValueChanged="nb_CustomCurve_ValueChanged"/>
</UniformGrid>
</StackPanel>
</Grid>

<!-- App Theme -->
Expand Down
Loading
Loading