mirror of
https://codeberg.org/armin/monoslicer.git
synced 2026-10-11 13:01:54 +02:00
215 lines
No EOL
7.5 KiB
C++
215 lines
No EOL
7.5 KiB
C++
// Regression test: dropping an audio file onto the plugin editor.
|
|
//
|
|
// Drag and drop discovery is driven entirely by RTTI. JUCE's
|
|
// ComponentPeer::DragHelpers::findDragAndDropTarget walks up the component
|
|
// chain and does dynamic_cast<FileDragAndDropTarget*> on each candidate, so the
|
|
// interface only has to be visible as a *public* base. Inheriting it privately
|
|
// makes the cast return null and the drop handlers are never called - silently,
|
|
// with no warning at build time.
|
|
//
|
|
// Build and run:
|
|
// cmake --build build --target MonoslicerDnDTest
|
|
// ./build/MonoslicerDnDTest_artefacts/Release/MonoslicerDnDTest
|
|
|
|
#include "../Source/PluginEditor.h"
|
|
#include "../Source/PluginProcessor.h"
|
|
|
|
#include <iostream>
|
|
|
|
namespace
|
|
{
|
|
int failures = 0;
|
|
|
|
void check (bool condition, const juce::String& what)
|
|
{
|
|
std::cout << (condition ? " \033[32mPASS\033[0m " : " \033[1;31mFAIL\033[0m ")
|
|
<< what << std::endl;
|
|
|
|
if (!condition)
|
|
++failures;
|
|
}
|
|
|
|
// Mirrors JUCE's DragHelpers::findDragAndDropTarget (juce_ComponentPeer.cpp).
|
|
// Returns the winning *component* rather than a FileDragAndDropTarget* so that
|
|
// this compiles - and fails at runtime rather than at build time - even when the
|
|
// interface is inherited privately. That is precisely the regression being
|
|
// guarded against: JUCE casts from Component*, which is always legal.
|
|
juce::Component* findDragTargetComponent (juce::Component* c,
|
|
const juce::StringArray& files)
|
|
{
|
|
for (; c != nullptr; c = c->getParentComponent())
|
|
{
|
|
if (auto* target = dynamic_cast<juce::FileDragAndDropTarget*> (c))
|
|
if (target->isInterestedInFileDrag (files))
|
|
return c;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Mirrors the host structure: JUCE's VST3 wrapper puts the plugin editor inside a
|
|
// ContentWrapperComponent and adds *that* to the desktop
|
|
// (juce_audio_plugin_client_VST3.cpp:1996). The peer therefore knows nothing about
|
|
// the editor directly - it can only reach it by walking the component chain.
|
|
struct HostWrapper final : juce::Component
|
|
{
|
|
explicit HostWrapper (juce::Component& child)
|
|
{
|
|
addAndMakeVisible (child);
|
|
setSize (child.getWidth(), child.getHeight());
|
|
}
|
|
|
|
void paint (juce::Graphics& g) override { g.fillAll (juce::Colours::black); }
|
|
};
|
|
|
|
void dumpChain (juce::Component* c)
|
|
{
|
|
std::cout << " chain:";
|
|
|
|
for (; c != nullptr; c = c->getParentComponent())
|
|
std::cout << " \"" << juce::String (typeid (*c).name()).upToLastOccurrenceOf ("::", true, false)
|
|
<< "\""
|
|
<< (c->isVisible() ? "" : "(hidden)")
|
|
<< (dynamic_cast<juce::FileDragAndDropTarget*> (c) != nullptr ? "[fdnd]" : "")
|
|
<< " " << c->getWidth() << "x" << c->getHeight();
|
|
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
// The editor scales itself via setTransform() in resized(); at the base size
|
|
// that transform is the identity, which keeps pointer maths straightforward.
|
|
constexpr int baseWidth = 920;
|
|
constexpr int baseHeight = 606;
|
|
|
|
juce::File writeTestWav()
|
|
{
|
|
auto file = juce::File::getSpecialLocation (juce::File::tempDirectory)
|
|
.getChildFile ("monoslicer_dnd_test.wav");
|
|
|
|
constexpr double sampleRate = 44100.0;
|
|
constexpr int numSamples = 4096;
|
|
|
|
juce::AudioBuffer<float> buffer (1, numSamples);
|
|
|
|
for (int i = 0; i < numSamples; ++i)
|
|
buffer.setSample (0, i, std::sin (i / 20.0f));
|
|
|
|
juce::WavAudioFormat format;
|
|
std::unique_ptr<juce::FileOutputStream> out (file.createOutputStream());
|
|
|
|
if (out == nullptr)
|
|
{
|
|
std::cout << " \033[1;31mFAIL\033[0m could not create " << file.getFullPathName() << std::endl;
|
|
++failures;
|
|
return {};
|
|
}
|
|
|
|
{
|
|
// The writer has to be destroyed before the stream it writes to.
|
|
std::unique_ptr<juce::AudioFormatWriter> writer (
|
|
format.createWriterFor (out.get(), sampleRate, 1, 16, {}, 0));
|
|
|
|
if (writer == nullptr)
|
|
{
|
|
std::cout << " \033[1;31mFAIL\033[0m could not create a WAV writer" << std::endl;
|
|
++failures;
|
|
return {};
|
|
}
|
|
|
|
writer->writeFromAudioSampleBuffer (buffer, 0, numSamples);
|
|
}
|
|
|
|
out.release(); // commits and closes the file
|
|
|
|
if (file.getSize() == 0)
|
|
{
|
|
std::cout << " \033[1;31mFAIL\033[0m test WAV was written empty" << std::endl;
|
|
++failures;
|
|
return {};
|
|
}
|
|
|
|
return file;
|
|
}
|
|
}
|
|
|
|
int main()
|
|
{
|
|
juce::ScopedJuceInitialiser_GUI juceInitialiser;
|
|
|
|
std::cout << "\n\033[1mMonoslicer - file drag and drop\033[0m\n\n";
|
|
|
|
const auto wavFile = writeTestWav();
|
|
|
|
if (!wavFile.existsAsFile())
|
|
return 1;
|
|
|
|
const auto path = wavFile.getFullPathName();
|
|
const juce::Point<int> pointerPos (400, 300);
|
|
|
|
MonoslicerProcessor processor;
|
|
MonoslicerEditor editor (processor);
|
|
editor.setSize (baseWidth, baseHeight);
|
|
|
|
HostWrapper wrapper (editor);
|
|
wrapper.addToDesktop (0);
|
|
wrapper.setVisible (true);
|
|
|
|
auto* peer = wrapper.getPeer();
|
|
|
|
std::cout << "\n\033[1mRTTI visibility\033[0m\n";
|
|
check (dynamic_cast<juce::FileDragAndDropTarget*> (static_cast<juce::Component*> (&editor)) != nullptr,
|
|
"editor exposes FileDragAndDropTarget as a public base");
|
|
|
|
std::cout << "\n\033[1mAcceptance\033[0m\n";
|
|
check (editor.isInterestedInFileDrag ({ path }), "accepts a .wav file");
|
|
check (editor.isInterestedInFileDrag ({ "/tmp/notes.txt" }) == false, "rejects a .txt file");
|
|
|
|
std::cout << "\n\033[1mTarget discovery (as used by JUCE)\033[0m\n";
|
|
auto* underPointer = peer != nullptr ? peer->getComponent().getComponentAt (pointerPos)
|
|
: wrapper.getComponentAt (pointerPos);
|
|
|
|
if (underPointer == nullptr)
|
|
std::cout << " (no component under the pointer)" << std::endl;
|
|
|
|
dumpChain (underPointer);
|
|
|
|
check (findDragTargetComponent (underPointer, { path }) == static_cast<juce::Component*> (&editor),
|
|
"lookup from the component under the pointer finds the editor");
|
|
check (findDragTargetComponent (underPointer, { "/tmp/notes.txt" }) == nullptr,
|
|
"lookup for an unsupported file finds no target");
|
|
|
|
std::cout << "\n\033[1mEnd-to-end drop via a real native peer\033[0m\n";
|
|
|
|
if (peer != nullptr)
|
|
{
|
|
juce::ComponentPeer::DragInfo info;
|
|
info.files.add (path);
|
|
info.position = pointerPos;
|
|
|
|
check (peer->handleDragMove (info), "peer accepts the drag (handleDragMove)");
|
|
check (peer->handleDragDrop (info), "peer accepts the drop (handleDragDrop)");
|
|
|
|
// handleDragDrop() defers filesDropped() through MessageManager::callAsync
|
|
juce::MessageManager::getInstance()->runDispatchLoopUntil (250);
|
|
|
|
check (processor.getSampleBuffer().getNumSamples() == 4096,
|
|
"the dropped sample was decoded into the processor");
|
|
check (processor.getCurrentFile() == wavFile,
|
|
"the dropped file is recorded as the current file");
|
|
// Note: dropping does not auto-slice - that is a separate explicit action,
|
|
// and matches the behaviour of the "Load" button.
|
|
check (processor.getSampleRateLoaded() == 44100.0,
|
|
"the reader set the sample rate of the dropped file");
|
|
}
|
|
else
|
|
{
|
|
std::cout << " \033[33mSKIP\033[0m no native peer available (headless session)"
|
|
<< std::endl;
|
|
}
|
|
|
|
std::cout << "\n" << (failures == 0 ? "\033[32;1mAll checks passed.\033[0m"
|
|
: juce::String (failures) + " check(s) failed.")
|
|
<< "\n\n";
|
|
|
|
return failures == 0 ? 0 : 1;
|
|
} |