Skip to content

Commit

Permalink
Merge pull request #77 from Nytra/audioWork
Browse files Browse the repository at this point in the history
Fix audio effect nodes passing through the carrier signal, and add new Phase Modulation algorithm that doesn't click
  • Loading branch information
Xlinka authored Jan 26, 2025
2 parents 7cb7cf3 + 1644bdd commit 62b4ca8
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 21 deletions.
134 changes: 116 additions & 18 deletions ProjectObsidian/Elements/Audio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,36 +187,123 @@ public static void SineShapedRingModulation<S>(Span<S> buffer, Span<S> input1, S
/// <summary>
/// Calculates instantaneous phase of a signal using a simple Hilbert transform approximation
/// </summary>
//private static 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;
//}

//public static void PhaseModulation<S>(Span<S> buffer, Span<S> input1, Span<S> input2, float modulationIndex, int channelCount) where S : unmanaged, IAudioSample<S>
//{
// 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);
// }
// }
//}

/// <summary>
/// Calculates instantaneous phase of a signal using a more robust Hilbert transform approximation
/// </summary>
private static 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++)
// Calculate average amplitudes across channels
for (int i = 0; i < length; i++)
{
double sum = 0;
for (int j = 0; j < buffer[i].ChannelCount; j++)
{
avgAmplitudes[i] += buffer[i][j];
sum += buffer[i][j];
}
avgAmplitudes[i] /= buffer[i].ChannelCount;
avgAmplitudes[i] = sum / buffer[i].ChannelCount;
}

// Simple 3-point derivative for phase approximation
for (int i = 1; i < length - 1; i++)
// Use a wider window for derivative calculation to reduce noise
const int windowSize = 5;
const double epsilon = 1e-10; // Small value to prevent division by zero

for (int i = windowSize; i < length - windowSize; 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);
// Calculate smoothed derivative using a wider window
double derivative = 0;
for (int j = 1; j <= windowSize; j++)
{
derivative += (avgAmplitudes[i + j] - avgAmplitudes[i - j]) / (2.0 * j);
}
derivative /= windowSize;

// Calculate analytic signal magnitude with protection against zero
double magnitude = Math.Sqrt(avgAmplitudes[i] * avgAmplitudes[i] + derivative * derivative + epsilon);

// Normalize with smoothing to prevent discontinuities
double normalizedSignal = avgAmplitudes[i] / magnitude;

// Clamp to valid arccos range to prevent NaN
normalizedSignal = Math.Max(-1.0, Math.Min(1.0, normalizedSignal));

// Calculate phase
phase[i] = Math.Acos(normalizedSignal);

// 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];
// Smooth out edge cases using linear interpolation
for (int i = 0; i < windowSize; i++)
{
phase[i] = phase[windowSize];
}
for (int i = length - windowSize; i < length; i++)
{
phase[i] = phase[length - windowSize - 1];
}

return phase;
}
Expand All @@ -225,21 +312,32 @@ public static void PhaseModulation<S>(Span<S> buffer, Span<S> input1, Span<S> in
{
double[] carrierPhase = CalculateInstantaneousPhase(input1);

// Apply phase modulation
// Apply phase modulation with improved amplitude handling
for (int i = 0; i < buffer.Length; i++)
{
// Get carrier amplitude for envelope
float carrierAmplitude = 0;
for (int j = 0; j < channelCount; j++)
{
carrierAmplitude += Math.Abs(input1[i][j]);
}
carrierAmplitude /= channelCount;

for (int j = 0; j < channelCount; j++)
{
// Apply modulation with smooth amplitude envelope
double modifiedPhase = carrierPhase[i] + (modulationIndex * input2[i][j]);

// Calculate amplitude using original carrier amplitude
float amplitude = input1[i][j];
// Generate output sample with envelope following
float outputSample = carrierAmplitude * (float)Math.Sin(modifiedPhase);

// Generate output sample
buffer[i] = buffer[i].SetChannel(j, amplitude * (float)Math.Sin(modifiedPhase));
// Soft clip instead of hard limiting
if (Math.Abs(outputSample) > 1f)
{
outputSample = Math.Sign(outputSample) * (1f - 1f / (Math.Abs(outputSample) + 1f));
}

if (buffer[i][j] > 1f) buffer[i] = buffer[i].SetChannel(j, 1f);
if (buffer[i][j] < -1f) buffer[i] = buffer[i].SetChannel(j, -1f);
buffer[i] = buffer[i].SetChannel(j, outputSample);
}
}
}
Expand Down
1 change: 0 additions & 1 deletion ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
}

Span<S> newBuffer = stackalloc S[buffer.Length];
newBuffer = buffer;
Span<S> newBuffer2 = stackalloc S[buffer.Length];
if (AudioInput != null)
{
Expand Down
1 change: 0 additions & 1 deletion ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
}

Span<S> newBuffer = stackalloc S[buffer.Length];
newBuffer = buffer;
Span<S> newBuffer2 = stackalloc S[buffer.Length];
if (AudioInput != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public void Read<S>(Span<S> buffer) where S : unmanaged, IAudioSample<S>
}

Span<S> newBuffer = stackalloc S[buffer.Length];
newBuffer = buffer;
Span<S> newBuffer2 = stackalloc S[buffer.Length];
if (AudioInput != null)
{
Expand Down

0 comments on commit 62b4ca8

Please sign in to comment.