#include "Processor.h" #include "Editor.h" #include #include #include #include juce::AudioProcessorValueTreeState::ParameterLayout XtcJuceProcessor::layout() { juce::AudioProcessorValueTreeState::ParameterLayout result; result.add(std::make_unique(juce::ParameterID{"gain",1}, "Output (dB)", -60.0f, 6.0f, 0.0f)); juce::NormalisableRange frequency(20.0f,20000.0f); frequency.setSkewForCentre(1000.0f); result.add(std::make_unique(juce::ParameterID{"cutoff",1}, "Low-pass (Hz)", frequency, 8000.0f)); result.add(std::make_unique(juce::ParameterID{"room",1}, "Room size", 0.0f, 1.0f, .5f)); result.add(std::make_unique(juce::ParameterID{"wet",1}, "Reverb mix", 0.0f, 1.0f, .2f)); result.add(std::make_unique(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::is_always_lock_free && std::atomic::is_always_lock_free && std::atomic::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(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(start1)]; e={0,on ? XTC_MIDI_NOTE_ON : XTC_MIDI_NOTE_OFF,static_cast(channel-1), static_cast(note),static_cast(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& 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::max()-static_cast(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(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(start1+i)]; for (int i=0;i(start2+i)]; keyboardQueue.finishedRead(size1+size2); } while (input!=midi.cend() && (*input).samplePosition(message.numBytes), static_cast(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(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(audio).getSubBlock(static_cast(base),static_cast(length)); juce::dsp::ProcessContextReplacing context(block); if (enableEffects) { filter.process(context); reverb.process(context); } gain.process(context); position+=static_cast(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) || valuegetNormalisableRange().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); }