DiffScope is a C++20/Qt 6 singing-voice editor. Keep src/app a thin launcher; product behavior belongs in application libraries or ChorusKit plugins.
src/app/: executable entry point, application metadata, icons, and platform configuration.src/libs/application/: reusable first-party libraries.loadapihandles startup hooks,uishellprovides the shared Qt Quick shell, andtransactionalprovides undoable transaction support.src/plugins/<plugin>/: built-in plugins such ascoreplugin,audio,visualeditor, andsynth. A normal plugin containsCMakeLists.txt,plugin.json.in, C++ implementation,qml/, andres/(actions, icons, and translations).src/plugins/coreplugin/: the public application-level API and common project, window, singer, notification, settings, and editing services.src/share/: shared install/deployment rules used after ChorusKit finishes configuring the application.src/libs/3rdparty/: Git submodules. Treat these as upstream projects; do not mix ordinary feature changes with submodule edits.src/libs/application/uishell/tests/: standalone visual/smoke applications.src/tests/is reserved for application-wide tests.scripts/vcpkg-manifest/andscripts/vcpkg/: dependency manifest and overlay ports/triplets.scripts/ci/is the packaging reference.dist/contains installers.
Never commit generated files from build/, cmake-build-*, out/, .qm, MOC, QML cache, or synchronized include directories.
Do not create any documents inside the project directory unless explicitly requested by the user. If documentation is necessary without such a request, create it outside the project directory and inform the user of its location.
Extend the closest existing plugin before creating a new one. New built-in plugins must use diffscope_add_builtin_plugin, declare project(<lowercase-target>), provide plugin.json.in, add their subdirectory in src/plugins/CMakeLists.txt, and keep CMake LINKS consistent with metadata Dependencies. Plugin IDs use reverse DNS (org.diffscope.audio); QML URIs use DiffScope.<PascalCaseName>.
Plugins derive from ExtensionSystem::IPlugin. Create self-contained services in initialize(), perform cross-plugin wiring only in extensionsInitialized(), and reserve delayedInitialize() for deferrable work. Per-window behavior belongs in a WindowInterfaceAddOn; register shared/per-window objects in the appropriate ChorusKit object pool and retrieve them through typed APIs such as getFirstObject<T>(). Do not reach into another plugin's source internal/ tree.
Do not build parallel service, action, theming, document, or audio frameworks.
- ChorusKit / ExtensionSystem / CoreApi: use the plugin lifecycle,
RuntimeInterface(QML engine, user/global settings, application services),CoreInterfaceBase, window interfaces, registries, and object pools. - QActionKit: declare action IDs and menu/toolbar insertions in
res/org.diffscope.<plugin>_actions.xml. Bind behavior with QMLActionCollection/ActionItem, then register it with the window'sactionContext. UseCore::CoreInterface::actionRegistry()for global action metadata and layouts. Do not hand-create duplicate shortcuts or menus. - SVSCraft: use its QML controls,
Theme,ThemedItem, dialogs, icons, settings helpers, and music time/timeline types. New UI should visually and behaviorally match these components rather than introducing local substitutes. Avoid hardcoding color values and use colors defined inTheme. - Core plugin: use
CoreInterfacefor application commands and registries;HomeWindowInterface/ProjectWindowInterfacefor window-scoped behavior;ProjectDocumentContextfor open/save;DspxDocumentfor model, selection, clipboard, andTransactionController;ProjectTimelinefor playhead/time mapping; and existing scenarios, notifications, singer registries, default-lyric services, and property-editor hooks. - Domain libraries: use
dspxmodel*for the live document/selection model, OpenDSPX for serialization/interchange, ScopicFlow for editor view models and interaction controllers, and TALCS/audio-plugin contexts for audio. Keep UI coordinates, document ticks, and audio samples in their owning layers and convert at boundaries.
Unless explicitly requested, do not account for compatibility with existing persisted data or interchange data formats. When changing such data or formats, do not update their header metadata or version numbers.
When asked to modify a feature, treat the requested result as the only current behavior. Unless the user explicitly requests otherwise, do not leave any remnants of the old behavior in code, comments, or documentation. Remove obsolete implementations, branches, APIs, settings, names, examples, compatibility paths, migration notes, comparisons, historical context, and explanations of why the feature was changed.
CMake's ck_sync_include publishes headers into the build include tree and deliberately hides most source-directory nesting. Include the published path, never src/... or a relative path across directories:
| Source example | Consumer include |
|---|---|
src/plugins/coreplugin/project/document/DspxDocument.h |
<coreplugin/DspxDocument.h> |
src/plugins/audio/audiocontext/ProjectAudioContext_p.h |
<audio/private/ProjectAudioContext_p.h> |
src/plugins/audio/internal/addon/PlaybackAddOn.h |
<audio/internal/PlaybackAddOn.h> |
src/plugins/maintenance/internal/ApplicationUpdateChecker_p.h |
<maintenance/internal/private/ApplicationUpdateChecker_p.h> |
src/libs/application/uishell/src/BubbleNotificationHandle.h |
<uishell/BubbleNotificationHandle.h> |
Public and private headers are flattened to <target>/ and <target>/private/; implementation headers are flattened to <target>/internal/ (or its private/ child). Consequently, header basenames must be unique within each publication class of a target. An internal header is build-private and must not become a cross-plugin API merely because it has a synchronized path.
In every header, use angle brackets for all includes, including first-party headers. In a .cpp/.mm, the matching public header and, when present, its private _p.h partner are the only quoted includes, written as plain relative filenames (#include "Class.h" then #include "Class_p.h", public before private). In a Class_p.h, the corresponding public header must be the first include, alone in its own standalone group. Every other header—including same-directory siblings and same-plugin headers—uses its synchronized angle-bracket path. Generated moc_*.cpp inclusions at the end of a source file are not headers and remain quoted.
Platform-split implementation files that share one class header (for example Class_icu.cpp and Class_NSString.mm both including Class.h) treat that shared header as their matching header. Generated headers outside the synchronized include tree, such as protobuf *.qpb.h outputs, are exempt from the synchronized-path rule and may remain quoted.
Separate include groups with one blank line and let .clang-format sort each group case-sensitively:
- Matching headers: the file's public header, then its
_p.hprivate header (.cpp/.mmonly). In a_p.h, the corresponding public header takes this place. - Platform/C and C++ standard library headers, then Boost.
- Qt public headers, Qt QPA headers, then Qt private headers.
- Frameworks in the configured order:
CoreApi, ExtensionSystem, OpenDSPX, QActionKit, QWindowKit, SVSCraft, ScopicFlow, then TALCS. - Application layers:
dspxmodel*,loadapi,uishell,coreplugin, then the current plugin and otherwise uncategorized headers.
#include "AudioExporter.h"
#include "AudioExporter_p.h"
#include <algorithm>
#include <memory>
#include <QFile>
#include <QString>
#include <CoreApi/runtimeinterface.h>
#include <SVSCraftQuick/MessageBox.h>
#include <coreplugin/ProjectWindowInterface.h>Include what the file uses and prefer forward declarations in headers when a complete type is unnecessary.
.clang-format is authoritative: four spaces, no tabs, attached braces, right-aligned pointers/references, LF endings, and no column limit. Format changed C/C++/Objective-C++ files with clang-format -i <files>.
- Use
PascalCase.h/.cppmatching the primary type. Module-global headers are the established lowercase exception, for examplecoreglobal.h. - Use
PascalCasetypes and QML components,lowerCamelCasefunctions/properties/locals, andm_lowerCamelCasedirect data members. PImpl members have nom_prefix. - Match established namespace and acronym spelling (
Core,Audio::Internal,Dspx,MIDI) instead of inventing variants. - Use
#ifndefguards such asDIFFSCOPE_AUDIO_AUDIOEXPORTER_H; do not introduce#pragma once. - Use the target export macro on public ABI classes (
CORE_EXPORT,AUDIO_EXPORT, and so on). - Use full Qt forms:
Q_SIGNALS,Q_SLOTS, andQ_EMIT, notsignals,slots, or bare signal calls when emitting. - Make single-argument constructors
explicit, mark overrides, initialize pointers, and use QObject parent ownership where appropriate. - Prefer C++20 ranges and
<=>where they make code clearer. Do not use exceptions for normal control flow; catch only at APIs that can throw, translate the failure there, and preserve useful diagnostics. - Unless explicitly required, never declare any classes as
final.
Treat a QML component identified by a fixed module URI and type name as a required application component unless the surrounding code or the user's requirement provides explicit evidence of fallback behavior.
QQmlComponent component(engine, "Module.Package", "ComponentName");
if (component.isError()) {
qFatal() << component.errorString();
}
QObject *object = component.createWithInitialProperties(initialProperties);
if (!object) {
qFatal() << component.errorString();
}
ExpectedClass *typedObject = qobject_cast<ExpectedClass *>(object);
if (!typedObject) {
qFatal("ComponentName in Module.Package is not ExpectedClass");
}- Check
isError()immediately after constructing aQQmlComponentfrom a fixed module URI and type name. Report the failure only withqFatal() << component.errorString(). Do not add prefixes, developer guidance, user-facing dialogs, or alternate diagnostics. - If the same code path immediately calls
create()orcreateWithInitialProperties(), retain the rawQObject *and check it before dereferencing it, assigning a parent, reading a property, or calling a method. Report a null result only withqFatal() << component.errorString(). - If the created object requires a specific C++ type, perform the null check before
qobject_cast. Report a failed cast with the exact messageComponentName in Module.Package is not ExpectedClass, using the actual QML component name, module URI, and C++ class name. Q_ASSERT,qWarning,qCritical, user-facing messages, and null returns do not replace these fatal checks.- The load check applies even if the component is subsequently passed to another object. Creation and type checks apply only when creation occurs immediately in the same code path. Do not trace components through helper parameters, members, registries, action contexts, or other indirect creation paths.
- Preserve an existing nonfatal path only if the code explicitly implements fallback behavior for that component.
These rules apply to comments, documentation, README files, help text, and diagnostic messages.
- Use formal technical writing. Comments and documentation are normative text, not narration. Each sentence states a fact, a constraint, or a reason, without preamble, impressions, or rhetoric.
- Do not personify. Code, files, formats, programs, and tests are not subjects with a will. Do not write says, tells, knows, asks, wants, means, cares, decides, promises, or is told. Use returns, indicates, records, specifies, reports, detects, requires, or rejects instead. Real actors such as the user, the author, or the caller may be subjects.
- Use terms, not descriptive paraphrases. Write invalid byte sequence, not bytes that do not decode. Write unpaired surrogate, reverse mapping, and unrepresentable character, not half of one, the way back, and what cannot be spelled. If no common term exists, define one at its first use and keep that name throughout.
- Headings, group names, and list labels are nouns or noun phrases. Write Motivation, Behavior, and Rationale, not Why, What it does, and How it works. If a noun works, do not use a what-clause as a subject or object. Write the requested encoding, not what the user asked for.
- Use if for conditions and where only for places. Write empty if absent, not empty where there is none. Do not use one to refer back to a noun (such a one, the one it wants). Repeat the noun.
- Avoid colloquial phrases such as whatever else, for good, as it stands, on its own, on the way out, at a glance, there and back, is given up on, the rest of why, and and all. Use precise written expressions instead.
- Write complete sentences. Do not write fragments, comma splices (two independent clauses joined only by a comma), or rhetorical questions. Do not start a causal statement with So, And so, Which is why, That is why, or Hence. State the cause in the same sentence with because or therefore. Do not address the reader in the second person.
- Do not use dashes, and do not join clauses with semicolons. Start a new sentence wherever a sentence break belongs. Use American spelling.
- Function documentation starts with a verb (Returns, Decodes, Reads, Rejects). State the return value for each case. Boolean queries read Returns whether ….
- Do not write a paragraph for what a few words can say. Comments explain constraints, ownership, lifetimes, and the reason the implementation must take its current form. Do not restate what the signature already shows.
- Do not write archaeological comments. Keep the reason the code must be this way now. Remove how it used to be and how it was fixed.
- The same rules apply to Chinese text. Use written language, not colloquial words such as 「别」「搞」「就行」「得(表必须)」「啥」「拿来」「反正」「其实」「说白了」「这玩意儿」. Headings use 「动机」「设计理由」「实现方式」, not 「为什么」「怎么做」. Normative imperatives such as 「不要」「必须」 are not colloquial and remain in use.
Exported QObject-based interfaces and other stable public APIs use Qt-style PImpl. Keep Class.h, Class.cpp, and Class_p.h together in the source tree; consumers see the private header as <module/private/Class_p.h>. The public class forward-declares ClassPrivate, uses Q_DECLARE_PRIVATE(Class), owns QScopedPointer<ClassPrivate> d_ptr, and has an out-of-line destructor. The private class uses Q_DECLARE_PUBLIC(Class) and stores Class *q_ptr. Use Q_D/Q_Q in implementations. Data/value types may use QSharedDataPointer when implicit sharing is intended.
// Widget.h
class MODULE_EXPORT Widget : public QObject {
Q_OBJECT
Q_DECLARE_PRIVATE(Widget)
public:
explicit Widget(QObject *parent = nullptr);
~Widget() override;
private:
QScopedPointer<WidgetPrivate> d_ptr;
};
// Widget.cpp
#include "Widget.h"
#include "Widget_p.h"Internal classes under internal/ do not need PImpl; store their members directly with m_ names. C++ types meant only for QML may use Type_p.h as their primary header; if such a type needs a private implementation/attached helper, use Type_p_p.h. QML property notify signals are normally parameterless; regular C++ API signals may carry the new value. Keep declarations ordered as Qt macros/properties, public API, signals/slots, protected API, then private storage.
Prefer ChorusKit registries/object pools over adding a singleton. If lifecycle ownership requires one, assert that no instance exists in the constructor, assign the static instance there, and clear it in the destructor; these objects are main-thread services. Restrict construction with a private/protected constructor and plugin friend where appropriate. A QML singleton additionally uses QML_SINGLETON and a static create(QQmlEngine *, QJSEngine *) factory.
Use four-space QML indentation. Name files/types PascalCase.qml, IDs and properties lowerCamelCase, and declare injected dependencies as required property. Group imports as Qt, QActionKit/other framework modules, SVSCraft, then DiffScope modules. Prefer layouts and anchors over fixed coordinates.
Avoid redundant control property declarations. Determine the effective defaults from the Qt base type, the SVSCraft control, and any project component in the inheritance chain.
- Omit properties that repeat effective defaults unless a specific interaction requires an explicit declaration. Examples include
ToolButton.flat: true,SpinBox.editable: true,DoubleSpinBox.editable: true, andDescriptiveText.activated: hoveredon controls that already provide that binding. - Layout settings, including padding, spacing, orientation, alignment, and font, may (but not must) explicitly repeat default values. Icon dimensions are an exception. Omit
icon.widthandicon.heightif they match the effective defaults, including the SVSCraft default of 16. - Set
ToolButton.focusPolicyonly if a specific interaction requires an explicit focus policy. Differences between platform defaults alone do not justify a declaration. - Button types derived from
AbstractButtonobtain their accessible names fromtext. Do not repeat the same value inAccessible.name, including through an intermediate property or an equivalent expression. Provide an explicit name only if it adds distinct semantics or the control has no suitable text. Field labels and custom content require separate consideration. - SVSCraft
ButtonandToolButtonprovide automatic action tooltips for nonemptytextinAbstractButton.IconOnlymode. Do not duplicate these tooltips withToolTip.visible: hovered,ToolTip.text: text, or an equivalentDescriptiveTextconfiguration. - Use
DescriptiveTextfor additional tooltip, status, and context-help text. If the accessible description should use the tooltip and status text, explicitly enableDescriptiveText.bindAccessibleDescriptioninstead of maintaining a duplicateAccessible.descriptionbinding. This binding is disabled by default.DescriptiveTextdoes not supply the accessible name. - Preserve custom tooltip content and activation conditions, including keyboard focus, disabled-state explanations, and text truncation. Ensure that custom tooltips do not duplicate the control's automatic action tooltip.
All user-visible C++ strings use tr() and QML strings use qsTr(). Action text belongs in the action XML so QActionKit can translate and reuse it. Commit source .ts updates under the owning module's res/translations/; .qm files are generated. After changing translatable text, run:
cmake --build build --target <plugin>_translations_lupdate
cmake --build build --target <plugin>_translations_lreleaseWrite user-facing window, dialog, and page titles, button text, and menu text in Title Case. Use sentence case for all other general user-facing text. Format numbers for the user's locale unless they are special identifiers such as HTTP status codes or require a defined representation such as timecodes. Use localized placeholders such as %L1 and %Ln, QLocale::toString() in C++, or toLocaleString() in JavaScript/QML instead of inserting locale-neutral numeric strings. Describe concepts, actions, and results from the user's business perspective; do not over-explain internal implementation details in user-facing copy.
Use RuntimeInterface::settings() for user preferences and RuntimeInterface::globalSettings() only for machine-wide state. Group C++ keys by staticMetaObject.className() (or an equally stable module name), use descriptive lowerCamelCase keys, supply defaults, and balance every beginGroup() with endGroup(). In QML, use SVSCraft.Extras.Settings with a stable module-qualified category.
Every interactive control must be keyboard reachable. Give icon-only controls meaningful text and use the control's tooltip and accessibility defaults unless additional semantics are required. Preserve logical focus order. Use control.mirrored, layouts, and left/right padding correctly for RTL interfaces. Test long translations and never encode meaning by color alone.
Unless explicitly required, do not fill in any translated strings in TS files.
Visualized editors must not mutate the DSPX document directly. Route gestures through interaction controllers or core scenarios. Every user edit must begin a TransactionController transaction, commit with a concise translated description only when data changed, and abort on cancellation, startup failure, or no-op.
View-model bindings maintain explicit document-to-view and view-to-document maps. Bind/unbind on collection signals, guard equality to avoid feedback loops, and allow view-to-document writes only in the corresponding active edit/move state; otherwise restore the document value. Synchronize selection explicitly. Complex gestures use named pending/progressing/committing/aborting states and log every state entry/exit with a per-context QLoggingCategory.
Use a recursive checkout, CMake 3.19+, Qt 6.10-compatible development packages, Ninja, and a C++20 compiler. During vcpkg installation, expose the Qt CMake directory through QT_DIR/Qt6_DIR and retain those variables for overlay ports.
Do not perform any test, build, run, or automatic formatting operations unless explicitly requested by the user. Also do not include any of these operations in the plan unless explicitly requested by the user.
git submodule update --init --recursive
<vcpkg>/vcpkg install --x-manifest-root=scripts/vcpkg-manifest --x-install-root=<vcpkg>/installed --triplet=<triplet>
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake \
-DQt6_DIR=<qt>/lib/cmake/Qt6 -DAPPLICATION_BUILD_TESTS=ON \
-DTALCS_DSPX=ON -DTALCS_WIDGETS=ON
cmake --build build --parallel
./build/out/bin/DiffScope
cmake --install build --prefix <install-dir>On Windows, run these commands in a compiler developer shell and use DiffScope.exe. Build one target with cmake --build build --target <target>. For CI/package parity, use scripts/ci/Build.ps1. It performs a RelWithDebInfo configure, full build, and install and requires a vcpkg root directory. The optional DsspDirectory parameter specifies the bundled dssp installation directory.
Useful options include APPLICATION_INSTALL, APPLICATION_ENABLE_DEVEL, APPLICATION_ENABLE_BREAKPAD, and APPLICATION_CONFIGURE_INSTALLER. Keep machine-specific paths in CMake cache entries or environment variables, never in source files.
APPLICATION_BUILD_TESTS=ON builds the tst_uishell_<Feature> visual applications under build/out/bin; launch the affected one and exercise the changed interaction. These are manual GUI smoke tests and are not currently registered with CTest. For new deterministic logic tests, prefer Qt Test, name the target tst_<module>_<Feature>, register it with add_test, and run:
ctest --test-dir build --output-on-failure
cmake --build build --target <qml-target>_qmllintThere is no numeric coverage gate. Cover new branches and regressions proportionally, and report exact automated and manual checks. UI changes require screenshots or a short capture, plus checks for keyboard operation, theme variants, scaling, and representative locales. Do not count third-party submodule tests as validation of first-party behavior.
- Write the commit message as a single line: an English, sentence-case imperative subject in American spelling, such as
Add synth plugin,Fix bugs with language settings, orUpdate audio export extension name logic. Do not add a body or a mandatory Conventional Commit prefix. Design rationale belongs in code comments or the pull request description, not in the commit message. - Commit messages carry no AI attribution. Do not add
Co-Authored-ByorGenerated withlines. - Each commit does one thing. Split work into small commits by concern instead of one large commit. If a file contains changes for two commits, take the old content with
git show HEAD:<path>, apply only one set of changes, commit it, and then restore the complete version. Do not stage the whole file for convenience. - Every commit must build and pass its tests on its own, including the intermediate states created by splitting.
- Do not commit without the author's authorization, and above all do not push without it. Leave the changes in the working tree until the author approves the commit.
A pull request must explain the user-visible result, affected modules, architecture/API choices, compatibility or migration concerns, and the commands/tests run. Link relevant issues. Include before/after visuals for QML or layout changes. Call out action IDs, settings keys, translation changes, dependency/license changes, and submodule pointer bumps explicitly. Keep unrelated formatting, generated output, and local configuration out of the diff.
Never commit credentials, tokens, private service URLs, machine paths, user projects, or generated package-manager state. Validate paths and external-process arguments at boundaries, keep log messages useful without exposing sensitive content, and use Qt's native path and process APIs. Preserve each plugin's declared license and review the license impact before adding or updating a dependency.