1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 Compressor.cpp
6
7 Dominic Mazzoni
8 Martyn Shaw
9 Steve Jolly
10
11 *******************************************************************//**
12
13 \class EffectCompressor
14 \brief An Effect derived from EffectTwoPassSimpleMono
15
16 - Martyn Shaw made it inherit from EffectTwoPassSimpleMono 10/2005.
17 - Steve Jolly made it inherit from EffectSimpleMono.
18 - GUI added and implementation improved by Dominic Mazzoni, 5/11/2003.
19
20 *//****************************************************************//**
21
22 \class CompressorPanel
23 \brief Panel used within the EffectCompressor for EffectCompressor.
24
25 *//*******************************************************************/
26
27
28 #include "Compressor.h"
29 #include "LoadEffects.h"
30
31 #include <math.h>
32
33 #include <wx/brush.h>
34 #include <wx/checkbox.h>
35 #include <wx/dcclient.h>
36 #include <wx/intl.h>
37 #include <wx/slider.h>
38 #include <wx/stattext.h>
39
40 #include "AColor.h"
41 #include "Prefs.h"
42 #include "../Shuttle.h"
43 #include "../ShuttleGui.h"
44 #include "Theme.h"
45 #include "float_cast.h"
46 #include "../widgets/Ruler.h"
47
48 #include "../WaveTrack.h"
49 #include "AllThemeResources.h"
50
51 enum
52 {
53 ID_Threshold = 10000,
54 ID_NoiseFloor,
55 ID_Ratio,
56 ID_Attack,
57 ID_Decay
58 };
59
60 // Define keys, defaults, minimums, and maximums for the effect parameters
61 //
62 // Name Type Key Def Min Max Scale
63 Param( Threshold, double, wxT("Threshold"), -12.0, -60.0, -1.0, 1 );
64 Param( NoiseFloor, double, wxT("NoiseFloor"), -40.0, -80.0, -20.0, 0.2 );
65 Param( Ratio, double, wxT("Ratio"), 2.0, 1.1, 10.0, 10 );
66 Param( AttackTime, double, wxT("AttackTime"), 0.2, 0.1, 5.0, 100 );
67 Param( ReleaseTime, double, wxT("ReleaseTime"), 1.0, 1.0, 30.0, 10 );
68 Param( Normalize, bool, wxT("Normalize"), true, false, true, 1 );
69 Param( UsePeak, bool, wxT("UsePeak"), false, false, true, 1 );
70
71 //----------------------------------------------------------------------------
72 // EffectCompressor
73 //----------------------------------------------------------------------------
74
75 const ComponentInterfaceSymbol EffectCompressor::Symbol
76 { XO("Compressor") };
77
78 namespace{ BuiltinEffectsModule::Registration< EffectCompressor > reg; }
79
BEGIN_EVENT_TABLE(EffectCompressor,wxEvtHandler)80 BEGIN_EVENT_TABLE(EffectCompressor, wxEvtHandler)
81 EVT_SLIDER(wxID_ANY, EffectCompressor::OnSlider)
82 END_EVENT_TABLE()
83
84 EffectCompressor::EffectCompressor()
85 {
86 mThresholdDB = DEF_Threshold;
87 mNoiseFloorDB = DEF_NoiseFloor;
88 mAttackTime = DEF_AttackTime; // seconds
89 mDecayTime = DEF_ReleaseTime; // seconds
90 mRatio = DEF_Ratio; // positive number > 1.0
91 mNormalize = DEF_Normalize;
92 mUsePeak = DEF_UsePeak;
93
94 mThreshold = 0.25;
95 mNoiseFloor = 0.01;
96 mCompression = 0.5;
97 mFollowLen = 0;
98
99 SetLinearEffectFlag(false);
100 }
101
~EffectCompressor()102 EffectCompressor::~EffectCompressor()
103 {
104 }
105
106 // ComponentInterface implementation
107
GetSymbol()108 ComponentInterfaceSymbol EffectCompressor::GetSymbol()
109 {
110 return Symbol;
111 }
112
GetDescription()113 TranslatableString EffectCompressor::GetDescription()
114 {
115 return XO("Compresses the dynamic range of audio");
116 }
117
ManualPage()118 ManualPageID EffectCompressor::ManualPage()
119 {
120 return L"Compressor";
121 }
122
123 // EffectDefinitionInterface implementation
124
GetType()125 EffectType EffectCompressor::GetType()
126 {
127 return EffectTypeProcess;
128 }
129
130 // EffectClientInterface implementation
DefineParams(ShuttleParams & S)131 bool EffectCompressor::DefineParams( ShuttleParams & S ){
132 S.SHUTTLE_PARAM( mThresholdDB, Threshold );
133 S.SHUTTLE_PARAM( mNoiseFloorDB, NoiseFloor );
134 S.SHUTTLE_PARAM( mRatio, Ratio);
135 S.SHUTTLE_PARAM( mAttackTime, AttackTime);
136 S.SHUTTLE_PARAM( mDecayTime, ReleaseTime);
137 S.SHUTTLE_PARAM( mNormalize, Normalize);
138 S.SHUTTLE_PARAM( mUsePeak, UsePeak);
139 return true;
140 }
141
GetAutomationParameters(CommandParameters & parms)142 bool EffectCompressor::GetAutomationParameters(CommandParameters & parms)
143 {
144 parms.Write(KEY_Threshold, mThresholdDB);
145 parms.Write(KEY_NoiseFloor, mNoiseFloorDB);
146 parms.Write(KEY_Ratio, mRatio);
147 parms.Write(KEY_AttackTime, mAttackTime);
148 parms.Write(KEY_ReleaseTime, mDecayTime);
149 parms.Write(KEY_Normalize, mNormalize);
150 parms.Write(KEY_UsePeak, mUsePeak);
151
152 return true;
153 }
154
SetAutomationParameters(CommandParameters & parms)155 bool EffectCompressor::SetAutomationParameters(CommandParameters & parms)
156 {
157 ReadAndVerifyDouble(Threshold);
158 ReadAndVerifyDouble(NoiseFloor);
159 ReadAndVerifyDouble(Ratio);
160 ReadAndVerifyDouble(AttackTime);
161 ReadAndVerifyDouble(ReleaseTime);
162 ReadAndVerifyBool(Normalize);
163 ReadAndVerifyBool(UsePeak);
164
165 mThresholdDB = Threshold;
166 mNoiseFloorDB = NoiseFloor;
167 mRatio = Ratio;
168 mAttackTime = AttackTime;
169 mDecayTime = ReleaseTime;
170 mNormalize = Normalize;
171 mUsePeak = UsePeak;
172
173 return true;
174 }
175
176 // Effect Implementation
177
Startup()178 bool EffectCompressor::Startup()
179 {
180 wxString base = wxT("/Effects/Compressor/");
181
182 // Migrate settings from 2.1.0 or before
183
184 // Already migrated, so bail
185 if (gPrefs->Exists(base + wxT("Migrated")))
186 {
187 return true;
188 }
189
190 // Load the old "current" settings
191 if (gPrefs->Exists(base))
192 {
193 gPrefs->Read(base + wxT("ThresholdDB"), &mThresholdDB, -12.0f );
194 gPrefs->Read(base + wxT("NoiseFloorDB"), &mNoiseFloorDB, -40.0f );
195 gPrefs->Read(base + wxT("Ratio"), &mRatio, 2.0f );
196 gPrefs->Read(base + wxT("AttackTime"), &mAttackTime, 0.2f );
197 gPrefs->Read(base + wxT("DecayTime"), &mDecayTime, 1.0f );
198 gPrefs->Read(base + wxT("Normalize"), &mNormalize, true );
199 gPrefs->Read(base + wxT("UsePeak"), &mUsePeak, false );
200
201 SaveUserPreset(GetCurrentSettingsGroup());
202
203 // Do not migrate again
204 gPrefs->Write(base + wxT("Migrated"), true);
205 gPrefs->Flush();
206 }
207
208 return true;
209 }
210
211 namespace {
212
ThresholdFormat(int value)213 TranslatableString ThresholdFormat( int value )
214 /* i18n-hint: usually leave this as is as dB doesn't get translated*/
215 { return XO("%3d dB").Format(value); }
216
AttackTimeFormat(double value)217 TranslatableString AttackTimeFormat( double value )
218 { return XO("%.2f secs").Format( value ); }
219
DecayTimeFormat(double value)220 TranslatableString DecayTimeFormat( double value )
221 { return XO("%.1f secs").Format( value ); }
222
RatioTextFormat(int sliderValue,double value)223 TranslatableString RatioTextFormat( int sliderValue, double value )
224 {
225 auto format = (sliderValue % 10 == 0)
226 /* i18n-hint: Unless your language has a different convention for ratios,
227 * like 8:1, leave as is.*/
228 ? XO("%.0f:1")
229 /* i18n-hint: Unless your language has a different convention for ratios,
230 * like 8:1, leave as is.*/
231 : XO("%.1f:1");
232 return format.Format( value );
233 }
234
RatioLabelFormat(int sliderValue,double value)235 TranslatableString RatioLabelFormat( int sliderValue, double value )
236 {
237 auto format = (sliderValue % 10 == 0)
238 ? XO("Ratio %.0f to 1")
239 : XO("Ratio %.1f to 1");
240 return format.Format( value );
241 }
242
243 }
244
PopulateOrExchange(ShuttleGui & S)245 void EffectCompressor::PopulateOrExchange(ShuttleGui & S)
246 {
247 S.SetBorder(5);
248
249 S.StartHorizontalLay(wxEXPAND, true);
250 {
251 S.SetBorder(10);
252 mPanel = safenew EffectCompressorPanel(S.GetParent(), wxID_ANY,
253 mThresholdDB,
254 mNoiseFloorDB,
255 mRatio);
256 S.Prop(true)
257 .Position(wxEXPAND | wxALL)
258 .MinSize( { 400, 200 } )
259 .AddWindow(mPanel);
260 S.SetBorder(5);
261 }
262 S.EndHorizontalLay();
263
264 S.StartStatic( {} );
265 {
266 S.StartMultiColumn(3, wxEXPAND);
267 {
268 S.SetStretchyCol(1);
269 mThresholdLabel = S.AddVariableText(XO("&Threshold:"), true,
270 wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
271 mThresholdSlider = S.Id(ID_Threshold)
272 .Name(XO("Threshold"))
273 .Style(wxSL_HORIZONTAL)
274 .AddSlider( {},
275 DEF_Threshold * SCL_Threshold,
276 MAX_Threshold * SCL_Threshold,
277 MIN_Threshold * SCL_Threshold);
278 mThresholdText = S.AddVariableText(ThresholdFormat(999), true,
279 wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
280
281 mNoiseFloorLabel = S.AddVariableText(XO("&Noise Floor:"), true,
282 wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
283 mNoiseFloorSlider = S.Id(ID_NoiseFloor)
284 .Name(XO("Noise Floor"))
285 .Style(wxSL_HORIZONTAL)
286 .AddSlider( {},
287 DEF_NoiseFloor * SCL_NoiseFloor,
288 MAX_NoiseFloor * SCL_NoiseFloor,
289 MIN_NoiseFloor * SCL_NoiseFloor);
290 mNoiseFloorText = S.AddVariableText(ThresholdFormat(999),
291 true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
292
293 mRatioLabel = S.AddVariableText(XO("&Ratio:"), true,
294 wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
295 mRatioSlider = S.Id(ID_Ratio)
296 .Name(XO("Ratio"))
297 .Style(wxSL_HORIZONTAL)
298 .AddSlider( {},
299 DEF_Ratio * SCL_Ratio,
300 MAX_Ratio * SCL_Ratio,
301 MIN_Ratio * SCL_Ratio);
302 mRatioSlider->SetPageSize(5);
303 mRatioText = S.AddVariableText(RatioTextFormat( 1, 99.9 ), true,
304 wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
305
306 /* i18n-hint: Particularly in percussion, sounds can be regarded as having
307 * an 'attack' phase where the sound builds up and a 'decay' where the
308 * sound dies away. So this means 'onset duration'. */
309 mAttackLabel = S.AddVariableText(XO("&Attack Time:"), true,
310 wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
311 mAttackSlider = S.Id(ID_Attack)
312 /* i18n-hint: Particularly in percussion, sounds can be regarded as having
313 * an 'attack' phase where the sound builds up and a 'decay' where the
314 * sound dies away. So this means 'onset duration'. */
315 .Name(XO("Attack Time"))
316 .Style(wxSL_HORIZONTAL)
317 .AddSlider( {},
318 DEF_AttackTime * SCL_AttackTime,
319 MAX_AttackTime * SCL_AttackTime,
320 MIN_AttackTime * SCL_AttackTime);
321 mAttackText = S.AddVariableText(
322 AttackTimeFormat(9.99),
323 true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
324
325 /* i18n-hint: Particularly in percussion, sounds can be regarded as having
326 * an 'attack' phase where the sound builds up and a 'decay' or 'release' where the
327 * sound dies away. */
328 mDecayLabel = S.AddVariableText(XO("R&elease Time:"), true,
329 wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
330 mDecaySlider = S.Id(ID_Decay)
331 /* i18n-hint: Particularly in percussion, sounds can be regarded as having
332 * an 'attack' phase where the sound builds up and a 'decay' or 'release' where the
333 * sound dies away. */
334 .Name(XO("Release Time"))
335 .Style(wxSL_HORIZONTAL)
336 .AddSlider( {},
337 DEF_ReleaseTime * SCL_ReleaseTime,
338 MAX_ReleaseTime * SCL_ReleaseTime,
339 MIN_ReleaseTime * SCL_ReleaseTime);
340
341 mDecayText = S.AddVariableText(
342 DecayTimeFormat(99.9),
343 true, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
344 }
345 S.EndMultiColumn();
346 }
347 S.EndStatic();
348
349 S.StartHorizontalLay(wxCENTER, false);
350 {
351 /* i18n-hint: Make-up, i.e. correct for any reduction, rather than fabricate it.*/
352 mGainCheckBox = S.AddCheckBox(XXO("Ma&ke-up gain for 0 dB after compressing"),
353 DEF_Normalize);
354 /* i18n-hint: "Compress" here means reduce variations of sound volume,
355 NOT related to file-size compression; Peaks means extremes in volume */
356 mPeakCheckBox = S.AddCheckBox(XXO("C&ompress based on Peaks"),
357 DEF_UsePeak);
358 }
359 S.EndHorizontalLay();
360 }
361
TransferDataToWindow()362 bool EffectCompressor::TransferDataToWindow()
363 {
364 mThresholdSlider->SetValue(lrint(mThresholdDB));
365 mNoiseFloorSlider->SetValue(lrint(mNoiseFloorDB * SCL_NoiseFloor));
366 mRatioSlider->SetValue(lrint(mRatio * SCL_Ratio));
367 mAttackSlider->SetValue(lrint(mAttackTime * SCL_AttackTime));
368 mDecaySlider->SetValue(lrint(mDecayTime * SCL_ReleaseTime));
369 mGainCheckBox->SetValue(mNormalize);
370 mPeakCheckBox->SetValue(mUsePeak);
371
372 UpdateUI();
373
374 return true;
375 }
376
TransferDataFromWindow()377 bool EffectCompressor::TransferDataFromWindow()
378 {
379 if (!mUIParent->Validate())
380 {
381 return false;
382 }
383
384 mThresholdDB = (double) mThresholdSlider->GetValue();
385 mNoiseFloorDB = (double) mNoiseFloorSlider->GetValue() / SCL_NoiseFloor;
386 mRatio = (double) mRatioSlider->GetValue() / SCL_Ratio;
387 mAttackTime = (double) mAttackSlider->GetValue() / 100.0; //SCL_AttackTime;
388 mDecayTime = (double) mDecaySlider->GetValue() / SCL_ReleaseTime;
389 mNormalize = mGainCheckBox->GetValue();
390 mUsePeak = mPeakCheckBox->GetValue();
391
392 return true;
393 }
394
395 // EffectTwoPassSimpleMono implementation
396
NewTrackPass1()397 bool EffectCompressor::NewTrackPass1()
398 {
399 mThreshold = DB_TO_LINEAR(mThresholdDB);
400 mNoiseFloor = DB_TO_LINEAR(mNoiseFloorDB);
401 mNoiseCounter = 100;
402
403 mAttackInverseFactor = exp(log(mThreshold) / (mCurRate * mAttackTime + 0.5));
404 mAttackFactor = 1.0 / mAttackInverseFactor;
405 mDecayFactor = exp(log(mThreshold) / (mCurRate * mDecayTime + 0.5));
406
407 if(mRatio > 1)
408 mCompression = 1.0-1.0/mRatio;
409 else
410 mCompression = 0.0;
411
412 mLastLevel = mThreshold;
413
414 mCircleSize = 100;
415 mCircle.reinit( mCircleSize, true );
416 mCirclePos = 0;
417 mRMSSum = 0.0;
418
419 return true;
420 }
421
InitPass1()422 bool EffectCompressor::InitPass1()
423 {
424 mMax=0.0;
425 if (!mNormalize)
426 DisableSecondPass();
427
428 // Find the maximum block length required for any track
429 size_t maxlen = inputTracks()->Selected< const WaveTrack >().max(
430 &WaveTrack::GetMaxBlockSize
431 );
432 mFollow1.reset();
433 mFollow2.reset();
434 // Allocate buffers for the envelope
435 if(maxlen > 0) {
436 mFollow1.reinit(maxlen);
437 mFollow2.reinit(maxlen);
438 }
439 mFollowLen = maxlen;
440
441 return true;
442 }
443
InitPass2()444 bool EffectCompressor::InitPass2()
445 {
446 // Actually, this should not even be called, because we call
447 // DisableSecondPass() before, if mNormalize is false.
448 return mNormalize;
449 }
450
451 // Process the input with 2 buffers available at a time
452 // buffer1 will be written upon return
453 // buffer2 will be passed as buffer1 on the next call
TwoBufferProcessPass1(float * buffer1,size_t len1,float * buffer2,size_t len2)454 bool EffectCompressor::TwoBufferProcessPass1
455 (float *buffer1, size_t len1, float *buffer2, size_t len2)
456 {
457 // If buffers are bigger than allocated, then abort
458 // (this should never happen, but if it does, we don't want to crash)
459 if((len1 > mFollowLen) || (len2 > mFollowLen))
460 return false;
461
462 // This makes sure that the initial value is well-chosen
463 // buffer1 == NULL on the first and only the first call
464 if (buffer1 == NULL) {
465 // Initialize the mLastLevel to the peak level in the first buffer
466 // This avoids problems with large spike events near the beginning of the track
467 mLastLevel = mThreshold;
468 for(size_t i=0; i<len2; i++) {
469 if(mLastLevel < fabs(buffer2[i]))
470 mLastLevel = fabs(buffer2[i]);
471 }
472 }
473
474 // buffer2 is NULL on the last and only the last call
475 if(buffer2 != NULL) {
476 Follow(buffer2, mFollow2.get(), len2, mFollow1.get(), len1);
477 }
478
479 if(buffer1 != NULL) {
480 for (size_t i = 0; i < len1; i++) {
481 buffer1[i] = DoCompression(buffer1[i], mFollow1[i]);
482 }
483 }
484
485
486 #if 0
487 // Copy the envelope over the track data (for debug purposes)
488 memcpy(buffer1, mFollow1, len1*sizeof(float));
489 #endif
490
491 // Rotate the buffer pointers
492 mFollow1.swap(mFollow2);
493
494 return true;
495 }
496
ProcessPass2(float * buffer,size_t len)497 bool EffectCompressor::ProcessPass2(float *buffer, size_t len)
498 {
499 if (mMax != 0)
500 {
501 for (size_t i = 0; i < len; i++)
502 buffer[i] /= mMax;
503 }
504
505 return true;
506 }
507
FreshenCircle()508 void EffectCompressor::FreshenCircle()
509 {
510 // Recompute the RMS sum periodically to prevent accumulation of rounding errors
511 // during long waveforms
512 mRMSSum = 0;
513 for(size_t i=0; i<mCircleSize; i++)
514 mRMSSum += mCircle[i];
515 }
516
AvgCircle(float value)517 float EffectCompressor::AvgCircle(float value)
518 {
519 float level;
520
521 // Calculate current level from root-mean-squared of
522 // circular buffer ("RMS")
523 mRMSSum -= mCircle[mCirclePos];
524 mCircle[mCirclePos] = value*value;
525 mRMSSum += mCircle[mCirclePos];
526 level = sqrt(mRMSSum/mCircleSize);
527 mCirclePos = (mCirclePos+1)%mCircleSize;
528
529 return level;
530 }
531
Follow(float * buffer,float * env,size_t len,float * previous,size_t previous_len)532 void EffectCompressor::Follow(float *buffer, float *env, size_t len, float *previous, size_t previous_len)
533 {
534 /*
535
536 "Follow"ing algorithm by Roger B. Dannenberg, taken from
537 Nyquist. His description follows. -DMM
538
539 Description: this is a sophisticated envelope follower.
540 The input is an envelope, e.g. something produced with
541 the AVG function. The purpose of this function is to
542 generate a smooth envelope that is generally not less
543 than the input signal. In other words, we want to "ride"
544 the peaks of the signal with a smooth function. The
545 algorithm is as follows: keep a current output value
546 (called the "value"). The value is allowed to increase
547 by at most rise_factor and decrease by at most fall_factor.
548 Therefore, the next value should be between
549 value * rise_factor and value * fall_factor. If the input
550 is in this range, then the next value is simply the input.
551 If the input is less than value * fall_factor, then the
552 next value is just value * fall_factor, which will be greater
553 than the input signal. If the input is greater than value *
554 rise_factor, then we compute a rising envelope that meets
555 the input value by working bacwards in time, changing the
556 previous values to input / rise_factor, input / rise_factor^2,
557 input / rise_factor^3, etc. until this NEW envelope intersects
558 the previously computed values. There is only a limited buffer
559 in which we can work backwards, so if the NEW envelope does not
560 intersect the old one, then make yet another pass, this time
561 from the oldest buffered value forward, increasing on each
562 sample by rise_factor to produce a maximal envelope. This will
563 still be less than the input.
564
565 The value has a lower limit of floor to make sure value has a
566 reasonable positive value from which to begin an attack.
567 */
568 double level,last;
569
570 if(!mUsePeak) {
571 // Update RMS sum directly from the circle buffer
572 // to avoid accumulation of rounding errors
573 FreshenCircle();
574 }
575 // First apply a peak detect with the requested decay rate
576 last = mLastLevel;
577 for(size_t i=0; i<len; i++) {
578 if(mUsePeak)
579 level = fabs(buffer[i]);
580 else // use RMS
581 level = AvgCircle(buffer[i]);
582 // Don't increase gain when signal is continuously below the noise floor
583 if(level < mNoiseFloor) {
584 mNoiseCounter++;
585 } else {
586 mNoiseCounter = 0;
587 }
588 if(mNoiseCounter < 100) {
589 last *= mDecayFactor;
590 if(last < mThreshold)
591 last = mThreshold;
592 if(level > last)
593 last = level;
594 }
595 env[i] = last;
596 }
597 mLastLevel = last;
598
599 // Next do the same process in reverse direction to get the requested attack rate
600 last = mLastLevel;
601 for(size_t i = len; i--;) {
602 last *= mAttackInverseFactor;
603 if(last < mThreshold)
604 last = mThreshold;
605 if(env[i] < last)
606 env[i] = last;
607 else
608 last = env[i];
609 }
610
611 if((previous != NULL) && (previous_len > 0)) {
612 // If the previous envelope was passed, propagate the rise back until we intersect
613 for(size_t i = previous_len; i--;) {
614 last *= mAttackInverseFactor;
615 if(last < mThreshold)
616 last = mThreshold;
617 if(previous[i] < last)
618 previous[i] = last;
619 else // Intersected the previous envelope buffer, so we are finished
620 return;
621 }
622 // If we can't back up far enough, project the starting level forward
623 // until we intersect the desired envelope
624 last = previous[0];
625 for(size_t i=1; i<previous_len; i++) {
626 last *= mAttackFactor;
627 if(previous[i] > last)
628 previous[i] = last;
629 else // Intersected the desired envelope, so we are finished
630 return;
631 }
632 // If we still didn't intersect, then continue ramp up into current buffer
633 for(size_t i=0; i<len; i++) {
634 last *= mAttackFactor;
635 if(buffer[i] > last)
636 buffer[i] = last;
637 else // Finally got an intersect
638 return;
639 }
640 // If we still didn't intersect, then reset mLastLevel
641 mLastLevel = last;
642 }
643 }
644
DoCompression(float value,double env)645 float EffectCompressor::DoCompression(float value, double env)
646 {
647 float out;
648 if(mUsePeak) {
649 // Peak values map 1.0 to 1.0 - 'upward' compression
650 out = value * pow(1.0/env, mCompression);
651 } else {
652 // With RMS-based compression don't change values below mThreshold - 'downward' compression
653 out = value * pow(mThreshold/env, mCompression);
654 }
655
656 // Retain the maximum value for use in the normalization pass
657 if(mMax < fabs(out))
658 mMax = fabs(out);
659
660 return out;
661 }
662
OnSlider(wxCommandEvent & WXUNUSED (evt))663 void EffectCompressor::OnSlider(wxCommandEvent & WXUNUSED(evt))
664 {
665 TransferDataFromWindow();
666 UpdateUI();
667 }
668
UpdateUI()669 void EffectCompressor::UpdateUI()
670 {
671 mThresholdLabel->SetName(wxString::Format(_("Threshold %d dB"), (int) mThresholdDB));
672 mThresholdText->SetLabel(ThresholdFormat((int) mThresholdDB).Translation());
673 mThresholdText->SetName(mThresholdText->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
674
675 mNoiseFloorLabel->SetName(wxString::Format(_("Noise Floor %d dB"), (int) mNoiseFloorDB));
676 mNoiseFloorText->SetLabel(ThresholdFormat((int) mNoiseFloorDB).Translation());
677 mNoiseFloorText->SetName(mNoiseFloorText->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
678
679 mRatioLabel->SetName(
680 RatioLabelFormat(mRatioSlider->GetValue(), mRatio).Translation());
681 mRatioText->SetLabel(
682 RatioTextFormat(mRatioSlider->GetValue(), mRatio).Translation());
683 mRatioText->SetName(mRatioText->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
684
685 mAttackLabel->SetName(wxString::Format(_("Attack Time %.2f secs"), mAttackTime));
686 mAttackText->SetLabel(AttackTimeFormat(mAttackTime).Translation());
687 mAttackText->SetName(mAttackText->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
688
689 mDecayLabel->SetName(wxString::Format(_("Release Time %.1f secs"), mDecayTime));
690 mDecayText->SetLabel(DecayTimeFormat(mDecayTime).Translation());
691 mDecayText->SetName(mDecayText->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
692
693 mPanel->Refresh(false);
694
695 return;
696 }
697
698 //----------------------------------------------------------------------------
699 // EffectCompressorPanel
700 //----------------------------------------------------------------------------
701
BEGIN_EVENT_TABLE(EffectCompressorPanel,wxPanelWrapper)702 BEGIN_EVENT_TABLE(EffectCompressorPanel, wxPanelWrapper)
703 EVT_PAINT(EffectCompressorPanel::OnPaint)
704 EVT_SIZE(EffectCompressorPanel::OnSize)
705 END_EVENT_TABLE()
706
707 EffectCompressorPanel::EffectCompressorPanel(wxWindow *parent, wxWindowID winid,
708 double & threshold,
709 double & noiseFloor,
710 double & ratio)
711 : wxPanelWrapper(parent, winid),
712 threshold(threshold),
713 noiseFloor(noiseFloor),
714 ratio(ratio)
715 {
716 }
717
OnPaint(wxPaintEvent & WXUNUSED (evt))718 void EffectCompressorPanel::OnPaint(wxPaintEvent & WXUNUSED(evt))
719 {
720 wxPaintDC dc(this);
721
722 int width, height;
723 GetSize(&width, &height);
724
725 double rangeDB = 60;
726
727 // Ruler
728 int w = 0;
729 int h = 0;
730
731 Ruler vRuler;
732 vRuler.SetBounds(0, 0, width, height);
733 vRuler.SetOrientation(wxVERTICAL);
734 vRuler.SetRange(0, -rangeDB);
735 vRuler.SetFormat(Ruler::LinearDBFormat);
736 vRuler.SetUnits(XO("dB"));
737 vRuler.GetMaxSize(&w, NULL);
738
739 Ruler hRuler;
740 hRuler.SetBounds(0, 0, width, height);
741 hRuler.SetOrientation(wxHORIZONTAL);
742 hRuler.SetRange(-rangeDB, 0);
743 hRuler.SetFormat(Ruler::LinearDBFormat);
744 hRuler.SetUnits(XO("dB"));
745 hRuler.SetFlip(true);
746 hRuler.GetMaxSize(NULL, &h);
747
748 vRuler.SetBounds(0, 0, w, height - h);
749 hRuler.SetBounds(w, height - h, width, height);
750
751 vRuler.SetTickColour( theTheme.Colour( clrGraphLabels ));
752 hRuler.SetTickColour( theTheme.Colour( clrGraphLabels ));
753
754 #if defined(__WXMSW__)
755 dc.Clear();
756 #endif
757
758 wxRect border;
759 border.x = w;
760 border.y = 0;
761 border.width = width - w;
762 border.height = height - h + 1;
763
764 dc.SetBrush(*wxWHITE_BRUSH);
765 dc.SetPen(*wxBLACK_PEN);
766 dc.DrawRectangle(border);
767
768 wxRect envRect = border;
769 envRect.Deflate( 2, 2 );
770
771 int kneeX = lrint((rangeDB+threshold)*envRect.width/rangeDB);
772 int kneeY = lrint((rangeDB+threshold/ratio)*envRect.height/rangeDB);
773
774 int finalY = envRect.height;
775 int startY = lrint((threshold*(1.0/ratio-1.0))*envRect.height/rangeDB);
776
777 // Yellow line for threshold
778 /* dc.SetPen(wxPen(wxColour(220, 220, 0), 1, wxSOLID));
779 AColor::Line(dc,
780 envRect.x,
781 envRect.y + envRect.height - kneeY,
782 envRect.x + envRect.width - 1,
783 envRect.y + envRect.height - kneeY);*/
784
785 // Was: Nice dark red line for the compression diagram
786 // dc.SetPen(wxPen(wxColour(180, 40, 40), 3, wxSOLID));
787
788 // Nice blue line for compressor, same color as used in the waveform envelope.
789 dc.SetPen( AColor::WideEnvelopePen) ;
790
791 AColor::Line(dc,
792 envRect.x,
793 envRect.y + envRect.height - startY,
794 envRect.x + kneeX - 1,
795 envRect.y + envRect.height - kneeY);
796
797 AColor::Line(dc,
798 envRect.x + kneeX,
799 envRect.y + envRect.height - kneeY,
800 envRect.x + envRect.width - 1,
801 envRect.y + envRect.height - finalY);
802
803 // Paint border again
804 dc.SetBrush(*wxTRANSPARENT_BRUSH);
805 dc.SetPen(*wxBLACK_PEN);
806 dc.DrawRectangle(border);
807
808 vRuler.Draw(dc);
809 hRuler.Draw(dc);
810 }
811
OnSize(wxSizeEvent & WXUNUSED (evt))812 void EffectCompressorPanel::OnSize(wxSizeEvent & WXUNUSED(evt))
813 {
814 Refresh(false);
815 }
816