Skip to content

Commit 840dcb7

Browse files
committed
Audio processing improvements
- Move metering calculations to audio thread - Try for 48K input if available - Better state management for I/O switching, enabled/disabled toggle
1 parent bcc16ba commit 840dcb7

File tree

10 files changed

+475
-287
lines changed

10 files changed

+475
-287
lines changed

src/main/java/heronarts/lx/audio/BandFilter.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
@LXModulator.Device("Band Filter")
3333
public class BandFilter extends LXModulator implements LXNormalizedParameter, LXOscComponent {
3434

35+
private static final int NYQUIST_FREQ = 24000;
36+
3537
/**
3638
* Gain of the meter, in decibels
3739
*/
@@ -110,15 +112,14 @@ public BandFilter(String label, LX lx) {
110112
public BandFilter(String label, GraphicMeter meter) {
111113
super(label);
112114

113-
this.impl = new LXMeterImpl(meter.numBands, meter.fft.getBandOctaveRatio());
115+
this.impl = new LXMeterImpl(meter.numBands);
114116
this.meter = meter;
115117

116-
final int nyquist = meter.fft.getSampleRate() / 2;
117-
this.minFreq = new BoundedParameter("Min Freq", 60, 0, nyquist)
118+
this.minFreq = new BoundedParameter("Min Freq", 60, 0, NYQUIST_FREQ)
118119
.setDescription("Minimum frequency the gate responds to")
119120
.setExponent(4)
120121
.setUnits(LXParameter.Units.HERTZ);
121-
this.maxFreq = new BoundedParameter("Max Freq", 120, 0, nyquist)
122+
this.maxFreq = new BoundedParameter("Max Freq", 120, 0, NYQUIST_FREQ)
122123
.setDescription("Maximum frequency the gate responds to")
123124
.setExponent(4)
124125
.setUnits(LXParameter.Units.HERTZ);
@@ -195,7 +196,7 @@ protected double computeValue(double deltaMs) {
195196
float newAverage = this.meter.fft.getAverage(this.minFreq.getValuef(), this.maxFreq.getValuef()) / this.meter.fft.getSize();
196197
float averageGain = (newAverage >= this.averageRaw) ? attackGain : releaseGain;
197198
this.averageRaw = newAverage + averageGain * (this.averageRaw - newAverage);
198-
double averageDb = 20 * Math.log(this.averageRaw) / DecibelMeter.LOG_10 + gainValue + slopeValue * this.averageOctave;
199+
double averageDb = DecibelMeter.amplitudeToDecibels(this.averageRaw) + gainValue + slopeValue * this.averageOctave;
199200
this.averageNorm = 1 + averageDb / rangeValue;
200201

201202
return LXUtils.constrain(this.averageNorm, 0, 1);

src/main/java/heronarts/lx/audio/DecibelMeter.java

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
public class DecibelMeter extends LXModulator implements LXNormalizedParameter, LXOscComponent {
3434

3535
protected static final double LOG_10 = Math.log(10);
36+
protected static final double RATIO_20_LOG10 = 20. / LOG_10;
3637

3738
protected LXAudioBuffer buffer;
3839

@@ -94,15 +95,16 @@ private static class Parameters {
9495

9596
private final static double PEAK_HOLD_MS = 250;
9697

97-
protected float attackGain;
98-
protected float releaseGain;
98+
protected double attackGain;
99+
protected double releaseGain;
99100

100101
private float rmsRaw = 0;
101-
private float rmsLevel = 0;
102-
private double dbLevel = -96;
102+
private double rmsEnv = 0;
103+
private double rmsPeak = 0;
103104

104-
private float rmsPeak = 0;
105+
private double dbEnv = -96;
105106
private double dbPeak = 0;
107+
106108
private double normalizedPeak = 0;
107109
private double peakMillis = 0;
108110

@@ -135,22 +137,39 @@ public DecibelMeter(String label, LXAudioBuffer buffer, Parameters params) {
135137

136138
public DecibelMeter(String label, LXAudioBuffer buffer, CompoundParameter gain, CompoundParameter range, CompoundParameter attack, CompoundParameter release) {
137139
super(label);
138-
this.buffer = buffer;
139140
this.gain = gain;
140141
this.range = range;
141142
this.attack = attack;
142143
this.release = release;
144+
setBuffer(buffer);
143145
}
144146

145-
public DecibelMeter setBuffer(LXAudioBuffer buffer) {
147+
public final DecibelMeter setBuffer(LXAudioBuffer buffer) {
148+
if (this.buffer != null) {
149+
this.buffer.removeMeter(this);
150+
}
146151
this.buffer = buffer;
152+
if (this.buffer != null) {
153+
this.buffer.addMeter(this);
154+
}
147155
return this;
148156
}
149157

150158
public double getExponent() {
151159
throw new UnsupportedOperationException("DecibelMeter does not support exponent");
152160
}
153161

162+
/**
163+
* Converts an amplitude (normalized 0-1, typically RMS) to decibels, negative
164+
* value up to 0dbFS
165+
*
166+
* @param amplitude Normalized amplitude, 0-1
167+
* @return Decibel value, negative infinity -> max 0dBFS
168+
*/
169+
public static double amplitudeToDecibels(double amplitude) {
170+
return RATIO_20_LOG10 * Math.log(amplitude);
171+
}
172+
154173
/**
155174
* Return raw underlying levels, no attack/gain smoothing
156175
*
@@ -164,7 +183,7 @@ public float getRaw() {
164183
* @return Raw decibel value of the meter
165184
*/
166185
public double getDecibels() {
167-
return this.dbLevel;
186+
return this.dbEnv;
168187
}
169188

170189
/**
@@ -189,37 +208,53 @@ public float getSquaref() {
189208
return (float) getSquare();
190209
}
191210

192-
@Override
193-
protected double computeValue(double deltaMs) {
194-
double releaseValue = this.release.getValue();
195-
this.attackGain = (float) Math.exp(-deltaMs / this.attack.getValue());
196-
this.releaseGain = (float) Math.exp(-deltaMs / releaseValue);
197-
211+
/**
212+
* Compute new values when a frame of audio input is received. This is called by the
213+
* thread that has filled the audio buffer.
214+
*/
215+
protected void onAudioFrame() {
198216
this.rmsRaw = this.buffer.getRms();
199-
float rmsGain = (this.rmsRaw >= this.rmsLevel) ? this.attackGain : this.releaseGain;
200-
this.rmsLevel = this.rmsRaw + rmsGain * (this.rmsLevel - this.rmsRaw);
217+
218+
this.attackGain = Math.exp(-this.buffer.bufferSize() / (this.attack.getValue() * this.buffer.sampleRate() * .001));
219+
this.releaseGain = Math.exp(-this.buffer.bufferSize() / (this.release.getValue() * this.buffer.sampleRate() * .001));
220+
221+
final double gain = (this.rmsRaw > this.rmsEnv) ? this.attackGain : this.releaseGain;
222+
this.rmsEnv = (this.rmsRaw + gain * (this.rmsEnv - this.rmsRaw));
201223

202224
if (this.rmsRaw > this.rmsPeak) {
203225
this.rmsPeak = this.rmsRaw;
204226
this.peakMillis = 0;
205227
} else {
206-
this.peakMillis += deltaMs;
228+
this.peakMillis += this.buffer.bufferSize() * 1000. / this.buffer.sampleRate();
207229
if (this.peakMillis > PEAK_HOLD_MS) {
208-
double peakReleaseTime = Math.min(deltaMs, this.peakMillis - PEAK_HOLD_MS);
209-
float releaseGain = (float) Math.exp(-peakReleaseTime / releaseValue);
210-
this.rmsPeak *= releaseGain;
230+
final double r = Math.exp(-this.buffer.bufferSize() / (this.release.getValue() * .001 * this.buffer.sampleRate()));
231+
this.rmsPeak = this.rmsRaw + r * (this.rmsPeak - this.rmsRaw);
211232
}
212233
}
213-
double range = this.range.getValue();
214-
double gain = this.gain.getValue();
234+
}
235+
236+
@Override
237+
protected double computeValue(double deltaMs) {
238+
final double range = this.range.getValue();
239+
final double gain = this.gain.getValue();
215240

216-
this.dbPeak = 20 * Math.log(this.rmsPeak) / LOG_10 + gain;
241+
this.dbPeak = amplitudeToDecibels(this.rmsPeak) + gain;
217242
this.normalizedPeak = LXUtils.constrain(1 + this.dbPeak / range, 0, 1);
218243

219-
this.dbLevel = 20 * Math.log(this.rmsLevel) / LOG_10 + gain;
220-
return LXUtils.constrain(1 + this.dbLevel / range, 0, 1);
244+
this.dbEnv = amplitudeToDecibels(this.rmsEnv) + gain;
245+
return LXUtils.constrain(1 + this.dbEnv / range, 0, 1);
246+
}
247+
248+
@Override
249+
protected void onStop() {
250+
super.onStop();
251+
this.rmsRaw = 0;
252+
this.rmsEnv = this.dbEnv = 0;
253+
this.rmsPeak = this.dbPeak = this.normalizedPeak = 0;
254+
setValue(0);
221255
}
222256

257+
223258
@Override
224259
public LXNormalizedParameter setNormalized(double value) {
225260
throw new UnsupportedOperationException("Cannot setNormalized on DecibelMeter");

src/main/java/heronarts/lx/audio/FourierTransform.java

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
package heronarts.lx.audio;
2020

21+
import java.util.Arrays;
22+
2123
public class FourierTransform {
2224

2325
public static final float LOG_2 = (float) Math.log(2);
@@ -40,8 +42,6 @@ public float getCoefficient(int i, int n) {
4042
private Window window = Window.HAMMING;
4143

4244
private final int bufferSize;
43-
private final int sampleRate;
44-
private final float bandWidthInv;
4545

4646
private final int logN;
4747
private final float[] sinN;
@@ -53,20 +53,25 @@ public float getCoefficient(int i, int n) {
5353
private final float[] imaginary;
5454
private final float[] amplitude;
5555

56-
private int numBands = 0;
57-
private float[] bands;
58-
private int[] bandOffset;
56+
private final int numBands;
57+
private final float[] bands;
58+
private final int[] bandOffset;
59+
60+
// Function of sample rate, specified by compute() call
61+
private int sampleRate;
62+
private float bandWidthInv;
5963
private float bandOctaveRatio;
6064

61-
public FourierTransform(int bufferSize, int sampleRate) {
65+
public FourierTransform(int bufferSize) {
66+
this(bufferSize, DEFAULT_NUM_BANDS);
67+
}
68+
69+
public FourierTransform(int bufferSize, int numBands) {
6270
if ((bufferSize & (bufferSize - 1)) != 0) {
6371
throw new IllegalArgumentException("bufferSize must be a power of two: " + bufferSize);
6472
}
6573

6674
this.bufferSize = bufferSize;
67-
this.sampleRate = sampleRate;
68-
this.bandWidthInv = this.bufferSize / (float) this.sampleRate;
69-
7075
this.logN = (int) (Math.log(bufferSize) / Math.log(2));
7176

7277
this.sinN = new float[this.logN];
@@ -83,7 +88,9 @@ public FourierTransform(int bufferSize, int sampleRate) {
8388
this.imaginary = new float[this.bufferSize];
8489
this.amplitude = new float[this.bufferSize/2 + 1];
8590

86-
setNumBands(DEFAULT_NUM_BANDS);
91+
this.numBands = numBands;
92+
this.bands = new float[this.numBands];
93+
this.bandOffset = new int[this.numBands + 1];
8794
}
8895

8996
private void computePhaseTables() {
@@ -109,10 +116,6 @@ public int getSize() {
109116
return this.bufferSize;
110117
}
111118

112-
public int getSampleRate() {
113-
return this.sampleRate;
114-
}
115-
116119
public FourierTransform setWindow(Window window) {
117120
if (this.window != window) {
118121
this.window = window;
@@ -127,10 +130,15 @@ private void computeWindowCoefficients() {
127130
}
128131
}
129132

130-
public FourierTransform compute(float[] samples) {
133+
public FourierTransform compute(LXAudioBuffer buffer) {
134+
final float[] samples = buffer.samples;
131135
if (samples.length != this.bufferSize) {
132136
throw new IllegalArgumentException("Samples must have same length as FourierTransform size: " + samples.length);
133137
}
138+
139+
// Set sample rate
140+
setSampleRate(buffer.sampleRate());
141+
134142
// Apply window function, initialize bit-reverse-indexed values
135143
for (int i = 0; i < this.bufferSize; ++i) {
136144
int bri = this.bitReverseIndex[i];
@@ -181,24 +189,27 @@ public float get(int i) {
181189
return this.amplitude[i];
182190
}
183191

184-
public FourierTransform setNumBands(int numBands) {
185-
if (this.numBands != numBands) {
186-
this.numBands = numBands;
187-
this.bands = new float[this.numBands];
188-
this.bandOffset = new int[this.numBands + 1];
189-
this.bandOffset[0] = 0;
190-
191-
float nyquist = this.sampleRate / 2;
192-
float nyquistRatio = nyquist / BASE_BAND_HZ;
193-
float bandExpRange = (float) Math.log(nyquistRatio) / LOG_2;
194-
this.bandOctaveRatio = bandExpRange / (this.numBands - 1);
195-
196-
for (int i = 0; i < this.numBands; ++i) {
197-
float bandLimitHz = (float) Math.pow(2, i * this.bandOctaveRatio) * BASE_BAND_HZ;
198-
this.bandOffset[i+1] = Math.round(this.bandWidthInv * bandLimitHz);
192+
private void setSampleRate(int sampleRate) {
193+
if (this.sampleRate != sampleRate) {
194+
this.sampleRate = sampleRate;
195+
if (this.sampleRate <= 0) {
196+
this.bandWidthInv = 0;
197+
this.bandOctaveRatio = 0;
198+
Arrays.fill(this.bandOffset, 0);
199+
} else {
200+
this.bandWidthInv = this.bufferSize / (float) this.sampleRate;
201+
202+
float nyquist = this.sampleRate / 2;
203+
float nyquistRatio = nyquist / BASE_BAND_HZ;
204+
float bandExpRange = (float) Math.log(nyquistRatio) / LOG_2;
205+
this.bandOctaveRatio = bandExpRange / (this.numBands - 1);
206+
207+
for (int i = 0; i < this.numBands; ++i) {
208+
float bandLimitHz = (float) Math.pow(2, i * this.bandOctaveRatio) * BASE_BAND_HZ;
209+
this.bandOffset[i+1] = Math.round(this.bandWidthInv * bandLimitHz);
210+
}
199211
}
200212
}
201-
return this;
202213
}
203214

204215
public int getNumBands() {

src/main/java/heronarts/lx/audio/GraphicMeter.java

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ public class GraphicMeter extends DecibelMeter {
5050

5151
public final FourierTransform fft;
5252

53-
private final float[] sampleBuffer;
54-
5553
public final NormalizedParameter[] bands;
5654

5755
public GraphicMeter(LXAudioComponent component) {
@@ -97,10 +95,8 @@ public GraphicMeter(LXAudioBuffer buffer, int numBands) {
9795
public GraphicMeter(String label, LXAudioBuffer buffer, int numBands) {
9896
super(label, buffer);
9997
addParameter("slope", this.slope);
100-
this.sampleBuffer = new float[buffer.bufferSize()];
101-
this.fft = new FourierTransform(buffer.bufferSize(), buffer.sampleRate());
102-
this.fft.setNumBands(this.numBands = numBands);
103-
this.impl = new LXMeterImpl(this.numBands, this.fft.getBandOctaveRatio());
98+
this.fft = new FourierTransform(buffer.bufferSize(), this.numBands = numBands);
99+
this.impl = new LXMeterImpl(this.numBands);
104100
this.bands = this.impl.bands;
105101
int i = 1;
106102
for (NormalizedParameter band : this.bands) {
@@ -109,32 +105,22 @@ public GraphicMeter(String label, LXAudioBuffer buffer, int numBands) {
109105
}
110106

111107
@Override
112-
protected double computeValue(double deltaMs) {
113-
double result = super.computeValue(deltaMs);
114-
this.buffer.getSamples(this.sampleBuffer);
115-
this.fft.compute(this.sampleBuffer);
116-
108+
protected void onAudioFrame() {
109+
super.onAudioFrame();
117110
this.impl.compute(
118-
this.fft,
119-
this.attackGain,
120-
this.releaseGain,
111+
this.fft.compute(this.buffer),
112+
this.attackGain, // set by DecibelMeter.onAudioFrame()
113+
this.releaseGain, // set by DecibelMeter.onAudioFrame()
121114
this.gain.getValue(),
122115
this.range.getValue(),
123116
this.slope.getValue()
124117
);
125-
126-
return result;
127118
}
128119

129-
/**
130-
* Returns a snapshot of the last raw audio sample buffer frame that was used to compute
131-
* this meter. Note that this is copy of the audio buffer local to the LX thread and this particular
132-
* meter. The buffer is only updated when the meter is running, once per LX engine loop.
133-
*
134-
* @return Raw audio sample buffer used to compute this meter
135-
*/
136-
public float[] getSamples() {
137-
return this.sampleBuffer;
120+
@Override
121+
protected void onStop() {
122+
super.onStop();
123+
this.impl.onStop();
138124
}
139125

140126
/**

0 commit comments

Comments
 (0)