From b9a01ec029cf7813bd382eb7f807331c2fd3bb7c Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:57:20 +0000 Subject: [PATCH 1/4] Add bandpass filter, update butterworth and others --- .../Components/Audio/BandPassFilter.cs | 64 +++++++++++++++++++ .../Components/Audio/ButterworthFilter.cs | 6 +- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 ProjectObsidian/Components/Audio/BandPassFilter.cs diff --git a/ProjectObsidian/Components/Audio/BandPassFilter.cs b/ProjectObsidian/Components/Audio/BandPassFilter.cs new file mode 100644 index 0000000..4709752 --- /dev/null +++ b/ProjectObsidian/Components/Audio/BandPassFilter.cs @@ -0,0 +1,64 @@ +using System; +using FrooxEngine; +using Elements.Assets; +using System.Net; +using ProtoFlux.Runtimes.Execution; + +namespace Obsidian.Components.Audio +{ + [Category(new string[] { "Obsidian/Audio" })] + public class BandPassFilter : Component, IAudioSource, IWorldElement + { + [Range(0.1f, 1.41f, "0.00")] + public readonly Sync Resonance; + + [Range(20f, 20000f, "0.00")] + public readonly Sync LowFrequency; + + [Range(20f, 20000f, "0.00")] + public readonly Sync HighFrequency; + + public readonly SyncRef Source; + + private double lastTime; + + public bool IsActive + { + get => 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 tempBuffer = stackalloc S[buffer.Length]; + tempBuffer = buffer; + Source.Target.Read(tempBuffer); + + var LowPass = new ButterworthFilter.FilterButterworth(HighFrequency, (int)(tempBuffer.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), ButterworthFilter.FilterButterworth.PassType.Lowpass, Resonance); + var HighPass = new ButterworthFilter.FilterButterworth(LowFrequency, (int)(tempBuffer.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), ButterworthFilter.FilterButterworth.PassType.Highpass, Resonance); + + for (int i = 0; i < tempBuffer.Length; i++) + { + LowPass.Update(ref tempBuffer[i]); + HighPass.Update(ref tempBuffer[i]); + } + + lastTime = Engine.Current.AudioSystem.DSPTime; + } + + protected override void OnAwake() + { + base.OnAwake(); + Resonance.Value = 1.41f; + LowFrequency.Value = 20f; + HighFrequency.Value = 20000f; + lastTime = Engine.Current.AudioSystem.DSPTime; + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/ButterworthFilter.cs b/ProjectObsidian/Components/Audio/ButterworthFilter.cs index 68ee19f..9b2ea1b 100644 --- a/ProjectObsidian/Components/Audio/ButterworthFilter.cs +++ b/ProjectObsidian/Components/Audio/ButterworthFilter.cs @@ -7,7 +7,7 @@ namespace Obsidian.Components.Audio; [Category(new string[] { "Obsidian/Audio" })] public class ButterworthFilter : Component, IAudioSource, IWorldElement { - [Range(0f, 10000f, "0.00")] + [Range(20f, 20000f, "0.00")] public readonly Sync Frequency; [Range(0.1f, 1.41f, "0.00")] @@ -30,7 +30,9 @@ public bool IsActive protected override void OnAwake() { base.OnAwake(); - lastTime = -1; + lastTime = Engine.Current.AudioSystem.DSPTime; + Frequency.Value = 20f; + Resonance.Value = 1.41f; } public int ChannelCount => Source.Target?.ChannelCount ?? 0; From eeedcd661e8f09cbf77f6ef4c73d558123a77832 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:31:59 +0000 Subject: [PATCH 2/4] Improve the filters by storing them persistently instead of constantly creating new ones --- .../Components/Audio/BandPassFilter.cs | 30 ++++++++++++++++--- .../Components/Audio/ButterworthFilter.cs | 29 ++++++++++++------ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/ProjectObsidian/Components/Audio/BandPassFilter.cs b/ProjectObsidian/Components/Audio/BandPassFilter.cs index 4709752..aa162d4 100644 --- a/ProjectObsidian/Components/Audio/BandPassFilter.cs +++ b/ProjectObsidian/Components/Audio/BandPassFilter.cs @@ -22,6 +22,9 @@ public class BandPassFilter : Component, IAudioSource, IWorldElement private double lastTime; + private object lowFilter = null; + private object highFilter = null; + public bool IsActive { get => Source.Target != null && Source.Target.IsActive; @@ -40,13 +43,22 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample tempBuffer = buffer; Source.Target.Read(tempBuffer); - var LowPass = new ButterworthFilter.FilterButterworth(HighFrequency, (int)(tempBuffer.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), ButterworthFilter.FilterButterworth.PassType.Lowpass, Resonance); - var HighPass = new ButterworthFilter.FilterButterworth(LowFrequency, (int)(tempBuffer.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), ButterworthFilter.FilterButterworth.PassType.Highpass, Resonance); + if (lowFilter == null) + { + lowFilter = new ButterworthFilter.FilterButterworth(); + } + if (highFilter == null) + { + highFilter = new ButterworthFilter.FilterButterworth(); + } + + ((ButterworthFilter.FilterButterworth)lowFilter).UpdateCoefficients(HighFrequency, (int)(tempBuffer.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), ButterworthFilter.FilterButterworth.PassType.Lowpass, Resonance); + ((ButterworthFilter.FilterButterworth)highFilter).UpdateCoefficients(LowFrequency, (int)(tempBuffer.Length / (Engine.Current.AudioSystem.DSPTime - lastTime)), ButterworthFilter.FilterButterworth.PassType.Highpass, Resonance); for (int i = 0; i < tempBuffer.Length; i++) { - LowPass.Update(ref tempBuffer[i]); - HighPass.Update(ref tempBuffer[i]); + ((ButterworthFilter.FilterButterworth)lowFilter).Update(ref tempBuffer[i]); + ((ButterworthFilter.FilterButterworth)highFilter).Update(ref tempBuffer[i]); } lastTime = Engine.Current.AudioSystem.DSPTime; @@ -60,5 +72,15 @@ protected override void OnAwake() HighFrequency.Value = 20000f; lastTime = Engine.Current.AudioSystem.DSPTime; } + + protected override void OnChanges() + { + base.OnChanges(); + if (Source.GetWasChangedAndClear()) + { + lowFilter = null; + highFilter = null; + } + } } } \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/ButterworthFilter.cs b/ProjectObsidian/Components/Audio/ButterworthFilter.cs index 9b2ea1b..cf5a70f 100644 --- a/ProjectObsidian/Components/Audio/ButterworthFilter.cs +++ b/ProjectObsidian/Components/Audio/ButterworthFilter.cs @@ -19,6 +19,8 @@ public class ButterworthFilter : Component, IAudioSource, IWorldElement private double lastTime; + private object filter = null; + public bool IsActive { get @@ -35,6 +37,15 @@ protected override void OnAwake() Resonance.Value = 1.41f; } + protected override void OnChanges() + { + base.OnChanges(); + if (Source.GetWasChangedAndClear()) + { + filter = null; + } + } + public int ChannelCount => Source.Target?.ChannelCount ?? 0; public void Read(Span buffer) where S : unmanaged, IAudioSample @@ -50,11 +61,16 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample 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); + if (filter == null) + { + filter = new FilterButterworth(); + } + + ((FilterButterworth)filter).UpdateCoefficients(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]); + ((FilterButterworth)filter).Update(ref span[i]); } lastTime = Engine.Current.AudioSystem.DSPTime; @@ -71,7 +87,7 @@ public class FilterButterworth where S: unmanaged, IAudioSample private readonly int sampleRate; private readonly PassType passType; - private readonly float c, a1, a2, a3, b1, b2; + private float c, a1, a2, a3, b1, b2; /// /// Array of input values, latest are in front @@ -83,13 +99,8 @@ public class FilterButterworth where S: unmanaged, IAudioSample /// private S[] outputHistory = new S[3]; - public FilterButterworth(float frequency, int sampleRate, PassType passType, float resonance) + public void UpdateCoefficients(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: From b6d5298541bd35f010e8a6202e52a24079e056b6 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:44:30 +0000 Subject: [PATCH 3/4] Add FrequenceModulator component (FM synthesis) --- .../Components/Audio/FrequencyModulator.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 ProjectObsidian/Components/Audio/FrequencyModulator.cs diff --git a/ProjectObsidian/Components/Audio/FrequencyModulator.cs b/ProjectObsidian/Components/Audio/FrequencyModulator.cs new file mode 100644 index 0000000..c091937 --- /dev/null +++ b/ProjectObsidian/Components/Audio/FrequencyModulator.cs @@ -0,0 +1,82 @@ +using System; +using FrooxEngine; +using Elements.Assets; + +namespace Obsidian.Components.Audio +{ + [Category(new string[] { "Obsidian/Audio" })] + public class FrequencyModulator : Component, IAudioSource, IWorldElement + { + [Range(0f, 1000f, "0.00")] + public readonly Sync ModulationIndex; + + public readonly SyncRef CarrierSource; + public readonly SyncRef ModulatorSource; + + private double dspTimeLastUpdate; + + public bool IsActive + { + get + { + return CarrierSource.Target != null && + ModulatorSource.Target != null && + CarrierSource.Target.IsActive && + ModulatorSource.Target.IsActive; + } + } + + public int ChannelCount + { + get + { + return CarrierSource.Target?.ChannelCount ?? 0; + } + } + + protected override void OnAwake() + { + base.OnAwake(); + dspTimeLastUpdate = Engine.Current.AudioSystem.DSPTime; + ModulationIndex.Value = 100f; // Default modulation index + } + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + return; + } + + int channelCount = ChannelCount; + if (channelCount == 0) + { + return; + } + + // Temporary buffers for carrier and modulator sources + Span carrierBuffer = stackalloc S[buffer.Length]; + Span modulatorBuffer = stackalloc S[buffer.Length]; + + // Read data from sources + CarrierSource.Target.Read(carrierBuffer); + ModulatorSource.Target.Read(modulatorBuffer); + + float modulationIndex = ModulationIndex.Value; + + // Apply FM synthesis + for (int i = 0; i < buffer.Length; i++) + { + // Get carrier and modulator values + float carrierValue = carrierBuffer[i].AbsoluteAmplitude; + float modulatorValue = modulatorBuffer[i].AbsoluteAmplitude; + + // Compute frequency modulation + float modulatedValue = (float)(carrierValue * Math.Sin(2 * Math.PI * modulationIndex * modulatorValue)); + + // Write modulated value to the buffer + buffer[i] = buffer[i].Bias(buffer[i].AbsoluteAmplitude - modulatedValue); + } + } + } +} \ No newline at end of file From 4569a264cd1609ebdf704949889021532ed452e7 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:45:56 +0000 Subject: [PATCH 4/4] Cleanup --- ProjectObsidian/Components/Audio/FrequencyModulator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/ProjectObsidian/Components/Audio/FrequencyModulator.cs b/ProjectObsidian/Components/Audio/FrequencyModulator.cs index c091937..ddc5b03 100644 --- a/ProjectObsidian/Components/Audio/FrequencyModulator.cs +++ b/ProjectObsidian/Components/Audio/FrequencyModulator.cs @@ -13,8 +13,6 @@ public class FrequencyModulator : Component, IAudioSource, IWorldElement public readonly SyncRef CarrierSource; public readonly SyncRef ModulatorSource; - private double dspTimeLastUpdate; - public bool IsActive { get @@ -37,7 +35,6 @@ public int ChannelCount protected override void OnAwake() { base.OnAwake(); - dspTimeLastUpdate = Engine.Current.AudioSystem.DSPTime; ModulationIndex.Value = 100f; // Default modulation index }