Skip to content

Commit f7491ac

Browse files
tucktuckg00seclaude
andcommitted
Lazy chop stretch, path-based state, missing sample relink, button order fix
- Lazy chop preview now respects sample-level stretch settings (Repitch/ Signalsmith/Bungee) via PreviewStretchParams passed at start - State serialization v9: store file path instead of PCM data for much smaller preset sizes; backward-compatible with v8 PCM restore - Missing sample detection with orange "MISSING" indicator and click-to- relink via file browser in HeaderBar - ActionPanel button order changed to ADD, LAZY, AUTO, COPY, DEL - VoicePool::initStretcher/initBungee promoted to public static methods so LazyChopEngine can reuse them without duplication - CLAUDE.md removed from tracking and added to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 87ba4d2 commit f7491ac

13 files changed

Lines changed: 257 additions & 162 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ desktop.ini
2020

2121
# Claude Code
2222
.claude/
23+
CLAUDE.md
2324

2425
# Compiled objects
2526
*.o

CLAUDE.md

Lines changed: 0 additions & 96 deletions
This file was deleted.

project/src/PluginProcessor.cpp

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ void IntersectProcessor::drainCommands()
4949
handleCommand (commandBuffer[(size_t) (scope.startIndex1 + i)]);
5050
for (int i = 0; i < scope.blockSize2; ++i)
5151
handleCommand (commandBuffer[(size_t) (scope.startIndex2 + i)]);
52+
53+
if (scope.blockSize1 + scope.blockSize2 > 0)
54+
updateHostDisplay (ChangeDetails().withNonParameterStateChanged (true));
5255
}
5356

5457
void IntersectProcessor::handleCommand (const Command& cmd)
@@ -58,8 +61,9 @@ void IntersectProcessor::handleCommand (const Command& cmd)
5861
case CmdLoadFile:
5962
if (sampleData.loadFromFile (cmd.fileParam, currentSampleRate))
6063
{
61-
// Clear existing slices but don't create a default one
6264
sliceManager.clearAll();
65+
sampleMissing.store (false);
66+
missingFilePath.clear();
6367
}
6468
break;
6569

@@ -73,7 +77,21 @@ void IntersectProcessor::handleCommand (const Command& cmd)
7377

7478
case CmdLazyChopStart:
7579
if (sampleData.isLoaded())
76-
lazyChop.start (sampleData.getNumFrames(), sliceManager);
80+
{
81+
PreviewStretchParams psp;
82+
psp.stretchEnabled = stretchParam->load() > 0.5f;
83+
psp.algorithm = (int) algoParam->load();
84+
psp.bpm = bpmParam->load();
85+
psp.pitch = pitchParam->load();
86+
psp.dawBpm = dawBpm.load();
87+
psp.tonality = tonalityParam->load();
88+
psp.formant = formantParam->load();
89+
psp.formantComp = formantCompParam->load() > 0.5f;
90+
psp.grainMode = (int) grainModeParam->load();
91+
psp.sampleRate = currentSampleRate;
92+
psp.sample = &sampleData;
93+
lazyChop.start (sampleData.getNumFrames(), sliceManager, psp);
94+
}
7795
break;
7896

7997
case CmdLazyChopStop:
@@ -321,7 +339,7 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
321339
juce::MemoryOutputStream stream (destData, false);
322340

323341
// Version
324-
stream.writeInt (8);
342+
stream.writeInt (9);
325343

326344
// APVTS state
327345
auto state = apvts.copyState();
@@ -368,20 +386,8 @@ void IntersectProcessor::getStateInformation (juce::MemoryBlock& destData)
368386
stream.writeFloat (s.volume);
369387
}
370388

371-
// Audio PCM data (stereo interleaved floats)
372-
int numFrames = sampleData.getNumFrames();
373-
stream.writeInt (numFrames);
374-
if (numFrames > 0)
375-
{
376-
const auto& buf = sampleData.getBuffer();
377-
for (int f = 0; f < numFrames; ++f)
378-
{
379-
stream.writeFloat (buf.getSample (0, f));
380-
stream.writeFloat (buf.getSample (1, f));
381-
}
382-
}
383-
384-
// v8: sample filename
389+
// v9: store file path only (no PCM)
390+
stream.writeString (sampleData.getFilePath());
385391
stream.writeString (sampleData.getFileName());
386392
}
387393

@@ -460,21 +466,47 @@ void IntersectProcessor::setStateInformation (const void* data, int sizeInBytes)
460466
}
461467
}
462468

463-
// Audio PCM
464-
int numFrames = stream.readInt();
465-
if (numFrames > 0)
469+
if (version >= 9)
466470
{
467-
juce::AudioBuffer<float> restoredBuf (2, numFrames);
468-
for (int f = 0; f < numFrames; ++f)
471+
// v9: path-only restore
472+
auto filePath = stream.readString();
473+
auto fileName = stream.readString();
474+
475+
if (filePath.isNotEmpty())
469476
{
470-
restoredBuf.setSample (0, f, stream.readFloat());
471-
restoredBuf.setSample (1, f, stream.readFloat());
477+
juce::File f (filePath);
478+
double sr = currentSampleRate > 0 ? currentSampleRate : 44100.0;
479+
if (f.existsAsFile() && sampleData.loadFromFile (f, sr))
480+
{
481+
sampleMissing.store (false);
482+
}
483+
else
484+
{
485+
sampleMissing.store (true);
486+
missingFilePath = filePath;
487+
sampleData.setFileName (fileName);
488+
sampleData.setFilePath (filePath);
489+
}
472490
}
473-
sampleData.loadFromBuffer (std::move (restoredBuf));
474491
}
492+
else
493+
{
494+
// v8 and earlier: PCM restore (backward compat)
495+
int numFrames = stream.readInt();
496+
if (numFrames > 0)
497+
{
498+
juce::AudioBuffer<float> restoredBuf (2, numFrames);
499+
for (int f = 0; f < numFrames; ++f)
500+
{
501+
restoredBuf.setSample (0, f, stream.readFloat());
502+
restoredBuf.setSample (1, f, stream.readFloat());
503+
}
504+
sampleData.loadFromBuffer (std::move (restoredBuf));
505+
}
475506

476-
if (version >= 8)
477-
sampleData.setFileName (stream.readString());
507+
if (version >= 8)
508+
sampleData.setFileName (stream.readString());
509+
}
478510

479511
sliceManager.rebuildMidiMap();
480512
}

project/src/PluginProcessor.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ class IntersectProcessor : public juce::AudioProcessor
100100
// MIDI-selects-slice toggle
101101
std::atomic<bool> midiSelectsSlice { false };
102102

103+
// Missing sample state (for relink UI)
104+
std::atomic<bool> sampleMissing { false };
105+
juce::String missingFilePath;
106+
103107
private:
104108
void drainCommands();
105109
void handleCommand (const Command& cmd);

project/src/audio/LazyChopEngine.cpp

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#include "LazyChopEngine.h"
22
#include <cmath>
33

4-
void LazyChopEngine::start (int sampleLen, SliceManager& sliceMgr)
4+
void LazyChopEngine::start (int sampleLen, SliceManager& sliceMgr,
5+
const PreviewStretchParams& params)
56
{
67
active = true;
78
playing = false;
89
chopPos = 0;
910
sampleLength = sampleLen;
1011
lastNote = -1;
12+
cachedParams = params;
1113

1214
nextMidiNote = sliceMgr.rootNote.load();
1315
int num = sliceMgr.getNumSlices();
@@ -26,7 +28,6 @@ void LazyChopEngine::startPreview (VoicePool& voicePool, int fromPos)
2628
v.active = true;
2729
v.sliceIdx = -1;
2830
v.position = (double) fromPos;
29-
v.speed = 1.0;
3031
v.direction = 1;
3132
v.velocity = 1.0f;
3233
v.midiNote = -1;
@@ -36,9 +37,75 @@ void LazyChopEngine::startPreview (VoicePool& voicePool, int fromPos)
3637
v.muteGroup = 0;
3738
v.wsolaActive = false;
3839
v.stretchActive = false;
40+
v.bungeeActive = false;
3941
v.pitchRatio = 1.0f;
4042
v.age = 0;
4143
v.looping = true;
44+
v.volume = 1.0f;
45+
v.speed = 1.0;
46+
47+
// Apply stretch from cached sample-level params
48+
const auto& p = cachedParams;
49+
int algo = p.algorithm;
50+
51+
if (p.stretchEnabled && p.dawBpm > 0.0f && p.bpm > 0.0f)
52+
{
53+
float speedRatio = p.dawBpm / p.bpm;
54+
55+
if (algo == 0)
56+
{
57+
// Repitch: speed change (pitch is consequence)
58+
v.speed = speedRatio;
59+
}
60+
else if (algo == 2 && p.sample != nullptr)
61+
{
62+
// Bungee
63+
v.bungeeActive = true;
64+
v.speed = 1.0;
65+
v.bungeeSpeed = (double) speedRatio;
66+
v.bungeeSrcPos = fromPos;
67+
int hopAdj = juce::jlimit (-1, 1, p.grainMode - 1);
68+
VoicePool::initBungee (v, p.pitch, p.sampleRate, hopAdj);
69+
}
70+
else if (p.sample != nullptr)
71+
{
72+
// Signalsmith Stretch
73+
v.stretchActive = true;
74+
v.speed = 1.0;
75+
v.stretchTimeRatio = speedRatio;
76+
v.stretchPitchSemis = p.pitch;
77+
v.stretchSrcPos = fromPos;
78+
VoicePool::initStretcher (v, p.pitch, p.sampleRate,
79+
p.tonality, p.formant, p.formantComp, *p.sample);
80+
}
81+
}
82+
else if (algo == 1 && p.sample != nullptr)
83+
{
84+
// Stretch algo, no stretch enabled — pitch only via Signalsmith
85+
v.stretchActive = true;
86+
v.speed = 1.0;
87+
v.stretchTimeRatio = 1.0f;
88+
v.stretchPitchSemis = p.pitch;
89+
v.stretchSrcPos = fromPos;
90+
VoicePool::initStretcher (v, p.pitch, p.sampleRate,
91+
p.tonality, p.formant, p.formantComp, *p.sample);
92+
}
93+
else if (algo == 2 && p.sample != nullptr)
94+
{
95+
// Bungee algo, no stretch — pitch only
96+
v.bungeeActive = true;
97+
v.speed = 1.0;
98+
v.bungeeSpeed = 1.0;
99+
v.bungeeSrcPos = fromPos;
100+
int hopAdj = juce::jlimit (-1, 1, p.grainMode - 1);
101+
VoicePool::initBungee (v, p.pitch, p.sampleRate, hopAdj);
102+
}
103+
else
104+
{
105+
// Repitch: apply pitch ratio to speed
106+
v.pitchRatio = std::pow (2.0f, p.pitch / 12.0f);
107+
v.speed = v.pitchRatio;
108+
}
42109

43110
// Sustain at half volume
44111
v.envelope.noteOn (0.0f, 0.0f, 0.5f, 0.02f);
@@ -49,6 +116,10 @@ void LazyChopEngine::stop (VoicePool& voicePool, SliceManager& /*sliceMgr*/)
49116
// Stop preview voice
50117
auto& v = voicePool.getVoice (getPreviewVoiceIndex());
51118
v.active = false;
119+
v.stretchActive = false;
120+
v.bungeeActive = false;
121+
v.stretcher.reset();
122+
v.bungeeStretcher.reset();
52123

53124
active = false;
54125
playing = false;

project/src/audio/LazyChopEngine.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,29 @@
22
#include "VoicePool.h"
33
#include "SliceManager.h"
44

5+
struct PreviewStretchParams
6+
{
7+
bool stretchEnabled = false;
8+
int algorithm = 0;
9+
float bpm = 120.0f;
10+
float pitch = 0.0f;
11+
float dawBpm = 120.0f;
12+
float tonality = 0.0f;
13+
float formant = 0.0f;
14+
bool formantComp = false;
15+
int grainMode = 0;
16+
double sampleRate = 44100.0;
17+
const SampleData* sample = nullptr;
18+
};
19+
520
class LazyChopEngine
621
{
722
public:
823
bool isActive() const { return active; }
924
bool isPlaying() const { return playing; }
1025
int getChopPos() const { return chopPos; }
1126

12-
void start (int sampleLen, SliceManager& sliceMgr);
27+
void start (int sampleLen, SliceManager& sliceMgr, const PreviewStretchParams& params);
1328
void stop (VoicePool& voicePool, SliceManager& sliceMgr);
1429
void onNote (int note, VoicePool& voicePool, SliceManager& sliceMgr);
1530

@@ -24,4 +39,6 @@ class LazyChopEngine
2439
int nextMidiNote = 36;
2540
int sampleLength = 0;
2641
int lastNote = -1;
42+
43+
PreviewStretchParams cachedParams;
2744
};

0 commit comments

Comments
 (0)