aerothesis

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit a5c7adfb95954021d459f80873c69ce786f8c74a
parent 09a4efe8947563cb18b2cdf659abceab8ea62a99
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Fri, 19 Jun 2026 03:38:45 +0000

feat: implement displacement-based delay line resonance model

Update the acoustic simulation to utilize a displacement-driven delay-line model for physical resonance.

- Refactored `x_history` to `displacement_history` and added `displacement_prev` for state tracking.
- Implemented a new non-linear damping update equation in the audio processing loop.
- Updated resonance decay logic to align with displacement-based wave propagation.
- Added comprehensive documentation in README.md detailing the wave propagation physics and signal flow.

Diffstat:
MREADME.md | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 31+++++++++++++++++++------------
2 files changed, 97 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md @@ -150,3 +150,81 @@ $$f[n] = \pm \frac{1}{2} \rho v_f[n]^2 g[n]$$ $$v_f[n] = \frac{-\alpha + \sqrt{\alpha^2 + 4 B[n] \Gamma[n-1]}}{2 B[n]}$$ #### Resonance Part + +<details> +<summary>Acoustic Simulation Logic: Displacement-Based Delay Line</summary> + +Rather than simulating wave reflection through complex fluid dynamics (changes in density or tube stiffness), this model treats acoustic wave propagation as a delay-based system. We rely on the physical principle that acoustic energy dissipates more rapidly at higher frequencies, which we implement as a damping model applied to the displacement velocity. + +#### 1. Damping Mechanism + +Energy in an acoustic system is proportional to the square of the time derivative of displacement ($(\partial x / \partial t)^2$). We apply a damping constant $a$ to this derivative. This effectively attenuates higher-frequency components, as their energy dissipates faster than lower-frequency components. Given an input displacement $x_n$, a delayed resonant displacement $x_{\text{resonance}}$, and the total previous displacement $x_{\text{prev}}$, the system state is updated as: + +$$x = a (x_{\text{prev}} - (x_n + x_{\text{resonance}}))^2$$ + +#### 2. Physical Validity (D’Alembert’s Solution) + +The simplification of representing reflection as a pure time delay with a coefficient is mathematically rooted in the 1D wave equation: + +$$\frac{\partial^2 p}{\partial t^2} - c^2 \frac{\partial^2 p}{\partial x^2} = 0$$ + +According to **D’Alembert’s solution**, any wave $p(x, t)$ can be decomposed into forward-traveling ($f$) and backward-traveling ($g$) waves: + + +$$p(x, t) = f(t - x/c) + g(t + x/c)$$ + +At the boundary $x=L$, we apply the following conditions: + +* **Open End:** Pressure must be zero ($p=0$), leading to $g(t + L/c) = -f(t - L/c)$. The wave reflects with a phase inversion (coefficient $-1$). +* **Closed End:** Velocity must be zero ($\partial p/\partial x = 0$), leading to $g(t + L/c) = f(t - L/c)$. The wave reflects with its phase preserved (coefficient $+1$). + +Consequently, calculating the resonance by multiplying the previously delayed displacement by a reflection coefficient is analytically equivalent to solving the wave equation for linear media. + +#### 3. Defining the Delay Time + +Given a note frequency $f$ and the speed of sound $c$, the wavelength $\lambda$ is defined as $\lambda = c/f$. + +* **Open Pipe:** $\lambda = 2L \implies \text{round-trip time} = 2L/c = 1/f$. +* **Closed Pipe:** $\lambda = 4L \implies \text{round-trip time} = 4L/c = 2/f$. + +Thus, the required delay samples can be derived directly from the frequency $f$ and sample rate $fs$ without needing explicit values for tube length $L$ or sound speed $c$. + +*Note: While applying a low-pass filter to the output would achieve a similar spectral result, this implementation utilizes an explicit wave-propagation model to maintain physical rigor and simulate the dynamic behavior of the air column.* + +</details> + +The core simulation is based on a displacement-driven delay-line model, where the system state at time $n$ is determined by the input $x_n$ and the resonant wave $x_{\text{resonance}}$ returning from the pipe's boundary. + +1. System Update Equation + +The total displacement $x[n]$ is calculated as a damped non-linear function of the input and the delayed resonant state. Given a damping constant $a$ ($0 < a \le 1$): + +$$x[n] = a \cdot \left( x[n-1] - (x_{\text{in}}[n] + x_{\text{resonance}}[n]) \right)^2$$ + +Where $x[n-1]$ represents the previous total displacement, capturing the system's memory. + +2. Resonant Feedback (Delay and Reflection) + +The resonant component $x_{\text{resonance}}$ is the delayed state derived from the pipe's boundary conditions. Given a delay buffer $D$ of length $T$ (where $T = f_s / f$), the resonance is defined by the reflection coefficient $R$: + +$$x_{\text{resonance}}[n] = R \cdot \text{buffer}[n - T]$$ + +* **For Open Pipes (Open-Open):** +* Reflection occurs twice per round-trip with a phase inversion, resulting in $R = 1$ (net phase preserved). + + +* **For Closed Pipes (Closed-Open):** +* Reflection occurs once with phase inversion and once with phase preservation, resulting in $R = -1$ (net phase inversion per round-trip). + + + +3. Signal Flow Summary + +To maintain a stable simulation without algebraic loops, the signal flow follows this recursive update per sample: + +1. **Retrieve:** $x_{\text{res}} = R \cdot \text{delay\_buffer}[\text{ptr}]$ +2. **Compute:** $x_{\text{curr}} = a \cdot (x_{\text{prev}} - (x_{\text{in}} + x_{\text{res}}))^2$ +3. **Update:** $\text{delay\_buffer}[\text{ptr}] = x_{\text{curr}}$ +4. **Advance:** $\text{ptr} = (\text{ptr} + 1) \pmod T$ + +This approach effectively emulates the harmonic series and spectral decay of real instruments by utilizing the time-domain round-trip of the displacement wave as the primary oscillator, while the non-linear term $( \dots )^2$ provides the necessary harmonic distortion and energy dissipation. diff --git a/src/lib.rs b/src/lib.rs @@ -22,8 +22,8 @@ pub struct Aerothesis { pub v_fluid_prev: f32, - pub x_history: VecDeque<f32>, - + pub displacement_history: VecDeque<f32>, + pub displacement_prev: f32, pub note_frequency: f32, } @@ -104,8 +104,8 @@ impl Default for Aerothesis { v_bite: 0.0, v_fluid_prev: 0.0, - x_history: VecDeque::new(), - + displacement_history: VecDeque::new(), + displacement_prev: 0.0, note_frequency: 0.0, } } @@ -223,7 +223,7 @@ impl Default for AerothesisParams { resonance_type: EnumParam::new("Resonance Type", ResonanceType::OpenPipe), resonance_decay: FloatParam::new( "Resonance Decay", - 0.9, + 0.01, FloatRange::Skewed { min: 0.0, max: 1.0, @@ -335,20 +335,26 @@ impl Aerothesis { let x_n = self.step(); let x_oscillator = x_n - self.equilibrium_offset(); - let resonance = if self.resonance_delay_samples() > self.x_history.len() as f32 { + let resonance = if self.resonance_delay_samples() > self.displacement_history.len() as f32 { 0.0 } else { let decay: f32 = if self.params.resonance_type.value() == ResonanceType::OpenPipe { 1.0 } else { -1.0 - } * self.params.resonance_decay.value(); - let x_delay = self.x_history.pop_front().unwrap_or(0.0); + }; + let x_delay = self.displacement_history.pop_front().unwrap_or(0.0); decay * x_delay }; - let x_current = x_oscillator + resonance; - self.x_history.push_back(x_current); + let x_nondamping = x_oscillator + resonance; + + let x_current = x_nondamping + * (1.0 - self.params.resonance_decay.value()) + * (self.displacement_prev - x_nondamping) + * (self.displacement_prev - x_nondamping); + + self.displacement_history.push_back(x_current); x_current } @@ -370,7 +376,7 @@ impl Aerothesis { } } fn avg_x_history(&self) -> f32 { - self.x_history.iter().sum::<f32>() / self.x_history.len() as f32 + self.displacement_history.iter().sum::<f32>() / self.displacement_history.len() as f32 } } @@ -428,7 +434,8 @@ impl Plugin for Aerothesis { fn reset(&mut self) { // Reset buffers and envelopes here. This can be called from the audio thread and may not // allocate. You can remove this function if you do not need it. - self.x_history.clear(); + + // self.displacement_history.clear(); } fn process(