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