Add user preset save/load to the preset menu

This commit is contained in:
Armin 2026-08-05 00:21:50 +02:00
commit 143e0bac76
4 changed files with 212 additions and 28 deletions

View file

@ -290,7 +290,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
int id = presetCombo.getSelectedId(); int id = presetCombo.getSelectedId();
if (id >= 1) if (id >= 1)
{ {
proc.loadKit(id - 1); proc.loadPreset(id - 1);
for (int i = 0; i < NUM_TRACKS; ++i) for (int i = 0; i < NUM_TRACKS; ++i)
updateFileLabel(i); updateFileLabel(i);
} }
@ -298,7 +298,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
addAndMakeVisible(presetCombo); addAndMakeVisible(presetCombo);
presetPrevBtn.onClick = [this] { presetPrevBtn.onClick = [this] {
proc.prevKit(); proc.prevPreset();
refreshPresetCombo(); refreshPresetCombo();
for (int i = 0; i < NUM_TRACKS; ++i) for (int i = 0; i < NUM_TRACKS; ++i)
updateFileLabel(i); updateFileLabel(i);
@ -306,13 +306,36 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
addAndMakeVisible(presetPrevBtn); addAndMakeVisible(presetPrevBtn);
presetNextBtn.onClick = [this] { presetNextBtn.onClick = [this] {
proc.nextKit(); proc.nextPreset();
refreshPresetCombo(); refreshPresetCombo();
for (int i = 0; i < NUM_TRACKS; ++i) for (int i = 0; i < NUM_TRACKS; ++i)
updateFileLabel(i); updateFileLabel(i);
}; };
addAndMakeVisible(presetNextBtn); addAndMakeVisible(presetNextBtn);
presetSaveBtn.onClick = [this]
{
auto* aw = new juce::AlertWindow("Save Preset", "Enter a name for this preset:",
juce::MessageBoxIconType::NoIcon);
aw->addTextEditor("name", "", "Preset name");
aw->addButton("Save", 1, juce::KeyPress(juce::KeyPress::returnKey));
aw->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey));
aw->enterModalState(true, juce::ModalCallbackFunction::create(
[this, aw](int result)
{
if (result == 1)
{
auto name = aw->getTextEditor("name")->getText().trim();
if (name.isNotEmpty())
{
proc.saveUserPreset(name);
refreshPresetCombo();
}
}
}), true);
};
addAndMakeVisible(presetSaveBtn);
scaleCombo.addItemList({"100%", "125%", "150%", "175%", "200%"}, 1); scaleCombo.addItemList({"100%", "125%", "150%", "175%", "200%"}, 1);
scaleCombo.setSelectedId(3, dontSendNotification); scaleCombo.setSelectedId(3, dontSendNotification);
scaleCombo.setColour(ComboBox::backgroundColourId, Colour(0xFF333333)); scaleCombo.setColour(ComboBox::backgroundColourId, Colour(0xFF333333));
@ -449,10 +472,10 @@ void TrommelkisteEditor::updateFileLabel(int idx)
void TrommelkisteEditor::refreshPresetCombo() void TrommelkisteEditor::refreshPresetCombo()
{ {
presetCombo.clear(dontSendNotification); presetCombo.clear(dontSendNotification);
const int n = proc.getNumKits(); const int n = proc.getNumPresets();
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
presetCombo.addItem(proc.getKitList()[i].name, i + 1); presetCombo.addItem(proc.getPresetName(i), i + 1);
presetCombo.setSelectedId(proc.currentKitIndex + 1, dontSendNotification); presetCombo.setSelectedId(proc.currentPresetIndex + 1, dontSendNotification);
} }
void TrommelkisteEditor::paint(Graphics& g) void TrommelkisteEditor::paint(Graphics& g)
@ -606,7 +629,8 @@ void TrommelkisteEditor::resized()
presetPrevBtn.setBounds(scaled(150), scaled(14), scaled(20), scaled(20)); presetPrevBtn.setBounds(scaled(150), scaled(14), scaled(20), scaled(20));
presetCombo.setBounds(scaled(172), scaled(14), scaled(100), scaled(20)); presetCombo.setBounds(scaled(172), scaled(14), scaled(100), scaled(20));
presetNextBtn.setBounds(scaled(274), scaled(14), scaled(20), scaled(20)); presetNextBtn.setBounds(scaled(274), scaled(14), scaled(20), scaled(20));
scaleCombo.setBounds(scaled(298), scaled(14), scaled(52), scaled(20)); presetSaveBtn.setBounds(scaled(296), scaled(14), scaled(48), scaled(20));
scaleCombo.setBounds(scaled(348), scaled(14), scaled(52), scaled(20));
// Header knobs // Header knobs
const int hk = HDR_KNOB; const int hk = HDR_KNOB;

View file

@ -42,6 +42,12 @@ public:
void mouseDown(const juce::MouseEvent&) override; void mouseDown(const juce::MouseEvent&) override;
void timerCallback() override; void timerCallback() override;
bool isInterestedInFileDrag(const juce::StringArray& files) override;
void fileDragEnter(const juce::StringArray& files, int x, int y) override;
void fileDragMove(const juce::StringArray& files, int x, int y) override;
void fileDragExit(const juce::StringArray& files) override;
void filesDropped(const juce::StringArray& files, int x, int y) override;
private: private:
TrommelkisteProcessor& proc; TrommelkisteProcessor& proc;
TrommelkisteLookAndFeel lnf; TrommelkisteLookAndFeel lnf;
@ -78,6 +84,7 @@ private:
juce::ComboBox presetCombo; juce::ComboBox presetCombo;
juce::TextButton presetPrevBtn{"<"}; juce::TextButton presetPrevBtn{"<"};
juce::TextButton presetNextBtn{">"}; juce::TextButton presetNextBtn{">"};
juce::TextButton presetSaveBtn{"SAVE"};
// Global effect controls in header // Global effect controls in header
KnobSlider globalRingFreq, globalDestruction; KnobSlider globalRingFreq, globalDestruction;

View file

@ -13,6 +13,7 @@ TrommelkisteProcessor::TrommelkisteProcessor()
formatManager.registerBasicFormats(); formatManager.registerBasicFormats();
scanSamples(); scanSamples();
loadKit(0); loadKit(0);
refreshUserPresets();
} }
TrommelkisteProcessor::~TrommelkisteProcessor() {} TrommelkisteProcessor::~TrommelkisteProcessor() {}
@ -605,19 +606,12 @@ static const TrommelkisteProcessor::Kit kits[] = {
const TrommelkisteProcessor::Kit* TrommelkisteProcessor::getKitList() { return kits; } const TrommelkisteProcessor::Kit* TrommelkisteProcessor::getKitList() { return kits; }
int TrommelkisteProcessor::getNumKits() const { return 4; } 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) void TrommelkisteProcessor::loadKit(int index)
{ {
if (index < 0 || index >= getNumKits()) if (index < 0 || index >= getNumKits())
return; return;
currentKitIndex = index;
const auto& kit = kits[index]; const auto& kit = kits[index];
for (int t = 0; t < NUM_TRACKS; ++t) for (int t = 0; t < NUM_TRACKS; ++t)
@ -647,18 +641,161 @@ void TrommelkisteProcessor::loadKit(int index)
} }
} }
void TrommelkisteProcessor::nextKit() juce::String TrommelkisteProcessor::getPresetName(int index) const
{ {
int idx = currentKitIndex + 1; if (index < 0 || index >= getNumPresets())
if (idx >= getNumKits()) idx = 0; return {};
loadKit(idx); if (index < getNumKits())
return kits[index].name;
return userPresets[index - getNumKits()].name;
} }
void TrommelkisteProcessor::prevKit() void TrommelkisteProcessor::loadPreset(int index)
{ {
int idx = currentKitIndex - 1; if (index < 0 || index >= getNumPresets())
if (idx < 0) idx = getNumKits() - 1; return;
loadKit(idx);
currentPresetIndex = index;
if (index < getNumKits())
loadKit(index);
else
loadUserPreset(index - getNumKits());
}
void TrommelkisteProcessor::nextPreset()
{
const int n = getNumPresets();
if (n <= 0) return;
loadPreset((currentPresetIndex + 1) % n);
}
void TrommelkisteProcessor::prevPreset()
{
const int n = getNumPresets();
if (n <= 0) return;
loadPreset((currentPresetIndex + n - 1) % n);
}
// ── User presets ───────────────────────────────────────────────
static juce::File getUserPresetDirectory()
{
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("Trommelkiste").getChildFile("Presets");
}
void TrommelkisteProcessor::refreshUserPresets()
{
userPresets.clear();
auto dir = getUserPresetDirectory();
if (! dir.isDirectory())
return;
juce::Array<juce::File> files =
dir.findChildFiles(juce::File::findFiles, false, "*.trommelpreset");
files.sort();
for (const auto& f : files)
{
auto xml = juce::XmlDocument::parse(f);
if (xml == nullptr)
continue;
UserPreset p;
p.name = xml->getStringAttribute("name");
if (p.name.isEmpty())
p.name = f.getFileNameWithoutExtension();
p.file = f;
userPresets.add(p);
}
}
void TrommelkisteProcessor::saveUserPreset(const juce::String& name)
{
auto dir = getUserPresetDirectory();
dir.createDirectory();
juce::String safeName = name;
safeName = safeName.replaceCharacter('\\', '_').replaceCharacter('/', '_');
auto file = dir.getChildFile(safeName + ".trommelpreset");
auto state = apvts.copyState();
state.setProperty("name", name, 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());
if (! xml->writeToFile(file, {}))
return;
refreshUserPresets();
for (int i = 0; i < userPresets.size(); ++i)
{
if (userPresets[i].file == file)
{
currentPresetIndex = getNumKits() + i;
break;
}
}
}
void TrommelkisteProcessor::loadUserPreset(int index)
{
if (index < 0 || index >= userPresets.size())
return;
std::unique_ptr<juce::XmlElement> xml = juce::XmlDocument::parse(userPresets[index].file);
if (xml == nullptr)
return;
auto state = juce::ValueTree::fromXml(*xml);
apvts.replaceState(state);
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::AudioProcessorEditor* TrommelkisteProcessor::createEditor() juce::AudioProcessorEditor* TrommelkisteProcessor::createEditor()
@ -670,7 +807,7 @@ void TrommelkisteProcessor::getStateInformation(juce::MemoryBlock& destData)
{ {
auto state = apvts.copyState(); auto state = apvts.copyState();
state.setProperty("kitIndex", currentKitIndex, nullptr); state.setProperty("presetIndex", currentPresetIndex, nullptr);
juce::ValueTree samples("Samples"); juce::ValueTree samples("Samples");
for (int i = 0; i < NUM_TRACKS; ++i) for (int i = 0; i < NUM_TRACKS; ++i)
@ -697,7 +834,8 @@ void TrommelkisteProcessor::setStateInformation(const void* data, int sizeInByte
auto state = juce::ValueTree::fromXml(*xml); auto state = juce::ValueTree::fromXml(*xml);
apvts.replaceState(state); apvts.replaceState(state);
currentKitIndex = (int)state.getProperty("kitIndex", 0); currentPresetIndex = (int)state.getProperty("presetIndex",
state.getProperty("kitIndex", 0));
auto samples = state.getChildWithName("Samples"); auto samples = state.getChildWithName("Samples");
if (samples.isValid()) if (samples.isValid())

View file

@ -78,14 +78,29 @@ public:
const char* samples[NUM_TRACKS]; const char* samples[NUM_TRACKS];
}; };
int currentKitIndex = 0; int currentPresetIndex = 0;
void loadKit(int index); void loadKit(int index);
void nextKit();
void prevKit();
int getNumKits() const; int getNumKits() const;
juce::String getCurrentKitName() const;
static const Kit* getKitList(); static const Kit* getKitList();
// User presets (saved to disk, shown in the preset menu)
struct UserPreset
{
juce::String name;
juce::File file;
};
juce::Array<UserPreset> userPresets;
void saveUserPreset(const juce::String& name);
void loadUserPreset(int index);
void refreshUserPresets();
int getNumPresets() const { return getNumKits() + userPresets.size(); }
juce::String getPresetName(int index) const;
void loadPreset(int index);
void nextPreset();
void prevPreset();
private: private:
struct Voice struct Voice
{ {