diff --git a/ProjectObsidian/Components/Audio/ButterworthFilter.cs b/ProjectObsidian/Components/Audio/ButterworthFilter.cs new file mode 100644 index 0000000..68ee19f --- /dev/null +++ b/ProjectObsidian/Components/Audio/ButterworthFilter.cs @@ -0,0 +1,142 @@ +using System; +using FrooxEngine; +using Elements.Assets; + +namespace Obsidian.Components.Audio; + +[Category(new string[] { "Obsidian/Audio" })] +public class ButterworthFilter : Component, IAudioSource, IWorldElement +{ + [Range(0f, 10000f, "0.00")] + public readonly Sync Frequency; + + [Range(0.1f, 1.41f, "0.00")] + public readonly Sync Resonance; + + public readonly Sync LowPass; + + public readonly SyncRef Source; + + private double lastTime; + + public bool IsActive + { + get + { + return Source.Target != null && Source.Target.IsActive; + } + } + + protected override void OnAwake() + { + base.OnAwake(); + lastTime = -1; + } + + public int ChannelCount => Source.Target?.ChannelCount ?? 0; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + return; + } + + Span span = stackalloc S[buffer.Length]; + + span = buffer; + + Source.Target.Read(span); + + var filter = new FilterButterworth(Frequency, (int)(span.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), LowPass ? FilterButterworth.PassType.Lowpass : FilterButterworth.PassType.Highpass, Resonance); + + for (int i = 0; i < span.Length; i++) + { + filter.Update(ref span[i]); + } + + lastTime = Engine.Current.AudioSystem.DSPTime; + } + + public class FilterButterworth where S: unmanaged, IAudioSample + { + /// + /// rez amount, from sqrt(2) to ~ 0.1 + /// + private readonly float resonance; + + private readonly float frequency; + private readonly int sampleRate; + private readonly PassType passType; + + private readonly float c, a1, a2, a3, b1, b2; + + /// + /// Array of input values, latest are in front + /// + private S[] inputHistory = new S[2]; + + /// + /// Array of output values, latest are in front + /// + private S[] outputHistory = new S[3]; + + public FilterButterworth(float frequency, int sampleRate, PassType passType, float resonance) + { + this.resonance = resonance; + this.frequency = frequency; + this.sampleRate = sampleRate; + this.passType = passType; + + switch (passType) + { + case PassType.Lowpass: + c = 1.0f / (float)Math.Tan(Math.PI * frequency / sampleRate); + a1 = 1.0f / (1.0f + resonance * c + c * c); + a2 = 2f * a1; + a3 = a1; + b1 = 2.0f * (1.0f - c * c) * a1; + b2 = (1.0f - resonance * c + c * c) * a1; + break; + case PassType.Highpass: + c = (float)Math.Tan(Math.PI * frequency / sampleRate); + a1 = 1.0f / (1.0f + resonance * c + c * c); + a2 = -2f * a1; + a3 = a1; + b1 = 2.0f * (c * c - 1.0f) * a1; + b2 = (1.0f - resonance * c + c * c) * a1; + break; + } + } + + public enum PassType + { + Highpass, + Lowpass, + } + + public void Update(ref S newInput) + { + S first = newInput.Multiply(a1); + S second = this.inputHistory[0].Multiply(a2); + S third = this.inputHistory[1].Multiply(a3); + S fourth = this.outputHistory[0].Multiply(b1); + S fifth = this.outputHistory[1].Multiply(b2); + S final = first.Add(second).Add(third).Subtract(fourth).Subtract(fifth); + + this.inputHistory[1] = this.inputHistory[0]; + this.inputHistory[0] = newInput; + + this.outputHistory[2] = this.outputHistory[1]; + this.outputHistory[1] = this.outputHistory[0]; + this.outputHistory[0] = final; + + newInput = final; + } + + public S Value + { + get { return this.outputHistory[0]; } + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs new file mode 100644 index 0000000..3cf78b2 --- /dev/null +++ b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs @@ -0,0 +1,60 @@ +using System; +using FrooxEngine; +using Elements.Assets; + +namespace Obsidian.Components.Audio; + +[Category(new string[] { "Obsidian/Audio" })] +public class EMA_IIR_SmoothSignal : Component, IAudioSource, IWorldElement +{ + [Range(0f, 1f, "0.00")] + public readonly Sync SmoothingFactor; + + public readonly SyncRef Source; + + public bool IsActive + { + get + { + return Source.Target != null && Source.Target.IsActive; + } + } + + public int ChannelCount => Source.Target?.ChannelCount ?? 0; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + return; + } + + Span span = stackalloc S[buffer.Length]; + + span = buffer; + + Source.Target.Read(span); + + EMAIIRSmoothSignal(ref span, span.Length, SmoothingFactor); + } + + // smoothingFactor is between 0.0 (no smoothing) and 0.9999.. (almost smoothing to DC) - *kind* of the inverse of cutoff frequency + public void EMAIIRSmoothSignal(ref Span input, int N, float smoothingFactor = 0.8f) where S : unmanaged, IAudioSample + { + // forward EMA IIR + S acc = input[0]; + for (int i = 0; i < N; ++i) + { + acc = input[i].LerpTo(acc, smoothingFactor); + input[i] = acc; + } + + // backward EMA IIR - required only if we need to preserve the phase (aka make the filter symetric) - we usually want this + acc = input[N - 1]; + for (int i = N - 1; i >= 0; --i) + { + acc = input[i].LerpTo(acc, smoothingFactor); + input[i] = acc; + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProjectObsidian.csproj b/ProjectObsidian/ProjectObsidian.csproj index 6fe136e..28ee510 100644 --- a/ProjectObsidian/ProjectObsidian.csproj +++ b/ProjectObsidian/ProjectObsidian.csproj @@ -72,6 +72,7 @@ +