Skip to content

Commit 3887467

Browse files
tucktuckg00seclaude
andcommitted
v0.8.0: Filled waveform rendering, peak mipmaps, theme-aware slice UI
- Waveform renders as filled anti-aliased path instead of vertical lines - Pre-computed 3-level peak mipmaps (64/512/4096 spp) for fast cache rebuilds - Centre-line stroke in transition zone prevents waveform disappearing - Max zoom increased from 2048x to 16384x - Slice handles, labels, and lane numbers use theme foreground color Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dbbb82f commit 3887467

19 files changed

Lines changed: 242 additions & 73 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A nondestructive, time-stretching, and intersecting sample slicer plugin with in
2424
- **Duplicate slice** — clone a slice with all its locked parameters
2525
- **Hi-DPI scaling** — adjustable UI scale factor (0.5x to 3x)
2626
- **Full state recall** — all parameters, slices, and audio data saved/restored with the DAW session
27-
- **Theming** — dark and light themes
27+
- **Custom theming** — dark, light, and custom theming
2828

2929
## Usage
3030

src/PluginEditor.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ void IntersectEditor::timerCallback()
107107
{
108108
lastScale = scale;
109109
setTransform (juce::AffineTransform::scale (scale));
110+
IntersectLookAndFeel::setMenuScale (scale);
110111
saveUserSettings (scale, getTheme().name);
111112
}
112113

src/PluginProcessor.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,7 @@ void IntersectProcessor::handleCommand (const Command& cmd)
345345
// Build boundary list: [startS, ...positions..., endS]
346346
std::vector<int> bounds;
347347
bounds.push_back (startS);
348-
for (int p : cmd.positions)
349-
bounds.push_back (p);
348+
std::copy (cmd.positions.begin(), cmd.positions.end(), std::back_inserter (bounds));
350349
bounds.push_back (endS);
351350

352351
int firstNew = -1;
@@ -393,7 +392,7 @@ void IntersectProcessor::handleCommand (const Command& cmd)
393392
}
394393
}
395394

396-
void IntersectProcessor::processMidi (juce::MidiBuffer& midi)
395+
void IntersectProcessor::processMidi (const juce::MidiBuffer& midi)
397396
{
398397
for (const auto metadata : midi)
399398
{

src/PluginProcessor.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class IntersectProcessor : public juce::AudioProcessor
125125
private:
126126
void drainCommands();
127127
void handleCommand (const Command& cmd);
128-
void processMidi (juce::MidiBuffer& midi);
128+
void processMidi (const juce::MidiBuffer& midi);
129129
void captureSnapshot();
130130
void restoreSnapshot (const UndoManager::Snapshot& snap);
131131

src/audio/AudioAnalysis.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <vector>
44
#include <cmath>
55
#include <algorithm>
6+
#include <iterator>
67

78
namespace AudioAnalysis
89
{
@@ -101,9 +102,8 @@ inline std::vector<int> detectTransients (const juce::AudioBuffer<float>& buffer
101102
// sensitivity 1.0 -> low percentile (many detections)
102103
// sensitivity 0.0 -> high percentile (only biggest hits)
103104
std::vector<float> sorted;
104-
for (float v : odf)
105-
if (v > 0.0f)
106-
sorted.push_back (v);
105+
std::copy_if (odf.begin(), odf.end(), std::back_inserter (sorted),
106+
[] (float v) { return v > 0.0f; });
107107

108108
if (sorted.empty())
109109
return onsets;

src/audio/SampleData.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ bool SampleData::loadFromFile (const juce::File& file, double projectSampleRate)
6161
loadedFileName = file.getFileName();
6262
loadedFilePath = file.getFullPathName();
6363
loaded = true;
64+
buildMipmaps();
6465
return true;
6566
}
6667

@@ -78,3 +79,48 @@ float SampleData::getInterpolatedSample (double pos, int channel) const
7879
auto* data = buffer.getReadPointer (channel);
7980
return data[ipos] + (data[ipos + 1] - data[ipos]) * frac;
8081
}
82+
83+
void SampleData::buildMipmaps()
84+
{
85+
int numFrames = buffer.getNumSamples();
86+
if (numFrames <= 0)
87+
{
88+
for (auto& m : peakMipmaps)
89+
{
90+
m.samplesPerPeak = 0;
91+
m.maxPeaks.clear();
92+
m.minPeaks.clear();
93+
}
94+
return;
95+
}
96+
97+
const float* dataL = buffer.getReadPointer (0);
98+
const float* dataR = buffer.getNumChannels() > 1 ? buffer.getReadPointer (1) : dataL;
99+
100+
static constexpr int kBlockSizes[kNumMipmapLevels] = { 64, 512, 4096 };
101+
102+
for (int level = 0; level < kNumMipmapLevels; ++level)
103+
{
104+
auto& m = peakMipmaps[(size_t) level];
105+
m.samplesPerPeak = kBlockSizes[level];
106+
int numPeaks = (numFrames + m.samplesPerPeak - 1) / m.samplesPerPeak;
107+
m.maxPeaks.resize ((size_t) numPeaks);
108+
m.minPeaks.resize ((size_t) numPeaks);
109+
110+
for (int i = 0; i < numPeaks; ++i)
111+
{
112+
int start = i * m.samplesPerPeak;
113+
int end = std::min (start + m.samplesPerPeak, numFrames);
114+
float hi = -1.0f;
115+
float lo = 1.0f;
116+
for (int s = start; s < end; ++s)
117+
{
118+
float val = (dataL[s] + dataR[s]) * 0.5f;
119+
if (val > hi) hi = val;
120+
if (val < lo) lo = val;
121+
}
122+
m.maxPeaks[(size_t) i] = hi;
123+
m.minPeaks[(size_t) i] = lo;
124+
}
125+
}
126+
}

src/audio/SampleData.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
#pragma once
22
#include <juce_audio_basics/juce_audio_basics.h>
33
#include <juce_audio_formats/juce_audio_formats.h>
4+
#include <array>
5+
#include <vector>
46

57
class SampleData
68
{
79
public:
10+
struct PeakMipmap
11+
{
12+
int samplesPerPeak = 0;
13+
std::vector<float> maxPeaks;
14+
std::vector<float> minPeaks;
15+
};
16+
17+
static constexpr int kNumMipmapLevels = 3;
18+
819
SampleData();
920

1021
bool loadFromFile (const juce::File& file, double projectSampleRate);
@@ -15,14 +26,19 @@ class SampleData
1526
bool isLoaded() const { return loaded; }
1627

1728
const juce::AudioBuffer<float>& getBuffer() const { return buffer; }
29+
const std::array<PeakMipmap, kNumMipmapLevels>& getMipmaps() const { return peakMipmaps; }
30+
1831
const juce::String& getFileName() const { return loadedFileName; }
1932
void setFileName (const juce::String& name) { loadedFileName = name; }
2033

2134
const juce::String& getFilePath() const { return loadedFilePath; }
2235
void setFilePath (const juce::String& path) { loadedFilePath = path; }
2336

2437
private:
38+
void buildMipmaps();
39+
2540
juce::AudioBuffer<float> buffer; // always stereo
41+
std::array<PeakMipmap, kNumMipmapLevels> peakMipmaps;
2642
juce::AudioFormatManager formatManager;
2743
juce::String loadedFileName;
2844
juce::String loadedFilePath;

src/audio/VoicePool.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// Include Bungee
88
#include "bungee/Bungee.h"
99

10+
#include <algorithm>
1011
#include <cmath>
1112

1213
static constexpr int kStretchBlockSize = 128;
@@ -26,9 +27,10 @@ VoicePool::VoicePool()
2627
int VoicePool::allocate()
2728
{
2829
// First pass: find inactive voice within maxActive range
29-
for (int i = 0; i < maxActive; ++i)
30-
if (! voices[i].active)
31-
return i;
30+
auto it = std::find_if (voices.begin(), voices.begin() + maxActive,
31+
[] (const Voice& v) { return ! v.active; });
32+
if (it != voices.begin() + maxActive)
33+
return (int) std::distance (voices.begin(), it);
3234

3335
// Second pass: steal — prefer releasing voices with lowest envelope
3436
int best = 0;

src/audio/VoicePool.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ class VoicePool
3131
void processSample (const SampleData& sample, double sampleRate,
3232
float& outL, float& outR);
3333

34-
void processSampleMultiOut (const SampleData& sample, double sampleRate,
35-
float* outPtrs[], int numOuts);
34+
static void processSampleMultiOut (const SampleData& sample, double sampleRate,
35+
float* outPtrs[], int numOuts);
3636

3737
void setSampleRate (double sr) { sampleRate = sr; }
3838
double getSampleRate() const { return sampleRate; }

src/ui/AutoChopPanel.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "WaveformView.h"
44
#include "../PluginProcessor.h"
55
#include "../audio/AudioAnalysis.h"
6+
#include <algorithm>
67

78
AutoChopPanel::AutoChopPanel (IntersectProcessor& p, WaveformView& wv)
89
: processor (p), waveformView (wv)
@@ -165,9 +166,9 @@ void AutoChopPanel::updatePreview()
165166

166167
if (processor.snapToZeroCrossing.load())
167168
{
168-
for (auto& p : positions)
169-
p = AudioAnalysis::findNearestZeroCrossing (
170-
processor.sampleData.getBuffer(), p);
169+
std::transform (positions.begin(), positions.end(), positions.begin(),
170+
[this] (int p) { return AudioAnalysis::findNearestZeroCrossing (
171+
processor.sampleData.getBuffer(), p); });
171172
}
172173

173174
waveformView.transientPreviewPositions = std::move (positions);

0 commit comments

Comments
 (0)