diff --git a/vu1_gui.py b/vu1_gui.py
index 3fbe65e..758aa5f 100644
--- a/vu1_gui.py
+++ b/vu1_gui.py
@@ -410,6 +410,7 @@ body {
+
@@ -450,6 +451,11 @@ body {
300ms Anstiegszeit, ~1% Überschwingen
Der Filter ist die Nadelphysik.
+
+ Natural+ — neue Ballistikformel
+ Hüllkurve mit separatem Attack/Release + 2.-Ordnung Nadelmodell
+ Natürliches Einschwingen, weniger Zappeln, weicher Rücklauf.
+
@@ -562,10 +568,11 @@ function setMode(mode){
fetch('/set/mode/'+mode);
document.getElementById('dualband-box').style.display=mode==='dualband'?'block':'none';
document.getElementById('iec-box').style.display=mode==='iec_true'?'block':'none';
- document.getElementById('physics-ctrls').style.display=mode==='iec_true'?'none':'block';
- ['full','dual','iec'].forEach(m=>{
+ document.getElementById('natural-box').style.display=mode==='natural_formula'?'block':'none';
+ document.getElementById('physics-ctrls').style.display=(mode==='iec_true'||mode==='natural_formula')?'none':'block';
+ ['full','dual','iec','natural'].forEach(m=>{
const btn=document.getElementById('mode-'+m);
- const act=(m==='full'&&mode==='full')||(m==='dual'&&mode==='dualband')||(m==='iec'&&mode==='iec_true');
+ const act=(m==='full'&&mode==='full')||(m==='dual'&&mode==='dualband')||(m==='iec'&&mode==='iec_true')||(m==='natural'&&mode==='natural_formula');
btn.classList.toggle('active', act);
});
}
@@ -1064,6 +1071,9 @@ class PhysicsVU:
self._iec_z = np.zeros(2)
self._iec_level = 0.0
+ # Natural+ Formel (neue Ballistik)
+ self._natural_env = 0.0
+
# ── Biquad helpers ──
def _calc_biquad(self, fc):
w0 = 2.0 * math.pi * fc / self.sample_rate
@@ -1270,6 +1280,50 @@ class PhysicsVU:
self.peak_pos = max(0.0, min(100.0, self.peak_pos))
return self.needle_pos, self.peak_pos
+ # Natural+ (eigene Formel):
+ # 1) getrennte Attack/Release-Hüllkurve
+ # 2) 2.-Ordnung Nadelmodell für natürliches Ein-/Ausschwingen
+ if self.mode == 'natural_formula':
+ target = self._rms_to_percent(self._latest_rms)
+
+ atk_t = 0.085 # schneller Angriff
+ rel_t = 0.650 # weicher Rücklauf
+ alpha_a = 1.0 - math.exp(-dt / atk_t)
+ alpha_r = 1.0 - math.exp(-dt / rel_t)
+ alpha = alpha_a if target > self._natural_env else alpha_r
+ self._natural_env += (target - self._natural_env) * alpha
+
+ # 2nd-order Needle (kritisch nahe gedämpft)
+ stiffness = 58.0
+ damping = 14.5
+ acc = stiffness * (self._natural_env - self.needle_pos) - damping * self.needle_vel
+ self.needle_vel += acc * dt
+ self.needle_pos += self.needle_vel * dt
+ self.needle_pos = max(0.0, min(100.0, self.needle_pos))
+
+ # Bei sehr kleinen Pegeln ruhig auf 0 setzen (kein Mikrozappeln)
+ if self._natural_env < 0.3 and self.needle_pos < 0.3:
+ self._natural_env = 0.0
+ self.needle_pos = 0.0
+ self.needle_vel *= 0.6
+
+ self._target = self._natural_env
+ self._f_spring = 0.0
+ self._f_damping = 0.0
+ self._f_gravity = 0.0
+
+ peak_t = self._rms_to_percent(self._latest_peak)
+ if peak_t > self.peak_pos:
+ self.peak_pos = peak_t
+ self.peak_hold_t = 1.2
+ else:
+ if self.peak_hold_t > 0:
+ self.peak_hold_t -= dt
+ else:
+ self.peak_pos -= 16.0 * dt
+ self.peak_pos = max(0.0, min(100.0, self.peak_pos))
+ return self.needle_pos, self.peak_pos
+
# Target
if self.mode == 'dualband':
target = min(100.0, self._rms_to_percent(self._latest_rms_lo)*self.band_lo_weight +
@@ -1414,7 +1468,10 @@ def set_param(param, value):
if not meter: return jsonify({"ok": False})
if param == 'mode':
meter.mode = value
- if value == 'iec_true': meter._iec_z[:] = 0
+ if value == 'iec_true':
+ meter._iec_z[:] = 0
+ if value == 'natural_formula':
+ meter._natural_env = meter.needle_pos
elif param == 'monitor':
meter.monitor = value == '1'
elif param == 'bypass':
@@ -1447,7 +1504,7 @@ def reset():
meter.needle_pos=0; meter.needle_vel=0; meter.peak_pos=0
meter.mode='full'; meter.monitor=False; meter.bypass=False; meter.solo='off'
meter.band_crossover=250; meter.band_lo_weight=0.6; meter.band_hi_weight=0.4
- meter._iec_z[:]=0; meter._iec_level=0
+ meter._iec_z[:]=0; meter._iec_level=0; meter._natural_env=0
return jsonify({"ok": True})