Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PhaseModulator, Add RingModulator, rename FrequencyModulator to SineShapedRingModulator #73

Merged
merged 4 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions ProjectObsidian/Components/Audio/PhaseModulator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using FrooxEngine;
using Elements.Assets;
using System.Threading;

namespace Obsidian.Components.Audio
{
[Category(new string[] { "Obsidian/Audio" })]
public class PhaseModulator : Component, IAudioSource, IWorldElement
{
[Range(0f, 5f, "0.00")]
public readonly Sync<float> ModulationIndex;

public readonly SyncRef<IAudioSource> CarrierSource;
public readonly SyncRef<IAudioSource> ModulatorSource;

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();
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>
{
if (!IsActive)
{
buffer.Fill(default(S));
return;
}

int channelCount = ChannelCount;
if (channelCount == 0)
{
buffer.Fill(default(S));
return;
}

Span<S> carrierBuffer = stackalloc S[buffer.Length];
Span<S> modulatorBuffer = stackalloc S[buffer.Length];

CarrierSource.Target.Read(carrierBuffer);
ModulatorSource.Target.Read(modulatorBuffer);

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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System;
using FrooxEngine;
using Elements.Assets;
using Elements.Core;

namespace Obsidian.Components.Audio
{
[Category(new string[] { "Obsidian/Audio" })]
public class FrequencyModulator : Component, IAudioSource, IWorldElement
public class RingModulator : Component, IAudioSource, IWorldElement
{
[Range(0f, 5f, "0.00")]
public readonly Sync<float> ModulationIndex;
Expand Down Expand Up @@ -63,15 +64,15 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>

float modulationIndex = ModulationIndex.Value;

// Apply FM synthesis
// 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 * Math.Sin(2 * Math.PI * modulationIndex * modulatorValue));
float modulatedValue = (float)(carrierValue * modulatorValue * modulationIndex);

buffer[i] = buffer[i].SetChannel(j, modulatedValue);

Expand Down
86 changes: 86 additions & 0 deletions ProjectObsidian/Components/Audio/SineShapedRingModulator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using FrooxEngine;
using Elements.Assets;
using Elements.Core;

namespace Obsidian.Components.Audio
{
[Category(new string[] { "Obsidian/Audio" })]
[OldTypeName("Obsidian.Components.Audio.FrequencyModulator")]
public class SineShapedRingModulator : Component, IAudioSource, IWorldElement
{
[Range(0f, 5f, "0.00")]
public readonly Sync<float> ModulationIndex;

public readonly SyncRef<IAudioSource> CarrierSource;
public readonly SyncRef<IAudioSource> ModulatorSource;

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();
ModulationIndex.Value = 1f; // Default modulation index
}

public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
{
if (!IsActive)
{
buffer.Fill(default(S));
return;
}

int channelCount = ChannelCount;
if (channelCount == 0)
{
buffer.Fill(default(S));
return;
}

// Temporary buffers for carrier and modulator sources
Span<S> carrierBuffer = stackalloc S[buffer.Length];
Span<S> modulatorBuffer = stackalloc S[buffer.Length];

// Read data from sources
CarrierSource.Target.Read(carrierBuffer);
ModulatorSource.Target.Read(modulatorBuffer);

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