diff --git a/src/FreqWindow.cpp b/src/FreqWindow.cpp index 2a6bb5d29..afce710d4 100644 --- a/src/FreqWindow.cpp +++ b/src/FreqWindow.cpp @@ -237,6 +237,12 @@ FrequencyPlotDialog::~FrequencyPlotDialog() /// I chose to not use wxSL_INVERSE and retain "min zoom is max slider value". constexpr int MAX_ZOOMED_OUT_V = 100; +/// The horizontal zoom slider ranges from [0..HZOOM_COUNT] inclusive. +constexpr int HZOOM_COUNT = 128; +/// Horizontal zoom is exponential. When you increase the slider position by +/// HZOOM_STEPS_PER_DOUBLE steps, the zoom doubles and the viewport size halves. +constexpr int HZOOM_STEPS_PER_DOUBLE = 16; + void FrequencyPlotDialog::Populate() { SetTitle(FrequencyAnalysisTitle); @@ -444,7 +450,8 @@ void FrequencyPlotDialog::Populate() S.AddSpace(5); - hZoomSlider = safenew wxSliderWrapper(S.GetParent(), FreqHZoomSliderID, 100, 1, 100, + hZoomSlider = safenew wxSliderWrapper(S.GetParent(), FreqHZoomSliderID, + 0, 0, HZOOM_COUNT, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL); S.Prop(1); S @@ -751,6 +758,8 @@ void FrequencyPlotDialog::DrawPlot() hRuler->ruler.SetLog(false); hRuler->ruler.SetRange(0, 1); + hNumberScale = NumberScale(); + hRuler->ruler.SetNumberScale(hNumberScale); DrawBackground(memDC); @@ -821,37 +830,76 @@ void FrequencyPlotDialog::DrawPlot() // Must be done after setting the vertical ruler above since the // the width could change. wxRect r = mPlotRect; + int width = r.width - 2; + + // Compute x axis ruler range, given current zoom level + // (range is between 0 and 1 inclusive.) + float xViewportRelMin, xViewportRelMax; + { + constexpr int H_SCROLLBAR_RANGE = 10000; + // How much of the entire graph to show horizontally. + + float showAmount = pow(0.5f, float(hZoomSlider->GetValue()) / float(HZOOM_STEPS_PER_DOUBLE)); + + int oldThumbSize = hPanScroller->GetThumbSize(); + int newThumbSize = showAmount * H_SCROLLBAR_RANGE; + + int oldThumbPosition = hPanScroller->GetThumbPosition(); + int newThumbPosition = oldThumbPosition - (newThumbSize - oldThumbSize) / 2; + + // Set scrollbar size and position + hPanScroller->SetScrollbar(newThumbPosition, newThumbSize, H_SCROLLBAR_RANGE, newThumbSize); + newThumbPosition = hPanScroller->GetThumbPosition(); + + xViewportRelMin = float(newThumbPosition) / float(H_SCROLLBAR_RANGE); + xViewportRelMax = float(newThumbPosition + newThumbSize) / float(H_SCROLLBAR_RANGE); + } // Set up x axis ruler - int width = r.width - 2; + // TODO store NumberScaleType as a member variable + NumberScaleType nst; - float xMin, xMax, xRatio, xStep; + // Frequencies/periods passed into SpectrumAnalyst::GetProcessedValue(). + // The entire range of computed values. + float fullXMin, fullXMax; 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); - } + nst = mLogAxis ? nstLogarithmic : nstLinear; + fullXMin = mRate / mWindowSize; + fullXMax = mRate / 2; + // TODO how does track spectrogram create mel/bark/erb/period rulers? + hRuler->ruler.SetLog(mLogAxis); hRuler->ruler.SetUnits(XO("Hz")); } else { - xMin = 0; - xMax = mAnalyst->GetProcessedSize() / mRate; - xStep = (xMax - xMin) / width; + nst = nstLinear; + fullXMin = 0; + fullXMax = mAnalyst->GetProcessedSize() / mRate; hRuler->ruler.SetLog(false); /* i18n-hint: short form of 'seconds'.*/ hRuler->ruler.SetUnits(XO("s")); } - hRuler->ruler.SetRange(xMin, xMax-xStep); + + // Frequencies/periods passed into SpectrumAnalyst::GetProcessedValue(). + // The currently visible region given our current zoom and scroll settings. + float viewportXMin, viewportXMax; + { + // The entire region, ignoring the zoom viewport. + auto fullScale = NumberScale(nst, fullXMin, fullXMax); + + // Compute the viewport size, in frequencies passed into GetProcessedValue(). + viewportXMin = fullScale.PositionToValue(xViewportRelMin); + viewportXMax = fullScale.PositionToValue(xViewportRelMax); + } + + hRuler->ruler.SetRange(viewportXMin, viewportXMax); + + // hNumberScale.PositionToValue() is used to map normalized x-coordinates [0, 1) + // to frequencies/periods (within the viewport's limits) + // passed into SpectrumAnalyst::GetProcessedValue(). + hNumberScale = NumberScale(nst, viewportXMin, viewportXMax); + hRuler->ruler.SetNumberScale(hNumberScale); // this allows it to eventually draw nonlinear scales. + hRuler->Refresh(false); // Draw the plot @@ -860,16 +908,14 @@ void FrequencyPlotDialog::DrawPlot() else memDC.SetPen(wxPen(theTheme.Colour( clrWavelengthPlot), 1, wxPENSTYLE_SOLID)); - float xPos = xMin; + float xPos; + float xPosNext = hNumberScale.PositionToValue(0.f); for (int i = 0; i < width; i++) { - float y; - - if (mLogAxis) - y = mAnalyst->GetProcessedValue(xPos, xPos * xStep); - else - y = mAnalyst->GetProcessedValue(xPos, xPos + xStep); + xPos = xPosNext; + xPosNext = hNumberScale.PositionToValue(float(i + 1) / float(width)); + float y = mAnalyst->GetProcessedValue(xPos, xPosNext); float ynorm = (y - yMin) / yTotal; int lineheight = (int)(ynorm * (r.height - 1)); @@ -880,11 +926,6 @@ void FrequencyPlotDialog::DrawPlot() 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 @@ -981,70 +1022,48 @@ void FrequencyPlotDialog::PlotPaint(wxPaintEvent & event) 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)); + auto calcXPosFromMouseX = [&r, &width, &hNumberScale = this->hNumberScale]( + int mouseX + ) -> float { + float relativeMouseX = float(mouseX - (r.x + 1)) / float(width); + return hNumberScale.PositionToValue(relativeMouseX); + }; - float bestValue = 0; - float bestpeak = mAnalyst->FindPeak(xPos, &bestValue); + /// Frequency of the mouse pixel + float xPos = calcXPosFromMouseX(mMouseX); + /// Frequency at 1 pixel to the right + float xPosNext = calcXPosFromMouseX(mMouseX + 1); - int px; - if (mLogAxis) - px = (int)(log(bestpeak / xMin) / log(xStep)); - else - px = (int)((bestpeak - xMin) * width / (xMax - xMin)); + float peakAmplitude = 0; + float peakPos = mAnalyst->FindPeak(xPos, &peakAmplitude); + + float relativePeakX = hNumberScale.ValueToPosition(peakPos); + int peakX = int(float(width) * relativePeakX); 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); + AColor::Line(dc, r.x + 1 + peakX, r.y, r.x + 1 + peakX, 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); - } + float value = mAnalyst->GetProcessedValue(xPos, xPosNext); TranslatableString cursor; TranslatableString peak; if (mAlg == SpectrumAnalyst::Spectrum) { auto xp = PitchName_Absolute(FreqToMIDInote(xPos)); - auto pp = PitchName_Absolute(FreqToMIDInote(bestpeak)); + auto pp = PitchName_Absolute(FreqToMIDInote(peakPos)); /* 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) { + .Format( (int)(peakPos + 0.5), pp, peakAmplitude ); + } else if (xPos > 0.0 && peakPos > 0.0) { auto xp = PitchName_Absolute(FreqToMIDInote(1.0 / xPos)); - auto pp = PitchName_Absolute(FreqToMIDInote(1.0 / bestpeak)); + auto pp = PitchName_Absolute(FreqToMIDInote(1.0 / peakPos)); /* 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") @@ -1052,7 +1071,7 @@ void FrequencyPlotDialog::PlotPaint(wxPaintEvent & event) /* 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 ); + .Format( peakPos, (int)(1.0 / peakPos + 0.5), pp, peakAmplitude ); } mCursorText->SetValue( cursor.Translation() ); mPeakText->SetValue( peak.Translation() ); diff --git a/src/FreqWindow.h b/src/FreqWindow.h index 23ce7d350..368ad2f3e 100644 --- a/src/FreqWindow.h +++ b/src/FreqWindow.h @@ -18,6 +18,7 @@ #include "SampleFormat.h" #include "SpectrumAnalyst.h" #include "widgets/wxPanelWrapper.h" // to inherit +#include "NumberScale.h" // member variable class wxMemoryDC; class wxScrollBar; @@ -150,6 +151,8 @@ private: /// The maximum y value to plot. float mYMax; + NumberScale hNumberScale; + std::unique_ptr mBitmap; int mMouseX;