/// <summary> /// Apply variable smoothing (in octaves) on a graph drawn with /// <see cref="ConvertToGraph(float[], double, double, int, int)"/>. /// </summary> public static float[] SmoothGraph(float[] samples, float startFreq, float endFreq, float startOctave, float endOctave) { float[] startGraph = SmoothGraph(samples, startFreq, endFreq, startOctave), endGraph = SmoothGraph(samples, startFreq, endFreq, endOctave), output = new float[samples.Length]; float positioner = 1f / samples.Length; for (int i = 0; i < samples.Length; ++i) { output[i] = QMath.Lerp(startGraph[i], endGraph[i], i * positioner); } return(output); }
/// <summary> /// Cache the samples if the source should be rendered. This wouldn't be thread safe. /// </summary> /// <returns>The collection should be performed, as all requirements are met</returns> protected internal virtual bool Precollect() { if (delay > 0) { delay -= listener.UpdateRate; return(false); } if (listener.sourceDistances.Contains(distance)) { if (listener.AudioQuality != QualityModes.Low) { if (DopplerLevel == 0) { calculatedPitch = Pitch; } else { float dopplerTarget = Pitch + lastDoppler * // c / (c - dv), dv = ds / dt (SpeedOfSound / (SpeedOfSound - (lastDistance - distance) / listener.pulseDelta) - 1); lastDoppler = Math.Clamp(QMath.Lerp(lastDoppler, dopplerTarget, 10 * listener.UpdateRate / (float)listener.SampleRate), 0, DopplerLevel); calculatedPitch = Math.Clamp(lastDoppler, .5f, 3f); } } else { calculatedPitch = 1; // Disable any pitch change on low quality } resampleMult = listener.SampleRate != Clip.SampleRate ? (float)Clip.SampleRate / listener.SampleRate : 1; baseUpdateRate = (int)(listener.UpdateRate * calculatedPitch); PitchedUpdateRate = (int)(baseUpdateRate * resampleMult); if (samples.Length != PitchedUpdateRate) { samples = new float[PitchedUpdateRate]; } if (Clip.Channels == 2 && leftSamples.Length != PitchedUpdateRate) { leftSamples = new float[PitchedUpdateRate]; rightSamples = new float[PitchedUpdateRate]; } Rendered = GetSamples(); if (rendered.Length != Listener.Channels.Length * listener.UpdateRate) { rendered = new float[Listener.Channels.Length * listener.UpdateRate]; } return(true); } Rendered = null; return(false); }
/// <summary> /// Gets the gain at a given frequency. /// </summary> public double this[double frequency] { get { int bandCount = bands.Count; if (bandCount == 0) { return(0); } int nextBand = 0, prevBand = 0; while (nextBand != bandCount && bands[nextBand].Frequency < frequency) { prevBand = nextBand; ++nextBand; } if (nextBand != bandCount && nextBand != 0) { return(QMath.Lerp(bands[prevBand].Gain, bands[nextBand].Gain, QMath.LerpInverse(bands[prevBand].Frequency, bands[nextBand].Frequency, frequency))); } return(bands[prevBand].Gain); } }
/// <summary> /// Resamples a single channel with medium quality (linear interpolation). /// </summary> /// <param name="samples">Samples of the source channel</param> /// <param name="to">New sample count</param> /// <returns>Returns a resampled version of the given array</returns> public static float[] Lerp(float[] samples, int to) { if (samples.Length == to) { return(samples); } float[] output = new float[to]; float ratio = samples.Length / (float)to; int lerpUntil = (int)((samples.Length - 1) / ratio); // Halving point where i * ratio would be over the array for (int i = 0; i < lerpUntil; ++i) { int sample = (int)(i * ratio); output[i] = QMath.Lerp(samples[sample], samples[sample + 1], i * ratio % 1); } for (int i = lerpUntil; i < to; ++i) { output[i] = samples[(int)(i * ratio)]; } return(output); }
void Update() { float[][] samples = Source.cavernSource.Rendered; if (samples != null) { float peakSize = float.NegativeInfinity; for (int channel = 0; channel < samples.Length; ++channel) { float channelSize = 20 * (float)Math.Log10(WaveformUtils.GetPeak(samples[channel])); if (channelSize < -600) { channelSize = -600; } if (peakSize < channelSize) { peakSize = channelSize; } } float size = Mathf.Clamp(peakSize / -DynamicRange + 1, 0, 1); scale = QMath.Lerp(scale, (MaxSize - MinSize) * size + MinSize, 1 - Smoothing); transform.localScale = new Vector3(scale, scale, scale); } }
/// <summary> /// Process the source and returns a mix to be added to the output. /// </summary> protected internal virtual float[] Collect() { // Preparations, clean environment int channels = Listener.Channels.Length, updateRate = listener.UpdateRate; Array.Clear(rendered, 0, rendered.Length); // Render audio if not muted if (!Mute) { int clipChannels = Clip.Channels; // 3D renderer preprocessing if (SpatialBlend != 0) { if (listener.AudioQuality >= QualityModes.High && clipChannels != 1) // Mono downmix above medium quality { Array.Clear(samples, 0, PitchedUpdateRate); for (int channel = 0; channel < clipChannels; ++channel) { WaveformUtils.Mix(Rendered[channel], samples); } WaveformUtils.Gain(samples, 1f / clipChannels); } else // First channel only otherwise { Array.Copy(Rendered[0], samples, PitchedUpdateRate); } } // 1D renderer if (SpatialBlend != 1) { float volume1D = Volume * (1f - SpatialBlend); // 1:1 mix for non-stereo sources if (clipChannels != 2) { samples = Resample.Adaptive(samples, updateRate, listener.AudioQuality); WriteOutput(samples, rendered, volume1D, channels); } // Full side mix for stereo sources else { Array.Copy(Rendered[0], leftSamples, PitchedUpdateRate); Array.Copy(Rendered[1], rightSamples, PitchedUpdateRate); leftSamples = Resample.Adaptive(leftSamples, updateRate, listener.AudioQuality); rightSamples = Resample.Adaptive(rightSamples, updateRate, listener.AudioQuality); Stereo1DMix(volume1D); } } // 3D mix, if the source is in range if (SpatialBlend != 0 && distance < listener.Range) { Vector3 direction = (Position - listener.Position).RotateInverse(listener.Rotation); float rolloffDistance = GetRolloff(); samples = Resample.Adaptive(samples, updateRate, listener.AudioQuality); baseUpdateRate = samples.Length; // Apply filter if set if (SpatialFilter != null) { SpatialFilter.Process(samples); } // Distance simulation for HRTF // TODO: gain correction for this in both engines if (DistanceSimulation && Listener.HeadphoneVirtualizer) { if (distancer == null) { distancer = new Distancer(this); } distancer.Generate(direction.X > 0, samples); } // ------------------------------------------------------------------ // Balance-based engine for symmetrical layouts // ------------------------------------------------------------------ if (Listener.IsSymmetric) { float volume3D = Volume * rolloffDistance * SpatialBlend; if (!LFE) { // Find a bounding box int bottomFrontLeft = -1, bottomFrontRight = -1, bottomRearLeft = -1, bottomRearRight = -1, topFrontLeft = -1, topFrontRight = -1, topRearLeft = -1, topRearRight = -1; // Closest layers on Y and Z axes float closestTop = 78, closestBottom = -73, closestTF = 75, closestTR = -73, closestBF = 75, closestBR = -69; // Find closest horizontal layers if (Listener.HeadphoneVirtualizer) { direction = direction.WarpToCube() / Listener.EnvironmentSize; } else { direction /= Listener.EnvironmentSize; } for (int channel = 0; channel < channels; ++channel) { if (!Listener.Channels[channel].LFE) { float channelY = Listener.Channels[channel].CubicalPos.Y; if (channelY < direction.Y) { if (channelY > closestBottom) { closestBottom = channelY; } } else if (channelY < closestTop) { closestTop = channelY; } } } for (int channel = 0; channel < channels; ++channel) { if (!Listener.Channels[channel].LFE) { Vector3 channelPos = Listener.Channels[channel].CubicalPos; if (channelPos.Y == closestBottom) // Bottom layer { AssignHorizontalLayer(channel, ref bottomFrontLeft, ref bottomFrontRight, ref bottomRearLeft, ref bottomRearRight, ref closestBF, ref closestBR, direction, channelPos); } if (channelPos.Y == closestTop) // Top layer { AssignHorizontalLayer(channel, ref topFrontLeft, ref topFrontRight, ref topRearLeft, ref topRearRight, ref closestTF, ref closestTR, direction, channelPos); } } } // Fix incomplete top layer FixIncompleteLayer(ref topFrontLeft, ref topFrontRight, ref topRearLeft, ref topRearRight); // When the bottom layer is completely empty (= the source is below all channels), copy the top layer if (bottomFrontLeft == -1 && bottomFrontRight == -1 && bottomRearLeft == -1 && bottomRearRight == -1) { bottomFrontLeft = topFrontLeft; bottomFrontRight = topFrontRight; bottomRearLeft = topRearLeft; bottomRearRight = topRearRight; } // Fix incomplete bottom layer else { FixIncompleteLayer(ref bottomFrontLeft, ref bottomFrontRight, ref bottomRearLeft, ref bottomRearRight); } // When the top layer is completely empty (= the source is above all channels), copy the bottom layer if (topFrontLeft == -1 || topFrontRight == -1 || topRearLeft == -1 || topRearRight == -1) { topFrontLeft = bottomFrontLeft; topFrontRight = bottomFrontRight; topRearLeft = bottomRearLeft; topRearRight = bottomRearRight; } // Spatial mix gain precalculation Vector2 layerVol = new Vector2(.5f); // (bottom; top) if (topFrontLeft != bottomFrontLeft) // Height ratio calculation { float bottomY = Listener.Channels[bottomFrontLeft].CubicalPos.Y; layerVol.Y = (direction.Y - bottomY) / (Listener.Channels[topFrontLeft].CubicalPos.Y - bottomY); layerVol.X = 1f - layerVol.Y; } // Length ratios (bottom; top) Vector2 frontVol = new Vector2(LengthRatio(bottomRearLeft, bottomFrontLeft, direction.Z), LengthRatio(topRearLeft, topFrontLeft, direction.Z)); // Width ratios float BFRVol = WidthRatio(bottomFrontLeft, bottomFrontRight, direction.X), BRRVol = WidthRatio(bottomRearLeft, bottomRearRight, direction.X), TFRVol = WidthRatio(topFrontLeft, topFrontRight, direction.X), TRRVol = WidthRatio(topRearLeft, topRearRight, direction.X), innerVolume3D = volume3D; if (Size != 0) { frontVol = QMath.Lerp(frontVol, new Vector2(.5f), Size); BFRVol = QMath.Lerp(BFRVol, .5f, Size); BRRVol = QMath.Lerp(BRRVol, .5f, Size); TFRVol = QMath.Lerp(TFRVol, .5f, Size); TRRVol = QMath.Lerp(TRRVol, .5f, Size); innerVolume3D *= 1f - Size; float extraChannelVolume = volume3D * Size / channels; for (int channel = 0; channel < channels; ++channel) { WriteOutput(samples, rendered, extraChannelVolume, channel, channels); } } // Spatial mix gain finalization Vector2 rearVol = new Vector2(1) - frontVol; layerVol *= innerVolume3D; frontVol *= layerVol; rearVol *= layerVol; WriteOutput(samples, rendered, frontVol.X * (1f - BFRVol), bottomFrontLeft, channels); WriteOutput(samples, rendered, frontVol.X * BFRVol, bottomFrontRight, channels); WriteOutput(samples, rendered, rearVol.X * (1f - BRRVol), bottomRearLeft, channels); WriteOutput(samples, rendered, rearVol.X * BRRVol, bottomRearRight, channels); WriteOutput(samples, rendered, frontVol.Y * (1f - TFRVol), topFrontLeft, channels); WriteOutput(samples, rendered, frontVol.Y * TFRVol, topFrontRight, channels); WriteOutput(samples, rendered, rearVol.Y * (1f - TRRVol), topRearLeft, channels); WriteOutput(samples, rendered, rearVol.Y * TRRVol, topRearRight, channels); } // LFE mix if (!listener.LFESeparation || LFE) { for (int channel = 0; channel < channels; ++channel) { if (Listener.Channels[channel].LFE) { WriteOutput(samples, rendered, volume3D, channel, channels); } } } } // ------------------------------------------------------------------ // Directional/distance-based engine for asymmetrical layouts // ------------------------------------------------------------------ else { // Angle match calculations float[] angleMatches; if (listener.AudioQuality >= QualityModes.High) { angleMatches = CalculateAngleMatches(channels, direction); } else { angleMatches = LinearizeAngleMatches(channels, direction); } // Object size extension if (Size != 0) { float maxAngleMatch = angleMatches[0]; for (int channel = 1; channel < channels; ++channel) { if (maxAngleMatch < angleMatches[channel]) { maxAngleMatch = angleMatches[channel]; } } for (int channel = 0; channel < channels; ++channel) { angleMatches[channel] = QMath.Lerp(angleMatches[channel], maxAngleMatch, Size); } } // Only use the closest 3 speakers on non-Perfect qualities or in Theatre mode if (listener.AudioQuality != QualityModes.Perfect || Listener.EnvironmentType == Environments.Theatre) { float top0 = 0, top1 = 0, top2 = 0; for (int channel = 0; channel < channels; ++channel) { if (!Listener.Channels[channel].LFE) { float match = angleMatches[channel]; if (top0 < match) { top2 = top1; top1 = top0; top0 = match; } else if (top1 < match) { top2 = top1; top1 = match; } else if (top2 < match) { top2 = match; } } } for (int channel = 0; channel < channels; ++channel) { if (!Listener.Channels[channel].LFE && angleMatches[channel] != top0 && angleMatches[channel] != top1 && angleMatches[channel] != top2) { angleMatches[channel] = 0; } } } float totalAngleMatch = 0; for (int channel = 0; channel < channels; ++channel) { totalAngleMatch += angleMatches[channel] * angleMatches[channel]; } totalAngleMatch = (float)Math.Sqrt(totalAngleMatch); // Place in sphere, write data to output channels float volume3D = Volume * rolloffDistance * SpatialBlend / totalAngleMatch; for (int channel = 0; channel < channels; ++channel) { if (Listener.Channels[channel].LFE) { if (!listener.LFESeparation || LFE) { WriteOutput(samples, rendered, volume3D * totalAngleMatch, channel, channels); } } else if (!LFE && angleMatches[channel] != 0) { WriteOutput(samples, rendered, volume3D * angleMatches[channel], channel, channels); } } } } } // Timing TimeSamples += PitchedUpdateRate; if (TimeSamples >= Clip.Samples) { if (Loop) { TimeSamples %= Clip.Samples; } else { TimeSamples = 0; IsPlaying = false; } } return(rendered); }
void Update() { if (!CavernSource) { OnDisable(); return; } for (int row = 0; row < Rows; ++row) { for (int column = 0; column < Columns; ++column) { SeatMovements[row][column].Height = 200; } } int lastRow = Rows - 1, lastColumn = Columns - 1; if (CavernSource[0] != null) // Front left { SeatMovements[0][0].Height = CavernSource[0].Height; } if (CavernSource[1] != null) // Front right { SeatMovements[0][lastColumn].Height = CavernSource[1].Height; } if (CavernSource[2] != null && CavernSource[2].Height != Cavernizer.unsetHeight) // Center { SeatMovements[0][lastColumn / 2].Height = CavernSource[2].Height; } if (CavernSource[6] != null) // Side left { SeatMovements[lastRow][0].Height = CavernSource[6].Height; } if (CavernSource[7] != null) // Side right { SeatMovements[lastRow][lastColumn].Height = CavernSource[7].Height; } // Addition is okay, and should be used, as the rears are near the sides in the back corners. if (CavernSource[4] != null) // Rear left { SeatMovements[lastRow][0].Height += CavernSource[4].Height; } if (CavernSource[5] != null) // Rear right { SeatMovements[lastRow][lastColumn].Height += CavernSource[5].Height; } SpatializedChannel rearCenter = CavernSource.GetChannel(ReferenceChannel.RearCenter); if (rearCenter != null) // Rear center { SeatMovements[lastRow][lastColumn / 2].Height = rearCenter.Height; } // Use the front channels for moving all seats if nothing else is available for the rear sides if (SeatMovements[lastRow][0].Height == 200) { SeatMovements[lastRow][0].Height = SeatMovements[0][0].Height; } if (SeatMovements[lastRow][lastColumn].Height == 200) { SeatMovements[lastRow][lastColumn].Height = SeatMovements[0][lastColumn].Height; } // Seat position interpolation for (int row = 0; row < Rows; ++row) { int prev = 0; for (int column = 0; column < Columns; ++column) { if (SeatMovements[row][column].Height != 200) { float lerpDiv = column - prev; for (int oldColumn = prev; oldColumn < column; ++oldColumn) { SeatMovements[row][oldColumn].Height = QMath.Lerp(SeatMovements[row][prev].Height, SeatMovements[row][column].Height, (oldColumn - prev) / lerpDiv); } prev = column; } } if (prev != lastColumn) { float lerpDiv = lastColumn - prev; for (int oldColumn = prev; oldColumn < lastColumn; ++oldColumn) { SeatMovements[row][oldColumn].Height = QMath.Lerp(SeatMovements[row][prev].Height, SeatMovements[row][lastColumn].Height, (oldColumn - prev) / lerpDiv); } } } for (int column = 0; column < Columns; ++column) { int prev = 0; for (int row = 0; row < Rows; ++row) { if (SeatMovements[row][column].Height != 200) { float lerpDiv = row - prev; for (int oldRow = prev; oldRow < row; ++oldRow) { SeatMovements[oldRow][column].Height = QMath.Lerp(SeatMovements[prev][column].Height, SeatMovements[row][column].Height, (oldRow - prev) / lerpDiv); } prev = row; } } if (prev != lastRow) { float lerpDiv = lastRow - prev; for (int oldRow = prev; oldRow < lastRow; ++oldRow) { SeatMovements[oldRow][column].Height = QMath.Lerp(SeatMovements[prev][column].Height, SeatMovements[lastRow][column].Height, (oldRow - prev) / lerpDiv); } } } // Seat rotation interpolation for (int row = 0; row < Rows; ++row) { SeatMovements[row][0].Rotation.z = Mathf.Clamp((SeatMovements[row][1].Height - SeatMovements[row][0].Height) * RotationConstant * 2, -MaxRotationSide, MaxRotationSide); for (int column = 1; column < lastColumn; ++column) { SeatMovements[row][column].Rotation.z = Mathf.Clamp((SeatMovements[row][column + 1].Height - SeatMovements[row][column - 1].Height) * RotationConstant, -MaxRotationSide, MaxRotationSide); } SeatMovements[row][lastColumn].Rotation.z = Mathf.Clamp((SeatMovements[row][lastColumn].Height - SeatMovements[row][lastColumn - 1].Height) * RotationConstant * 2, -MaxRotationSide, MaxRotationSide); } for (int column = 0; column < Columns; ++column) { SeatMovements[0][column].Rotation.x = Mathf.Clamp((SeatMovements[1][column].Height - SeatMovements[0][column].Height) * RotationConstant * 2, -20, 20); for (int row = 1; row < lastRow; ++row) { SeatMovements[row][column].Rotation.x = Mathf.Clamp((SeatMovements[row + 1][column].Height - SeatMovements[row - 1][column].Height) * RotationConstant, -MaxRotationFace, MaxRotationFace); } SeatMovements[lastRow][column].Rotation.x = Mathf.Clamp((SeatMovements[lastRow][column].Height - SeatMovements[lastRow - 1][column].Height) * RotationConstant * 2, -MaxRotationFace, MaxRotationFace); } }