mirror of
https://codeberg.org/armin/trommelkiste.git
synced 2026-09-01 04:10:46 +02:00
Use a latch pattern for trackActive: the audio thread sets it true whenever a voice is active, and the UI timer atomically reads and clears it via exchange(false). This eliminates the race where the flag was cleared at the start of every processBlock, causing the 30Hz timer to miss brief activations from short samples.
806 lines
28 KiB
C++
806 lines
28 KiB
C++
#include "PluginProcessor.h"
|
|
#include "PluginEditor.h"
|
|
#include <BinaryData.h>
|
|
#include <SampleLibrary.h>
|
|
|
|
using namespace juce;
|
|
|
|
TrommelkisteProcessor::TrommelkisteProcessor()
|
|
: AudioProcessor(BusesProperties()
|
|
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
|
apvts(*this, nullptr, "Parameters", createParameterLayout())
|
|
{
|
|
formatManager.registerBasicFormats();
|
|
scanSamples();
|
|
loadKit(0);
|
|
}
|
|
|
|
TrommelkisteProcessor::~TrommelkisteProcessor() {}
|
|
|
|
std::optional<juce::String> TrommelkisteProcessor::getNameForMidiNoteNumber(int note, int)
|
|
{
|
|
static const char* names[] = {"BD", "RS", "SN", "CL", "CH", "PH", "OH", "RD", "LT", "MT"};
|
|
int t = note - BASE_NOTE;
|
|
if (t >= 0 && t < NUM_TRACKS)
|
|
return juce::String(names[t]);
|
|
return std::nullopt;
|
|
}
|
|
|
|
void TrommelkisteProcessor::prepareToPlay(double sr, int)
|
|
{
|
|
currentSampleRate = sr;
|
|
|
|
juce::dsp::ProcessSpec spec;
|
|
spec.sampleRate = sr;
|
|
spec.maximumBlockSize = 512;
|
|
spec.numChannels = 2;
|
|
reverb.prepare(spec);
|
|
reverbParams.roomSize = 0.5f;
|
|
reverbParams.damping = 0.5f;
|
|
reverbParams.wetLevel = 0.33f;
|
|
reverbParams.dryLevel = 0.8f;
|
|
reverbParams.width = 1.0f;
|
|
reverbParams.freezeMode = 0.0f;
|
|
reverb.setParameters(reverbParams);
|
|
|
|
const int delaySamples = DELAY_BUFFER_SECONDS * (int)sr;
|
|
delayBuffer.setSize(2, delaySamples);
|
|
delayBuffer.clear();
|
|
delayWritePos = 0;
|
|
}
|
|
|
|
void TrommelkisteProcessor::releaseResources() {}
|
|
|
|
void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
|
|
{
|
|
juce::ScopedNoDenormals nd;
|
|
const int numSamples = buffer.getNumSamples();
|
|
buffer.clear();
|
|
|
|
// Shuffle: add ghost notes (bar-quantized)
|
|
const float globalShuffleAmt = apvts.getRawParameterValue("global_shuffleAmount")->load();
|
|
const float globalGroove = apvts.getRawParameterValue("global_groove")->load();
|
|
const int shuffleDivIdx = (int)apvts.getRawParameterValue("global_shuffleDiv")->load();
|
|
// 0=OFF, 1=every 4 bars, 2=every 8, 3=every 16, 4=every 32
|
|
static constexpr int shuffleDivBars[] = { 0, 4, 8, 16, 32 };
|
|
const int divBars = shuffleDivBars[juce::jlimit(0, 4, shuffleDivIdx)];
|
|
|
|
// Try to get bar position from play head
|
|
bool hasTransport = false;
|
|
int64_t currentBar = 0;
|
|
if (auto* ph = getPlayHead())
|
|
{
|
|
auto info = ph->getPosition();
|
|
if (info.hasValue() && info->getPpqPosition().hasValue())
|
|
{
|
|
hasTransport = true;
|
|
double ppq = *info->getPpqPosition();
|
|
double beatsPerBar = 4.0;
|
|
if (info->getTimeSignature().hasValue())
|
|
beatsPerBar = (double)(*info->getTimeSignature()).numerator;
|
|
currentBar = (int64_t)(ppq / beatsPerBar);
|
|
}
|
|
}
|
|
|
|
const bool barAllowed = (divBars == 0) || !hasTransport || (currentBar % divBars == 0);
|
|
|
|
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
|
|
for (const auto metadata : midi)
|
|
{
|
|
auto msg = metadata.getMessage();
|
|
if (msg.isNoteOn())
|
|
{
|
|
int t = msg.getNoteNumber() - BASE_NOTE;
|
|
if (t >= 0 && t < NUM_TRACKS)
|
|
{
|
|
handleNoteOn(t, msg.getFloatVelocity());
|
|
|
|
const float shuffleAmt = apvts.getRawParameterValue(
|
|
"t" + juce::String(t) + "_shuffle")->load();
|
|
const float effShuffle = shuffleAmt * globalShuffleAmt;
|
|
|
|
if (effShuffle > 0.0f && barAllowed)
|
|
{
|
|
// Random note repeat on same drum
|
|
if (dist(rng) < effShuffle * 0.5f)
|
|
handleNoteOn(t, msg.getFloatVelocity() * effShuffle * 0.7f);
|
|
|
|
// Ghost note on random other drum
|
|
if (dist(rng) < effShuffle * 0.9f)
|
|
{
|
|
int ghostTrack = t;
|
|
while (ghostTrack == t)
|
|
ghostTrack = std::uniform_int_distribution<int>(0, NUM_TRACKS - 1)(rng);
|
|
handleNoteOn(ghostTrack, msg.getFloatVelocity() * effShuffle);
|
|
}
|
|
}
|
|
|
|
if (shuffleAmt > 0.0f && globalGroove > 0.0f)
|
|
handleNoteOn(t, msg.getFloatVelocity() * (1.0f + globalGroove * 0.3f * shuffleAmt));
|
|
}
|
|
}
|
|
else if (msg.isNoteOff())
|
|
{
|
|
int t = msg.getNoteNumber() - BASE_NOTE;
|
|
if (t >= 0 && t < NUM_TRACKS)
|
|
handleNoteOff(t);
|
|
}
|
|
}
|
|
|
|
auto* outL = buffer.getWritePointer(0);
|
|
auto* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : nullptr;
|
|
|
|
// Temporary send buffers
|
|
juce::AudioBuffer<float> delaySend(2, numSamples);
|
|
juce::AudioBuffer<float> reverbSend(2, numSamples);
|
|
delaySend.clear();
|
|
reverbSend.clear();
|
|
|
|
for (auto& v : voices)
|
|
{
|
|
if (!v.active)
|
|
continue;
|
|
|
|
const int ti = v.trackIndex;
|
|
trackActive[ti] = true;
|
|
auto& track = tracks[ti];
|
|
const juce::SpinLock::ScopedLockType sl(track.lock);
|
|
|
|
const int bufLen = track.buffer.getNumSamples();
|
|
if (bufLen == 0)
|
|
{
|
|
v.active = false;
|
|
continue;
|
|
}
|
|
|
|
const juce::String p = "t" + juce::String(ti) + "_";
|
|
const float level = apvts.getRawParameterValue(p + "level")->load();
|
|
const float length = apvts.getRawParameterValue(p + "length")->load();
|
|
const float velAmt = apvts.getRawParameterValue(p + "velocity")->load();
|
|
const float pitchSemitones = apvts.getRawParameterValue(p + "pitch")->load();
|
|
const float toneHz = apvts.getRawParameterValue(p + "tone")->load();
|
|
const float pan = apvts.getRawParameterValue(p + "pan")->load();
|
|
const bool decayGate = apvts.getRawParameterValue(p + "decay")->load() > 0.5f;
|
|
const float delaySendAmt = apvts.getRawParameterValue(p + "delaySend")->load();
|
|
const float reverbSendAmt = apvts.getRawParameterValue(p + "reverbSend")->load();
|
|
const float ringModAmt = apvts.getRawParameterValue(p + "ringMod")->load();
|
|
const float destructionAmt = apvts.getRawParameterValue(p + "destruction")->load();
|
|
|
|
float pitchRatio = std::pow(2.0f, pitchSemitones / 12.0f)
|
|
* (float)track.fileSampleRate / (float)currentSampleRate;
|
|
|
|
const float maxSamples = length * (float)currentSampleRate;
|
|
const float toneA = (toneHz < 19000.0f)
|
|
? std::exp(-2.0f * juce::MathConstants<float>::pi * toneHz / (float)currentSampleRate)
|
|
: 0.0f;
|
|
|
|
const float effVel = 1.0f - velAmt + velAmt * v.noteVelocity;
|
|
const float gain = level * effVel;
|
|
|
|
const float panAngle = (pan + 1.0f) * 0.25f * juce::MathConstants<float>::pi;
|
|
const float panL = std::cos(panAngle) * juce::Decibels::decibelsToGain(-3.0f);
|
|
const float panR = std::sin(panAngle) * juce::Decibels::decibelsToGain(-3.0f);
|
|
|
|
const float* sData = track.buffer.getReadPointer(0);
|
|
const bool hasR = track.buffer.getNumChannels() > 1;
|
|
const float* sDataR = hasR ? track.buffer.getReadPointer(1) : nullptr;
|
|
|
|
const float globalRingFreq = apvts.getRawParameterValue("global_ringFreq")->load();
|
|
const float globalDestr = apvts.getRawParameterValue("global_destruction")->load();
|
|
const float ringModInc = 2.0f * juce::MathConstants<float>::pi * globalRingFreq / (float)currentSampleRate;
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
{
|
|
if (!v.active)
|
|
break;
|
|
|
|
float env = 1.0f;
|
|
if (!decayGate)
|
|
{
|
|
env = 1.0f - (float)v.samplesPlayed / maxSamples;
|
|
if (env <= 0.0f) { v.active = false; break; }
|
|
}
|
|
else
|
|
{
|
|
if (v.samplesPlayed >= (int)maxSamples) { v.active = false; break; }
|
|
}
|
|
|
|
int pos0 = (int)v.position;
|
|
if (pos0 >= bufLen) { v.active = false; break; }
|
|
const float frac = v.position - (float)pos0;
|
|
int pos1 = pos0 + 1;
|
|
if (pos1 >= bufLen) pos1 = bufLen - 1;
|
|
|
|
float smplL = sData[pos0] + (sData[pos1] - sData[pos0]) * frac;
|
|
float smplR = hasR ? (sDataR[pos0] + (sDataR[pos1] - sDataR[pos0]) * frac) : smplL;
|
|
|
|
if (toneA > 0.0f)
|
|
{
|
|
v.filterStateL = (1.0f - toneA) * smplL + toneA * v.filterStateL;
|
|
smplL = v.filterStateL;
|
|
v.filterStateR = (1.0f - toneA) * smplR + toneA * v.filterStateR;
|
|
smplR = v.filterStateR;
|
|
}
|
|
|
|
// Ring modulation
|
|
if (ringModAmt > 0.0f)
|
|
{
|
|
float mod = std::sin(v.ringModPhase);
|
|
smplL = smplL * (1.0f - ringModAmt + ringModAmt * mod);
|
|
smplR = smplR * (1.0f - ringModAmt + ringModAmt * mod);
|
|
v.ringModPhase += ringModInc;
|
|
if (v.ringModPhase >= 2.0f * juce::MathConstants<float>::pi)
|
|
v.ringModPhase -= 2.0f * juce::MathConstants<float>::pi;
|
|
}
|
|
|
|
// Destruction (soft clip + bitcrush)
|
|
const float effDestr = juce::jmin(1.0f, destructionAmt + globalDestr);
|
|
if (effDestr > 0.0f)
|
|
{
|
|
float drive = 1.0f + effDestr * 19.0f;
|
|
smplL = std::tanh(smplL * drive);
|
|
smplR = std::tanh(smplR * drive);
|
|
float bits = juce::jmax(2.0f, 16.0f - effDestr * 14.0f);
|
|
float levels = std::pow(2.0f, bits);
|
|
smplL = std::round(smplL * levels) / levels;
|
|
smplR = std::round(smplR * levels) / levels;
|
|
}
|
|
|
|
smplL *= env * gain;
|
|
smplR *= env * gain;
|
|
|
|
outL[s] += smplL * panL;
|
|
if (outR)
|
|
outR[s] += smplR * panR;
|
|
|
|
// Send to effects
|
|
delaySend.addSample(0, s, smplL * delaySendAmt);
|
|
delaySend.addSample(1, s, smplR * delaySendAmt);
|
|
reverbSend.addSample(0, s, smplL * reverbSendAmt);
|
|
reverbSend.addSample(1, s, smplR * reverbSendAmt);
|
|
|
|
v.position += pitchRatio;
|
|
++v.samplesPlayed;
|
|
|
|
if (v.position >= (float)bufLen)
|
|
v.active = false;
|
|
}
|
|
}
|
|
|
|
// Read master effect levels
|
|
const float masterDelayMix = apvts.getRawParameterValue("master_delay")->load();
|
|
const float masterReverbMix = apvts.getRawParameterValue("master_reverb")->load();
|
|
|
|
// Delay sync
|
|
const int delaySyncDiv = (int)apvts.getRawParameterValue("delay_syncDiv")->load();
|
|
float delayTimeSec = 0.45f; // default free-running delay time
|
|
if (delaySyncDiv >= 1 && delaySyncDiv <= 8)
|
|
{
|
|
double bpm = 120.0;
|
|
if (auto* ph = getPlayHead())
|
|
{
|
|
auto info = ph->getPosition();
|
|
if (info.hasValue() && info->getBpm().hasValue())
|
|
bpm = *info->getBpm();
|
|
}
|
|
double beats = delaySyncDiv * 0.25;
|
|
delayTimeSec = (float)(beats * 60.0 / bpm);
|
|
}
|
|
|
|
// Process delay
|
|
const int delaySamples = delayBuffer.getNumSamples();
|
|
auto* delL = delayBuffer.getWritePointer(0);
|
|
auto* delR = delayBuffer.getNumChannels() > 1
|
|
? delayBuffer.getWritePointer(1) : nullptr;
|
|
const float feedback = 0.4f;
|
|
const float delayMix = 0.3f;
|
|
const int delayTimeSamples = (int)(delayTimeSec * currentSampleRate);
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
{
|
|
const int readPos = (delayWritePos - delayTimeSamples + delaySamples)
|
|
% delaySamples;
|
|
const float delOutL = delL[readPos];
|
|
const float delOutR = delR ? delR[readPos] : delOutL;
|
|
|
|
delL[delayWritePos] = delaySend.getSample(0, s) + delOutL * feedback;
|
|
if (delR)
|
|
delR[delayWritePos] = delaySend.getSample(1, s) + delOutR * feedback;
|
|
|
|
outL[s] += delOutL * delayMix * masterDelayMix;
|
|
if (outR)
|
|
outR[s] += delOutR * delayMix * masterDelayMix;
|
|
|
|
delayWritePos = (delayWritePos + 1) % delaySamples;
|
|
}
|
|
|
|
// Process reverb
|
|
juce::dsp::AudioBlock<float> reverbBlock(reverbSend);
|
|
juce::dsp::ProcessContextReplacing<float> reverbCtx(reverbBlock);
|
|
reverb.process(reverbCtx);
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
{
|
|
outL[s] += reverbSend.getSample(0, s) * masterReverbMix;
|
|
if (outR)
|
|
outR[s] += reverbSend.getSample(1, s) * masterReverbMix;
|
|
}
|
|
|
|
// Master volume & pan
|
|
const float masterVol = apvts.getRawParameterValue("master_volume")->load();
|
|
const float masterPan = apvts.getRawParameterValue("master_pan")->load();
|
|
const float mPanAngle = (masterPan + 1.0f) * 0.25f * MathConstants<float>::pi;
|
|
const float mPanL = std::cos(mPanAngle) * Decibels::decibelsToGain(-3.0f);
|
|
const float mPanR = std::sin(mPanAngle) * Decibels::decibelsToGain(-3.0f);
|
|
|
|
for (int s = 0; s < numSamples; ++s)
|
|
{
|
|
float tmpL = outL[s] * mPanL * masterVol;
|
|
float tmpR = outR ? outR[s] * mPanR * masterVol : tmpL;
|
|
outL[s] = tmpL;
|
|
if (outR)
|
|
outR[s] = tmpR;
|
|
}
|
|
}
|
|
|
|
int TrommelkisteProcessor::findFreeVoice(int trackIndex)
|
|
{
|
|
for (int i = 0; i < MAX_VOICES; ++i)
|
|
if (!voices[i].active)
|
|
return i;
|
|
|
|
int oldest = 0, bestAge = -1;
|
|
for (int i = 0; i < MAX_VOICES; ++i)
|
|
{
|
|
if (voices[i].trackIndex == trackIndex && voices[i].samplesPlayed > bestAge)
|
|
{
|
|
bestAge = voices[i].samplesPlayed;
|
|
oldest = i;
|
|
}
|
|
}
|
|
if (bestAge >= 0)
|
|
return oldest;
|
|
|
|
oldest = 0;
|
|
bestAge = -1;
|
|
for (int i = 0; i < MAX_VOICES; ++i)
|
|
{
|
|
if (voices[i].samplesPlayed > bestAge)
|
|
{
|
|
bestAge = voices[i].samplesPlayed;
|
|
oldest = i;
|
|
}
|
|
}
|
|
return oldest;
|
|
}
|
|
|
|
void TrommelkisteProcessor::handleNoteOn(int trackIndex, float velocity)
|
|
{
|
|
int idx = findFreeVoice(trackIndex);
|
|
auto& v = voices[idx];
|
|
v.active = true;
|
|
v.trackIndex = trackIndex;
|
|
v.position = 0.0f;
|
|
v.samplesPlayed = 0;
|
|
v.filterStateL = 0.0f;
|
|
v.filterStateR = 0.0f;
|
|
v.noteVelocity = velocity;
|
|
}
|
|
|
|
void TrommelkisteProcessor::handleNoteOff(int trackIndex)
|
|
{
|
|
for (auto& v : voices)
|
|
{
|
|
if (v.active && v.trackIndex == trackIndex)
|
|
{
|
|
const bool decayGate = apvts.getRawParameterValue(
|
|
"t" + juce::String(trackIndex) + "_decay")->load() > 0.5f;
|
|
if (decayGate)
|
|
v.active = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TrommelkisteProcessor::loadSample(int trackIndex, const juce::File& file)
|
|
{
|
|
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
|
return;
|
|
|
|
std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
|
|
if (reader == nullptr)
|
|
return;
|
|
|
|
juce::AudioBuffer<float> newBuf(reader->numChannels, (int)reader->lengthInSamples);
|
|
reader->read(&newBuf, 0, (int)reader->lengthInSamples, 0, true, true);
|
|
|
|
{
|
|
const juce::SpinLock::ScopedLockType sl(tracks[trackIndex].lock);
|
|
tracks[trackIndex].buffer = std::move(newBuf);
|
|
tracks[trackIndex].fileSampleRate = (int)reader->sampleRate;
|
|
tracks[trackIndex].filePath = file.getFullPathName();
|
|
}
|
|
}
|
|
|
|
void TrommelkisteProcessor::clearSample(int trackIndex)
|
|
{
|
|
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
|
return;
|
|
const juce::SpinLock::ScopedLockType sl(tracks[trackIndex].lock);
|
|
tracks[trackIndex].buffer.setSize(1, 0);
|
|
tracks[trackIndex].filePath = "";
|
|
}
|
|
|
|
static void readAudioFromBuffer(juce::AudioFormatManager& fmtMgr,
|
|
juce::AudioBuffer<float>& dest,
|
|
int& destSampleRate,
|
|
const char* data, int size)
|
|
{
|
|
auto memStream = std::make_unique<juce::MemoryInputStream>(data, size, false);
|
|
std::unique_ptr<juce::AudioFormatReader> reader(fmtMgr.createReaderFor(std::move(memStream)));
|
|
if (reader == nullptr)
|
|
return;
|
|
dest.setSize((int)reader->numChannels, (int)reader->lengthInSamples);
|
|
reader->read(&dest, 0, (int)reader->lengthInSamples, 0, true, true);
|
|
destSampleRate = (int)reader->sampleRate;
|
|
}
|
|
|
|
// ── Sample library ─────────────────────────────────────────────
|
|
|
|
static bool matchesPrefix(const juce::String& name, const juce::String& prefix)
|
|
{
|
|
return name.toUpperCase().startsWith(prefix.toUpperCase());
|
|
}
|
|
|
|
void TrommelkisteProcessor::scanSamples()
|
|
{
|
|
static const char* prefixes[][8] = {
|
|
{ "BT", nullptr }, // 0 BD
|
|
{ "RIM", nullptr }, // 1 RS
|
|
{ "ST", "STAT", nullptr }, // 2 SN
|
|
{ "HANDCLP", nullptr }, // 3 CL
|
|
{ "HHCD", nullptr }, // 4 CH
|
|
{ "CLOP", nullptr }, // 5 PH
|
|
{ "HHOD", nullptr }, // 6 OH
|
|
{ "RIDE", nullptr }, // 7 RD
|
|
{ "LT", nullptr }, // 8 LT
|
|
{ "MT", nullptr }, // 9 MT
|
|
};
|
|
|
|
for (int t = 0; t < NUM_TRACKS; ++t)
|
|
{
|
|
trackSamples[t].clear();
|
|
currentSampleIndex[t] = -1;
|
|
}
|
|
|
|
for (int i = 0; i < numEmbeddedSamples; ++i)
|
|
{
|
|
const auto& es = embeddedSamples[i];
|
|
juce::String name(es.name);
|
|
|
|
for (int t = 0; t < NUM_TRACKS; ++t)
|
|
{
|
|
for (int p = 0; prefixes[t][p] != nullptr; ++p)
|
|
{
|
|
if (matchesPrefix(name, prefixes[t][p]))
|
|
{
|
|
SampleRef ref;
|
|
ref.name = name;
|
|
ref.data = es.data;
|
|
ref.dataSize = es.size;
|
|
trackSamples[t].add(ref);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SampleRefSorter
|
|
{
|
|
int compareElements(const SampleRef& a, const SampleRef& b) const { return a.name.compare(b.name); }
|
|
};
|
|
SampleRefSorter sorter;
|
|
for (int t = 0; t < NUM_TRACKS; ++t)
|
|
trackSamples[t].sort(sorter);
|
|
}
|
|
|
|
void TrommelkisteProcessor::loadSampleByIndex(int trackIndex, int sampleIndex)
|
|
{
|
|
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
|
return;
|
|
if (sampleIndex < 0 || sampleIndex >= trackSamples[trackIndex].size())
|
|
return;
|
|
|
|
currentSampleIndex[trackIndex] = sampleIndex;
|
|
const auto& ref = trackSamples[trackIndex][sampleIndex];
|
|
|
|
if (ref.data != nullptr && ref.dataSize > 0)
|
|
{
|
|
const juce::SpinLock::ScopedLockType sl(tracks[trackIndex].lock);
|
|
readAudioFromBuffer(formatManager, tracks[trackIndex].buffer,
|
|
tracks[trackIndex].fileSampleRate,
|
|
ref.data, ref.dataSize);
|
|
tracks[trackIndex].filePath = "";
|
|
}
|
|
else if (ref.file.existsAsFile())
|
|
{
|
|
loadSample(trackIndex, ref.file);
|
|
}
|
|
}
|
|
|
|
void TrommelkisteProcessor::nextSample(int trackIndex)
|
|
{
|
|
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
|
return;
|
|
const int n = trackSamples[trackIndex].size();
|
|
if (n == 0) return;
|
|
|
|
int idx = currentSampleIndex[trackIndex] + 1;
|
|
if (idx >= n) idx = 0;
|
|
loadSampleByIndex(trackIndex, idx);
|
|
}
|
|
|
|
void TrommelkisteProcessor::prevSample(int trackIndex)
|
|
{
|
|
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
|
return;
|
|
const int n = trackSamples[trackIndex].size();
|
|
if (n == 0) return;
|
|
|
|
int idx = currentSampleIndex[trackIndex] - 1;
|
|
if (idx < 0) idx = n - 1;
|
|
loadSampleByIndex(trackIndex, idx);
|
|
}
|
|
|
|
// ── Kit / Preset system ───────────────────────────────────────
|
|
|
|
static const TrommelkisteProcessor::Kit kits[] = {
|
|
{ "Classic", {
|
|
"BT0AADA", "RIM63", "ST0T0SA", "HANDCLP2", "HHCD6",
|
|
"CLOP2", "HHOD6", "RIDED6", "LT0DA", "MT0DA" } },
|
|
{ "Punchy", {
|
|
"BT7A0D3", "RIM127", "ST7T7S3", "HANDCLP1", "HHCD2",
|
|
"CLOP1", "HHOD2", "RIDED2", "LT7D3", "MT7D3" } },
|
|
{ "Soft", {
|
|
"BT0A0D7", "RIM63", "ST0T0S7", "HANDCLP2", "HHCD8",
|
|
"CLOP4", "HHOD8", "RIDED8", "LT0D7", "MT0D7" } },
|
|
{ "Raw", {
|
|
"BTAAADA", "RIM127", "STATASA", "HANDCLP1", "HHCDA",
|
|
"CLOP3", "HHODA", "RIDEDA", "LTADA", "MTADA" } },
|
|
};
|
|
|
|
const TrommelkisteProcessor::Kit* TrommelkisteProcessor::getKitList() { return kits; }
|
|
int TrommelkisteProcessor::getNumKits() const { return 4; }
|
|
juce::String TrommelkisteProcessor::getCurrentKitName() const
|
|
{
|
|
if (currentKitIndex >= 0 && currentKitIndex < getNumKits())
|
|
return kits[currentKitIndex].name;
|
|
return {};
|
|
}
|
|
|
|
void TrommelkisteProcessor::loadKit(int index)
|
|
{
|
|
if (index < 0 || index >= getNumKits())
|
|
return;
|
|
|
|
currentKitIndex = index;
|
|
const auto& kit = kits[index];
|
|
|
|
for (int t = 0; t < NUM_TRACKS; ++t)
|
|
{
|
|
if (kit.samples[t] == nullptr || kit.samples[t][0] == '\0')
|
|
{
|
|
currentSampleIndex[t] = -1;
|
|
clearSample(t);
|
|
continue;
|
|
}
|
|
|
|
juce::String target(kit.samples[t]);
|
|
int found = -1;
|
|
for (int s = 0; s < trackSamples[t].size(); ++s)
|
|
{
|
|
if (trackSamples[t][s].name.equalsIgnoreCase(target))
|
|
{
|
|
found = s;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found >= 0)
|
|
loadSampleByIndex(t, found);
|
|
else
|
|
clearSample(t);
|
|
}
|
|
}
|
|
|
|
void TrommelkisteProcessor::nextKit()
|
|
{
|
|
int idx = currentKitIndex + 1;
|
|
if (idx >= getNumKits()) idx = 0;
|
|
loadKit(idx);
|
|
}
|
|
|
|
void TrommelkisteProcessor::prevKit()
|
|
{
|
|
int idx = currentKitIndex - 1;
|
|
if (idx < 0) idx = getNumKits() - 1;
|
|
loadKit(idx);
|
|
}
|
|
|
|
juce::AudioProcessorEditor* TrommelkisteProcessor::createEditor()
|
|
{
|
|
return new TrommelkisteEditor(*this);
|
|
}
|
|
|
|
void TrommelkisteProcessor::getStateInformation(juce::MemoryBlock& destData)
|
|
{
|
|
auto state = apvts.copyState();
|
|
|
|
state.setProperty("kitIndex", currentKitIndex, nullptr);
|
|
|
|
juce::ValueTree samples("Samples");
|
|
for (int i = 0; i < NUM_TRACKS; ++i)
|
|
{
|
|
juce::ValueTree tr("Track" + juce::String(i));
|
|
tr.setProperty("path", tracks[i].filePath, nullptr);
|
|
tr.setProperty("sampleIndex", currentSampleIndex[i], nullptr);
|
|
if (currentSampleIndex[i] >= 0 && currentSampleIndex[i] < trackSamples[i].size())
|
|
tr.setProperty("sampleName", trackSamples[i][currentSampleIndex[i]].name, nullptr);
|
|
samples.appendChild(tr, nullptr);
|
|
}
|
|
state.appendChild(samples, nullptr);
|
|
|
|
std::unique_ptr<juce::XmlElement> xml(state.createXml());
|
|
copyXmlToBinary(*xml, destData);
|
|
}
|
|
|
|
void TrommelkisteProcessor::setStateInformation(const void* data, int sizeInBytes)
|
|
{
|
|
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
|
|
if (xml == nullptr)
|
|
return;
|
|
|
|
auto state = juce::ValueTree::fromXml(*xml);
|
|
apvts.replaceState(state);
|
|
|
|
currentKitIndex = (int)state.getProperty("kitIndex", 0);
|
|
|
|
auto samples = state.getChildWithName("Samples");
|
|
if (samples.isValid())
|
|
{
|
|
for (int i = 0; i < NUM_TRACKS; ++i)
|
|
{
|
|
auto tr = samples.getChildWithName("Track" + juce::String(i));
|
|
if (!tr.isValid())
|
|
continue;
|
|
|
|
auto path = tr.getProperty("path", "").toString();
|
|
auto sampleName = tr.getProperty("sampleName", "").toString();
|
|
|
|
if (path.isNotEmpty())
|
|
{
|
|
loadSample(i, juce::File(path));
|
|
currentSampleIndex[i] = (int)tr.getProperty("sampleIndex", -1);
|
|
}
|
|
else if (sampleName.isNotEmpty())
|
|
{
|
|
int found = -1;
|
|
for (int s = 0; s < trackSamples[i].size(); ++s)
|
|
{
|
|
if (trackSamples[i][s].name.equalsIgnoreCase(sampleName))
|
|
{
|
|
found = s;
|
|
break;
|
|
}
|
|
}
|
|
if (found >= 0)
|
|
loadSampleByIndex(i, found);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::createParameterLayout()
|
|
{
|
|
AudioProcessorValueTreeState::ParameterLayout layout;
|
|
|
|
for (int i = 0; i < NUM_TRACKS; ++i)
|
|
{
|
|
const String p = "t" + String(i) + "_";
|
|
const String tn = "T" + String(i + 1) + " ";
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "level", tn + "Level",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.8f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "length", tn + "Length",
|
|
NormalisableRange<float>(0.01f, 2.0f, 0.001f, 0.45f), 0.5f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "velocity", tn + "Velocity",
|
|
NormalisableRange<float>(0.0f, 1.0f), 1.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "pitch", tn + "Pitch",
|
|
NormalisableRange<float>(-12.0f, 12.0f, 0.1f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "tone", tn + "Tone",
|
|
NormalisableRange<float>(20.0f, 20000.0f, 1.0f, 0.25f), 20000.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "pan", tn + "Pan",
|
|
NormalisableRange<float>(-1.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterBool>(
|
|
p + "decay", tn + "Decay", false));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "delaySend", tn + "Delay",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "reverbSend", tn + "Reverb",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "ringMod", tn + "RingMod",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "destruction", tn + "Destr",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
p + "shuffle", tn + "Shuffle",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
|
}
|
|
|
|
// Global controls
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"master_volume", "Master Volume",
|
|
NormalisableRange<float>(0.0f, 1.2f, 0.01f), 1.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"master_pan", "Master Pan",
|
|
NormalisableRange<float>(-1.0f, 1.0f, 0.01f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"master_delay", "Master Delay",
|
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"master_reverb", "Master Reverb",
|
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"delay_syncDiv", "Delay Sync",
|
|
NormalisableRange<float>(0.0f, 8.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"global_shuffleAmount", "Shuffle Amt",
|
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"global_groove", "Groove",
|
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"global_shuffleDiv", "Shuffle Div",
|
|
NormalisableRange<float>(0.0f, 4.0f, 1.0f), 0.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"global_ringFreq", "Ring Freq",
|
|
NormalisableRange<float>(20.0f, 2000.0f, 1.0f, 0.3f), 80.0f));
|
|
|
|
layout.add(std::make_unique<AudioParameterFloat>(
|
|
"global_destruction", "Destruction",
|
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
|
|
|
return layout;
|
|
}
|
|
|
|
AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
|
{
|
|
return new TrommelkisteProcessor();
|
|
}
|