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:
parent
dc55aeb568
commit
65b7c100d6
@ -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() );
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user