Chris@0
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@0
|
2
|
Chris@0
|
3 #include "flattendynamics-ladspa.h"
|
Chris@0
|
4
|
Chris@0
|
5 #include <iostream>
|
Chris@0
|
6 #include <cmath>
|
Chris@0
|
7
|
Chris@2
|
8 using std::cerr;
|
Chris@2
|
9 using std::endl;
|
Chris@2
|
10
|
Chris@7
|
11 const float longTermSeconds = 4.f;
|
Chris@9
|
12 const float shortTermSeconds = 1.f;
|
Chris@9
|
13 const float catchUpSeconds = 0.2f;
|
Chris@13
|
14 const float targetMaxRMS = 0.04f;
|
Chris@14
|
15 const float rmsMaxDecay = 0.999f; // per sample
|
Chris@3
|
16 const float maxGain = 20.f;
|
Chris@1
|
17
|
Chris@0
|
18 const char *const
|
Chris@0
|
19 FlattenDynamics::portNames[PortCount] =
|
Chris@0
|
20 {
|
Chris@0
|
21 "Input",
|
Chris@0
|
22 "Output",
|
Chris@1
|
23 "Gain",
|
Chris@0
|
24 };
|
Chris@0
|
25
|
Chris@0
|
26 const LADSPA_PortDescriptor
|
Chris@0
|
27 FlattenDynamics::ports[PortCount] =
|
Chris@0
|
28 {
|
Chris@0
|
29 LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO,
|
Chris@0
|
30 LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO,
|
Chris@1
|
31 LADSPA_PORT_OUTPUT | LADSPA_PORT_CONTROL,
|
Chris@0
|
32 };
|
Chris@0
|
33
|
Chris@0
|
34 const LADSPA_PortRangeHint
|
Chris@0
|
35 FlattenDynamics::hints[PortCount] =
|
Chris@0
|
36 {
|
Chris@0
|
37 { 0, 0, 0 },
|
Chris@0
|
38 { 0, 0, 0 },
|
Chris@1
|
39 { 0, 0, 0 },
|
Chris@0
|
40 };
|
Chris@0
|
41
|
Chris@0
|
42 const LADSPA_Properties
|
Chris@0
|
43 FlattenDynamics::properties = LADSPA_PROPERTY_HARD_RT_CAPABLE;
|
Chris@0
|
44
|
Chris@0
|
45 const LADSPA_Descriptor
|
Chris@0
|
46 FlattenDynamics::ladspaDescriptor =
|
Chris@0
|
47 {
|
Chris@0
|
48 0xf0b375, // "Unique" ID
|
Chris@0
|
49 "flattendynamics", // Label
|
Chris@0
|
50 properties,
|
Chris@2
|
51 "Flatten Dynamics", // Name
|
Chris@0
|
52 "Queen Mary, University of London", // Maker
|
Chris@0
|
53 "BSD", // Copyright
|
Chris@0
|
54 PortCount,
|
Chris@0
|
55 ports,
|
Chris@0
|
56 portNames,
|
Chris@0
|
57 hints,
|
Chris@0
|
58 0, // Implementation data
|
Chris@15
|
59 ladspaInstantiate,
|
Chris@15
|
60 ladspaConnectPort,
|
Chris@15
|
61 ladspaActivate,
|
Chris@15
|
62 ladspaRun,
|
Chris@0
|
63 0, // Run adding
|
Chris@0
|
64 0, // Set run adding gain
|
Chris@15
|
65 ladspaDeactivate,
|
Chris@15
|
66 ladspaCleanup
|
Chris@0
|
67 };
|
Chris@0
|
68
|
Chris@0
|
69 const LADSPA_Descriptor *
|
Chris@0
|
70 FlattenDynamics::getDescriptor(unsigned long index)
|
Chris@0
|
71 {
|
Chris@0
|
72 if (index == 0) return &ladspaDescriptor;
|
Chris@0
|
73 return 0;
|
Chris@0
|
74 }
|
Chris@0
|
75
|
Chris@0
|
76 FlattenDynamics::FlattenDynamics(int sampleRate) :
|
Chris@0
|
77 m_sampleRate(sampleRate),
|
Chris@0
|
78 m_input(0),
|
Chris@1
|
79 m_output(0),
|
Chris@1
|
80 m_pgain(0),
|
Chris@1
|
81 m_history(0),
|
Chris@1
|
82 m_histlen(0),
|
Chris@1
|
83 m_histwrite(0),
|
Chris@1
|
84 m_histread(0),
|
Chris@7
|
85 m_sumOfSquaresLongTerm(0.f),
|
Chris@7
|
86 m_sumOfSquaresShortTerm(0.f),
|
Chris@7
|
87 m_rmsLongTerm(0.f),
|
Chris@7
|
88 m_rmsShortTerm(0.f),
|
Chris@7
|
89 m_maxRmsLongTerm(0.f),
|
Chris@8
|
90 m_maxRmsShortTerm(0.f),
|
Chris@1
|
91 m_gain(1.f)
|
Chris@0
|
92 {
|
Chris@0
|
93 reset();
|
Chris@0
|
94 }
|
Chris@0
|
95
|
Chris@0
|
96 FlattenDynamics::~FlattenDynamics()
|
Chris@0
|
97 {
|
Chris@3
|
98 delete[] m_history;
|
Chris@0
|
99 }
|
Chris@0
|
100
|
Chris@0
|
101 LADSPA_Handle
|
Chris@15
|
102 FlattenDynamics::ladspaInstantiate(const LADSPA_Descriptor *, unsigned long rate)
|
Chris@0
|
103 {
|
Chris@0
|
104 FlattenDynamics *flatten = new FlattenDynamics(rate);
|
Chris@0
|
105 return flatten;
|
Chris@0
|
106 }
|
Chris@0
|
107
|
Chris@0
|
108 void
|
Chris@15
|
109 FlattenDynamics::ladspaConnectPort(LADSPA_Handle handle,
|
Chris@15
|
110 unsigned long port,
|
Chris@15
|
111 LADSPA_Data *location)
|
Chris@0
|
112 {
|
Chris@0
|
113 FlattenDynamics *flatten = (FlattenDynamics *)handle;
|
Chris@15
|
114 if (ports[port] & LADSPA_PORT_INPUT) {
|
Chris@15
|
115 flatten->connectInputPort(Port(port), location);
|
Chris@15
|
116 } else {
|
Chris@15
|
117 flatten->connectOutputPort(Port(port), location);
|
Chris@15
|
118 }
|
Chris@0
|
119 }
|
Chris@0
|
120
|
Chris@0
|
121 void
|
Chris@15
|
122 FlattenDynamics::ladspaActivate(LADSPA_Handle handle)
|
Chris@0
|
123 {
|
Chris@0
|
124 FlattenDynamics *flatten = (FlattenDynamics *)handle;
|
Chris@0
|
125 flatten->reset();
|
Chris@0
|
126 }
|
Chris@0
|
127
|
Chris@0
|
128 void
|
Chris@15
|
129 FlattenDynamics::ladspaRun(LADSPA_Handle handle, unsigned long samples)
|
Chris@0
|
130 {
|
Chris@0
|
131 FlattenDynamics *flatten = (FlattenDynamics *)handle;
|
Chris@15
|
132 flatten->process(samples);
|
Chris@0
|
133 }
|
Chris@0
|
134
|
Chris@0
|
135 void
|
Chris@15
|
136 FlattenDynamics::ladspaDeactivate(LADSPA_Handle handle)
|
Chris@0
|
137 {
|
Chris@15
|
138 ladspaActivate(handle); // both functions just reset the plugin
|
Chris@0
|
139 }
|
Chris@0
|
140
|
Chris@0
|
141 void
|
Chris@15
|
142 FlattenDynamics::ladspaCleanup(LADSPA_Handle handle)
|
Chris@0
|
143 {
|
Chris@0
|
144 delete (FlattenDynamics *)handle;
|
Chris@0
|
145 }
|
Chris@0
|
146
|
Chris@0
|
147 void
|
Chris@15
|
148 FlattenDynamics::connectInputPort(Port p, const float *location)
|
Chris@15
|
149 {
|
Chris@15
|
150 const float **ports[PortCount] = {
|
Chris@15
|
151 &m_input, 0, 0,
|
Chris@15
|
152 };
|
Chris@15
|
153
|
Chris@15
|
154 *ports[int(p)] = location;
|
Chris@15
|
155 }
|
Chris@15
|
156
|
Chris@15
|
157 void
|
Chris@15
|
158 FlattenDynamics::connectOutputPort(Port p, float *location)
|
Chris@15
|
159 {
|
Chris@15
|
160 float **ports[PortCount] = {
|
Chris@15
|
161 0, &m_output, &m_pgain,
|
Chris@15
|
162 };
|
Chris@15
|
163
|
Chris@15
|
164 *ports[int(p)] = location;
|
Chris@15
|
165 }
|
Chris@15
|
166
|
Chris@15
|
167 void
|
Chris@0
|
168 FlattenDynamics::reset()
|
Chris@0
|
169 {
|
Chris@3
|
170 delete[] m_history;
|
Chris@7
|
171 m_histlen = int(round(m_sampleRate * longTermSeconds));
|
Chris@2
|
172 if (m_histlen < 1) m_histlen = 1;
|
Chris@2
|
173 m_history = new float[m_histlen];
|
Chris@7
|
174 for (int i = 0; i < m_histlen; ++i) {
|
Chris@7
|
175 m_history[i] = 0.f;
|
Chris@7
|
176 }
|
Chris@1
|
177 m_histwrite = 0;
|
Chris@1
|
178 m_histread = 0;
|
Chris@1
|
179
|
Chris@7
|
180 m_sumOfSquaresLongTerm = 0.0;
|
Chris@7
|
181 m_sumOfSquaresShortTerm = 0.0;
|
Chris@7
|
182 m_rmsLongTerm = 0.f;
|
Chris@7
|
183 m_rmsShortTerm = 0.f;
|
Chris@7
|
184 m_maxRmsLongTerm = 0.f;
|
Chris@8
|
185 m_maxRmsShortTerm = 0.f;
|
Chris@1
|
186 m_gain = 1.f;
|
Chris@0
|
187 }
|
Chris@0
|
188
|
Chris@0
|
189 void
|
Chris@0
|
190 FlattenDynamics::updateParameters()
|
Chris@0
|
191 {
|
Chris@1
|
192 if (m_pgain) *m_pgain = m_gain;
|
Chris@0
|
193 }
|
Chris@0
|
194
|
Chris@0
|
195 void
|
Chris@15
|
196 FlattenDynamics::process(int sampleCount)
|
Chris@0
|
197 {
|
Chris@0
|
198 if (!m_input || !m_output) return;
|
Chris@0
|
199 updateParameters();
|
Chris@0
|
200
|
Chris@1
|
201 // Adjust gain so that
|
Chris@1
|
202 // * RMS level of the past N seconds is some fixed R, but
|
Chris@1
|
203 // * peak level does not clip
|
Chris@1
|
204 // We aim to take M seconds to move to our target gain
|
Chris@1
|
205
|
Chris@2
|
206 for (int i = 0; i < sampleCount; ++i) {
|
Chris@15
|
207 m_output[i] = processSingle(m_input[i]);
|
Chris@0
|
208 }
|
Chris@0
|
209 }
|
Chris@0
|
210
|
Chris@1
|
211 float
|
Chris@15
|
212 FlattenDynamics::processSingle(float f)
|
Chris@1
|
213 {
|
Chris@1
|
214 updateRMS(f);
|
Chris@2
|
215
|
Chris@7
|
216 if (m_rmsLongTerm == 0.f) {
|
Chris@2
|
217 return f;
|
Chris@2
|
218 }
|
Chris@2
|
219
|
Chris@10
|
220 if (m_rmsLongTerm >= m_maxRmsLongTerm) {
|
Chris@7
|
221 m_maxRmsLongTerm = m_rmsLongTerm;
|
Chris@11
|
222 } else if (m_rmsLongTerm < m_maxRmsLongTerm * rmsMaxDecay) {
|
Chris@11
|
223 m_maxRmsLongTerm *= rmsMaxDecay;
|
Chris@5
|
224 }
|
Chris@5
|
225
|
Chris@8
|
226 if (m_rmsShortTerm > m_maxRmsShortTerm) {
|
Chris@8
|
227 m_maxRmsShortTerm = m_rmsShortTerm;
|
Chris@8
|
228 }
|
Chris@8
|
229
|
Chris@9
|
230 float fixedGain = targetMaxRMS / m_maxRmsLongTerm;
|
Chris@8
|
231
|
Chris@9
|
232 float frac = m_rmsShortTerm / m_maxRmsShortTerm;
|
Chris@9
|
233
|
Chris@9
|
234 // push up toward top of 0,1 range
|
Chris@9
|
235 frac = pow(frac, 0.3);
|
Chris@9
|
236
|
Chris@9
|
237 float targetRMS = (frac * m_maxRmsShortTerm);
|
Chris@9
|
238
|
Chris@9
|
239 float targetGain = fixedGain * (targetRMS / m_rmsShortTerm);
|
Chris@8
|
240
|
Chris@3
|
241 if (targetGain > maxGain) {
|
Chris@3
|
242 targetGain = maxGain;
|
Chris@3
|
243 }
|
Chris@7
|
244
|
Chris@2
|
245 float catchUpSamples = catchUpSeconds * m_sampleRate;
|
Chris@2
|
246 // asymptotic, could improve?
|
Chris@2
|
247 m_gain = m_gain + (targetGain - m_gain) / catchUpSamples;
|
Chris@7
|
248
|
Chris@2
|
249 if (fabsf(f) * m_gain > 1.f) {
|
Chris@2
|
250 m_gain = 1.f / fabsf(f);
|
Chris@2
|
251 }
|
Chris@7
|
252
|
Chris@2
|
253 // cerr << "target gain = " << targetGain << ", gain = " << m_gain << endl;
|
Chris@2
|
254 return f * m_gain;
|
Chris@1
|
255 }
|
Chris@1
|
256
|
Chris@1
|
257 void
|
Chris@1
|
258 FlattenDynamics::updateRMS(float f)
|
Chris@1
|
259 {
|
Chris@11
|
260 // We update the RMS values by maintaining a sum-of-last-n-squares
|
Chris@11
|
261 // total (which the RMS is the square root of 1/n of) and
|
Chris@11
|
262 // recording the last n samples of history in a circular
|
Chris@11
|
263 // buffer. When a sample drops off the start of the history, we
|
Chris@11
|
264 // remove the square of it from the sum-of-squares total; then we
|
Chris@11
|
265 // add the square of the new sample.
|
Chris@11
|
266
|
Chris@1
|
267 int nextWrite = (m_histwrite + 1) % m_histlen;
|
Chris@1
|
268
|
Chris@7
|
269 float loseLongTerm = 0.f;
|
Chris@7
|
270 float loseShortTerm = 0.f;
|
Chris@1
|
271
|
Chris@1
|
272 if (nextWrite == m_histread) {
|
Chris@1
|
273 // full
|
Chris@7
|
274 loseLongTerm = m_history[m_histread];
|
Chris@1
|
275 m_histread = (m_histread + 1) % m_histlen;
|
Chris@1
|
276 }
|
Chris@1
|
277
|
Chris@7
|
278 int shortTermLength = round(shortTermSeconds * m_sampleRate);
|
Chris@7
|
279 int shortTermLoseIndex = nextWrite - shortTermLength;
|
Chris@7
|
280 if (shortTermLoseIndex < 0) {
|
Chris@7
|
281 shortTermLoseIndex += m_histlen;
|
Chris@7
|
282 }
|
Chris@7
|
283 // This depends on history being zero-initialised, to be correct at start:
|
Chris@7
|
284 loseShortTerm = m_history[shortTermLoseIndex];
|
Chris@7
|
285
|
Chris@1
|
286 m_history[m_histwrite] = f;
|
Chris@1
|
287 m_histwrite = nextWrite;
|
Chris@1
|
288
|
Chris@2
|
289 int fill = (m_histwrite - m_histread + m_histlen) % m_histlen;
|
Chris@2
|
290
|
Chris@7
|
291 m_sumOfSquaresLongTerm -= loseLongTerm * loseLongTerm;
|
Chris@7
|
292 m_sumOfSquaresLongTerm += f * f;
|
Chris@1
|
293
|
Chris@7
|
294 m_sumOfSquaresShortTerm -= loseShortTerm * loseShortTerm;
|
Chris@7
|
295 m_sumOfSquaresShortTerm += f * f;
|
Chris@7
|
296
|
Chris@7
|
297 m_rmsLongTerm = sqrt(m_sumOfSquaresLongTerm / fill);
|
Chris@7
|
298 m_rmsShortTerm = sqrt(m_sumOfSquaresShortTerm / shortTermLength);
|
Chris@2
|
299 // cerr << "rms = " << m_rms << " (from " << fill << " samples of " << m_histlen << ", latest " << f << ")" << endl;
|
Chris@1
|
300 }
|
Chris@1
|
301
|
Chris@0
|
302 const LADSPA_Descriptor *
|
Chris@0
|
303 ladspa_descriptor(unsigned long ix)
|
Chris@0
|
304 {
|
Chris@0
|
305 return FlattenDynamics::getDescriptor(ix);
|
Chris@0
|
306 }
|
Chris@0
|
307
|