#include "Processor.h" #include #include #include #include #include #include namespace { void require(bool condition,const char* message) { if (!condition) throw std::runtime_error(message); } void set(XtcJuceProcessor& p,const juce::String& id,float value) { auto* parameter=p.parameters.getParameter(id); require(parameter!=nullptr,"Unknown parameter"); parameter->setValueNotifyingHost(parameter->convertTo0to1(value)); } void prepare(XtcJuceProcessor& processor,int channels=2,double rate=48000) { processor.setPlayConfigDetails(0,channels,rate,256); processor.prepareToPlay(rate,256); require(processor.ready(),"Could not prepare xtc synth"); // Consume preparation panic before callers enqueue keyboard notes. juce::AudioBuffer silence(channels,1); juce::MidiBuffer empty; processor.processBlock(silence,empty); processor.reset(); } void selfTest() { XtcJuceProcessor processor; prepare(processor); juce::AudioBuffer audio(2,513); juce::MidiBuffer midi; midi.addEvent(juce::MidiMessage::noteOn(3,69,static_cast(110)),13); midi.addEvent(juce::MidiMessage::noteOff(3,69),401); midi.addEvent(juce::MidiMessage::allNotesOff(3),480); processor.processBlock(audio,midi); require(!processor.processingFailed() && midi.isEmpty(),"Callback failed or produced unexpected MIDI"); for (int i=0;i<13;++i) require(audio.getSample(0,i)==0,"Note played early"); require(audio.getMagnitude(20,380)>.02f,"Note did not sound"); for (int i=480;i<513;++i) require(audio.getSample(0,i)==0,"All notes off was not sample aligned"); xtc_plugin_spec spec{}; xtc_plugin* reference=nullptr; require(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,0,48000,&spec)==XTC_OK,"Reference probe failed"); require(xtc_plugin_create(&spec,48000,256,&reference)==XTC_OK,"Reference prepare failed"); std::array expected{}; const std::array source{{{13,XTC_MIDI_NOTE_ON,2,69,110}, {401,XTC_MIDI_NOTE_OFF,2,69,0},{480,XTC_MIDI_ALL_OFF,2,0,0}}}; for (int base=0;base<513;base+=256) { std::array block{}; size_t count=0; const int length=std::min(256,513-base); for (const auto& event : source) if (event.offset>=static_cast(base) && event.offset(base+length)) { block[count]=event; block[count++].offset-=static_cast(base); } require(xtc_plugin_process_midi_at(reference,block.data(),count,expected.data(),static_cast(length),static_cast(base))==XTC_OK,"Reference render failed"); for (int i=0;i(i)],"JUCE bridge changed the C synth output"); } xtc_plugin_destroy(reference); processor.reset(); midi.clear(); processor.processBlock(audio,midi); require(audio.getMagnitude(0,audio.getNumSamples())==0,"Reset retained held voices"); // GUI events use a lock-free queue; its overflow must never leave a held note. processor.enqueueNote(true,1,60,1); processor.processBlock(audio,midi); require(audio.getMagnitude(0,513)>0,"Keyboard queue did not sound"); for (int i=0;i<300;++i) processor.enqueueNote(true,1,60,1); processor.processBlock(audio,midi); require(processor.droppedEvents()>0 && audio.getMagnitude(0,513)==0,"GUI overflow did not panic safely"); for (int i=0;i<300;++i) midi.addEvent(juce::MidiMessage::noteOn(1,60,static_cast(127)),0); processor.processBlock(audio,midi); require(audio.getMagnitude(0,513)==0,"Host MIDI overflow emitted a partial block"); processor.processBlock(audio,midi); require(audio.getMagnitude(0,513)==0,"Host MIDI overflow left stuck voices"); // Host-sized blocks may exceed the prepared internal chunk size. XtcJuceProcessor whole,split; prepare(whole); prepare(split); juce::AudioBuffer large(2,4097); midi.addEvent(juce::MidiMessage::noteOn(1,65,static_cast(100)),0); whole.processBlock(large,midi); for (int base=0;base<4097;base+=256) { const int length=std::min(256,4097-base); juce::AudioBuffer part(2,length); if (base==0) midi.addEvent(juce::MidiMessage::noteOn(1,65,static_cast(100)),0); split.processBlock(part,midi); for (int i=0;i(state.getSize())); require(restored.parameters.getRawParameterValue("gain")->load()==-12 && restored.parameters.getRawParameterValue("effects")->load()==1,"Preset did not restore"); auto reject=[&](juce::ValueTree candidate) { juce::MemoryBlock corrupt; auto xml=candidate.createXml(); juce::AudioProcessor::copyXmlToBinary(*xml,corrupt); restored.setStateInformation(corrupt.getData(),static_cast(corrupt.getSize())); require(restored.parameters.getRawParameterValue("gain")->load()==-12,"Invalid state partially changed parameters"); }; auto candidate=processor.parameters.copyState(); candidate.setProperty("version",99,nullptr); reject(candidate); candidate=processor.parameters.copyState(); candidate.removeChild(0,nullptr); reject(candidate); candidate=processor.parameters.copyState(); candidate.getChild(0).setProperty("value",999999,nullptr); reject(candidate); candidate=processor.parameters.copyState(); candidate.getChild(0).setProperty("value","not-a-number",nullptr); reject(candidate); restored.setStateInformation("bad",3); require(restored.parameters.getRawParameterValue("gain")->load()==-12,"Malformed state accepted"); prepare(restored,1,44100); midi.addEvent(juce::MidiMessage::noteOn(1,60,static_cast(100)),0); juce::AudioBuffer mono(1,256); restored.processBlock(mono,midi); for (int i=0;i<256;++i) require(std::isfinite(mono.getSample(0,i)),"DSP produced nonfinite samples"); restored.releaseResources(); mono.applyGain(0); midi.clear(); restored.processBlock(mono,midi); require(mono.getMagnitude(0,256)==0,"Released processor did not silence"); restored.prepareToPlay(0,256); require(!restored.ready() && restored.processingFailed(),"Invalid format accepted"); std::cout << "JUCE bridge: exact C synthesis, MIDI offsets, stereo/mono, variable blocks, reset, overflow, preset recovery and DSP passed.\n"; } struct Scheduled { int64_t frame; juce::MidiMessage message; }; std::vector readMidi(const juce::File& path) { require(path.existsAsFile() && path.getSize()<=4*1024*1024,"MIDI file missing or larger than 4 MiB"); juce::FileInputStream input(path); juce::MidiFile file; int fileType=-1; require(input.openedOk() && file.readFrom(input,false,&fileType),"Cannot read Standard MIDI File"); require(fileType==0 || fileType==1,"Only type 0/1 MIDI files are supported"); require(file.getTimeFormat()>0,"Only PPQN MIDI files are supported"); file.convertTimestampTicksToSeconds(); std::vector events; for (int track=0;trackgetNumEvents();++i) { const auto& message=sequence->getEventPointer(i)->message; if (!(message.isNoteOnOrOff() || message.isController() || message.isPitchWheel())) continue; const double seconds=message.getTimeStamp(); require(std::isfinite(seconds) && seconds>=0 && seconds<=600,"MIDI time exceeds ten-minute example limit"); require(events.size()<100000,"MIDI example limit: 100000 events"); events.push_back({static_cast(std::llround(seconds*48000)),message}); } } std::stable_sort(events.begin(),events.end(),[](const auto& a,const auto& b){return a.frame(); auto* vst3=format.get(); manager.addFormat(std::move(format)); juce::OwnedArray descriptions; vst3->findAllTypesForFile(descriptions,bundle.getFullPathName()); require(!descriptions.isEmpty(),"No VST3 audio classes found"); for (const auto* description : descriptions) { juce::String error; auto instance=manager.createPluginInstance(*description,48000,256,error); require(instance!=nullptr,error.isNotEmpty() ? error.toRawUTF8() : "Cannot instantiate VST3"); std::cout << description->name << " | " << description->manufacturerName << " | " << description->version << " | " << instance->getTotalNumInputChannels() << " inputs / " << instance->getTotalNumOutputChannels() << " outputs | " << instance->getParameters().size() << " parameters\n"; for (auto* parameter : instance->getParameters()) std::cout << " " << parameter->getName(64) << " = " << parameter->getValue() << '\n'; if (verifyExample) { require(instance->getParameters().size()>=5 && description->isInstrument,"Not the xtc JUCE example"); juce::MemoryBlock state; instance->getStateInformation(state); require(state.getSize()>0,"JUCE host did not capture VST3 state"); auto* output=instance->getParameters().getFirst(); const float original=output->getValue(); output->setValueNotifyingHost(.3f); instance->setStateInformation(state.getData(),static_cast(state.getSize())); require(std::abs(output->getValue()-original)<.00001f,"VST3 opaque-state roundtrip failed"); instance->setPlayConfigDetails(0,2,48000,256); instance->prepareToPlay(48000,256); juce::AudioBuffer audio(2,256); juce::MidiBuffer midi; midi.addEvent(juce::MidiMessage::noteOn(1,69,static_cast(110)),13); midi.addEvent(juce::MidiMessage::allNotesOff(1),200); instance->processBlock(audio,midi); for (int i=0;i<13;++i) require(audio.getSample(0,i)==0,"VST3 note played early"); require(audio.getMagnitude(13,187)>.005f,"JUCE-hosted VST3 produced silence"); for (int i=200;i<256;++i) require(audio.getSample(0,i)==0,"VST3 all-off offset failed"); instance->releaseResources(); } } if (verifyExample) std::cout << "JUCE VST3 hosting: discovery, parameters, opaque-state roundtrip and MIDI processing passed.\n"; } void snapshot(const juce::File& destination) { require(!destination.exists(),"Snapshot already exists"); XtcJuceProcessor processor; prepare(processor); set(processor,"effects",1); set(processor,"gain",-6); std::unique_ptr editor(processor.createEditor()); require(editor && editor->getWidth()==780 && editor->getHeight()==490,"Editor layout failed"); const auto image=editor->createComponentSnapshot(editor->getLocalBounds(),true,2.0f); require(image.isValid(),"Editor snapshot failed"); juce::TemporaryFile temporary(destination); juce::PNGImageFormat png; { juce::FileOutputStream stream(temporary.getFile()); require(stream.openedOk() && png.writeImageToStream(image,stream),"PNG encoding failed"); } require(::link(temporary.getFile().getFullPathName().toRawUTF8(),destination.getFullPathName().toRawUTF8())==0,"PNG publication failed"); editor.reset(); editor.reset(processor.createEditor()); require(editor!=nullptr,"Editor could not reopen"); std::cout << "Saved actual JUCE editor snapshot without opening an audio device.\n"; } void render(const juce::File& destination,const juce::File& midiFile) { require(!destination.exists(),"Output already exists; choose a new WAV path"); auto scheduled=midiFile==juce::File{} ? std::vector{ {0,juce::MidiMessage::noteOn(1,60,static_cast(100))}, {12000,juce::MidiMessage::noteOn(1,64,static_cast(95))}, {24000,juce::MidiMessage::noteOn(1,67,static_cast(90))}, {48000,juce::MidiMessage::allNotesOff(1)}} : readMidi(midiFile); require(!scheduled.empty(),"MIDI file has no supported events"); const int64_t total=scheduled.back().frame+24000+3*48000; XtcJuceProcessor processor; set(processor,"effects",1); set(processor,"gain",-6); prepare(processor); auto name=(destination.getParentDirectory().getFullPathName()+"/.xtc-juce-XXXXXX").toStdString(); std::vector temporary(name.begin(),name.end()); temporary.push_back(0); const int fd=mkstemp(temporary.data()); require(fd>=0,"Cannot create output temporary file"); close(fd); struct Cleanup { juce::File file; ~Cleanup(){file.deleteFile();} } cleanup{juce::File(temporary.data())}; std::unique_ptr stream=std::make_unique(cleanup.file); juce::WavAudioFormat format; auto writer=format.createWriterFor(stream,juce::AudioFormatWriterOptions{}.withSampleRate(48000) .withNumChannels(2).withBitsPerSample(32).withSampleFormat(juce::AudioFormatWriterOptions::SampleFormat::floatingPoint)); require(writer!=nullptr,"Cannot open WAV writer"); size_t index=0; for (int64_t base=0;base(std::min(256,total-base)); juce::AudioBuffer audio(2,length); juce::MidiBuffer midi; while (index(scheduled[index].frame-base)); ++index; } // Force release at the start of the fixed tail, including files with missing note-offs. if (base<=total-3*48000 && base+length>total-3*48000) for (int channel=1;channel<=16;++channel) midi.addEvent(juce::MidiMessage::allNotesOff(channel),static_cast(total-3*48000-base)); processor.processBlock(audio,midi); require(!processor.processingFailed(),"xtc processing failed"); require(processor.droppedEvents()==0,"MIDI density exceeds 256 supported events per 256-frame chunk"); require(writer->writeFromAudioSampleBuffer(audio,0,length),"WAV write failed"); } require(writer->flush(),"WAV finalization failed"); writer.reset(); // Atomic no-clobber publication on the qualified POSIX platform. require(::link(cleanup.file.getFullPathName().toRawUTF8(),destination.getFullPathName().toRawUTF8())==0, "Cannot publish WAV (destination may already exist)"); std::cout << "Rendered " << total << " stereo float32 frames at 48000 Hz to " << destination.getFullPathName() << '\n'; } } int main(int argc,char** argv) { try { juce::ScopedJuceInitialiser_GUI juceRuntime; if (argc==2 && juce::String(argv[1])=="--self-test") { selfTest(); return 0; } if (argc==3 && (juce::String(argv[1])=="--inspect-vst3" || juce::String(argv[1])=="--host-check")) { inspectPlugin(juce::File::getCurrentWorkingDirectory().getChildFile(argv[2]),juce::String(argv[1])=="--host-check"); return 0; } if (argc==3 && juce::String(argv[1])=="--snapshot") { snapshot(juce::File::getCurrentWorkingDirectory().getChildFile(argv[2])); return 0; } if ((argc==3 || argc==5) && juce::String(argv[1])=="--render") { require(argc==3 || juce::String(argv[3])=="--midi","Expected --midi FILE.mid"); render(juce::File::getCurrentWorkingDirectory().getChildFile(argv[2]), argc==5 ? juce::File::getCurrentWorkingDirectory().getChildFile(argv[4]) : juce::File{}); return 0; } std::cerr << "Usage: xtc-juce-render --self-test | --snapshot NEW.png | --render NEW.wav [--midi FILE.mid] | --inspect-vst3 BUNDLE | --host-check EXAMPLE_BUNDLE\n"; return 2; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } }