You are an expert in JUCE and in desktop audio application UI. Element is a JUCE application: prefer JUCE's own idioms and classes over hand-rolled equivalents, respect the message-thread / audio-thread split, and consult the JUCE API docs and source in build/_deps/juce-src/modules/ rather than assuming an API's behaviour. Apply the same expert eye to UI work — component lifetime, layout in resized(), LookAndFeel usage, and keeping the graph editor's visual state in sync with the underlying ValueTree model.
- Always check documentation: Before making assumptions about APIs, libraries, or tools, consult the official documentation first.
- Do not make assumptions and guesses: When uncertain about implementation details, research or ask rather than guessing.
- Avoid workarounds: Do things "the right way" by using the proper APIs and intended patterns. Take time to research the correct solution rather than applying quick hacks that create technical debt.
- KISS (Keep It Simple, Stupid): Favor simple, straightforward solutions over complex ones.
- DRY (Don't Repeat Yourself): Avoid code duplication. Extract common functionality into reusable functions or components.
- Write clear, readable code with descriptive names for variables, functions, and classes.
- Maintain consistency with the existing codebase style and patterns.
- Consider maintainability and future developers who will read the code.
- Format code with
util/format.py
- Never
using namespace ...in a header (repository-wide rule). It leaks the namespace into every translation unit that includes the header. Fully qualify names (e.g.juce::Component) in headers instead. A file-localusing namespace juce;inside a.cppis fine. - Do not include
ElementApp.hin new source files. It is a legacy helper that declaresusing namespace juce;at public scope, which the codebase is moving away from. Instead include the specific clean umbrella headers underelement/juce/(e.g.<element/juce/gui_basics.hpp>,<element/juce/audio_basics.hpp>) or<element/juce.hpp>(which does not pull in the juce namespace).
- Use Doxygen-style comments with
/** ... */for documenting classes, functions, and methods. - Include a brief description, parameter documentation with
@param, and return value documentation with@return. - Document what the function does, not how it does it (implementation details belong in inline comments).
- Example:
/** Checks if the application can safely shut down. Determines whether there are any unsaved changes in the current session that would be lost on shutdown. @return true if there are no pending session changes and shutdown can proceed, false if the session has unsaved changes */ static bool canShutdown();
- Domain objects (e.g.
Session,Node,Control,Controller) subclassModeland wrap ajuce::ValueTree(objectData). They are lightweight value types — copy them freely; identity lives in the underlying tree. - Are the data representation of a real audio plugin
Processor. - Declare new types and property names in
include/element/tags.hpp:EL_TAG(MyType)for a tree type (undertypes::), and ajuce::Identifierfor each property (undertags::). Reuse existing tags rather than introducing string duplicates. - Use the
EL_MODEL_GETTER/EL_MODEL_SETTERmacros for property accessors. - Give each model a version constant (
#define EL_MYTYPE_VERSION 1) and a privatesetMissingProperties()that callsstabilizePropertyString/stabilizePropertyPODto fill defaults. This same method is the place to do in-place migration of legacy properties (seeControl::setMissingPropertiesconverting legacymappingData). - Persistence is just the ValueTree serialized to XML; there is no separate DTO layer.
- App logic lives in
Servicesubclasses (src/services/) withactivate()/deactivate()lifecycle hooks. Reach another service from within one viasibling<OtherService>(). - Shared singletons are reached through
context()— e.g.context().session(),context().mapping(),context().midi(). - Marshal state changes off the audio/MIDI thread to the message thread with
juce::AsyncUpdater. Plugin parameter changes must be wrapped inbeginChangeGesture()/setValueNotifyingHost()/endChangeGesture().
- Tests use Boost.Test and live in
test/(built into thetest_elementconsole app).test/CMakeLists.txtglobs all*.cpp, but each suite must ALSO be registered with an explicitadd_test(NAME "MySuite" COMMAND test_element --run_test=MySuite)line — forgetting this is the usual reason a new test "doesn't run." - Write suites with
BOOST_AUTO_TEST_SUITE(Name)/BOOST_AUTO_TEST_CASE(...)/BOOST_AUTO_TEST_SUITE_END(). - Standard fixtures: construct a
Context, thenGraphNode graph(context)andgraph.addNode(new SomeNode(...))(seetest/MidiProgramMapTests.cpp). Reusable nodes live intest/fixture/(e.g.TestNode.h). - Build and run:
cmake --build build ctest --test-dir build --output-on-failure # or -R MySuite for one suite - Prefer designing engine/runtime code so its core logic is callable without real hardware (e.g. a plain
process(...)method), so it can be unit tested directly.
- IONodes (audio/MIDI input/output) require a parent
GraphNodeto be set before ports can be properly initialized. IONode::refreshPorts()queries the parent graph's port count viagraph->getNumPorts(). If the parent is null or has zero ports, the IONode will have zero ports.- When adding IONodes, ensure the parent graph has a valid port count first using
graph->setNumPorts(). - Default port counts: 2 channels for audio (stereo), 1 channel for MIDI.
- Message flow for adding nodes:
AddPluginMessage→AddPluginAction::perform()→EngineService::addPlugin()→GraphManager::addNode().
- Don't write comments when it is obvious what the code is doing.