From 9a10fc5b7a96c7e398308371c43d5cfb0f4abc1b Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 1 Sep 2024 08:15:38 -0400 Subject: [PATCH] Improve sample draw algorithm (#1234) * Improve sample draw algorithm 1. Better in high zoom 2. Cleaner zero crossing handling 3. Makes more sense in the code * Few more drawing tweaks --- .../mapping-pane/SampleWaveform.cpp | 119 ++++++++++-------- .../components/mapping-pane/SampleWaveform.h | 7 +- 2 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.cpp b/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.cpp index d570b5a0..fc3058d2 100644 --- a/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.cpp +++ b/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.cpp @@ -40,7 +40,7 @@ SampleWaveform::SampleWaveform(VariantDisplay *d) : display(d), HasEditor(d->edi void SampleWaveform::rebuildHotZones() { - rebuildLegacyPathForSample(); + rebuildEnvelopePaths(); static constexpr int hotZoneSize{10}; auto &v = display->variantView.variants[display->selectedVariation]; auto samp = editor->sampleManager.getSample(v.sampleID); @@ -124,7 +124,7 @@ int SampleWaveform::xPixelForSample(int64_t samplePos, bool doClamp) } } -void SampleWaveform::rebuildLegacyPathForSample() +void SampleWaveform::rebuildEnvelopePaths() { if (display->selectedVariation < 0 || display->selectedVariation > scxt::maxVariantsPerZone) return; @@ -141,12 +141,13 @@ void SampleWaveform::rebuildLegacyPathForSample() * OK so we have pctStart and zoomFactor so whats that in sample space */ auto l = samp->getSampleLength(); - auto startSample = std::clamp((int)std::floor(l * pctStart), 0, (int)l); + int samplePad{10}; // a fudge for very very high zooms stops start glitches + auto startSample = std::clamp((int)std::floor(l * pctStart) - samplePad, 0, (int)l); auto numSamples = (int)std::ceil(1.f * l / zoomFactor); - auto endSample = std::clamp(startSample + numSamples, 0, (int)l); + auto endSample = std::clamp(startSample + numSamples + 2 * samplePad, 0, (int)l); std::vector> topLine, bottomLine; - auto fac = 1.0 * numSamples / r.getWidth(); + auto fac = std::max(1.0 * numSamples / r.getWidth(), 1.0); auto downSampleForUI = [startSample, endSample, fac, &topLine, &bottomLine](auto *data) { using T = std::remove_pointer_t; @@ -172,18 +173,11 @@ void SampleWaveform::rebuildLegacyPathForSample() { if (c + fac < s) { - // nmx are in -1..1 coordinates. We want to go to 0..1 coordinates - // and opposite - mx = std::max(T(0), mx); - mn = std::min(T(0), mn); double nmx = mx * 1.0 / normFactor; double nmn = mn * 1.0 / normFactor; - double pmx = (-nmx + 1) * 0.5; - double pmn = (-nmn + 1) * 0.5; - - topLine.emplace_back(s, pmx); - bottomLine.emplace_back(s, pmn); + topLine.emplace_back(s, nmx); + bottomLine.emplace_back(s, nmn); c += fac; ct++; @@ -210,49 +204,73 @@ void SampleWaveform::rebuildLegacyPathForSample() jassertfalse; } - juce::Path res; - bool first{true}; - upperEnvelope = juce::Path(); - lowerEnvelope = juce::Path(); - upperEnvelope.startNewSubPath(0, getHeight() / 2); - lowerEnvelope.startNewSubPath(getWidth(), getHeight() / 2); - for (auto &[smp, val] : topLine) + upperFill = juce::Path(); + upperStroke = juce::Path(); + lowerFill = juce::Path(); + lowerStroke = juce::Path(); + + auto sVToPx = [this](float val) { + // val is -1..1 so move it to 0..1 inverted + auto nval = (-val + 1) * 0.5; + // then scale it by height + return nval * getHeight(); + }; + for (const auto &[smp, val] : topLine) + { + auto pos = xPixelForSample(smp); + auto uval = std::max(val, 0.0f); + + if (first) + { + upperStroke.startNewSubPath(pos, sVToPx(val)); + upperFill.startNewSubPath(pos, sVToPx(uval)); + first = false; + } + else + { + upperStroke.lineTo(pos, sVToPx(val)); + upperFill.lineTo(pos, sVToPx(uval)); + } + } + + for (const auto &[smp, val] : bottomLine) { auto pos = xPixelForSample(smp); + auto lval = std::min(val, 0.0f); if (first) { - res.startNewSubPath(pos, val * r.getHeight()); + lowerStroke.startNewSubPath(pos, sVToPx(val)); + lowerFill.startNewSubPath(pos, sVToPx(lval)); first = false; } else { - res.lineTo(pos, val * r.getHeight()); + lowerStroke.lineTo(pos, sVToPx(val)); + lowerFill.lineTo(pos, sVToPx(lval)); } - upperEnvelope.lineTo(pos, val * r.getHeight()); } std::reverse(bottomLine.begin(), bottomLine.end()); + std::reverse(topLine.begin(), topLine.end()); first = true; - for (auto &[smp, val] : bottomLine) + for (const auto &[smp, val] : bottomLine) { auto pos = xPixelForSample(smp); - lowerEnvelope.lineTo(pos, val * r.getHeight()); + auto uval = std::max(val, 0.f); + upperFill.lineTo(pos, sVToPx(uval)); } - upperEnvelope.lineTo(getWidth(), getHeight() / 2); - lowerEnvelope.lineTo(0, getHeight() / 2); - - for (auto &[smp, val] : bottomLine) + for (const auto &[smp, val] : topLine) { auto pos = xPixelForSample(smp); - - res.lineTo(pos, val * r.getHeight()); + auto uval = std::min(val, 0.f); + lowerFill.lineTo(pos, sVToPx(uval)); } - res.closeSubPath(); - legacyPath = res; + upperFill.closeSubPath(); + lowerFill.closeSubPath(); } void SampleWaveform::mouseDown(const juce::MouseEvent &e) @@ -340,13 +358,10 @@ void SampleWaveform::paint(juce::Graphics &g) return; } - auto l = samp->getSampleLength(); + g.setColour(editor->themeColor(theme::ColorMap::grid_secondary)); + g.drawHorizontalLine(getHeight() / 2, 0, getWidth()); - /*g.setColour(juce::Colours::red); - g.fillPath(upperEnvelope); - g.setColour(juce::Colours::blue); - g.fillPath(lowerEnvelope); - */ + auto l = samp->getSampleLength(); auto ssp = xPixelForSample(v.startSample); auto esp = xPixelForSample(v.endSample); @@ -373,10 +388,10 @@ void SampleWaveform::paint(juce::Graphics &g) g.reduceClipRegion(cr); g.setGradientFill(gTop); - g.fillPath(upperEnvelope); + g.fillPath(upperFill); g.setGradientFill(gBot); - g.fillPath(lowerEnvelope); + g.fillPath(lowerFill); } if (esp <= getWidth()) { @@ -385,10 +400,10 @@ void SampleWaveform::paint(juce::Graphics &g) g.reduceClipRegion(cr); g.setGradientFill(gTop); - g.fillPath(upperEnvelope); + g.fillPath(upperFill); g.setGradientFill(gBot); - g.fillPath(lowerEnvelope); + g.fillPath(lowerFill); } auto spC = std::clamp(ssp, 0, getWidth()); @@ -399,14 +414,14 @@ void SampleWaveform::paint(juce::Graphics &g) g.reduceClipRegion(cr); g.setGradientFill(sTop); - g.fillPath(upperEnvelope); + g.fillPath(upperFill); g.setGradientFill(sBot); - g.fillPath(lowerEnvelope); + g.fillPath(lowerFill); g.setColour(a1a); - g.strokePath(upperEnvelope, juce::PathStrokeType(1)); - g.strokePath(lowerEnvelope, juce::PathStrokeType(1)); + g.strokePath(upperStroke, juce::PathStrokeType(1)); + g.strokePath(lowerStroke, juce::PathStrokeType(1)); } if (v.loopActive) @@ -427,14 +442,14 @@ void SampleWaveform::paint(juce::Graphics &g) g.reduceClipRegion(dr); g.setGradientFill(lTop); - g.fillPath(upperEnvelope); + g.fillPath(upperFill); g.setGradientFill(lBot); - g.fillPath(lowerEnvelope); + g.fillPath(lowerFill); g.setColour(a1a); - g.strokePath(upperEnvelope, juce::PathStrokeType(1)); - g.strokePath(lowerEnvelope, juce::PathStrokeType(1)); + g.strokePath(upperStroke, juce::PathStrokeType(1)); + g.strokePath(lowerStroke, juce::PathStrokeType(1)); } } diff --git a/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.h b/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.h index dc17008a..05c7341b 100644 --- a/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.h +++ b/src-ui/app/edit-screen/components/mapping-pane/SampleWaveform.h @@ -52,7 +52,7 @@ struct SampleWaveform : juce::Component, HasEditor, sst::jucegui::components::Zo { if (isVisible()) { - rebuildLegacyPathForSample(); + rebuildEnvelopePaths(); repaint(); } } @@ -78,9 +78,8 @@ struct SampleWaveform : juce::Component, HasEditor, sst::jucegui::components::Zo void rebuildPaths(); - juce::Path legacyPath; - juce::Path upperEnvelope, lowerEnvelope; - void rebuildLegacyPathForSample(); + juce::Path upperStroke, lowerStroke, upperFill, lowerFill; + void rebuildEnvelopePaths(); int64_t sampleForXPixel(float xpos); int xPixelForSample(int64_t samplePos, bool doClamp = true);