/********************************************************************** Audacity: A Digital Audio Editor FreqWindow.cpp Dominic Mazzoni *******************************************************************//** \class FrequencyPlotDialog \brief Displays a spectrum plot of the waveform. Has options for selecting parameters of the plot. Has a feature that finds peaks and reports their value as you move the mouse around. *//****************************************************************//** \class FreqPlot \brief Works with FrequencyPlotDialog to dsplay a spectrum plot of the waveform. This class actually does the graph display. Has a feature that finds peaks and reports their value as you move the mouse around. *//*******************************************************************/ /* Salvo Ventura - November 2006 Extended range check for additional FFT windows */ #include "FreqWindow.h" #include #include // for wxUSE_* macros #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ShuttleGui.h" #include "AColor.h" #include "CommonCommandFlags.h" #include "FFT.h" #include "PitchName.h" #include "prefs/GUISettings.h" #include "Prefs.h" #include "Project.h" #include "ProjectWindow.h" #include "Theme.h" #include "ViewInfo.h" #include "AllThemeResources.h" #include "FileNames.h" #include "WaveTrack.h" #include "./widgets/HelpSystem.h" #include "widgets/AudacityMessageBox.h" #include "widgets/Ruler.h" #if wxUSE_ACCESSIBILITY #include "widgets/WindowAccessible.h" #endif #define FrequencyAnalysisTitle XO("Frequency Analysis") DEFINE_EVENT_TYPE(EVT_FREQWINDOW_RECALC); enum { FirstID = 7000, FreqZoomSliderID, FreqPanScrollerID, FreqExportButtonID, FreqAlgChoiceID, FreqSizeChoiceID, FreqFuncChoiceID, FreqAxisChoiceID, ReplotButtonID, GridOnOffID }; // These specify the minimum plot window width #define FREQ_WINDOW_WIDTH 480 #define FREQ_WINDOW_HEIGHT 330 static const char * ZoomIn[] = { "16 16 6 1", " c None", "+ c #1C1C1C", "@ c #AEAEAE", "# c #F7F7F7", "$ c #CFCECC", "* c #1C1CA0", " ++++ ", " @+# @$+@ ", " + @** +@ ", " +#@ ** #+ ", " +@****** +@", " + ****** +@", " +# ** #+@", " + ** +@@", " +++# #+@@ ", " +++@++++@@ ", " +++@@ @@@@ ", " +++@@ ", " +++@@ ", "+++@@ ", "@+@@ ", " @@ "}; static const char * ZoomOut[] = { "16 16 6 1", " c None", "+ c #1C1C1C", "@ c #AEAEAE", "# c #F7F7F7", "$ c #CFCECC", "* c #1C1CA0", " ++++ ", " @+# $+@ ", " + @@ +@ ", " +# @ #+ ", " +@****** +@", " + ****** +@", " +# #+@", " + +@@", " +++# #+@@ ", " +++@++++@@ ", " +++@@ @@@@ ", " +++@@ ", " +++@@ ", "+++@@ ", "@+@@ ", " @@ "}; // FrequencyPlotDialog BEGIN_EVENT_TABLE(FrequencyPlotDialog, wxDialogWrapper) EVT_CLOSE(FrequencyPlotDialog::OnCloseWindow) EVT_SIZE(FrequencyPlotDialog::OnSize) EVT_SLIDER(FreqZoomSliderID, FrequencyPlotDialog::OnZoomSlider) EVT_COMMAND_SCROLL(FreqPanScrollerID, FrequencyPlotDialog::OnPanScroller) EVT_CHOICE(FreqAlgChoiceID, FrequencyPlotDialog::OnAlgChoice) EVT_CHOICE(FreqSizeChoiceID, FrequencyPlotDialog::OnSizeChoice) EVT_CHOICE(FreqFuncChoiceID, FrequencyPlotDialog::OnFuncChoice) EVT_CHOICE(FreqAxisChoiceID, FrequencyPlotDialog::OnAxisChoice) EVT_BUTTON(FreqExportButtonID, FrequencyPlotDialog::OnExport) EVT_BUTTON(ReplotButtonID, FrequencyPlotDialog::OnReplot) EVT_BUTTON(wxID_CANCEL, FrequencyPlotDialog::OnCloseButton) EVT_BUTTON(wxID_HELP, FrequencyPlotDialog::OnGetURL) EVT_CHECKBOX(GridOnOffID, FrequencyPlotDialog::OnGridOnOff) EVT_COMMAND(wxID_ANY, EVT_FREQWINDOW_RECALC, FrequencyPlotDialog::OnRecalc) END_EVENT_TABLE() FrequencyPlotDialog::FrequencyPlotDialog(wxWindow * parent, wxWindowID id, AudacityProject &project, const TranslatableString & title, const wxPoint & pos) : wxDialogWrapper(parent, id, title, pos, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX), mAnalyst(std::make_unique()) , mProject{ &project } { SetName(); mMouseX = 0; mMouseY = 0; mRate = 0; mDataLen = 0; gPrefs->Read(wxT("/FrequencyPlotDialog/DrawGrid"), &mDrawGrid, true); gPrefs->Read(wxT("/FrequencyPlotDialog/SizeChoice"), &mSize, 3); int alg; gPrefs->Read(wxT("/FrequencyPlotDialog/AlgChoice"), &alg, 0); mAlg = static_cast(alg); gPrefs->Read(wxT("/FrequencyPlotDialog/FuncChoice"), &mFunc, 3); gPrefs->Read(wxT("/FrequencyPlotDialog/AxisChoice"), &mAxis, 1); Populate(); } FrequencyPlotDialog::~FrequencyPlotDialog() { } void FrequencyPlotDialog::Populate() { SetTitle(FrequencyAnalysisTitle); TranslatableStrings algChoices{ XO("Spectrum") , XO("Standard Autocorrelation") , XO("Cuberoot Autocorrelation") , XO("Enhanced Autocorrelation") , /* i18n-hint: This is a technical term, derived from the word * "spectrum". Do not translate it unless you are sure you * know the correct technical word in your language. */ XO("Cepstrum") , }; TranslatableStrings sizeChoices{ Verbatim( "128" ) , Verbatim( "256" ) , Verbatim( "512" ) , Verbatim( "1024" ) , Verbatim( "2048" ) , Verbatim( "4096" ) , Verbatim( "8192" ) , Verbatim( "16384" ) , Verbatim( "32768" ) , Verbatim( "65536" ) , }; TranslatableStrings funcChoices; for (int i = 0, cnt = NumWindowFuncs(); i < cnt; i++) { funcChoices.push_back( /* i18n-hint: This refers to a "window function", * such as Hann or Rectangular, used in the * Frequency analyze dialog box. */ XO("%s window").Format( WindowFuncName(i) ) ); } TranslatableStrings axisChoices{ XO("Linear frequency") , XO("Log frequency") , }; mFreqFont = wxFont(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL); mArrowCursor = std::make_unique(wxCURSOR_ARROW); mCrossCursor = std::make_unique(wxCURSOR_CROSS); long size; // reinterpret one of the verbatim strings above as a number sizeChoices[mSize].MSGID().GET().ToLong(&size); mWindowSize = size; gPrefs->Read(ENV_DB_KEY, &dBRange, ENV_DB_RANGE); if(dBRange < 90.) dBRange = 90.; ShuttleGui S(this, eIsCreating); S.SetBorder(0); S.AddSpace(5); S.SetSizerProportion(1); S.StartMultiColumn(3, wxEXPAND); { S.SetStretchyCol(1); S.SetStretchyRow(0); // ------------------------------------------------------------------- // ROW 1: Freq response panel and sliders for vertical scale // ------------------------------------------------------------------- S.StartVerticalLay(2); { vRuler = safenew RulerPanel( S.GetParent(), wxID_ANY, wxVERTICAL, wxSize{ 100, 100 }, // Ruler can't handle small sizes RulerPanel::Range{ 0.0, -dBRange }, Ruler::LinearDBFormat, XO("dB"), RulerPanel::Options{} .LabelEdges(true) .TickColour( theTheme.Colour( clrGraphLabels ) ) ); S.AddSpace(wxDefaultCoord, 1); S.Prop(1) .Position(wxALIGN_RIGHT | wxALIGN_TOP) .AddWindow(vRuler); S.AddSpace(wxDefaultCoord, 1); } S.EndVerticalLay(); mFreqPlot = safenew FreqPlot(S.GetParent(), wxID_ANY); S.Prop(1) .Position(wxEXPAND) .MinSize( { wxDefaultCoord, FREQ_WINDOW_HEIGHT } ) .AddWindow(mFreqPlot); S.StartHorizontalLay(wxEXPAND, 0); { S.StartVerticalLay(); { mPanScroller = safenew wxScrollBar(S.GetParent(), FreqPanScrollerID, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL); #if wxUSE_ACCESSIBILITY // so that name can be set on a standard control mPanScroller->SetAccessible(safenew WindowAccessible(mPanScroller)); #endif S.Prop(1); S .Name(XO("Scroll")) .Position( wxALIGN_LEFT | wxTOP) .AddWindow(mPanScroller); } S.EndVerticalLay(); S.StartVerticalLay(); { wxStaticBitmap *zi = safenew wxStaticBitmap(S.GetParent(), wxID_ANY, wxBitmap(ZoomIn)); S.Position(wxALIGN_CENTER) .AddWindow(zi); S.AddSpace(5); mZoomSlider = safenew wxSliderWrapper(S.GetParent(), FreqZoomSliderID, 100, 1, 100, wxDefaultPosition, wxDefaultSize, wxSL_VERTICAL); S.Prop(1); S .Name(XO("Zoom")) .Position(wxALIGN_CENTER_HORIZONTAL) .AddWindow(mZoomSlider); #if wxUSE_ACCESSIBILITY // so that name can be set on a standard control mZoomSlider->SetAccessible(safenew WindowAccessible(mZoomSlider)); #endif S.AddSpace(5); wxStaticBitmap *zo = safenew wxStaticBitmap(S.GetParent(), wxID_ANY, wxBitmap(ZoomOut)); S.Position(wxALIGN_CENTER) .AddWindow(zo); } S.EndVerticalLay(); S.AddSpace(5, wxDefaultCoord); } S.EndHorizontalLay(); // ------------------------------------------------------------------- // ROW 2: Frequency ruler // ------------------------------------------------------------------- S.AddSpace(1); S.StartHorizontalLay(wxEXPAND, 0); { hRuler = safenew RulerPanel( S.GetParent(), wxID_ANY, wxHORIZONTAL, wxSize{ 100, 100 }, // Ruler can't handle small sizes RulerPanel::Range{ 10, 20000 }, Ruler::RealFormat, XO("Hz"), RulerPanel::Options{} .Log(true) .Flip(true) .LabelEdges(true) .TickColour( theTheme.Colour( clrGraphLabels ) ) ); S.AddSpace(1, wxDefaultCoord); S.Prop(1) .Position(wxALIGN_LEFT | wxALIGN_TOP) .AddWindow(hRuler); S.AddSpace(1, wxDefaultCoord); } S.EndHorizontalLay(); S.AddSpace(1); // ------------------------------------------------------------------- // ROW 3: Spacer // ------------------------------------------------------------------- S.AddSpace(5); S.AddSpace(5); S.AddSpace(5); // ------------------------------------------------------------------- // ROW 4: Info // ------------------------------------------------------------------- S.AddSpace(1); S.StartHorizontalLay(wxEXPAND); { S.SetSizerProportion(1); S.StartMultiColumn(6); S.SetStretchyCol(1); S.SetStretchyCol(3); { S.AddPrompt(XXO("Cursor:")); mCursorText = S.Style(wxTE_READONLY) .AddTextBox( {}, wxT(""), 10); S.AddPrompt(XXO("Peak:")); mPeakText = S.Style(wxTE_READONLY) .AddTextBox( {}, wxT(""), 10); S.AddSpace(5); mGridOnOff = S.Id(GridOnOffID).AddCheckBox(XXO("&Grids"), mDrawGrid); } S.EndMultiColumn(); } S.EndHorizontalLay(); S.AddSpace(1); } S.EndMultiColumn(); // ------------------------------------------------------------------- // ROW 5: Spacer // ------------------------------------------------------------------- S.AddSpace(5); S.SetBorder(2); S.SetSizerProportion(0); S.StartMultiColumn(9, wxALIGN_CENTER); { // ---------------------------------------------------------------- // ROW 6: Algorithm, Size, Export, Replot // ---------------------------------------------------------------- S.AddSpace(5); mAlgChoice = S.Id(FreqAlgChoiceID).Focus() .MinSize( { wxDefaultCoord, wxDefaultCoord } ) .AddChoice(XXO("&Algorithm:"), algChoices, mAlg); S.AddSpace(5); mSizeChoice = S.Id(FreqSizeChoiceID) .MinSize( { wxDefaultCoord, wxDefaultCoord } ) .AddChoice(XXO("&Size:"), sizeChoices, mSize); S.AddSpace(5); mExportButton = S.Id(FreqExportButtonID).AddButton(XXO("&Export...")); S.AddSpace(5); // ---------------------------------------------------------------- // ROW 7: Function, Axix, Grids, Close // ---------------------------------------------------------------- S.AddSpace(5); mFuncChoice = S.Id(FreqFuncChoiceID) .MinSize( { wxDefaultCoord, wxDefaultCoord } ) .AddChoice(XXO("&Function:"), funcChoices, mFunc); mFuncChoice->MoveAfterInTabOrder(mSizeChoice); S.AddSpace(5); mAxisChoice = S.Id(FreqAxisChoiceID) .MinSize( { wxDefaultCoord, wxDefaultCoord } ) .AddChoice(XXO("&Axis:"), axisChoices, mAxis); mAxisChoice->MoveAfterInTabOrder(mFuncChoice); S.AddSpace(5); mReplotButton = S.Id(ReplotButtonID).AddButton(XXO("&Replot...")); S.AddSpace(5); //mCloseButton = S.Id(wxID_CANCEL).AddButton(XO("&Close")); //S.AddSpace(5); } S.EndMultiColumn(); S.AddStandardButtons( eHelpButton | eCloseButton ); // ------------------------------------------------------------------- // ROW 8: Spacer // ------------------------------------------------------------------- S.AddSpace(5); mProgress = safenew FreqGauge(S.GetParent(), wxID_ANY); //, wxST_SIZEGRIP); S.Position(wxEXPAND) .AddWindow(mProgress); // Log-frequency axis works for spectrum plots only. if (mAlg != SpectrumAnalyst::Spectrum) { mAxis = 0; mAxisChoice->Disable(); } mLogAxis = mAxis != 0; mCloseButton = static_cast(FindWindowById( wxID_CANCEL )); mCloseButton->SetDefault(); Layout(); Fit(); // Bug 1607: Center(); SetMinSize(GetSize()); #if defined(__WXGTK__) // This should be rechecked with wx3. // // The scrollbar (focus some reason) doesn't allow tabbing past it // because it can't receive focus. So, convince it otherwise. // // Unfortunately, this still doesn't let you adjust the scrollbar // from the keyboard. Near as I can tell, wxWGTK is capturing the // keyboard input, so the GTK widget doesn't see it, preventing // the normal scroll events from being generated. // // I guess the only way round it would be to handle key actions // ourselves, but we'll leave that for a future date. // gtk_widget_set_can_focus(mPanScroller->m_widget, true); #endif } void FrequencyPlotDialog::OnGetURL(wxCommandEvent & WXUNUSED(event)) { // Original help page is back on-line (March 2016), but the manual should be more reliable. // http://www.eramp.com/WCAG_2_audio_contrast_tool_help.htm HelpSystem::ShowHelp(this, L"Plot Spectrum"); } bool FrequencyPlotDialog::Show(bool show) { if (!show) { mFreqPlot->SetCursor(*mArrowCursor); } bool shown = IsShown(); if (show && !shown) { gPrefs->Read(ENV_DB_KEY, &dBRange, ENV_DB_RANGE); if(dBRange < 90.) dBRange = 90.; GetAudio(); // Don't send an event. We need the recalc right away. // so that mAnalyst is valid when we paint. //SendRecalcEvent(); Recalc(); } bool res = wxDialogWrapper::Show(show); return res; } void FrequencyPlotDialog::GetAudio() { mData.reset(); mDataLen = 0; int selcount = 0; bool warning = false; for (auto track : TrackList::Get( *mProject ).Selected< const WaveTrack >()) { auto &selectedRegion = ViewInfo::Get( *mProject ).selectedRegion; if (selcount==0) { mRate = track->GetRate(); auto start = track->TimeToLongSamples(selectedRegion.t0()); auto end = track->TimeToLongSamples(selectedRegion.t1()); auto dataLen = end - start; if (dataLen > 10485760) { warning = true; mDataLen = 10485760; } else // dataLen is not more than 10 * 2 ^ 20 mDataLen = dataLen.as_size_t(); mData = Floats{ mDataLen }; // Don't allow throw for bad reads track->GetFloats(mData.get(), start, mDataLen, fillZero, false); } else { if (track->GetRate() != mRate) { AudacityMessageBox( XO( "To plot the spectrum, all selected tracks must be the same sample rate.") ); mData.reset(); mDataLen = 0; return; } auto start = track->TimeToLongSamples(selectedRegion.t0()); Floats buffer2{ mDataLen }; // Again, stop exceptions track->GetFloats(buffer2.get(), start, mDataLen, fillZero, false); for (size_t i = 0; i < mDataLen; i++) mData[i] += buffer2[i]; } selcount++; } if (selcount == 0) return; if (warning) { auto msg = XO( "Too much audio was selected. Only the first %.1f seconds of audio will be analyzed.") .Format(mDataLen / mRate); AudacityMessageBox( msg ); } } void FrequencyPlotDialog::OnSize(wxSizeEvent & WXUNUSED(event)) { Layout(); DrawPlot(); Refresh(true); } void FrequencyPlotDialog::DrawBackground(wxMemoryDC & dc) { Layout(); mBitmap.reset(); mPlotRect = mFreqPlot->GetClientRect(); mBitmap = std::make_unique(mPlotRect.width, mPlotRect.height,24); dc.SelectObject(*mBitmap); dc.SetBackground(wxBrush(wxColour(254, 254, 254)));// DONT-THEME Mask colour. dc.Clear(); dc.SetPen(*wxBLACK_PEN); dc.SetBrush(*wxWHITE_BRUSH); dc.DrawRectangle(mPlotRect); dc.SetFont(mFreqFont); } void FrequencyPlotDialog::DrawPlot() { if (!mData || mDataLen < mWindowSize || mAnalyst->GetProcessedSize() == 0) { wxMemoryDC memDC; vRuler->ruler.SetLog(false); vRuler->ruler.SetRange(0.0, -dBRange); hRuler->ruler.SetLog(false); hRuler->ruler.SetRange(0, 1); DrawBackground(memDC); if (mDataLen < mWindowSize) { wxString msg = _("Not enough data selected."); wxSize sz = memDC.GetTextExtent(msg); memDC.DrawText(msg, (mPlotRect.GetWidth() - sz.GetWidth()) / 2, (mPlotRect.GetHeight() - sz.GetHeight()) / 2); } memDC.SelectObject(wxNullBitmap); mFreqPlot->Refresh(); Refresh(); return; } float yRange = mYMax - mYMin; float yTotal = yRange * ((float) mZoomSlider->GetValue() / 100.0f); int sTotal = yTotal * 100; int sRange = yRange * 100; int sPos = mPanScroller->GetThumbPosition() + ((mPanScroller->GetThumbSize() - sTotal) / 2); mPanScroller->SetScrollbar(sPos, sTotal, sRange, sTotal); float yMax = mYMax - ((float)sPos / 100); float yMin = yMax - yTotal; // Set up y axis ruler if (mAlg == SpectrumAnalyst::Spectrum) { vRuler->ruler.SetUnits(XO("dB")); vRuler->ruler.SetFormat(Ruler::LinearDBFormat); } else { vRuler->ruler.SetUnits({}); vRuler->ruler.SetFormat(Ruler::RealFormat); } int w1, w2, h; vRuler->ruler.GetMaxSize(&w1, &h); vRuler->ruler.SetRange(yMax, yMin); // Note inversion for vertical. vRuler->ruler.GetMaxSize(&w2, &h); if( w1 != w2 ) // Reduces flicker { vRuler->SetMinSize(wxSize(w2,h)); Layout(); } vRuler->Refresh(false); wxMemoryDC memDC; DrawBackground(memDC); // Get the plot dimensions // // Must be done after setting the vertical ruler above since the // the width could change. wxRect r = mPlotRect; // Set up x axis ruler int width = r.width - 2; float xMin, xMax, xRatio, xStep; if (mAlg == SpectrumAnalyst::Spectrum) { xMin = mRate / mWindowSize; xMax = mRate / 2; xRatio = xMax / xMin; if (mLogAxis) { xStep = pow(2.0f, (log(xRatio) / log(2.0f)) / width); hRuler->ruler.SetLog(true); } else { xStep = (xMax - xMin) / width; hRuler->ruler.SetLog(false); } hRuler->ruler.SetUnits(XO("Hz")); } else { xMin = 0; xMax = mAnalyst->GetProcessedSize() / mRate; xStep = (xMax - xMin) / width; hRuler->ruler.SetLog(false); /* i18n-hint: short form of 'seconds'.*/ hRuler->ruler.SetUnits(XO("s")); } hRuler->ruler.SetRange(xMin, xMax-xStep); hRuler->Refresh(false); // Draw the plot if (mAlg == SpectrumAnalyst::Spectrum) memDC.SetPen(wxPen(theTheme.Colour( clrHzPlot ), 1, wxPENSTYLE_SOLID)); else memDC.SetPen(wxPen(theTheme.Colour( clrWavelengthPlot), 1, wxPENSTYLE_SOLID)); float xPos = xMin; for (int i = 0; i < width; i++) { float y; if (mLogAxis) y = mAnalyst->GetProcessedValue(xPos, xPos * xStep); else y = mAnalyst->GetProcessedValue(xPos, xPos + xStep); float ynorm = (y - yMin) / yTotal; int lineheight = (int)(ynorm * (r.height - 1)); if (lineheight > r.height - 2) lineheight = r.height - 2; if (ynorm > 0.0) AColor::Line(memDC, r.x + 1 + i, r.y + r.height - 1 - lineheight, r.x + 1 + i, r.y + r.height - 1); if (mLogAxis) xPos *= xStep; else xPos += xStep; } // Outline the graph memDC.SetPen(*wxBLACK_PEN); memDC.SetBrush(*wxTRANSPARENT_BRUSH); memDC.DrawRectangle(r); if(mDrawGrid) { hRuler->ruler.DrawGrid(memDC, r.height, true, true, 1, 1); vRuler->ruler.DrawGrid(memDC, r.width, true, true, 1, 1); } memDC.SelectObject( wxNullBitmap ); mFreqPlot->Refresh(); } void FrequencyPlotDialog::PlotMouseEvent(wxMouseEvent & event) { if (event.Moving() && (event.m_x != mMouseX || event.m_y != mMouseY)) { mMouseX = event.m_x; mMouseY = event.m_y; if (mPlotRect.Contains(mMouseX, mMouseY)) mFreqPlot->SetCursor(*mCrossCursor); else mFreqPlot->SetCursor(*mArrowCursor); mFreqPlot->Refresh(false); } } void FrequencyPlotDialog::OnPanScroller(wxScrollEvent & WXUNUSED(event)) { DrawPlot(); } void FrequencyPlotDialog::OnZoomSlider(wxCommandEvent & WXUNUSED(event)) { DrawPlot(); } void FrequencyPlotDialog::OnAlgChoice(wxCommandEvent & WXUNUSED(event)) { mAlg = SpectrumAnalyst::Algorithm(mAlgChoice->GetSelection()); // Log-frequency axis works for spectrum plots only. if (mAlg == SpectrumAnalyst::Spectrum) { mAxisChoice->Enable(true); mLogAxis = mAxisChoice->GetSelection() ? true : false; } else { mAxisChoice->Disable(); mLogAxis = false; } SendRecalcEvent(); } void FrequencyPlotDialog::OnSizeChoice(wxCommandEvent & WXUNUSED(event)) { long windowSize = 0; mSizeChoice->GetStringSelection().ToLong(&windowSize); mWindowSize = windowSize; SendRecalcEvent(); } void FrequencyPlotDialog::OnFuncChoice(wxCommandEvent & WXUNUSED(event)) { SendRecalcEvent(); } void FrequencyPlotDialog::OnAxisChoice(wxCommandEvent & WXUNUSED(event)) { mLogAxis = mAxisChoice->GetSelection() ? true : false; DrawPlot(); } void FrequencyPlotDialog::PlotPaint(wxPaintEvent & event) { wxPaintDC dc( (wxWindow *) event.GetEventObject() ); dc.DrawBitmap( *mBitmap, 0, 0, true ); // Fix for Bug 1226 "Plot Spectrum freezes... if insufficient samples selected" if (!mData || mDataLen < mWindowSize) return; dc.SetFont(mFreqFont); wxRect r = mPlotRect; int width = r.width - 2; float xMin, xMax, xRatio, xStep; if (mAlg == SpectrumAnalyst::Spectrum) { xMin = mRate / mWindowSize; xMax = mRate / 2; xRatio = xMax / xMin; if (mLogAxis) xStep = pow(2.0f, (log(xRatio) / log(2.0f)) / width); else xStep = (xMax - xMin) / width; } else { xMin = 0; xMax = mAnalyst->GetProcessedSize() / mRate; xStep = (xMax - xMin) / width; } float xPos = xMin; // Find the peak nearest the cursor and plot it if ( r.Contains(mMouseX, mMouseY) & (mMouseX!=0) & (mMouseX!=r.width-1) ) { if (mLogAxis) xPos = xMin * pow(xStep, mMouseX - (r.x + 1)); else xPos = xMin + xStep * (mMouseX - (r.x + 1)); float bestValue = 0; float bestpeak = mAnalyst->FindPeak(xPos, &bestValue); int px; if (mLogAxis) px = (int)(log(bestpeak / xMin) / log(xStep)); else px = (int)((bestpeak - xMin) * width / (xMax - xMin)); dc.SetPen(wxPen(wxColour(160,160,160), 1, wxPENSTYLE_SOLID)); AColor::Line(dc, r.x + 1 + px, r.y, r.x + 1 + px, r.y + r.height); // print out info about the cursor location float value; if (mLogAxis) { xPos = xMin * pow(xStep, mMouseX - (r.x + 1)); value = mAnalyst->GetProcessedValue(xPos, xPos * xStep); } else { xPos = xMin + xStep * (mMouseX - (r.x + 1)); value = mAnalyst->GetProcessedValue(xPos, xPos + xStep); } TranslatableString cursor; TranslatableString peak; if (mAlg == SpectrumAnalyst::Spectrum) { auto xp = PitchName_Absolute(FreqToMIDInote(xPos)); auto pp = PitchName_Absolute(FreqToMIDInote(bestpeak)); /* i18n-hint: The %d's are replaced by numbers, the %s by musical notes, e.g. A#*/ cursor = XO("%d Hz (%s) = %d dB") .Format( (int)(xPos + 0.5), xp, (int)(value + 0.5)); /* i18n-hint: The %d's are replaced by numbers, the %s by musical notes, e.g. A#*/ peak = XO("%d Hz (%s) = %.1f dB") .Format( (int)(bestpeak + 0.5), pp, bestValue ); } else if (xPos > 0.0 && bestpeak > 0.0) { auto xp = PitchName_Absolute(FreqToMIDInote(1.0 / xPos)); auto pp = PitchName_Absolute(FreqToMIDInote(1.0 / bestpeak)); /* i18n-hint: The %d's are replaced by numbers, the %s by musical notes, e.g. A# * the %.4f are numbers, and 'sec' should be an abbreviation for seconds */ cursor = XO("%.4f sec (%d Hz) (%s) = %f") .Format( xPos, (int)(1.0 / xPos + 0.5), xp, value ); /* i18n-hint: The %d's are replaced by numbers, the %s by musical notes, e.g. A# * the %.4f are numbers, and 'sec' should be an abbreviation for seconds */ peak = XO("%.4f sec (%d Hz) (%s) = %.3f") .Format( bestpeak, (int)(1.0 / bestpeak + 0.5), pp, bestValue ); } mCursorText->SetValue( cursor.Translation() ); mPeakText->SetValue( peak.Translation() ); } else { mCursorText->SetValue(wxT("")); mPeakText->SetValue(wxT("")); } // Outline the graph dc.SetPen(*wxBLACK_PEN); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(r); } void FrequencyPlotDialog::OnCloseWindow(wxCloseEvent & WXUNUSED(event)) { Show(false); } void FrequencyPlotDialog::OnCloseButton(wxCommandEvent & WXUNUSED(event)) { gPrefs->Write(wxT("/FrequencyPlotDialog/DrawGrid"), mDrawGrid); gPrefs->Write(wxT("/FrequencyPlotDialog/SizeChoice"), mSizeChoice->GetSelection()); gPrefs->Write(wxT("/FrequencyPlotDialog/AlgChoice"), mAlgChoice->GetSelection()); gPrefs->Write(wxT("/FrequencyPlotDialog/FuncChoice"), mFuncChoice->GetSelection()); gPrefs->Write(wxT("/FrequencyPlotDialog/AxisChoice"), mAxisChoice->GetSelection()); gPrefs->Flush(); Show(false); } void FrequencyPlotDialog::SendRecalcEvent() { wxCommandEvent e(EVT_FREQWINDOW_RECALC, wxID_ANY); GetEventHandler()->AddPendingEvent(e); } void FrequencyPlotDialog::Recalc() { if (!mData || mDataLen < mWindowSize) { DrawPlot(); return; } SpectrumAnalyst::Algorithm alg = SpectrumAnalyst::Algorithm(mAlgChoice->GetSelection()); int windowFunc = mFuncChoice->GetSelection(); wxWindow *hadFocus = FindFocus(); // In wxMac, the skipped window MUST be a top level window. I'd originally made it // just the mProgress window with the idea of preventing user interaction with the // controls while the plot was being recalculated. This doesn't appear to be necessary // so just use the top level window instead. { Optional blocker; if (IsShown()) blocker.emplace(this); wxYieldIfNeeded(); mAnalyst->Calculate(alg, windowFunc, mWindowSize, mRate, mData.get(), mDataLen, &mYMin, &mYMax, mProgress); } if (hadFocus) { hadFocus->SetFocus(); } if (alg == SpectrumAnalyst::Spectrum) { if(mYMin < -dBRange) mYMin = -dBRange; if(mYMax <= -dBRange) mYMax = -dBRange + 10.; // it's all out of range, but show a scale. else mYMax += .5; } // Prime the scrollbar mPanScroller->SetScrollbar(0, (mYMax - mYMin) * 100, (mYMax - mYMin) * 100, 1); DrawPlot(); } void FrequencyPlotDialog::OnExport(wxCommandEvent & WXUNUSED(event)) { wxString fName = _("spectrum.txt"); fName = FileNames::SelectFile(FileNames::Operation::Export, XO("Export Spectral Data As:"), wxEmptyString, fName, wxT("txt"), { FileNames::TextFiles, FileNames::AllFiles }, wxFD_SAVE | wxRESIZE_BORDER, this); if (fName.empty()) return; wxFFileOutputStream ffStream{ fName }; if (!ffStream.IsOk()) { AudacityMessageBox( XO("Couldn't write to file: %s").Format( fName ) ); return; } wxTextOutputStream ss(ffStream); const int processedSize = mAnalyst->GetProcessedSize(); const float *const processed = mAnalyst->GetProcessed(); if (mAlgChoice->GetSelection() == 0) { ss << XO("Frequency (Hz)\tLevel (dB)") << '\n'; for (int i = 1; i < processedSize; i++) ss << wxString::Format(wxT("%f\t%f\n"), i * mRate / mWindowSize, processed[i] ); } else { ss << XO("Lag (seconds)\tFrequency (Hz)\tLevel") << '\n'; for (int i = 1; i < processedSize; i++) ss << wxString::Format(wxT("%f\t%f\t%f\n"), i / mRate, mRate / i, processed[i] ); } } void FrequencyPlotDialog::OnReplot(wxCommandEvent & WXUNUSED(event)) { gPrefs->Read(ENV_DB_KEY, &dBRange, ENV_DB_RANGE); if(dBRange < 90.) dBRange = 90.; GetAudio(); SendRecalcEvent(); } void FrequencyPlotDialog::OnGridOnOff(wxCommandEvent & WXUNUSED(event)) { mDrawGrid = mGridOnOff->IsChecked(); DrawPlot(); } void FrequencyPlotDialog::OnRecalc(wxCommandEvent & WXUNUSED(event)) { Recalc(); } void FrequencyPlotDialog::UpdatePrefs() { bool shown = IsShown(); if (shown) { Show(false); } auto zoomSlider = mZoomSlider->GetValue(); auto drawGrid = mGridOnOff->GetValue(); auto sizeChoice = mSizeChoice->GetStringSelection(); auto algChoice = mAlgChoice->GetSelection(); auto funcChoice = mFuncChoice->GetSelection(); auto axisChoice = mAxisChoice->GetSelection(); SetSizer(nullptr); DestroyChildren(); Populate(); mZoomSlider->SetValue(zoomSlider); mDrawGrid = drawGrid; mGridOnOff->SetValue(drawGrid); long windowSize = 0; sizeChoice.ToLong(&windowSize); mWindowSize = windowSize; mSizeChoice->SetStringSelection(sizeChoice); mAlg = static_cast(algChoice); mAlgChoice->SetSelection(algChoice); mFunc = funcChoice; mFuncChoice->SetSelection(funcChoice); mAxis = axisChoice; mAxisChoice->SetSelection(axisChoice); if (shown) { Show(true); } } BEGIN_EVENT_TABLE(FreqPlot, wxWindow) EVT_ERASE_BACKGROUND(FreqPlot::OnErase) EVT_PAINT(FreqPlot::OnPaint) EVT_MOUSE_EVENTS(FreqPlot::OnMouseEvent) END_EVENT_TABLE() FreqPlot::FreqPlot(wxWindow *parent, wxWindowID winid) : wxWindow(parent, winid) { freqWindow = (FrequencyPlotDialog *) parent; } bool FreqPlot::AcceptsFocus() const { return false; } void FreqPlot::OnErase(wxEraseEvent & WXUNUSED(event)) { // Ignore it to prevent flashing } void FreqPlot::OnPaint(wxPaintEvent & evt) { freqWindow->PlotPaint(evt); } void FreqPlot::OnMouseEvent(wxMouseEvent & event) { freqWindow->PlotMouseEvent(event); } // Remaining code hooks this add-on into the application #include "commands/CommandContext.h" #include "commands/CommandManager.h" #include "commands/ScreenshotCommand.h" namespace { AudacityProject::AttachedWindows::RegisteredFactory sFrequencyWindowKey{ []( AudacityProject &parent ) -> wxWeakRef< wxWindow > { auto &window = ProjectWindow::Get( parent ); return safenew FrequencyPlotDialog( &window, -1, parent, FrequencyAnalysisTitle, wxPoint{ 150, 150 } ); } }; // Define our extra menu item that invokes that factory struct Handler : CommandHandlerObject { void OnPlotSpectrum(const CommandContext &context) { auto &project = context.project; CommandManager::Get(project).RegisterLastAnalyzer(context); //Register Plot Spectrum as Last Analyzer auto freqWindow = &project.AttachedWindows::Get< FrequencyPlotDialog >( sFrequencyWindowKey ); if( ScreenshotCommand::MayCapture( freqWindow ) ) return; freqWindow->Show(true); freqWindow->Raise(); freqWindow->SetFocus(); } }; CommandHandlerObject &findCommandHandler(AudacityProject &) { // Handler is not stateful. Doesn't need a factory registered with // AudacityProject. static Handler instance; return instance; } // Register that menu item using namespace MenuTable; AttachedItem sAttachment{ wxT("Analyze/Analyzers/Windows"), ( FinderScope{ findCommandHandler }, Command( wxT("PlotSpectrum"), XXO("Plot Spectrum..."), &Handler::OnPlotSpectrum, AudioIONotBusyFlag() | WaveTracksSelectedFlag() | TimeSelectedFlag() ) ) }; }