diff --git a/Cavern/Channels/ChannelPrototype.cs b/Cavern/Channels/ChannelPrototype.cs index 75aeabf5..3e31131c 100644 --- a/Cavern/Channels/ChannelPrototype.cs +++ b/Cavern/Channels/ChannelPrototype.cs @@ -124,6 +124,24 @@ public static ChannelPrototype[] Get(ReferenceChannel[] source) { return result; } + /// + /// Convert a mapping of s to s with . + /// + public static ChannelPrototype[] GetAlternative(ReferenceChannel[] source) { + ChannelPrototype[] result = new ChannelPrototype[source.Length]; + for (int i = 0; i < source.Length; ++i) { + int index = (int)source[i]; + ChannelPrototype old = Mapping[index]; + Channel moved = new Channel(AlternativePositions[index], old.LFE); + if (old.X == 0) { + result[i] = new ChannelPrototype(moved.Y, old.Name, old.LFE, old.Muted); + } else { + result[i] = new ChannelPrototype(moved.Y, moved.X, old.Name); + } + } + return result; + } + /// /// Convert a mapping of s to the names of the channels. /// @@ -191,15 +209,21 @@ public static Channel[] ToLayout(ChannelPrototype[] source) { /// public static Channel[] ToLayout(ReferenceChannel[] source) => ToLayout(Get(source)); + /// + /// Convert a reference array to a array that can be set in , + /// using the . + /// + public static Channel[] ToLayoutAlternative(ReferenceChannel[] source) => ToLayout(GetAlternative(source)); + /// /// Check if two channel prototypes are the same. /// - public bool Equals(ChannelPrototype other) => X == other.X && Y == other.Y && LFE == other.LFE; + public readonly bool Equals(ChannelPrototype other) => X == other.X && Y == other.Y && LFE == other.LFE; /// /// Human-readable channel prototype data. /// - public override string ToString() { + public override readonly string ToString() { string basic = $"{(LFE ? Name + "(LFE)" : Name)} ({X}; {Y})"; if (Muted) { return basic + " (muted)"; diff --git a/Cavern/Channels/SpatialRemapping.cs b/Cavern/Channels/SpatialRemapping.cs new file mode 100644 index 00000000..6a00aac9 --- /dev/null +++ b/Cavern/Channels/SpatialRemapping.cs @@ -0,0 +1,61 @@ +using System; + +using Cavern.Utilities; + +namespace Cavern.Channels { + /// + /// Multiple ways of getting a mixing matrix to simulate one channel layout on a different one. While Cavern can play any standard + /// content on any layout, getting the matrix is useful for applying this feature in calibrations. + /// + public static class SpatialRemapping { + /// + /// Get a mixing matrix that maps the to a . The result is a set of + /// multipliers for each output (playback) channel, with which the input (content) channels should be multiplied and mixed to that + /// specific channel. The dimensions are [output channels][input channels]. + /// + public static float[][] GetMatrix(Channel[] playedContent, Channel[] usedLayout) { + Channel[] oldChannels = Listener.Channels; + Listener.ReplaceChannels(usedLayout); + int inputs = playedContent.Length, + outputs = usedLayout.Length; + + // Create simulation + Listener simulator = new Listener() { + UpdateRate = Math.Max(inputs, 16), + LFESeparation = true, + DirectLFE = true + }; + for (int i = 0; i < inputs; i++) { + simulator.AttachSource(new Source() { + Clip = GetClipForChannel(i, inputs, simulator.SampleRate), + Position = playedContent[i].SpatialPos * Listener.EnvironmentSize, + LFE = playedContent[i].LFE, + VolumeRolloff = Rolloffs.Disabled + }); + } + + // Simulate and format + float[] result = simulator.Render(); + Listener.ReplaceChannels(oldChannels); + int expectedLength = inputs * outputs; + if (result.Length > expectedLength) { + Array.Resize(ref result, expectedLength); + } + float[][] output = new float[outputs][]; + for (int i = 0; i < outputs; i++) { + output[i] = new float[inputs]; + } + WaveformUtils.InterlacedToMultichannel(result, output); + return output; + } + + /// + /// Create a that is 1 at the channel's index and 0 everywhere else. + /// + static Clip GetClipForChannel(int channel, int channels, int sampleRate) { + float[] data = new float[channels]; + data[channel] = 1; + return new Clip(data, 1, sampleRate); + } + } +} \ No newline at end of file diff --git a/Cavern/Listener.cs b/Cavern/Listener.cs index 5bef5661..5f32f7f5 100644 --- a/Cavern/Listener.cs +++ b/Cavern/Listener.cs @@ -437,7 +437,7 @@ public float[] Render(int frames = 1) { for (int source = 0; source < sourceDistances.Length; ++source) { sourceDistances[source] = Range; } - pulseDelta = (frames * UpdateRate) / (float)SampleRate; + pulseDelta = frames * UpdateRate / (float)SampleRate; // Choose the sources to play LinkedListNode node = activeSources.First; diff --git a/Tests/Test.Cavern.Format/Test.Cavern.Format.csproj b/Tests/Test.Cavern.Format/Test.Cavern.Format.csproj index 978d017c..b762c5d1 100644 --- a/Tests/Test.Cavern.Format/Test.Cavern.Format.csproj +++ b/Tests/Test.Cavern.Format/Test.Cavern.Format.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable disable diff --git a/Tests/Test.Cavern.QuickEQ/Test.Cavern.QuickEQ.csproj b/Tests/Test.Cavern.QuickEQ/Test.Cavern.QuickEQ.csproj index d9d6da48..7f53469a 100644 --- a/Tests/Test.Cavern.QuickEQ/Test.Cavern.QuickEQ.csproj +++ b/Tests/Test.Cavern.QuickEQ/Test.Cavern.QuickEQ.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable disable diff --git a/Tests/Test.Cavern/ChannelPrototype_Tests.cs b/Tests/Test.Cavern/Channels/ChannelPrototype_Tests.cs similarity index 94% rename from Tests/Test.Cavern/ChannelPrototype_Tests.cs rename to Tests/Test.Cavern/Channels/ChannelPrototype_Tests.cs index 90585225..61c46d7f 100644 --- a/Tests/Test.Cavern/ChannelPrototype_Tests.cs +++ b/Tests/Test.Cavern/Channels/ChannelPrototype_Tests.cs @@ -1,6 +1,6 @@ using Cavern.Channels; -namespace Test.Cavern { +namespace Test.Cavern.Channels { /// /// Tests the struct. /// diff --git a/Tests/Test.Cavern/Channels/SpatialRemapping_Tests.cs b/Tests/Test.Cavern/Channels/SpatialRemapping_Tests.cs new file mode 100644 index 00000000..f99e854d --- /dev/null +++ b/Tests/Test.Cavern/Channels/SpatialRemapping_Tests.cs @@ -0,0 +1,29 @@ +using Cavern; +using Cavern.Channels; + +namespace Test.Cavern.Channels { + /// + /// Tests the functions. + /// + [TestClass] + public class SpatialRemapping_Tests { + /// + /// Tests if remapping the alternative 5.1 is done correctly to average 5.1 placement. + /// + [TestMethod, Timeout(1000)] + public void Remap5Point1() { + Channel[] content = ChannelPrototype.ToLayoutAlternative(ChannelPrototype.GetStandardMatrix(6)), + playback = ChannelPrototype.ToLayout(ChannelPrototype.GetStandardMatrix(6)); + float[][] matrix = SpatialRemapping.GetMatrix(content, playback); + Assert.AreEqual(1, matrix[0][0]); // FL + Assert.AreEqual(1, matrix[1][1]); // FR + Assert.AreEqual(1, matrix[2][2]); // C + Assert.AreEqual(1, matrix[3][3]); // LFE + Assert.AreEqual(.570968032f, matrix[0][4]); // SL front mix + Assert.AreEqual(.570968032f, matrix[1][5]); // SR front mix + Assert.AreEqual(.820972264f, matrix[4][4]); // SL side mix + Assert.AreEqual(.820972264f, matrix[5][5]); // SR side mix + TestUtils.AssertNumberOfZeros(matrix, 28); + } + } +} \ No newline at end of file diff --git a/Tests/Test.Cavern/Consts/TestUtils.cs b/Tests/Test.Cavern/Consts/TestUtils.cs new file mode 100644 index 00000000..97dbffad --- /dev/null +++ b/Tests/Test.Cavern/Consts/TestUtils.cs @@ -0,0 +1,22 @@ +namespace Test.Cavern { + /// + /// Common utilities used in testing like assertions. + /// + internal static class TestUtils { + /// + /// Test if the number of zeros in a jagged match an expected . + /// + public static void AssertNumberOfZeros(float[][] array, int count) { + int zeros = 0; + for (int i = 0; i < array.Length; i++) { + float[] subarray = array[i]; + for (int j = 0; j < subarray.Length; j++) { + if (subarray[j] == 0) { + zeros++; + } + } + } + Assert.AreEqual(count, zeros); + } + } +} \ No newline at end of file diff --git a/Tests/Test.Cavern/Test.Cavern.csproj b/Tests/Test.Cavern/Test.Cavern.csproj index bdf93b26..92cfbbd2 100644 --- a/Tests/Test.Cavern/Test.Cavern.csproj +++ b/Tests/Test.Cavern/Test.Cavern.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable disable