1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   DtmfGen.cpp
6 
7   Salvo Ventura - Dec 2006
8 
9 *******************************************************************//**
10 
11 \class EffectDtmf
12 \brief An effect that generates DTMF tones
13 
14 *//*******************************************************************/
15 
16 
17 #include "DtmfGen.h"
18 #include "LoadEffects.h"
19 
20 #include <wx/intl.h>
21 #include <wx/slider.h>
22 #include <wx/valgen.h>
23 #include <wx/valtext.h>
24 #include <wx/stattext.h>
25 
26 #include "Prefs.h"
27 #include "../Shuttle.h"
28 #include "../ShuttleGui.h"
29 #include "../widgets/NumericTextCtrl.h"
30 #include "../widgets/valnum.h"
31 
32 
33 enum
34 {
35    ID_Sequence,
36    ID_Amplitude,
37    ID_Duration,
38    ID_DutyCycle,
39 };
40 
41 // DA: DTMF for Audacity uses a different string.
42 #ifdef EXPERIMENTAL_DA
43 #define SHORT_APP_NAME "darkaudacity"
44 #else
45 #define SHORT_APP_NAME "audacity"
46 #endif
47 
48 // Define keys, defaults, minimums, and maximums for the effect parameters
49 //
50 //     Name       Type        Key               Def                   Min      Max      Scale
51 Param( Sequence,  wxString,   wxT("Sequence"),   wxT(SHORT_APP_NAME),  wxT(""), wxT(""), wxT(""));
52 Param( DutyCycle, double,     wxT("Duty Cycle"), 55.0,                 0.0,     100.0,   10.0   );
53 Param( Amplitude, double,     wxT("Amplitude"),  0.8,                  0.001,   1.0,     1      );
54 
55 static const double kFadeInOut = 250.0; // used for fadein/out needed to remove clicking noise
56 
57 const static wxChar *kSymbols[] =
58 {
59    wxT("0"), wxT("1"), wxT("2"), wxT("3"),
60    wxT("4"), wxT("5"), wxT("6"), wxT("7"),
61    wxT("8"), wxT("9"), wxT("*"), wxT("#"),
62    wxT("A"), wxT("B"), wxT("C"), wxT("D"),
63    wxT("a"), wxT("b"), wxT("c"), wxT("d"),
64    wxT("e"), wxT("f"), wxT("g"), wxT("h"),
65    wxT("i"), wxT("j"), wxT("k"), wxT("l"),
66    wxT("m"), wxT("n"), wxT("o"), wxT("p"),
67    wxT("q"), wxT("r"), wxT("s"), wxT("t"),
68    wxT("u"), wxT("v"), wxT("w"), wxT("x"),
69    wxT("y"), wxT("z")
70 };
71 
72 //
73 // EffectDtmf
74 //
75 
76 const ComponentInterfaceSymbol EffectDtmf::Symbol
77 { XO("DTMF Tones") };
78 
79 namespace{ BuiltinEffectsModule::Registration< EffectDtmf > reg; }
80 
BEGIN_EVENT_TABLE(EffectDtmf,wxEvtHandler)81 BEGIN_EVENT_TABLE(EffectDtmf, wxEvtHandler)
82     EVT_TEXT(ID_Sequence, EffectDtmf::OnSequence)
83     EVT_TEXT(ID_DutyCycle, EffectDtmf::OnAmplitude)
84     EVT_TEXT(ID_Duration, EffectDtmf::OnDuration)
85     EVT_SLIDER(ID_DutyCycle, EffectDtmf::OnDutyCycle)
86 END_EVENT_TABLE()
87 
88 EffectDtmf::EffectDtmf()
89 {
90    dtmfDutyCycle = DEF_DutyCycle;
91    dtmfAmplitude = DEF_Amplitude;
92    dtmfSequence = DEF_Sequence;
93    dtmfTone = 0.0;
94    dtmfSilence = 0.0;
95 }
96 
~EffectDtmf()97 EffectDtmf::~EffectDtmf()
98 {
99 }
100 
101 // ComponentInterface implementation
102 
GetSymbol()103 ComponentInterfaceSymbol EffectDtmf::GetSymbol()
104 {
105    return Symbol;
106 }
107 
GetDescription()108 TranslatableString EffectDtmf::GetDescription()
109 {
110    return XO("Generates dual-tone multi-frequency (DTMF) tones like those produced by the keypad on telephones");
111 }
112 
ManualPage()113 ManualPageID EffectDtmf::ManualPage()
114 {
115    return L"DTMF_Tones";
116 }
117 
118 // EffectDefinitionInterface implementation
119 
GetType()120 EffectType EffectDtmf::GetType()
121 {
122    return EffectTypeGenerate;
123 }
124 
125 // EffectClientInterface implementation
126 
GetAudioOutCount()127 unsigned EffectDtmf::GetAudioOutCount()
128 {
129    return 1;
130 }
131 
ProcessInitialize(sampleCount WXUNUSED (totalLen),ChannelNames WXUNUSED (chanMap))132 bool EffectDtmf::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap))
133 {
134    if (dtmfNTones <= 0) {   // Bail if no DTFM sequence.
135       ::Effect::MessageBox(
136                XO("DTMF sequence empty.\nCheck ALL settings for this effect."),
137          wxICON_ERROR );
138 
139       return false;
140    }
141    double duration = GetDuration();
142 
143    // all dtmf sequence durations in samples from seconds
144    // MJS: Note that mDuration is in seconds but will have been quantised to the units of the TTC.
145    // If this was 'samples' and the project rate was lower than the track rate,
146    // extra samples may get created as mDuration may now be > mT1 - mT0;
147    // However we are making our best efforts at creating what was asked for.
148 
149    auto nT0 = (sampleCount)floor(mT0 * mSampleRate + 0.5);
150    auto nT1 = (sampleCount)floor((mT0 + duration) * mSampleRate + 0.5);
151    numSamplesSequence = nT1 - nT0;  // needs to be exact number of samples selected
152 
153    //make under-estimates if anything, and then redistribute the few remaining samples
154    numSamplesTone = sampleCount( floor(dtmfTone * mSampleRate) );
155    numSamplesSilence = sampleCount( floor(dtmfSilence * mSampleRate) );
156 
157    // recalculate the sum, and spread the difference - due to approximations.
158    // Since diff should be in the order of "some" samples, a division (resulting in zero)
159    // is not sufficient, so we add the additional remaining samples in each tone/silence block,
160    // at least until available.
161    diff = numSamplesSequence - (dtmfNTones*numSamplesTone) - (dtmfNTones-1)*numSamplesSilence;
162    while (diff > 2*dtmfNTones - 1) {   // more than one per thingToBeGenerated
163       // in this case, both numSamplesTone and numSamplesSilence would change, so it makes sense
164       //  to recalculate diff here, otherwise just keep the value we already have
165 
166       // should always be the case that dtmfNTones>1, as if 0, we don't even start processing,
167       // and with 1 there is no difference to spread (no silence slot)...
168       wxASSERT(dtmfNTones > 1);
169       numSamplesTone += (diff/(dtmfNTones));
170       numSamplesSilence += (diff/(dtmfNTones-1));
171       diff = numSamplesSequence - (dtmfNTones*numSamplesTone) - (dtmfNTones-1)*numSamplesSilence;
172    }
173    wxASSERT(diff >= 0);  // should never be negative
174 
175    curSeqPos = -1; // pointer to string in dtmfSequence
176    isTone = false;
177    numRemaining = 0;
178 
179    return true;
180 }
181 
ProcessBlock(float ** WXUNUSED (inbuf),float ** outbuf,size_t size)182 size_t EffectDtmf::ProcessBlock(float **WXUNUSED(inbuf), float **outbuf, size_t size)
183 {
184    float *buffer = outbuf[0];
185    decltype(size) processed = 0;
186 
187    // for the whole dtmf sequence, we will be generating either tone or silence
188    // according to a bool value, and this might be done in small chunks of size
189    // 'block', as a single tone might sometimes be larger than the block
190    // tone and silence generally have different duration, thus two generation blocks
191    //
192    // Note: to overcome a 'clicking' noise introduced by the abrupt transition from/to
193    // silence, I added a fade in/out of 1/250th of a second (4ms). This can still be
194    // tweaked but gives excellent results at 44.1kHz: I haven't tried other freqs.
195    // A problem might be if the tone duration is very short (<10ms)... (?)
196    //
197    // One more problem is to deal with the approximations done when calculating the duration
198    // of both tone and silence: in some cases the final sum might not be same as the initial
199    // duration. So, to overcome this, we had a redistribution block up, and now we will spread
200    // the remaining samples in every bin in order to achieve the full duration: test case was
201    // to generate an 11 tone DTMF sequence, in 4 seconds, and with DutyCycle=75%: after generation
202    // you ended up with 3.999s or in other units: 3 seconds and 44097 samples.
203    //
204    while (size)
205    {
206       if (numRemaining == 0)
207       {
208          isTone = !isTone;
209 
210          if (isTone)
211          {
212             curSeqPos++;
213             numRemaining = numSamplesTone;
214             curTonePos = 0;
215          }
216          else
217          {
218             numRemaining = numSamplesSilence;
219          }
220 
221          // the statement takes care of extracting one sample from the diff bin and
222          // adding it into the current block until depletion
223          numRemaining += (diff-- > 0 ? 1 : 0);
224       }
225 
226       const auto len = limitSampleBufferSize( size, numRemaining );
227 
228       if (isTone)
229       {
230          // generate the tone and append
231          MakeDtmfTone(buffer, len, mSampleRate, dtmfSequence[curSeqPos], curTonePos, numSamplesTone, dtmfAmplitude);
232          curTonePos += len;
233       }
234       else
235       {
236          memset(buffer, 0, sizeof(float) * len);
237       }
238 
239       numRemaining -= len;
240 
241       buffer += len;
242       size -= len;
243       processed += len;
244    }
245 
246    return processed;
247 }
DefineParams(ShuttleParams & S)248 bool EffectDtmf::DefineParams( ShuttleParams & S ){
249    S.SHUTTLE_PARAM( dtmfSequence, Sequence );
250    S.SHUTTLE_PARAM( dtmfDutyCycle, DutyCycle );
251    S.SHUTTLE_PARAM( dtmfAmplitude, Amplitude );
252    return true;
253 }
254 
GetAutomationParameters(CommandParameters & parms)255 bool EffectDtmf::GetAutomationParameters(CommandParameters & parms)
256 {
257    parms.Write(KEY_Sequence, dtmfSequence);
258    parms.Write(KEY_DutyCycle, dtmfDutyCycle);
259    parms.Write(KEY_Amplitude, dtmfAmplitude);
260 
261    return true;
262 }
263 
SetAutomationParameters(CommandParameters & parms)264 bool EffectDtmf::SetAutomationParameters(CommandParameters & parms)
265 {
266    ReadAndVerifyDouble(DutyCycle);
267    ReadAndVerifyDouble(Amplitude);
268    ReadAndVerifyString(Sequence);
269 
270    wxString symbols;
271    for (unsigned int i = 0; i < WXSIZEOF(kSymbols); i++)
272    {
273       symbols += kSymbols[i];
274    }
275 
276    if (Sequence.find_first_not_of(symbols) != wxString::npos)
277    {
278       return false;
279    }
280 
281    dtmfDutyCycle = DutyCycle;
282    dtmfAmplitude = Amplitude;
283    dtmfSequence = Sequence;
284 
285    Recalculate();
286 
287    return true;
288 }
289 
290 // Effect implementation
291 
Startup()292 bool EffectDtmf::Startup()
293 {
294    wxString base = wxT("/Effects/DtmfGen/");
295 
296    // Migrate settings from 2.1.0 or before
297 
298    // Already migrated, so bail
299    if (gPrefs->Exists(base + wxT("Migrated")))
300    {
301       return true;
302    }
303 
304    // Load the old "current" settings
305    if (gPrefs->Exists(base))
306    {
307       gPrefs->Read(base + wxT("String"), &dtmfSequence, wxT(SHORT_APP_NAME));
308       gPrefs->Read(base + wxT("DutyCycle"), &dtmfDutyCycle, 550L);
309       gPrefs->Read(base + wxT("Amplitude"), &dtmfAmplitude, 0.8f);
310 
311       SaveUserPreset(GetCurrentSettingsGroup());
312 
313       // Do not migrate again
314       gPrefs->Write(base + wxT("Migrated"), true);
315       gPrefs->Flush();
316    }
317 
318    return true;
319 }
320 
Init()321 bool EffectDtmf::Init()
322 {
323    Recalculate();
324 
325    return true;
326 }
327 
PopulateOrExchange(ShuttleGui & S)328 void EffectDtmf::PopulateOrExchange(ShuttleGui & S)
329 {
330    // dialog will be passed values from effect
331    // Effect retrieves values from saved config
332    // Dialog will take care of using them to initialize controls
333    // If there is a selection, use that duration, otherwise use
334    // value from saved config: this is useful is user wants to
335    // replace selection with dtmf sequence
336 
337    S.AddSpace(0, 5);
338    S.StartMultiColumn(2, wxCENTER);
339    {
340       mDtmfSequenceT = S.Id(ID_Sequence)
341          .Validator([this]{
342             wxTextValidator vldDtmf(wxFILTER_INCLUDE_CHAR_LIST, &dtmfSequence);
343             vldDtmf.SetIncludes(wxArrayString(WXSIZEOF(kSymbols), kSymbols));
344             return vldDtmf;
345          })
346          .AddTextBox(XXO("DTMF &sequence:"), wxT(""), 10);
347 
348       S.Id(ID_Amplitude)
349          .Validator<FloatingPointValidator<double>>(
350             3, &dtmfAmplitude, NumValidatorStyle::NO_TRAILING_ZEROES,
351             MIN_Amplitude, MAX_Amplitude)
352          .AddTextBox(XXO("&Amplitude (0-1):"), wxT(""), 10);
353 
354       S.AddPrompt(XXO("&Duration:"));
355       mDtmfDurationT = safenew
356          NumericTextCtrl(S.GetParent(), ID_Duration,
357                          NumericConverter::TIME,
358                          GetDurationFormat(),
359                          GetDuration(),
360                          mProjectRate,
361                          NumericTextCtrl::Options{}
362                             .AutoPos(true));
363       S.Name(XO("Duration"))
364          .AddWindow(mDtmfDurationT);
365 
366       S.AddFixedText(XO("&Tone/silence ratio:"), false);
367       mDtmfDutyCycleS = S.Id(ID_DutyCycle)
368          .Style(wxSL_HORIZONTAL | wxEXPAND)
369          .MinSize( { -1, -1 } )
370          .AddSlider( {},
371                      dtmfDutyCycle * SCL_DutyCycle,
372                      MAX_DutyCycle * SCL_DutyCycle,
373                      MIN_DutyCycle * SCL_DutyCycle);
374    }
375    S.EndMultiColumn();
376 
377    S.StartMultiColumn(2, wxCENTER);
378    {
379       S.AddFixedText(XO("Duty cycle:"), false);
380       mDtmfDutyT =
381          S.AddVariableText(XO("%.1f %%").Format( dtmfDutyCycle ), false);
382 
383       S.AddFixedText(XO("Tone duration:"), false);
384       mDtmfSilenceT =
385          /* i18n-hint milliseconds */
386          S.AddVariableText(XO("%.0f ms").Format( dtmfTone * 1000.0 ), false);
387 
388       S.AddFixedText(XO("Silence duration:"), false);
389       mDtmfToneT =
390          /* i18n-hint milliseconds */
391          S.AddVariableText(XO("%0.f ms").Format( dtmfSilence * 1000.0 ), false);
392    }
393    S.EndMultiColumn();
394 }
395 
TransferDataToWindow()396 bool EffectDtmf::TransferDataToWindow()
397 {
398    Recalculate();
399 
400    if (!mUIParent->TransferDataToWindow())
401    {
402       return false;
403    }
404 
405    mDtmfDutyCycleS->SetValue(dtmfDutyCycle * SCL_DutyCycle);
406 
407    mDtmfDurationT->SetValue(GetDuration());
408 
409    UpdateUI();
410 
411    return true;
412 }
413 
TransferDataFromWindow()414 bool EffectDtmf::TransferDataFromWindow()
415 {
416    if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
417    {
418       return false;
419    }
420 
421    dtmfDutyCycle = (double) mDtmfDutyCycleS->GetValue() / SCL_DutyCycle;
422    SetDuration(mDtmfDurationT->GetValue());
423 
424    // recalculate to make sure all values are up-to-date. This is especially
425    // important if the user did not change any values in the dialog
426    Recalculate();
427 
428    return true;
429 }
430 
431 // EffectDtmf implementation
432 
Recalculate()433 void EffectDtmf::Recalculate()
434 {
435    // remember that dtmfDutyCycle is in range (0.0-100.0)
436 
437    dtmfNTones = (int) dtmfSequence.length();
438 
439    if (dtmfNTones==0) {
440       // no tones, all zero: don't do anything
441       // this should take care of the case where user got an empty
442       // dtmf sequence into the generator: track won't be generated
443       SetDuration(0.0);
444       dtmfTone = 0;
445       dtmfSilence = 0;
446    } else {
447       if (dtmfNTones==1) {
448         // single tone, as long as the sequence
449           dtmfTone = GetDuration();
450           dtmfSilence = 0;
451       } else {
452          // Don't be fooled by the fact that you divide the sequence into dtmfNTones:
453          // the last slot will only contain a tone, not ending with silence.
454          // Given this, the right thing to do is to divide the sequence duration
455          // by dtmfNTones tones and (dtmfNTones-1) silences each sized according to the duty
456          // cycle: original division was:
457          // slot=mDuration / (dtmfNTones*(dtmfDutyCycle/MAX_DutyCycle)+(dtmfNTones-1)*(1.0-dtmfDutyCycle/MAX_DutyCycle))
458          // which can be simplified in the one below.
459          // Then just take the part that belongs to tone or silence.
460          //
461          double slot = GetDuration() / ((double)dtmfNTones + (dtmfDutyCycle / 100.0) - 1);
462          dtmfTone = slot * (dtmfDutyCycle / 100.0); // seconds
463          dtmfSilence = slot * (1.0 - (dtmfDutyCycle / 100.0)); // seconds
464 
465          // Note that in the extremes we have:
466          // - dutyCycle=100%, this means no silence, so each tone will measure mDuration/dtmfNTones
467          // - dutyCycle=0%, this means no tones, so each silence slot will measure mDuration/(NTones-1)
468          // But we always count:
469          // - dtmfNTones tones
470          // - dtmfNTones-1 silences
471       }
472    }
473 }
474 
MakeDtmfTone(float * buffer,size_t len,float fs,wxChar tone,sampleCount last,sampleCount total,float amplitude)475 bool EffectDtmf::MakeDtmfTone(float *buffer, size_t len, float fs, wxChar tone, sampleCount last, sampleCount total, float amplitude)
476 {
477 /*
478   --------------------------------------------
479               1209 Hz 1336 Hz 1477 Hz 1633 Hz
480 
481                           ABC     DEF
482    697 Hz          1       2       3       A
483 
484                   GHI     JKL     MNO
485    770 Hz          4       5       6       B
486 
487                   PQRS     TUV     WXYZ
488    852 Hz          7       8       9       C
489 
490                           oper
491    941 Hz          *       0       #       D
492   --------------------------------------------
493   Essentially we need to generate two sin with
494   frequencies according to this table, and sum
495   them up.
496   sin wave is generated by:
497    s(n)=sin(2*pi*n*f/fs)
498 
499   We will precalculate:
500      A= 2*pi*f1/fs
501      B= 2*pi*f2/fs
502 
503   And use two switch statements to select the frequency
504 
505   Note: added support for letters, like those on the keypad
506         This support is only for lowercase letters: uppercase
507         are still considered to be the 'military'/carrier extra
508         tones.
509 */
510 
511    float f1, f2=0.0;
512    double A,B;
513 
514    // select low tone: left column
515    switch (tone) {
516       case '1':   case '2':   case '3':   case 'A':
517       case 'a':   case 'b':   case 'c':
518       case 'd':   case 'e':   case 'f':
519          f1=697;
520          break;
521       case '4':   case '5':   case '6':   case 'B':
522       case 'g':   case 'h':   case 'i':
523       case 'j':   case 'k':   case 'l':
524       case 'm':   case 'n':   case 'o':
525          f1=770;
526          break;
527       case '7':   case '8':   case '9':   case 'C':
528       case 'p':   case 'q':   case 'r':   case 's':
529       case 't':   case 'u':   case 'v':
530       case 'w':   case 'x':   case 'y':   case 'z':
531          f1=852;
532          break;
533       case '*':   case '0':   case '#':   case 'D':
534          f1=941;
535          break;
536       default:
537          f1=0;
538    }
539 
540    // select high tone: top row
541    switch (tone) {
542       case '1':   case '4':   case '7':   case '*':
543       case 'g':   case 'h':   case 'i':
544       case 'p':   case 'q':   case 'r':   case 's':
545          f2=1209;
546          break;
547       case '2':   case '5':   case '8':   case '0':
548       case 'a':   case 'b':   case 'c':
549       case 'j':   case 'k':   case 'l':
550       case 't':   case 'u':   case 'v':
551          f2=1336;
552          break;
553       case '3':   case '6':   case '9':   case '#':
554       case 'd':   case 'e':   case 'f':
555       case 'm':   case 'n':   case 'o':
556       case 'w':   case 'x':   case 'y':   case 'z':
557          f2=1477;
558          break;
559       case 'A':   case 'B':   case 'C':   case 'D':
560          f2=1633;
561          break;
562       default:
563          f2=0;
564    }
565 
566    // precalculations
567    A=B=2*M_PI/fs;
568    A*=f1;
569    B*=f2;
570 
571    // now generate the wave: 'last' is used to avoid phase errors
572    // when inside the inner for loop of the Process() function.
573    for(decltype(len) i = 0; i < len; i++) {
574       buffer[i] = amplitude * 0.5 *
575          (sin( A * (i + last).as_double() ) +
576           sin( B * (i + last).as_double() ));
577    }
578 
579    // generate a fade-in of duration 1/250th of second
580    if (last == 0) {
581       A = wxMin(len, (fs / kFadeInOut));
582       for(size_t i = 0; i < A; i++) {
583          buffer[i] *= i/A;
584       }
585    }
586 
587    // generate a fade-out of duration 1/250th of second
588    if (last >= total - len) {
589       // we are at the last buffer of 'len' size, so, offset is to
590       // backup 'A' samples, from 'len'
591       A = wxMin(len, (fs / kFadeInOut));
592       size_t offset = len - A;
593       wxASSERT(offset >= 0);
594       for(size_t i = 0; i < A; i++) {
595          buffer[i + offset] *= (1 - (i / A));
596       }
597    }
598    return true;
599 }
600 
UpdateUI(void)601 void EffectDtmf::UpdateUI(void)
602 {
603    mDtmfDutyT->SetLabel(wxString::Format(wxT("%.1f %%"), dtmfDutyCycle));
604    mDtmfDutyT->SetName(mDtmfDutyT->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
605 
606    mDtmfSilenceT->SetLabel(wxString::Format(_("%.0f ms"), dtmfTone * 1000.0));
607    mDtmfSilenceT->SetName(mDtmfSilenceT->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
608 
609    mDtmfToneT->SetLabel(wxString::Format(_("%.0f ms"), dtmfSilence * 1000.0));
610    mDtmfToneT->SetName(mDtmfToneT->GetLabel()); // fix for bug 577 (NVDA/Narrator screen readers do not read static text in dialogs)
611 }
612 
OnSequence(wxCommandEvent & WXUNUSED (evt))613 void EffectDtmf::OnSequence(wxCommandEvent & WXUNUSED(evt))
614 {
615    dtmfSequence = mDtmfSequenceT->GetValue();
616    Recalculate();
617    UpdateUI();
618 }
619 
OnAmplitude(wxCommandEvent & WXUNUSED (evt))620 void EffectDtmf::OnAmplitude(wxCommandEvent & WXUNUSED(evt))
621 {
622    if (!mDtmfAmplitudeT->GetValidator()->TransferFromWindow())
623    {
624       return;
625    }
626    Recalculate();
627    UpdateUI();
628 }
OnDuration(wxCommandEvent & WXUNUSED (evt))629 void EffectDtmf::OnDuration(wxCommandEvent & WXUNUSED(evt))
630 {
631    SetDuration(mDtmfDurationT->GetValue());
632    Recalculate();
633    UpdateUI();
634 }
635 
OnDutyCycle(wxCommandEvent & evt)636 void EffectDtmf::OnDutyCycle(wxCommandEvent & evt)
637 {
638    dtmfDutyCycle = (double) evt.GetInt() / SCL_DutyCycle;
639    Recalculate();
640    UpdateUI();
641 }
642