audacia/src/FreqWindow.cpp

1618 lines
42 KiB
C++
Raw Normal View History

/**********************************************************************
Audacity: A Digital Audio Editor
FreqWindow.cpp
Dominic Mazzoni
*******************************************************************//**
\class FreqWindow
\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 FreqWindow 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.
*//****************************************************************//**
\class SpectrumAnalyst
\brief Used for finding the peaks, for snapping to peaks.
This class is used to do the 'find peaks' snapping both in FreqPlot
and in the spectrogram spectral selection.
*//*******************************************************************/
/*
Salvo Ventura - November 2006
Extended range check for additional FFT windows
*/
#include "Audacity.h"
#include "FreqWindow.h"
#include <algorithm>
#include <wx/brush.h>
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/font.h>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/msgdlg.h>
#include <wx/file.h>
#include <wx/filedlg.h>
#include <wx/intl.h>
#include <wx/sizer.h>
#include <wx/statbmp.h>
#include <wx/stattext.h>
#include <wx/statusbr.h>
#include <wx/textfile.h>
#include <math.h>
#include "ShuttleGui.h"
#include "AColor.h"
#include "FFT.h"
#include "Internat.h"
#include "PitchName.h"
#include "prefs/GUISettings.h"
#include "Prefs.h"
#include "Project.h"
#include "WaveClip.h"
#include "Theme.h"
#include "AllThemeResources.h"
#include "FileDialog.h"
2015-07-03 04:20:21 +00:00
#include "WaveTrack.h"
#include "Experimental.h"
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",
" ++++ ",
" @+# $+@ ",
" + @@ +@ ",
" +# @ #+ ",
" +@****** +@",
" + ****** +@",
" +# #+@",
" + +@@",
" +++# #+@@ ",
" +++@++++@@ ",
" +++@@ @@@@ ",
" +++@@ ",
" +++@@ ",
"+++@@ ",
"@+@@ ",
" @@ "};
// FreqWindow
BEGIN_EVENT_TABLE(FreqWindow, wxDialog)
EVT_CLOSE(FreqWindow::OnCloseWindow)
EVT_SIZE(FreqWindow::OnSize)
EVT_SLIDER(FreqZoomSliderID, FreqWindow::OnZoomSlider)
EVT_COMMAND_SCROLL(FreqPanScrollerID, FreqWindow::OnPanScroller)
EVT_CHOICE(FreqAlgChoiceID, FreqWindow::OnAlgChoice)
EVT_CHOICE(FreqSizeChoiceID, FreqWindow::OnSizeChoice)
EVT_CHOICE(FreqFuncChoiceID, FreqWindow::OnFuncChoice)
EVT_CHOICE(FreqAxisChoiceID, FreqWindow::OnAxisChoice)
EVT_BUTTON(FreqExportButtonID, FreqWindow::OnExport)
EVT_BUTTON(ReplotButtonID, FreqWindow::OnReplot)
EVT_BUTTON(wxID_CANCEL, FreqWindow::OnCloseButton)
EVT_CHECKBOX(GridOnOffID, FreqWindow::OnGridOnOff)
EVT_COMMAND(wxID_ANY, EVT_FREQWINDOW_RECALC, FreqWindow::OnRecalc)
END_EVENT_TABLE()
SpectrumAnalyst::SpectrumAnalyst()
: mAlg(Spectrum)
, mRate(0.0)
, mWindowSize(0)
{
}
SpectrumAnalyst::~SpectrumAnalyst()
{
}
FreqWindow::FreqWindow(wxWindow * parent, wxWindowID id,
const wxString & title,
const wxPoint & pos)
: wxDialog(parent, id, title, pos, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMAXIMIZE_BOX),
mData(NULL),
mBitmap(NULL),
mAnalyst(new SpectrumAnalyst())
{
SetName(GetTitle());
mMouseX = 0;
mMouseY = 0;
mRate = 0;
mDataLen = 0;
p = GetActiveProject();
if (!p)
return;
wxArrayString algChoices;
algChoices.Add(_("Spectrum"));
algChoices.Add(_("Standard Autocorrelation"));
algChoices.Add(_("Cuberoot Autocorrelation"));
algChoices.Add(_("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. */
algChoices.Add(_("Cepstrum"));
wxArrayString sizeChoices;
sizeChoices.Add(wxT("128"));
sizeChoices.Add(wxT("256"));
sizeChoices.Add(wxT("512"));
sizeChoices.Add(wxT("1024"));
sizeChoices.Add(wxT("2048"));
sizeChoices.Add(wxT("4096"));
sizeChoices.Add(wxT("8192"));
sizeChoices.Add(wxT("16384"));
sizeChoices.Add(wxT("32768"));
sizeChoices.Add(wxT("65536"));
wxArrayString funcChoices;
for (int i = 0, cnt = NumWindowFuncs(); i < cnt; i++)
{
/* i18n-hint: This refers to a "window function", used in the
* Frequency analyze dialog box. */
funcChoices.Add(wxString(WindowFuncName(i)) + wxT(" ") + _("window"));
}
wxArrayString axisChoices;
axisChoices.Add(_("Linear frequency"));
axisChoices.Add(_("Log frequency"));
mFreqFont = wxFont(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
mArrowCursor = new wxCursor(wxCURSOR_ARROW);
mCrossCursor = new wxCursor(wxCURSOR_CROSS);
gPrefs->Read(wxT("/FreqWindow/DrawGrid"), &mDrawGrid, true);
long size;
gPrefs->Read(wxT("/FreqWindow/SizeChoice"), &mSize, 2);
sizeChoices[mSize].ToLong(&size);
mWindowSize = size;
int alg;
gPrefs->Read(wxT("/FreqWindow/AlgChoice"), &alg, 0);
mAlg = static_cast<SpectrumAnalyst::Algorithm>(alg);
gPrefs->Read(wxT("/FreqWindow/FuncChoice"), &mFunc, 3);
gPrefs->Read(wxT("/FreqWindow/AxisChoice"), &mAxis, 0);
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(this, wxID_ANY);
vRuler->ruler.SetBounds(0, 0, 100, 100); // Ruler can't handle small sizes
vRuler->ruler.SetOrientation(wxVERTICAL);
vRuler->ruler.SetRange(0.0, -dBRange);
vRuler->ruler.SetFormat(Ruler::LinearDBFormat);
vRuler->ruler.SetUnits(_("dB"));
vRuler->ruler.SetLabelEdges(true);
int w;
vRuler->ruler.GetMaxSize(&w, NULL);
2015-08-15 05:00:56 +00:00
vRuler->SetMinSize(wxSize(w, 150)); // height needed for wxGTK
S.AddSpace(wxDefaultCoord, 1);
S.Prop(1);
S.AddWindow(vRuler, wxALIGN_RIGHT | wxALIGN_TOP);
S.AddSpace(wxDefaultCoord, 1);
}
S.EndVerticalLay();
mFreqPlot = safenew FreqPlot(this);
mFreqPlot->SetMinSize(wxSize(wxDefaultCoord, FREQ_WINDOW_HEIGHT));
S.Prop(1);
S.AddWindow(mFreqPlot, wxEXPAND);
S.StartHorizontalLay(wxEXPAND, 0);
{
S.StartVerticalLay();
{
mPanScroller = safenew wxScrollBar(this, FreqPanScrollerID,
wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL);
mPanScroller->SetName(_("Scroll"));
S.Prop(1);
S.AddWindow(mPanScroller, wxALIGN_LEFT | wxTOP);
}
S.EndVerticalLay();
S.StartVerticalLay();
{
wxStaticBitmap *zi = safenew wxStaticBitmap(this, wxID_ANY, wxBitmap(ZoomIn));
S.AddWindow((wxWindow *) zi, wxALIGN_CENTER);
S.AddSpace(5);
mZoomSlider = safenew wxSlider(this, FreqZoomSliderID, 100, 1, 100,
wxDefaultPosition, wxDefaultSize, wxSL_VERTICAL);
S.Prop(1);
S.AddWindow(mZoomSlider, wxALIGN_CENTER_HORIZONTAL);
mZoomSlider->SetName(_("Zoom"));
S.AddSpace(5);
wxStaticBitmap *zo = safenew wxStaticBitmap(this, wxID_ANY, wxBitmap(ZoomOut));
S.AddWindow((wxWindow *) zo, wxALIGN_CENTER);
}
S.EndVerticalLay();
S.AddSpace(5, wxDefaultCoord);
}
S.EndHorizontalLay();
// -------------------------------------------------------------------
// ROW 2: Frequency ruler
// -------------------------------------------------------------------
S.AddSpace(1);
S.StartHorizontalLay(wxEXPAND, 0);
{
hRuler = safenew RulerPanel(this, wxID_ANY);
hRuler->ruler.SetBounds(0, 0, 100, 100); // Ruler can't handle small sizes
hRuler->ruler.SetOrientation(wxHORIZONTAL);
hRuler->ruler.SetLog(true);
hRuler->ruler.SetRange(10, 20000);
hRuler->ruler.SetFormat(Ruler::RealFormat);
hRuler->ruler.SetUnits(wxT("Hz"));
hRuler->ruler.SetFlip(true);
hRuler->ruler.SetLabelEdges(true);
int h;
hRuler->ruler.GetMaxSize(NULL, &h);
hRuler->SetMinSize(wxSize(wxDefaultCoord, h));
S.AddSpace(1, wxDefaultCoord);
S.Prop(1);
S.AddWindow(hRuler, wxALIGN_LEFT | wxALIGN_TOP);
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(4);
S.SetStretchyCol(1);
S.SetStretchyCol(3);
{
S.AddPrompt(wxT("Cursor:"));
S.SetStyle(wxTE_READONLY);
mCursorText = S.AddTextBox(wxT(""), wxT(""), 10);
S.AddPrompt(wxT("Peak:"));
S.SetStyle(wxTE_READONLY);
mPeakText = S.AddTextBox(wxT(""), wxT(""), 10);
}
S.EndMultiColumn();
}
S.EndHorizontalLay();
S.AddSpace(1);
}
S.EndMultiColumn();
// -------------------------------------------------------------------
// ROW 5: Spacer
// -------------------------------------------------------------------
S.AddSpace(5);
S.SetBorder(2);
S.SetSizerProportion(0);
S.StartMultiColumn(11, wxALIGN_CENTER);
{
// ----------------------------------------------------------------
// ROW 6: Algorithm, Size, Export, Replot
// ----------------------------------------------------------------
S.AddSpace(5);
mAlgChoice = S.Id(FreqAlgChoiceID).AddChoice(_("&Algorithm:"), wxT(""), &algChoices);
mAlgChoice->SetSelection(mAlg);
S.SetSizeHints(wxDefaultCoord, wxDefaultCoord);
S.AddSpace(5);
mSizeChoice = S.Id(FreqSizeChoiceID).AddChoice(_("&Size:"), wxT(""), &sizeChoices);
mSizeChoice->SetSelection(mSize);
S.SetSizeHints(wxDefaultCoord, wxDefaultCoord);
S.AddSpace(5);
mExportButton = S.Id(FreqExportButtonID).AddButton(_("&Export..."));
S.AddSpace(5);
mReplotButton = S.Id(ReplotButtonID).AddButton(_("&Replot..."));
S.AddSpace(5);
// ----------------------------------------------------------------
// ROW 7: Function, Axix, Grids, Close
// ----------------------------------------------------------------
S.AddSpace(5);
mFuncChoice = S.Id(FreqFuncChoiceID).AddChoice(_("&Function:"), wxT(""), &funcChoices);
mFuncChoice->SetSelection(mFunc);
S.SetSizeHints(wxDefaultCoord, wxDefaultCoord);
2015-05-16 07:39:57 +00:00
mFuncChoice->MoveAfterInTabOrder(mSizeChoice);
S.AddSpace(5);
mAxisChoice = S.Id(FreqAxisChoiceID).AddChoice(_("&Axis:"), wxT(""), &axisChoices);
mAxisChoice->SetSelection(mAxis);
S.SetSizeHints(wxDefaultCoord, wxDefaultCoord);
2015-05-16 07:39:57 +00:00
mAxisChoice->MoveAfterInTabOrder(mFuncChoice);
S.AddSpace(5);
mGridOnOff = S.Id(GridOnOffID).AddCheckBox(_("&Grids"), wxT("false"));
mGridOnOff->SetValue(mDrawGrid);
S.AddSpace(5);
mCloseButton = S.Id(wxID_CANCEL).AddButton(_("&Close"));
S.AddSpace(5);
}
S.EndMultiColumn();
// -------------------------------------------------------------------
// ROW 8: Spacer
// -------------------------------------------------------------------
S.AddSpace(5);
mProgress = safenew FreqGauge(this); //, wxID_ANY, wxST_SIZEGRIP);
S.AddWindow(mProgress, wxEXPAND);
// Log-frequency axis works for spectrum plots only.
if (mAlg != SpectrumAnalyst::Spectrum)
{
mAxis = 0;
mAxisChoice->Disable();
}
mLogAxis = mAxis != 0;
mCloseButton->SetDefault();
mCloseButton->SetFocus();
Layout();
Fit();
SetMinSize(GetSize());
mAlgChoice->SetFocus();
#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
}
FreqWindow::~FreqWindow()
{
if (mBitmap)
delete mBitmap;
if (mData)
delete[] mData;
delete mArrowCursor;
delete mCrossCursor;
}
bool FreqWindow::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 = wxDialog::Show(show);
return res;
}
void FreqWindow::GetAudio()
{
if (mData) {
delete [] mData;
mData = NULL;
}
mDataLen = 0;
int selcount = 0;
bool warning = false;
TrackListIterator iter(p->GetTracks());
Track *t = iter.First();
while (t) {
if (t->GetSelected() && t->GetKind() == Track::Wave) {
WaveTrack *track = (WaveTrack *)t;
if (selcount==0) {
mRate = track->GetRate();
sampleCount start, end;
start = track->TimeToLongSamples(p->mViewInfo.selectedRegion.t0());
end = track->TimeToLongSamples(p->mViewInfo.selectedRegion.t1());
mDataLen = (sampleCount)(end - start);
if (mDataLen > 10485760) {
warning = true;
mDataLen = 10485760;
}
mData = new float[mDataLen];
track->Get((samplePtr)mData, floatSample, start, mDataLen);
}
else {
if (track->GetRate() != mRate) {
wxMessageBox(_("To plot the spectrum, all selected tracks must be the same sample rate."));
delete[] mData;
mData = NULL;
mDataLen = 0;
return;
}
sampleCount start;
start = track->TimeToLongSamples(p->mViewInfo.selectedRegion.t0());
float *buffer2 = new float[mDataLen];
track->Get((samplePtr)buffer2, floatSample, start, mDataLen);
for (int i = 0; i < mDataLen; i++)
mData[i] += buffer2[i];
delete[] buffer2;
}
selcount++;
}
t = iter.Next();
}
2014-06-03 20:30:19 +00:00
if (selcount == 0)
return;
if (warning) {
wxString msg;
msg.Printf(_("Too much audio was selected. Only the first %.1f seconds of audio will be analyzed."),
(mDataLen / mRate));
wxMessageBox(msg);
}
}
void FreqWindow::OnSize(wxSizeEvent & WXUNUSED(event))
{
Layout();
DrawPlot();
Refresh(true);
}
void FreqWindow::DrawBackground(wxMemoryDC & dc)
{
Layout();
if (mBitmap)
{
delete mBitmap;
mBitmap = NULL;
}
mPlotRect = mFreqPlot->GetClientRect();
2014-06-03 20:30:19 +00:00
mBitmap = new wxBitmap(mPlotRect.width, mPlotRect.height);
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 FreqWindow::DrawPlot()
{
2015-08-02 05:58:46 +00:00
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(_("dB"));
vRuler->ruler.SetFormat(Ruler::LinearDBFormat);
} else {
vRuler->ruler.SetUnits(wxT(""));
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(_("Hz"));
} else {
xMin = 0;
xMax = mAnalyst->GetProcessedSize() / mRate;
xStep = (xMax - xMin) / width;
hRuler->ruler.SetLog(false);
hRuler->ruler.SetUnits(_("s"));
}
hRuler->ruler.SetRange(xMin, xMax-xStep);
hRuler->Refresh(false);
// Draw the plot
if (mAlg == SpectrumAnalyst::Spectrum)
memDC.SetPen(wxPen(theTheme.Colour( clrHzPlot ), 1, wxSOLID));
else
memDC.SetPen(wxPen(theTheme.Colour( clrWavelengthPlot), 1, wxSOLID));
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 FreqWindow::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 FreqWindow::OnPanScroller(wxScrollEvent & WXUNUSED(event))
{
DrawPlot();
}
void FreqWindow::OnZoomSlider(wxCommandEvent & WXUNUSED(event))
{
DrawPlot();
}
void FreqWindow::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 FreqWindow::OnSizeChoice(wxCommandEvent & WXUNUSED(event))
{
long windowSize = 0;
mSizeChoice->GetStringSelection().ToLong(&windowSize);
mWindowSize = windowSize;
SendRecalcEvent();
}
void FreqWindow::OnFuncChoice(wxCommandEvent & WXUNUSED(event))
{
SendRecalcEvent();
}
void FreqWindow::OnAxisChoice(wxCommandEvent & WXUNUSED(event))
{
mLogAxis = mAxisChoice->GetSelection() ? true : false;
DrawPlot();
}
void FreqWindow::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, wxSOLID));
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);
}
wxString cursor;
wxString peak;
wxString xpitch;
wxString peakpitch;
const wxChar *xp;
const wxChar *pp;
if (mAlg == SpectrumAnalyst::Spectrum) {
xpitch = PitchName_Absolute(FreqToMIDInote(xPos));
peakpitch = PitchName_Absolute(FreqToMIDInote(bestpeak));
xp = xpitch.c_str();
pp = peakpitch.c_str();
/* i18n-hint: The %d's are replaced by numbers, the %s by musical notes, e.g. A#*/
cursor.Printf(_("%d Hz (%s) = %d dB"), int (xPos + 0.5), xp, int (value + 0.5));
peak.Printf(_("%d Hz (%s) = %.1f dB"), int (bestpeak + 0.5), pp, bestValue);
} else if (xPos > 0.0 && bestpeak > 0.0) {
xpitch = PitchName_Absolute(FreqToMIDInote(1.0 / xPos));
peakpitch = PitchName_Absolute(FreqToMIDInote(1.0 / bestpeak));
xp = xpitch.c_str();
pp = peakpitch.c_str();
/* i18n-hint: The %d's are replaced by numbers, the %s by musical notes, e.g. A#
2012-03-20 15:36:02 +00:00
* the %.4f are numbers, and 'sec' should be an abbreviation for seconds */
cursor.Printf(_("%.4f sec (%d Hz) (%s) = %f"),
xPos, int (1.0 / xPos + 0.5), xp, value);
peak.Printf(_("%.4f sec (%d Hz) (%s) = %.3f"),
bestpeak, int (1.0 / bestpeak + 0.5), pp, bestValue);
}
mCursorText->SetValue(cursor);
mPeakText->SetValue(peak);
}
else {
mCursorText->SetValue(wxT(""));
mPeakText->SetValue(wxT(""));
}
// Outline the graph
dc.SetPen(*wxBLACK_PEN);
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(r);
}
void FreqWindow::OnCloseWindow(wxCloseEvent & WXUNUSED(event))
{
Show(false);
}
void FreqWindow::OnCloseButton(wxCommandEvent & WXUNUSED(event))
{
gPrefs->Write(wxT("/FreqWindow/DrawGrid"), mDrawGrid);
gPrefs->Write(wxT("/FreqWindow/SizeChoice"), mSizeChoice->GetSelection());
gPrefs->Write(wxT("/FreqWindow/AlgChoice"), mAlgChoice->GetSelection());
gPrefs->Write(wxT("/FreqWindow/FuncChoice"), mFuncChoice->GetSelection());
gPrefs->Write(wxT("/FreqWindow/AxisChoice"), mAxisChoice->GetSelection());
gPrefs->Flush();
Show(false);
}
void FreqWindow::SendRecalcEvent()
{
wxCommandEvent e(EVT_FREQWINDOW_RECALC, wxID_ANY);
GetEventHandler()->AddPendingEvent(e);
}
void FreqWindow::Recalc()
{
if (!mData || mDataLen < mWindowSize) {
DrawPlot();
return;
}
SpectrumAnalyst::Algorithm alg =
SpectrumAnalyst::Algorithm(mAlgChoice->GetSelection());
int windowFunc = mFuncChoice->GetSelection();
wxWindow *hadFocus = FindFocus();
2015-09-02 05:52:20 +00:00
// 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 the top level window instead.
{
wxWindowDisabler blocker(this);
wxYieldIfNeeded();
mAnalyst->Calculate(alg, windowFunc, mWindowSize, mRate,
mData, 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 FreqWindow::OnExport(wxCommandEvent & WXUNUSED(event))
{
wxString fName = _("spectrum.txt");
fName = FileSelector(_("Export Spectral Data As:"),
wxEmptyString, fName, wxT("txt"), wxT("*.txt"), wxFD_SAVE | wxRESIZE_BORDER, this);
if (fName == wxT(""))
return;
wxTextFile f(fName);
#ifdef __WXMAC__
wxFile{}.Create(fName);
#else
f.Create();
#endif
f.Open();
if (!f.IsOpened()) {
wxMessageBox(_("Couldn't write to file: ") + fName);
return;
}
const int processedSize = mAnalyst->GetProcessedSize();
const float *const processed = mAnalyst->GetProcessed();
if (mAlgChoice->GetSelection() == 0) {
f.AddLine(_("Frequency (Hz)\tLevel (dB)"));
for (int i = 1; i < processedSize; i++)
f.AddLine(wxString::
Format(wxT("%f\t%f"), i * mRate / mWindowSize,
processed[i]));
} else {
f.AddLine(_("Lag (seconds)\tFrequency (Hz)\tLevel"));
for (int i = 1; i < processedSize; i++)
f.AddLine(wxString::Format(wxT("%f\t%f\t%f"),
i / mRate, mRate / i, processed[i]));
}
#ifdef __WXMAC__
f.Write(wxTextFileType_Mac);
#else
f.Write();
#endif
f.Close();
}
void FreqWindow::OnReplot(wxCommandEvent & WXUNUSED(event))
{
gPrefs->Read(ENV_DB_KEY, &dBRange, ENV_DB_RANGE);
if(dBRange < 90.)
dBRange = 90.;
GetAudio();
SendRecalcEvent();
}
void FreqWindow::OnGridOnOff(wxCommandEvent & WXUNUSED(event))
{
mDrawGrid = mGridOnOff->IsChecked();
DrawPlot();
}
void FreqWindow::OnRecalc(wxCommandEvent & WXUNUSED(event))
{
Recalc();
}
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)
: wxWindow(parent, wxID_ANY)
{
freqWindow = (FreqWindow *) 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);
}
FreqGauge::FreqGauge(wxWindow * parent)
: wxStatusBar(parent, wxID_ANY, wxST_SIZEGRIP)
{
mRange = 0;
}
void FreqGauge::SetRange(int range, int bar, int gap)
{
mRange = range;
mBar = bar;
mGap = gap;
GetFieldRect(0, mRect);
mRect.Inflate(-1);
mInterval = mRange / (mRect.width / (mBar + mGap));
mRect.width = mBar;
mMargin = mRect.x;
mLast = -1;
Update();
}
void FreqGauge::SetValue(int value)
{
mCur = value / mInterval;
if (mCur != mLast)
{
wxClientDC dc(this);
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxColour(100, 100, 220));
while (mLast < mCur)
{
mLast++;
mRect.x = mMargin + mLast * (mBar + mGap);
dc.DrawRectangle(mRect);
}
Update();
}
}
void FreqGauge::Reset()
{
mRange = 0;
Refresh(true);
}
bool SpectrumAnalyst::Calculate(Algorithm alg, int windowFunc,
int windowSize, double rate,
const float *data, int dataLen,
float *pYMin, float *pYMax,
FreqGauge *progress)
{
// Wipe old data
mProcessed.resize(0);
mRate = 0.0;
mWindowSize = 0;
// Validate inputs
int f = NumWindowFuncs();
if (!(windowSize >= 32 && windowSize <= 65536 &&
alg >= SpectrumAnalyst::Spectrum &&
alg < SpectrumAnalyst::NumAlgorithms &&
windowFunc >= 0 && windowFunc < f)) {
return false;
}
if (dataLen < windowSize) {
return false;
}
// Now repopulate
mRate = rate;
mWindowSize = windowSize;
mAlg = alg;
int half = mWindowSize / 2;
mProcessed.resize(mWindowSize);
float *in = new float[mWindowSize];
float *out = new float[mWindowSize];
float *out2 = new float[mWindowSize];
float *win = new float[mWindowSize];
for (int i = 0; i < mWindowSize; i++) {
mProcessed[i] = 0.0f;
win[i] = 1.0f;
}
WindowFunc(windowFunc, mWindowSize, win);
// Scale window such that an amplitude of 1.0 in the time domain
// shows an amplitude of 0dB in the frequency domain
double wss = 0;
for(int i=0; i<mWindowSize; i++)
wss += win[i];
if(wss > 0)
wss = 4.0 / (wss*wss);
else
wss = 1.0;
if (progress) {
progress->SetRange(dataLen);
}
int start = 0;
int windows = 0;
while (start + mWindowSize <= dataLen) {
for (int i = 0; i < mWindowSize; i++)
in[i] = win[i] * data[start + i];
switch (alg) {
case Spectrum:
PowerSpectrum(mWindowSize, in, out);
for (int i = 0; i < half; i++)
mProcessed[i] += out[i];
break;
case Autocorrelation:
case CubeRootAutocorrelation:
case EnhancedAutocorrelation:
// Take FFT
#ifdef EXPERIMENTAL_USE_REALFFTF
RealFFT(mWindowSize, in, out, out2);
#else
FFT(mWindowSize, false, in, NULL, out, out2);
#endif
// Compute power
for (int i = 0; i < mWindowSize; i++)
in[i] = (out[i] * out[i]) + (out2[i] * out2[i]);
if (alg == Autocorrelation) {
for (int i = 0; i < mWindowSize; i++)
in[i] = sqrt(in[i]);
}
if (alg == CubeRootAutocorrelation ||
alg == EnhancedAutocorrelation) {
// Tolonen and Karjalainen recommend taking the cube root
// of the power, instead of the square root
for (int i = 0; i < mWindowSize; i++)
in[i] = pow(in[i], 1.0f / 3.0f);
}
// Take FFT
#ifdef EXPERIMENTAL_USE_REALFFTF
RealFFT(mWindowSize, in, out, out2);
#else
FFT(mWindowSize, false, in, NULL, out, out2);
#endif
// Take real part of result
for (int i = 0; i < half; i++)
mProcessed[i] += out[i];
break;
case Cepstrum:
#ifdef EXPERIMENTAL_USE_REALFFTF
RealFFT(mWindowSize, in, out, out2);
#else
FFT(mWindowSize, false, in, NULL, out, out2);
#endif
// Compute log power
// Set a sane lower limit assuming maximum time amplitude of 1.0
{
float power;
float minpower = 1e-20*mWindowSize*mWindowSize;
for (int i = 0; i < mWindowSize; i++)
{
power = (out[i] * out[i]) + (out2[i] * out2[i]);
if(power < minpower)
in[i] = log(minpower);
else
in[i] = log(power);
}
// Take IFFT
#ifdef EXPERIMENTAL_USE_REALFFTF
InverseRealFFT(mWindowSize, in, NULL, out);
#else
FFT(mWindowSize, true, in, NULL, out, out2);
#endif
// Take real part of result
for (int i = 0; i < half; i++)
mProcessed[i] += out[i];
}
break;
default:
wxASSERT(false);
break;
} //switch
// Update the progress bar
if (progress) {
progress->SetValue(start);
}
start += half;
windows++;
}
if (progress) {
// Reset for next time
progress->Reset();
}
float mYMin = 1000000, mYMax = -1000000;
switch (alg) {
double scale;
case Spectrum:
// Convert to decibels
mYMin = 1000000.;
mYMax = -1000000.;
scale = wss / (double)windows;
for (int i = 0; i < half; i++)
{
mProcessed[i] = 10 * log10(mProcessed[i] * scale);
if(mProcessed[i] > mYMax)
mYMax = mProcessed[i];
else if(mProcessed[i] < mYMin)
mYMin = mProcessed[i];
}
break;
case Autocorrelation:
case CubeRootAutocorrelation:
for (int i = 0; i < half; i++)
mProcessed[i] = mProcessed[i] / windows;
// Find min/max
mYMin = mProcessed[0];
mYMax = mProcessed[0];
for (int i = 1; i < half; i++)
if (mProcessed[i] > mYMax)
mYMax = mProcessed[i];
else if (mProcessed[i] < mYMin)
mYMin = mProcessed[i];
break;
case EnhancedAutocorrelation:
for (int i = 0; i < half; i++)
mProcessed[i] = mProcessed[i] / windows;
// Peak Pruning as described by Tolonen and Karjalainen, 2000
// Clip at zero, copy to temp array
for (int i = 0; i < half; i++) {
if (mProcessed[i] < 0.0)
mProcessed[i] = float(0.0);
out[i] = mProcessed[i];
}
// Subtract a time-doubled signal (linearly interp.) from the original
// (clipped) signal
for (int i = 0; i < half; i++)
if ((i % 2) == 0)
mProcessed[i] -= out[i / 2];
else
mProcessed[i] -= ((out[i / 2] + out[i / 2 + 1]) / 2);
// Clip at zero again
for (int i = 0; i < half; i++)
if (mProcessed[i] < 0.0)
mProcessed[i] = float(0.0);
// Find NEW min/max
mYMin = mProcessed[0];
mYMax = mProcessed[0];
for (int i = 1; i < half; i++)
if (mProcessed[i] > mYMax)
mYMax = mProcessed[i];
else if (mProcessed[i] < mYMin)
mYMin = mProcessed[i];
break;
case Cepstrum:
for (int i = 0; i < half; i++)
mProcessed[i] = mProcessed[i] / windows;
// Find min/max, ignoring first and last few values
{
int ignore = 4;
mYMin = mProcessed[ignore];
mYMax = mProcessed[ignore];
for (int i = ignore + 1; i < half - ignore; i++)
if (mProcessed[i] > mYMax)
mYMax = mProcessed[i];
else if (mProcessed[i] < mYMin)
mYMin = mProcessed[i];
}
break;
default:
wxASSERT(false);
break;
}
delete[]in;
delete[]out;
delete[]out2;
delete[]win;
if (pYMin)
*pYMin = mYMin;
if (pYMax)
*pYMax = mYMax;
return true;
}
const float *SpectrumAnalyst::GetProcessed() const
{
return &mProcessed[0];
}
int SpectrumAnalyst::GetProcessedSize() const
{
return mProcessed.size() / 2;
}
float SpectrumAnalyst::GetProcessedValue(float freq0, float freq1) const
{
float bin0, bin1, binwidth;
if (mAlg == Spectrum) {
bin0 = freq0 * mWindowSize / mRate;
bin1 = freq1 * mWindowSize / mRate;
} else {
bin0 = freq0 * mRate;
bin1 = freq1 * mRate;
}
binwidth = bin1 - bin0;
float value = float(0.0);
if (binwidth < 1.0) {
float binmid = (bin0 + bin1) / 2.0;
int ibin = int (binmid) - 1;
if (ibin < 1)
ibin = 1;
if (ibin >= GetProcessedSize() - 3)
ibin = std::max(0, GetProcessedSize() - 4);
value = CubicInterpolate(mProcessed[ibin],
mProcessed[ibin + 1],
mProcessed[ibin + 2],
mProcessed[ibin + 3], binmid - ibin);
} else {
if (bin0 < 0)
bin0 = 0;
if (bin1 >= GetProcessedSize())
bin1 = GetProcessedSize() - 1;
if (int (bin1) > int (bin0))
value += mProcessed[int (bin0)] * (int (bin0) + 1 - bin0);
bin0 = 1 + int (bin0);
while (bin0 < int (bin1)) {
value += mProcessed[int (bin0)];
bin0 += 1.0;
}
value += mProcessed[int (bin1)] * (bin1 - int (bin1));
value /= binwidth;
}
return value;
}
float SpectrumAnalyst::FindPeak(float xPos, float *pY) const
{
float bestpeak = 0.0f;
float bestValue = 0.0;
if (GetProcessedSize() > 1) {
bool up = (mProcessed[1] > mProcessed[0]);
float bestdist = 1000000;
for (int bin = 3; bin < GetProcessedSize() - 1; bin++) {
bool nowUp = mProcessed[bin] > mProcessed[bin - 1];
if (!nowUp && up) {
// Local maximum. Find actual value by cubic interpolation
int leftbin = bin - 2;
/*
if (leftbin < 1)
leftbin = 1;
*/
float valueAtMax = 0.0;
float max = leftbin + CubicMaximize(mProcessed[leftbin],
mProcessed[leftbin + 1],
mProcessed[leftbin + 2],
mProcessed[leftbin + 3],
&valueAtMax);
float thispeak;
if (mAlg == Spectrum)
thispeak = max * mRate / mWindowSize;
else
thispeak = max / mRate;
if (fabs(thispeak - xPos) < bestdist) {
bestpeak = thispeak;
bestdist = fabs(thispeak - xPos);
bestValue = valueAtMax;
// Should this test come after the enclosing if?
if (thispeak > xPos)
break;
}
}
up = nowUp;
}
}
if (pY)
*pY = bestValue;
return bestpeak;
}
// If f(0)=y0, f(1)=y1, f(2)=y2, and f(3)=y3, this function finds
// the degree-three polynomial which best fits these points and
// returns the value of this polynomial at a value x. Usually
// 0 < x < 3
float SpectrumAnalyst::CubicInterpolate(float y0, float y1, float y2, float y3, float x) const
{
float a, b, c, d;
a = y0 / -6.0 + y1 / 2.0 - y2 / 2.0 + y3 / 6.0;
b = y0 - 5.0 * y1 / 2.0 + 2.0 * y2 - y3 / 2.0;
c = -11.0 * y0 / 6.0 + 3.0 * y1 - 3.0 * y2 / 2.0 + y3 / 3.0;
d = y0;
float xx = x * x;
float xxx = xx * x;
return (a * xxx + b * xx + c * x + d);
}
float SpectrumAnalyst::CubicMaximize(float y0, float y1, float y2, float y3, float * max) const
{
// Find coefficients of cubic
float a, b, c, d;
a = y0 / -6.0 + y1 / 2.0 - y2 / 2.0 + y3 / 6.0;
b = y0 - 5.0 * y1 / 2.0 + 2.0 * y2 - y3 / 2.0;
c = -11.0 * y0 / 6.0 + 3.0 * y1 - 3.0 * y2 / 2.0 + y3 / 3.0;
d = y0;
// Take derivative
float da, db, dc;
da = 3 * a;
db = 2 * b;
dc = c;
// Find zeroes of derivative using quadratic equation
float discriminant = db * db - 4 * da * dc;
if (discriminant < 0.0)
return float(-1.0); // error
float x1 = (-db + sqrt(discriminant)) / (2 * da);
float x2 = (-db - sqrt(discriminant)) / (2 * da);
// The one which corresponds to a local _maximum_ in the
// cubic is the one we want - the one with a negative
// second derivative
float dda = 2 * da;
float ddb = db;
if (dda * x1 + ddb < 0)
{
*max = a*x1*x1*x1+b*x1*x1+c*x1+d;
return x1;
}
else
{
*max = a*x2*x2*x2+b*x2*x2+c*x2+d;
return x2;
}
}