Skip to content

Commit 7a5d4ec

Browse files
committed
Added new audio limiter & more:
- Implemented audo limiter - Implemented improved audio file length and seeking calculation (WIP) - Fixed bug where untrimmed audio cues wouldn't loop - Fixed some audio meetering bugs - Implemented new vectorized methods - Peak file format updated to help with file seeking - UI tweaks + misc refactorings
1 parent ba0b44d commit 7a5d4ec

23 files changed

Lines changed: 1231 additions & 60 deletions
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
using NAudio.Wave;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading;
9+
10+
namespace QPlayer.Audio;
11+
12+
public class AudioLimiterSampleProvider : ISamplePositionProvider
13+
{
14+
private readonly ISamplePositionProvider source;
15+
private readonly DelaySampleProvider delay;
16+
private readonly int channelCount;
17+
private readonly int fs;
18+
private readonly float[] envBuff;
19+
private int attack = 1;
20+
private int release = 1;
21+
private float threshold;
22+
private readonly Lock lockObj;
23+
24+
// State
25+
private float minHoldVal = 1;
26+
private int minHoldTime = int.MaxValue;
27+
private float relLastMin = 0;
28+
private float relLastRes = 1;
29+
private float smoothV1 = 1;
30+
private float smoothV2 = 1;
31+
private int meterSampleCount;
32+
private float meterLastG = 1;
33+
34+
const int BLOCK_SIZE = 1024;
35+
const int MAX_ATTACK = 4096;
36+
37+
public float AttackTime
38+
{
39+
get => attack / (float)(fs * channelCount);
40+
set => attack = Math.Clamp((int)(value * (fs * channelCount)), 5, MAX_ATTACK);
41+
}
42+
43+
public float ReleaseTime
44+
{
45+
get => release / (float)(fs * channelCount);
46+
set => release = Math.Max(5, (int)(value * (fs * channelCount)));
47+
}
48+
49+
public float Threshold
50+
{
51+
get => threshold;
52+
set => threshold = value;
53+
}
54+
55+
public bool Enabled { get; set; } = true;
56+
public float InputGain { get; set; } = 1;
57+
58+
public long Position
59+
{
60+
get => Math.Max(0, source.Position - attack);
61+
set => source.Position = Math.Max(0, source.Position - attack);
62+
}
63+
64+
public WaveFormat WaveFormat => source.WaveFormat;
65+
66+
/// <summary>
67+
/// The number of samples to read between each metering event.
68+
/// </summary>
69+
public int SamplesPerNotification { get; set; }
70+
71+
/// <summary>
72+
/// An event raised every <see cref="SamplesPerNotification"/> samples with metering information.
73+
/// </summary>
74+
public event Action<float>? OnMeter;
75+
76+
public AudioLimiterSampleProvider(ISamplePositionProvider source)
77+
{
78+
this.source = source;
79+
this.channelCount = source.WaveFormat.Channels;
80+
this.fs = source.WaveFormat.SampleRate;
81+
lockObj = new();
82+
envBuff = new float[BLOCK_SIZE];
83+
delay = new(source, MAX_ATTACK);
84+
}
85+
86+
public int Read(float[] buffer, int offset, int count)
87+
{
88+
if (!Enabled)
89+
{
90+
return source.Read(buffer, offset, count);
91+
}
92+
93+
using var _ = lockObj.EnterScope();
94+
95+
count = Math.Min(count, BLOCK_SIZE);
96+
int read = delay.Read(envBuff, 0, count);
97+
98+
// Clip
99+
VectorExtensions.ClipGR(envBuff.AsSpan(0, read), InputGain, threshold);
100+
101+
// Moving min --> vey slow, replaced with min-hold
102+
/*movingMinDelay.Push(envBuff.AsSpan(0, read));
103+
for (int i = 0; i < read; i++)
104+
{
105+
var first = movingMinDelay.GetDelayed(attack, read - i, out var second);
106+
float min = VectorExtensions.Min(first);
107+
if (second.Length > 0)
108+
min = MathF.Min(min, VectorExtensions.Min(second));
109+
envBuff[i] = min;
110+
}*/
111+
112+
// Min hold
113+
float minVal = minHoldVal;
114+
int minTime = minHoldTime;
115+
for (int i = 0; i < read; i++)
116+
{
117+
float v = envBuff[i];
118+
if (v < minVal || minTime >= attack)
119+
{
120+
minTime = 0;
121+
minVal = v;
122+
}
123+
else
124+
{
125+
minTime++;
126+
envBuff[i] = v;
127+
}
128+
}
129+
minHoldVal = minVal;
130+
minHoldTime = minTime;
131+
132+
// Exponential release stage
133+
float gradientFactor = 1f / (release + 1);
134+
// releaseDelay.Push(envBuff.AsSpan(0, read));
135+
float last = relLastMin;
136+
float output = relLastRes;
137+
for (int i = 0; i < read; i++)
138+
{
139+
float min = envBuff[i];
140+
output += (min - output) * gradientFactor;
141+
output = MathF.Min(output, min);
142+
last = min;
143+
envBuff[i] = output;
144+
}
145+
relLastMin = last;
146+
relLastRes = output;
147+
148+
// Smoothing - A weighted moving average (a convolution of a window function) --> tooo slowww
149+
/*smootherDelay.Push(envBuff.AsSpan(0, read));
150+
float recip = 1f/windowSum;
151+
for (int i = 0; i < read; i++)
152+
{
153+
var first = smootherDelay.GetDelayed(attack, read - i, out var second);
154+
float sum = VectorExtensions.Convolve(first, smootherWindow);
155+
if (second.Length > 0)
156+
sum += VectorExtensions.Convolve(second, smootherWindow.AsSpan(first.Length));
157+
envBuff[i] = sum * recip;
158+
}*/
159+
// Smoothing - Exponential/Constant rate hybrid
160+
float rate = 5 / (float)attack;
161+
float v1 = smoothV1;
162+
float v2 = smoothV2;
163+
for (int i = 0; i < read; i++)
164+
{
165+
var v = envBuff[i];
166+
v1 = Math.Max(v, v1 + (v - 1) * rate);
167+
var n = v2 + (v1 - v2) * rate * 3;
168+
v2 = Math.Clamp(n, v1, 1);
169+
envBuff[i] = v2;
170+
}
171+
smoothV1 = v1;
172+
smoothV2 = v2;
173+
174+
// TODO: > 2 channel support?
175+
if (channelCount == 2)
176+
{
177+
// Link stereo limiting by computing the min per 2-samples
178+
VectorExtensions.StereoMin(envBuff);
179+
}
180+
181+
int k = 0;
182+
float meterMin = meterLastG;
183+
while (k < read && SamplesPerNotification > 0)
184+
{
185+
int toTake = Math.Min(SamplesPerNotification - meterSampleCount, read - k);
186+
meterMin = MathF.Min(meterMin, VectorExtensions.Min(envBuff.AsSpan(k, toTake)));
187+
k += toTake;
188+
meterSampleCount += toTake;
189+
if (meterSampleCount >= SamplesPerNotification)
190+
{
191+
NotifySample(meterMin);//(1 / meterMin - 1);
192+
meterMin = 1;
193+
}
194+
}
195+
meterLastG = meterMin;
196+
197+
if (MathF.Abs(envBuff[0]) > 1.5f)
198+
Debugger.Break();
199+
200+
read = delay.ReadDelayed(buffer, offset, read, attack);
201+
VectorExtensions.Multiply(buffer.AsSpan(offset, read), envBuff.AsSpan(0, read));
202+
VectorExtensions.Multiply(buffer.AsSpan(offset, read), InputGain);
203+
return read;
204+
205+
void NotifySample(float meterMin)
206+
{
207+
float gr = 1 - meterMin;
208+
OnMeter?.Invoke(gr);
209+
meterSampleCount = 0;
210+
}
211+
}
212+
213+
/*[MemberNotNull(nameof(smootherWindow))]
214+
private void CreateSmootherWindow()
215+
{
216+
smootherWindow = new float[attack];
217+
// Blackmann window
218+
float alpha = 0.16f;
219+
float a0 = (1 - alpha) / 2;
220+
float a1 = 0.5f;
221+
float a2 = alpha / 2;
222+
windowSum = 0;
223+
for (int i = 0; i < smootherWindow.Length; i++)
224+
{
225+
float t = MathF.Tau * i / (float)smootherWindow.Length;
226+
float x = a0 - a1 * MathF.Cos(t) + a2 * MathF.Cos(2 * t);
227+
smootherWindow[i] = x;
228+
windowSum += x;
229+
}
230+
}*/
231+
}

QPlayer/Audio/AudioPlaybackManager.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using NAudio.CoreAudioApi;
22
using NAudio.Wave;
3-
using NAudio.Wave.SampleProviders;
43
using QPlayer.ViewModels;
54
using System;
65
using System.Collections.Generic;
@@ -79,6 +78,20 @@ public AudioPlaybackManager(MainViewModel mainViewModel)
7978
meteringProvider.SamplesPerNotification = mixer.WaveFormat.SampleRate / 30;
8079
}
8180

81+
/// <summary>
82+
/// When building a mastering chain, use this sample provider as the source, it should never be read from directly.
83+
/// </summary>
84+
public ISamplePositionProvider MixerSampleProvider => mixer;
85+
86+
/// <summary>
87+
/// Registers the given sample provider as a mastering chain, this is the last sample provider before the output.
88+
/// </summary>
89+
/// <param name="sampleProvider"></param>
90+
public void RegisterMasterChain(ISampleProvider sampleProvider)
91+
{
92+
meteringProvider.Source = sampleProvider;
93+
}
94+
8295
public void Stop()
8396
{
8497
device?.Stop();

QPlayer/Audio/DelayBuffer.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using System.Numerics;
3+
4+
namespace QPlayer.Audio;
5+
6+
public class DelayBuffer
7+
{
8+
private readonly float[] delayBuff;
9+
protected readonly int maxDelayTime;
10+
private int headPos;
11+
12+
public DelayBuffer(uint maxDelayTime)
13+
{
14+
this.maxDelayTime = (int)maxDelayTime;
15+
delayBuff = new float[BitOperations.RoundUpToPowerOf2(maxDelayTime) << 1];
16+
}
17+
18+
public Span<float> GetDelayed(int count, int delaySamples, out Span<float> secondHalf)
19+
{
20+
// Rewind the head based on how much we're delaying and how many samples we need to retrieve
21+
int pos = headPos - delaySamples - count;
22+
int available = headPos - pos;
23+
pos &= delayBuff.Length - 1; // Wrap the position
24+
int toRead = Math.Min(count, available);
25+
secondHalf = default;
26+
27+
int firstHalf = delayBuff.Length - pos;
28+
if (toRead >= firstHalf)
29+
{
30+
// Split span
31+
secondHalf = delayBuff.AsSpan(0, toRead - firstHalf);
32+
return delayBuff.AsSpan(pos, firstHalf);
33+
}
34+
else
35+
{
36+
// Contiguous span
37+
return delayBuff.AsSpan(pos, toRead);
38+
}
39+
}
40+
41+
/// <summary>
42+
/// Pushes the span of samples into this delay buffer.
43+
/// </summary>
44+
/// <param name="values"></param>
45+
public void Push(ReadOnlySpan<float> values)
46+
{
47+
int firstHalf = delayBuff.Length - headPos;
48+
if (values.Length >= firstHalf)
49+
{
50+
// Split copy
51+
values[..firstHalf].CopyTo(delayBuff.AsSpan(headPos, firstHalf));
52+
values[firstHalf..].CopyTo(delayBuff.AsSpan(0));
53+
}
54+
else
55+
{
56+
// Contiguous copy
57+
values.CopyTo(delayBuff.AsSpan(headPos));
58+
}
59+
headPos = (headPos + values.Length) & (delayBuff.Length - 1);
60+
}
61+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using NAudio.Wave;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Numerics;
5+
using System.Text;
6+
using System.Threading;
7+
8+
namespace QPlayer.Audio;
9+
10+
public class DelaySampleProvider : DelayBuffer, ISamplePositionProvider
11+
{
12+
private readonly ISamplePositionProvider source;
13+
private readonly WaveFormat waveFormat;
14+
15+
public WaveFormat WaveFormat => waveFormat;
16+
17+
public long Position
18+
{
19+
get => source.Position;
20+
set => source.Position = value;
21+
}
22+
23+
public DelaySampleProvider(ISamplePositionProvider source, uint maxDelayTime) : base(maxDelayTime)
24+
{
25+
this.source = source;
26+
this.waveFormat = source.WaveFormat;
27+
}
28+
29+
/// <summary>
30+
/// Reads the specified number of samples from the input stream and pushes them to the delay buffer.
31+
/// </summary>
32+
/// <param name="buffer"></param>
33+
/// <param name="offset"></param>
34+
/// <param name="count"></param>
35+
/// <returns></returns>
36+
public int Read(float[] buffer, int offset, int count)
37+
{
38+
// Read enough samples to fill at most half the buffer (half the buffer represents the maximum delay time)
39+
int read = source.Read(buffer, offset, Math.Min(count, maxDelayTime));
40+
41+
// Now copy these samples into the delay buffer
42+
Push(buffer.AsSpan(offset, read));
43+
44+
return read;
45+
}
46+
47+
/// <summary>
48+
/// Reads the specified number of samples from the delay buffer with the specified amount of delay.
49+
/// This should imediately preceed a call to <see cref="Read(float[], int, int)"/> and should read
50+
/// the same number of samples the read call returned.
51+
/// </summary>
52+
/// <param name="buffer"></param>
53+
/// <param name="offset"></param>
54+
/// <param name="count"></param>
55+
/// <returns></returns>
56+
public int ReadDelayed(float[] buffer, int offset, int count, int delaySamples)
57+
{
58+
var firstHalf = GetDelayed(count, delaySamples, out var secondHalf);
59+
firstHalf.CopyTo(buffer.AsSpan(offset));
60+
secondHalf.CopyTo(buffer.AsSpan(offset + firstHalf.Length));
61+
62+
return firstHalf.Length + secondHalf.Length;
63+
}
64+
}

0 commit comments

Comments
 (0)