1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-06-15 23:59:37 +02:00

FreqWindow: Implement horizontal zoom/scroll

This uses an exponential curve when zooming horizontally. This feels
more natural than the "reciprocal" curve used for vertical zoom. I tried
using that curve for horizontal zoom as well, but it was flawed because
it would zoom slowly at first but accelerate as you approached maximum
zoom level.

One minor issue is that dragging the zoom slider now results in zooming
in slightly to the left of the center of the plot. This is because every
change in zoom level recomputes the horizontal scrollbar's size and
position, rounding the position down to the nearest integer. The
rounding errors accumulate over many updates.

This needs to be solved by storing the intended center/boundaries of the
viewport (either its frequency or position) independently of the
scrollbar.

Signed-off-by: nyanpasu64 <nyanpasu64@tuta.io>
This commit is contained in:
nyanpasu64 2021-07-10 17:42:11 -07:00 committed by Be
parent dc55aeb568
commit 65b7c100d6
2 changed files with 98 additions and 76 deletions

View File

@ -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() );

View File

@ -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<wxBitmap> mBitmap;
int mMouseX;