JUCE SDK integration.
This commit is contained in:
parent
8e09f58944
commit
adcbcdfd5e
16 changed files with 1057 additions and 0 deletions
243
examples/juce/Render.cpp
Normal file
243
examples/juce/Render.cpp
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#include "Processor.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
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<float> silence(channels,1); juce::MidiBuffer empty;
|
||||
processor.processBlock(silence,empty); processor.reset();
|
||||
}
|
||||
void selfTest()
|
||||
{
|
||||
XtcJuceProcessor processor; prepare(processor);
|
||||
juce::AudioBuffer<float> audio(2,513); juce::MidiBuffer midi;
|
||||
midi.addEvent(juce::MidiMessage::noteOn(3,69,static_cast<juce::uint8>(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<float,256> expected{};
|
||||
const std::array<xtc_midi_event,3> 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<xtc_midi_event,3> block{}; size_t count=0;
|
||||
const int length=std::min(256,513-base);
|
||||
for (const auto& event : source) if (event.offset>=static_cast<unsigned int>(base) && event.offset<static_cast<unsigned int>(base+length)) {
|
||||
block[count]=event; block[count++].offset-=static_cast<unsigned int>(base);
|
||||
}
|
||||
require(xtc_plugin_process_midi_at(reference,block.data(),count,expected.data(),static_cast<size_t>(length),static_cast<uint64_t>(base))==XTC_OK,"Reference render failed");
|
||||
for (int i=0;i<length;++i) for (int channel=0;channel<2;++channel)
|
||||
require(audio.getSample(channel,base+i)==expected[static_cast<size_t>(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<juce::uint8>(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<float> large(2,4097); midi.addEvent(juce::MidiMessage::noteOn(1,65,static_cast<juce::uint8>(100)),0);
|
||||
whole.processBlock(large,midi);
|
||||
for (int base=0;base<4097;base+=256) {
|
||||
const int length=std::min(256,4097-base); juce::AudioBuffer<float> part(2,length);
|
||||
if (base==0) midi.addEvent(juce::MidiMessage::noteOn(1,65,static_cast<juce::uint8>(100)),0);
|
||||
split.processBlock(part,midi);
|
||||
for (int i=0;i<length;++i) require(large.getSample(0,base+i)==part.getSample(0,i),"Block-size-dependent synthesis");
|
||||
}
|
||||
// Roundtrip all parameters, reject malformed/incomplete/out-of-range presets.
|
||||
set(processor,"gain",-12); set(processor,"cutoff",2300); set(processor,"effects",1);
|
||||
juce::MemoryBlock state; processor.getStateInformation(state);
|
||||
XtcJuceProcessor restored; restored.setStateInformation(state.getData(),static_cast<int>(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<int>(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<juce::uint8>(100)),0);
|
||||
juce::AudioBuffer<float> 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<Scheduled> 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<Scheduled> events;
|
||||
for (int track=0;track<file.getNumTracks();++track) {
|
||||
const auto* sequence=file.getTrack(track);
|
||||
for (int i=0;i<sequence->getNumEvents();++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<int64_t>(std::llround(seconds*48000)),message});
|
||||
}
|
||||
}
|
||||
std::stable_sort(events.begin(),events.end(),[](const auto& a,const auto& b){return a.frame<b.frame;});
|
||||
return events;
|
||||
}
|
||||
void inspectPlugin(const juce::File& bundle,bool verifyExample)
|
||||
{
|
||||
require(bundle.exists(),"Selected VST3 bundle does not exist");
|
||||
juce::AudioPluginFormatManager manager;
|
||||
auto format=std::make_unique<juce::VST3PluginFormat>();
|
||||
auto* vst3=format.get(); manager.addFormat(std::move(format));
|
||||
juce::OwnedArray<juce::PluginDescription> 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<int>(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<float> audio(2,256); juce::MidiBuffer midi;
|
||||
midi.addEvent(juce::MidiMessage::noteOn(1,69,static_cast<juce::uint8>(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<juce::AudioProcessorEditor> 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<Scheduled>{
|
||||
{0,juce::MidiMessage::noteOn(1,60,static_cast<juce::uint8>(100))},
|
||||
{12000,juce::MidiMessage::noteOn(1,64,static_cast<juce::uint8>(95))},
|
||||
{24000,juce::MidiMessage::noteOn(1,67,static_cast<juce::uint8>(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<char> 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<juce::OutputStream> stream=std::make_unique<juce::FileOutputStream>(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<total;base+=256) {
|
||||
const int length=static_cast<int>(std::min<int64_t>(256,total-base));
|
||||
juce::AudioBuffer<float> audio(2,length); juce::MidiBuffer midi;
|
||||
while (index<scheduled.size() && scheduled[index].frame<base+length) {
|
||||
midi.addEvent(scheduled[index].message,static_cast<int>(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<int>(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; }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue