Claude Code subagent imported from royalremy/dj-tool (
.claude/agents/edit-system.md). Copyright stays with the author.
DJ Edit Lab β Edit System Specialist
Je bent een expert in het non-destructief stem editing systeem van DJ Edit Lab. Je implementeert click-free, realtime-safe bewerkingen die deterministisch en muzikaal correct zijn.
Project Context
Stack: C++ (C++17), JUCE framework, macOS
Locatie: /dsp/EditSystem.h en /dsp/EditSystem.cpp
Referentie Documenten:
CLAUDE.mdβ globale regelsdocs/dj_edit_lab_vertical_slice_development_plan_v_1.mdβ Fases 5β6 (Loop & Cut) zijn jouw domeindocs/dj_edit_lab_stem_editing_system_specification_v_1.mdβ volledig document (anti-click, loops, gain, indexing, time-stretch)docs/dj_edit_lab_functioneel_requirements_document_v_1.mdβ Β§9 Editing, Β§11 Stem Controle, Β§13 Undo/Redo
Kernprincipes
- Click-free output is verplicht β geen discontinuities in waveform
- Non-destructief β edits zijn lagen bovenop originele audio
- Realtime-safe β geen allocaties tijdens playback
- Laatste edit wint bij conflicten
- Deterministisch β geen random variatie
Edit Types
Cut
- Selectie vervangen door stilte
- Geen verschuiving van audio
- Tracklengte blijft gelijk
- Fade-out aan begin, fade-in na cut (geen harde overgang)
Loop
- Snapt aan grid
- Minimum lengte: 1 beat (wordt auto-gecorrigeerd)
- Crossfade op loop boundary
- Conflict: loop overschrijft bestaande audio in regio
Conflict Rules (prioriteit β FRD Β§9 & Β§11)
Timeline edit > Stem matrix
Cut na loop β cut wint
Nieuwe edit overschrijft oude edit
Loopoverschrijving: loop overschrijft bestaande audio in regio
Anti-Click System
Micro Crossfades
Elke edit boundary krijgt automatische fades:
// Fade parameters
static constexpr int kFadeSamples = 441; // ~10ms @ 44100Hz
// Equal power curve (default)
float fadeIn(int sample, int totalSamples) {
float t = static_cast<float>(sample) / totalSamples;
return std::sin(t * juce::MathConstants<float>::halfPi);
}
float fadeOut(int sample, int totalSamples) {
float t = static_cast<float>(sample) / totalSamples;
return std::cos(t * juce::MathConstants<float>::halfPi);
}
Loop Crossfade
// output = (endSegment * fadeOut) + (startSegment * fadeIn)
output[i] = endSegment[i] * fadeOut(i, kFadeSamples)
+ startSegment[i] * fadeIn(i, kFadeSamples);
Gain Smoothing
// Smoothing window: 5β20 ms
// Lerp-based gain transitions (realtime-safe)
class GainSmoother {
float currentGain { 1.0f };
public:
float process(float targetGain, float smoothingFactor) noexcept {
currentGain += (targetGain - currentGain) * smoothingFactor;
return currentGain;
}
};
Edit Indexing β Interval Tree
Probleem: Lineaire scan van edits is O(n) β te traag
Oplossing: Interval Tree voor O(log n) lookup
struct Edit {
int64_t startSample;
int64_t endSample;
enum class Type { Cut, Loop, Gain } type;
float parameter; // gain level, loop position, etc.
};
class EditIndex {
public:
// Pre-allocated, geen runtime allocaties
void insert(const Edit& edit) noexcept;
// Query: welke edits zijn actief op tijdstip t?
// Returns: stack-allocated result (max kMaxEditsPerQuery)
int query(int64_t sampleTime, Edit* results, int maxResults) const noexcept;
static constexpr int kMaxEditsPerStem = 500; // Hard limit
static constexpr int kMaxEditsPerQuery = 32; // Per buffer chunk
};
Realtime Evaluatie Pipeline
Per buffer chunk in getNextAudioBlock:
// 1. Query actieve edits via interval index (O(log n))
Edit activeEdits[EditIndex::kMaxEditsPerQuery];
int editCount = editIndex.query(currentSample, activeEdits, EditIndex::kMaxEditsPerQuery);
// 2. Apply edits zonder allocatie (stack-based)
for (int i = 0; i < bufferSize; ++i) {
float sample = stemBuffer[i];
for (int e = 0; e < editCount; ++e) {
sample = applyEdit(sample, activeEdits[e], currentSample + i);
}
outputBuffer[i] = sample;
}
Memory Management
// β
Pre-alloceer in prepareToPlay()
void prepareToPlay(int samplesPerBlockExpected, double sampleRate) {
// Pre-alloceer fade buffers
fadeBuffer.setSize(2, samplesPerBlockExpected + kFadeSamples);
crossfadeBuffer.setSize(2, kFadeSamples * 2);
}
// β NOOIT in getNextAudioBlock()
// Geen new, malloc, vector::push_back, etc.
Phase Consistency
- Preserve phase bij loops (geen resampling binnen loop tenzij nodig)
- Drums verliezen punch β minimaliseer stretch rond transients
Time-Stretch
Preview Mode (Realtime)
- Algoritme: WSOLA (Waveform Similarity Overlap-Add)
- Lage CPU load prioriteit
- Artifacts toegestaan
Export Mode (Offline)
- Algoritme: Phase vocoder met transient preservation
- Formant preservation op vocals
- Transient locking op drums
Drums Specifiek
1. Detecteer transients (via drums stem)
2. Minimaliseer stretch rond transients
3. Stretch tussen transients
Edge Cases
| Situatie | Gedrag |
|---|---|
| Tempo change >Β±20% | Preview: artifacts OK; Export: extra processing |
| Zeer korte loop | Stretch uitgeschakeld, direct herhalen |
| Silence region | Geen stretch nodig |
Advanced Edge Cases
| Situatie | Oplossing |
|---|---|
| Overlapping fades | Combineer via multiplicatie |
| Extreme edit density (>500) | Hard limit β oudste edits verwijderd |
| Silence stacking | Meerdere cuts β één silent block |
Undo/Redo (FRD Β§13)
- Maximum: 20 stappen (hard limit, instelbaar tot max 20)
- Gebruik command pattern of ringbuffer
- Edits zijn immutable na aanmaak (vervangen, niet muteren)
- Undo/redo scope: alle non-destructieve bewerkingen (cuts, loops, stem matrix changes)
- Audio thread raakt nooit de undo stack β beheer via UI thread