This commit is contained in:
Armin 2026-07-23 23:40:48 +02:00
commit d21bc831e1
178 changed files with 24136 additions and 0 deletions

584
Source/PluginProcessor.cpp Normal file
View file

@ -0,0 +1,584 @@
/*
==============================================================================
PluginProcessor.cpp
Created: 5 Sep 2023
Author: binya
==============================================================================
*/
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "PluginParameters.h"
#include "Sampler/CustomSynthesizer.h"
#if JUCE_DEBUG
#include "Utilities/BufferUtils.h"
#endif
JustaSampleAudioProcessor::JustaSampleAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
: AudioProcessor(BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput("Input", juce::AudioChannelSet::stereo(), true)
#endif
.withOutput("Output", juce::AudioChannelSet::stereo(), true)
#endif
),
apvts(*this, &undoManager, "Parameters", PluginParameters::createParameterLayout()),
samplerSound(apvts, pluginState, sampleBuffer, int(bufferSampleRate)),
fileFilter("", {}, {}),
deviceRecorder(deviceManager)
#endif
{
deviceRecorder.addListener(this);
pitchDetector.addListener(this);
formatManager.registerBasicFormats();
fileFilter = juce::WildcardFileFilter(formatManager.getWildcardForAllFormats(), {}, {});
mtsClient = MTS_RegisterClient();
}
JustaSampleAudioProcessor::~JustaSampleAudioProcessor()
{
for (int i = synth.getNumVoices() - 1; i >= 0; i--)
synth.removeVoiceWithoutDeleting(i);
MTS_DeregisterClient(mtsClient);
}
#ifndef JucePlugin_PreferredChannelConfigurations
bool JustaSampleAudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const
{
#if JucePlugin_IsMidiEffect
juce::ignoreUnused(layouts);
return true;
#else
// This is the place where you check if the layout is supported.
// In this template code we only support mono or stereo.
// Some plugin hosts, such as certain GarageBand versions, will only
// load plugins that support stereo bus layouts.
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
// This checks if the input layout matches the output layout
#if ! JucePlugin_IsSynth
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
#endif
return true;
#endif
}
#endif
bool JustaSampleAudioProcessor::acceptsMidi() const
{
#if JucePlugin_WantsMidiInput
return true;
#else
return false;
#endif
}
bool JustaSampleAudioProcessor::producesMidi() const
{
#if JucePlugin_ProducesMidiOutput
return true;
#else
return false;
#endif
}
bool JustaSampleAudioProcessor::isMidiEffect() const
{
#if JucePlugin_IsMidiEffect
return true;
#else
return false;
#endif
}
juce::AudioProcessorEditor* JustaSampleAudioProcessor::createEditor()
{
// A bit of a hack to set a default look and feel for the editor as it's created without sharing the object globally
juce::LookAndFeel::setDefaultLookAndFeel(&lookAndFeel);
auto* editor = new JustaSampleAudioProcessorEditor(*this);
editor->setLookAndFeel(&lookAndFeel);
juce::LookAndFeel::setDefaultLookAndFeel(nullptr);
return editor;
}
juce::VST3ClientExtensions* JustaSampleAudioProcessor::getVST3ClientExtensions()
{
if (PluginParameters::REAPER_INTEGRATION_ENABLED && hostType.isReaper() && wrapperType == wrapperType_VST3)
return &reaperExtensions;
return nullptr;
}
void JustaSampleAudioProcessor::prepareToPlay(double sampleRate, int /*maximumExpectedSamplesPerBlock*/)
{
juce::ScopedLock lock(voiceLock);
synth.clearSounds();
synth.addSound(new BlankSynthesizerSound());
for (int i = synth.getNumVoices() - 1; i >= 0; i--)
synth.removeVoiceWithoutDeleting(i);
samplerVoices.clear();
synth.setCurrentPlaybackSampleRate(sampleRate);
for (int i = 0; i < PluginParameters::MAX_VOICES; i++)
{
const bool initializeSample = samplerSound.sampleRate > 0 && samplerSound.sample.getNumSamples() > 0;
auto* voice = new CustomSamplerVoice(samplerSound, mtsClient, sampleRate, getBlockSize(), initializeSample);
samplerVoices.add(voice);
}
}
void JustaSampleAudioProcessor::releaseResources()
{
// When playback stops, this is a place to clean up resources
}
void JustaSampleAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples());
// Handle VST3 Reaper extensions functionality
if (PluginParameters::REAPER_INTEGRATION_ENABLED && hostType.isReaper() && wrapperType == wrapperType_VST3)
{
auto file = reaperExtensions.getNamedConfigParam(REAPER_FILE_PATH);
juce::File filePath{ file };
auto currentFile = lastLoadAttempt.isNotEmpty() ? lastLoadAttempt : juce::String(pluginState.filePath);
juce::File currentFilePath{ currentFile };
if (file.isNotEmpty() && filePath.getFileIdentifier() != currentFilePath.getFileIdentifier())
{
loadedFromReaper = true;
loadSampleFromPath(file, true, "", false, [&](bool loaded) -> void
{
if (!loaded)
loadedFromReaper = false;
});
}
}
if (sampleBuffer.getNumSamples() == 0 || sampleBuffer.getNumChannels() == 0)
return;
juce::ScopedTryLock lock(voiceLock);
if (lock.isLocked())
{
adjustVoiceCount();
synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
#if JUCE_DEBUG
for (int ch = 0; ch < buffer.getNumChannels(); ch++)
{
protectYourEars(buffer.getWritePointer(ch), buffer.getNumSamples());
}
#endif
}
}
void JustaSampleAudioProcessor::adjustVoiceCount(int count)
{
int numVoices = juce::jmax<int>(1, p(PluginParameters::NUM_VOICES));
if (count >= 0)
numVoices = count;
int currentVoices = synth.getNumVoices();
// We never need to instantiate new voices
if (currentVoices > numVoices)
{
for (int i = currentVoices - 1; i >= numVoices; --i)
{
synth.removeVoiceWithoutDeleting(i);
samplerVoices[i]->immediateHalt();
}
}
if (currentVoices < numVoices)
{
for (int i = currentVoices; i < numVoices; ++i)
{
synth.addVoice(samplerVoices[i]);
}
}
}
//==============================================================================
void JustaSampleAudioProcessor::getStateInformation(juce::MemoryBlock& destData)
{
// All state properties should be saved at this point
spv(PluginParameters::State::WIDTH) = pluginState.width.load();
spv(PluginParameters::State::HEIGHT) = pluginState.height.load();
spv(PluginParameters::State::FILE_PATH) = pluginState.filePath.load();
spv(PluginParameters::State::SAMPLE_HASH) = pluginState.sampleHash.load();
spv(PluginParameters::State::USING_FILE_REFERENCE) = pluginState.usingFileReference.load();
spv(PluginParameters::State::RECENT_FILES) = pluginState.recentFiles.load();
if (const auto stateXml = deviceManager.createStateXml())
spv(PluginParameters::State::SAVED_DEVICE_SETTINGS) = stateXml->toString();
spv(PluginParameters::State::UI_VIEW_START) = pluginState.viewStart.load();
spv(PluginParameters::State::UI_VIEW_END) = pluginState.viewEnd.load();
spv(PluginParameters::State::PIN_VIEW) = pluginState.pinView.load();
spv(PluginParameters::State::PRIMARY_CHANNEL) = pluginState.primaryChannel.load();
spv(PluginParameters::State::SAMPLE_START) = pluginState.sampleStart.load();
spv(PluginParameters::State::SAMPLE_END) = pluginState.sampleEnd.load();
spv(PluginParameters::State::LOOP_START) = pluginState.loopStart.load();
spv(PluginParameters::State::LOOP_END) = pluginState.loopEnd.load();
spv(PluginParameters::State::SHOW_FX) = pluginState.showFX.load();
spv(PluginParameters::State::DARK_MODE) = pluginState.darkMode.load();
// Then, write empty "header" information to the stream
size_t initialSize{ 0 };
size_t apvtsSize{ 0 };
{
auto apvtsMos = juce::MemoryOutputStream{ destData, true };
apvtsMos.writeInt(0); // apvts size
apvtsMos.writeInt(0); // sample size
initialSize = apvtsMos.getDataSize();
apvts.state.writeToStream(apvtsMos);
apvtsSize = apvtsMos.getDataSize() - initialSize;
}
size_t sampleSize = 0;
// If we're not using a file reference, write the sample buffer to the stream
if (!pluginState.usingFileReference && sampleBuffer.getNumSamples())
{
juce::WavAudioFormat wavFormat;
auto options = juce::AudioFormatWriterOptions{}
.withSampleRate(bufferSampleRate)
.withNumChannels(sampleBuffer.getNumChannels())
.withBitsPerSample(PluginParameters::STORED_BITRATE);
std::unique_ptr<juce::OutputStream> stream = std::make_unique<juce::MemoryOutputStream>(destData, true);
auto formatWriter = wavFormat.createWriterFor(stream, options);
formatWriter->writeFromAudioSampleBuffer(sampleBuffer, 0, sampleBuffer.getNumSamples());
formatWriter.reset();
sampleSize = destData.getSize() - apvtsSize - initialSize;
}
// Write the header
auto headerMos = juce::MemoryOutputStream{ destData, true };
headerMos.setPosition(0);
headerMos.writeInt(int(apvtsSize));
headerMos.writeInt(int(sampleSize));
}
void JustaSampleAudioProcessor::setStateInformation(const void* data, int sizeInBytes)
{
// First, read the header information which includes the sizes of the APVTS and sample buffer
juce::MemoryInputStream mis(data, sizeInBytes, false);
size_t apvtsSize = mis.readInt();
size_t sampleSize = mis.readInt();
if (size_t(sizeInBytes) != apvtsSize + sampleSize + 8)
return; // format issue
// Read the APVTS
juce::SubregionStream apvtsStream{ &mis, mis.getPosition(), juce::int64(apvtsSize), false };
auto tree = juce::ValueTree::readFromStream(apvtsStream);
if (tree.isValid())
{
apvts.replaceState(tree);
// Load the plugin state struct
pluginState.width = sp(PluginParameters::State::WIDTH);
pluginState.height = sp(PluginParameters::State::HEIGHT);
pluginState.filePath = sp(PluginParameters::State::FILE_PATH);
pluginState.usingFileReference = sp(PluginParameters::State::USING_FILE_REFERENCE);
pluginState.pinView = sp(PluginParameters::State::PIN_VIEW);
pluginState.primaryChannel = sp(PluginParameters::State::PRIMARY_CHANNEL);
pluginState.showFX = sp(PluginParameters::State::SHOW_FX);
pluginState.darkMode = sp(PluginParameters::State::DARK_MODE);
// We'd rather wait to update certain fields until the sample is actually loaded. This is usually irrelevant, but if the DAW
// allows undo and redo then a previous sample will likely be loaded while the new one is loading, so the new info will not make sense.
auto sampleHash = sp(PluginParameters::State::SAMPLE_HASH);
auto updateFileInfo = [this, sampleHash,
viewStart = sp(PluginParameters::State::UI_VIEW_START), viewEnd = sp(PluginParameters::State::UI_VIEW_END),
sampleStart = sp(PluginParameters::State::SAMPLE_START), sampleEnd = sp(PluginParameters::State::SAMPLE_END),
loopStart = sp(PluginParameters::State::LOOP_START), loopEnd = sp(PluginParameters::State::LOOP_END)]
{
pluginState.sampleHash = sampleHash;
pluginState.viewStart = viewStart;
pluginState.viewEnd = viewEnd;
pluginState.sampleStart = sampleStart;
pluginState.sampleEnd = sampleEnd;
pluginState.loopStart = loopStart;
pluginState.loopEnd = loopEnd;
};
bool newFile = pluginState.sampleHash != sp(PluginParameters::State::SAMPLE_HASH).toString();
if (!newFile)
updateFileInfo();
auto recentFiles{ sp(PluginParameters::State::RECENT_FILES) };
juce::StringArray fileArray{};
for (int i = recentFiles.size() - 1; i >= 0; i--)
fileArray.add(recentFiles[i].toString());
pluginState.recentFiles = fileArray;
juce::XmlDocument deviceSettingsDocument{ sp(PluginParameters::State::SAVED_DEVICE_SETTINGS) };
deviceManagerLoadedState = deviceSettingsDocument.getDocumentElement();
auto currentState = deviceManager.createStateXml();
deviceManagerLoaded = currentState && currentState->isEquivalentTo(&*deviceManagerLoadedState, true);
// Either load the sample from the file reference or from the stream directly
juce::String filePath = pluginState.filePath;
if (pluginState.usingFileReference && filePath.isNotEmpty() && newFile)
{
loadSampleFromPath(filePath, false, sampleHash, false, [this, filePath, updateFileInfo, sampleHash](bool fileLoaded) -> void
{
if (!fileLoaded) // Either the file was not found, loaded incorrectly, or the hash was incorrect
{
openFileChooser("File was not found. Please locate " + juce::File(filePath).getFileName(),
juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles, [this, updateFileInfo, sampleHash](const juce::FileChooser& chooser)
{
auto file = chooser.getResult();
loadSampleFromPath(file.getFullPathName(), false, sampleHash, true,
[updateFileInfo](bool loaded) { if (loaded) updateFileInfo(); });
});
}
else updateFileInfo();
});
}
else if (sampleSize && newFile)
{
// A copy must be made to allow reading in another thread, outside the lifetime of this function
auto sampleData = std::make_shared<juce::MemoryBlock>(sampleSize);
mis.read(sampleData->getData(), int(sampleSize));
auto wavStream = new juce::MemoryInputStream(*sampleData, false);
juce::WavAudioFormat wavFormat;
std::unique_ptr<juce::AudioFormatReader> wavFormatReader{ wavFormat.createReaderFor(wavStream, true) };
if (wavFormatReader)
{
lastLoadAttempt = "";
reaperExtensions.setNamedConfigParam(REAPER_FILE_PATH, "");
sampleLoader.loadSample(std::move(wavFormatReader), [this, sampleData /* necessary capture */, updateFileInfo, sampleHash]
(const std::unique_ptr<juce::AudioBuffer<float>>& loadedSample, const juce::String&, const std::unique_ptr<juce::AudioFormatReader>& reader) -> void
{
if (!reader)
return;
loadSample(*loadedSample, int(reader->sampleRate), false, sampleHash);
updateFileInfo();
});
}
}
}
}
//==============================================================================
void JustaSampleAudioProcessor::loadSample(juce::AudioBuffer<float>& sample, int sampleRate, bool resetParameters, const juce::String& precomputedHash)
{
juce::ScopedLock lock(voiceLock);
haltVoices();
sampleBuffer = std::move(sample);
bufferSampleRate = float(sampleRate);
if (precomputedHash.isNotEmpty())
pluginState.sampleHash = precomputedHash;
else
pluginState.sampleHash = getSampleHash(sampleBuffer);
if (resetParameters)
{
pluginState.usingFileReference = sampleBufferNeedsReference();
pluginState.sampleStart = 0;
pluginState.sampleEnd = sampleBuffer.getNumSamples() - 1;
// The order here is actually important, because the start and end buttons can otherwise enable the main loop button
pv(PluginParameters::LOOPING_HAS_START) = false;
pv(PluginParameters::LOOPING_HAS_END) = false;
pv(PluginParameters::IS_LOOPING) = false;
pluginState.loopStart = 0;
pluginState.loopEnd = sampleBuffer.getNumSamples() - 1;
}
samplerSound.sampleChanged(int(bufferSampleRate));
for (const auto& voice : samplerVoices)
voice->initializeSample();
}
void JustaSampleAudioProcessor::loadSampleFromPath(const juce::String& path, bool resetParameters, const juce::String& expectedHash, bool continueWithWrongHash, const std::function<void(bool)>& callback)
{
const juce::File file{ path };
std::unique_ptr<juce::AudioFormatReader> formatReader{ formatManager.createReaderFor(file) };
if (!formatReader || !formatReader->lengthInSamples)
return callback(false);
lastLoadAttempt = path;
reaperExtensions.setNamedConfigParam(REAPER_FILE_PATH, "");
// Load the file and check the hash
sampleLoader.loadSample(std::move(formatReader), [this, callback, path, expectedHash, continueWithWrongHash, resetParameters]
(const std::unique_ptr<juce::AudioBuffer<float>>& loadedSample, const juce::String& sampleHash, const std::unique_ptr<juce::AudioFormatReader>& reader) -> void
{
if (expectedHash.isNotEmpty() && sampleHash != expectedHash && !continueWithWrongHash)
return callback(false);
pluginState.filePath = path;
loadSample(*loadedSample, int(reader->sampleRate), resetParameters || (sampleHash != expectedHash && continueWithWrongHash), sampleHash);
return callback(true);
});
}
//==============================================================================
bool JustaSampleAudioProcessor::canLoadFileExtension(const juce::String& filePath) const
{
return fileFilter.isFileSuitable(filePath);
}
bool JustaSampleAudioProcessor::sampleBufferNeedsReference(const juce::AudioBuffer<float>& buffer) const
{
double totalFileBits = double(buffer.getNumSamples()) * buffer.getNumChannels() * PluginParameters::STORED_BITRATE;
return totalFileBits > PluginParameters::MAX_FILE_SIZE;
}
//==============================================================================
void JustaSampleAudioProcessor::openFileChooser(const juce::String& message, int flags, const std::function<void(const juce::FileChooser&)>& callback, bool wavOnly)
{
// DAW specific exceptions
bool useNative = !hostType.isArdour();
fileChooser = std::make_unique<juce::FileChooser>(message, juce::File::getSpecialLocation(juce::File::userDesktopDirectory), wavOnly ? "*.wav" : formatManager.getWildcardForAllFormats(), useNative);
juce::MessageManager::callAsync([this, flags, callback] { fileChooser->launchAsync(flags, callback); });
}
void JustaSampleAudioProcessor::haltVoices()
{
for (auto voice : samplerVoices)
{
voice->immediateHalt();
}
}
void JustaSampleAudioProcessor::playVoice()
{
synth.noteOn(0, 70, 1.0f);
}
void JustaSampleAudioProcessor::initializeDeviceManager()
{
if (!deviceManagerLoaded)
{
deviceManager.initialise(2, 0, &*deviceManagerLoadedState, true);
deviceManagerLoaded = true;
}
}
void JustaSampleAudioProcessor::recordingFinished(juce::AudioBuffer<float> recordingBuffer, int recordingSampleRate)
{
// If the recording is too large, we prompt to save it to a file, otherwise load it into the plugin simply
if (sampleBufferNeedsReference(recordingBuffer))
{
openFileChooser("Recording too large for plugin state, save to a file",
juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles,
[this, recordingBuffer, recordingSampleRate](const juce::FileChooser& chooser) -> void {
juce::File file = chooser.getResult();
auto fileStream = std::make_unique<juce::FileOutputStream>(file);
if (file.hasWriteAccess() && fileStream->openedOk())
{
fileStream->setPosition(0);
fileStream->truncate();
juce::WavAudioFormat wavFormat;
auto options = juce::AudioFormatWriterOptions{}
.withSampleRate(recordingSampleRate)
.withNumChannels(recordingBuffer.getNumChannels())
.withBitsPerSample(PluginParameters::STORED_BITRATE);
std::unique_ptr<juce::OutputStream> outputStream = std::move(fileStream);
auto formatWriter = wavFormat.createWriterFor(outputStream, options);
formatWriter->writeFromAudioSampleBuffer(recordingBuffer, 0, recordingBuffer.getNumSamples());
outputStream.release();
loadSampleFromPath(file.getFullPathName(), true);
}
}, true);
}
else
{
pluginState.filePath = "";
lastLoadAttempt = "";
reaperExtensions.setNamedConfigParam(REAPER_FILE_PATH, "");
loadSample(recordingBuffer, recordingSampleRate, true);
}
}
bool JustaSampleAudioProcessor::startPitchDetectionRoutine(int startSample, int endSample)
{
if (!sampleBuffer.getNumSamples())
return false;
// Truth be told, this does not need to be in a separate thread, but the algorithm I was using before was much slower
pitchDetector.setData(sampleBuffer, startSample, endSample, bufferSampleRate);
pitchDetector.startThread();
return true;
}
void JustaSampleAudioProcessor::exitSignalSent()
{
double pitch = pitchDetector.getPitch();
if (pitch > 0)
{
float a4_hz = p(PluginParameters::A4_HZ);
if (a4_hz <= 0)
a4_hz = 440.0f;
// Large pitch adjustments result in worse quality, so we limit to +/- 6 semitones for this feature
float tuningAmount = 12.f * std::log2(a4_hz / pitch);
float rangeMin = -6.f;
float rangeMax = 6.f;
float range = rangeMax - rangeMin;
tuningAmount = std::fmod(tuningAmount - rangeMin, range);
if (tuningAmount < 0)
tuningAmount += range;
tuningAmount += rangeMin;
int semitones = int(tuningAmount);
int cents = int(100 * (tuningAmount - semitones));
apvts.getParameterAsValue(PluginParameters::SEMITONE_TUNING) = semitones;
apvts.getParameterAsValue(PluginParameters::CENT_TUNING) = cents;
apvts.getParameterAsValue(PluginParameters::WIDE_TUNING) = 0.0f;
}
}
//==============================================================================
int JustaSampleAudioProcessor::visibleSamples() const
{
return pluginState.viewEnd.load() - pluginState.viewStart.load() + 1;
}
//==============================================================================
// This creates new instances of the plugin...
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new JustaSampleAudioProcessor();
}