Skip to content

Commit

Permalink
Partial virtualization
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Apr 6, 2024
1 parent ef62aa6 commit 5dea2f4
Show file tree
Hide file tree
Showing 19 changed files with 223 additions and 102 deletions.
5 changes: 5 additions & 0 deletions Cavern.Format/Common/RenderTrack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public RenderTrack(Codec format, int blockSize, int channelCount, long length, i
/// </summary>
public void EncodeNextBlock(float[] samples) => encoder.WriteBlock(samples, 0, samples.Length);

/// <summary>
/// Allows encoding a partial sample block.
/// </summary>
public void EncodeNextBlock(float[] samples, long from, long to) => encoder.WriteBlock(samples, from, to);

/// <summary>
/// The following block of the track is rendered and available.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Cavern.Format/Output/Container/AudioWriterIntoContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public AudioWriterIntoContainer(string path, Track[] tracks, Codec newTrack, int
/// <param name="from">Start position in the input array (inclusive)</param>
/// <param name="to">End position in the input array (exclusive)</param>
public override void WriteBlock(float[] samples, long from, long to) {
track.EncodeNextBlock(samples);
track.EncodeNextBlock(samples, from, to);
container.WriteBlock(track.timeStep);
}

Expand Down
3 changes: 3 additions & 0 deletions Cavern.QuickEQ/Equalization/Equalizer.Transform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ public void Limit(double startFreq, double endFreq, EQCurve targetCurve, bool co
/// </summary>
public void LimitPeaks(double peak, double startFreq, double endFreq) {
(int startBand, int endBand) = GetBandLimits(startFreq, endFreq);
if (startBand == -1 && endBand == -1) {
return; // The frequency range is not on the curve
}
LimitPeaks(startBand, endBand, peak);
}

Expand Down
28 changes: 14 additions & 14 deletions Cavern/Listener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,15 @@ public Listener(bool loadGlobals) {
try {
int savePos = 1;
Channels = new Channel[Convert.ToInt32(save[0])];
for (int i = 0; i < Channels.Length; ++i) {
for (int i = 0; i < Channels.Length; i++) {
Channels[i] = new Channel(QMath.ParseFloat(save[savePos++]), QMath.ParseFloat(save[savePos++]),
Convert.ToBoolean(save[savePos++]));
}
EnvironmentType = (Environments)Convert.ToInt32(save[savePos++]);
EnvironmentSize = new Vector3(QMath.ParseFloat(save[savePos++]), QMath.ParseFloat(save[savePos++]),
QMath.ParseFloat(save[savePos++]));
HeadphoneVirtualizer = save.Length > savePos && Convert.ToBoolean(save[savePos++]); // Added: 2016.04.24.
++savePos; // Environment compensation (bool), added: 2017.06.18, removed: 2019.06.06.
savePos++; // Environment compensation (bool), added: 2017.06.18, removed: 2019.06.06.
} catch {
Channels = ChannelPrototype.ToLayout(ChannelPrototype.GetStandardMatrix(6));
EnvironmentType = Environments.Home;
Expand All @@ -317,15 +317,15 @@ public static string GetLayoutName() {
return "Virtualization";
} else {
int regular = 0, sub = 0, ceiling = 0, floor = 0;
for (int channel = 0; channel < Channels.Length; ++channel) {
for (int channel = 0; channel < Channels.Length; channel++) {
if (Channels[channel].LFE) {
++sub;
sub++;
} else if (Channels[channel].X == 0) {
++regular;
regular++;
} else if (Channels[channel].X < 0) {
++ceiling;
ceiling++;
} else if (Channels[channel].X > 0) {
++floor;
floor++;
}
}
StringBuilder layout = new StringBuilder(regular.ToString()).Append('.').Append(sub);
Expand Down Expand Up @@ -367,7 +367,7 @@ public static void ReplaceChannels(int channelCount) =>
/// Recalculate the rendering environment.
/// </summary>
static void Recalculate() {
for (int channel = 0; channel < Channels.Length; ++channel) {
for (int channel = 0; channel < Channels.Length; channel++) {
Channels[channel].Recalculate();
}
}
Expand Down Expand Up @@ -408,7 +408,7 @@ public void DetachSource(Source source) {
/// Detach all sources from this listener.
/// </summary>
public void DetachAllSources() {
for (int i = 0, c = activeSources.Count; i < c; ++i) {
for (int i = 0, c = activeSources.Count; i < c; i++) {
activeSources.First.Value.listener = null;
activeSources.RemoveFirst();
}
Expand All @@ -434,7 +434,7 @@ public float[] Render(int frames = 1) {
if (SampleRate < 44100 || UpdateRate < 16) { // Don't work with wrong settings
return null;
}
for (int source = 0; source < sourceDistances.Length; ++source) {
for (int source = 0; source < sourceDistances.Length; source++) {
sourceDistances[source] = Range;
}
pulseDelta = frames * UpdateRate / (float)SampleRate;
Expand All @@ -458,7 +458,7 @@ public float[] Render(int frames = 1) {
if (multiframeBuffer.Length != sampleCount) {
multiframeBuffer = new float[sampleCount];
}
for (int frame = 0; frame < frames; ++frame) {
for (int frame = 0; frame < frames; frame++) {
float[] frameBuffer = Frame();
Array.Copy(frameBuffer, 0, multiframeBuffer, frame * frameBuffer.Length, frameBuffer.Length);
}
Expand All @@ -478,7 +478,7 @@ void Reoptimize() {
lastUpdateRate = UpdateRate;
renderBuffer = new float[channelCount * UpdateRate];
lowpasses = new Lowpass[channelCount];
for (int i = 0; i < channelCount; ++i) {
for (int i = 0; i < channelCount; i++) {
lowpasses[i] = new Lowpass(SampleRate, 120);
}
}
Expand Down Expand Up @@ -507,12 +507,12 @@ float[] Frame() {

// Mix sources to output
Array.Clear(renderBuffer, 0, renderBuffer.Length);
for (int result = 0; result < results.Count; ++result) {
for (int result = 0; result < results.Count; result++) {
WaveformUtils.Mix(results[result], renderBuffer);
}

// Volume and subwoofers' lowpass
for (int channel = 0; channel < channelCount; ++channel) {
for (int channel = 0; channel < channelCount; channel++) {
if (Channels[channel].LFE) {
if (!DirectLFE) {
lowpasses[channel].Process(renderBuffer, channel, channelCount);
Expand Down
29 changes: 23 additions & 6 deletions Cavern/Virtualizer/VirtualChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,29 @@ public struct VirtualChannel : IEquatable<VirtualChannel> {
/// </summary>
public float y;

/// <summary>
/// This channel should be virtualized and downmixed to stereo.
/// </summary>
public bool active;

/// <summary>
/// Constructs a virtual channel with disabled virtualization.
/// </summary>
public VirtualChannel(float x, float y) {
this.x = x;
this.y = y;
active = false;
Crossover = null;
Filter = null;
}

/// <summary>
/// Constructs a virtualizable channel with impulse responses for both ears.
/// </summary>
public VirtualChannel(float x, float y, float[] leftEarIR, float[] rightEarIR, int sampleRate, float crossoverFrequency) {
this.x = x;
this.y = y;
active = true;
Crossover = new Crossover(sampleRate, crossoverFrequency);
Filter = new DualConvolver(leftEarIR, rightEarIR, y % 180 > 0 ? GetDelay(y % 180) : 0, y < 0 ? GetDelay(-y) : 0);
}
Expand Down Expand Up @@ -95,7 +112,7 @@ public static VirtualChannel[] Parse(MultichannelWaveform hrir, int sampleRate,
MultichannelWaveform[] splits = hrir.Split(rPeak - lPeak);
int lastToKeep = splits.Length;
while (lastToKeep > 1 && splits[lastToKeep - 1].IsMute()) {
--lastToKeep;
lastToKeep--;
}
if (splits.Length != lastToKeep) {
splits = splits[..lastToKeep];
Expand Down Expand Up @@ -131,18 +148,18 @@ public static VirtualChannel[] Parse(MultichannelWaveform hrir, int sampleRate,
}
}

/// <summary>
/// Check if the two virtual channels handle the same source channel position.
/// </summary>
public readonly bool Equals(VirtualChannel other) => x == other.x && y == other.y;

/// <summary>
/// Get the secondary ear's delay by angle of attack.
/// </summary>
/// <remarks>This formula is based on measurements and the sine wave's usability was disproven.
/// See Bence S. (2022). Extending HRTF with distance simulation based on ray-tracing.</remarks>
static int GetDelay(float angle) => (int)((90 - Math.Abs(angle - 90)) / 2.7f);

/// <summary>
/// Check if the two virtual channels handle the same source channel position.
/// </summary>
public bool Equals(VirtualChannel other) => x == other.x && y == other.y;

/// <summary>
/// Precalculated -10 dB as voltage gain. Used by angle gain post-processing.
/// </summary>
Expand Down
66 changes: 52 additions & 14 deletions Cavern/Virtualizer/VirtualizerFilter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

using Cavern.Filters;
using Cavern.Utilities;
Expand Down Expand Up @@ -95,13 +96,40 @@ public static void Reset() {
ReferenceDistance = defaultDistance;
}

/// <summary>
/// Change the virtualized layout based on <see cref="Listener.Channels"/>, with added virtualized heights. Ground channels
/// will not be virtualized. This can be used for conveying height information on legacy surround sound systems such as 5.1 or 7.1.
/// </summary>
public static void SetupForSpeakers() {
List<VirtualChannel> channels = new List<VirtualChannel>();

// Add ground channels (non-virtualized)
Channel[] source = Listener.Channels;
for (int i = 0; i < source.Length; i++) {
if (source[i].X == 0 || source[i].LFE) {
channels.Add(new VirtualChannel(source[i].X, source[i].Y));
} else {
throw new NonGroundChannelPresentException();
}
}

// Add height channels (virtualized)
for (int i = 0; i < SpatialChannels.Length; i++) {
if (spatialChannels[i].x != 0) {
channels.Add(spatialChannels[i]);
}
}

SpatialChannels = channels.ToArray();
}

/// <summary>
/// Set up virtual channel set for the virtualization filters.
/// </summary>
public void SetLayout() {
bool setAgain = centerDelay == null;
if (Listener.Channels.Length == SpatialChannels.Length) {
for (int channel = 0; channel < SpatialChannels.Length; ++channel) {
for (int channel = 0; channel < SpatialChannels.Length; channel++) {
if (Listener.Channels[channel].X != SpatialChannels[channel].x ||
Listener.Channels[channel].Y != SpatialChannels[channel].y) {
setAgain = true;
Expand All @@ -117,7 +145,7 @@ public void SetLayout() {

centerDelay = new Delay(.0075f, FilterSampleRate);
Channel[] newChannels = new Channel[SpatialChannels.Length];
for (int channel = 0; channel < SpatialChannels.Length; ++channel) {
for (int channel = 0; channel < SpatialChannels.Length; channel++) {
newChannels[channel] = new Channel(SpatialChannels[channel].x, SpatialChannels[channel].y);
if (SpatialChannels[channel].x == 0) {
if (SpatialChannels[channel].y == 0) {
Expand All @@ -133,7 +161,7 @@ public void SetLayout() {
originalSplit = new float[SpatialChannels.Length][];
leftSplit = new float[SpatialChannels.Length][];
rightSplit = new float[SpatialChannels.Length][];
for (int channel = 0; channel < SpatialChannels.Length; ++channel) {
for (int channel = 0; channel < SpatialChannels.Length; channel++) {
rightSplit[channel] = new float[0];
}
originalSplit[0] = new float[0];
Expand All @@ -151,14 +179,14 @@ public void Process(float[] output, int sampleRate) {
}

if (originalSplit[0].Length != blockSize) {
for (int channel = 0; channel < channels; ++channel) {
for (int channel = 0; channel < channels; channel++) {
originalSplit[channel] = new float[blockSize];
}
delayedCenter = new float[blockSize];
}

for (int sample = 0, outSample = 0; sample < blockSize; ++sample) {
for (int channel = 0; channel < channels; ++channel, ++outSample) {
for (int sample = 0, outSample = 0; sample < blockSize; sample++) {
for (int channel = 0; channel < channels; channel++, outSample++) {
originalSplit[channel][sample] = output[outSample];
}
}
Expand All @@ -176,12 +204,18 @@ public void Process(float[] output, int sampleRate) {

// Stereo downmix
output.Clear();
for (int sample = 0; sample < blockSize; ++sample) {
int leftOut = sample * channels, rightOut = leftOut + 1;
for (int channel = 0; channel < channels; ++channel) {
float unspatialized = originalSplit[channel][sample] * .5f;
output[leftOut] += leftSplit[channel][sample] + unspatialized;
output[rightOut] += rightSplit[channel][sample] + unspatialized;
for (int channel = 0; channel < channels; channel++) {
if (spatialChannels[channel].active) {
float[] original = originalSplit[channel],
left = leftSplit[channel],
right = rightSplit[channel];
for (int sample = 0; sample < blockSize; sample++) {
float unspatialized = original[sample] * .5f;
output[sample * channels] += left[sample] + unspatialized;
output[sample * channels + 1] += right[sample] + unspatialized;
}
} else {
WaveformUtils.Mix(originalSplit[channel], output, channel, channels, 1);
}
}
}
Expand All @@ -190,8 +224,12 @@ public void Process(float[] output, int sampleRate) {
/// Split and convolve a single channel by ID.
/// </summary>
void ProcessChannel(int channel) {
if (!SpatialChannels[channel].active) {
return;
}

// Select the retain range
Crossover lowCrossover = SpatialChannels[channel].Crossover;
Crossover lowCrossover = spatialChannels[channel].Crossover;
lowCrossover.Process(originalSplit[channel]);
originalSplit[channel] = lowCrossover.LowOutput;

Expand All @@ -201,7 +239,7 @@ void ProcessChannel(int channel) {
}
leftSplit[channel] = lowCrossover.HighOutput;
Array.Copy(leftSplit[channel], rightSplit[channel], blockSize);
SpatialChannels[channel].Filter.Process(leftSplit[channel], rightSplit[channel]);
spatialChannels[channel].Filter.Process(leftSplit[channel], rightSplit[channel]);
}

/// <summary>
Expand Down
20 changes: 20 additions & 0 deletions Cavern/Virtualizer/_Exceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace Cavern.Virtualizer {
/// <summary>
/// Tells if a ground channel is present, preventing an opteration.
/// </summary>
public class NonGroundChannelPresentException : Exception {
const string message = "A non-ground channel present in the layout is preventing this operation.";

/// <summary>
/// Tells if a ground channel is present and preventing an opteration.
/// </summary>
public NonGroundChannelPresentException() : base(message) { }

/// <summary>
/// Tells if a ground channel is present and preventing an opteration, with a custom message.
/// </summary>
public NonGroundChannelPresentException(string message) : base(message) { }
}
}
3 changes: 3 additions & 0 deletions CavernSamples/CavernizeGUI/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<setting name="lastDirectory" serializeAs="String">
<value />
</setting>
<setting name="speakerVirtualizer" serializeAs="String">
<value>False</value>
</setting>
</CavernizeGUI.Resources.Settings>
</userSettings>
</configuration>
10 changes: 5 additions & 5 deletions CavernSamples/CavernizeGUI/Elements/Track.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ public Track(AudioReader reader, Codec codec, int index) {
FormatHeader = Renderer.HasObjects ? (string)strings["ObTra"] : (string)strings["ChTra"];
}

ReferenceChannel[] beds = Renderer != null ? Renderer.GetChannels() : Array.Empty<ReferenceChannel>();
ReferenceChannel[] beds = Renderer != null ? Renderer.GetChannels() : [];
string bedList = string.Join(' ', ChannelPrototype.GetShortNames(beds));
List<(string, string)> builder = new();
List<(string, string)> builder = [];
if (eac3 != null && eac3.HasObjects) {
builder.Add(((string)strings["SouCh"], $"{beds.Length} - {bedList}"));
string[] newBeds = ChannelPrototype.GetShortNames(eac3.GetStaticChannels());
Expand Down Expand Up @@ -159,13 +159,13 @@ public void Attach(Listener listener) {
RunningChannelSeparator separator = new RunningChannelSeparator(channels.Length) {
GetSamples = input => reader.ReadBlock(input, 0, input.Length)
};
upmixer.OnSamplesNeeded += updateRate => separator.Update(updateRate);
upmixer.OnSamplesNeeded = updateRate => separator.Update(updateRate);

listener.LFESeparation = channels.Contains(ReferenceChannel.ScreenLFE); // Apply crossover if LFE is not present
attachables = upmixer.IntermediateSources;
} else {
listener.LFESeparation = true;
attachables = Renderer.Objects.ToArray();
attachables = [.. Renderer.Objects];
}

if (UpmixingSettings.Default.Cavernize && !Renderer.HasObjects) {
Expand Down Expand Up @@ -209,7 +209,7 @@ public void Dispose() {
public override string ToString() {
ResourceDictionary strings = Consts.Language.GetTrackStrings();
string codecName = (string)strings[Codec.ToString()] ??
(formatNames.ContainsKey(Codec) ? formatNames[Codec] : Codec.ToString());
(formatNames.TryGetValue(Codec, out string? value) ? value : Codec.ToString());
string objects = Renderer != null && Renderer.HasObjects ? " " + strings["WiObj"] : string.Empty;
return string.IsNullOrEmpty(Language) ? codecName : $"{codecName}{objects} ({Language})";
}
Expand Down
Loading

0 comments on commit 5dea2f4

Please sign in to comment.