/********************************************************************** Audacity: A Digital Audio Editor DtmfGen.cpp Salvo Ventura - Dec 2006 *******************************************************************//** \class EffectDtmf \brief An effect for the "Generator" menu to generate DTMF tones *//*******************************************************************/ // For compilers that support precompilation, includes "wx/wx.h". #include #ifndef WX_PRECOMP // Include your minimal set of headers here, or wx.h #include #endif #include "DtmfGen.h" #include "../Audacity.h" #include "../Project.h" #include "../Prefs.h" #include "../ShuttleGui.h" #include "../WaveTrack.h" #include #include #include #include #include #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 /* pi */ #endif #define DUTY_MIN 0 #define DUTY_MAX 1000 #define DUTY_SCALE (DUTY_MAX/100.0) // ensure float division #define FADEINOUT 250.0 // used for fadein/out needed to remove clicking noise #define AMP_MIN 0 #define AMP_MAX 1 // // EffectDtmf // bool EffectDtmf::Init() { // dialog will be passed values from effect // Effect retrieves values from saved config // Dialog will take care of using them to initialize controls // If there is a selection, use that duration, otherwise use // value from saved config: this is useful is user wants to // replace selection with dtmf sequence if (mT1 > mT0) { // there is a selection: let's fit in there... mDuration = mT1 - mT0; mIsSelection = true; } else { // retrieve last used values gPrefs->Read(wxT("/CsPresets/DtmfGen_SequenceDuration"), &mDuration, 1L); mIsSelection = false; } /// \todo this code shouldn't be using /CsPresets - need to review its use gPrefs->Read(wxT("/CsPresets/DtmfGen_String"), &dtmfString, wxT("audacity")); gPrefs->Read(wxT("/CsPresets/DtmfGen_DutyCycle"), &dtmfDutyCycle, 550L); gPrefs->Read(wxT("/CsPresets/DtmfGen_Amplitude"), &dtmfAmplitude, 0.8f); dtmfNTones = wxStrlen(dtmfString); return true; } bool EffectDtmf::PromptUser() { DtmfDialog dlog(this, mParent, _("DTMF Tone Generator")); Init(); // Initialize dialog locals dlog.dIsSelection = mIsSelection; dlog.dString = dtmfString; dlog.dDutyCycle = dtmfDutyCycle; dlog.dDuration = mDuration; dlog.dAmplitude = dtmfAmplitude; // start dialog dlog.Init(); dlog.ShowModal(); if (dlog.GetReturnCode() == wxID_CANCEL) return false; // if there was an OK, retrieve values dtmfString = dlog.dString; dtmfDutyCycle = dlog.dDutyCycle; mDuration = dlog.dDuration; dtmfAmplitude = dlog.dAmplitude; dtmfNTones = dlog.dNTones; dtmfTone = dlog.dTone; dtmfSilence = dlog.dSilence; return true; } bool EffectDtmf::TransferParameters( Shuttle & shuttle ) { return true; } bool EffectDtmf::MakeDtmfTone(float *buffer, sampleCount len, float fs, wxChar tone, sampleCount last, sampleCount total, float amplitude) { /* -------------------------------------------- 1209 Hz 1336 Hz 1477 Hz 1633 Hz ABC DEF 697 Hz 1 2 3 A GHI JKL MNO 770 Hz 4 5 6 B PQRS TUV WXYZ 852 Hz 7 8 9 C oper 941 Hz * 0 # D -------------------------------------------- Essentially we need to generate two sin with frequencies according to this table, and sum them up. sin wave is generated by: s(n)=sin(2*pi*n*f/fs) We will precalculate: A= 2*pi*f1/fs B= 2*pi*f2/fs And use two switch statements to select the frequency Note: added support for letters, like those on the keypad This support is only for lowercase letters: uppercase are still considered to be the 'military'/carrier extra tones. */ float f1, f2=0.0; double A,B; // select low tone: left column switch (tone) { case '1': case '2': case '3': case 'A': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': f1=697; break; case '4': case '5': case '6': case 'B': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': f1=770; break; case '7': case '8': case '9': case 'C': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': f1=852; break; case '*': case '0': case '#': case 'D': f1=941; break; default: f1=0; } // select high tone: top row switch (tone) { case '1': case '4': case '7': case '*': case 'g': case 'h': case 'i': case 'p': case 'q': case 'r': case 's': f2=1209; break; case '2': case '5': case '8': case '0': case 'a': case 'b': case 'c': case 'j': case 'k': case 'l': case 't': case 'u': case 'v': f2=1336; break; case '3': case '6': case '9': case '#': case 'd': case 'e': case 'f': case 'm': case 'n': case 'o': case 'w': case 'x': case 'y': case 'z': f2=1477; break; case 'A': case 'B': case 'C': case 'D': f2=1633; break; default: f2=0; } // precalculations A=B=2*M_PI/fs; A*=f1; B*=f2; // now generate the wave: 'last' is used to avoid phase errors // when inside the inner for loop of the Process() function. for(sampleCount i=0; i=0) { for(sampleCount i=0; idtmfNTones) { // in this case, both these values would change, so it makes sense to recalculate diff // otherwise just keep the value we already have // should always be the case that dtmfNTones>1, as if 0, we don't even start processing, // and with 1 there is no difference to spread (no silence slot)... wxASSERT(dtmfNTones > 1); numSamplesTone += (diff/(dtmfNTones)); numSamplesSilence += (diff/(dtmfNTones-1)); diff = numSamplesSequence - (dtmfNTones*numSamplesTone) - (dtmfNTones-1)*numSamplesSilence; } // this var will be used as extra samples distributor int extra=0; sampleCount i = 0; sampleCount j = 0; int n=0; // pointer to string in dtmfString sampleCount block; bool isTone = true; float *data = new float[tmp->GetMaxBlockSize()]; // for the whole dtmf sequence, we will be generating either tone or silence // according to a bool value, and this might be done in small chunks of size // 'block', as a single tone might sometimes be larger than the block // tone and silence generally have different duration, thus two generation blocks // // Note: to overcome a 'clicking' noise introduced by the abrupt transition from/to // silence, I added a fade in/out of 1/250th of a second (4ms). This can still be // tweaked but gives excellent results at 44.1kHz: I haven't tried other freqs. // A problem might be if the tone duration is very short (<10ms)... (?) // // One more problem is to deal with the approximations done when calculating the duration // of both tone and silence: in some cases the final sum might not be same as the initial // duration. So, to overcome this, we had a redistribution block up, and now we will spread // the remaining samples in every bin in order to achieve the full duration: test case was // to generate an 11 tone DTMF sequence, in 4 seconds, and with DutyCycle=75%: after generation // you ended up with 3.999s or in other units: 3 seconds and 44097 samples. // while ((i < numSamplesSequence) && bGoodResult) { if (isTone) // generate tone { // the statement takes care of extracting one sample from the diff bin and // adding it into the tone block until depletion extra=(diff-- > 0?1:0); for(j=0; j < numSamplesTone+extra && bGoodResult; j+=block) { block = tmp->GetBestBlockSize(j); if (block > (numSamplesTone+extra - j)) block = numSamplesTone+extra - j; // generate the tone and append MakeDtmfTone(data, block, track.GetRate(), dtmfString[n], j, numSamplesTone, dtmfAmplitude); tmp->Append((samplePtr)data, floatSample, block); //Update the Progress meter if (TrackProgress(ntrack, (double)(i+j) / numSamplesSequence)) bGoodResult = false; } i += numSamplesTone; n++; if(n>=dtmfNTones)break; } else // generate silence { // the statement takes care of extracting one sample from the diff bin and // adding it into the silence block until depletion extra=(diff-- > 0?1:0); for(j=0; j < numSamplesSilence+extra && bGoodResult; j+=block) { block = tmp->GetBestBlockSize(j); if (block > (numSamplesSilence+extra - j)) block = numSamplesSilence+extra - j; // generate silence and append memset(data, 0, sizeof(float)*block); tmp->Append((samplePtr)data, floatSample, block); //Update the Progress meter if (TrackProgress(ntrack, (double)(i+j) / numSamplesSequence)) bGoodResult = false; } i += numSamplesSilence; } // flip flag isTone=!isTone; } // finished the whole dtmf sequence delete[] data; return bGoodResult; } void EffectDtmf::Success() { /* save last used values save duration unless value was got from selection, so we save only when user explicitely setup a value */ if (mT1 == mT0) gPrefs->Write(wxT("/CsPresets/DtmfGen_SequenceDuration"), mDuration); gPrefs->Write(wxT("/CsPresets/DtmfGen_String"), dtmfString); gPrefs->Write(wxT("/CsPresets/DtmfGen_DutyCycle"), dtmfDutyCycle); gPrefs->Write(wxT("/CsPresets/DtmfGen_Amplitude"), dtmfAmplitude); } //---------------------------------------------------------------------------- // DtmfDialog //---------------------------------------------------------------------------- const static wxChar *dtmfSymbols[] = { wxT("0"), wxT("1"), wxT("2"), wxT("3"), wxT("4"), wxT("5"), wxT("6"), wxT("7"), wxT("8"), wxT("9"), wxT("*"), wxT("#"), wxT("A"), wxT("B"), wxT("C"), wxT("D"), wxT("a"), wxT("b"), wxT("c"), wxT("d"), wxT("e"), wxT("f"), wxT("g"), wxT("h"), wxT("i"), wxT("j"), wxT("k"), wxT("l"), wxT("m"), wxT("n"), wxT("o"), wxT("p"), wxT("q"), wxT("r"), wxT("s"), wxT("t"), wxT("u"), wxT("v"), wxT("w"), wxT("x"), wxT("y"), wxT("z") }; #define ID_DTMF_DUTYCYCLE_SLIDER 10001 #define ID_DTMF_STRING_TEXT 10002 #define ID_DTMF_DURATION_TEXT 10003 #define ID_DTMF_DUTYCYCLE_TEXT 10004 #define ID_DTMF_TONELEN_TEXT 10005 #define ID_DTMF_SILENCE_TEXT 10006 BEGIN_EVENT_TABLE(DtmfDialog, EffectDialog) EVT_TEXT(ID_DTMF_STRING_TEXT, DtmfDialog::OnDtmfStringText) EVT_TEXT(ID_DTMF_DURATION_TEXT, DtmfDialog::OnDtmfDurationText) EVT_COMMAND(wxID_ANY, EVT_TIMETEXTCTRL_UPDATED, DtmfDialog::OnTimeCtrlUpdate) EVT_SLIDER(ID_DTMF_DUTYCYCLE_SLIDER, DtmfDialog::OnDutyCycleSlider) END_EVENT_TABLE() DtmfDialog::DtmfDialog(EffectDtmf * effect, wxWindow * parent, const wxString & title) : EffectDialog(parent, title, INSERT_EFFECT), mEffect(effect) { /* wxString dString; // dtmf tone string int dNTones; // total number of tones to generate double dTone; // duration of a single tone double dSilence; // duration of silence between tones double dDuration; // duration of the whole dtmf tone sequence */ dTone = 0; dSilence = 0; dDuration = 0; mDtmfDurationT = NULL; } void DtmfDialog::PopulateOrExchange( ShuttleGui & S ) { wxTextValidator vldDtmf(wxFILTER_INCLUDE_CHAR_LIST); vldDtmf.SetIncludes(wxArrayString(42, dtmfSymbols)); S.AddTitle(_("by Salvo Ventura (2006)")); S.StartMultiColumn(2, wxEXPAND); { mDtmfStringT = S.Id(ID_DTMF_STRING_TEXT).AddTextBox(_("DTMF sequence:"), wxT(""), 10); mDtmfStringT->SetValidator(vldDtmf); S.TieTextBox(_("Amplitude (0-1)"), dAmplitude, 10); S.AddPrompt(_("Duration:")); if (mDtmfDurationT == NULL) { mDtmfDurationT = new TimeTextCtrl(this, ID_DTMF_DURATION_TEXT, wxT(""), dDuration, mEffect->mProjectRate, wxDefaultPosition, wxDefaultSize, true); /* use this instead of "seconds" because if a selection is passed to the * effect, I want it (dDuration) to be used as the duration, and with * "seconds" this does not always work properly. For example, it rounds * down to zero... */ mDtmfDurationT->SetName(_("Duration")); mDtmfDurationT->SetFormatString(mDtmfDurationT->GetBuiltinFormat(dIsSelection==true?(_("hh:mm:ss + samples")):(_("seconds")))); mDtmfDurationT->EnableMenu(); } S.AddWindow(mDtmfDurationT); S.AddFixedText(_("Tone/silence ratio:"), false); S.SetStyle(wxSL_HORIZONTAL | wxEXPAND); mDtmfDutyS = S.Id(ID_DTMF_DUTYCYCLE_SLIDER).AddSlider(wxT(""), (int)dDutyCycle, DUTY_MAX, DUTY_MIN); S.SetSizeHints(-1,-1); } S.EndMultiColumn(); S.StartMultiColumn(2, wxCENTER); { S.AddFixedText(_("Duty cycle:"), false); mDtmfDutyT = S.Id(ID_DTMF_DUTYCYCLE_TEXT).AddVariableText(wxString::Format(wxT("%.1f %%"), (float) dDutyCycle/DUTY_SCALE), false); S.AddFixedText(_("Tone duration:"), false); mDtmfSilenceT = S.Id(ID_DTMF_TONELEN_TEXT).AddVariableText(wxString::Format(wxString(wxT("%d ")) + _("ms"), (int) dTone * 1000), false); S.AddFixedText(_("Silence duration:"), false); mDtmfToneT = S.Id(ID_DTMF_SILENCE_TEXT).AddVariableText(wxString::Format(wxString(wxT("%d ")) + _("ms"), (int) dSilence * 1000), false); } S.EndMultiColumn(); } bool DtmfDialog::TransferDataToWindow() { mDtmfDutyS->SetValue((int)dDutyCycle); mDtmfDurationT->SetTimeValue(dDuration); mDtmfStringT->SetValue(dString); return true; } bool DtmfDialog::TransferDataFromWindow() { EffectDialog::TransferDataFromWindow(); dAmplitude = TrapDouble(dAmplitude, AMP_MIN, AMP_MAX); // recalculate to make sure all values are up-to-date. This is especially // important if the user did not change any values in the dialog Recalculate(); return true; } /* * */ void DtmfDialog::Recalculate(void) { // remember that dDutyCycle is in range (0-1000) double slot; dString = mDtmfStringT->GetValue(); dDuration = mDtmfDurationT->GetTimeValue(); dNTones = wxStrlen(dString); dDutyCycle = TrapLong(mDtmfDutyS->GetValue(), DUTY_MIN, DUTY_MAX); if (dNTones==0) { // no tones, all zero: don't do anything // this should take care of the case where user got an empty // dtmf sequence into the generator: track won't be generated dTone = 0; dDuration = 0; dSilence = dDuration; } else if (dNTones==1) { // single tone, as long as the sequence dSilence = 0; dTone = dDuration; } else { // Don't be fooled by the fact that you divide the sequence into dNTones: // the last slot will only contain a tone, not ending with silence. // Given this, the right thing to do is to divide the sequence duration // by dNTones tones and (dNTones-1) silences each sized according to the duty // cycle: original division was: // slot=dDuration / (dNTones*(dDutyCycle/DUTY_MAX)+(dNTones-1)*(1.0-dDutyCycle/DUTY_MAX)) // which can be simplified in the one below. // Then just take the part that belongs to tone or silence. // slot=dDuration/((double)dNTones+(dDutyCycle/DUTY_MAX)-1); dTone = slot * (dDutyCycle/DUTY_MAX); // seconds dSilence = slot * (1.0 - (dDutyCycle/DUTY_MAX)); // seconds // Note that in the extremes we have: // - dutyCycle=100%, this means no silence, so each tone will measure dDuration/dNTones // - dutyCycle=0%, this means no tones, so each silence slot will measure dDuration/(NTones-1) // But we always count: // - dNTones tones // - dNTones-1 silences } mDtmfDutyT->SetLabel(wxString::Format(wxT("%.1f %%"), (float)dDutyCycle/DUTY_SCALE)); mDtmfSilenceT->SetLabel(wxString::Format(wxString(wxT("%d ")) + _("ms"), (int) (dTone * 1000))); mDtmfToneT->SetLabel(wxString::Format(wxString(wxT("%d ")) + _("ms"), (int) (dSilence * 1000))); } void DtmfDialog::OnDutyCycleSlider(wxCommandEvent & event) { Recalculate(); } void DtmfDialog::OnDtmfStringText(wxCommandEvent & event) { Recalculate(); } void DtmfDialog::OnDtmfDurationText(wxCommandEvent & event) { Recalculate(); } void DtmfDialog::OnTimeCtrlUpdate(wxCommandEvent & event) { this->Fit(); }