diff --git a/ProjectObsidian/Components/Audio/BandPassFilter.cs b/ProjectObsidian/Components/Audio/BandPassFilter.cs index b5f04fc..9395bf8 100644 --- a/ProjectObsidian/Components/Audio/BandPassFilter.cs +++ b/ProjectObsidian/Components/Audio/BandPassFilter.cs @@ -1,13 +1,11 @@ using System; using FrooxEngine; using Elements.Assets; -using System.Net; -using ProtoFlux.Runtimes.Execution; -using System.Collections.Generic; +using Obsidian.Elements; namespace Obsidian.Components.Audio { - [Category(new string[] { "Obsidian/Audio" })] + [Category(new string[] { "Obsidian/Audio/Filters" })] public class BandPassFilter : Component, IAudioSource, IWorldElement { [Range(0.1f, 1.41f, "0.00")] @@ -21,10 +19,7 @@ public class BandPassFilter : Component, IAudioSource, IWorldElement public readonly SyncRef Source; - private double lastTime; - - private Dictionary lowFilters = new(); - private Dictionary highFilters = new(); + private BandPassFilterController _controller = new(); public bool IsActive { @@ -38,8 +33,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample if (!IsActive) { buffer.Fill(default(S)); - lowFilters.Clear(); - highFilters.Clear(); + _controller.Clear(); return; } @@ -48,27 +42,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample Source.Target.Read(tempBuffer); - if (!lowFilters.TryGetValue(typeof(S), out object lowFilter)) - { - lowFilter = new ButterworthFilter.FilterButterworth(); - lowFilters.Add(typeof(S), lowFilter); - } - if (!highFilters.TryGetValue(typeof(S), out object highFilter)) - { - highFilter = new ButterworthFilter.FilterButterworth(); - highFilters.Add(typeof(S), highFilter); - } - - ((ButterworthFilter.FilterButterworth)lowFilter).UpdateCoefficients(HighFrequency, Engine.AudioSystem.SampleRate, ButterworthFilter.FilterButterworth.PassType.Lowpass, Resonance); - ((ButterworthFilter.FilterButterworth)highFilter).UpdateCoefficients(LowFrequency, Engine.AudioSystem.SampleRate, ButterworthFilter.FilterButterworth.PassType.Highpass, Resonance); - - for (int i = 0; i < tempBuffer.Length; i++) - { - ((ButterworthFilter.FilterButterworth)lowFilter).Update(ref tempBuffer[i]); - ((ButterworthFilter.FilterButterworth)highFilter).Update(ref tempBuffer[i]); - } - - lastTime = Engine.Current.AudioSystem.DSPTime; + _controller.Process(tempBuffer, LowFrequency, HighFrequency, Resonance); } protected override void OnAwake() @@ -77,7 +51,6 @@ protected override void OnAwake() Resonance.Value = 1.41f; LowFrequency.Value = 20f; HighFrequency.Value = 20000f; - lastTime = Engine.Current.AudioSystem.DSPTime; } protected override void OnChanges() @@ -85,8 +58,7 @@ protected override void OnChanges() base.OnChanges(); if (Source.GetWasChangedAndClear()) { - lowFilters.Clear(); - highFilters.Clear(); + _controller.Clear(); } } } diff --git a/ProjectObsidian/Components/Audio/ButterworthFilter.cs b/ProjectObsidian/Components/Audio/ButterworthFilter.cs index 64a34e4..7bf04fa 100644 --- a/ProjectObsidian/Components/Audio/ButterworthFilter.cs +++ b/ProjectObsidian/Components/Audio/ButterworthFilter.cs @@ -1,11 +1,11 @@ using System; using FrooxEngine; using Elements.Assets; -using System.Collections.Generic; +using Obsidian.Elements; namespace Obsidian.Components.Audio; -[Category(new string[] { "Obsidian/Audio" })] +[Category(new string[] { "Obsidian/Audio/Filters" })] public class ButterworthFilter : Component, IAudioSource, IWorldElement { [Range(20f, 20000f, "0.00")] @@ -18,9 +18,7 @@ public class ButterworthFilter : Component, IAudioSource, IWorldElement public readonly SyncRef Source; - private double lastTime; - - private Dictionary filters = new(); + private ButterworthFilterController _controller = new(); public bool IsActive { @@ -33,7 +31,6 @@ public bool IsActive protected override void OnAwake() { base.OnAwake(); - lastTime = Engine.Current.AudioSystem.DSPTime; Frequency.Value = 20f; Resonance.Value = 1.41f; } @@ -43,7 +40,7 @@ protected override void OnChanges() base.OnChanges(); if (Source.GetWasChangedAndClear()) { - filters.Clear(); + _controller.Clear(); } } @@ -54,7 +51,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample if (!IsActive) { buffer.Fill(default(S)); - filters.Clear(); + _controller.Clear(); return; } @@ -64,99 +61,6 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample Source.Target.Read(span); - if (!filters.TryGetValue(typeof(S), out object filter)) { - filter = new FilterButterworth(); - filters.Add(typeof(S), filter); - } - - ((FilterButterworth)filter).UpdateCoefficients(Frequency, Engine.AudioSystem.SampleRate, LowPass ? FilterButterworth.PassType.Lowpass : FilterButterworth.PassType.Highpass, Resonance); - - for (int i = 0; i < span.Length; i++) - { - ((FilterButterworth)filter).Update(ref span[i]); - } - } - - 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 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 void UpdateCoefficients(float frequency, int sampleRate, PassType passType, float resonance) - { - 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); - - for (int i = 0; i < final.ChannelCount; i++) - { - if (final[i] > 1f) final = final.SetChannel(i, 1f); - else if (final[i] < -1f) final = final.SetChannel(i, -1f); - } - - 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]; } - } + _controller.Process(span, LowPass, Frequency, Resonance); } } \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs index 7d49be5..4d8c1dd 100644 --- a/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs +++ b/ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs @@ -1,10 +1,11 @@ using System; using FrooxEngine; using Elements.Assets; +using Obsidian.Elements; namespace Obsidian.Components.Audio; -[Category(new string[] { "Obsidian/Audio" })] +[Category(new string[] { "Obsidian/Audio/Effects" })] public class EMA_IIR_SmoothSignal : Component, IAudioSource, IWorldElement { [Range(0f, 1f, "0.00")] @@ -36,26 +37,6 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample 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; - } + Algorithms.EMAIIRSmoothSignal(ref span, span.Length, SmoothingFactor); } } \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/PhaseModulator.cs b/ProjectObsidian/Components/Audio/PhaseModulator.cs index ce070b1..a9c38c1 100644 --- a/ProjectObsidian/Components/Audio/PhaseModulator.cs +++ b/ProjectObsidian/Components/Audio/PhaseModulator.cs @@ -1,11 +1,12 @@ using System; using FrooxEngine; using Elements.Assets; -using System.Threading; +using Obsidian.Elements; +using Elements.Core; namespace Obsidian.Components.Audio { - [Category(new string[] { "Obsidian/Audio" })] + [Category(new string[] { "Obsidian/Audio/Effects" })] public class PhaseModulator : Component, IAudioSource, IWorldElement { [Range(0f, 5f, "0.00")] @@ -29,7 +30,7 @@ public int ChannelCount { get { - return CarrierSource.Target?.ChannelCount ?? 0; + return MathX.Min(CarrierSource.Target?.ChannelCount ?? 0, ModulatorSource.Target?.ChannelCount ?? 0); } } @@ -39,43 +40,6 @@ protected override void OnAwake() ModulationIndex.Value = 1f; // Default modulation index } - /// - /// Calculates instantaneous phase of a signal using a simple Hilbert transform approximation - /// - private double[] CalculateInstantaneousPhase(Span buffer) where S : unmanaged, IAudioSample - { - int length = buffer.Length; - double[] phase = new double[length]; - double[] avgAmplitudes = new double[length]; - - for (int i = 1; i < length - 1; i++) - { - for (int j = 0; j < buffer[i].ChannelCount; j++) - { - avgAmplitudes[i] += buffer[i][j]; - } - avgAmplitudes[i] /= buffer[i].ChannelCount; - } - - // Simple 3-point derivative for phase approximation - for (int i = 1; i < length - 1; i++) - { - double derivative = (avgAmplitudes[i + 1] - avgAmplitudes[i - 1]) / 2.0; - double hilbertApprox = avgAmplitudes[i] / Math.Sqrt(avgAmplitudes[i] * avgAmplitudes[i] + derivative * derivative); - phase[i] = Math.Acos(hilbertApprox); - - // Correct phase quadrant based on derivative sign - if (derivative < 0) - phase[i] = 2 * Math.PI - phase[i]; - } - - // Handle edge cases - phase[0] = phase[1]; - phase[length - 1] = phase[length - 2]; - - return phase; - } - // TODO: Make this not click when the signal goes silent and then not silent? public void Read(Span buffer) where S : unmanaged, IAudioSample { @@ -100,25 +64,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample float modulationIndex = ModulationIndex.Value; - double[] carrierPhase = CalculateInstantaneousPhase(carrierBuffer); - - // Apply phase modulation - for (int i = 0; i < buffer.Length; i++) - { - for (int j = 0; j < buffer[i].ChannelCount; j++) - { - double modifiedPhase = carrierPhase[i] + (modulationIndex * modulatorBuffer[i][j]); - - // Calculate amplitude using original carrier amplitude - float amplitude = carrierBuffer[i][j]; - - // Generate output sample - buffer[i] = buffer[i].SetChannel(j, amplitude * (float)Math.Sin(modifiedPhase)); - - if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f); - if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f); - } - } + Algorithms.PhaseModulation(buffer, carrierBuffer, modulatorBuffer, modulationIndex, channelCount); } } } \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/RingModulator.cs b/ProjectObsidian/Components/Audio/RingModulator.cs index b51f991..ed5516c 100644 --- a/ProjectObsidian/Components/Audio/RingModulator.cs +++ b/ProjectObsidian/Components/Audio/RingModulator.cs @@ -2,10 +2,11 @@ using FrooxEngine; using Elements.Assets; using Elements.Core; +using Obsidian.Elements; namespace Obsidian.Components.Audio { - [Category(new string[] { "Obsidian/Audio" })] + [Category(new string[] { "Obsidian/Audio/Effects" })] public class RingModulator : Component, IAudioSource, IWorldElement { [Range(0f, 5f, "0.00")] @@ -29,7 +30,7 @@ public int ChannelCount { get { - return CarrierSource.Target?.ChannelCount ?? 0; + return MathX.Min(CarrierSource.Target?.ChannelCount ?? 0, ModulatorSource.Target?.ChannelCount ?? 0); } } @@ -64,22 +65,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample float modulationIndex = ModulationIndex.Value; - // Apply ring modulation - for (int i = 0; i < buffer.Length; i++) - { - for (int j = 0; j < buffer[i].ChannelCount; j++) - { - float carrierValue = carrierBuffer[i][j]; - float modulatorValue = modulatorBuffer[i][j]; - - float modulatedValue = (float)(carrierValue * modulatorValue * modulationIndex); - - buffer[i] = buffer[i].SetChannel(j, modulatedValue); - - if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f); - if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f); - } - } + Algorithms.RingModulation(buffer, carrierBuffer, modulatorBuffer, modulationIndex, channelCount); } } } \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs b/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs index 04bea86..dd54820 100644 --- a/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs +++ b/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs @@ -2,10 +2,11 @@ using FrooxEngine; using Elements.Assets; using Elements.Core; +using Obsidian.Elements; namespace Obsidian.Components.Audio { - [Category(new string[] { "Obsidian/Audio" })] + [Category(new string[] { "Obsidian/Audio/Effects" })] [OldTypeName("Obsidian.Components.Audio.FrequencyModulator")] public class SineShapedRingModulator : Component, IAudioSource, IWorldElement { @@ -30,7 +31,7 @@ public int ChannelCount { get { - return CarrierSource.Target?.ChannelCount ?? 0; + return MathX.Min(CarrierSource.Target?.ChannelCount ?? 0, ModulatorSource.Target?.ChannelCount ?? 0); } } @@ -65,22 +66,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample float modulationIndex = ModulationIndex.Value; - // Apply sine-shaped ring modulation - for (int i = 0; i < buffer.Length; i++) - { - for (int j = 0; j < buffer[i].ChannelCount; j++) - { - float carrierValue = carrierBuffer[i][j]; - float modulatorValue = modulatorBuffer[i][j]; - - float modulatedValue = (float)(carrierValue * Math.Sin(2 * Math.PI * modulationIndex * modulatorValue)); - - buffer[i] = buffer[i].SetChannel(j, modulatedValue); - - if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f); - if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f); - } - } + Algorithms.SineShapedRingModulation(buffer, carrierBuffer, modulatorBuffer, modulationIndex, channelCount); } } } \ No newline at end of file diff --git a/ProjectObsidian/Elements/Audio.cs b/ProjectObsidian/Elements/Audio.cs new file mode 100644 index 0000000..1d07767 --- /dev/null +++ b/ProjectObsidian/Elements/Audio.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Elements.Assets; +using Elements.Core; +using FrooxEngine; + +namespace Obsidian.Elements; + +public class FilterButterworth where S : unmanaged, IAudioSample +{ + /// resonance amount goes from sqrt(2) to ~ 0.1 + + private 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 void UpdateCoefficients(float frequency, int sampleRate, PassType passType, float resonance) + { + 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); + + for (int i = 0; i < final.ChannelCount; i++) + { + if (final[i] > 1f) final = final.SetChannel(i, 1f); + else if (final[i] < -1f) final = final.SetChannel(i, -1f); + } + + 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]; } + } +} + +public class ButterworthFilterController +{ + private Dictionary filters = new(); + + public void Clear() + { + filters.Clear(); + } + + public void Process(Span buffer, bool lowPass, float freq, float resonance) where S : unmanaged, IAudioSample + { + // avoid dividing by zero + if (freq == 0f) + { + buffer.Fill(default(S)); + Clear(); + return; + } + + if (!filters.TryGetValue(typeof(S), out object filter)) + { + filter = new FilterButterworth(); + filters.Add(typeof(S), filter); + } + + ((FilterButterworth)filter).UpdateCoefficients(freq, Engine.Current.AudioSystem.SampleRate, lowPass ? FilterButterworth.PassType.Lowpass : FilterButterworth.PassType.Highpass, resonance); + + for (int i = 0; i < buffer.Length; i++) + { + ((FilterButterworth)filter).Update(ref buffer[i]); + } + } +} + +public class BandPassFilterController +{ + private Dictionary lowFilters = new(); + private Dictionary highFilters = new(); + + public void Clear() + { + lowFilters.Clear(); + highFilters.Clear(); + } + + public void Process(Span buffer, float lowFreq, float highFreq, float resonance) where S : unmanaged, IAudioSample + { + // avoid dividing by zero + if (lowFreq == 0f || highFreq == 0f) + { + buffer.Fill(default(S)); + Clear(); + return; + } + + if (!lowFilters.TryGetValue(typeof(S), out object lowFilter)) + { + lowFilter = new FilterButterworth(); + lowFilters.Add(typeof(S), lowFilter); + } + if (!highFilters.TryGetValue(typeof(S), out object highFilter)) + { + highFilter = new FilterButterworth(); + highFilters.Add(typeof(S), highFilter); + } + + ((FilterButterworth)lowFilter).UpdateCoefficients(highFreq, Engine.Current.AudioSystem.SampleRate, FilterButterworth.PassType.Lowpass, resonance); + ((FilterButterworth)highFilter).UpdateCoefficients(lowFreq, Engine.Current.AudioSystem.SampleRate, FilterButterworth.PassType.Highpass, resonance); + + for (int i = 0; i < buffer.Length; i++) + { + ((FilterButterworth)lowFilter).Update(ref buffer[i]); + ((FilterButterworth)highFilter).Update(ref buffer[i]); + } + } +} + +public static class Algorithms +{ + public static void SineShapedRingModulation(Span buffer, Span input1, Span input2, float modulationIndex, int channelCount) where S : unmanaged, IAudioSample + { + // Apply sine-shaped ring modulation + for (int i = 0; i < buffer.Length; i++) + { + for (int j = 0; j < channelCount; j++) + { + float carrierValue = input1[i][j]; + float modulatorValue = input2[i][j]; + + float modulatedValue = (float)(carrierValue * Math.Sin(2 * Math.PI * modulationIndex * modulatorValue)); + + buffer[i] = buffer[i].SetChannel(j, modulatedValue); + + if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f); + if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f); + } + } + } + + /// + /// Calculates instantaneous phase of a signal using a simple Hilbert transform approximation + /// + private static double[] CalculateInstantaneousPhase(Span buffer) where S : unmanaged, IAudioSample + { + int length = buffer.Length; + double[] phase = new double[length]; + double[] avgAmplitudes = new double[length]; + + for (int i = 1; i < length - 1; i++) + { + for (int j = 0; j < buffer[i].ChannelCount; j++) + { + avgAmplitudes[i] += buffer[i][j]; + } + avgAmplitudes[i] /= buffer[i].ChannelCount; + } + + // Simple 3-point derivative for phase approximation + for (int i = 1; i < length - 1; i++) + { + double derivative = (avgAmplitudes[i + 1] - avgAmplitudes[i - 1]) / 2.0; + double hilbertApprox = avgAmplitudes[i] / Math.Sqrt(avgAmplitudes[i] * avgAmplitudes[i] + derivative * derivative); + phase[i] = Math.Acos(hilbertApprox); + + // Correct phase quadrant based on derivative sign + if (derivative < 0) + phase[i] = 2 * Math.PI - phase[i]; + } + + // Handle edge cases + phase[0] = phase[1]; + phase[length - 1] = phase[length - 2]; + + return phase; + } + + public static void PhaseModulation(Span buffer, Span input1, Span input2, float modulationIndex, int channelCount) where S : unmanaged, IAudioSample + { + double[] carrierPhase = CalculateInstantaneousPhase(input1); + + // Apply phase modulation + for (int i = 0; i < buffer.Length; i++) + { + for (int j = 0; j < channelCount; j++) + { + double modifiedPhase = carrierPhase[i] + (modulationIndex * input2[i][j]); + + // Calculate amplitude using original carrier amplitude + float amplitude = input1[i][j]; + + // Generate output sample + buffer[i] = buffer[i].SetChannel(j, amplitude * (float)Math.Sin(modifiedPhase)); + + if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f); + if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f); + } + } + } + + public static void RingModulation(Span buffer, Span input1, Span input2, float modulationIndex, int channelCount) where S : unmanaged, IAudioSample + { + // Apply ring modulation + for (int i = 0; i < buffer.Length; i++) + { + for (int j = 0; j < channelCount; j++) + { + float carrierValue = input1[i][j]; + float modulatorValue = input2[i][j]; + + float modulatedValue = (float)(carrierValue * modulatorValue * modulationIndex); + + buffer[i] = buffer[i].SetChannel(j, modulatedValue); + + if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f); + if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f); + } + } + } + + // smoothingFactor is between 0.0 (no smoothing) and 0.9999.. (almost smoothing to DC) - *kind* of the inverse of cutoff frequency + public static 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/ProtoFlux/Audio/AudioAdder.cs b/ProjectObsidian/ProtoFlux/Audio/AudioAdder.cs index bae1beb..7ed9c25 100644 --- a/ProjectObsidian/ProtoFlux/Audio/AudioAdder.cs +++ b/ProjectObsidian/ProtoFlux/Audio/AudioAdder.cs @@ -17,7 +17,7 @@ public class AudioAdderProxy : ProtoFluxEngineProxy, IAudioSource public bool IsActive => Active; - public int ChannelCount => 1; + public int ChannelCount => AudioInput.ChannelCount; public void Read(Span buffer) where S : unmanaged, IAudioSample { @@ -50,7 +50,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample { newBuffer[i] = newBuffer[i].Add(newBuffer2[i]); - for (int j = 0; j < newBuffer[i].ChannelCount; j++) + for (int j = 0; j < ChannelCount; j++) { if (newBuffer[i][j] > 1f) newBuffer[i] = newBuffer[i].SetChannel(j, 1f); else if (newBuffer[i][j] < -1f) newBuffer[i] = newBuffer[i].SetChannel(j, -1f); diff --git a/ProjectObsidian/ProtoFlux/Audio/AudioMultiply.cs b/ProjectObsidian/ProtoFlux/Audio/AudioMultiply.cs index 5221353..cb5c57d 100644 --- a/ProjectObsidian/ProtoFlux/Audio/AudioMultiply.cs +++ b/ProjectObsidian/ProtoFlux/Audio/AudioMultiply.cs @@ -4,7 +4,6 @@ using FrooxEngine.ProtoFlux; using FrooxEngine; using Elements.Assets; -using Elements.Core; namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio { @@ -18,7 +17,7 @@ public class AudioMultiplyProxy : ProtoFluxEngineProxy, IAudioSource public bool IsActive => Active; - public int ChannelCount => 1; + public int ChannelCount => AudioInput.ChannelCount; public void Read(Span buffer) where S : unmanaged, IAudioSample { @@ -42,7 +41,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample { newBuffer[i] = newBuffer[i].Multiply(Value); - for (int j = 0; j < newBuffer[i].ChannelCount; j++) + for (int j = 0; j < ChannelCount; j++) { if (newBuffer[i][j] > 1f) newBuffer[i] = newBuffer[i].SetChannel(j, 1f); if (newBuffer[i][j] < -1f) newBuffer[i] = newBuffer[i].SetChannel(j, -1f); diff --git a/ProjectObsidian/ProtoFlux/Audio/AudioSourceInfo.cs b/ProjectObsidian/ProtoFlux/Audio/AudioSourceInfo.cs new file mode 100644 index 0000000..87f91a3 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/AudioSourceInfo.cs @@ -0,0 +1,40 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + [NodeCategory("Obsidian/Audio")] + public class AudioSourceInfo : VoidNode + { + public readonly ObjectInput Source; + + [ContinuouslyChanging] + public readonly ValueOutput IsActive; + + public readonly ValueOutput ChannelCount; + + protected override void ComputeOutputs(FrooxEngineContext context) + { + IAudioSource source = Source.Evaluate(context); + if (source != null) + { + IsActive.Write(source.IsActive, context); + ChannelCount.Write(source.ChannelCount, context); + } + else + { + IsActive.Write(false, context); + ChannelCount.Write(0, context); + } + } + + public AudioSourceInfo() + { + IsActive = new ValueOutput(this); + ChannelCount = new ValueOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/AudioSubtractor.cs b/ProjectObsidian/ProtoFlux/Audio/AudioSubtractor.cs index 3bd0e5b..112e452 100644 --- a/ProjectObsidian/ProtoFlux/Audio/AudioSubtractor.cs +++ b/ProjectObsidian/ProtoFlux/Audio/AudioSubtractor.cs @@ -17,7 +17,7 @@ public class AudioSubtractorProxy : ProtoFluxEngineProxy, IAudioSource public bool IsActive => Active; - public int ChannelCount => 1; + public int ChannelCount => AudioInput.ChannelCount; public void Read(Span buffer) where S : unmanaged, IAudioSample { @@ -50,7 +50,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample { newBuffer[i] = newBuffer[i].Subtract(newBuffer2[i]); - for (int j = 0; j < newBuffer[i].ChannelCount; j++) + for (int j = 0; j < ChannelCount; j++) { if (newBuffer[i][j] > 1f) newBuffer[i] = newBuffer[i].SetChannel(j, 1f); else if (newBuffer[i][j] < -1f) newBuffer[i] = newBuffer[i].SetChannel(j, -1f); diff --git a/ProjectObsidian/ProtoFlux/Audio/BandPassFilterNode.cs b/ProjectObsidian/ProtoFlux/Audio/BandPassFilterNode.cs new file mode 100644 index 0000000..4edeb13 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/BandPassFilterNode.cs @@ -0,0 +1,165 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; +using Obsidian.Elements; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class BandPassFilterProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public float LowFrequency; + + public float HighFrequency; + + public float Resonance; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => AudioInput.ChannelCount; + + private BandPassFilterController _controller = new(); + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + newBuffer = buffer; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + + _controller.Process(newBuffer, LowFrequency, HighFrequency, Resonance); + } + } + [NodeCategory("Obsidian/Audio/Filters")] + public class BandPassFilter : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + [DefaultValueAttribute(20f)] + public readonly ValueInput LowFrequency; + + [ChangeListener] + [DefaultValueAttribute(20000f)] + public readonly ValueInput HighFrequency; + + [ChangeListener] + [DefaultValueAttribute(1.41f)] + public readonly ValueInput Resonance; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(BandPassFilterProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(BandPassFilterProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(BandPassFilterProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + BandPassFilterProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + BandPassFilterProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.LowFrequency = LowFrequency.Evaluate(context, 20f); + proxy.HighFrequency = HighFrequency.Evaluate(context, 20000f); + proxy.Resonance = Resonance.Evaluate(context, 1.41f); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + BandPassFilterProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public BandPassFilter() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/ButterworthFilterNode.cs b/ProjectObsidian/ProtoFlux/Audio/ButterworthFilterNode.cs new file mode 100644 index 0000000..ac7016d --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/ButterworthFilterNode.cs @@ -0,0 +1,164 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; +using Obsidian.Elements; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class ButterworthFilterProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public bool LowPass; + + public float Frequency; + + public float Resonance; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => AudioInput.ChannelCount; + + private ButterworthFilterController _controller = new(); + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + newBuffer = buffer; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + + _controller.Process(newBuffer, LowPass, Frequency, Resonance); + } + } + [NodeCategory("Obsidian/Audio/Filters")] + public class ButterworthFilter : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + public readonly ValueInput LowPass; + + [ChangeListener] + [DefaultValueAttribute(20f)] + public readonly ValueInput Frequency; + + [ChangeListener] + [DefaultValueAttribute(1.41f)] + public readonly ValueInput Resonance; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(ButterworthFilterProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(ButterworthFilterProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(ButterworthFilterProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + ButterworthFilterProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + ButterworthFilterProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.LowPass = LowPass.Evaluate(context); + proxy.Frequency = Frequency.Evaluate(context, 20f); + proxy.Resonance = Resonance.Evaluate(context, 1.41f); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + ButterworthFilterProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public ButterworthFilter() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/ChannelSplitter.cs b/ProjectObsidian/ProtoFlux/Audio/ChannelSplitter.cs new file mode 100644 index 0000000..fa7d71d --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/ChannelSplitter.cs @@ -0,0 +1,155 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class ChannelSplitterProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public int Channel; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => 1; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + if (AudioInput.ChannelCount < Channel + 1 || Channel < 0) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = buffer[i].SetChannel(Channel, newBuffer[i][Channel]); + } + } + } + [NodeCategory("Obsidian/Audio")] + public class ChannelSplitter : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + public readonly ValueInput Channel; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(ChannelSplitterProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(ChannelSplitterProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(ChannelSplitterProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + ChannelSplitterProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + ChannelSplitterProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.Channel = Channel.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + ChannelSplitterProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public ChannelSplitter() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/EMA_IIR_SmoothSignalNode.cs b/ProjectObsidian/ProtoFlux/Audio/EMA_IIR_SmoothSignalNode.cs new file mode 100644 index 0000000..29820aa --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/EMA_IIR_SmoothSignalNode.cs @@ -0,0 +1,148 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; +using Obsidian.Elements; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class EMA_IIR_SmoothSignalProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public float SmoothingFactor; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => AudioInput.ChannelCount; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + newBuffer = buffer; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + + Algorithms.EMAIIRSmoothSignal(ref newBuffer, newBuffer.Length, SmoothingFactor); + } + } + [NodeCategory("Obsidian/Audio/Filters")] + public class EMA_IIR_SmoothSignal : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + public readonly ValueInput SmoothingFactor; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(EMA_IIR_SmoothSignalProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(EMA_IIR_SmoothSignalProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(EMA_IIR_SmoothSignalProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + EMA_IIR_SmoothSignalProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + EMA_IIR_SmoothSignalProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.SmoothingFactor = SmoothingFactor.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + EMA_IIR_SmoothSignalProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public EMA_IIR_SmoothSignal() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs b/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs new file mode 100644 index 0000000..e0e48f0 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs @@ -0,0 +1,164 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; +using Elements.Core; +using Obsidian.Elements; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class PhaseModulatorProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public IAudioSource AudioInput2; + + public float ModulationIndex; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => MathX.Min(AudioInput.ChannelCount, AudioInput2.ChannelCount); + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + newBuffer = buffer; + Span newBuffer2 = stackalloc S[buffer.Length]; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + if (AudioInput2 != null) + { + AudioInput2.Read(newBuffer2); + } + else + { + newBuffer2.Fill(default); + } + + Algorithms.PhaseModulation(buffer, newBuffer, newBuffer2, ModulationIndex, ChannelCount); + } + } + [NodeCategory("Obsidian/Audio/Effects")] + public class PhaseModulator : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + public readonly ObjectInput AudioInput2; + + [ChangeListener] + public readonly ValueInput ModulationIndex; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(PhaseModulatorProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(PhaseModulatorProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(PhaseModulatorProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + PhaseModulatorProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + PhaseModulatorProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.AudioInput2 = AudioInput2.Evaluate(context); + proxy.ModulationIndex = ModulationIndex.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + PhaseModulatorProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public PhaseModulator() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/QuadCombiner.cs b/ProjectObsidian/ProtoFlux/Audio/QuadCombiner.cs new file mode 100644 index 0000000..ea32781 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/QuadCombiner.cs @@ -0,0 +1,193 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class QuadCombinerProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource LeftFront; + + public IAudioSource RightFront; + + public IAudioSource LeftRear; + + public IAudioSource RightRear; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => 4; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span samples = stackalloc QuadSample[buffer.Length]; + Span leftFrontBuf = stackalloc S[buffer.Length]; + Span rightFrontBuf = stackalloc S[buffer.Length]; + Span leftRearBuf = stackalloc S[buffer.Length]; + Span rightRearBuf = stackalloc S[buffer.Length]; + if (LeftFront != null) + { + LeftFront.Read(leftFrontBuf); + } + else + { + leftFrontBuf.Fill(default); + } + if (RightFront != null) + { + RightFront.Read(rightFrontBuf); + } + else + { + rightFrontBuf.Fill(default); + } + if (LeftRear != null) + { + LeftRear.Read(leftRearBuf); + } + else + { + leftRearBuf.Fill(default); + } + if (RightRear != null) + { + RightRear.Read(rightRearBuf); + } + else + { + rightRearBuf.Fill(default); + } + + for (int i = 0; i < buffer.Length; i++) + { + samples[i] = new QuadSample(leftFrontBuf[i][0], rightFrontBuf[i][0], leftRearBuf[i][0], rightRearBuf[i][0]); + } + + double position = 0.0; + QuadSample lastSample = default(QuadSample); + samples.CopySamples(buffer, ref position, ref lastSample); + } + } + [NodeCategory("Obsidian/Audio")] + public class QuadCombiner : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput LeftFront; + + [ChangeListener] + public readonly ObjectInput RightFront; + + [ChangeListener] + public readonly ObjectInput LeftRear; + + [ChangeListener] + public readonly ObjectInput RightRear; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(QuadCombinerProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(QuadCombinerProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(QuadCombinerProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + QuadCombinerProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + QuadCombinerProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.LeftFront = LeftFront.Evaluate(context); + proxy.RightFront = RightFront.Evaluate(context); + proxy.LeftRear = LeftRear.Evaluate(context); + proxy.RightRear = RightRear.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + QuadCombinerProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public QuadCombiner() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs b/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs new file mode 100644 index 0000000..c2f23e6 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs @@ -0,0 +1,165 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; +using Obsidian.Components.Audio; +using Elements.Core; +using Obsidian.Elements; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class RingModulatorProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public IAudioSource AudioInput2; + + public float ModulationIndex; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => MathX.Min(AudioInput.ChannelCount, AudioInput2.ChannelCount); + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + newBuffer = buffer; + Span newBuffer2 = stackalloc S[buffer.Length]; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + if (AudioInput2 != null) + { + AudioInput2.Read(newBuffer2); + } + else + { + newBuffer2.Fill(default); + } + + Algorithms.RingModulation(buffer, newBuffer, newBuffer2, ModulationIndex, ChannelCount); + } + } + [NodeCategory("Obsidian/Audio/Effects")] + public class RingModulator : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + public readonly ObjectInput AudioInput2; + + [ChangeListener] + public readonly ValueInput ModulationIndex; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(RingModulatorProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(RingModulatorProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(RingModulatorProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + RingModulatorProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + RingModulatorProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.AudioInput2 = AudioInput2.Evaluate(context); + proxy.ModulationIndex = ModulationIndex.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + RingModulatorProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public RingModulator() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/SawtoothGenerator.cs b/ProjectObsidian/ProtoFlux/Audio/SawtoothGenerator.cs index e288a05..5da7322 100644 --- a/ProjectObsidian/ProtoFlux/Audio/SawtoothGenerator.cs +++ b/ProjectObsidian/ProtoFlux/Audio/SawtoothGenerator.cs @@ -67,7 +67,7 @@ protected override void OnStart() }; } } - [NodeCategory("Obsidian/Audio")] + [NodeCategory("Obsidian/Audio/Generators")] public class SawtoothGenerator : ProxyVoidNode, IExecutionChangeListener { [ChangeListener] diff --git a/ProjectObsidian/ProtoFlux/Audio/SineGenerator.cs b/ProjectObsidian/ProtoFlux/Audio/SineGenerator.cs index 649d872..7b0bddb 100644 --- a/ProjectObsidian/ProtoFlux/Audio/SineGenerator.cs +++ b/ProjectObsidian/ProtoFlux/Audio/SineGenerator.cs @@ -65,7 +65,7 @@ protected override void OnStart() }; } } - [NodeCategory("Obsidian/Audio")] + [NodeCategory("Obsidian/Audio/Generators")] public class SineGenerator : ProxyVoidNode, IExecutionChangeListener { [ChangeListener] diff --git a/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs b/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs new file mode 100644 index 0000000..8df9049 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs @@ -0,0 +1,164 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; +using Elements.Core; +using Obsidian.Elements; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class SineShapedRingModulatorProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource AudioInput; + + public IAudioSource AudioInput2; + + public float ModulationIndex; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => MathX.Min(AudioInput.ChannelCount, AudioInput2.ChannelCount); + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span newBuffer = stackalloc S[buffer.Length]; + newBuffer = buffer; + Span newBuffer2 = stackalloc S[buffer.Length]; + if (AudioInput != null) + { + AudioInput.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + if (AudioInput2 != null) + { + AudioInput2.Read(newBuffer2); + } + else + { + newBuffer2.Fill(default); + } + + Algorithms.SineShapedRingModulation(buffer, newBuffer, newBuffer2, ModulationIndex, ChannelCount); + } + } + [NodeCategory("Obsidian/Audio/Effects")] + public class SineShapedRingModulator : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput AudioInput; + + [ChangeListener] + public readonly ObjectInput AudioInput2; + + [ChangeListener] + public readonly ValueInput ModulationIndex; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(SineShapedRingModulatorProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(SineShapedRingModulatorProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(SineShapedRingModulatorProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + SineShapedRingModulatorProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + SineShapedRingModulatorProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.AudioInput = AudioInput.Evaluate(context); + proxy.AudioInput2 = AudioInput2.Evaluate(context); + proxy.ModulationIndex = ModulationIndex.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + SineShapedRingModulatorProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public SineShapedRingModulator() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/SquareGenerator.cs b/ProjectObsidian/ProtoFlux/Audio/SquareGenerator.cs index d483e95..00ff6fd 100644 --- a/ProjectObsidian/ProtoFlux/Audio/SquareGenerator.cs +++ b/ProjectObsidian/ProtoFlux/Audio/SquareGenerator.cs @@ -76,7 +76,7 @@ protected override void OnStart() }; } } - [NodeCategory("Obsidian/Audio")] + [NodeCategory("Obsidian/Audio/Generators")] public class SquareGenerator : ProxyVoidNode, IExecutionChangeListener { [ChangeListener] diff --git a/ProjectObsidian/ProtoFlux/Audio/StereoCombiner.cs b/ProjectObsidian/ProtoFlux/Audio/StereoCombiner.cs new file mode 100644 index 0000000..964ed44 --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/StereoCombiner.cs @@ -0,0 +1,163 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class StereoCombinerProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource Left; + + public IAudioSource Right; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => 2; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span samples = stackalloc StereoSample[buffer.Length]; + Span newBuffer = stackalloc S[buffer.Length]; + Span newBuffer2 = stackalloc S[buffer.Length]; + if (Left != null) + { + Left.Read(newBuffer); + } + else + { + newBuffer.Fill(default); + } + if (Right != null) + { + Right.Read(newBuffer2); + } + else + { + newBuffer2.Fill(default); + } + + for (int i = 0; i < buffer.Length; i++) + { + samples[i] = new StereoSample(newBuffer[i][0], newBuffer2[i][0]); + } + + double position = 0.0; + StereoSample lastSample = default(StereoSample); + samples.CopySamples(buffer, ref position, ref lastSample); + } + } + [NodeCategory("Obsidian/Audio")] + public class StereoCombiner : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput Left; + + [ChangeListener] + public readonly ObjectInput Right; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(StereoCombinerProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(StereoCombinerProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(StereoCombinerProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + StereoCombinerProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + StereoCombinerProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.Left = Left.Evaluate(context); + proxy.Right = Right.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + StereoCombinerProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public StereoCombiner() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/Surround51_Combiner.cs b/ProjectObsidian/ProtoFlux/Audio/Surround51_Combiner.cs new file mode 100644 index 0000000..042567f --- /dev/null +++ b/ProjectObsidian/ProtoFlux/Audio/Surround51_Combiner.cs @@ -0,0 +1,223 @@ +using System; +using ProtoFlux.Core; +using ProtoFlux.Runtimes.Execution; +using FrooxEngine.ProtoFlux; +using FrooxEngine; +using Elements.Assets; + +namespace ProtoFlux.Runtimes.Execution.Nodes.Obsidian.Audio +{ + public class Surround51_CombinerProxy : ProtoFluxEngineProxy, IAudioSource + { + public IAudioSource LeftFront; + + public IAudioSource RightFront; + + public IAudioSource Center; + + public IAudioSource Subwoofer; + + public IAudioSource LeftRear; + + public IAudioSource RightRear; + + public bool Active; + + public bool IsActive => Active; + + public int ChannelCount => 6; + + public void Read(Span buffer) where S : unmanaged, IAudioSample + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + Span samples = stackalloc Surround51Sample[buffer.Length]; + Span leftFrontBuf = stackalloc S[buffer.Length]; + Span rightFrontBuf = stackalloc S[buffer.Length]; + Span centerBuf = stackalloc S[buffer.Length]; + Span subwooferBuf = stackalloc S[buffer.Length]; + Span leftRearBuf = stackalloc S[buffer.Length]; + Span rightRearBuf = stackalloc S[buffer.Length]; + if (LeftFront != null) + { + LeftFront.Read(leftFrontBuf); + } + else + { + leftFrontBuf.Fill(default); + } + if (RightFront != null) + { + RightFront.Read(rightFrontBuf); + } + else + { + rightFrontBuf.Fill(default); + } + if (Center != null) + { + Center.Read(centerBuf); + } + else + { + centerBuf.Fill(default); + } + if (Subwoofer != null) + { + Subwoofer.Read(subwooferBuf); + } + else + { + subwooferBuf.Fill(default); + } + if (LeftRear != null) + { + LeftRear.Read(leftRearBuf); + } + else + { + leftRearBuf.Fill(default); + } + if (RightRear != null) + { + RightRear.Read(rightRearBuf); + } + else + { + rightRearBuf.Fill(default); + } + + for (int i = 0; i < buffer.Length; i++) + { + samples[i] = new Surround51Sample(leftFrontBuf[i][0], rightFrontBuf[i][0], centerBuf[i][0], subwooferBuf[i][0], leftRearBuf[i][0], rightRearBuf[i][0]); + } + + double position = 0.0; + Surround51Sample lastSample = default(Surround51Sample); + samples.CopySamples(buffer, ref position, ref lastSample); + } + } + [NodeCategory("Obsidian/Audio")] + public class Surround51_Combiner : ProxyVoidNode, IExecutionChangeListener + { + [ChangeListener] + public readonly ObjectInput LeftFront; + + [ChangeListener] + public readonly ObjectInput RightFront; + + [ChangeListener] + public readonly ObjectInput Center; + + [ChangeListener] + public readonly ObjectInput Subwoofer; + + [ChangeListener] + public readonly ObjectInput LeftRear; + + [ChangeListener] + public readonly ObjectInput RightRear; + + public readonly ObjectOutput AudioOutput; + + private ObjectStore> _enabledChangedHandler; + + private ObjectStore _activeChangedHandler; + + public bool ValueListensToChanges { get; private set; } + + private bool ShouldListen(Surround51_CombinerProxy proxy) + { + if (proxy.Enabled) + { + return proxy.Slot.IsActive; + } + return false; + } + + protected override void ProxyAdded(Surround51_CombinerProxy proxy, FrooxEngineContext context) + { + base.ProxyAdded(proxy, context); + NodeContextPath path = context.CaptureContextPath(); + ProtoFluxNodeGroup group = context.Group; + context.GetEventDispatcher(out var dispatcher); + Action enabledHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + SlotEvent activeHandler = delegate + { + dispatcher.ScheduleEvent(path, delegate (FrooxEngineContext c) + { + UpdateListenerState(c); + }); + }; + proxy.EnabledField.Changed += enabledHandler; + proxy.Slot.ActiveChanged += activeHandler; + _enabledChangedHandler.Write(enabledHandler, context); + _activeChangedHandler.Write(activeHandler, context); + ValueListensToChanges = ShouldListen(proxy); + proxy.Active = ValueListensToChanges; + } + + protected override void ProxyRemoved(Surround51_CombinerProxy proxy, FrooxEngineContext context, bool inUseByAnotherInstance) + { + if (!inUseByAnotherInstance) + { + proxy.EnabledField.Changed -= _enabledChangedHandler.Read(context); + proxy.Slot.ActiveChanged -= _activeChangedHandler.Read(context); + _enabledChangedHandler.Clear(context); + _activeChangedHandler.Clear(context); + proxy.Active = false; + } + } + + protected void UpdateListenerState(FrooxEngineContext context) + { + Surround51_CombinerProxy proxy = GetProxy(context); + if (proxy != null) + { + bool shouldListen = ShouldListen(proxy); + if (shouldListen != ValueListensToChanges) + { + ValueListensToChanges = shouldListen; + context.Group.MarkChangeTrackingDirty(); + proxy.Active = shouldListen; + } + } + } + + public void Changed(FrooxEngineContext context) + { + Surround51_CombinerProxy proxy = GetProxy(context); + if (proxy == null) + { + return; + } + proxy.LeftFront = LeftFront.Evaluate(context); + proxy.RightFront = RightFront.Evaluate(context); + proxy.Center = Center.Evaluate(context); + proxy.Subwoofer = Subwoofer.Evaluate(context); + proxy.LeftRear = LeftRear.Evaluate(context); + proxy.RightRear = RightRear.Evaluate(context); + } + + protected override void ComputeOutputs(FrooxEngineContext context) + { + Surround51_CombinerProxy proxy = GetProxy(context); + AudioOutput.Write(proxy, context); + } + + public Surround51_Combiner() + { + AudioOutput = new ObjectOutput(this); + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/ProtoFlux/Audio/TriangleGenerator.cs b/ProjectObsidian/ProtoFlux/Audio/TriangleGenerator.cs index 35433be..0d69279 100644 --- a/ProjectObsidian/ProtoFlux/Audio/TriangleGenerator.cs +++ b/ProjectObsidian/ProtoFlux/Audio/TriangleGenerator.cs @@ -72,7 +72,7 @@ protected override void OnStart() }; } } - [NodeCategory("Obsidian/Audio")] + [NodeCategory("Obsidian/Audio/Generators")] public class TriangleGenerator : ProxyVoidNode, IExecutionChangeListener { [ChangeListener]