From 2918c17e48459bfc83e1e3f17a118fd244dbbfcc Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:36:36 +0000 Subject: [PATCH 1/5] Add AudioLowPassFilter component --- .../Components/Audio/AudioLowPassFilter.cs | 93 +++++++++++++++++++ ProjectObsidian/ProjectObsidian.csproj | 1 + 2 files changed, 94 insertions(+) create mode 100644 ProjectObsidian/Components/Audio/AudioLowPassFilter.cs diff --git a/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs b/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs new file mode 100644 index 0000000..231e521 --- /dev/null +++ b/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs @@ -0,0 +1,93 @@ +using System; +using FrooxEngine; +using Elements.Assets; + +namespace Obsidian.Components.Audio; + +[Category(new string[] { "Audio" })] +public class AudioLowPassFilter : Component, IAudioSource, IWorldElement +{ + [Range(0f, 1f, "0.00")] + public readonly Sync SmoothingFactor; + + public readonly AssetRef Clip; + + private double lastPos; + + private double nextPos; + + private double lastAudioTime; + + public bool IsActive + { + get + { + return Clip.IsAssetAvailable; + } + } + + public int ChannelCount => (Clip.Asset?.Data?.ChannelCount).GetValueOrDefault(); + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + var clipData = Clip.Asset?.Data; + if (clipData == null) + { + buffer.Fill(default(S)); + return; + } + double dSPTime = base.Engine.AudioSystem.DSPTime; + if (lastAudioTime != dSPTime) + { + lastPos = nextPos; + lastAudioTime = dSPTime; + } + bool flag = false; + Span span = stackalloc S[buffer.Length]; + if (!IsActive) + { + return; + } + Span span2 = span; + if (!flag) + { + span2 = buffer; + } + + int num = Clip.Asset.Data.Read(span2, lastPos * (double)Clip.Asset.Data.SampleRate, 1f, loop: true); + + EMAIIRSmoothSignal(ref span2, span2.Length, SmoothingFactor); + + nextPos = (lastPos + (double)num * base.Engine.AudioSystem.InvSampleRate * (double)1f) % Clip.Asset.Data.Duration; + + if (flag) + { + buffer.Add(span2); + } + flag = true; + if (!flag) + { + buffer.Fill(default(S)); + } + } + + // 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 @@ + From e2f57531c67d4e522b792e46a0e71d8b22719a7d Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:56:37 +0000 Subject: [PATCH 2/5] Switch to an IAudioSource ref so it works for videos etc --- .../Components/Audio/AudioLowPassFilter.cs | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs b/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs index 231e521..1097025 100644 --- a/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs +++ b/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs @@ -10,11 +10,7 @@ public class AudioLowPassFilter : Component, IAudioSource, IWorldElement [Range(0f, 1f, "0.00")] public readonly Sync SmoothingFactor; - public readonly AssetRef Clip; - - private double lastPos; - - private double nextPos; + public readonly SyncRef Source; private double lastAudioTime; @@ -22,53 +18,33 @@ public bool IsActive { get { - return Clip.IsAssetAvailable; + return Source.Target.IsActive; } } - public int ChannelCount => (Clip.Asset?.Data?.ChannelCount).GetValueOrDefault(); + public int ChannelCount => Source.Target?.ChannelCount ?? 0; public void Read(Span buffer) where S : unmanaged, IAudioSample { - var clipData = Clip.Asset?.Data; - if (clipData == null) - { - buffer.Fill(default(S)); - return; - } double dSPTime = base.Engine.AudioSystem.DSPTime; if (lastAudioTime != dSPTime) { - lastPos = nextPos; lastAudioTime = dSPTime; } - bool flag = false; + Span span = stackalloc S[buffer.Length]; + if (!IsActive) { return; } + Span span2 = span; - if (!flag) - { - span2 = buffer; - } + span2 = buffer; - int num = Clip.Asset.Data.Read(span2, lastPos * (double)Clip.Asset.Data.SampleRate, 1f, loop: true); + Source.Target.Read(span2); EMAIIRSmoothSignal(ref span2, span2.Length, SmoothingFactor); - - nextPos = (lastPos + (double)num * base.Engine.AudioSystem.InvSampleRate * (double)1f) % Clip.Asset.Data.Duration; - - if (flag) - { - buffer.Add(span2); - } - flag = true; - if (!flag) - { - buffer.Fill(default(S)); - } } // smoothingFactor is between 0.0 (no smoothing) and 0.9999.. (almost smoothing to DC) - *kind* of the inverse of cutoff frequency From d82b1c8117f346f52c9ec3c692c0cac860401e5f Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:03:15 +0000 Subject: [PATCH 3/5] Remove unneeded code --- .../Components/Audio/AudioLowPassFilter.cs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs b/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs index 1097025..aa90664 100644 --- a/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs +++ b/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs @@ -12,13 +12,11 @@ public class AudioLowPassFilter : Component, IAudioSource, IWorldElement public readonly SyncRef Source; - private double lastAudioTime; - public bool IsActive { get { - return Source.Target.IsActive; + return Source.Target != null && Source.Target.IsActive; } } @@ -26,25 +24,18 @@ public bool IsActive public void Read(Span buffer) where S : unmanaged, IAudioSample { - double dSPTime = base.Engine.AudioSystem.DSPTime; - if (lastAudioTime != dSPTime) - { - lastAudioTime = dSPTime; - } - - Span span = stackalloc S[buffer.Length]; - if (!IsActive) { return; } - Span span2 = span; - span2 = buffer; + Span span = stackalloc S[buffer.Length]; + + span = buffer; - Source.Target.Read(span2); + Source.Target.Read(span); - EMAIIRSmoothSignal(ref span2, span2.Length, SmoothingFactor); + 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 From d8ed31f391acfd252f9a058f9183bca38eccb3d5 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 03:09:50 +0000 Subject: [PATCH 4/5] Add Butterworth filter, rename the other filter --- .../Components/Audio/ButterworthFilter.cs | 142 ++++++++++++++++++ ...wPassFilter.cs => EMA_IIR_SmoothSignal.cs} | 2 +- 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 ProjectObsidian/Components/Audio/ButterworthFilter.cs rename ProjectObsidian/Components/Audio/{AudioLowPassFilter.cs => EMA_IIR_SmoothSignal.cs} (95%) diff --git a/ProjectObsidian/Components/Audio/ButterworthFilter.cs b/ProjectObsidian/Components/Audio/ButterworthFilter.cs new file mode 100644 index 0000000..3a4e743 --- /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[] { "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/AudioLowPassFilter.cs b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs similarity index 95% rename from ProjectObsidian/Components/Audio/AudioLowPassFilter.cs rename to ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs index aa90664..9bb2a68 100644 --- a/ProjectObsidian/Components/Audio/AudioLowPassFilter.cs +++ b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs @@ -5,7 +5,7 @@ namespace Obsidian.Components.Audio; [Category(new string[] { "Audio" })] -public class AudioLowPassFilter : Component, IAudioSource, IWorldElement +public class EMA_IIR_SmoothSignal : Component, IAudioSource, IWorldElement { [Range(0f, 1f, "0.00")] public readonly Sync SmoothingFactor; From ae4e6592ad36057da74f6b546d05fde6464d224a Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:23:22 +0000 Subject: [PATCH 5/5] Fix categories --- ProjectObsidian/Components/Audio/ButterworthFilter.cs | 2 +- ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProjectObsidian/Components/Audio/ButterworthFilter.cs b/ProjectObsidian/Components/Audio/ButterworthFilter.cs index 3a4e743..68ee19f 100644 --- a/ProjectObsidian/Components/Audio/ButterworthFilter.cs +++ b/ProjectObsidian/Components/Audio/ButterworthFilter.cs @@ -4,7 +4,7 @@ namespace Obsidian.Components.Audio; -[Category(new string[] { "Audio" })] +[Category(new string[] { "Obsidian/Audio" })] public class ButterworthFilter : Component, IAudioSource, IWorldElement { [Range(0f, 10000f, "0.00")] diff --git a/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs index 9bb2a68..3cf78b2 100644 --- a/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs +++ b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs @@ -4,7 +4,7 @@ namespace Obsidian.Components.Audio; -[Category(new string[] { "Audio" })] +[Category(new string[] { "Obsidian/Audio" })] public class EMA_IIR_SmoothSignal : Component, IAudioSource, IWorldElement { [Range(0f, 1f, "0.00")]