cmake_minimum_required(VERSION 3.19)

# ---------------------------------------------------------------------------
# Version — SINGLE SOURCE OF TRUTH (Phase V, V1). Edit REASAMPLER_VERSION here and
# nowhere else: it flows to the binary constant, the ext-state writing-version stamp,
# and the "show version" action via a configure_file'd header (below). The string is
# authoritative verbatim — any leading zeros are preserved exactly as written (Daniel's
# ruling: padded and unpadded versions are BOTH legitimate — "0.9.8" and "0.9.80" are
# different versions, and the system must never force zero-padding). We deliberately do
# NOT reconstruct the display string from project(VERSION)'s numeric components, since
# CMake may normalize a numeric patch field; the string variable is what renders.
# project(VERSION ...) is still set for CMake hygiene / any downstream numeric use, but
# it is NOT the rendered source of truth.
#
# *** INVARIANT — DO NOT COLLAPSE OR DERIVE ***
# The set(REASAMPLER_VERSION ...) line below MUST remain a verbatim string literal,
# even when it is textually identical to the project(VERSION ...) line. It must NEVER
# become
#   set(REASAMPLER_VERSION "${PROJECT_VERSION}")
#   set(REASAMPLER_VERSION "${CMAKE_PROJECT_VERSION}")
# or any other form that derives the string from project(). CMake normalizes numeric
# patch fields: a padded "0.9.01" would silently round-trip to "0.9.1" and break the
# displayed version string. The verbatim-threading invariant is guarded at build time
# by app_version_padding_tests (a synthetic zero-padded canary; see the padding-canary
# block after app_version_tests). That guard detects reconstruct-from-components
# regressions in app_version.cpp but does NOT guard against this line being changed to
# a CMake variable derivation — that is this comment's job.
#
# There is a SECOND set(REASAMPLER_VERSION ...) inside the padding-canary function
# further down. That one is a permanent test fixture ("0.9.01") and must NEVER be
# bumped on release.
set(REASAMPLER_VERSION "1.0.0")

project(reaper_reasampler VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# ---------------------------------------------------------------------------
# Channel — SINGLE SOURCE OF TRUTH for beta-in-isolation (Phase V, V4). One flag,
# `-DREASAMPLER_CHANNEL=beta`, forks the whole channel identity from one build tree:
# absent (or `stable`) = today's build with BYTE-IDENTICAL identity (binary name,
# ext-state namespace, command-id strings, version render); `beta` = a fully isolated
# `reaper_reasampler_beta` that coexists with stable in one REAPER. The flag reduces to
# ONE bit (REASAMPLER_CHANNEL_IS_BETA) threaded through the SAME configure_file'd header
# as the version string, so the pure app_version module derives every channel-qualified
# identity from it — no scattered #ifdefs. Any value other than exactly `beta` is treated
# as stable (a typo must not silently produce a half-forked build), and we hard-error on
# an unrecognized non-empty value so a misspelled `-DREASAMPLER_CHANNEL=betaa` is caught
# at configure time rather than shipping stable identity under a beta intent.
set(REASAMPLER_CHANNEL "stable" CACHE STRING "Build channel: stable (default) or beta")
if(REASAMPLER_CHANNEL STREQUAL "beta")
    set(REASAMPLER_CHANNEL_IS_BETA 1)
    set(REASAMPLER_OUTPUT_NAME "reaper_reasampler_beta")
    # The VST3 instrument's on-disk name forks the same way (S18) — must match
    # app_version::vstOutputName() so the artifact name and the in-binary self-id agree.
    set(REASAMPLER_VST_OUTPUT_NAME "reasampler_9000_beta")
elseif(REASAMPLER_CHANNEL STREQUAL "stable")
    set(REASAMPLER_CHANNEL_IS_BETA 0)
    set(REASAMPLER_OUTPUT_NAME "reaper_reasampler")
    set(REASAMPLER_VST_OUTPUT_NAME "reasampler_9000")
else()
    message(FATAL_ERROR
        "REASAMPLER_CHANNEL must be 'stable' or 'beta' (got '${REASAMPLER_CHANNEL}')")
endif()

# Generate version_generated.h from the one REASAMPLER_VERSION variable + the channel bit.
# Regenerated at configure time whenever either changes; the exact string is substituted
# verbatim and the channel bit fans out through app_version.
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/src/core/version/version_generated.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/generated/version_generated.h
    @ONLY)

# ---------------------------------------------------------------------------
# Vendored dependencies — add as git submodules (see README):
#   git submodule add https://github.com/justinfrankel/reaper-sdk vendor/reaper-sdk
#   git submodule add https://github.com/justinfrankel/WDL        vendor/WDL
# ---------------------------------------------------------------------------
set(SDK_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/reaper-sdk/sdk)
set(WDL_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/WDL/WDL)
set(SWELL   ${WDL_INC}/swell)

# ---------------------------------------------------------------------------
# 0) core/ — the Q-W1 shared pure substrate. NO REAPER, NO SWELL, NO VST3.
#    json:  the ONE JSON lexical layer (reader + writer) behind the five
#           persisted-blob (de)serializers (bank_model / bank_book /
#           view_mode_model / owned_manifest / tail_control). T2-02 / §2.
#    wire:  the ONE length-prefixed ext-state wire codec (putField + Cursor +
#           the guarded decimal accumulate) behind provenance /
#           assignment_request / sample_usage / bank_sync. T2-01(b).
#    file_bytes: the ONE whole-file byte loader both artifacts link. T2-03.
#    Headers are included as "core/json/json.h" etc. (rooted at src/), so the
#    include paths survive the Q-W1 part-2 directory relocation unchanged.
# ---------------------------------------------------------------------------
add_library(json STATIC src/core/json/json.cpp)
target_include_directories(json PUBLIC src)

add_library(wire STATIC src/core/wire/wire.cpp)
target_include_directories(wire PUBLIC src)

add_library(file_bytes STATIC src/core/util/file_bytes.cpp)
target_include_directories(file_bytes PUBLIC src)

# ---------------------------------------------------------------------------
# 1) Pure model library — NO REAPER, NO SWELL. Builds & tests anywhere.
#    The sampler's heart: Sample metadata + BankIndex (Milestone 1).
# ---------------------------------------------------------------------------
add_library(bank_model STATIC src/core/model/bank_model.cpp)
target_include_directories(bank_model PUBLIC src)
target_link_libraries(bank_model PRIVATE json)

# ---------------------------------------------------------------------------
# 2) Pure peaks library — NO REAPER, NO SWELL. Waveform min/max thumbnails from
#    raw PCM (Milestone 2). A sibling pure lib, kept distinct from bank_model.
# ---------------------------------------------------------------------------
add_library(peaks STATIC src/core/audio/peaks.cpp)
target_include_directories(peaks PUBLIC src)

# ---------------------------------------------------------------------------
# 2b) Pure capture-path arithmetic — NO REAPER, NO SWELL. Bank-folder / unique
#     file-name / project-relative path derivation for the capture shell (M3).
#     Split out so the fiddly path logic is unit-tested outside the DAW.
# ---------------------------------------------------------------------------
add_library(capture_paths STATIC src/core/capture/capture_paths.cpp)
target_include_directories(capture_paths PUBLIC src)

# ---------------------------------------------------------------------------
# 2c) Pure bank-grid layout — NO REAPER, NO SWELL. Grid tiling math (panel WxH +
#     cell size + N -> cell rects, wrapping, partial last row) and the thumbnail
#     cache key for the docked bank_panel (M5). Split out so the layout logic is
#     unit-tested outside the DAW; the panel shell (SWELL/LICE/PCM) is DAW-verified.
# ---------------------------------------------------------------------------
add_library(bank_grid STATIC src/core/ui/bank_grid.cpp)
target_include_directories(bank_grid PUBLIC src)

# ---------------------------------------------------------------------------
# 2c') Pure mode_switch layout — NO REAPER, NO SWELL. The Design-View mode-switch
#      geometry (D5): header rect + N modes -> N equal segment rects (exact tiling),
#      and point -> segment hit-test. Split out so the switch's layout math is
#      unit-tested outside the DAW; the bank_panel header strip that draws it and
#      routes clicks to view::applyMode is DAW-verified. Mirror of bank_grid.
# ---------------------------------------------------------------------------
add_library(mode_switch STATIC src/core/view/mode_switch.cpp)
target_include_directories(mode_switch PUBLIC src)

# ---------------------------------------------------------------------------
# 2c'') Pure tab_strip layout — NO REAPER, NO SWELL. The named-banks tab-strip
#       geometry (B4): strip rect + N tabs at a fixed tab width + scroll offset ->
#       per-tab rects (overflow-clipped), overflow chevron reservation + maxScroll,
#       and point -> tab / chevron hit-test. Split out so the strip's layout +
#       overflow/scroll math is unit-tested outside the DAW; the bank_panel region
#       that draws it and routes clicks is DAW-verified. Mirror of mode_switch.
# ---------------------------------------------------------------------------
add_library(tab_strip STATIC src/core/ui/tab_strip.cpp)
target_include_directories(tab_strip PUBLIC src)

# ---------------------------------------------------------------------------
# 2d) Pure view_mode_model library — NO REAPER, NO SWELL. The Design View heart
#     (Phase D1): mode registry + GUID-keyed membership index + folder-tree-aware
#     visibility derivation + parking/restore planner + JSON round-trip. Mirror of
#     bank_model; the folder tree is an INPUT supplied by the D2 shell.
# ---------------------------------------------------------------------------
add_library(view_mode_model STATIC src/core/view/view_mode_model.cpp)
target_include_directories(view_mode_model PUBLIC src)
target_link_libraries(view_mode_model PRIVATE json)
# The pure lane-minting decision (planLaneMinting) names managed lanes via the ONE
# durable-key convention in lane_keys (laneNameForMode), so the model depends on that
# pure sibling. PUBLIC so every consumer (tests + module) resolves the symbol.
target_link_libraries(view_mode_model PUBLIC lane_keys)

# ---------------------------------------------------------------------------
# 2d) Pure view_tree library — NO REAPER, NO SWELL. The one testable-outside-DAW
#     piece of the D2 view shell: turning REAPER's linear I_FOLDERDEPTH stream into
#     the parent<->child FolderTree the model consumes. The REAPER reads stay in
#     view.cpp; this fiddly folder-depth walk is unit-tested here (mirrors
#     capture_paths splitting the path math out of the capture shell).
# ---------------------------------------------------------------------------
add_library(view_tree STATIC src/core/view/view_tree.cpp)
target_include_directories(view_tree PUBLIC src)
target_link_libraries(view_tree PUBLIC view_mode_model)

# ---------------------------------------------------------------------------
# 2d'') Pure guid_diff library — NO REAPER, NO SWELL. The D2 Wave-2 new-content
#       detection core: current \ previous GUID diff + the first-poll-after-open
#       baseline guard (and per-project reset). Split out so the fiddly baseline/diff
#       logic is unit-tested outside the DAW; the bank_panel timer that reads REAPER's
#       live track/item GUID set and applies the tags is DAW-verified. Mirror of
#       view_tree splitting the folder-depth walk out of view.cpp.
# ---------------------------------------------------------------------------
add_library(guid_diff STATIC src/core/view/guid_diff.cpp)
target_include_directories(guid_diff PUBLIC src)

# ---------------------------------------------------------------------------
# 2d''') Pure lane_keys library — NO REAPER, NO SWELL. The managed/manual fixed-lane
#        heuristic (D2 Wave-2): a lane whose durable P_LANENAME:n carries the
#        "reasampler:" prefix is tool-managed and keyed by that stable name; any other
#        lane is user-minted manual and off-limits. Resolves design point #1 (auto-tag
#        exemption) and #2 (name-keyed identity survives ordinal renumber). Split out
#        so the prefix rule is unit-tested; view.cpp reads the names from REAPER.
# ---------------------------------------------------------------------------
add_library(lane_keys STATIC src/core/view/lane_keys.cpp)
target_include_directories(lane_keys PUBLIC src)

# ---------------------------------------------------------------------------
# 2e) Pure insert_plan library — NO REAPER, NO SWELL. The InsertMedia `mode`
#     bitmask arithmetic behind the `insert` shell (M6). Split out so the
#     load-bearing bit computation (no silent stretch, opt-in conform) is
#     unit-tested outside the DAW; the InsertMedia call itself is DAW-verified.
# ---------------------------------------------------------------------------
add_library(insert_plan STATIC src/core/capture/insert_plan.cpp)
target_include_directories(insert_plan PUBLIC src)

# ---------------------------------------------------------------------------
# 2f) Pure render_settings library — NO REAPER, NO SWELL. The M7 capture-family
#     logic: source-mode + wet/dry -> RENDER_SETTINGS bit value, P_RAZOREDITS
#     string -> time ranges + union bound, and the capture-action taxonomy table.
#     Split out so the fiddly bit-mapping / razor-parsing is unit-tested outside
#     the DAW; the render-driving + selection reads stay in capture.cpp / main.cpp.
#     Depends on bank_model for the pure SourceMode enum.
# ---------------------------------------------------------------------------
add_library(render_settings STATIC src/core/capture/render_settings.cpp)
target_include_directories(render_settings PUBLIC src)
target_link_libraries(render_settings PUBLIC bank_model)

# ---------------------------------------------------------------------------
# 2f'') Pure batch_capture library — NO REAPER, NO SWELL. The M11 batch-capture
#       logic: an ordered list of source ranges (one per selected item / per razor
#       area) -> validated, ordinal-assigned capture units (empty/inverted dropped),
#       plus order-preserving per-unit result aggregation into a mixed-result summary
#       line. Split out so the plan/aggregate/summary is unit-tested outside the DAW;
#       the selection read, transient re-selection, and render loop stay in main.cpp.
#       No dependency on bank_model — it takes plain ranges/values at its boundary.
# ---------------------------------------------------------------------------
add_library(batch_capture STATIC src/core/capture/batch_capture.cpp)
target_include_directories(batch_capture PUBLIC src)

# ---------------------------------------------------------------------------
# 2f') Pure tail_control library — NO REAPER, NO SWELL. The docked bank_panel's
#      tail-mode toggle logic (T1 exposure): TailSetting state, cycle order
#      (None->Auto->Manual->None), the manual-length clamp to the 8 s cap, and the
#      toggle label text. Split out so the toggle's cycle/clamp/label is unit-tested
#      outside the DAW; the bank_panel footer that draws it + routes clicks is
#      DAW-verified. Depends on render_settings for the pure TailMode enum + caps.
# ---------------------------------------------------------------------------
add_library(tail_control STATIC src/core/capture/tail_control.cpp)
target_include_directories(tail_control PUBLIC src)
target_link_libraries(tail_control PUBLIC render_settings)
target_link_libraries(tail_control PRIVATE json)

# ---------------------------------------------------------------------------
# 2g''') Pure slot_map library — NO REAPER, NO SWELL. The L7 gap-preserving
#        display-position carrier for one bank (extracted from bank_book, Q-W1
#        T4-05): sample id -> slot, gap-preserving append/remove/reorder/reconcile,
#        JSON round-trip. Mirror of bank_model; wrapped (not merged) by bank_book.
# ---------------------------------------------------------------------------
add_library(slot_map STATIC src/core/model/slot_map.cpp)
target_include_directories(slot_map PUBLIC src)
target_link_libraries(slot_map PRIVATE json)

# ---------------------------------------------------------------------------
# 2g') Pure bank_book library — NO REAPER, NO SWELL. The multi-bank phase heart
#      (Phase B1): an ordered registry of banks (pool seeded as bank-zero + named
#      banks), each wrapping a BankIndex; create/rename/reorder/delete named banks,
#      pool privileges enforced in-model, active-bank id, index-only move/copy of a
#      sample between banks, JSON round-trip + legacy-bank_index→pool migration.
#      Mirror of bank_model / view_mode_model; wraps BankIndex (bank_model untouched).
# ---------------------------------------------------------------------------
add_library(bank_book STATIC
    src/core/model/bank_book.cpp
    src/core/model/bank_book_json.cpp)
target_include_directories(bank_book PUBLIC src)
target_link_libraries(bank_book PUBLIC bank_model)
target_link_libraries(bank_book PUBLIC slot_map)
target_link_libraries(bank_book PRIVATE json)

# ---------------------------------------------------------------------------
# 2g'') Pure owned_manifest library — NO REAPER, NO SWELL. The owned-file manifest
#       seam (Phase B B-cap): the set of project-relative files the capture path
#       itself created, so Phase R prune can tell the bank system's own orphans from
#       hand-dropped files. Deliberately DECOUPLED from bank_book — it tracks files
#       CREATED, not index membership (sample-remove is not manifest-remove). Small
#       pure type + JSON round-trip; mirror of wav_codec / tab_strip. B-cap writes +
#       persists it; Phase R (R1/R2) consumes it — no prune logic here.
# ---------------------------------------------------------------------------
add_library(owned_manifest STATIC src/core/model/owned_manifest.cpp)
target_include_directories(owned_manifest PUBLIC src)
target_link_libraries(owned_manifest PRIVATE json)

# ---------------------------------------------------------------------------
# 2g''') Pure prune_reconcile library — NO REAPER, NO SWELL, NO filesystem. The
#        Phase R (Reclaim) Wave-1 safety-critical decision: given the files present
#        in the bank folder, the union of files referenced across every bank, and the
#        owned-file manifest, compute the orphan set (owned ∩ present) − referenced.
#        Mirror of view_mode_model::reconcile one level down (files, not GUIDs). Small
#        pure function; R2/R3 wrap the two ends (folder enumeration + deletion) in the
#        shell. Standalone — depends only on the standard library.
# ---------------------------------------------------------------------------
add_library(prune_reconcile STATIC src/core/reclaim/prune_reconcile.cpp)
target_include_directories(prune_reconcile PUBLIC src)

# ---------------------------------------------------------------------------
# 2g'''') Pure prune_button layout — NO REAPER, NO SWELL. The Phase R (Reclaim)
#         Wave-3 (R3, fork R-E) prune-button geometry: footer rect -> right-anchored
#         button rect (suppressed when the footer is too narrow), and point -> in/out
#         hit-test. Split out so the button's placement + hit-test math is unit-tested
#         outside the DAW; the bank_panel footer that draws it and dispatches the
#         "Prune bank folder" command is DAW-verified. Mirror of mode_switch / tab_strip.
# ---------------------------------------------------------------------------
add_library(prune_button STATIC src/core/ui/prune_button.cpp)
target_include_directories(prune_button PUBLIC src)

# ---------------------------------------------------------------------------
# 2g) Pure capture_realtime library — NO REAPER, NO SWELL. (Renamed from
#     realtime_record in Q-W3 — the Q-9 naming rider: pure module takes the stem,
#     the shell takes the suffix, matching drag_out <-> drag_out_win.) The M8
#     realtime-record logic: capture scope + FX-tap point -> I_RECMODE /
#     I_RECMODE_FLAGS values, wet/dry -> tap point, the recorded-file -> Sample
#     mapping, and the async record-phase state machine. Split out so the fiddly
#     record-mode bit values + Sample population + phase transitions are
#     unit-tested outside the DAW; the transport/temp-track/send recipe stays in
#     capture_realtime_shell.cpp (+ the file-side capture_realtime_finalize.cpp).
#     Depends on bank_model for the pure Sample / SourceMode types.
# ---------------------------------------------------------------------------
add_library(capture_realtime STATIC src/core/capture/capture_realtime.cpp)
target_include_directories(capture_realtime PUBLIC src)
target_link_libraries(capture_realtime PUBLIC bank_model)

# ---------------------------------------------------------------------------
# 2h) Pure wav_codec library — NO REAPER, NO SWELL. The ONE owner of the WAV/RIFF
#     byte format (Q-W3, audit §4e: T2-08 / T4-10 / T4-23 consolidation): the RIFF
#     chunk walker + layout parse (formerly wav_trim), the tail-trim truncate plan +
#     size-field patch (formerly duplicated in capture_realtime), the float32 WAV
#     build (formerly hand-rolled in ingest), and the WAV-aware content hashes
#     (formerly in capture_paths). The dedup-by-hash and null-test invariants rest
#     on this one implementation. File I/O stays in the shells. Depends on peaks
#     for the AudioSample float alias. (The transitional `wav_trim` alias was
#     retired in Q-W6 — every includer points here directly.)
# ---------------------------------------------------------------------------
add_library(wav_codec STATIC src/core/capture/wav_codec.cpp)
target_include_directories(wav_codec PUBLIC src)
target_link_libraries(wav_codec PUBLIC peaks)

# ---------------------------------------------------------------------------
# 2i) Pure app_version library — NO REAPER, NO SWELL. The Phase V (V1) version-identity
#     core: re-exports the ONE CMake-sourced version string (via the configure_file'd
#     version_generated.h) and owns the pure parse/compare/classify logic — the semver
#     ordering a within-channel forward migration needs, and the three-way writing-version
#     classification (PreVersioning / Unknown / Stamped) persist reads back from ext state.
#     Split out so the exact-string fidelity + parse/compare are unit-tested outside the
#     DAW; the ext-state write/read (persist) and the show-version action (main) are shell.
#     Depends on the generated header in the build tree (PUBLIC so every consumer sees it).
# ---------------------------------------------------------------------------
add_library(app_version STATIC src/core/version/app_version.cpp)
target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/generated)

# ---------------------------------------------------------------------------
# 2j) Pure provenance library — NO REAPER, NO SWELL. The Milestone 10 core: the
#     recorded capture recipe (CaptureRecipe) + the thin drift-fingerprint that
#     rides in Provenance.fxChainSnapshot (P1=a), its build/parse round-trip, the
#     FX-chain identity fold, and the pure parent-detection decision (resample-from-
#     sample by resolved file path). Split out so the encoding + decision logic are
#     unit-tested outside the DAW; the FX-chain query, capture re-run, and action
#     registration stay in the shell (main.cpp / shell/actions/). No dependency on
#     bank_model — it takes plain strings/values at its boundary.
# ---------------------------------------------------------------------------
add_library(provenance STATIC src/core/model/provenance.cpp)
target_include_directories(provenance PUBLIC src)
target_link_libraries(provenance PRIVATE wire)

# ---------------------------------------------------------------------------
# 2j') Pure assignment_request library — NO REAPER, NO SWELL, NO VST3. The S8 ingest
#      assignment-request wire format: the (bankId, sampleId, generation) value the
#      EXTENSION writes to "reasampler" ext-state after an ingest-with-assign, decoded by
#      the VST3 instrument in a later dispatch. Only the wire (build/parse round-trip)
#      lives here — writing it is the persist shell's job, reading it the instrument's.
#      Split out (mirror of provenance / owned_manifest) so the format both artifacts
#      depend on is unit-tested outside the DAW; the reader lands in a separate artifact,
#      so the round-trip test is the contract guard. No dependency — plain strings + int64.
# ---------------------------------------------------------------------------
add_library(assignment_request STATIC src/core/wire/assignment_request.cpp)
target_include_directories(assignment_request PUBLIC src)
target_link_libraries(assignment_request PRIVATE wire)

# ---------------------------------------------------------------------------
# 2j'') Pure sample_usage library — NO REAPER, NO SWELL, NO VST3. The pS-usage seam:
#       the per-instance usage record the INSTRUMENT writes ("rsusage_<guid>" ext-state,
#       the one sanctioned VST-side write) and the EXTENSION reads at prune time. Owns
#       the wire round-trip, the publish plan (copy-collision fail-safe resolution),
#       the liveness fold (which records count against the live FX enumeration,
#       protect-all when zero identified, abort on unreadable), and the FX identity
#       matcher.
#       Linked by BOTH artifacts — the mirror of assignment_request, reversed direction.
# ---------------------------------------------------------------------------
add_library(sample_usage STATIC src/core/wire/sample_usage.cpp)
target_include_directories(sample_usage PUBLIC src)
target_link_libraries(sample_usage PRIVATE wire)

# ---------------------------------------------------------------------------
# 2l) Pure drag_out library — NO REAPER, NO SWELL, NO OS/OLE. The Milestone 11
#     native-OS-drag-out decision core: the gesture-boundary decision (drag state +
#     pointer + panel rect -> internal drag / OS drag / none — the invariant-#4 seam
#     that keeps the internal bank-to-bank drag byte-identical) and the drag path-list
#     assembly (resolved sample paths -> de-duped, existing-only absolute path list with
#     an explicit skip-missing / skip-unresolved policy). Split out so the boundary +
#     set algebra are unit-tested outside the DAW; the OLE DoDragDrop / SWELL file-list
#     initiation (drag_out_win) and the bank_panel gesture hook are DAW-verified. Mirror
#     of mode_switch.
# ---------------------------------------------------------------------------
add_library(drag_out STATIC src/core/ui/drag_out.cpp)
target_include_directories(drag_out PUBLIC src)

# ---------------------------------------------------------------------------
# 2l') Pure instrument_drop library — NO REAPER, NO SWELL, NO VST3 SDK. The S17
#      drop-and-load payload-construction core (S-GA-DropFX revision): turn the dragged
#      capture id into a Steinberg-format .vstpreset image the shell applies via
#      TrackFX_SetPreset so a freshly-added ReaSampler 9000 plays that capture (the
#      former "vst_chunk" named-config-parm write fed REAPER raw component bytes its
#      VST3 wrapper framing cannot apply — the blank-on-drop regression). Reuses the
#      instrument's OWN serializer (component_state_io::serializeComponentState) — NOT
#      a parallel byte writer — so the cross-artifact contract cannot drift; links
#      component_state_io (Q-W2v codec split, T4-13 ≡ T2-07: the extension no longer
#      links sampler_core/pitch_shift object code to serialize one preset blob) and
#      NEITHER SDK. The class-ID string derives from the FROZEN UID macros
#      (src/core/wire/reasampler_uid.h, SDK-free), channel-selected via the generated
#      version header — hence the generated include dir. The round-trip test parses
#      the container and decodes back through the instrument's own reader. Mirror of
#      assignment_request.
# ---------------------------------------------------------------------------
add_library(instrument_drop STATIC src/core/wire/instrument_drop.cpp)
target_include_directories(instrument_drop PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/generated)
target_link_libraries(instrument_drop PUBLIC component_state_io)

# ---------------------------------------------------------------------------
# 2m) Pure theme library — NO REAPER, NO SWELL, NO LICE. The Phase L (L1) palette
#     core of the shared drawing kit: a ROLE-based color model (bg/base..warn), the
#     interaction-state transform (rest/hover/active/pressed/dragging/focus/disabled),
#     the Direction C spectral hue ramp, and the WCAG contrast math that lets a unit
#     test prove every text-on-surface pair clears its floor ("punch to the floor").
#     THE SINGLE POINT OF CHANGE (DS-2): one direction constants block feeds roleColor;
#     switching the visual direction is a one-file edit. The draw shell (draw_kit) turns
#     a KitColor into a LICE_pixel at the boundary. Mirror of mode_switch — pure.
# ---------------------------------------------------------------------------
add_library(theme STATIC src/core/ui/theme.cpp)
target_include_directories(theme PUBLIC src)

# ---------------------------------------------------------------------------
# 2n) Pure component_geometry library — NO REAPER, NO SWELL, NO LICE. The Phase L (L1)
#     generic component geometry the kit draws against: button box (inset + graceful
#     suppression), horizontal slider track/handle/filled geometry + value<->px inverse,
#     and list-row rect + hover hit-test. The kit-level primitives that don't already
#     have a pure owner (bank_grid/mode_switch/tab_strip/prune_button
#     stay the source of truth for THEIR surfaces). Names KitBox/KitButtonBox/
#     SliderGeometry/ListRowBox avoid the existing ButtonRect/CellRect collisions.
#     Mirror of prune_button — pure, CTest-covered.
# ---------------------------------------------------------------------------
add_library(component_geometry STATIC src/core/ui/component_geometry.cpp)
target_include_directories(component_geometry PUBLIC src)

# ---------------------------------------------------------------------------
# 2o) Pure action_bar library — NO REAPER, NO SWELL, NO LICE. The Phase L (L2)
#     dock-panel layout redesign core: the TASK-GROUPED action bar geometry for the
#     panel's action inventory — clusters (capture / placement / maintenance) tile the
#     bar at a fixed button
#     width with intra/inter-cluster gaps, each button carrying a label + keybinding
#     micro sub-rect, whole trailing buttons dropped (never clipped) on a narrow panel,
#     and point -> flat action index hit-test. Split out so the layout + hit-test math
#     is unit-tested outside the DAW; the bank_panel L1-kit draw + NamedCommandLookup/
#     Main_OnCommand dispatch + kbd_getTextFromCmd query are DAW-verified. Mirror of
#     mode_switch / prune_button.
# ---------------------------------------------------------------------------
add_library(action_bar STATIC src/core/ui/action_bar.cpp)
target_include_directories(action_bar PUBLIC src)

# ---------------------------------------------------------------------------
# 2p) Pure footer_bar layout — NO REAPER, NO SWELL, NO LICE. The Phase L (L4)
#     dock-panel button-layout enhancement's footer LEFT group: the narrowed
#     [Arrange|Design] mode toggle, its compact per-mode count label, and the Tail
#     button, laid out left-to-right with greedy right-reserve suppression (the RIGHT
#     Prune button stays owned by prune_button). Point -> Toggle/Tail/None hit-test.
#     Split out so the footer's new multi-affordance row math is unit-tested outside
#     the DAW; the bank_panel L1-kit draw + tail-cycle/mode-activate dispatch are
#     DAW-verified. Reuses prune_button's FooterRect input type. Mirror of action_bar /
#     mode_switch / prune_button.
# ---------------------------------------------------------------------------
add_library(footer_bar STATIC src/core/ui/footer_bar.cpp)
target_include_directories(footer_bar PUBLIC src)
target_link_libraries(footer_bar PUBLIC prune_button)

# ---------------------------------------------------------------------------
# 2q) Pure overflow_menu layout — NO REAPER, NO SWELL, NO LICE. The Phase L (L5,
#     refinement 1) top-toolbar "⋯" More-button geometry: band rect -> right-anchored
#     menu-button rect (suppressed when the band is too narrow) + the horizontal reserve
#     the action_bar must leave for it, and point -> in/out hit-test. Split out so the
#     button placement + reserve math is unit-tested outside the DAW; the bank_panel
#     L1-kit draw + TrackPopupMenu popup + NamedCommandLookup/Main_OnCommand dispatch are
#     DAW-verified. Mirror of prune_button / mode_switch.
# ---------------------------------------------------------------------------
add_library(overflow_menu STATIC src/core/ui/overflow_menu.cpp)
target_include_directories(overflow_menu PUBLIC src)

# ---------------------------------------------------------------------------
# 2r) Pure mode_enable predicate — NO REAPER, NO SWELL, NO LICE. The Phase L (L5,
#     refinement 3) opposite-mode tag-button enablement: (active mode id, button target
#     mode) -> live/disabled, so only the buttons for the OPPOSITE of the active mode are
#     clickable. Split out so both active modes are covered by CTest (not only whichever a
#     DAW pass sat in); the bank_panel reads the active mode from view().activeModeId() and
#     draws disabled buttons in the kit Disabled state. Depends on view_mode_model for the
#     seed mode-id constants (kArrangeModeId / kDesignModeId — ONE home for the ids).
# ---------------------------------------------------------------------------
add_library(mode_enable STATIC src/core/ui/mode_enable.cpp)
target_include_directories(mode_enable PUBLIC src)
target_link_libraries(mode_enable PUBLIC view_mode_model)

# ---------------------------------------------------------------------------
# 2s) Pure tooltip layout — NO REAPER, NO SWELL, NO LICE. The Phase L (L5, refinement 2)
#     custom hover-delay tooltip's placement geometry (anchor rect + text extent + client
#     bounds -> tooltip box, preferring below, flipping above near the bottom edge, clamped
#     to the client) + the action DISPLAY-PREFIX strip helper. Split out so placement + the
#     prefix strip are unit-tested outside the DAW; the bank_panel hover timer + LICE overlay
#     draw are DAW-verified. Mirror of prune_button / component_geometry.
# ---------------------------------------------------------------------------
add_library(tooltip STATIC src/core/ui/tooltip.cpp)
target_include_directories(tooltip PUBLIC src)

# ---------------------------------------------------------------------------
# 2t) Pure card_meta formatters — NO REAPER, NO SWELL, NO LICE. The Phase L (L7)
#     decorative card metadata overlay's formatting: bars.beats.subdivisions from a
#     capture-time tempo + meter stamp (F1) and seconds.milliseconds from length. Split
#     out so the musical/wall-clock string derivation (with its bar-boundary + unstamped
#     edge cases) is unit-tested outside the DAW; the bank_panel kit-text overlay draw is
#     DAW-verified. Mirror of tooltip's prefix-strip helper — pure, CTest-covered.
# ---------------------------------------------------------------------------
add_library(card_meta STATIC src/core/ui/card_meta.cpp)
target_include_directories(card_meta PUBLIC src)

# ---------------------------------------------------------------------------
# 2u) Pure card_drag gesture — NO REAPER, NO SWELL, NO LICE, NO OS. The Phase L (L7)
#     in-grid reorder drag decision logic: the F3 gesture precedence (leave-client ->
#     OS-drag; else other-bank -> move/copy; else same-bank grid -> reorder/replace),
#     the resolved-gesture -> cursor-cue map, and the sparse-aware slot rect layout +
#     point -> slot hit-test (empties included). Split out so the precedence + slot math
#     is unit-tested outside the DAW; the SWELL wiring + SetCursor call + drop-target draw
#     are DAW-verified. Reuses drag_out's PanelClientRect/DragState + bank_grid's CellRect/
#     GridSpec. Mirror of drag_out::decideGesture / bank_grid.
# ---------------------------------------------------------------------------
add_library(card_drag STATIC src/core/ui/card_drag.cpp)
target_include_directories(card_drag PUBLIC src)
target_link_libraries(card_drag PUBLIC drag_out bank_grid)

# ---------------------------------------------------------------------------
# 2v) Pure sampler_core library — NO VST3, NO REAPER, NO SWELL. The HEART of the
#     MIDI-playback instrument (D3): polyphonic voice allocation with bounded stealing,
#     the three envelope evaluators, and repitch/interpolation from a root note with
#     loop-point-aware sustain over ONE loaded capture. The mirror of bank_model / peaks /
#     bank_book, tested hard outside any host. Lives under core/instrument/engine/ but
#     links NEITHER SDK — the plain-data boundary is enforced structurally: the test
#     target below links only sampler_core (+ its peaks dep for the AudioSample alias,
#     the one house precedent wav_codec also relies on). The VST3 shell (shell/instrument/
#     reasampler_processor.cpp) marshals MIDI/audio to/from it and is DAW-verified.
# ---------------------------------------------------------------------------
# pitch_shift (S16) — the pure duration-preserving PitchShifter (Preserve-engine DSP core).
# NO VST3/REAPER/SWELL/vendor: a hand-rolled OLA shifter chosen over WDL_SimplePitchShifter
# because that header drags <windows.h> (via wdltypes.h) into any TU that includes it, which
# cannot enter the pure sampler_core. Links only peaks (the AudioSample alias). sampler_core
# depends on it (Voice owns two PitchShifters).
add_library(pitch_shift STATIC src/core/instrument/engine/pitch_shift.cpp)
target_include_directories(pitch_shift PUBLIC src)
target_link_libraries(pitch_shift PUBLIC peaks)

# velocity_curve (S-VIEW-9) — the pure velocity->amp transfer curve (eval + editing/clamp/inverse
# map). NO VST3/REAPER/SWELL/vendor and DELIBERATELY no editor_geometry (its hit-test takes an
# explicit pixel box, not a Rect) so the engine can depend on it WITHOUT gaining a transitive
# dependency on the editor's layout types. play_params.h carries one (SampleData holds the
# curve; Voice::start eval's it). Mirror of pitch_shift's role, one layer below the engine.
add_library(velocity_curve STATIC src/core/instrument/engine/velocity_curve.cpp)
target_include_directories(velocity_curve PUBLIC src)

# Two TUs on the engine's own responsibility seam: voice.cpp is the per-NOTE half (note-on
# setup, the Preserve ring prime, legato retune), voice_engine.cpp the note routing /
# allocation / stealing / mono stack / panic / block render. The per-SAMPLE render half is
# inline in voice.h (with the envelope evaluators in envelopes.h) precisely so this TU
# boundary costs the hot path nothing — see voice.h's header.
add_library(sampler_core STATIC
    src/core/instrument/engine/voice.cpp
    src/core/instrument/engine/voice_engine.cpp)
target_include_directories(sampler_core PUBLIC src)
target_link_libraries(sampler_core PUBLIC peaks pitch_shift velocity_curve)

# ---------------------------------------------------------------------------
# 3) Standalone tests for the pure modules (run without launching REAPER).
# ---------------------------------------------------------------------------
enable_testing()

# core/ (Q-W1): the shared JSON lexical layer, wire codec, and file loader.
add_executable(json_tests tests/test_json.cpp)
target_link_libraries(json_tests PRIVATE json)
add_test(NAME json_tests COMMAND json_tests)

add_executable(wire_tests tests/test_wire.cpp)
target_link_libraries(wire_tests PRIVATE wire)
add_test(NAME wire_tests COMMAND wire_tests)

add_executable(file_bytes_tests tests/test_file_bytes.cpp)
target_link_libraries(file_bytes_tests PRIVATE file_bytes)
add_test(NAME file_bytes_tests COMMAND file_bytes_tests)

add_executable(bank_model_tests tests/test_bank_model.cpp)
target_link_libraries(bank_model_tests PRIVATE bank_model)
add_test(NAME bank_model_tests COMMAND bank_model_tests)

add_executable(peaks_tests tests/test_peaks.cpp)
target_link_libraries(peaks_tests PRIVATE peaks)
add_test(NAME peaks_tests COMMAND peaks_tests)

add_executable(capture_paths_tests tests/test_capture_paths.cpp)
target_link_libraries(capture_paths_tests PRIVATE capture_paths)
add_test(NAME capture_paths_tests COMMAND capture_paths_tests)

add_executable(bank_grid_tests tests/test_bank_grid.cpp)
target_link_libraries(bank_grid_tests PRIVATE bank_grid)
add_test(NAME bank_grid_tests COMMAND bank_grid_tests)

add_executable(mode_switch_tests tests/test_mode_switch.cpp)
target_link_libraries(mode_switch_tests PRIVATE mode_switch)
add_test(NAME mode_switch_tests COMMAND mode_switch_tests)

add_executable(tab_strip_tests tests/test_tab_strip.cpp)
target_link_libraries(tab_strip_tests PRIVATE tab_strip)
add_test(NAME tab_strip_tests COMMAND tab_strip_tests)

add_executable(view_mode_model_tests tests/test_view_mode_model.cpp)
target_link_libraries(view_mode_model_tests PRIVATE view_mode_model)
add_test(NAME view_mode_model_tests COMMAND view_mode_model_tests)

add_executable(view_tree_tests tests/test_view_tree.cpp)
target_link_libraries(view_tree_tests PRIVATE view_tree)
add_test(NAME view_tree_tests COMMAND view_tree_tests)

add_executable(guid_diff_tests tests/test_guid_diff.cpp)
target_link_libraries(guid_diff_tests PRIVATE guid_diff)
add_test(NAME guid_diff_tests COMMAND guid_diff_tests)

add_executable(lane_keys_tests tests/test_lane_keys.cpp)
target_link_libraries(lane_keys_tests PRIVATE lane_keys)
add_test(NAME lane_keys_tests COMMAND lane_keys_tests)

add_executable(insert_plan_tests tests/test_insert_plan.cpp)
target_link_libraries(insert_plan_tests PRIVATE insert_plan)
add_test(NAME insert_plan_tests COMMAND insert_plan_tests)

add_executable(render_settings_tests tests/test_render_settings.cpp)
target_link_libraries(render_settings_tests PRIVATE render_settings)
add_test(NAME render_settings_tests COMMAND render_settings_tests)

add_executable(batch_capture_tests tests/test_batch_capture.cpp)
target_link_libraries(batch_capture_tests PRIVATE batch_capture)
add_test(NAME batch_capture_tests COMMAND batch_capture_tests)

add_executable(tail_control_tests tests/test_tail_control.cpp)
target_link_libraries(tail_control_tests PRIVATE tail_control)
add_test(NAME tail_control_tests COMMAND tail_control_tests)

add_executable(capture_realtime_tests tests/test_capture_realtime.cpp)
target_link_libraries(capture_realtime_tests PRIVATE capture_realtime)
add_test(NAME capture_realtime_tests COMMAND capture_realtime_tests)

add_executable(bank_book_tests tests/test_bank_book.cpp)
target_link_libraries(bank_book_tests PRIVATE bank_book)
add_test(NAME bank_book_tests COMMAND bank_book_tests)

add_executable(slot_map_tests tests/test_slot_map.cpp)
target_link_libraries(slot_map_tests PRIVATE slot_map json)
add_test(NAME slot_map_tests COMMAND slot_map_tests)

add_executable(wav_codec_tests tests/test_wav_codec.cpp)
target_link_libraries(wav_codec_tests PRIVATE wav_codec)
add_test(NAME wav_codec_tests COMMAND wav_codec_tests)

add_executable(owned_manifest_tests tests/test_owned_manifest.cpp)
target_link_libraries(owned_manifest_tests PRIVATE owned_manifest)
add_test(NAME owned_manifest_tests COMMAND owned_manifest_tests)

add_executable(prune_reconcile_tests tests/test_prune_reconcile.cpp)
target_link_libraries(prune_reconcile_tests PRIVATE prune_reconcile bank_book)
add_test(NAME prune_reconcile_tests COMMAND prune_reconcile_tests)

add_executable(prune_button_tests tests/test_prune_button.cpp)
target_link_libraries(prune_button_tests PRIVATE prune_button)
add_test(NAME prune_button_tests COMMAND prune_button_tests)

add_executable(app_version_tests tests/test_app_version.cpp)
target_link_libraries(app_version_tests PRIVATE app_version)
add_test(NAME app_version_tests COMMAND app_version_tests)

# Anti-normalization padding canary (see tests/test_app_version_padding.cpp for the full
# rationale). The live-version assertions in app_version_tests are version-shape-blind:
# relative checks (e.g. appVersion() == stampVersion()) hold whether the string was
# threaded verbatim or reconstructed from numeric components, at any version, padded or
# not — the live suite cannot detect a reconstruct-from-components regression at all.
# So: run the SAME version_generated.h.in template through configure_file a second time
# with a SYNTHETIC padded version, and compile the SAME src/core/version/app_version.cpp against that
# header (include-dir substitution — the canary target never sees the live generated/
# dir). The "0.9.01" here is a permanent test fixture, NOT the shipped version (see also
# the source-of-truth comment at the top of this file for the live-version invariant); it
# must match the literals in test_app_version_padding.cpp and must never be bumped on
# release.
#
# COVERAGE BOUNDARY: the canary detects "reconstruct-from-numeric-components"
# regressions inside app_version.cpp. It does NOT detect the live source-of-truth line
# becoming a CMake variable derivation; that case is separately guarded by the comment
# block at the top of this file.
function(_configure_padding_canary)
    # set() inside a function is function-scoped — the clobber cannot leak into the
    # parent scope, so no save/restore/unset dance is needed. REASAMPLER_CHANNEL_IS_BETA
    # and CMAKE_CURRENT_SOURCE_DIR / CMAKE_CURRENT_BINARY_DIR are inherited read-only.
    set(REASAMPLER_VERSION "0.9.01")
    configure_file(
        ${CMAKE_CURRENT_SOURCE_DIR}/src/core/version/version_generated.h.in
        ${CMAKE_CURRENT_BINARY_DIR}/generated_padding_canary/version_generated.h
        @ONLY)
endfunction()
_configure_padding_canary()

# NOTE: app_version_padding_tests deliberately recompiles src/core/version/app_version.cpp rather
# than linking the app_version library target. This is required for the include-dir
# substitution to work — the canary needs to see generated_padding_canary/ instead of
# the live generated/ dir. If app_version ever gains a link dependency (e.g. a new
# pure-module link), the canary target_link_libraries must mirror it here.
add_executable(app_version_padding_tests
    tests/test_app_version_padding.cpp
    src/core/version/app_version.cpp)
target_include_directories(app_version_padding_tests PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/generated_padding_canary src)
add_test(NAME app_version_padding_tests COMMAND app_version_padding_tests)

# The provenance test links bank_model too — it proves the recorded recipe survives
# the Sample-JSON round-trip (Provenance.fxChainSnapshot), the M1 seam M10 rides on.
add_executable(provenance_tests tests/test_provenance.cpp)
target_link_libraries(provenance_tests PRIVATE provenance bank_model)
add_test(NAME provenance_tests COMMAND provenance_tests)

add_executable(drag_out_tests tests/test_drag_out.cpp)
target_link_libraries(drag_out_tests PRIVATE drag_out)
add_test(NAME drag_out_tests COMMAND drag_out_tests)

# instrument_drop (S17): the drop-and-load .vstpreset payload builder. The round-trip test
# parses the preset container and decodes its Comp chunk back through the instrument's OWN
# reader (deserializeComponentState) to prove the extension feeds setState exactly what it
# accepts — the cross-artifact contract guard.
add_executable(instrument_drop_tests tests/test_instrument_drop.cpp)
target_link_libraries(instrument_drop_tests PRIVATE instrument_drop)
add_test(NAME instrument_drop_tests COMMAND instrument_drop_tests)

add_executable(theme_tests tests/test_theme.cpp)
target_link_libraries(theme_tests PRIVATE theme)
add_test(NAME theme_tests COMMAND theme_tests)

add_executable(component_geometry_tests tests/test_component_geometry.cpp)
target_link_libraries(component_geometry_tests PRIVATE component_geometry)
add_test(NAME component_geometry_tests COMMAND component_geometry_tests)

add_executable(action_bar_tests tests/test_action_bar.cpp)
target_link_libraries(action_bar_tests PRIVATE action_bar)
add_test(NAME action_bar_tests COMMAND action_bar_tests)

add_executable(footer_bar_tests tests/test_footer_bar.cpp)
target_link_libraries(footer_bar_tests PRIVATE footer_bar)
add_test(NAME footer_bar_tests COMMAND footer_bar_tests)

add_executable(overflow_menu_tests tests/test_overflow_menu.cpp)
target_link_libraries(overflow_menu_tests PRIVATE overflow_menu)
add_test(NAME overflow_menu_tests COMMAND overflow_menu_tests)

add_executable(mode_enable_tests tests/test_mode_enable.cpp)
target_link_libraries(mode_enable_tests PRIVATE mode_enable)
add_test(NAME mode_enable_tests COMMAND mode_enable_tests)

add_executable(tooltip_tests tests/test_tooltip.cpp)
target_link_libraries(tooltip_tests PRIVATE tooltip)
add_test(NAME tooltip_tests COMMAND tooltip_tests)

add_executable(card_meta_tests tests/test_card_meta.cpp)
target_link_libraries(card_meta_tests PRIVATE card_meta)
add_test(NAME card_meta_tests COMMAND card_meta_tests)

add_executable(card_drag_tests tests/test_card_drag.cpp)
target_link_libraries(card_drag_tests PRIVATE card_drag)
add_test(NAME card_drag_tests COMMAND card_drag_tests)

add_executable(assignment_request_tests tests/test_assignment_request.cpp)
target_link_libraries(assignment_request_tests PRIVATE assignment_request)
add_test(NAME assignment_request_tests COMMAND assignment_request_tests)

# sample_usage (pS-usage): the instance-usage wire + publish plan + liveness fold. The
# tests are the prune-protection proof at the pure layer: a capture held by a live
# instance lands in the referenced union and pruneOrphans can never emit it (links
# prune_reconcile for the composed proof).
add_executable(sample_usage_tests tests/test_sample_usage.cpp)
target_link_libraries(sample_usage_tests PRIVATE sample_usage prune_reconcile)
add_test(NAME sample_usage_tests COMMAND sample_usage_tests)

# sampler_core: the S3 heart. Links ONLY sampler_core (+ its peaks dep) — NEITHER the
# VST3 SDK nor the REAPER SDK — which is the structural proof of the plain-data
# boundary (a VST3/REAPER type in the core would fail to compile/link here).
# pitch_shift (S16): the pure Preserve-engine OLA shifter. Links ONLY pitch_shift (+ peaks) —
# NEITHER SDK — the same plain-data-boundary proof, and specifically the compile-time proof it
# does NOT drag in the WDL <windows.h> chain the built-in WDL shifter would.
add_executable(pitch_shift_tests tests/test_pitch_shift.cpp)
target_link_libraries(pitch_shift_tests PRIVATE pitch_shift)
add_test(NAME pitch_shift_tests COMMAND pitch_shift_tests)

add_executable(sampler_core_tests tests/test_sampler_core.cpp)
target_link_libraries(sampler_core_tests PRIVATE sampler_core)
add_test(NAME sampler_core_tests COMMAND sampler_core_tests)

# velocity_curve (S-VIEW-9): the pure velocity->amp transfer curve. Links ONLY velocity_curve —
# NEITHER SDK, and specifically not editor_geometry — the plain-data-boundary + engine-clean proof.
add_executable(velocity_curve_tests tests/test_velocity_curve.cpp)
target_link_libraries(velocity_curve_tests PRIVATE velocity_curve)
add_test(NAME velocity_curve_tests COMMAND velocity_curve_tests)

# ---------------------------------------------------------------------------
# 2i) Pure VST3-instrument helpers (Phase S1) — NO VST3, NO REAPER, NO SWELL/LICE.
#     editor_geometry: the shared geometry vocabulary every instrument UI module speaks
#     (the `Rect` alias + contains()) — header-only, hence INTERFACE; the Sample face's
#     own layout lives in sample_bands/sample_chrome below. bridge_marshal: the REAPER
#     VST-host bridge read marshalling — GetProjExtState result decode + the ONE
#     grow-loop retry policy (readProjExtStateGrowing, Q-W5 rider T2-04) shared by the
#     VST bridge read AND the extension's persist/usage_scan ext-state reads (hence
#     linked into reaper_reasampler too); unit-tested outside the DAW, while the VST3
#     shell (shell/instrument/*) that draws/routes/invokes is DAW-verified.
# ---------------------------------------------------------------------------
add_library(editor_geometry INTERFACE)
target_include_directories(editor_geometry INTERFACE src)

# sample_bands — THE band-stack allocator: the ONE module that owns the Sample face's
# vertical inventory (chrome / two-lane waveform / decks) plus the waveform band's lane
# split. A shared READ-ONLY surface for every band owner; a band's interior module lays out
# inside the rect it is handed and never re-allocates the stack. NEITHER SDK.
add_library(sample_bands STATIC src/core/instrument/ui/sample_bands.cpp)
target_include_directories(sample_bands PUBLIC src)
target_link_libraries(sample_bands PUBLIC editor_geometry)

# sample_chrome — the CHROME band's interior: the toolbar row (title + Browse) over the
# control row (root strip, preview, velocity knob cell, curve button, channel toggle). Reads
# the band rect from sample_bands; owns no vertical inventory of its own. NEITHER SDK.
add_library(sample_chrome STATIC src/core/instrument/ui/sample_chrome.cpp)
target_include_directories(sample_chrome PUBLIC src)
target_link_libraries(sample_chrome PUBLIC sample_bands)

add_library(bridge_marshal STATIC src/core/instrument/map/bridge_marshal.cpp)
target_include_directories(bridge_marshal PUBLIC src)

# embed_strip (Phase S6) — PURE layout for the embedded TCP/MCP strip: the 128-key span ->
# key-span rects (the loaded capture's full span, and its root marker) and the level-band
# fill. Read-only, so no hit-test. Unit-tested outside the DAW, while the embed shell
# (src/shell/instrument/reasampler_embed.cpp) marshals REAPER's embed messages (the paint
# bitmap) into it. Links editor_geometry for the shared Rect.
add_library(embed_strip STATIC src/core/instrument/ui/embed_strip.cpp)
target_include_directories(embed_strip PUBLIC src)
target_link_libraries(embed_strip PUBLIC editor_geometry)

# sample_map (Phase S4; RESOLUTION half since Q-W2v) — PURE mapping logic for the
# instrument: the live bank blob -> selected sample (via the SHARED bank_book JSON parse,
# NOT a second parser), interleaved->mono downmix (the channel policy), the one parameter
# set's override-beats-intrinsic fold, and the SampleData build. Links the pure modules it
# composes — bank_book (shared JSON), wav_codec (shared WAV parse), velocity_curve (the
# curve field play_params.h carries) — and NEITHER SDK. NOT the voice engine: since the
# build's product is plain SampleData, the engine's object code is no longer a dependency.
# The VST3 shell (reasampler_processor) does the bridge read + file I/O off the audio
# thread, then calls these; the process callback stays allocation-free. The ComponentState
# codec is component_state_io below.
add_library(sample_map STATIC src/core/instrument/map/sample_map.cpp)
target_include_directories(sample_map PUBLIC src)
target_link_libraries(sample_map PUBLIC bank_book wav_codec velocity_curve peaks)

# component_state_io (Q-W2v split of sample_map, T4-13 ≡ T2-07) — the ComponentState
# ENVELOPE + params-payload binary codec (envelope v1..v11, params payload v1..v8, every
# lift preserved byte-identically; v1..v7 are the retired zone lists, read via the
# adopt-zone-one migration). Split so the codec — which grows on every envelope bump and is
# shared with the EXTENSION's preset-blob path (instrument_drop) — links WITHOUT the voice
# engine: its deps are velocity_curve (the curve field) and master_gain (the v8 wire cap)
# only; sampler_core/pitch_shift object code never enters the extension binary. Its own test
# target linking exactly these is the structural proof.
add_library(component_state_io STATIC src/core/instrument/map/component_state_io.cpp)
target_include_directories(component_state_io PUBLIC src)
target_link_libraries(component_state_io PUBLIC velocity_curve master_gain)

# capture_browser (Phase S10) — PURE card-grid + bank-filter-tab layout + hit-test for the
# capture-first editor's default face. The mirror of mode_switch/editor_geometry: the fiddly
# grid/tab arithmetic lives here, unit-tested outside the DAW; the editor shell draws each
# card's peak thumbnail + name + badge and routes clicks into it. Links editor_geometry for
# the shared Rect + contains(). NEITHER SDK.
add_library(capture_browser STATIC src/core/instrument/ui/capture_browser.cpp)
target_include_directories(capture_browser PUBLIC src)
target_link_libraries(capture_browser PUBLIC editor_geometry)

# keyboard_strip (Phase S10) — PURE piano-key geometry, hit-test, root marker, drag
# resolver, and note naming for the editor's keyboard strip (root display + root-set). The
# mirror of embed_strip; links editor_geometry for the shared Rect. NEITHER SDK.
add_library(keyboard_strip STATIC src/core/instrument/ui/keyboard_strip.cpp)
target_include_directories(keyboard_strip PUBLIC src)
target_link_libraries(keyboard_strip PUBLIC editor_geometry)

# waveform_view (Phase S11) — PURE frame<->pixel mapping, marker grab regions, drag-delta
# frame resolver, the zero-crossing snap, and the WAVEFORM band's drawn surface (channel
# lanes + the overlay area — see waveform_view.h) for the capture-first editor. The mirror of
# keyboard_strip; links editor_geometry for the shared Rect/OverlayArea, peaks for the
# AudioSample alias the snap scans + the per-lane Envelope split. sample_bands is a PRIVATE
# implementation dep (waveformLanes, used internally) — nothing in the public header needs it.
# NEITHER SDK.
add_library(waveform_view STATIC src/core/instrument/ui/waveform_view.cpp)
target_include_directories(waveform_view PUBLIC src)
target_link_libraries(waveform_view PUBLIC editor_geometry peaks)
target_link_libraries(waveform_view PRIVATE sample_bands)

# bank_sync (Phase S9/S8 reader) — PURE decision logic for the instrument's off-audio-thread
# poll: parse/compare the S9 bank-generation stamp, and the S8 assignment-request CONSUME
# decision (new-and-resolvable-and-target -> apply; unresolvable -> drop-and-mark; non-target
# -> stay eligible). The shell owns the timer cadence + side effects (reloadFromBank,
# setSelectedSampleId, component-state marker); this owns only the yes/no maths, unit-tested
# outside the DAW. Links assignment_request for the decoded AssignmentRequest it consumes.
# NEITHER SDK.
add_library(bank_sync STATIC src/core/instrument/map/bank_sync.cpp)
target_include_directories(bank_sync PUBLIC src)
target_link_libraries(bank_sync PUBLIC assignment_request)
target_link_libraries(bank_sync PRIVATE wire)

# browser_scroll (Phase S12) — PURE scroll-window + scrollbar-thumb + type-to-filter-search
# geometry LAYERED over the S10 capture_browser: the visible-card window, thumb rect +
# thumb-drag<->offset mapping, and the name-substring filter that composes with the bank
# filter. The mirror of capture_browser; links capture_browser (for BrowserLayout + the card
# metrics/cell rect) which pulls editor_geometry transitively. NEITHER SDK.
add_library(browser_scroll STATIC src/core/instrument/ui/browser_scroll.cpp)
target_include_directories(browser_scroll PUBLIC src)
target_link_libraries(browser_scroll PUBLIC capture_browser sample_chrome)

# param_slider (Phase S12 + the S15/S16 control surfaces deferred here) — PURE control-surface
# layout + hit-test + normalized value<->pixel mapping for the editor parameter panel (the
# Gate|Trigger + Varispeed|Preserve toggles and the AHDSR / Trigger / pitch-env sliders). The
# mirror of keyboard_strip; links editor_geometry for the shared Rect. Deliberately engine-free
# (no sampler_core types) — the shell owns the control-id -> param binding + the value DOMAIN
# mapping. NEITHER SDK.
add_library(param_slider STATIC src/core/instrument/ui/param_slider.cpp)
target_include_directories(param_slider PUBLIC src)
target_link_libraries(param_slider PUBLIC editor_geometry)

# trigger_seam (Phase S-VIEW-3) — PURE Trigger-mode frames<->fraction converter for the TRIGGER
# SEAM documented in envelope_overlay.h: triggerPlayLength / framesToFadeFraction /
# fadeFractionToFrames. Owns the one formula so pack (draw) and unpack (commit) are provably
# consistent. No shell/LICE/REAPER types — only <cstdint>. NEITHER SDK.
add_library(trigger_seam STATIC src/core/instrument/map/trigger_seam.cpp)
target_include_directories(trigger_seam PUBLIC src)

# envelope_overlay (Phase S-VIEW-3) — PURE amp-envelope -> polyline geometry for the Sample-view
# envelope overlay: AHDSR (Gate) / fade+%-length (Trigger) params + the sample's wall-clock
# duration -> a breakpoint polyline in the waveform rect, at the same time base waveform_view maps.
# The mirror of waveform_view / param_slider; links editor_geometry for the shared Rect.
# Deliberately engine-free (no sample_map / sampler_core) — the shell packs the one parameter
# set's stored AdsrSeconds / TriggerParams into the small AmpEnvelope view struct. NEITHER SDK.
add_library(envelope_overlay STATIC src/core/instrument/ui/envelope_overlay.cpp)
target_include_directories(envelope_overlay PUBLIC src)
target_link_libraries(envelope_overlay PUBLIC editor_geometry)

# envelope_edit (Phase S-VIEW-3) — PURE node hit-test + pixel-delta -> clamped-param inverse map
# for the draggable envelope nodes: monotonic-in-time + range-clamped (against caller-supplied
# slider maxima) so a drag can never produce a param a slider couldn't. The mirror of card_drag;
# links envelope_overlay for the shared node vocabulary + the timeToX/levelToY maps. NEITHER SDK.
add_library(envelope_edit STATIC src/core/instrument/ui/envelope_edit.cpp)
target_include_directories(envelope_edit PUBLIC src)
target_link_libraries(envelope_edit PUBLIC envelope_overlay)

# knob_deck (Wave B FB1, r11) — PURE knob-deck layout + hit-test for the recomposed Sample face:
# fenced groups (caption row + compact caption toggles + fixed 48x58 knob cells + optional row
# toggle), deterministic whole-group wrap, point -> control-id hit-test. The mirror of
# action_bar / param_slider; links editor_geometry for the shared Rect. Engine-free — cells and
# toggles carry opaque shell-owned control ids. NEITHER SDK.
add_library(knob_deck STATIC src/core/instrument/ui/knob_deck.cpp)
target_include_directories(knob_deck PUBLIC src)
target_link_libraries(knob_deck PUBLIC editor_geometry)

# curve_popup (Wave B FB1, r11) — PURE centered-sheet geometry for the velocity-curve popup
# editor: size clamps (60%/55% of window, 360..520 x 260..380), title row + close button, the
# curve-box border rect, and the outside-sheet dismissal test. The mirror of overflow_menu;
# links editor_geometry for the shared Rect. NEITHER SDK.
add_library(curve_popup STATIC src/core/instrument/ui/curve_popup.cpp)
target_include_directories(curve_popup PUBLIC src)
target_link_libraries(curve_popup PUBLIC editor_geometry)

# master_gain (Wave B FB1) — PURE dB<->linear<->knob-taper math for the post-mixer master gain
# (-inf..+24 dB; norm 0 = TRUE zero linear). One formula shared by the editor's Gain knob, the
# v8 component-state wire cap, and the processor's applied gain. Standard library only. NEITHER SDK.
add_library(master_gain STATIC src/core/instrument/engine/master_gain.cpp)
target_include_directories(master_gain PUBLIC src)

# filter — the per-voice TPT/SVF with a continuous morph (HP->BP->LP or HP->notch->LP, selected
# at prepare() time) and an in-loop drive stage.
# The Cortex-M4 source's virtual FilterBase/Filter/Biquad hierarchy dispatched per channel per
# sample, which the per-voice per-sample path forbids, so none of it came across. Control
# mapping, SVF coefficients, morph weights, and the filter type each get their own file;
# VoiceFilter::process is header-inline so the kernel still inlines at the call site. Standard
# library only. NEITHER SDK.
add_library(filter STATIC
    src/core/instrument/engine/filter/filter_params.cpp
    src/core/instrument/engine/filter/filter_coeffs.cpp
    src/core/instrument/engine/filter/filter_morph.cpp
    src/core/instrument/engine/filter/voice_filter.cpp)
target_include_directories(filter PUBLIC src)

# sample_bands: the band-stack allocator's vertical inventory, asserted as pure geometry
# (chrome / two-lane waveform / deck row) independent of any paint call — the contract the
# band owners downstream read.
add_executable(sample_bands_tests tests/test_sample_bands.cpp)
target_link_libraries(sample_bands_tests PRIVATE sample_bands)
add_test(NAME sample_bands_tests COMMAND sample_bands_tests)

add_executable(sample_chrome_tests tests/test_sample_chrome.cpp)
target_link_libraries(sample_chrome_tests PRIVATE sample_chrome)
add_test(NAME sample_chrome_tests COMMAND sample_chrome_tests)

add_executable(bridge_marshal_tests tests/test_bridge_marshal.cpp)
target_link_libraries(bridge_marshal_tests PRIVATE bridge_marshal)
add_test(NAME bridge_marshal_tests COMMAND bridge_marshal_tests)

add_executable(embed_strip_tests tests/test_embed_strip.cpp)
target_link_libraries(embed_strip_tests PRIVATE embed_strip)
add_test(NAME embed_strip_tests COMMAND embed_strip_tests)

# sample_map: the S4 mapping heart. Links ONLY sample_map + component_state_io (+ their
# pure deps) — NEITHER the VST3 SDK nor the REAPER SDK — the same structural
# plain-data-boundary proof the sampler_core test enforces. (The historical suite spans
# both halves of the Q-W2v split; the frozen-format assertions live here unmodified.)
add_executable(sample_map_tests tests/test_sample_map.cpp)
target_link_libraries(sample_map_tests PRIVATE sample_map component_state_io)
add_test(NAME sample_map_tests COMMAND sample_map_tests)

# component_state_io (Q-W2v): the ComponentState codec's OWN target. Links ONLY
# component_state_io (velocity_curve + master_gain transitively) — deliberately NO
# sampler_core/pitch_shift — the structural proof the codec is engine-free (T2-07).
add_executable(component_state_io_tests tests/test_component_state_io.cpp)
target_link_libraries(component_state_io_tests PRIVATE component_state_io)
add_test(NAME component_state_io_tests COMMAND component_state_io_tests)

add_executable(capture_browser_tests tests/test_capture_browser.cpp)
target_link_libraries(capture_browser_tests PRIVATE capture_browser)
add_test(NAME capture_browser_tests COMMAND capture_browser_tests)

add_executable(keyboard_strip_tests tests/test_keyboard_strip.cpp)
target_link_libraries(keyboard_strip_tests PRIVATE keyboard_strip sample_bands sample_chrome)
add_test(NAME keyboard_strip_tests COMMAND keyboard_strip_tests)

# waveform_view (S11): the pure marker geometry + zero-crossing snap. Links waveform_view (+
# its pure editor_geometry/peaks deps) plus sample_bands directly, since the test exercises
# waveformLanes/kWaveformMinHeight/kLaneGap, which waveform_view no longer re-exports. NEITHER SDK.
add_executable(waveform_view_tests tests/test_waveform_view.cpp)
target_link_libraries(waveform_view_tests PRIVATE waveform_view sample_bands)
add_test(NAME waveform_view_tests COMMAND waveform_view_tests)

# bank_sync (S9/S8 reader): the pure generation-parse + assignment-consume decision. Links
# ONLY bank_sync (+ its assignment_request dep) — NEITHER SDK — the plain-data-boundary proof.
add_executable(bank_sync_tests tests/test_bank_sync.cpp)
target_link_libraries(bank_sync_tests PRIVATE bank_sync)
add_test(NAME bank_sync_tests COMMAND bank_sync_tests)

# browser_scroll (S12): the pure scroll-window/thumb + search geometry over capture_browser.
add_executable(browser_scroll_tests tests/test_browser_scroll.cpp)
target_link_libraries(browser_scroll_tests PRIVATE browser_scroll)
add_test(NAME browser_scroll_tests COMMAND browser_scroll_tests)

# param_slider (S12 + S15/S16 control surfaces): the pure control-panel layout + slider/toggle
# value<->pixel mapping the editor parameter surface draws + routes against.
add_executable(param_slider_tests tests/test_param_slider.cpp)
target_link_libraries(param_slider_tests PRIVATE param_slider)
add_test(NAME param_slider_tests COMMAND param_slider_tests)

# trigger_seam (S-VIEW-3): the pure Trigger-mode frames<->fraction converter.
# Links ONLY trigger_seam — no editor_geometry dep, the plainest data-boundary proof possible.
add_executable(trigger_seam_tests tests/test_trigger_seam.cpp)
target_link_libraries(trigger_seam_tests PRIVATE trigger_seam)
add_test(NAME trigger_seam_tests COMMAND trigger_seam_tests)

# envelope_overlay (S-VIEW-3): the pure amp-envelope -> polyline geometry (Gate AHDSR + Trigger
# fade/%-length) at the waveform time base. Links ONLY envelope_overlay (+ its editor_geometry
# dep) — NEITHER SDK — the plain-data-boundary proof.
add_executable(envelope_overlay_tests tests/test_envelope_overlay.cpp)
target_link_libraries(envelope_overlay_tests PRIVATE envelope_overlay)
add_test(NAME envelope_overlay_tests COMMAND envelope_overlay_tests)

# envelope_edit (S-VIEW-3): the pure node hit-test + clamped/monotonic pixel->param inverse map.
# Links ONLY envelope_edit (+ its envelope_overlay dep) — NEITHER SDK.
add_executable(envelope_edit_tests tests/test_envelope_edit.cpp)
target_link_libraries(envelope_edit_tests PRIVATE envelope_edit)
add_test(NAME envelope_edit_tests COMMAND envelope_edit_tests)

# knob_deck (Wave B FB1): the pure r11 deck layout/wrap/hit-test. Links ONLY knob_deck — NEITHER SDK.
add_executable(knob_deck_tests tests/test_knob_deck.cpp)
target_link_libraries(knob_deck_tests PRIVATE knob_deck)
add_test(NAME knob_deck_tests COMMAND knob_deck_tests)

# curve_popup (Wave B FB1): the pure r11 popup-sheet geometry at the size clamps. NEITHER SDK.
add_executable(curve_popup_tests tests/test_curve_popup.cpp)
target_link_libraries(curve_popup_tests PRIVATE curve_popup)
add_test(NAME curve_popup_tests COMMAND curve_popup_tests)

# master_gain (Wave B FB1): the pure dB<->linear<->knob taper. NEITHER SDK.
add_executable(master_gain_tests tests/test_master_gain.cpp)
target_link_libraries(master_gain_tests PRIVATE master_gain)
add_test(NAME master_gain_tests COMMAND master_gain_tests)

# filter: four targets along the module's own seams, so each asserts one domain.
# filter_params_tests — the rate-free control mappings (cutoff/Q/drive) and their inverses.
# filter_morph_tests — the pure morph-weight algebra under both morph laws; no DSP is run.
# filter_state_tests — numerical stability, the denormal flush, bounded-output/self-oscillation
#   under full drive, and the state lifecycle — none of it needs the measurement harness below.
# filter_tests — the frequency response: pins the SVF coefficients against an independent
#   derivation, holds the morph endpoints to the analytic 2-pole targets, and measures the
#   HP-BP-LP corner flatness, the HP-notch-LP null, and rate/level invariance and drive
#   stability by driving real sines. The seams above were chosen so this file alone owns the
#   analytic reference and the steady-state gain measurement — a forked copy of a measurement
#   reference is a worse defect than a long file.
# NEITHER SDK.
add_executable(filter_params_tests tests/test_filter_params.cpp)
target_link_libraries(filter_params_tests PRIVATE filter)
add_test(NAME filter_params_tests COMMAND filter_params_tests)

add_executable(filter_morph_tests tests/test_filter_morph.cpp)
target_link_libraries(filter_morph_tests PRIVATE filter)
add_test(NAME filter_morph_tests COMMAND filter_morph_tests)

add_executable(filter_state_tests tests/test_filter_state.cpp)
target_link_libraries(filter_state_tests PRIVATE filter)
add_test(NAME filter_state_tests COMMAND filter_state_tests)

add_executable(filter_tests tests/test_filter.cpp)
target_link_libraries(filter_tests PRIVATE filter)
add_test(NAME filter_tests COMMAND filter_tests)

# ---------------------------------------------------------------------------
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
# ---------------------------------------------------------------------------
# LICE sources the bank_panel + draw_kit draw with: lice.cpp (LICE_SysBitmap, FillRect,
# GradRect, Clear, Blit) + lice_line.cpp (Line, DrawRect, RoundRect). lice_line.cpp's
# bezier helpers call LICE_FillCircle from lice_arc.cpp, so that TU is required to link.
# lice_textnew.cpp provides LICE_CachedFont (the kit's AA cached-font engine, Phase L L1 —
# the "temple os -> modern" text lever; see draw_kit.cpp). NOTE the "new" variant: it is the
# TU that implements the LICE_CachedFont CLASS (lice_text.cpp is the legacy bitmap-font
# renderer with no class and would not resolve the symbols). LICE routes GDI through native
# Win32 or, on mac/linux, the host SWELL (SWELL_PROVIDED_BY_APP).
set(LICE_SRC
    ${WDL_INC}/lice/lice.cpp
    ${WDL_INC}/lice/lice_line.cpp
    ${WDL_INC}/lice/lice_arc.cpp
    ${WDL_INC}/lice/lice_textnew.cpp
)

add_library(reaper_reasampler MODULE
    src/app/main.cpp
    src/shell/capture/capture.cpp
    src/shell/capture/capture_orchestrator.cpp
    src/shell/capture/capture_batch.cpp
    src/shell/capture/scope_resolve.cpp
    src/shell/capture/realtime_lifecycle.cpp
    src/shell/capture/capture_realtime_shell.cpp
    src/shell/capture/capture_realtime_finalize.cpp
    src/core/capture/capture_realtime.cpp
    src/shell/persist/session.cpp
    src/shell/persist/ext_state_io.cpp
    src/shell/persist/prune_fs.cpp
    src/shell/bank_ops/bank_ops.cpp
    src/shell/panel/panel_audition.cpp
    src/shell/panel/panel_bank_ops.cpp
    src/shell/panel/panel_drag.cpp
    src/shell/panel/panel_input.cpp
    src/shell/panel/panel_layout.cpp
    src/shell/panel/panel_render.cpp
    src/shell/panel/panel_thumbnails.cpp
    src/shell/panel/panel_window.cpp
    src/shell/panel/draw_kit.cpp
    src/core/view/mode_switch.cpp
    src/core/ui/tab_strip.cpp
    src/shell/capture/insert.cpp
    src/core/capture/insert_plan.cpp
    ${LICE_SRC}
    src/core/view/view_mode_model.cpp
    src/core/view/view_tree.cpp
    src/shell/view/view.cpp
    src/shell/capture/track_guid.cpp
    src/shell/capture/provenance_shell.cpp
    src/core/view/guid_diff.cpp
    src/core/view/lane_keys.cpp
    src/shell/capture/item_read.cpp
    src/shell/actions/action_registry.cpp
    src/shell/actions/design_view_actions.cpp
    src/shell/actions/bank_actions.cpp
    src/shell/actions/prune_action.cpp
    src/shell/actions/ingest.cpp
    src/core/model/bank_book.cpp
    src/core/model/bank_book_json.cpp
    src/core/model/owned_manifest.cpp
    src/shell/actions/drag_out_win.cpp
    src/shell/actions/instrument_drop_win.cpp
    src/core/ui/action_bar.cpp
    src/core/ui/footer_bar.cpp
    src/core/ui/overflow_menu.cpp
    src/core/ui/mode_enable.cpp
    src/core/ui/tooltip.cpp
    src/core/ui/card_meta.cpp
    src/core/ui/card_drag.cpp
    src/shell/persist/usage_scan.cpp
)
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control capture_realtime bank_book wav_codec owned_manifest prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip card_meta card_drag assignment_request bank_sync bridge_marshal sample_usage)
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
# artifacts load side-by-side. The CMake TARGET name stays "reaper_reasampler" for both
# configs — one source tree, one target; only the emitted file name forks by channel.
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "${REASAMPLER_OUTPUT_NAME}")

if(WIN32)
    # Native Win32. REAPER provides nothing extra to link. The bank_panel dialog
    # template (M5) is compiled from src/resource.rc by the platform RC compiler.
    target_sources(reaper_reasampler PRIVATE src/resource.rc)

elseif(APPLE)
    # macOS: use REAPER's OWN SWELL at runtime via the modstub.
    # Do NOT build full SWELL. SWELL_PROVIDED_BY_APP routes calls to the host.
    target_sources(reaper_reasampler PRIVATE ${SWELL}/swell-modstub.mm)
    target_compile_definitions(reaper_reasampler PRIVATE SWELL_PROVIDED_BY_APP)
    target_link_libraries(reaper_reasampler PRIVATE "-framework AppKit")
    set_target_properties(reaper_reasampler PROPERTIES SUFFIX ".dylib")
    # bank_panel dialog (M5): SWELL can't read a Win32 .rc directly. Run resgen
    # once to turn src/resource.rc into a C++ source, then add it here:
    #   php ${WDL_INC}/swell/mac_resgen.php src/resource.rc
    # target_sources(reaper_reasampler PRIVATE src/resource.rc_mac_dlg.h)  # generated

else()
    # Linux: REAPER's libSwell.so is used at runtime via the generic modstub.
    # With SWELL_PROVIDED_BY_APP you can drop pkg-config / -lX11 entirely.
    target_sources(reaper_reasampler PRIVATE ${SWELL}/swell-modstub-generic.cpp)
    target_compile_definitions(reaper_reasampler PRIVATE SWELL_PROVIDED_BY_APP)
    set_target_properties(reaper_reasampler PROPERTIES SUFFIX ".so")
    # bank_panel dialog (M5): reuse the macOS resgen output (see README /
    # CLAUDE.md §SWELL dialog resources), then add the generated source:
    #   php ${WDL_INC}/swell/mac_resgen.php src/resource.rc
    # target_sources(reaper_reasampler PRIVATE src/resource.rc_mac_dlg.h)  # generated
endif()

# ===========================================================================
# 5) The ReaSampler VST3 instrument — the SECOND build artifact (Phase S1).
#
# Windows-only, VST3-only, REAPER-only (D5). A separate native VST3 plugin the user
# instantiates on an instrument track. Additive: the reaper_reasampler target above
# builds unchanged. This is the S1 opening spike — a silent-but-loading
# SingleComponentEffect skeleton, an IPlugView<->LICE editor, and the REAPER VST-host
# bridge read — not yet a sampler.
#
# ONE-TIME SDK SUBMODULE SETUP (see README / .gitmodules): the vst3sdk superproject is
# vendored pinned to tag v3.7.9_build_61; only three of its sub-submodules are needed
# (VSTGUI/examples/tests are NOT). After `git submodule update --init vendor/vst3sdk`:
#     cd vendor/vst3sdk && git submodule update --init pluginterfaces base public.sdk
# ===========================================================================
set(VST3_SDK ${CMAKE_CURRENT_SOURCE_DIR}/vendor/vst3sdk)
# The VST3 module needs the nested vst3sdk slice (pluginterfaces / base / public.sdk)
# checked out — the one-time step documented above. When it is absent (a fresh clone
# that ran only the top-level `git submodule update --init`), skip the module rather than
# fail configure on missing sources: the pure geometry/mapping libraries + their CTest
# targets still build and test without the SDK. Probe one representative source file.
if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")

    # --- 5a) The bounded slice of the Steinberg VST3 SDK this spike needs. --------
    # Enumerated (not add_subdirectory of the whole SDK) to keep the build hermetic and
    # lean, matching the project's two-submodule discipline: no VSTGUI, no examples, no
    # SDK-global CMake helpers/install machinery. Pinned to tag v3.7.9_build_61, so the
    # list is fixed. If the SDK tag is bumped, re-verify this set.
    add_library(vst3_sdk STATIC
        # pluginterfaces/base — FUnknown, IIDs, string table, ustring.
        ${VST3_SDK}/pluginterfaces/base/funknown.cpp
        ${VST3_SDK}/pluginterfaces/base/coreiids.cpp
        ${VST3_SDK}/pluginterfaces/base/conststringtable.cpp
        ${VST3_SDK}/pluginterfaces/base/ustring.cpp
        # base/source — FObject, strings, buffers, streamer, debug, IIDs, update handler.
        ${VST3_SDK}/base/source/fobject.cpp
        ${VST3_SDK}/base/source/fstring.cpp
        ${VST3_SDK}/base/source/fbuffer.cpp
        ${VST3_SDK}/base/source/fstreamer.cpp
        ${VST3_SDK}/base/source/fdebug.cpp
        ${VST3_SDK}/base/source/baseiids.cpp
        ${VST3_SDK}/base/source/updatehandler.cpp
        ${VST3_SDK}/base/thread/source/flock.cpp
        # public.sdk/source/vst — the SingleComponentEffect base + its deps. NOTE:
        # vstsinglecomponenteffect.cpp #includes vsteditcontroller.cpp (unity-style), so
        # vsteditcontroller.cpp must NOT be listed separately (double definition).
        ${VST3_SDK}/public.sdk/source/vst/vstsinglecomponenteffect.cpp
        ${VST3_SDK}/public.sdk/source/vst/vstcomponentbase.cpp
        ${VST3_SDK}/public.sdk/source/vst/vstbus.cpp
        ${VST3_SDK}/public.sdk/source/vst/vstparameters.cpp
        ${VST3_SDK}/public.sdk/source/vst/vstinitiids.cpp
        # public.sdk/source/common — CPluginView (IPlugView base) + IIDs.
        ${VST3_SDK}/public.sdk/source/common/pluginview.cpp
        ${VST3_SDK}/public.sdk/source/common/commoniids.cpp
        # public.sdk/source/main — the class-factory (GetPluginFactory) support. NOTE:
        # dllmain.cpp + moduleinit.cpp (which carry the InitDll/ExitDll dll exports) are
        # compiled into the MODULE target directly, NOT here: their SMTG_EXPORT_SYMBOL
        # functions have no internal referrer, so the linker strips them from a static
        # lib. Compiling them into the module keeps the exports.
        ${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp
    )
    target_include_directories(vst3_sdk PUBLIC ${VST3_SDK})
    # The SDK requires exactly one of RELEASE / DEVELOPMENT (fdebug.cpp keys off it).
    target_compile_definitions(vst3_sdk PUBLIC $<IF:$<CONFIG:Debug>,DEVELOPMENT=1,RELEASE=1>)

    # --- 5b) The VST3 module (loadable .vst3 DLL). -------------------------------
    add_library(reasampler_vst MODULE
        src/shell/instrument/vst_entry.cpp
        # The processor family (Q-W2v, T4-12): lifecycle + process() whole (T4-29), with
        # component-state I/O and the off-thread reload/publish family in sibling TUs.
        src/shell/instrument/reasampler_processor.cpp
        src/shell/instrument/processor_state.cpp
        src/shell/instrument/processor_reload.cpp
        # The editor family, split on the Sample face's BAND axis: session/bridge state,
        # param plumbing + the shared band-layout resolve, then paint and input in matching
        # sets — dispatch, chrome, waveform, decks — plus the two band-independent surfaces
        # (the Browse modal and the velocity-curve popup) and the platform/window TU. The
        # layout math itself is PURE (core/instrument/ui/sample_bands + sample_chrome +
        # browser_scroll). Shared internals: editor_internal.h (no TU).
        src/shell/instrument/editor_session.cpp
        src/shell/instrument/editor_controls.cpp
        src/shell/instrument/editor_paint.cpp
        src/shell/instrument/editor_paint_chrome.cpp
        src/shell/instrument/editor_paint_waveform.cpp
        src/shell/instrument/editor_paint_deck.cpp
        src/shell/instrument/editor_paint_browse.cpp
        src/shell/instrument/editor_paint_curve.cpp
        src/shell/instrument/editor_input.cpp
        src/shell/instrument/editor_input_chrome.cpp
        src/shell/instrument/editor_input_waveform.cpp
        src/shell/instrument/editor_input_deck.cpp
        src/shell/instrument/editor_input_browse.cpp
        src/shell/instrument/editor_input_curve.cpp
        src/shell/instrument/editor_platform.cpp
        src/shell/instrument/reasampler_embed.cpp
        src/shell/instrument/reaper_bridge.cpp
        # The Phase L (L1) draw kit — the ONE source of drawing the editor + embed shells
        # now consume (L3). Compiled into the MODULE (not a static lib) for the same reason
        # bank_panel does: it is the only kit TU touching LICE, and its cached-font engine
        # (LICE_CachedFont) needs the LICE_SRC TUs below linked into this artifact.
        src/shell/panel/draw_kit.cpp
        # SDK module entry — compiled into the module (not the static lib) so the
        # InitDll/ExitDll dll exports survive the link (see vst3_sdk note above).
        ${VST3_SDK}/public.sdk/source/main/dllmain.cpp
        ${VST3_SDK}/public.sdk/source/main/moduleinit.cpp
        ${LICE_SRC}
    )
    # editor_geometry + bridge_marshal: the pure spike helpers. sample_map (S4): the pure
    # bank -> one-capture resolve + SampleData build the processor drives off the audio
    # thread; linking it pulls its pure deps (bank_book, wav_codec, velocity_curve, peaks)
    # transitively — deliberately NOT sampler_core (the voice engine). capture_paths: the
    # shared M4 path resolution (resolveBankFile / projectDirOfRpp) the bridge + processor
    # use. Its PUBLIC include dir (src)
    # gives the shell TUs their headers (ext_keys.h, bank_book.h, voice_engine.h, ...).
    # embed_strip (S6): the pure inline-strip layout + hit-test the embed shell marshals
    # into; it links editor_geometry transitively (shared Rect).
    # app_version: ext_keys.h's channel-derived namespace accessor (V4) delegates to it, so
    # the instrument reads the SAME namespace the extension writes; its PUBLIC include dir
    # (build/generated) carries version_generated.h for the channel bit.
    # sampler_core: the voice engine the processor drives (sample_map no longer pulls it —
    # its build yields plain SampleData — so the module links it directly.)
    # sample_bands + sample_chrome: the band-stack allocator the Sample face's three band
    # TUs read, and the chrome band's interior geometry.
    # capture_browser + keyboard_strip (S10): the pure card-grid/tab + keyboard-strip
    # geometry the capture-first editor draws + hit-tests against; both link editor_geometry
    # transitively (shared Rect).
    # waveform_view (S11): the pure frame<->pixel marker geometry + zero-crossing snap the
    # editor's waveform surface draws + hit-tests against; links editor_geometry + peaks
    # transitively (shared Rect + AudioSample).
    # bank_sync (S9/S8 reader): the pure generation-compare + assignment-consume decision the
    # processor's off-thread poll runs; links assignment_request transitively (the decoded
    # request it consumes) — the same key the extension writes, shared via the pure module.
    # browser_scroll + param_slider (S12 + S15/S16 control surfaces): the pure scroll/search
    # geometry over the capture browser, and the control-panel layout + slider/toggle
    # value<->pixel mapping the editor's parameter surface draws + routes against.
    # browser_scroll pulls capture_browser transitively; param_slider links editor_geometry /
    # the stdlib only. All engine-free, DAW-verified in the shell.
    # tooltip: the pure placement math (anchor + text extent -> clamped box) the piano
    # strip's note-name chip shares with the dock panel's hover tooltip.
    # theme + component_geometry + bank_grid: the Phase L (L1) draw-kit's PURE deps (L3). The
    # kit draws every editor/embed surface by palette ROLE via draw_kit.cpp (compiled into the
    # module above): theme supplies role->KitColor + spectralColor, component_geometry the
    # KitBox/button/slider geometry, bank_grid the shared dB display-compression the waveform
    # primitive uses. All REAPER/SWELL-free.
    # envelope_overlay + envelope_edit (S-VIEW-3): the pure amp-envelope -> polyline forward map
    # and the node hit-test + pixel-delta -> clamped-param inverse map the Sample-view envelope
    # overlay draws + drags against; envelope_edit links envelope_overlay transitively (shared
    # node vocabulary + timeToX/levelToY). Both engine-free, DAW-verified in the shell.
    # knob_deck + curve_popup + master_gain (Wave B FB1, r11): the pure deck layout/hit-test,
    # the curve-popup sheet geometry, and the master-gain taper the recomposed Sample face
    # draws + routes against (master_gain also rides in via sample_map for the v8 wire cap).
    # sample_usage (pS-usage): the usage-record wire + publish plan the processor's
    # reloadInstrument publishes through the bridge (the one sanctioned VST-side write).
    target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal
        sampler_core sample_map component_state_io capture_paths embed_strip app_version
        capture_browser keyboard_strip sample_bands sample_chrome
        waveform_view bank_sync browser_scroll param_slider tooltip
        theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
        knob_deck curve_popup master_gain sample_usage file_bytes)
    # SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
    # WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
    # src: the Q-W1 rooted include convention ("core/..." / "shell/..."). src/vst is GONE
    # (Q-W2v): the two former god TUs live split under shell/instrument/.
    target_include_directories(reasampler_vst PRIVATE src ${SDK_INC} ${WDL_INC})
    # A .vst3 is a DLL with a .vst3 extension and no lib-prefix. OUTPUT_NAME is the on-disk
    # product name, channel-forked (S18): reasampler_9000.vst3 (stable, byte-identical to
    # pre-S18) / reasampler_9000_beta.vst3 (beta) — driven by REASAMPLER_VST_OUTPUT_NAME set
    # from the ONE channel decision above, mirroring the extension's REASAMPLER_OUTPUT_NAME
    # and matching app_version::vstOutputName(). The two channels install side-by-side; the
    # per-channel VST3 class UID (reasampler_vst.h) keeps a saved instance rebinding to its
    # own channel (save-rename-reopen is a DAW-verify).
    set_target_properties(reasampler_vst PROPERTIES PREFIX "" SUFFIX ".vst3"
        OUTPUT_NAME "${REASAMPLER_VST_OUTPUT_NAME}")
endif()
