Skip to content

Commit

Permalink
Merge pull request #75 from Nytra/audioWork
Browse files Browse the repository at this point in the history
A bunch of new audio nodes and other related changes
  • Loading branch information
Xlinka authored Jan 5, 2025
2 parents 405e5ee + 3363a34 commit db9cfa9
Show file tree
Hide file tree
Showing 25 changed files with 2,068 additions and 264 deletions.
40 changes: 6 additions & 34 deletions ProjectObsidian/Components/Audio/BandPassFilter.cs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -21,10 +19,7 @@ public class BandPassFilter : Component, IAudioSource, IWorldElement

public readonly SyncRef<IAudioSource> Source;

private double lastTime;

private Dictionary<Type, object> lowFilters = new();
private Dictionary<Type, object> highFilters = new();
private BandPassFilterController _controller = new();

public bool IsActive
{
Expand All @@ -38,8 +33,7 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
if (!IsActive)
{
buffer.Fill(default(S));
lowFilters.Clear();
highFilters.Clear();
_controller.Clear();
return;
}

Expand All @@ -48,27 +42,7 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>

Source.Target.Read(tempBuffer);

if (!lowFilters.TryGetValue(typeof(S), out object lowFilter))
{
lowFilter = new ButterworthFilter.FilterButterworth<S>();
lowFilters.Add(typeof(S), lowFilter);
}
if (!highFilters.TryGetValue(typeof(S), out object highFilter))
{
highFilter = new ButterworthFilter.FilterButterworth<S>();
highFilters.Add(typeof(S), highFilter);
}

((ButterworthFilter.FilterButterworth<S>)lowFilter).UpdateCoefficients(HighFrequency, Engine.AudioSystem.SampleRate, ButterworthFilter.FilterButterworth<S>.PassType.Lowpass, Resonance);
((ButterworthFilter.FilterButterworth<S>)highFilter).UpdateCoefficients(LowFrequency, Engine.AudioSystem.SampleRate, ButterworthFilter.FilterButterworth<S>.PassType.Highpass, Resonance);

for (int i = 0; i < tempBuffer.Length; i++)
{
((ButterworthFilter.FilterButterworth<S>)lowFilter).Update(ref tempBuffer[i]);
((ButterworthFilter.FilterButterworth<S>)highFilter).Update(ref tempBuffer[i]);
}

lastTime = Engine.Current.AudioSystem.DSPTime;
_controller.Process(tempBuffer, LowFrequency, HighFrequency, Resonance);
}

protected override void OnAwake()
Expand All @@ -77,16 +51,14 @@ protected override void OnAwake()
Resonance.Value = 1.41f;
LowFrequency.Value = 20f;
HighFrequency.Value = 20000f;
lastTime = Engine.Current.AudioSystem.DSPTime;
}

protected override void OnChanges()
{
base.OnChanges();
if (Source.GetWasChangedAndClear())
{
lowFilters.Clear();
highFilters.Clear();
_controller.Clear();
}
}
}
Expand Down
108 changes: 6 additions & 102 deletions ProjectObsidian/Components/Audio/ButterworthFilter.cs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -18,9 +18,7 @@ public class ButterworthFilter : Component, IAudioSource, IWorldElement

public readonly SyncRef<IAudioSource> Source;

private double lastTime;

private Dictionary<Type, object> filters = new();
private ButterworthFilterController _controller = new();

public bool IsActive
{
Expand All @@ -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;
}
Expand All @@ -43,7 +40,7 @@ protected override void OnChanges()
base.OnChanges();
if (Source.GetWasChangedAndClear())
{
filters.Clear();
_controller.Clear();
}
}

Expand All @@ -54,7 +51,7 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
if (!IsActive)
{
buffer.Fill(default(S));
filters.Clear();
_controller.Clear();
return;
}

Expand All @@ -64,99 +61,6 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>

Source.Target.Read(span);

if (!filters.TryGetValue(typeof(S), out object filter)) {
filter = new FilterButterworth<S>();
filters.Add(typeof(S), filter);
}

((FilterButterworth<S>)filter).UpdateCoefficients(Frequency, Engine.AudioSystem.SampleRate, LowPass ? FilterButterworth<S>.PassType.Lowpass : FilterButterworth<S>.PassType.Highpass, Resonance);

for (int i = 0; i < span.Length; i++)
{
((FilterButterworth<S>)filter).Update(ref span[i]);
}
}

public class FilterButterworth<S> where S: unmanaged, IAudioSample<S>
{
/// <summary>
/// rez amount, from sqrt(2) to ~ 0.1
/// </summary>
private readonly float resonance;

private readonly float frequency;
private readonly int sampleRate;
private readonly PassType passType;

private float c, a1, a2, a3, b1, b2;

/// <summary>
/// Array of input values, latest are in front
/// </summary>
private S[] inputHistory = new S[2];

/// <summary>
/// Array of output values, latest are in front
/// </summary>
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);
}
}
25 changes: 3 additions & 22 deletions ProjectObsidian/Components/Audio/EMA_IIR_SmoothSignal.cs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -36,26 +37,6 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>

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<S>(ref Span<S> input, int N, float smoothingFactor = 0.8f) where S : unmanaged, IAudioSample<S>
{
// 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);
}
}
64 changes: 5 additions & 59 deletions ProjectObsidian/Components/Audio/PhaseModulator.cs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -29,7 +30,7 @@ public int ChannelCount
{
get
{
return CarrierSource.Target?.ChannelCount ?? 0;
return MathX.Min(CarrierSource.Target?.ChannelCount ?? 0, ModulatorSource.Target?.ChannelCount ?? 0);
}
}

Expand All @@ -39,43 +40,6 @@ protected override void OnAwake()
ModulationIndex.Value = 1f; // Default modulation index
}

/// <summary>
/// Calculates instantaneous phase of a signal using a simple Hilbert transform approximation
/// </summary>
private double[] CalculateInstantaneousPhase<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
{
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<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
{
Expand All @@ -100,25 +64,7 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>

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);
}
}
}
Loading

0 comments on commit db9cfa9

Please sign in to comment.