Audacity 3.2.0
DtmfBase.cpp
Go to the documentation of this file.
1#include "DtmfBase.h"
2#include "BasicUI.h"
3#include "Prefs.h"
4#include <cmath>
5
6namespace
7{
8wxString AllSymbols();
9}
10
11static const double kFadeInOut =
12 250.0; // used for fadein/out needed to remove clicking noise
13
14namespace
15{
16wxString AllSymbols()
17{
18 wxString symbols;
19 for (unsigned int i = 0; i < DtmfBase::kSymbols.size(); ++i)
20 symbols += DtmfBase::kSymbols[i];
21 return symbols;
22}
23} // namespace
24
25const ComponentInterfaceSymbol DtmfBase::Symbol { XO("DTMF Tones") };
26
28{
29}
30
32{
33}
34
36{
38 parameters {
39 [](DtmfBase&, EffectSettings& es, DtmfSettings& s, bool updating) {
40 if (updating)
41 {
42 if (
43 s.dtmfSequence.find_first_not_of(AllSymbols()) !=
45 return false;
46 s.Recalculate(es);
47 }
48 return true;
49 },
50 };
51 return parameters;
52}
53
54// ComponentInterface implementation
55
57{
58 return Symbol;
59}
60
62{
63 return XO(
64 "Generates dual-tone multi-frequency (DTMF) tones like those produced by the keypad on telephones");
65}
66
68{
69 return L"DTMF_Tones";
70}
71
72// EffectDefinitionInterface implementation
73
75{
76 return EffectTypeGenerate;
77}
78
81{
83
84 auto& dtmfSettings = GetSettings(settings);
85 if (dtmfSettings.dtmfNTones == 0)
86 { // Bail if no DTFM sequence.
87 using namespace BasicUI;
88 // TODO: don't use mProcessor for this
90 XO("DTMF sequence empty.\nCheck ALL settings for this effect."),
91 MessageBoxOptions {}.IconStyle(Icon::Error));
92 return false;
93 }
94
95 double duration = settings.extra.GetDuration();
96
97 // all dtmf sequence durations in samples from seconds
98 // MJS: Note that mDuration is in seconds but will have been quantised to the
99 // units of the TTC. If this was 'samples' and the project rate was lower
100 // than the track rate, extra samples may get created as mDuration may now be
101 // > mT1 - mT0; However we are making our best efforts at creating what was
102 // asked for.
103
104 auto nT0 = (sampleCount)floor(mT0 * mSampleRate + 0.5);
105 auto nT1 = (sampleCount)floor((mT0 + duration) * mSampleRate + 0.5);
107 nT1 - nT0; // needs to be exact number of samples selected
108
109 // make under-estimates if anything, and then redistribute the few remaining
110 // samples
111 numSamplesTone = sampleCount(floor(dtmfSettings.dtmfTone * mSampleRate));
113 sampleCount(floor(dtmfSettings.dtmfSilence * mSampleRate));
114
115 // recalculate the sum, and spread the difference - due to approximations.
116 // Since diff should be in the order of "some" samples, a division (resulting
117 // in zero) is not sufficient, so we add the additional remaining samples in
118 // each tone/silence block, at least until available.
119 diff = numSamplesSequence - (dtmfSettings.dtmfNTones * numSamplesTone) -
120 (dtmfSettings.dtmfNTones - 1) * numSamplesSilence;
121 while (diff > 2 * dtmfSettings.dtmfNTones - 1)
122 { // more than one per thingToBeGenerated
123 // in this case, both numSamplesTone and numSamplesSilence would change,
124 // so it makes sense
125 // to recalculate diff here, otherwise just keep the value we already
126 // have
127
128 // should always be the case that dtmfNTones>1, as if 0, we don't even
129 // start processing, and with 1 there is no difference to spread (no
130 // silence slot)...
131 wxASSERT(dtmfSettings.dtmfNTones > 1);
132 numSamplesTone += (diff / (dtmfSettings.dtmfNTones));
133 numSamplesSilence += (diff / (dtmfSettings.dtmfNTones - 1));
134 diff = numSamplesSequence - (dtmfSettings.dtmfNTones * numSamplesTone) -
135 (dtmfSettings.dtmfNTones - 1) * numSamplesSilence;
136 }
137 wxASSERT(diff >= 0); // should never be negative
138
139 curSeqPos = -1; // pointer to string in dtmfSequence
140 isTone = false;
141 numRemaining = 0;
142
143 return true;
144}
145
147 EffectSettings& settings, const float* const*, float* const* outbuf,
148 size_t size)
149{
150 auto& dtmfSettings = GetSettings(settings);
151 float* buffer = outbuf[0];
152 decltype(size) processed = 0;
153
154 // for the whole dtmf sequence, we will be generating either tone or silence
155 // according to a bool value, and this might be done in small chunks of size
156 // 'block', as a single tone might sometimes be larger than the block
157 // tone and silence generally have different duration, thus two generation
158 // blocks
159 //
160 // Note: to overcome a 'clicking' noise introduced by the abrupt transition
161 // from/to silence, I added a fade in/out of 1/250th of a second (4ms). This
162 // can still be tweaked but gives excellent results at 44.1kHz: I haven't
163 // tried other freqs. A problem might be if the tone duration is very short
164 // (<10ms)... (?)
165 //
166 // One more problem is to deal with the approximations done when calculating
167 // the duration of both tone and silence: in some cases the final sum might
168 // not be same as the initial duration. So, to overcome this, we had a
169 // redistribution block up, and now we will spread the remaining samples in
170 // every bin in order to achieve the full duration: test case was to generate
171 // an 11 tone DTMF sequence, in 4 seconds, and with DutyCycle=75%: after
172 // generation you ended up with 3.999s or in other units: 3 seconds and 44097
173 // samples.
174 //
175 while (size)
176 {
177 if (numRemaining == 0)
178 {
179 isTone = !isTone;
180
181 if (isTone)
182 {
183 curSeqPos++;
184 numRemaining = numSamplesTone;
185 curTonePos = 0;
186 }
187 else
188 {
189 numRemaining = numSamplesSilence;
190 }
191
192 // the statement takes care of extracting one sample from the diff bin
193 // and adding it into the current block until depletion
194 numRemaining += (diff-- > 0 ? 1 : 0);
195 }
196
197 const auto len = limitSampleBufferSize(size, numRemaining);
198
199 if (isTone)
200 {
201 // generate the tone and append
202 assert(curSeqPos < dtmfSettings.dtmfNTones);
204 buffer, len, mSampleRate, dtmfSettings.dtmfSequence[curSeqPos],
205 curTonePos, numSamplesTone, dtmfSettings.dtmfAmplitude);
206 curTonePos += len;
207 }
208 else
209 {
210 memset(buffer, 0, sizeof(float) * len);
211 }
212
213 numRemaining -= len;
214
215 buffer += len;
216 size -= len;
217 processed += len;
218 }
219
220 return processed;
221}
222
223// EffectDtmf implementation
224
225// Updates dtmfNTones, dtmfTone, dtmfSilence, and sometimes duration
226// They depend on dtmfSequence, dtmfDutyCycle, and duration
228{
229 auto& extra = settings.extra;
230 // remember that dtmfDutyCycle is in range (0.0-100.0)
231
232 dtmfNTones = dtmfSequence.length();
233
234 if (dtmfNTones == 0)
235 {
236 // no tones, all zero: don't do anything
237 // this should take care of the case where user got an empty
238 // dtmf sequence into the generator: track won't be generated
239 extra.SetDuration(0.0);
240 dtmfTone = 0;
241 dtmfSilence = 0;
242 }
243 else
244 {
245 if (dtmfNTones == 1)
246 {
247 // single tone, as long as the sequence
248 dtmfTone = extra.GetDuration();
249 dtmfSilence = 0;
250 }
251 else
252 {
253 // Don't be fooled by the fact that you divide the sequence into
254 // dtmfNTones: the last slot will only contain a tone, not ending with
255 // silence. Given this, the right thing to do is to divide the sequence
256 // duration by dtmfNTones tones and (dtmfNTones-1) silences each sized
257 // according to the duty cycle: original division was: slot=mDuration /
258 // (dtmfNTones*(dtmfDutyCycle/DutyCycle.max)+(dtmfNTones-1)*(1.0-dtmfDutyCycle/DutyCycle.max))
259 // which can be simplified in the one below.
260 // Then just take the part that belongs to tone or silence.
261 //
262 double slot = extra.GetDuration() /
263 ((double)dtmfNTones + (dtmfDutyCycle / 100.0) - 1);
264 dtmfTone = slot * (dtmfDutyCycle / 100.0); // seconds
265 dtmfSilence = slot * (1.0 - (dtmfDutyCycle / 100.0)); // seconds
266
267 // Note that in the extremes we have:
268 // - dutyCycle=100%, this means no silence, so each tone will measure
269 // mDuration/dtmfNTones
270 // - dutyCycle=0%, this means no tones, so each silence slot will
271 // measure mDuration/(NTones-1) But we always count:
272 // - dtmfNTones tones
273 // - dtmfNTones-1 silences
274 }
275 }
276
277 // `this` is the settings copy in the validator
278 // Update the EffectSettings held by the dialog
280}
281
283 float* buffer, size_t len, float fs, wxChar tone, sampleCount last,
284 sampleCount total, float amplitude)
285{
286 /*
287 --------------------------------------------
288 1209 Hz 1336 Hz 1477 Hz 1633 Hz
289
290 ABC DEF
291 697 Hz 1 2 3 A
292
293 GHI JKL MNO
294 770 Hz 4 5 6 B
295
296 PQRS TUV WXYZ
297 852 Hz 7 8 9 C
298
299 oper
300 941 Hz * 0 # D
301 --------------------------------------------
302 Essentially we need to generate two sin with
303 frequencies according to this table, and sum
304 them up.
305 sin wave is generated by:
306 s(n)=sin(2*pi*n*f/fs)
307
308 We will precalculate:
309 A= 2*pi*f1/fs
310 B= 2*pi*f2/fs
311
312 And use two switch statements to select the frequency
313
314 Note: added support for letters, like those on the keypad
315 This support is only for lowercase letters: uppercase
316 are still considered to be the 'military'/carrier extra
317 tones.
318 */
319
320 float f1, f2 = 0.0;
321 double A, B;
322
323 // select low tone: left column
324 switch (tone)
325 {
326 case '1':
327 case '2':
328 case '3':
329 case 'A':
330 case 'a':
331 case 'b':
332 case 'c':
333 case 'd':
334 case 'e':
335 case 'f':
336 f1 = 697;
337 break;
338 case '4':
339 case '5':
340 case '6':
341 case 'B':
342 case 'g':
343 case 'h':
344 case 'i':
345 case 'j':
346 case 'k':
347 case 'l':
348 case 'm':
349 case 'n':
350 case 'o':
351 f1 = 770;
352 break;
353 case '7':
354 case '8':
355 case '9':
356 case 'C':
357 case 'p':
358 case 'q':
359 case 'r':
360 case 's':
361 case 't':
362 case 'u':
363 case 'v':
364 case 'w':
365 case 'x':
366 case 'y':
367 case 'z':
368 f1 = 852;
369 break;
370 case '*':
371 case '0':
372 case '#':
373 case 'D':
374 f1 = 941;
375 break;
376 default:
377 f1 = 0;
378 }
379
380 // select high tone: top row
381 switch (tone)
382 {
383 case '1':
384 case '4':
385 case '7':
386 case '*':
387 case 'g':
388 case 'h':
389 case 'i':
390 case 'p':
391 case 'q':
392 case 'r':
393 case 's':
394 f2 = 1209;
395 break;
396 case '2':
397 case '5':
398 case '8':
399 case '0':
400 case 'a':
401 case 'b':
402 case 'c':
403 case 'j':
404 case 'k':
405 case 'l':
406 case 't':
407 case 'u':
408 case 'v':
409 f2 = 1336;
410 break;
411 case '3':
412 case '6':
413 case '9':
414 case '#':
415 case 'd':
416 case 'e':
417 case 'f':
418 case 'm':
419 case 'n':
420 case 'o':
421 case 'w':
422 case 'x':
423 case 'y':
424 case 'z':
425 f2 = 1477;
426 break;
427 case 'A':
428 case 'B':
429 case 'C':
430 case 'D':
431 f2 = 1633;
432 break;
433 default:
434 f2 = 0;
435 }
436
437 // precalculations
438 A = B = 2 * M_PI / fs;
439 A *= f1;
440 B *= f2;
441
442 // now generate the wave: 'last' is used to avoid phase errors
443 // when inside the inner for loop of the Process() function.
444 for (decltype(len) i = 0; i < len; i++)
445 {
446 buffer[i] =
447 amplitude * 0.5 *
448 (sin(A * (i + last).as_double()) + sin(B * (i + last).as_double()));
449 }
450
451 // generate a fade-in of duration 1/250th of second
452 if (last == 0)
453 {
454 A = std::min<double>(len, (fs / kFadeInOut));
455 for (size_t i = 0; i < A; i++)
456 {
457 buffer[i] *= i / A;
458 }
459 }
460
461 // generate a fade-out of duration 1/250th of second
462 if (last >= total - len)
463 {
464 // we are at the last buffer of 'len' size, so, offset is to
465 // backup 'A' samples, from 'len'
466 A = std::min<double>(len, (fs / kFadeInOut));
467 size_t offset = len - A;
468 wxASSERT(offset >= 0);
469 for (size_t i = 0; i < A; i++)
470 {
471 buffer[i + offset] *= (1 - (i / A));
472 }
473 }
474 return true;
475}
476
477std::shared_ptr<EffectInstance> DtmfBase::MakeInstance() const
478{
479 // TODO: don't use Effect::mT0 and Effect::mSampleRate, but use an
480 // EffectContext (that class is not yet defined)
481 return std::make_shared<Instance>(*this, mT0);
482}
Toolkit-neutral facade for basic user interface services.
#define M_PI
Definition: Distortion.cpp:22
static const double kFadeInOut
Definition: DtmfBase.cpp:11
EffectType
@ EffectTypeGenerate
ChannelName
XO("Cut/Copy/Paste")
size_t limitSampleBufferSize(size_t bufferSize, sampleCount limit)
Definition: SampleCount.cpp:22
#define A(N)
Definition: ToChars.cpp:62
static Settings & settings()
Definition: TrackInfo.cpp:51
Generates EffectParameterMethods overrides from variadic template arguments.
ComponentInterfaceSymbol pairs a persistent string identifier used internally with an optional,...
ComponentInterfaceSymbol GetSymbol() const override
Definition: DtmfBase.cpp:56
ManualPageID ManualPage() const override
Name of a page in the Audacity alpha manual, default is empty.
Definition: DtmfBase.cpp:67
const EffectParameterMethods & Parameters() const override
Definition: DtmfBase.cpp:35
EffectType GetType() const override
Type determines how it behaves.
Definition: DtmfBase.cpp:74
static constexpr std::array< char, 6 *7 > kSymbols
Definition: DtmfBase.h:34
static const ComponentInterfaceSymbol Symbol
Definition: DtmfBase.h:32
DtmfBase()
Definition: DtmfBase.cpp:27
std::shared_ptr< EffectInstance > MakeInstance() const override
Make an object maintaining short-term state of an Effect.
Definition: DtmfBase.cpp:477
static bool MakeDtmfTone(float *buffer, size_t len, float fs, wxChar tone, sampleCount last, sampleCount total, float amplitude)
Definition: DtmfBase.cpp:282
TranslatableString GetDescription() const override
Definition: DtmfBase.cpp:61
virtual ~DtmfBase()
Definition: DtmfBase.cpp:31
double mT0
Definition: EffectBase.h:122
Interface for manipulations of an Effect's settings.
static DtmfSettings & GetSettings(EffectSettings &settings)
Assume settings originated from MakeSettings() and copies thereof.
Definition: Effect.h:166
Holds a msgid for the translation catalog; may also bind format arguments.
Positions or offsets within audio files need a wide type.
Definition: SampleCount.h:19
MessageBoxResult ShowMessageBox(const TranslatableString &message, MessageBoxOptions options={})
Show a modal message box with either Ok or Yes and No, and optionally Cancel.
Definition: BasicUI.h:287
constexpr size_t npos(-1)
MessageBoxOptions && IconStyle(Icon style) &&
Definition: BasicUI.h:104
bool ProcessInitialize(EffectSettings &settings, double sampleRate, ChannelNames chanMap) override
Definition: DtmfBase.cpp:79
size_t ProcessBlock(EffectSettings &settings, const float *const *inBlock, float *const *outBlock, size_t blockLen) override
Called for destructive effect computation.
Definition: DtmfBase.cpp:146
sampleCount diff
Definition: DtmfBase.h:85
sampleCount numSamplesSilence
Definition: DtmfBase.h:84
sampleCount numSamplesSequence
Definition: DtmfBase.h:82
sampleCount numSamplesTone
Definition: DtmfBase.h:83
double mSampleRate
Definition: DtmfBase.h:80
const double mT0
Definition: DtmfBase.h:79
sampleCount numRemaining
Definition: DtmfBase.h:87
void Recalculate(EffectSettings &settings)
Definition: DtmfBase.cpp:227
Externalized state of a plug-in.