From 8c145182aecd6b8211131430e2f0efbcc1de4437 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Wed, 1 Jan 2025 17:36:38 +0000 Subject: [PATCH 1/2] Add PhaseModulator, Add RingModulator, rename FrequencyModulator to SineShapedRingModulator --- .../Components/Audio/PhaseModulator.cs | 124 ++++++++++++++++++ ...FrequencyModulator.cs => RingModulator.cs} | 7 +- .../Audio/SineShapedRingModulator.cs | 86 ++++++++++++ 3 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 ProjectObsidian/Components/Audio/PhaseModulator.cs rename ProjectObsidian/Components/Audio/{FrequencyModulator.cs => RingModulator.cs} (93%) create mode 100644 ProjectObsidian/Components/Audio/SineShapedRingModulator.cs diff --git a/ProjectObsidian/Components/Audio/PhaseModulator.cs b/ProjectObsidian/Components/Audio/PhaseModulator.cs new file mode 100644 index 0000000..ce070b1 --- /dev/null +++ b/ProjectObsidian/Components/Audio/PhaseModulator.cs @@ -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 ModulationIndex; + + public readonly SyncRef CarrierSource; + public readonly SyncRef 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 + } + + /// + /// 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 + { + if (!IsActive) + { + buffer.Fill(default(S)); + return; + } + + int channelCount = ChannelCount; + if (channelCount == 0) + { + buffer.Fill(default(S)); + return; + } + + Span carrierBuffer = stackalloc S[buffer.Length]; + Span 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); + } + } + } + } +} \ No newline at end of file diff --git a/ProjectObsidian/Components/Audio/FrequencyModulator.cs b/ProjectObsidian/Components/Audio/RingModulator.cs similarity index 93% rename from ProjectObsidian/Components/Audio/FrequencyModulator.cs rename to ProjectObsidian/Components/Audio/RingModulator.cs index 8336c8f..d5ec689 100644 --- a/ProjectObsidian/Components/Audio/FrequencyModulator.cs +++ b/ProjectObsidian/Components/Audio/RingModulator.cs @@ -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 ModulationIndex; @@ -63,7 +64,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample float modulationIndex = ModulationIndex.Value; - // Apply FM synthesis + // Apply sine-shaped ring modulation for (int i = 0; i < buffer.Length; i++) { for (int j = 0; j < buffer[i].ChannelCount; j++) @@ -71,7 +72,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample 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); diff --git a/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs b/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs new file mode 100644 index 0000000..04bea86 --- /dev/null +++ b/ProjectObsidian/Components/Audio/SineShapedRingModulator.cs @@ -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 ModulationIndex; + + public readonly SyncRef CarrierSource; + public readonly SyncRef 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(Span buffer) where S : unmanaged, IAudioSample + { + 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 carrierBuffer = stackalloc S[buffer.Length]; + Span 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); + } + } + } + } +} \ No newline at end of file From 52104f54117e835ab686fbbfffdb2d7a1de37891 Mon Sep 17 00:00:00 2001 From: Nytra <14206961+Nytra@users.noreply.github.com> Date: Wed, 1 Jan 2025 17:38:04 +0000 Subject: [PATCH 2/2] Adjust comment --- ProjectObsidian/Components/Audio/RingModulator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectObsidian/Components/Audio/RingModulator.cs b/ProjectObsidian/Components/Audio/RingModulator.cs index d5ec689..b51f991 100644 --- a/ProjectObsidian/Components/Audio/RingModulator.cs +++ b/ProjectObsidian/Components/Audio/RingModulator.cs @@ -64,7 +64,7 @@ public void Read(Span buffer) where S : unmanaged, IAudioSample float modulationIndex = ModulationIndex.Value; - // Apply sine-shaped ring modulation + // Apply ring modulation for (int i = 0; i < buffer.Length; i++) { for (int j = 0; j < buffer[i].ChannelCount; j++)