mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 04:10:48 +02:00
init
This commit is contained in:
commit
d21bc831e1
178 changed files with 24136 additions and 0 deletions
584
Source/PluginProcessor.cpp
Normal file
584
Source/PluginProcessor.cpp
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue