170 lines
9.1 KiB
C++
170 lines
9.1 KiB
C++
|
|
#include "Processor.h"
|
||
|
|
#include "Editor.h"
|
||
|
|
#include <algorithm>
|
||
|
|
#include <cmath>
|
||
|
|
#include <charconv>
|
||
|
|
#include <limits>
|
||
|
|
|
||
|
|
juce::AudioProcessorValueTreeState::ParameterLayout XtcJuceProcessor::layout()
|
||
|
|
{
|
||
|
|
juce::AudioProcessorValueTreeState::ParameterLayout result;
|
||
|
|
result.add(std::make_unique<juce::AudioParameterFloat>(juce::ParameterID{"gain",1}, "Output (dB)", -60.0f, 6.0f, 0.0f));
|
||
|
|
juce::NormalisableRange<float> frequency(20.0f,20000.0f);
|
||
|
|
frequency.setSkewForCentre(1000.0f);
|
||
|
|
result.add(std::make_unique<juce::AudioParameterFloat>(juce::ParameterID{"cutoff",1}, "Low-pass (Hz)", frequency, 8000.0f));
|
||
|
|
result.add(std::make_unique<juce::AudioParameterFloat>(juce::ParameterID{"room",1}, "Room size", 0.0f, 1.0f, .5f));
|
||
|
|
result.add(std::make_unique<juce::AudioParameterFloat>(juce::ParameterID{"wet",1}, "Reverb mix", 0.0f, 1.0f, .2f));
|
||
|
|
result.add(std::make_unique<juce::AudioParameterBool>(juce::ParameterID{"effects",1}, "Enable filter + reverb", false));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
XtcJuceProcessor::XtcJuceProcessor()
|
||
|
|
: AudioProcessor(BusesProperties().withOutput("Output",juce::AudioChannelSet::stereo(),true)),
|
||
|
|
parameters(*this,nullptr,"XTC_JUCE_STATE",layout())
|
||
|
|
{
|
||
|
|
static_assert(std::atomic<float>::is_always_lock_free && std::atomic<bool>::is_always_lock_free &&
|
||
|
|
std::atomic<unsigned int>::is_always_lock_free,"Realtime bridge requires lock-free atomics");
|
||
|
|
parameters.state.setProperty("version",1,nullptr);
|
||
|
|
gainDb=parameters.getRawParameterValue("gain"); cutoff=parameters.getRawParameterValue("cutoff");
|
||
|
|
room=parameters.getRawParameterValue("room"); wet=parameters.getRawParameterValue("wet");
|
||
|
|
effects=parameters.getRawParameterValue("effects");
|
||
|
|
}
|
||
|
|
XtcJuceProcessor::~XtcJuceProcessor() { releaseResources(); }
|
||
|
|
bool XtcJuceProcessor::isBusesLayoutSupported(const BusesLayout& bus) const
|
||
|
|
{
|
||
|
|
return bus.getMainInputChannelSet().isDisabled() &&
|
||
|
|
(bus.getMainOutputChannelSet()==juce::AudioChannelSet::mono() ||
|
||
|
|
bus.getMainOutputChannelSet()==juce::AudioChannelSet::stereo());
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::prepareToPlay(double sampleRate, int maxBlock)
|
||
|
|
{
|
||
|
|
releaseResources();
|
||
|
|
if (!std::isfinite(sampleRate) || sampleRate<8000 || sampleRate>192000 || maxBlock<1) {
|
||
|
|
failed.store(true); return;
|
||
|
|
}
|
||
|
|
xtc_plugin_spec spec{};
|
||
|
|
auto result=xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,0,sampleRate,&spec);
|
||
|
|
if (result==XTC_OK) result=xtc_plugin_create(&spec,sampleRate,chunkFrames,&synth);
|
||
|
|
if (result!=XTC_OK) { failed.store(true); return; }
|
||
|
|
rate=sampleRate;
|
||
|
|
const juce::dsp::ProcessSpec processing{rate,chunkFrames,static_cast<juce::uint32>(getTotalNumOutputChannels())};
|
||
|
|
filter.prepare(processing); filter.setType(juce::dsp::StateVariableTPTFilterType::lowpass);
|
||
|
|
reverb.prepare(processing);
|
||
|
|
gain.setRampDurationSeconds(.01); gain.setGainDecibels(gainDb->load()); gain.prepare(processing);
|
||
|
|
filter.reset(); reverb.reset(); gain.reset();
|
||
|
|
position=0; panicRequested.store(true); failed.store(false); prepared.store(true,std::memory_order_release);
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::releaseResources()
|
||
|
|
{
|
||
|
|
prepared.store(false,std::memory_order_release);
|
||
|
|
xtc_plugin_destroy(synth); synth=nullptr;
|
||
|
|
meter.store(0); position=0;
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::reset()
|
||
|
|
{
|
||
|
|
// Host contract: reset has exclusive processing ownership.
|
||
|
|
xtc_plugin_reset_history(synth); filter.reset(); reverb.reset(); gain.reset(); position=0;
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::enqueueNote(bool on, int channel, int note, float velocity) noexcept
|
||
|
|
{
|
||
|
|
if (channel<1 || channel>16 || note<0 || note>127 || !std::isfinite(velocity)) return;
|
||
|
|
int start1=0,size1=0,start2=0,size2=0;
|
||
|
|
keyboardQueue.prepareToWrite(1,start1,size1,start2,size2);
|
||
|
|
if (!size1) { dropped.fetch_add(1); panic(); return; }
|
||
|
|
auto& e=keyboardEvents[static_cast<size_t>(start1)];
|
||
|
|
e={0,on ? XTC_MIDI_NOTE_ON : XTC_MIDI_NOTE_OFF,static_cast<unsigned int>(channel-1),
|
||
|
|
static_cast<unsigned int>(note),static_cast<unsigned int>(juce::jlimit(1,127,juce::roundToInt(velocity*127)))};
|
||
|
|
keyboardQueue.finishedWrite(1);
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::discardKeyboard() noexcept
|
||
|
|
{
|
||
|
|
int start1=0,size1=0,start2=0,size2=0;
|
||
|
|
keyboardQueue.prepareToRead(queueCapacity,start1,size1,start2,size2);
|
||
|
|
keyboardQueue.finishedRead(size1+size2);
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::processBlock(juce::AudioBuffer<float>& audio, juce::MidiBuffer& midi)
|
||
|
|
{
|
||
|
|
juce::ScopedNoDenormals noDenormals;
|
||
|
|
const int frames=audio.getNumSamples(),channels=audio.getNumChannels();
|
||
|
|
audio.clear();
|
||
|
|
if (!ready() || !synth || (channels!=1 && channels!=2)) { midi.clear(); return; }
|
||
|
|
if (frames==0) { midi.clear(); return; }
|
||
|
|
if (position>std::numeric_limits<uint64_t>::max()-static_cast<uint64_t>(frames)) reset();
|
||
|
|
if (panicRequested.exchange(false,std::memory_order_acq_rel)) {
|
||
|
|
discardKeyboard(); xtc_plugin_reset_history(synth);
|
||
|
|
}
|
||
|
|
gain.setGainDecibels(gainDb->load(std::memory_order_relaxed));
|
||
|
|
filter.setCutoffFrequency(std::min(cutoff->load(std::memory_order_relaxed),static_cast<float>(rate*.45)));
|
||
|
|
juce::Reverb::Parameters settings;
|
||
|
|
settings.roomSize=room->load(std::memory_order_relaxed);
|
||
|
|
settings.wetLevel=wet->load(std::memory_order_relaxed); settings.dryLevel=1-settings.wetLevel;
|
||
|
|
reverb.setParameters(settings);
|
||
|
|
const bool enableEffects=effects->load(std::memory_order_relaxed)>.5f;
|
||
|
|
auto input=midi.cbegin();
|
||
|
|
for (int base=0;base<frames;base+=chunkFrames) {
|
||
|
|
const int length=std::min(chunkFrames,frames-base);
|
||
|
|
size_t count=0;
|
||
|
|
if (base==0) {
|
||
|
|
int start1=0,size1=0,start2=0,size2=0;
|
||
|
|
keyboardQueue.prepareToRead(queueCapacity,start1,size1,start2,size2);
|
||
|
|
for (int i=0;i<size1;++i) events[count++]=keyboardEvents[static_cast<size_t>(start1+i)];
|
||
|
|
for (int i=0;i<size2;++i) events[count++]=keyboardEvents[static_cast<size_t>(start2+i)];
|
||
|
|
keyboardQueue.finishedRead(size1+size2);
|
||
|
|
}
|
||
|
|
while (input!=midi.cend() && (*input).samplePosition<base+length) {
|
||
|
|
const auto message=*input++;
|
||
|
|
if (message.samplePosition<base) continue;
|
||
|
|
xtc_midi_event event{};
|
||
|
|
if (xtc_midi_decode(message.data,static_cast<size_t>(message.numBytes),
|
||
|
|
static_cast<unsigned int>(message.samplePosition-base),&event)!=XTC_OK) continue;
|
||
|
|
if (count==events.size()) {
|
||
|
|
dropped.fetch_add(1); xtc_plugin_reset_history(synth);
|
||
|
|
audio.clear(); midi.clear(); meter.store(0); return;
|
||
|
|
}
|
||
|
|
events[count++]=event;
|
||
|
|
}
|
||
|
|
if (xtc_plugin_process_midi_at(synth,events.data(),count,scratch.data(),static_cast<size_t>(length),position)!=XTC_OK) {
|
||
|
|
failed.store(true); xtc_plugin_reset_history(synth); audio.clear(); midi.clear(); meter.store(0); return;
|
||
|
|
}
|
||
|
|
for (int channel=0;channel<channels;++channel)
|
||
|
|
juce::FloatVectorOperations::copy(audio.getWritePointer(channel,base),scratch.data(),length);
|
||
|
|
auto block=juce::dsp::AudioBlock<float>(audio).getSubBlock(static_cast<size_t>(base),static_cast<size_t>(length));
|
||
|
|
juce::dsp::ProcessContextReplacing<float> context(block);
|
||
|
|
if (enableEffects) { filter.process(context); reverb.process(context); }
|
||
|
|
gain.process(context);
|
||
|
|
position+=static_cast<uint64_t>(length);
|
||
|
|
}
|
||
|
|
// Single atomics only; editor timer reads the last block's absolute peak.
|
||
|
|
meter.store(audio.getMagnitude(0,frames),std::memory_order_relaxed);
|
||
|
|
midi.clear();
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::getStateInformation(juce::MemoryBlock& destination)
|
||
|
|
{
|
||
|
|
const auto state=parameters.copyState();
|
||
|
|
if (auto xml=state.createXml()) copyXmlToBinary(*xml,destination);
|
||
|
|
}
|
||
|
|
void XtcJuceProcessor::setStateInformation(const void* data, int bytes)
|
||
|
|
{ (void)restorePreset(data,bytes); }
|
||
|
|
bool XtcJuceProcessor::restorePreset(const void* data, int bytes)
|
||
|
|
{
|
||
|
|
if (!data || bytes<=0 || bytes>65536) return false;
|
||
|
|
auto xml=getXmlFromBinary(data,bytes);
|
||
|
|
if (!xml || !xml->hasTagName("XTC_JUCE_STATE")) return false;
|
||
|
|
auto state=juce::ValueTree::fromXml(*xml);
|
||
|
|
// Validate the entire candidate before APVTS can clamp or partially adopt it.
|
||
|
|
if (!state.isValid() || state["version"].toString()!="1" || state.getNumChildren()!=5) return false;
|
||
|
|
juce::StringArray seen;
|
||
|
|
for (const auto& child : state) {
|
||
|
|
const auto id=child["id"].toString();
|
||
|
|
auto* parameter=parameters.getParameter(id);
|
||
|
|
const auto encoded=child["value"].toString().toStdString();
|
||
|
|
float value=0;
|
||
|
|
const auto parsed=std::from_chars(encoded.data(),encoded.data()+encoded.size(),value);
|
||
|
|
if (!child.hasType("PARAM") || !parameter || seen.contains(id) || !child.hasProperty("value") ||
|
||
|
|
parsed.ec!=std::errc{} || parsed.ptr!=encoded.data()+encoded.size() ||
|
||
|
|
!std::isfinite(value) || value<parameter->getNormalisableRange().start || value>parameter->getNormalisableRange().end ||
|
||
|
|
(id=="effects" && value>0 && value<1)) return false;
|
||
|
|
seen.add(id);
|
||
|
|
}
|
||
|
|
parameters.replaceState(state); panic(); return true;
|
||
|
|
}
|
||
|
|
juce::AudioProcessorEditor* XtcJuceProcessor::createEditor() { return new XtcJuceEditor(*this); }
|