From f6c050cf1f5783cd25f42c04cace293d65b2a023 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:41:04 +0000 Subject: [PATCH 1/2] Fix audio effect nodes passing through the carrier signal --- ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs | 1 - ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs | 1 - ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs b/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs index ab13cdc..c91b01c 100644 --- a/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs +++ b/ProjectObsidian/ProtoFlux/Audio/PhaseModulatorNode.cs @@ -32,7 +32,6 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample } Span newBuffer = stackalloc S[buffer.Length]; - newBuffer = buffer; Span newBuffer2 = stackalloc S[buffer.Length]; if (AudioInput != null) { diff --git a/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs b/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs index 8b9e05c..1a25d5f 100644 --- a/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs +++ b/ProjectObsidian/ProtoFlux/Audio/RingModulatorNode.cs @@ -33,7 +33,6 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample } Span newBuffer = stackalloc S[buffer.Length]; - newBuffer = buffer; Span newBuffer2 = stackalloc S[buffer.Length]; if (AudioInput != null) { diff --git a/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs b/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs index 0a12183..7f1957d 100644 --- a/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs +++ b/ProjectObsidian/ProtoFlux/Audio/SineShapedRingModulatorNode.cs @@ -32,7 +32,6 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample } Span newBuffer = stackalloc S[buffer.Length]; - newBuffer = buffer; Span newBuffer2 = stackalloc S[buffer.Length]; if (AudioInput != null) { From 1644bdd70efd71c5d106d33fe3636ce04a765766 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:02:57 +0000 Subject: [PATCH 2/2] New Phase Modulation algorithm that doesn't click --- ProjectObsidian/Elements/Audio.cs | 134 ++++++++++++++++++++++++++---- 1 file changed, 116 insertions(+), 18 deletions(-) diff --git a/ProjectObsidian/Elements/Audio.cs b/ProjectObsidian/Elements/Audio.cs index 1d07767..92ce475 100644 --- a/ProjectObsidian/Elements/Audio.cs +++ b/ProjectObsidian/Elements/Audio.cs @@ -187,36 +187,123 @@ public static void SineShapedRingModulation(Span buffer, Span input1, S /// /// 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); + // } + // } + //} + + /// + /// Calculates instantaneous phase of a signal using a more robust 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++) + // 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; } @@ -225,21 +312,32 @@ public static void PhaseModulation(Span buffer, Span input1, Span 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); } } }