JUCE SDK integration.

This commit is contained in:
Johannes Findeisen 2026-11-06 18:29:35 +01:00
commit adcbcdfd5e
16 changed files with 1057 additions and 0 deletions

170
examples/juce/Processor.cpp Normal file
View file

@ -0,0 +1,170 @@
#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); }