Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+120 -117
View File
@@ -69,7 +69,7 @@ endif()
# 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/version_generated.h.in
${CMAKE_CURRENT_SOURCE_DIR}/src/core/version/version_generated.h.in
${CMAKE_CURRENT_BINARY_DIR}/generated/version_generated.h
@ONLY)
@@ -107,7 +107,7 @@ 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/bank_model.cpp)
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)
@@ -115,7 +115,7 @@ 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/peaks.cpp)
add_library(peaks STATIC src/core/audio/peaks.cpp)
target_include_directories(peaks PUBLIC src)
# ---------------------------------------------------------------------------
@@ -123,7 +123,7 @@ target_include_directories(peaks PUBLIC src)
# 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/capture_paths.cpp)
add_library(capture_paths STATIC src/core/capture/capture_paths.cpp)
target_include_directories(capture_paths PUBLIC src)
# ---------------------------------------------------------------------------
@@ -132,7 +132,7 @@ target_include_directories(capture_paths PUBLIC src)
# 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/bank_grid.cpp)
add_library(bank_grid STATIC src/core/ui/bank_grid.cpp)
target_include_directories(bank_grid PUBLIC src)
# ---------------------------------------------------------------------------
@@ -142,7 +142,7 @@ target_include_directories(bank_grid PUBLIC src)
# 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/mode_switch.cpp)
add_library(mode_switch STATIC src/core/view/mode_switch.cpp)
target_include_directories(mode_switch PUBLIC src)
# ---------------------------------------------------------------------------
@@ -153,7 +153,7 @@ target_include_directories(mode_switch PUBLIC src)
# 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/tab_strip.cpp)
add_library(tab_strip STATIC src/core/ui/tab_strip.cpp)
target_include_directories(tab_strip PUBLIC src)
# ---------------------------------------------------------------------------
@@ -162,7 +162,7 @@ target_include_directories(tab_strip PUBLIC src)
# 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/view_mode_model.cpp)
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
@@ -177,7 +177,7 @@ target_link_libraries(view_mode_model PUBLIC lane_keys)
# 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/view_tree.cpp)
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)
@@ -189,7 +189,7 @@ target_link_libraries(view_tree PUBLIC view_mode_model)
# 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/guid_diff.cpp)
add_library(guid_diff STATIC src/core/view/guid_diff.cpp)
target_include_directories(guid_diff PUBLIC src)
# ---------------------------------------------------------------------------
@@ -200,7 +200,7 @@ target_include_directories(guid_diff PUBLIC src)
# 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/lane_keys.cpp)
add_library(lane_keys STATIC src/core/view/lane_keys.cpp)
target_include_directories(lane_keys PUBLIC src)
# ---------------------------------------------------------------------------
@@ -209,7 +209,7 @@ target_include_directories(lane_keys PUBLIC src)
# 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/insert_plan.cpp)
add_library(insert_plan STATIC src/core/capture/insert_plan.cpp)
target_include_directories(insert_plan PUBLIC src)
# ---------------------------------------------------------------------------
@@ -220,7 +220,7 @@ target_include_directories(insert_plan PUBLIC src)
# 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/render_settings.cpp)
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)
@@ -233,7 +233,7 @@ target_link_libraries(render_settings PUBLIC bank_model)
# 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/batch_capture.cpp)
add_library(batch_capture STATIC src/core/capture/batch_capture.cpp)
target_include_directories(batch_capture PUBLIC src)
# ---------------------------------------------------------------------------
@@ -244,7 +244,7 @@ target_include_directories(batch_capture PUBLIC src)
# 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/tail_control.cpp)
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)
@@ -257,7 +257,7 @@ target_link_libraries(tail_control PRIVATE json)
# 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/bank_book.cpp)
add_library(bank_book STATIC src/core/model/bank_book.cpp src/core/model/slot_map.cpp)
target_include_directories(bank_book PUBLIC src)
target_link_libraries(bank_book PUBLIC bank_model)
target_link_libraries(bank_book PRIVATE json)
@@ -271,7 +271,7 @@ target_link_libraries(bank_book PRIVATE json)
# pure type + JSON round-trip; mirror of wav_trim / tab_strip. B-cap writes +
# persists it; Phase R (R1/R2) consumes it — no prune logic here.
# ---------------------------------------------------------------------------
add_library(owned_manifest STATIC src/owned_manifest.cpp)
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)
@@ -284,7 +284,7 @@ target_link_libraries(owned_manifest PRIVATE json)
# 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/prune_reconcile.cpp)
add_library(prune_reconcile STATIC src/core/reclaim/prune_reconcile.cpp)
target_include_directories(prune_reconcile PUBLIC src)
# ---------------------------------------------------------------------------
@@ -295,7 +295,7 @@ target_include_directories(prune_reconcile PUBLIC src)
# 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/prune_button.cpp)
add_library(prune_button STATIC src/core/ui/prune_button.cpp)
target_include_directories(prune_button PUBLIC src)
# ---------------------------------------------------------------------------
@@ -306,7 +306,7 @@ target_include_directories(prune_button PUBLIC src)
# the DAW; the transport/temp-track/send/file-move recipe stays in capture.cpp.
# Depends on bank_model for the pure Sample / SourceMode types.
# ---------------------------------------------------------------------------
add_library(realtime_record STATIC src/realtime_record.cpp)
add_library(realtime_record STATIC src/core/capture/realtime_record.cpp)
target_include_directories(realtime_record PUBLIC src)
target_link_libraries(realtime_record PUBLIC bank_model)
@@ -320,7 +320,7 @@ target_link_libraries(realtime_record PUBLIC bank_model)
# patched RIFF/data size fields). The file read/write/truncate I/O stays in the
# realtime shell. Depends on peaks for the AudioSample float alias.
# ---------------------------------------------------------------------------
add_library(wav_trim STATIC src/wav_trim.cpp)
add_library(wav_trim STATIC src/core/capture/wav_trim.cpp)
target_include_directories(wav_trim PUBLIC src)
target_link_libraries(wav_trim PUBLIC peaks)
@@ -334,7 +334,7 @@ target_link_libraries(wav_trim PUBLIC peaks)
# 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/app_version.cpp)
add_library(app_version STATIC src/core/version/app_version.cpp)
target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/generated)
# ---------------------------------------------------------------------------
@@ -347,7 +347,7 @@ target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/ge
# registration stay in the shell (main.cpp / actions.cpp). No dependency on
# bank_model — it takes plain strings/values at its boundary.
# ---------------------------------------------------------------------------
add_library(provenance STATIC src/provenance.cpp)
add_library(provenance STATIC src/core/model/provenance.cpp)
target_include_directories(provenance PUBLIC src)
target_link_libraries(provenance PRIVATE wire)
@@ -361,7 +361,7 @@ target_link_libraries(provenance PRIVATE wire)
# 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/assignment_request.cpp)
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)
@@ -375,7 +375,7 @@ target_link_libraries(assignment_request PRIVATE wire)
# matcher.
# Linked by BOTH artifacts — the mirror of assignment_request, reversed direction.
# ---------------------------------------------------------------------------
add_library(sample_usage STATIC src/sample_usage.cpp)
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)
@@ -390,7 +390,7 @@ target_link_libraries(sample_usage PRIVATE wire)
# initiation (drag_out_win) and the bank_panel gesture hook are DAW-verified. Mirror
# of mode_switch.
# ---------------------------------------------------------------------------
add_library(drag_out STATIC src/drag_out.cpp)
add_library(drag_out STATIC src/core/ui/drag_out.cpp)
target_include_directories(drag_out PUBLIC src)
# ---------------------------------------------------------------------------
@@ -404,13 +404,13 @@ target_include_directories(drag_out PUBLIC src)
# parallel byte writer — so the cross-artifact contract cannot drift; links
# sample_map (which pulls bank_book/wav_trim/sampler_core transitively) and
# NEITHER SDK. The class-ID string derives from the FROZEN UID macros
# (src/vst/reasampler_uid.h, SDK-free), channel-selected via the generated
# (src/shell/instrument/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/instrument_drop.cpp)
target_include_directories(instrument_drop PUBLIC src src/vst ${CMAKE_CURRENT_BINARY_DIR}/generated)
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 sample_map)
# ---------------------------------------------------------------------------
@@ -423,7 +423,7 @@ target_link_libraries(instrument_drop PUBLIC sample_map)
# 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/theme.cpp)
add_library(theme STATIC src/core/ui/theme.cpp)
target_include_directories(theme PUBLIC src)
# ---------------------------------------------------------------------------
@@ -436,7 +436,7 @@ target_include_directories(theme PUBLIC src)
# SliderGeometry/ListRowBox avoid the existing ButtonRect/CellRect collisions.
# Mirror of prune_button — pure, CTest-covered.
# ---------------------------------------------------------------------------
add_library(component_geometry STATIC src/component_geometry.cpp)
add_library(component_geometry STATIC src/core/ui/component_geometry.cpp)
target_include_directories(component_geometry PUBLIC src)
# ---------------------------------------------------------------------------
@@ -451,7 +451,7 @@ target_include_directories(component_geometry PUBLIC src)
# Main_OnCommand dispatch + kbd_getTextFromCmd query are DAW-verified. Mirror of
# mode_switch / prune_button.
# ---------------------------------------------------------------------------
add_library(action_bar STATIC src/action_bar.cpp)
add_library(action_bar STATIC src/core/ui/action_bar.cpp)
target_include_directories(action_bar PUBLIC src)
# ---------------------------------------------------------------------------
@@ -465,7 +465,7 @@ target_include_directories(action_bar PUBLIC src)
# DAW-verified. Reuses prune_button's FooterRect input type. Mirror of action_bar /
# mode_switch / prune_button.
# ---------------------------------------------------------------------------
add_library(footer_bar STATIC src/footer_bar.cpp)
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)
@@ -478,7 +478,7 @@ target_link_libraries(footer_bar PUBLIC prune_button)
# L1-kit draw + TrackPopupMenu popup + NamedCommandLookup/Main_OnCommand dispatch are
# DAW-verified. Mirror of prune_button / mode_switch.
# ---------------------------------------------------------------------------
add_library(overflow_menu STATIC src/overflow_menu.cpp)
add_library(overflow_menu STATIC src/core/ui/overflow_menu.cpp)
target_include_directories(overflow_menu PUBLIC src)
# ---------------------------------------------------------------------------
@@ -490,7 +490,7 @@ target_include_directories(overflow_menu PUBLIC src)
# 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/mode_enable.cpp)
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)
@@ -502,7 +502,7 @@ target_link_libraries(mode_enable PUBLIC view_mode_model)
# 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/tooltip.cpp)
add_library(tooltip STATIC src/core/ui/tooltip.cpp)
target_include_directories(tooltip PUBLIC src)
# ---------------------------------------------------------------------------
@@ -513,7 +513,7 @@ target_include_directories(tooltip PUBLIC src)
# 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/card_meta.cpp)
add_library(card_meta STATIC src/core/ui/card_meta.cpp)
target_include_directories(card_meta PUBLIC src)
# ---------------------------------------------------------------------------
@@ -526,7 +526,7 @@ target_include_directories(card_meta PUBLIC src)
# 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/card_drag.cpp)
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)
@@ -547,8 +547,8 @@ target_link_libraries(card_drag PUBLIC drag_out bank_grid)
# 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/vst/pitch_shift.cpp)
target_include_directories(pitch_shift PUBLIC src src/vst)
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
@@ -556,11 +556,11 @@ target_link_libraries(pitch_shift PUBLIC peaks)
# explicit pixel box, not a Rect) so the engine can depend on it WITHOUT gaining a transitive
# dependency on the editor's layout types. sampler_core depends on it (KeyZone carries a
# VelocityCurve; Voice::start eval's it). Mirror of pitch_shift's role, one layer below the engine.
add_library(velocity_curve STATIC src/vst/velocity_curve.cpp)
target_include_directories(velocity_curve PUBLIC src/vst)
add_library(velocity_curve STATIC src/core/instrument/engine/velocity_curve.cpp)
target_include_directories(velocity_curve PUBLIC src)
add_library(sampler_core STATIC src/vst/sampler_core.cpp)
target_include_directories(sampler_core PUBLIC src src/vst)
add_library(sampler_core STATIC src/core/instrument/engine/sampler_core.cpp)
target_include_directories(sampler_core PUBLIC src)
target_link_libraries(sampler_core PUBLIC peaks pitch_shift velocity_curve)
# ---------------------------------------------------------------------------
@@ -671,7 +671,7 @@ add_test(NAME app_version_tests COMMAND app_version_tests)
# 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/app_version.cpp against that
# 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
@@ -688,20 +688,20 @@ function(_configure_padding_canary)
# 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/version_generated.h.in
${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/app_version.cpp rather
# 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/app_version.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)
@@ -800,19 +800,19 @@ add_test(NAME velocity_curve_tests COMMAND velocity_curve_tests)
# reader (mirror of capture_paths/wav_trim). Both are unit-tested outside the DAW;
# the VST3 shell (src/vst/*) that draws/routes/invokes is DAW-verified.
# ---------------------------------------------------------------------------
add_library(editor_geometry STATIC src/vst/editor_geometry.cpp)
target_include_directories(editor_geometry PUBLIC src/vst)
add_library(editor_geometry STATIC src/core/instrument/ui/editor_geometry.cpp)
target_include_directories(editor_geometry PUBLIC src)
add_library(bridge_marshal STATIC src/vst/bridge_marshal.cpp)
target_include_directories(bridge_marshal PUBLIC src/vst)
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 + hit-test for the embedded TCP/MCP strip: the
# 128-key span -> zone-segment rects, point -> zone selection, and the level-band fill.
# The mirror of editor_geometry (whose Rect + contains() it reuses); unit-tested outside
# the DAW, while the embed shell (src/vst/reasampler_embed.cpp) marshals REAPER's embed
# the DAW, while the embed shell (src/shell/instrument/reasampler_embed.cpp) marshals REAPER's embed
# messages (paint bitmap + mouse coords) into it. Links editor_geometry for the shared Rect.
add_library(embed_strip STATIC src/vst/embed_strip.cpp)
target_include_directories(embed_strip PUBLIC src/vst)
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) — PURE mapping logic for the Tier-0 instrument: the live bank
@@ -823,8 +823,8 @@ target_link_libraries(embed_strip PUBLIC editor_geometry)
# sampler_core (the Keymap/SampleData it yields) — and NEITHER SDK. The VST3 shell
# (reasampler_processor.cpp) does the bridge read + file I/O off the audio thread, then
# calls these; the process callback stays allocation-free.
add_library(sample_map STATIC src/vst/sample_map.cpp)
target_include_directories(sample_map PUBLIC src/vst src)
add_library(sample_map STATIC src/core/instrument/map/sample_map.cpp)
target_include_directories(sample_map PUBLIC src)
# master_gain: the v8 component-state master-gain field validates against the pure taper's
# linear cap at the (de)serialization boundary (one cap, shared with the knob + the processor).
target_link_libraries(sample_map PUBLIC bank_book wav_trim sampler_core master_gain)
@@ -834,16 +834,16 @@ target_link_libraries(sample_map PUBLIC bank_book wav_trim sampler_core master_g
# 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/vst/capture_browser.cpp)
target_include_directories(capture_browser PUBLIC src/vst)
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 key-span<->pixel mapping, root marker, zone-bar rects +
# edge-grab hit regions, and the drag-delta note resolver for the capture-first editor's
# keyboard strip (single-capture root-set) and the opt-in Zones panel (S10-Z). The mirror of
# embed_strip; links editor_geometry for the shared Rect. NEITHER SDK.
add_library(keyboard_strip STATIC src/vst/keyboard_strip.cpp)
target_include_directories(keyboard_strip PUBLIC src/vst)
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
@@ -851,8 +851,8 @@ target_link_libraries(keyboard_strip PUBLIC editor_geometry)
# (draggable start + loop markers over the picked capture's decoded PCM). The mirror of
# keyboard_strip; links editor_geometry for the shared Rect and peaks for the AudioSample
# alias the snap scans. NEITHER SDK.
add_library(waveform_view STATIC src/vst/waveform_view.cpp)
target_include_directories(waveform_view PUBLIC src/vst src)
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)
# bank_sync (Phase S9/S8 reader) — PURE decision logic for the instrument's off-audio-thread
@@ -862,8 +862,8 @@ target_link_libraries(waveform_view PUBLIC editor_geometry peaks)
# 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/vst/bank_sync.cpp)
target_include_directories(bank_sync PUBLIC src/vst src)
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)
@@ -872,15 +872,15 @@ target_link_libraries(bank_sync PRIVATE wire)
# 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/vst/browser_scroll.cpp)
target_include_directories(browser_scroll PUBLIC src/vst)
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)
# note_entry (Phase S12) — PURE text->clamped-MIDI-note parse for the direct numeric entry of
# a zone's low/high/root (decimal integer OR note name under the C4==60 convention, clamped to
# [0,127]). No dependency beyond the standard library. NEITHER SDK.
add_library(note_entry STATIC src/vst/note_entry.cpp)
target_include_directories(note_entry PUBLIC src/vst)
add_library(note_entry STATIC src/core/instrument/map/note_entry.cpp)
target_include_directories(note_entry PUBLIC src)
# 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
@@ -888,16 +888,16 @@ target_include_directories(note_entry PUBLIC src/vst)
# 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/vst/param_slider.cpp)
target_include_directories(param_slider PUBLIC src/vst)
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/vst/trigger_seam.cpp)
target_include_directories(trigger_seam PUBLIC src/vst)
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
@@ -905,16 +905,16 @@ target_include_directories(trigger_seam PUBLIC src/vst)
# 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 zone's stored
# AdsrSeconds / TriggerParams into the small AmpEnvelope view struct. NEITHER SDK.
add_library(envelope_overlay STATIC src/vst/envelope_overlay.cpp)
target_include_directories(envelope_overlay PUBLIC src/vst)
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/vst/envelope_edit.cpp)
target_include_directories(envelope_edit PUBLIC src/vst)
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:
@@ -922,23 +922,23 @@ target_link_libraries(envelope_edit PUBLIC envelope_overlay)
# 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/vst/knob_deck.cpp)
target_include_directories(knob_deck PUBLIC src/vst)
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/vst/curve_popup.cpp)
target_include_directories(curve_popup PUBLIC src/vst)
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/vst/master_gain.cpp)
target_include_directories(master_gain PUBLIC src/vst)
add_library(master_gain STATIC src/core/instrument/engine/master_gain.cpp)
target_include_directories(master_gain PUBLIC src)
add_executable(editor_geometry_tests tests/test_editor_geometry.cpp)
target_link_libraries(editor_geometry_tests PRIVATE editor_geometry)
@@ -1048,40 +1048,40 @@ set(LICE_SRC
)
add_library(reaper_reasampler MODULE
src/main.cpp
src/capture.cpp
src/capture_realtime.cpp
src/realtime_record.cpp
src/app/main.cpp
src/shell/capture/capture.cpp
src/shell/capture/capture_realtime.cpp
src/core/capture/realtime_record.cpp
src/persist.cpp
src/bank_panel.cpp
src/draw_kit.cpp
src/mode_switch.cpp
src/tab_strip.cpp
src/insert.cpp
src/insert_plan.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/view_mode_model.cpp
src/view_tree.cpp
src/view.cpp
src/track_guid.cpp
src/provenance_shell.cpp
src/guid_diff.cpp
src/lane_keys.cpp
src/item_read.cpp
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/actions.cpp
src/ingest.cpp
src/bank_book.cpp
src/owned_manifest.cpp
src/drag_out_win.cpp
src/instrument_drop_win.cpp
src/action_bar.cpp
src/footer_bar.cpp
src/overflow_menu.cpp
src/mode_enable.cpp
src/tooltip.cpp
src/card_meta.cpp
src/card_drag.cpp
src/usage_scan.cpp
src/core/model/bank_book.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 realtime_record bank_book wav_trim 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 sample_usage)
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
@@ -1186,16 +1186,16 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
# --- 5b) The VST3 module (loadable .vst3 DLL). -------------------------------
add_library(reasampler_vst MODULE
src/vst/vst_entry.cpp
src/shell/instrument/vst_entry.cpp
src/vst/reasampler_processor.cpp
src/vst/reasampler_editor.cpp
src/vst/reasampler_embed.cpp
src/vst/reaper_bridge.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/draw_kit.cpp
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
@@ -1248,7 +1248,10 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
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.
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
# src: the Q-W1 rooted include convention ("core/..." / "shell/..."). src/vst: the
# two not-yet-split god TUs (reasampler_editor / reasampler_processor, Q-W2v) still
# live there and are included flat by the shell TUs.
target_include_directories(reasampler_vst PRIVATE src src/vst ${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
+11 -10
View File
@@ -1,3 +1,4 @@
#include "core/namespaces.h"
// actions.cpp — the Design View action family (Phase D4). See actions.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
@@ -25,16 +26,16 @@
#include <string>
#include <vector>
#include "app_version.h" // channelCommandId / channelActionName — one channel-identity point
#include "core/version/app_version.h" // channelCommandId / channelActionName — one channel-identity point
#include "bank_book.h" // BankBook, nextBankId, TransferResult, kPoolBankId (B1)
#include "core/model/bank_book.h" // BankBook, nextBankId, TransferResult, kPoolBankId (B1)
#include "bank_panel.h" // selection seam + full-height toggles (B3/B4)
#include "item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
#include "lane_keys.h" // isOnManualLane — the single managed/manual predicate
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
#include "core/view/lane_keys.h" // isOnManualLane — the single managed/manual predicate
#include "persist.h" // ReaSamplerSession (owns book() + view() model)
#include "track_guid.h" // shared MediaTrack* -> canonical GUID key
#include "view.h" // applyMode + mintManagedLanes (D2 shell)
#include "view_mode_model.h"
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
#include "shell/view/view.h" // applyMode + mintManagedLanes (D2 shell)
#include "core/view/view_mode_model.h"
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs)
@@ -431,7 +432,7 @@ void designViewUnregisterActions(reaper_plugin_info_t* rec) {
//
// REFERENCE-INVALIDATION GUARDRAIL (B2 review): book().activeIndex() / bank()->index
// return a reference INTO the book's internal vector, which a create/delete can
// reallocate. No handler here caches a BankIndex& (or a Bank*) across a structural
// reallocate. No handler here caches a BankModel& (or a Bank*) across a structural
// mutation — each resolves ids to strings up front and re-resolves after any
// create/delete. Move/copy pass ids (not references) straight to moveSample/copySample.
@@ -720,7 +721,7 @@ void doBankTransferSelected(bool copy) {
return;
}
// Source = the bank the selection lives in (the focused region's displayed bank).
// Pass ids by value — no BankIndex& is cached across the loop's mutations.
// Pass ids by value — no BankModel& is cached across the loop's mutations.
const std::string srcId = bankPanelSelectedSourceBankId();
if (srcId == destId) {
ShowConsoleMsg("ReaSampler: source and destination are the same bank.\n");
@@ -788,7 +789,7 @@ void doBankRemoveSelected() {
return;
}
// Perform the removes (this-bank scope). Pass ids by value — no BankIndex& is cached
// Perform the removes (this-bank scope). Pass ids by value — no BankModel& is cached
// across the loop's mutations. Count real drops so the no-op guardrail can skip the
// undo point when nothing was removed (every id was already absent).
int removed = 0;
+1
View File
@@ -1,3 +1,4 @@
#include "core/namespaces.h"
#pragma once
// actions — the Design View action family (Phase D4). Registers the bindable
// actions that drive the mode workflow and wires them end-to-end: toggle/activate
+12 -11
View File
@@ -1,3 +1,4 @@
#include "core/namespaces.h"
// main.cpp — the SINGLE translation unit that OWNS the REAPER API pointers.
//
// This file is the entire contract between REAPER and the extension:
@@ -27,19 +28,19 @@
#include <vector>
#include "actions.h"
#include "app_version.h"
#include "bank_model.h"
#include "core/version/app_version.h"
#include "core/model/bank_model.h"
#include "bank_panel.h"
#include "batch_capture.h"
#include "capture.h"
#include "core/capture/batch_capture.h"
#include "shell/capture/capture.h"
#include "ingest.h"
#include "insert.h"
#include "shell/capture/insert.h"
#include "persist.h"
#include "provenance.h"
#include "provenance_shell.h"
#include "render_settings.h"
#include "track_guid.h"
#include "view.h"
#include "core/model/provenance.h"
#include "shell/capture/provenance_shell.h"
#include "core/capture/render_settings.h"
#include "shell/capture/track_guid.h"
#include "shell/view/view.h"
#include <filesystem> // project-dir derivation for provenance parent resolution
@@ -175,7 +176,7 @@ static int g_cmdCancelRealtime = 0;
// window). The user copies this line into a bug report.
static int g_cmdShowVersion = 0;
// The persistence session (M4): owns the in-memory BankIndex and bridges it to
// The persistence session (M4): owns the in-memory BankModel and bridges it to
// project ext state. A timer tick drives g_session.poll() to detect project
// load / Save-As; capture adds Samples to g_session.bank() — which (B2) resolves to
// the ACTIVE bank's index inside the session's BankBook; after a capture we serialize
+47 -46
View File
@@ -1,3 +1,4 @@
#include "core/namespaces.h"
// bank_panel.cpp — REAPER-facing docked grid (M5 Wave A/B + Phase B4). See
// bank_panel.h.
//
@@ -32,7 +33,7 @@
//
// REFERENCE-INVALIDATION GUARDRAIL (CONTEXT.md §Multi-bank): a bank-structural
// mutation (create/delete/evacuate/activate/move) can reallocate the book's vector,
// so a BankIndex& / Bank* must NEVER be cached across one. Every handler below
// so a BankModel& / Bank* must NEVER be cached across one. Every handler below
// resolves fresh AFTER any mutation and passes bank IDS (not references) into the
// model ops.
@@ -47,39 +48,39 @@
#include <unordered_map>
#include <vector>
#include "action_bar.h" // pure TASK-GROUPED action-bar layout + hit-test (L2)
#include "core/ui/action_bar.h" // pure TASK-GROUPED action-bar layout + hit-test (L2)
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B panel path)
#include "drag_out.h" // pure gesture-boundary decision + path-list assembly (M11)
#include "drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
#include "app_version.h" // channelCommandId — compose the named-command lookup string (M11)
#include "bank_book.h"
#include "bank_grid.h"
#include "bank_model.h"
#include "card_drag.h" // L7 pure gesture precedence + sparse slot layout/hit-test
#include "card_meta.h" // L7 decorative overlay formatters: bars.beats + s.ms (pure)
#include "capture_paths.h"
#include "component_geometry.h" // KitBox — the kit text()'s draw box (L1)
#include "draw_kit.h" // kit text() over cached AA fonts — retires GDI DrawText (L1)
#include "footer_bar.h" // pure footer LEFT-group layout: toggle + count + Tail button (L4)
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
#include "core/ui/drag_out.h" // pure gesture-boundary decision + path-list assembly (M11)
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
#include "core/version/app_version.h" // channelCommandId — compose the named-command lookup string (M11)
#include "core/model/bank_book.h"
#include "core/ui/bank_grid.h"
#include "core/model/bank_model.h"
#include "core/ui/card_drag.h" // L7 pure gesture precedence + sparse slot layout/hit-test
#include "core/ui/card_meta.h" // L7 decorative overlay formatters: bars.beats + s.ms (pure)
#include "core/capture/capture_paths.h"
#include "core/ui/component_geometry.h" // KitBox — the kit text()'s draw box (L1)
#include "shell/panel/draw_kit.h" // kit text() over cached AA fonts — retires GDI DrawText (L1)
#include "core/ui/footer_bar.h" // pure footer LEFT-group layout: toggle + count + Tail button (L4)
#include "core/view/guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
#include "ingest.h" // ingestDroppedFiles — S8 drop-onto-panel ingest
#include "instrument_drop.h" // pure buildInstrumentDropPreset — the .vstpreset payload (S17)
#include "instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop shell (S17)
#include "item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
#include "mode_enable.h" // opposite-mode tag-button enablement predicate (pure, L5)
#include "mode_switch.h"
#include "overflow_menu.h" // top-toolbar More-button geometry + reserve (pure, L5)
#include "peaks.h"
#include "core/wire/instrument_drop.h" // pure buildInstrumentDropPreset — the .vstpreset payload (S17)
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop shell (S17)
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
#include "core/view/lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
#include "core/ui/mode_enable.h" // opposite-mode tag-button enablement predicate (pure, L5)
#include "core/view/mode_switch.h"
#include "core/ui/overflow_menu.h" // top-toolbar More-button geometry + reserve (pure, L5)
#include "core/audio/peaks.h"
#include "persist.h"
#include "prune_button.h" // footer prune-button layout + hit-test (pure, R3)
#include "tooltip.h" // tooltip placement + prefix-strip (pure, L5)
#include "render_settings.h" // captureActionTable — the table-driven button rows (M11)
#include "tab_strip.h"
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
#include "track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
#include "view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires
#include "view_mode_model.h" // autoTagNewContent / NewItem (D2 Wave 2)
#include "core/ui/prune_button.h" // footer prune-button layout + hit-test (pure, R3)
#include "core/ui/tooltip.h" // tooltip placement + prefix-strip (pure, L5)
#include "core/capture/render_settings.h" // captureActionTable — the table-driven button rows (M11)
#include "core/ui/tab_strip.h"
#include "core/capture/tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
#include "shell/view/view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem (D2 Wave 2)
// SWELL / LICE. On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP);
// on Windows we use native Win32 (windows.h first, then swell.h no-ops on _WIN32).
@@ -390,10 +391,10 @@ std::string currentProjectDir() {
BankBook* book() { return g_panel.session ? &g_panel.session->book() : nullptr; }
// The BankIndex a region currently displays. Pool region -> the pool; banks region ->
// The BankModel a region currently displays. Pool region -> the pool; banks region ->
// the shown tab's bank (or nullptr when no named banks / the id went stale). Resolved
// FRESH every call (never cached across a mutation).
const BankIndex* indexForRegion(Region r) {
const BankModel* indexForRegion(Region r) {
BankBook* b = book();
if (!b) return nullptr;
if (r == Region::Pool) return &b->pool().index;
@@ -1256,7 +1257,7 @@ RECT createBtnRect(const RECT& region) {
// --- L7 slot-order display bridge ---------------------------------------------
//
// L7 re-maps cell index <-> sample identity: the grid draws in the bank's persisted
// SlotMap order (sparse, gap-preserving), NOT BankIndex insertion order. This one helper
// SlotMap order (sparse, gap-preserving), NOT BankModel insertion order. This one helper
// is the single place that resolves a region's display, composed purely from bank_book's
// slot order (orderedSampleIds) + card_drag's sparse slot rects (computeSlotRects) — the
// shell adds no layout math of its own.
@@ -1337,7 +1338,7 @@ RegionDisplay focusedDisplay() {
// viewport. `selectionOwner` is true when this region holds the live selection, so
// its cells show selection/focus chrome; the other region draws plain.
void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
const BankIndex* index, const std::string& emptyMsg,
const BankModel* index, const std::string& emptyMsg,
bool selectionOwner, const std::string& projectDir, Region reg) {
const RECT grid = regionGridRect(region, isBanks);
if (grid.bottom <= grid.top) return;
@@ -1348,7 +1349,7 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
}
// L7: iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense
// BankIndex insertion order. Selection/focus are keyed by the occupied-ordinal (selection
// BankModel insertion order. Selection/focus are keyed by the occupied-ordinal (selection
// space); a slot maps back to its ordinal via selectionForSlot.
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
// FA3 gap-free: request one bin per drawn pixel column; drawWaveform's
@@ -1697,7 +1698,7 @@ bool refreshFingerprint() {
stopAudition();
}
reconcileShownBank();
const BankIndex* idx = indexForRegion(g_panel.focusedRegion);
const BankModel* idx = indexForRegion(g_panel.focusedRegion);
g_panel.selItemCount = idx ? static_cast<int>(idx->size()) : 0;
return true;
}
@@ -1926,11 +1927,11 @@ void deinitPreview() {
// Auditions the sample at selection ordinal `idx` of the FOCUSED region's displayed bank.
// L7: `idx` is a DISPLAY-order (slot) ordinal, resolved through orderedIds, not a raw
// BankIndex position.
// BankModel position.
void startAudition(int idx) {
stopAudition();
const BankIndex* index = indexForRegion(g_panel.focusedRegion);
const BankModel* index = indexForRegion(g_panel.focusedRegion);
if (!index) return;
const RegionDisplay disp = focusedDisplay();
if (idx < 0 || idx >= disp.occupiedCount()) return;
@@ -1978,7 +1979,7 @@ void invalidatePanel() {
// exactly one occupied slot (gaps are empty slots, which the index never backs), so the
// raw index size IS the dense selection-space extent.
int focusedItemCount() {
const BankIndex* idx = indexForRegion(g_panel.focusedRegion);
const BankModel* idx = indexForRegion(g_panel.focusedRegion);
return idx ? static_cast<int>(idx->size()) : 0;
}
@@ -2006,7 +2007,7 @@ bool regionAt(int x, int y, Region& out) {
// --- Bank management ops (id-keyed; drive the B1 model + persist) --------------
//
// Each op mutates g_session.book() then persists via persistBankOp(). After a
// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankIndex& is invalid — we
// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we
// resolve fresh, pass ids, and let the next refreshFingerprint repaint. On an
// unsaved project the empty-close discard in persistBankOp ensures no stale state
// survives (matches the capture/B3 quiet-persist idiom).
@@ -2123,7 +2124,7 @@ void doActivateBank(const std::string& bankId) {
}
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Both pass
// ids straight to the model op (no BankIndex& cached across the loop's mutations).
// ids straight to the model op (no BankModel& cached across the loop's mutations).
//
// NO-OP GUARDRAIL — VERB-AWARE (matches the action layer's doBankTransferSelected):
// * MOVE collapse: the source entry WAS removed (bank_book removes unconditionally
@@ -2166,7 +2167,7 @@ void transferSamples(const std::vector<std::string>& sampleIds,
// the file: a last-reference remove leaves the file on disk, orphaned until Phase R
// prune — remove NEVER deletes bytes (the manifest is untouched). Removes are silent
// (no confirm dialog); recoverability is provided by the batched REAPER undo (R-B) —
// one Ctrl-Z restores the index entry. Ids passed by value — no BankIndex& cached
// one Ctrl-Z restores the index entry. Ids passed by value — no BankModel& cached
// across the loop's mutations.
void removeSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
@@ -2190,7 +2191,7 @@ void removeSamples(const std::vector<std::string>& sampleIds,
// The selection's sample ids resolved against the FOCUSED region's bank (source of a
// move/copy). Returns ids in bank order; empty when nothing selected.
std::vector<std::string> focusedSelectionIds() {
// L7: selection ordinals index the DISPLAY (slot) order, not BankIndex insertion order.
// L7: selection ordinals index the DISPLAY (slot) order, not BankModel insertion order.
// orderedIds[i] is the id at selection ordinal i.
std::vector<std::string> ids;
const RegionDisplay disp = focusedDisplay();
@@ -2212,7 +2213,7 @@ std::vector<std::string> resolveDragPathsForOs() {
std::vector<ResolvedSample> resolved;
BankBook* b = book();
if (!b) return {};
const BankIndex* idx = b->index(g_panel.dragSourceBankId);
const BankModel* idx = b->index(g_panel.dragSourceBankId);
if (!idx) return {};
const std::string projectDir = currentProjectDir();
@@ -3138,7 +3139,7 @@ void onLBtnUp(int x, int y) {
// collapses the multi-selection to the pressed cell (standard behavior).
// Release capture acquired at arm time (handleClick) — drag never started.
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
const BankIndex* idx = indexForRegion(g_panel.focusedRegion);
const BankModel* idx = indexForRegion(g_panel.focusedRegion);
const int count = idx ? static_cast<int>(idx->size()) : 0;
const int focus = g_panel.selection.focus;
if (focus >= 0)
+2 -1
View File
@@ -1,3 +1,4 @@
#include "core/namespaces.h"
#pragma once
// bank_panel — the docked grid window (M5, Wave A). REAPER-facing shell: it owns
// a SWELL dialog docked via DockWindowAddEx, and paints the current project's
@@ -13,7 +14,7 @@
#include <string>
#include <vector>
#include "tail_control.h" // TailSetting — the panel's tail-mode toggle state
#include "core/capture/tail_control.h" // TailSetting — the panel's tail-mode toggle state
namespace reasampler {
+3 -3
View File
@@ -1,4 +1,4 @@
#include "peaks.h"
#include "core/audio/peaks.h"
#include <algorithm>
#include <climits>
@@ -14,7 +14,7 @@
// extra frames) with no rounding drift and no dropped tail — the last bin's end is
// always exactly frameCount.
namespace reasampler {
namespace reasampler::audio {
Envelope computeEnvelope(const std::vector<AudioSample>& interleaved,
std::size_t channelCount,
@@ -123,4 +123,4 @@ std::size_t lastFrameAboveThreshold(const std::vector<AudioSample>& interleaved,
return kNoFrameAboveThreshold;
}
} // namespace reasampler
} // namespace reasampler::audio
+2 -2
View File
@@ -11,7 +11,7 @@
#include <cstddef>
#include <vector>
namespace reasampler {
namespace reasampler::audio {
// Canonical in-memory audio-sample type. `float` is REAPER's native audio buffer
// format (its render/PCM_source callbacks hand back interleaved 32-bit float), so
@@ -119,4 +119,4 @@ std::size_t lastFrameAboveThreshold(const std::vector<AudioSample>& interleaved,
std::size_t frameCount,
AudioSample linearThreshold);
} // namespace reasampler
} // namespace reasampler::audio
@@ -1,11 +1,11 @@
// batch_capture.cpp — pure logic for M11 batch capture. See header.
// NO REAPER types; unit-tested by tests/test_batch_capture.cpp.
#include "batch_capture.h"
#include "core/capture/batch_capture.h"
#include <algorithm>
namespace reasampler {
namespace reasampler::capture {
std::vector<CaptureUnit> planCaptureUnits(const std::vector<BatchRange>& ranges) {
std::vector<CaptureUnit> units;
@@ -73,4 +73,4 @@ std::string BatchOutcome::summaryLine(const std::string& noun) const {
return line;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -29,7 +29,7 @@
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::capture {
// One capture in a batch: an exact source range plus its 1-based ordinal within the
// KEPT set. The ordinal disambiguates per-unit file stems (the offline backend's
@@ -97,4 +97,4 @@ private:
std::vector<BatchUnitResult> results_;
};
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,4 +1,4 @@
#include "capture_paths.h"
#include "core/capture/capture_paths.h"
#include <cassert>
#include <cctype>
@@ -8,7 +8,7 @@
#include <filesystem>
#include <vector>
namespace reasampler {
namespace reasampler::capture {
std::string hashBytes(const std::uint8_t* data, std::size_t len) {
// FNV-1a 64-bit: deterministic, no dependencies, adequate for dedup identity.
@@ -284,4 +284,4 @@ ProjectTransition classifyProjectTransition(bool sameProjectObject,
return ProjectTransition::NoOp;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -17,7 +17,7 @@
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::capture {
// The project-relative bank subfolder. All captured wavs live here so the bank
// travels with the .rpp (CONTEXT.md §Settled decisions: per-project bank).
@@ -25,7 +25,7 @@ inline constexpr const char* kBankSubfolder = "reasampler_bank";
// A resolved pair of paths for one capture: where REAPER must be told to write
// (absolute, because RENDER_FILE wants a directory REAPER can create/open) and
// what we store in the BankIndex (project-relative, because the index is
// what we store in the BankModel (project-relative, because the index is
// relative-paths-only — CLAUDE.md precision invariant).
struct BankPaths {
std::string absoluteDir; // <projectDir>/reasampler_bank (forward slash)
@@ -87,7 +87,7 @@ std::string sanitizeStem(const std::string& baseName);
// timestamp or counter) so repeated captures do not collide.
// Also sanitized. May be empty.
// Produces "<stem>[_<tag>].wav". The relativePath is always project-relative and
// forward-slashed so it satisfies BankIndex::add's relative-only invariant.
// forward-slashed so it satisfies BankModel::add's relative-only invariant.
BankPaths deriveBankPaths(const std::string& projectDir,
const std::string& baseName,
const std::string& uniqueTag);
@@ -212,4 +212,4 @@ ProjectTransition classifyProjectTransition(bool sameProjectObject,
const std::string& currentGuid,
const std::string& currentPath);
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,8 +1,8 @@
// insert_plan.cpp — see insert_plan.h. Pure InsertMedia mode-bit arithmetic.
#include "insert_plan.h"
#include "core/capture/insert_plan.h"
namespace reasampler {
namespace reasampler::capture {
namespace {
@@ -46,4 +46,4 @@ int computeInsertMode(const InsertOptions& opts) {
return mode;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -22,7 +22,7 @@
#include <cstdint>
namespace reasampler {
namespace reasampler::capture {
// Where InsertMedia drops the item. Maps to the low bits of `mode` (mode&3).
// We expose only the two placement targets M6 needs; "add as takes" (3) is a
@@ -72,4 +72,4 @@ int computeInsertMode(const InsertOptions& opts);
// any computed mode (the "no silent time-stretch" invariant, made checkable).
inline constexpr int kStretchToTimeSelBit = 4;
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,9 +1,9 @@
// realtime_record.cpp — pure logic for the realtime-record backend (M8). See header.
// NO REAPER types; unit-tested by tests/test_realtime_record.cpp.
#include "realtime_record.h"
#include "core/capture/realtime_record.h"
namespace reasampler {
namespace reasampler::capture {
RecordModePlan recordModePlanFor(int channelCount, OutputTap tap) {
RecordModePlan p;
@@ -124,4 +124,4 @@ bool isTerminalPhase(RecordPhase phase) {
return phase == RecordPhase::Done || phase == RecordPhase::Failed;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -24,9 +24,13 @@
#include <string>
#include <vector>
#include "bank_model.h" // Sample, SourceMode (pure)
#include "core/model/bank_model.h" // Sample, SourceMode (pure)
namespace reasampler {
namespace reasampler::capture {
using model::Sample;
using model::Tier;
using model::SourceMode;
// --- I_RECMODE values (verbatim from SDK header ~2197) -----------------------
//
@@ -231,4 +235,4 @@ bool isStopRequested(RecordPhase phase);
// Only Done and Failed are terminal; Recording and Finalizing are live.
bool isTerminalPhase(RecordPhase phase);
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,13 +1,13 @@
// render_settings.cpp — pure logic for the three-scope capture action family. See header.
// NO REAPER types; unit-tested by tests/test_render_settings.cpp.
#include "render_settings.h"
#include "core/capture/render_settings.h"
#include <algorithm>
#include <cmath>
#include <sstream>
namespace reasampler {
namespace reasampler::capture {
double autoTrimEndRatio() {
// Amplitude ratio = 10^(dB/20). Derived from kAutoTrimThresholdDb so the dB is
@@ -221,4 +221,4 @@ const std::vector<CaptureActionDef>& captureActionTable() {
return table;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -25,9 +25,11 @@
#include <string>
#include <vector>
#include "bank_model.h" // SourceMode (pure enum)
#include "core/model/bank_model.h" // SourceMode (pure enum)
namespace reasampler {
namespace reasampler::capture {
using model::SourceMode;
// --- RENDER_SETTINGS source/processing bits (verbatim from SDK header ~3041) --
//
@@ -260,4 +262,4 @@ struct CaptureActionDef {
// capture applies is read from the docked-panel setting, not baked into the row.
const std::vector<CaptureActionDef>& captureActionTable();
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,13 +1,13 @@
// tail_control — pure implementation. See tail_control.h. NO REAPER / SWELL / vendor.
#include "tail_control.h"
#include "core/capture/tail_control.h"
#include <algorithm>
#include <cstdio>
#include "core/json/json.h"
namespace reasampler {
namespace reasampler::capture {
TailMode cycleTailMode(TailMode current) {
switch (current) {
@@ -128,4 +128,4 @@ std::optional<TailSetting> deserializeTailSetting(const std::string& blob) {
return out;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -12,9 +12,9 @@
#include <optional>
#include <string>
#include "render_settings.h" // TailMode (pure enum) — the three-state tail contract
#include "core/capture/render_settings.h" // TailMode (pure enum) — the three-state tail contract
namespace reasampler {
namespace reasampler::capture {
// The Manual-mode starting length. 2 s is a musically useful default tail (a bar of
// reverb throw at a moderate tempo) that is well under the 8 s cap. Also the value a
@@ -67,4 +67,4 @@ std::string tailToggleLabel(const TailSetting& setting);
std::string serializeTailSetting(const TailSetting& setting);
std::optional<TailSetting> deserializeTailSetting(const std::string& json);
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,10 +1,10 @@
// wav_trim — pure implementation. See wav_trim.h. NO REAPER / SWELL / vendor.
#include "wav_trim.h"
#include "core/capture/wav_trim.h"
#include <cstring> // std::memcpy, std::memcmp
namespace reasampler {
namespace reasampler::capture {
namespace {
@@ -157,4 +157,4 @@ WavTruncatePlan planWavTruncate(const WavLayout& layout, std::size_t keptFrames)
return plan;
}
} // namespace reasampler
} // namespace reasampler::capture
@@ -29,9 +29,11 @@
#include <cstdint>
#include <vector>
#include "peaks.h" // AudioSample (float)
#include "core/audio/peaks.h" // AudioSample (float)
namespace reasampler {
namespace reasampler::capture {
using audio::AudioSample;
// The parsed geometry of a canonical 32-bit-float WAV. `valid` is false when the
// bytes are not a WAV we can safely trim (see FORMAT ASSUMPTION); every other field
@@ -98,4 +100,4 @@ struct WavTruncatePlan {
// truncate the file to newFileByteLength.
WavTruncatePlan planWavTruncate(const WavLayout& layout, std::size_t keptFrames);
} // namespace reasampler
} // namespace reasampler::capture
@@ -1,17 +1,17 @@
// master_gain.cpp — see master_gain.h. Pure math; no LICE/VST3/REAPER includes.
#include "master_gain.h"
#include "core/instrument/engine/master_gain.h"
#include "core/util/clamp01.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
namespace reasampler::vst {
namespace reasampler::instrument::engine {
namespace {
double clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); }
} // namespace
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
double masterGainMaxLinear() { return std::pow(10.0, kMasterGainMaxDb / 20.0); }
@@ -48,4 +48,4 @@ void formatMasterGainLabel(double norm, char* buf, std::size_t len) {
std::snprintf(buf, len, "%+.1fdB", db);
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::engine
@@ -19,7 +19,7 @@
#include <cstddef>
namespace reasampler::vst {
namespace reasampler::instrument::engine {
// The dB taper endpoints. norm 0 is -inf (true zero); norm just above 0 starts at the
// finite floor kMasterGainMinDb and sweeps linearly in dB to kMasterGainMaxDb at norm 1.
@@ -55,4 +55,4 @@ double masterGainNormFromLinear(double linear);
// including the terminator. Pure.
void formatMasterGainLabel(double norm, char* buf, std::size_t len);
} // namespace reasampler::vst
} // namespace reasampler::instrument::engine
@@ -20,13 +20,13 @@
// ratio the delay is frozen mid-band and no splice ever fires: a primed shifter passes the
// stream through with ZERO added latency; a silence-warmed one is a clean window delay.
#include "pitch_shift.h"
#include "core/instrument/engine/pitch_shift.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace reasampler {
namespace reasampler::instrument::engine {
namespace {
@@ -431,4 +431,4 @@ AudioSample PitchShifter::processImpl(AudioSample in, const SpliceEvent* linked)
return static_cast<AudioSample>(out);
}
} // namespace reasampler
} // namespace reasampler::instrument::engine
@@ -66,9 +66,11 @@
#include <cstdint>
#include <vector>
#include "peaks.h" // AudioSample (float)
#include "core/audio/peaks.h" // AudioSample (float)
namespace reasampler {
namespace reasampler::instrument::engine {
using audio::AudioSample;
// The splice decision made by the most recent process()/processLinked() call — the LINKED-LAG
// stereo contract (Q-W0 T1-01). A stereo voice runs channel 0 as the MASTER (full correlation
@@ -225,4 +227,4 @@ private:
// live fade by that rate.
};
} // namespace reasampler
} // namespace reasampler::instrument::engine
@@ -2,7 +2,7 @@
// contract and the design rationale (keymap resolution, pitch ratio, ADSR shape,
// voice allocation + stealing policy). NO VST3 / REAPER / SWELL / vendor includes.
#include "sampler_core.h"
#include "core/instrument/engine/sampler_core.h"
#include <cmath>
@@ -270,7 +270,7 @@ bool Voice::sustainLoopUsable() const {
}
void Voice::start(int note, int velocity, const SampleData& sample, int rootNote,
double keyTrack, const vst::VelocityCurve& velocityCurve,
double keyTrack, const VelocityCurve& velocityCurve,
bool declickTakeover) {
// Takeover declick (Phase S GA fix, rev 2): BEFORE any state reset, record the PRE-CUT
// REFERENCE — the last rendered output — and mark the compensation PENDING iff this
@@ -22,12 +22,19 @@
#include <cstdint>
#include <vector>
#include "peaks.h" // AudioSample (float)
#include "pitch_shift.h" // PitchShifter (S16 Preserve engine DSP core)
#include "velocity_curve.h" // VelocityCurve (S-VIEW-9 velocity->amp transfer curve; eval at start)
#include "core/audio/peaks.h" // AudioSample (float)
#include "core/instrument/engine/pitch_shift.h" // PitchShifter (S16 Preserve engine DSP core)
#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (S-VIEW-9 velocity->amp transfer curve; eval at start)
namespace reasampler {
// Q-W1 interim: the engine deps live in their sub-namespace homes now; sampler_core
// re-namespaces in its own split wave (Q-W2v).
using audio::AudioSample;
using instrument::engine::PitchShifter;
using instrument::engine::VelocityCurve;
using instrument::engine::VelocityPoint;
// The instrument's per-instance output channel mode (S7, D-E). MONO keeps the pre-S7
// downmix path (one channel out); STEREO negotiates a 2-channel output bus and renders
// per-channel. A PERFORMANCE choice the instrument owns (component state), never written
@@ -232,7 +239,7 @@ struct KeyZone {
// of keyTrack), carried from PerformanceZone by resolvePerformance and eval'd ONCE in
// Voice::start (never per frame). DEFAULT flat y=1 (R10-F1 Option A) — every velocity plays at
// unity, a deliberate behavior change from the pre-r10 linear map.
vst::VelocityCurve velocityCurve = vst::VelocityCurve::flat();
VelocityCurve velocityCurve = VelocityCurve::flat();
std::size_t sampleIndex = 0; // index into Keymap::samples
};
@@ -439,7 +446,7 @@ public:
// kDeclickDecay). A fresh start never declicks.
void start(int note, int velocity, const SampleData& sample, int rootNote,
double keyTrack = 1.0,
const vst::VelocityCurve& velocityCurve = vst::VelocityCurve::flat(),
const VelocityCurve& velocityCurve = VelocityCurve::flat(),
bool declickTakeover = false);
// MONO LEGATO takeover (Phase S): re-pitch this ACTIVE voice to `note` without touching the
@@ -1,23 +1,18 @@
// velocity_curve.cpp — see velocity_curve.h. Pure eval + editing/clamp/inverse map; no host types.
#include "velocity_curve.h"
#include "core/instrument/engine/velocity_curve.h"
#include <algorithm> // std::max, std::min, std::abs, std::stable_sort
#include <cmath> // std::fabs
#include <utility> // std::move
namespace reasampler::vst {
namespace reasampler::instrument::engine {
namespace {
double clamp(double v, double lo, double hi) {
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
double clampVelocity(double v) { return clamp(v, kVelMin, kVelMax); }
double clampAmp(double a) { return clamp(a, kAmpMin, kAmpMax); }
double clampVelocity(double v) { return std::clamp(v, kVelMin, kVelMax); }
double clampAmp(double a) { return std::clamp(a, kAmpMin, kAmpMax); }
// Pixel<->box maps (mirror of envelope_edit's timeToX/levelToY). X spans the width for [0,127]; Y
// spans (height-1) rows for amp [0,1] with amp 1 at the TOP (y increases downward).
@@ -203,7 +198,7 @@ VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, doubl
// Interior point: clamp X strictly within its immediate neighbours so it can't cross them.
const double lo = points_[index - 1].velocity;
const double hi = points_[index + 1].velocity;
newVel = clamp(clampVelocity(velocity), lo, hi);
newVel = std::clamp(clampVelocity(velocity), lo, hi);
}
points_[index] = VelocityPoint{newVel, newAmp};
return points_[index];
@@ -274,4 +269,4 @@ bool VelocityCurve::equals(const VelocityCurve& other, double eps) const {
return true;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::engine
@@ -38,7 +38,7 @@
// Rect — the future editor shell (S-VIEW-10) passes its box coords directly. Mirror of envelope_edit's
// role, but one layer lower, so the coupling stays out of the engine core.
namespace reasampler::vst {
namespace reasampler::instrument::engine {
// The MIDI velocity domain [0,127] and the amp range [0,1] — the box every point clamps into.
inline constexpr double kVelMin = 0.0;
@@ -168,4 +168,4 @@ private:
std::vector<VelocityPoint> points_;
};
} // namespace reasampler::vst
} // namespace reasampler::instrument::engine
@@ -1,13 +1,13 @@
// bank_sync.cpp — see bank_sync.h. Pure; standard library only.
#include "bank_sync.h"
#include "core/instrument/map/bank_sync.h"
#include <cstdint>
#include <string>
#include "core/wire/wire.h"
namespace reasampler::vst {
namespace reasampler::instrument::map {
std::int64_t parseBankGeneration(const std::string& raw) {
// Whole-string, non-negative decimal parse WITHOUT exceptions or locale
@@ -60,4 +60,4 @@ AssignConsumeDecision consumeDecision(const std::optional<AssignmentRequest>& re
return d;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -21,9 +21,11 @@
#include <optional>
#include <string>
#include "assignment_request.h" // AssignmentRequest (the decoded request this consumes)
#include "core/wire/assignment_request.h" // AssignmentRequest (the decoded request this consumes)
namespace reasampler::vst {
namespace reasampler::instrument::map {
using wire::AssignmentRequest;
// The S9 bank-generation "generation 0 = never stamped" default. A project saved before
// S9 shipped carries no bank_generation key; the bridge read yields an absent/empty value
@@ -102,4 +104,4 @@ AssignConsumeDecision consumeDecision(const std::optional<AssignmentRequest>& re
std::int64_t lastConsumed, bool resolves,
bool isFocusedTarget);
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -1,8 +1,8 @@
// bridge_marshal.cpp — see bridge_marshal.h. Pure; no host types.
#include "bridge_marshal.h"
#include "core/instrument/map/bridge_marshal.h"
namespace reasampler::vst {
namespace reasampler::instrument::map {
std::optional<std::string> decodeGetProjExtState(int apiReturn,
const std::string& buffer) {
@@ -13,4 +13,4 @@ std::optional<std::string> decodeGetProjExtState(int apiReturn,
return buffer;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -22,7 +22,7 @@
#include <optional>
#include <string>
namespace reasampler::vst {
namespace reasampler::instrument::map {
// Interpret a GetProjExtState result: the int return value (bytes the API reports for
// the key) and the buffer it filled. Returns the value only when the API reported a
@@ -34,4 +34,4 @@ namespace reasampler::vst {
std::optional<std::string> decodeGetProjExtState(int apiReturn,
const std::string& buffer);
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -1,11 +1,11 @@
// note_entry.cpp — see note_entry.h. PURE text->MIDI-note parse for the S12 numeric entry.
#include "note_entry.h"
#include "core/instrument/map/note_entry.h"
#include <algorithm>
#include <cctype>
namespace reasampler::vst {
namespace reasampler::instrument::map {
namespace {
char asciiUpper(char c) {
@@ -110,4 +110,4 @@ std::optional<int> parseNoteEntry(const std::string& text) {
return parseNoteName(s);
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -22,7 +22,7 @@
#include <optional>
#include <string>
namespace reasampler::vst {
namespace reasampler::instrument::map {
// Parse a typed low/high/root field into a clamped MIDI note [0,127]. Accepts a decimal
// integer OR a note name (see the header notes). Leading/trailing ASCII whitespace is
@@ -30,4 +30,4 @@ namespace reasampler::vst {
// into [0,127]; empty or unparseable input returns nullopt (no change). Pure — no host types.
std::optional<int> parseNoteEntry(const std::string& text);
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -1,7 +1,7 @@
// sample_map — pure implementation. See sample_map.h. NO VST3 / REAPER / SWELL /
// vendor includes; standard library + the pure bank_book / wav_trim / sampler_core.
#include "sample_map.h"
#include "core/instrument/map/sample_map.h"
#include <algorithm> // std::min
#include <cassert> // assert
@@ -9,10 +9,12 @@
#include <cstring> // std::memcpy
#include <utility> // std::move
#include "master_gain.h" // masterGainMaxLinear — the v8 master-gain wire cap
#include "core/instrument/engine/master_gain.h" // masterGainMaxLinear — the v8 master-gain wire cap
namespace reasampler {
using instrument::engine::masterGainMaxLinear;
namespace {
// Translate a bank_model Sample's S2 intrinsics into the core's SampleLoop. The bank
@@ -544,9 +546,9 @@ void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map)
putU64le(out, doubleToBits(z.keyTrack));
// PAYLOAD v7 (S-VIEW-9): the per-zone velocity->amp transfer curve, appended last. 4-byte LE
// control-point count, then per point velocity + amp as IEEE-754 doubles (endpoints included).
const std::vector<reasampler::vst::VelocityPoint>& pts = z.velocityCurve.points();
const std::vector<VelocityPoint>& pts = z.velocityCurve.points();
putU32le(out, static_cast<std::uint32_t>(pts.size()));
for (const reasampler::vst::VelocityPoint& p : pts) {
for (const VelocityPoint& p : pts) {
putU64le(out, doubleToBits(p.velocity));
putU64le(out, doubleToBits(p.amp));
}
@@ -642,7 +644,7 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map, double projectRate) {
// false mid-curve) leaves the flat default and the mid-zone break below drops the rest.
if (curveTail) {
const std::uint32_t ptCount = r.u32();
std::vector<reasampler::vst::VelocityPoint> pts;
std::vector<VelocityPoint> pts;
// Bound the reserve to what the blob can actually hold (16 bytes/point) so a corrupt huge
// count can't trigger a giant allocation before the bounded reads fail — the loop still
// stops on r.ok, this only caps the speculative reserve.
@@ -651,9 +653,9 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map, double projectRate) {
for (std::uint32_t p = 0; p < ptCount && r.ok; ++p) {
const double vel = bitsToDouble(r.u64());
const double amp = bitsToDouble(r.u64());
pts.push_back(reasampler::vst::VelocityPoint{vel, amp});
pts.push_back(VelocityPoint{vel, amp});
}
if (r.ok) z.velocityCurve = reasampler::vst::VelocityCurve::fromPoints(std::move(pts));
if (r.ok) z.velocityCurve = reasampler::VelocityCurve::fromPoints(std::move(pts));
}
// Payload versions 4 (branch-only frames tail, never shipped) and any unknown pv leave the
// seconds product defaults on z.play — a v4 blob cannot exist outside this branch.
@@ -729,7 +731,7 @@ std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
// negative falls back to unity; above the +24 dB cap clamps to the cap.
{
double g = state.masterGainLinear;
const double maxLin = vst::masterGainMaxLinear();
const double maxLin = masterGainMaxLinear();
if (!std::isfinite(g) || g < 0.0) g = 1.0;
if (g > maxLin) g = maxLin;
putU64le(out, doubleToBits(g));
@@ -887,7 +889,7 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
if (!r.ok) return out; // truncated inside the gain double — out already carries
// mode/marker/velocity/voice fields from above; unity holds
out.masterGainLinear =
(std::isfinite(g) && g >= 0.0 && g <= vst::masterGainMaxLinear() * (1.0 + 1e-9))
(std::isfinite(g) && g >= 0.0 && g <= masterGainMaxLinear() * (1.0 + 1e-9))
? g
: 1.0;
}
@@ -23,12 +23,18 @@
#include <string>
#include <vector>
#include "bank_book.h" // BankBook::deserialize (shared bank JSON parse)
#include "sampler_core.h" // Keymap, SampleData, SampleLoop
#include "wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
#include "core/model/bank_book.h" // BankBook::deserialize (shared bank JSON parse)
#include "core/instrument/engine/sampler_core.h" // Keymap, SampleData, SampleLoop
#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
namespace reasampler {
// Q-W1 interim: clean deps live in their sub-namespace homes now; sample_map
// re-namespaces in its own split wave (Q-W2v).
using audio::AudioSample;
using instrument::engine::VelocityCurve;
using instrument::engine::VelocityPoint;
// The bank sample this instance is bound to, distilled from the live "banks" blob:
// the project-relative WAV path the file seam must resolve+decode, plus the S2 bank
// intrinsics the core repitches / loops by. A pure value — no host, no PCM yet.
@@ -287,7 +293,7 @@ struct PerformanceZone {
// already-saved zone's soft hits play LOUDER than under the old linear map. Intended; do NOT
// preserve the linear response. Carried to KeyZone by resolvePerformance, eval'd in Voice::start.
// Sequenced on the zones-payload axis AFTER keyTrack (payload v6 -> v7).
vst::VelocityCurve velocityCurve = vst::VelocityCurve::flat();
VelocityCurve velocityCurve = VelocityCurve::flat();
// S15/S16 per-zone play parameters (play mode + AHDSR + Trigger %-length/fades; pitch
// engine + AD pitch envelope). Instrument-owned (D-B), never a bank fact — mirror of the
@@ -341,7 +347,7 @@ struct ResolvedZone {
int highNote = 127;
int rootNote = 60; // effective: override, else bank intrinsic, else 60
double keyTrack = 1.0; // S-VIEW-6 key-tracking scalar, carried from PerformanceZone (1.0 = 100% ET)
vst::VelocityCurve velocityCurve = vst::VelocityCurve::flat(); // S-VIEW-9 velocity->amp curve, carried from PerformanceZone
VelocityCurve velocityCurve = VelocityCurve::flat(); // S-VIEW-9 velocity->amp curve, carried from PerformanceZone
SampleLoop loop; // effective: loopOverride, else bank S2 intrinsic (S11)
std::int64_t startFrame = 0; // effective initial read frame: startPoint, else 0 (S11)
ZonePlaySeconds play; // S15/S16 per-zone play params (SECONDS; resolved to frames at build)
@@ -1,10 +1,10 @@
// trigger_seam.cpp — PURE Trigger-mode frames↔fraction converter (see trigger_seam.h).
#include "trigger_seam.h"
#include "core/instrument/map/trigger_seam.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::map {
std::int64_t triggerPlayLength(double lengthFraction,
std::int64_t frameCount,
@@ -24,4 +24,4 @@ std::int64_t fadeFractionToFrames(double fadeFraction, std::int64_t playLength)
return static_cast<std::int64_t>(fadeFraction * static_cast<double>(playLength) + 0.5);
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -25,7 +25,7 @@
#include <cstdint>
namespace reasampler::vst {
namespace reasampler::instrument::map {
// The source-frame length of the Trigger played span:
// postStart = max(0, frameCount - startFrame)
@@ -48,4 +48,4 @@ double framesToFadeFraction(std::int64_t fadeFrames, std::int64_t playLength);
// Rounds to nearest integer frame. Returns 0 when playLength == 0.
std::int64_t fadeFractionToFrames(double fadeFraction, std::int64_t playLength);
} // namespace reasampler::vst
} // namespace reasampler::instrument::map
@@ -1,12 +1,12 @@
// browser_scroll.cpp — see browser_scroll.h. PURE scroll + search geometry over the S10
// capture_browser. No host types; only the shared Rect + BrowserLayout.
#include "browser_scroll.h"
#include "core/instrument/ui/browser_scroll.h"
#include <algorithm>
#include <cctype>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
// The minimum thumb height so a very long bank still yields a grabbable thumb.
@@ -26,7 +26,7 @@ int scrollContentHeight(const BrowserLayout& layout, int cardCount) {
int scrollMaxOffset(const BrowserLayout& layout, int cardCount) {
const int content = scrollContentHeight(layout, cardCount);
const int gridH = (std::max)(0, layout.grid.height());
const int gridH = (std::max)(0, layout.grid.height);
return (std::max)(0, content - gridH);
}
@@ -41,7 +41,7 @@ VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int of
VisibleRange vr;
if (cardCount <= 0) return vr;
const int columns = (std::max)(1, layout.columns);
const int gridH = (std::max)(0, layout.grid.height());
const int gridH = (std::max)(0, layout.grid.height);
if (gridH <= 0 || kBrowserCardHeight <= 0) {
vr.first = 0;
vr.last = 0;
@@ -66,21 +66,21 @@ VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int of
Rect scrolledCardCellRect(const BrowserLayout& layout, int index, int offset) {
Rect r = cardCellRect(layout, index);
if (r.right <= r.left && r.bottom <= r.top) return r; // empty (negative index) stays empty
return Rect{r.left, r.top - offset, r.right, r.bottom - offset};
if (r.right() <= r.x && r.bottom() <= r.y) return r; // empty (negative index) stays empty
return Rect::ltrb(r.x, r.y - offset, r.right(), r.bottom() - offset);
}
Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset) {
const int content = scrollContentHeight(layout, cardCount);
const int gridH = (std::max)(0, layout.grid.height());
const int gridH = (std::max)(0, layout.grid.height);
if (content <= gridH || gridH <= 0) return Rect{}; // fits -> no scrollbar
const int maxOff = content - gridH;
if (offset < 0) offset = 0;
if (offset > maxOff) offset = maxOff;
const int trackRight = layout.grid.right;
const int trackRight = layout.grid.right();
const int trackLeft = trackRight - kScrollbarWidth;
const int trackTop = layout.grid.top;
const int trackTop = layout.grid.y;
// Thumb height proportional to the visible fraction, floored at a grabbable minimum but
// never taller than the track.
@@ -95,13 +95,13 @@ Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset) {
thumbTop = trackTop + static_cast<int>(
static_cast<long long>(offset) * trackSpan / maxOff);
}
return Rect{trackLeft, thumbTop, trackRight, thumbTop + thumbH};
return Rect::ltrb(trackLeft, thumbTop, trackRight, thumbTop + thumbH);
}
int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffset,
int dyPixels) {
const int content = scrollContentHeight(layout, cardCount);
const int gridH = (std::max)(0, layout.grid.height());
const int gridH = (std::max)(0, layout.grid.height);
if (content <= gridH || gridH <= 0) return clampScrollOffset(layout, cardCount, startOffset);
// Thumb height (same formula as scrollThumbRect) -> movable track span in thumb pixels.
@@ -124,7 +124,7 @@ int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffse
Rect searchBoxRect(int w) {
if (w <= 0) return Rect{};
return Rect{0, 0, w, kSearchBoxHeight};
return Rect::ltrb(0, 0, w, kSearchBoxHeight);
}
bool nameMatchesQuery(const std::string& name, const std::string& query) {
@@ -155,4 +155,4 @@ std::vector<int> filterNameIndices(const std::vector<std::string>& names,
return out;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -24,9 +24,9 @@
#include <string>
#include <vector>
#include "capture_browser.h" // BrowserLayout, cardCellRect, kBrowserCardHeight, Rect
#include "core/instrument/ui/capture_browser.h" // BrowserLayout, cardCellRect, kBrowserCardHeight, Rect
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// The width (px) of the vertical scrollbar gutter at the right edge of the grid. The shell
// draws the track + thumb here and hit-tests thumb grabs against scrollThumbRect. Exposed so
@@ -104,4 +104,4 @@ bool nameMatchesQuery(const std::string& name, const std::string& query);
std::vector<int> filterNameIndices(const std::vector<std::string>& names,
const std::string& query);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,10 +1,10 @@
// capture_browser.cpp — see capture_browser.h. Pure math; no host types.
#include "capture_browser.h"
#include "core/instrument/ui/capture_browser.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
@@ -23,10 +23,10 @@ BrowserLayout layoutBrowser(int w, int h) {
BrowserLayout out;
const int tabH = std::min(kBrowserTabHeight, ch);
out.tabStrip = Rect{0, 0, cw, tabH};
out.grid = Rect{0, tabH, cw, ch};
out.tabStrip = Rect::ltrb(0, 0, cw, tabH);
out.grid = Rect::ltrb(0, tabH, cw, ch);
const int gridW = std::max(0, out.grid.width());
const int gridW = std::max(0, out.grid.width);
out.columns = std::max(1, gridW / kBrowserCardWidth);
return out;
}
@@ -36,38 +36,38 @@ Rect cardCellRect(const BrowserLayout& layout, int index) {
const int cols = std::max(1, layout.columns);
const int col = index % cols;
const int row = index / cols;
const int left = layout.grid.left + col * kBrowserCardWidth;
const int top = layout.grid.top + row * kBrowserCardHeight;
return Rect{left, top, left + kBrowserCardWidth, top + kBrowserCardHeight};
const int left = layout.grid.x + col * kBrowserCardWidth;
const int top = layout.grid.y + row * kBrowserCardHeight;
return Rect::ltrb(left, top, left + kBrowserCardWidth, top + kBrowserCardHeight);
}
Rect cardContentRect(const BrowserLayout& layout, int index) {
if (index < 0) return Rect{};
const Rect cell = cardCellRect(layout, index);
return Rect{cell.left + kBrowserCardGutter, cell.top + kBrowserCardGutter,
cell.right - kBrowserCardGutter, cell.bottom - kBrowserCardGutter};
return Rect::ltrb(cell.x + kBrowserCardGutter, cell.y + kBrowserCardGutter,
cell.right() - kBrowserCardGutter, cell.bottom() - kBrowserCardGutter);
}
Rect cardThumbnailRect(const BrowserLayout& layout, int index) {
if (index < 0) return Rect{};
const Rect content = cardContentRect(layout, index);
const int thumbH = std::min(kBrowserThumbHeight, std::max(0, content.height()));
return Rect{content.left, content.top, content.right, content.top + thumbH};
const int thumbH = std::min(kBrowserThumbHeight, std::max(0, content.height));
return Rect::ltrb(content.x, content.y, content.right(), content.y + thumbH);
}
Rect cardLabelRect(const BrowserLayout& layout, int index) {
if (index < 0) return Rect{};
const Rect content = cardContentRect(layout, index);
const Rect thumb = cardThumbnailRect(layout, index);
return Rect{content.left, thumb.bottom, content.right, content.bottom};
return Rect::ltrb(content.x, thumb.bottom(), content.right(), content.bottom());
}
int cardHitTest(const BrowserLayout& layout, int cardCount, int x, int y) {
if (cardCount <= 0) return -1;
if (!contains(layout.grid, x, y)) return -1;
const int cols = std::max(1, layout.columns);
const int col = (x - layout.grid.left) / kBrowserCardWidth;
const int row = (y - layout.grid.top) / kBrowserCardHeight;
const int col = (x - layout.grid.x) / kBrowserCardWidth;
const int row = (y - layout.grid.y) / kBrowserCardHeight;
if (col < 0 || col >= cols) return -1; // past the last column (right dead-zone)
const int index = row * cols + col;
if (index < 0 || index >= cardCount) return -1;
@@ -79,9 +79,9 @@ int cardHitTest(const BrowserLayout& layout, int cardCount, int x, int y) {
Rect filterTabRect(const BrowserLayout& layout, int tabCount, int index) {
if (tabCount <= 0 || index < 0 || index >= tabCount) return Rect{};
const Rect& strip = layout.tabStrip;
const int left = tabEdge(strip.left, std::max(0, strip.width()), index, tabCount);
const int right = tabEdge(strip.left, std::max(0, strip.width()), index + 1, tabCount);
return Rect{left, strip.top, right, strip.bottom};
const int left = tabEdge(strip.x, std::max(0, strip.width), index, tabCount);
const int right = tabEdge(strip.x, std::max(0, strip.width), index + 1, tabCount);
return Rect::ltrb(left, strip.y, right, strip.bottom());
}
int filterTabHitTest(const BrowserLayout& layout, int tabCount, int x, int y) {
@@ -93,4 +93,4 @@ int filterTabHitTest(const BrowserLayout& layout, int tabCount, int x, int y) {
return -1;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -20,9 +20,9 @@
#pragma once
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// Fixed browser metrics, exposed so the shell and tests agree. The card is sized to show a
// peak thumbnail with a name + badge line under it — scannable by eye, not a dense list.
@@ -37,12 +37,12 @@ inline constexpr int kBrowserThumbHeight = 44; // the peak-thumbnail band inside
struct BrowserLayout {
Rect tabStrip; // top: the bank-filter tabs
Rect grid; // below the tabs: where the capture cards tile
int columns = 1; // cards per row in `grid` (>= 1); derived from grid.width()
int columns = 1; // cards per row in `grid` (>= 1); derived from grid.width
};
// Divide a (w x h) browser area into its regions and compute the column count. Pure: same
// inputs -> same layout. The tab strip takes a fixed height at the top (clamped so it never
// exceeds the area); the grid takes the rest. columns = max(1, grid.width()/cardWidth) so a
// exceeds the area); the grid takes the rest. columns = max(1, grid.width/cardWidth) so a
// browser narrower than one card still lays out a single column. A zero/negative size
// yields empty rects + columns==1.
BrowserLayout layoutBrowser(int w, int h);
@@ -89,4 +89,4 @@ Rect filterTabRect(const BrowserLayout& layout, int tabCount, int index);
// tab strip. Pure.
int filterTabHitTest(const BrowserLayout& layout, int tabCount, int x, int y);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
+41
View File
@@ -0,0 +1,41 @@
// curve_popup.cpp — see curve_popup.h. Pure arithmetic; no LICE/VST3/REAPER includes.
#include "core/instrument/ui/curve_popup.h"
#include <algorithm>
namespace reasampler::instrument::ui {
namespace {
int clampDim(int want, int lo, int hi, int windowDim) {
const int clamped = (std::max)(lo, (std::min)(hi, want));
return (std::min)(clamped, (std::max)(0, windowDim));
}
} // namespace
CurvePopupLayout computeCurvePopup(int w, int h) {
CurvePopupLayout out;
const int sheetW = clampDim((w * 60) / 100, kCurvePopupMinW, kCurvePopupMaxW, w);
const int sheetH = clampDim((h * 55) / 100, kCurvePopupMinH, kCurvePopupMaxH, h);
const int left = (w - sheetW) / 2;
const int top = (h - sheetH) / 2;
out.sheet = Rect::ltrb(left, top, left + sheetW, top + sheetH);
const int titleBottom = out.sheet.y + kCurvePopupTitleH;
const int closeTop = out.sheet.y + (kCurvePopupTitleH - kCurvePopupCloseSize) / 2;
out.close = Rect::ltrb(out.sheet.right() - kCurvePopupPad - kCurvePopupCloseSize, closeTop,
out.sheet.right() - kCurvePopupPad, closeTop + kCurvePopupCloseSize);
out.title = Rect::ltrb(out.sheet.x + kCurvePopupPad, out.sheet.y,
out.close.x - kCurvePopupPad, titleBottom);
out.curveBox = Rect::ltrb(out.sheet.x + kCurvePopupPad, titleBottom + 2,
out.sheet.right() - kCurvePopupPad,
out.sheet.bottom() - kCurvePopupPad);
return out;
}
bool popupOutsideSheet(const CurvePopupLayout& layout, int x, int y) {
return !contains(layout.sheet, x, y);
}
} // namespace reasampler::instrument::ui
@@ -15,9 +15,9 @@
#pragma once
#include "editor_geometry.h" // Rect, contains
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// Fixed popup metrics (spec r11), exposed so the shell and tests agree.
inline constexpr int kCurvePopupMinW = 360;
@@ -45,4 +45,4 @@ CurvePopupLayout computeCurvePopup(int w, int h);
// The shell additionally gates on "no drag in flight" (spec). Pure.
bool popupOutsideSheet(const CurvePopupLayout& layout, int x, int y);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,10 +1,10 @@
// editor_geometry.cpp — see editor_geometry.h. Pure math; no host types.
#include "editor_geometry.h"
#include "core/instrument/ui/editor_geometry.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
@@ -17,10 +17,8 @@ constexpr int kButtonHeight = 24;
} // namespace
bool contains(const Rect& r, int x, int y) {
if (r.width() <= 0 || r.height() <= 0) return false;
return x >= r.left && x < r.right && y >= r.top && y < r.bottom;
}
// contains() now lives with the shared ui::Rect (core/ui/rect.h) — same half-open
// semantics, re-exported through the header's using-declaration.
EditorLayout layoutEditor(int w, int h) {
// Clamp the surface to non-negative extents so a degenerate view can't produce
@@ -32,18 +30,18 @@ EditorLayout layoutEditor(int w, int h) {
// Title bar spans the top, clamped so it never exceeds the client height.
const int titleH = std::min(kTitleBarHeight, ch);
out.titleBar = Rect{0, 0, cw, titleH};
out.titleBar = Rect::ltrb(0, 0, cw, titleH);
// Canvas is everything below the title bar.
out.canvas = Rect{0, titleH, cw, ch};
out.canvas = Rect::ltrb(0, titleH, cw, ch);
// Button sits at the top-left of the canvas, inset by a margin, and is clamped to
// fit inside the canvas so it never overhangs on a small view.
const int bx = out.canvas.left + kButtonMargin;
const int by = out.canvas.top + kButtonMargin;
const int bRight = std::min(bx + kButtonWidth, out.canvas.right);
const int bBottom = std::min(by + kButtonHeight, out.canvas.bottom);
out.button = Rect{bx, by, std::max(bx, bRight), std::max(by, bBottom)};
const int bx = out.canvas.x + kButtonMargin;
const int by = out.canvas.y + kButtonMargin;
const int bRight = std::min(bx + kButtonWidth, out.canvas.right());
const int bBottom = std::min(by + kButtonHeight, out.canvas.bottom());
out.button = Rect::ltrb(bx, by, std::max(bx, bRight), std::max(by, bBottom));
return out;
}
@@ -55,23 +53,23 @@ HitTarget hitTest(const EditorLayout& layout, int x, int y) {
Rect sampleRowRect(const EditorLayout& layout, int index) {
if (index < 0) return Rect{};
const int top = layout.canvas.top + index * kSampleRowHeight;
return Rect{layout.canvas.left, top, layout.canvas.right, top + kSampleRowHeight};
const int top = layout.canvas.y + index * kSampleRowHeight;
return Rect::ltrb(layout.canvas.x, top, layout.canvas.right(), top + kSampleRowHeight);
}
int sampleRowHitTest(const EditorLayout& layout, int rowCount, int x, int y) {
if (rowCount <= 0) return -1;
// Must be within the canvas horizontally and at/below its top.
if (x < layout.canvas.left || x >= layout.canvas.right) return -1;
if (y < layout.canvas.top) return -1;
if (x < layout.canvas.x || x >= layout.canvas.right()) return -1;
if (y < layout.canvas.y) return -1;
// Clip at the canvas bottom: clicks in the canvas's dead-zone below the last
// visible row agree with sampleRowRect, which does not clamp rows to canvas.bottom.
if (y >= layout.canvas.bottom) return -1;
const int index = (y - layout.canvas.top) / kSampleRowHeight;
// visible row agree with sampleRowRect, which does not clamp rows to canvas.bottom().
if (y >= layout.canvas.bottom()) return -1;
const int index = (y - layout.canvas.y) / kSampleRowHeight;
if (index < 0 || index >= rowCount) return -1;
// Guard the bottom edge: a click below the last row's bottom is outside.
const Rect r = sampleRowRect(layout, index);
if (y >= r.bottom) return -1;
if (y >= r.bottom()) return -1;
return index;
}
@@ -85,60 +83,60 @@ KeymapEditorLayout layoutKeymapEditor(int w, int h) {
// Split the canvas vertically: the left column is the bank-sample list, the right
// column (1/kZonePanelFraction of the width) is the zone panel. Guard tiny widths so
// the split point never crosses the canvas edges.
const int canvasW = std::max(0, canvas.width());
const int canvasW = std::max(0, canvas.width);
const int splitW = canvasW / kZonePanelFraction; // width of the zone panel
const int splitX = std::max(canvas.left, canvas.right - splitW);
const int splitX = std::max(canvas.x, canvas.right() - splitW);
out.sampleList = Rect{canvas.left, canvas.top, splitX, canvas.bottom};
out.zonePanel = Rect{splitX, canvas.top, canvas.right, canvas.bottom};
out.sampleList = Rect::ltrb(canvas.x, canvas.y, splitX, canvas.bottom());
out.zonePanel = Rect::ltrb(splitX, canvas.y, canvas.right(), canvas.bottom());
// "Add Zone" button spans the top of the zone panel, clamped to its height.
const int addH = std::min(kAddZoneHeight, std::max(0, out.zonePanel.height()));
const int addH = std::min(kAddZoneHeight, std::max(0, out.zonePanel.height));
out.addZoneButton =
Rect{out.zonePanel.left, out.zonePanel.top, out.zonePanel.right,
out.zonePanel.top + addH};
Rect::ltrb(out.zonePanel.x, out.zonePanel.y, out.zonePanel.right(),
out.zonePanel.y + addH);
// Zone rows stack below the button.
out.zoneRowArea = Rect{out.zonePanel.left, out.addZoneButton.bottom,
out.zonePanel.right, out.zonePanel.bottom};
out.zoneRowArea = Rect::ltrb(out.zonePanel.x, out.addZoneButton.bottom(),
out.zonePanel.right(), out.zonePanel.bottom());
return out;
}
Rect keymapSampleRowRect(const KeymapEditorLayout& layout, int index) {
if (index < 0) return Rect{};
const int top = layout.sampleList.top + index * kSampleRowHeight;
return Rect{layout.sampleList.left, top, layout.sampleList.right,
top + kSampleRowHeight};
const int top = layout.sampleList.y + index * kSampleRowHeight;
return Rect::ltrb(layout.sampleList.x, top, layout.sampleList.right(),
top + kSampleRowHeight);
}
int keymapSampleRowHitTest(const KeymapEditorLayout& layout, int rowCount, int x, int y) {
if (rowCount <= 0) return -1;
const Rect& list = layout.sampleList;
if (x < list.left || x >= list.right) return -1;
if (y < list.top || y >= list.bottom) return -1;
const int index = (y - list.top) / kSampleRowHeight;
if (x < list.x || x >= list.right()) return -1;
if (y < list.y || y >= list.bottom()) return -1;
const int index = (y - list.y) / kSampleRowHeight;
if (index < 0 || index >= rowCount) return -1;
const Rect r = keymapSampleRowRect(layout, index);
if (y >= r.bottom) return -1;
if (y >= r.bottom()) return -1;
return index;
}
Rect zoneRowRect(const KeymapEditorLayout& layout, int index) {
if (index < 0) return Rect{};
const int top = layout.zoneRowArea.top + index * kZoneRowHeight;
return Rect{layout.zoneRowArea.left, top, layout.zoneRowArea.right,
top + kZoneRowHeight};
const int top = layout.zoneRowArea.y + index * kZoneRowHeight;
return Rect::ltrb(layout.zoneRowArea.x, top, layout.zoneRowArea.right(),
top + kZoneRowHeight);
}
ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int y) {
if (zoneCount <= 0) return ZoneHit{};
const Rect& area = layout.zoneRowArea;
if (x < area.left || x >= area.right) return ZoneHit{};
if (y < area.top || y >= area.bottom) return ZoneHit{};
const int index = (y - area.top) / kZoneRowHeight;
if (x < area.x || x >= area.right()) return ZoneHit{};
if (y < area.y || y >= area.bottom()) return ZoneHit{};
const int index = (y - area.y) / kZoneRowHeight;
if (index < 0 || index >= zoneCount) return ZoneHit{};
const Rect row = zoneRowRect(layout, index);
if (y >= row.bottom) return ZoneHit{};
if (y >= row.bottom()) return ZoneHit{};
// Seven mini-buttons pinned to the right edge, right-to-left:
// delete, root+, root-, high+, high-, low+, low-
@@ -150,7 +148,7 @@ ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int
ZoneField::kDelete,
};
const int slots = 7;
const int ctrlBlockLeft = row.right - slots * kZoneCtrlWidth;
const int ctrlBlockLeft = row.right() - slots * kZoneCtrlWidth;
if (x < ctrlBlockLeft) return ZoneHit{index, ZoneField::kZoneNone}; // label -> select
const int slot = (x - ctrlBlockLeft) / kZoneCtrlWidth;
if (slot < 0 || slot >= slots) return ZoneHit{index, ZoneField::kZoneNone};
@@ -161,4 +159,4 @@ bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y) {
return contains(layout.addZoneButton, x, y);
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -12,24 +12,17 @@
#pragma once
namespace reasampler::vst {
#include "core/ui/rect.h"
// A plain integer rectangle. left/top inclusive, right/bottom exclusive — the same
// half-open convention LICE/SWELL RECTs use, kept REAPER-free here.
struct Rect {
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
namespace reasampler::instrument::ui {
int width() const { return right - left; }
int height() const { return bottom - top; }
};
// Returns true if (x, y) falls inside r under the half-open convention
// (left <= x < right, top <= y < bottom). A zero-or-negative-area rect contains
// nothing.
bool contains(const Rect& r, int x, int y);
// The shared pixel rectangle + containment test (Q-W1, T2-05 ≡ T4-21): the former
// LTRB Rect defined here is folded into the ONE concrete ui::Rect (XYWH storage,
// right()/bottom() accessors, Rect::ltrb() for edge-wise construction, same
// half-open convention). Aliased here so every instrument-ui call site keeps its
// established `Rect` / `contains` spelling.
using Rect = ::reasampler::ui::Rect;
using ::reasampler::ui::contains;
// The regions the spike editor draws, derived from the current view size. All are
// clamped to the client area so a degenerate (too-small) view never yields a region
@@ -93,7 +86,7 @@ inline constexpr int kAddZoneHeight = 22; // the "Add Zone" button band heig
// The keymap editor's regions, derived from the (w x h) client area. All clamp to the
// canvas so a degenerate view yields in-bounds rects.
struct KeymapEditorLayout {
EditorLayout base; // title bar + canvas (the sample list uses base.canvas.left half)
EditorLayout base; // title bar + canvas (the sample list uses base.canvas.x half)
Rect sampleList; // LEFT column: the bank-sample rows (sampleRowRect is relative here)
Rect zonePanel; // RIGHT column: the "Add Zone" button + the zone rows
Rect addZoneButton; // top of the zone panel
@@ -146,4 +139,4 @@ ZoneHit zoneHitTest(const KeymapEditorLayout& layout, int zoneCount, int x, int
// True if (x, y) lands on the "Add Zone" button. Pure.
bool addZoneHitTest(const KeymapEditorLayout& layout, int x, int y);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,10 +1,10 @@
// embed_strip.cpp — see embed_strip.h. Pure math; no host types.
#include "embed_strip.h"
#include "core/instrument/ui/embed_strip.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
@@ -42,22 +42,22 @@ EmbedLayout layoutEmbed(int w, int h) {
}
const int keymapBottom = ch - bandH;
out.keymap = Rect{0, 0, cw, keymapBottom};
out.levelBand = Rect{0, keymapBottom, cw, ch};
out.keymap = Rect::ltrb(0, 0, cw, keymapBottom);
out.levelBand = Rect::ltrb(0, keymapBottom, cw, ch);
return out;
}
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote) {
const Rect& band = layout.keymap;
const int bandWidth = std::max(0, band.width());
const int bandWidth = std::max(0, band.width);
int lo = clampNote(lowNote);
int hi = clampNote(highNote);
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
const int leftX = keyEdgeToX(band.left, bandWidth, lo);
const int rightX = keyEdgeToX(band.left, bandWidth, hi + 1);
return Rect{leftX, band.top, std::max(leftX, rightX), band.bottom};
const int leftX = keyEdgeToX(band.x, bandWidth, lo);
const int rightX = keyEdgeToX(band.x, bandWidth, hi + 1);
return Rect::ltrb(leftX, band.y, std::max(leftX, rightX), band.bottom());
}
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
@@ -74,13 +74,13 @@ int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount
Rect levelFillRect(const EmbedLayout& layout, double level) {
const Rect& band = layout.levelBand;
if (band.width() <= 0 || band.height() <= 0) return Rect{};
if (band.width <= 0 || band.height <= 0) return Rect{};
double l = level;
if (l < 0.0) l = 0.0;
if (l > 1.0) l = 1.0;
const int fillW = static_cast<int>(l * band.width());
const int fillW = static_cast<int>(l * band.width);
if (fillW <= 0) return Rect{};
return Rect{band.left, band.top, band.left + fillW, band.bottom};
return Rect::ltrb(band.x, band.y, band.x + fillW, band.bottom());
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -18,9 +18,9 @@
#pragma once
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// The full MIDI key span the strip maps across its width. 128 keys (0..127); the strip's
// horizontal axis is this range, so a zone [lowNote, highNote] becomes a sub-rectangle.
@@ -54,7 +54,7 @@ struct EmbedLayout {
EmbedLayout layoutEmbed(int w, int h);
// The horizontal sub-rectangle of the keymap band for a zone spanning [lowNote, highNote]
// (inclusive). The 128-key span maps linearly across keymap.width(); the returned rect
// (inclusive). The 128-key span maps linearly across keymap.width; the returned rect
// spans the half-open pixel range [x(lowNote), x(highNote+1)) so adjacent zones (e.g.
// 0..59 and 60..127) tile without a gap or overlap. Notes are clamped to [0,127] and low
// is clamped to <= high, so a malformed zone yields an in-band (possibly zero-width) rect,
@@ -73,4 +73,4 @@ int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount
// (rounded down). level <= 0 -> empty rect; level >= 1 -> the whole band. Pure.
Rect levelFillRect(const EmbedLayout& layout, double level);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,24 +1,19 @@
// envelope_edit.cpp — see envelope_edit.h. Pure inverse map + hit-test; no host types.
#include "envelope_edit.h"
#include "core/instrument/ui/envelope_edit.h"
#include <algorithm>
#include <cstdlib> // std::abs
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
double clamp(double v, double lo, double hi) {
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
// Seconds represented by one horizontal pixel under the overlay's linear time base. Zero when the
// area is degenerate (the caller then produces no motion). Matches envelope_overlay::timeToX.
double secondsPerPixel(const Rect& area, double totalSeconds) {
const int w = std::max(0, area.width());
const int w = std::max(0, area.width);
if (w <= 0 || totalSeconds <= 0.0) return 0.0;
return totalSeconds / static_cast<double>(w);
}
@@ -36,7 +31,7 @@ double gateSecondsPerPixel(const Rect& area) {
// Level (0..1) represented by one vertical pixel. levelToY spans (height-1) rows for [0,1], so one
// pixel is 1/(height-1). Zero when degenerate. Matches envelope_overlay::levelToY.
double levelPerPixel(const Rect& area) {
const int h = std::max(0, area.height());
const int h = std::max(0, area.height);
if (h <= 1) return 0.0;
return 1.0 / static_cast<double>(h - 1);
}
@@ -118,23 +113,23 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
// because every segment stays >= 0), so the [0, max] clamp is the whole constraint.
case EnvNode::AttackEnd:
out.attackSeconds =
clamp(grabEnv.attackSeconds + gateDSec, 0.0, bounds.maxAttackSeconds);
std::clamp(grabEnv.attackSeconds + gateDSec, 0.0, bounds.maxAttackSeconds);
break;
case EnvNode::HoldEnd:
out.holdSeconds = clamp(grabEnv.holdSeconds + gateDSec, 0.0, bounds.maxHoldSeconds);
out.holdSeconds = std::clamp(grabEnv.holdSeconds + gateDSec, 0.0, bounds.maxHoldSeconds);
break;
case EnvNode::DecayEnd: {
// Sustain node: X sets decay time, Y sets sustain level (drag DOWN = higher y = lower
// level, so subtract the level delta).
out.decaySeconds = clamp(grabEnv.decaySeconds + gateDSec, 0.0, bounds.maxDecaySeconds);
out.decaySeconds = std::clamp(grabEnv.decaySeconds + gateDSec, 0.0, bounds.maxDecaySeconds);
const double lvlPerPx = levelPerPixel(area);
const double dLevel = -static_cast<double>(dyPixels) * lvlPerPx;
out.sustainLevel = clamp(grabEnv.sustainLevel + dLevel, 0.0, 1.0);
out.sustainLevel = std::clamp(grabEnv.sustainLevel + dLevel, 0.0, 1.0);
break;
}
case EnvNode::ReleaseEnd:
out.releaseSeconds =
clamp(grabEnv.releaseSeconds + gateDSec, 0.0, bounds.maxReleaseSeconds);
std::clamp(grabEnv.releaseSeconds + gateDSec, 0.0, bounds.maxReleaseSeconds);
break;
// --- Trigger: fades + length are FRACTIONS. X pixels convert to a fraction of the PLAYED
@@ -154,7 +149,7 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
const double dFrac = playSeconds > 0.0 ? dSec / playSeconds : 0.0;
const double hi = std::min(bounds.maxFadeInFraction,
1.0 - std::max(0.0, grabEnv.fadeOutFraction));
out.fadeInFraction = clamp(grabEnv.fadeInFraction + dFrac, 0.0, std::max(0.0, hi));
out.fadeInFraction = std::clamp(grabEnv.fadeInFraction + dFrac, 0.0, std::max(0.0, hi));
break;
}
case EnvNode::FadeOutStart: {
@@ -165,13 +160,13 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
const double dFrac = playSeconds > 0.0 ? -dSec / playSeconds : 0.0;
const double hi = std::min(bounds.maxFadeOutFraction,
1.0 - std::max(0.0, grabEnv.fadeInFraction));
out.fadeOutFraction = clamp(grabEnv.fadeOutFraction + dFrac, 0.0, std::max(0.0, hi));
out.fadeOutFraction = std::clamp(grabEnv.fadeOutFraction + dFrac, 0.0, std::max(0.0, hi));
break;
}
case EnvNode::LengthEnd: {
// LengthEnd sits at lengthFraction of the WHOLE sample; X maps to a fraction of it.
const double dFrac = totalSeconds > 0.0 ? dSec / totalSeconds : 0.0;
out.lengthFraction = clamp(grabEnv.lengthFraction + dFrac, 0.0, bounds.maxLengthFraction);
out.lengthFraction = std::clamp(grabEnv.lengthFraction + dFrac, 0.0, bounds.maxLengthFraction);
break;
}
@@ -182,4 +177,4 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
return out;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -42,10 +42,10 @@
#include <cstdint>
#include <vector>
#include "editor_geometry.h" // Rect
#include "envelope_overlay.h" // EnvNode, EnvMode, AmpEnvelope, EnvVertex, timeToX/levelToY
#include "core/instrument/ui/editor_geometry.h" // Rect
#include "core/instrument/ui/envelope_overlay.h" // EnvNode, EnvMode, AmpEnvelope, EnvVertex, timeToX/levelToY
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// The pick radius (px) around a node's drawn point: a grab within this many pixels (in BOTH x and
// y) of a node handle grabs it. Mirrors waveform_view's kMarkerGrabWidth — wide enough to grab a
@@ -105,4 +105,4 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
double totalSeconds, const EnvClampBounds& bounds,
int dxPixels, int dyPixels);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,25 +1,29 @@
// envelope_overlay.cpp — see envelope_overlay.h. Pure geometry; no host types.
#include "envelope_overlay.h"
#include "core/instrument/ui/envelope_overlay.h"
#include "core/util/clamp01.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
int timeToX(const Rect& area, double totalSeconds, double t) {
const int w = std::max(0, area.width());
if (w <= 0 || totalSeconds <= 0.0) return area.left;
const int w = std::max(0, area.width);
if (w <= 0 || totalSeconds <= 0.0) return area.x;
if (t < 0.0) t = 0.0;
// Linear map, clamped on BOTH sides (FA2 bounds invariant): t past totalSeconds pins to the
// last in-bounds column area.right-1. Clamp in DOUBLE space BEFORE the integer cast — a huge
// last in-bounds column area.right()-1. Clamp in DOUBLE space BEFORE the integer cast — a huge
// t would overflow a 32-bit long (Windows) and wrap to the WRONG edge — then round.
double px = (t / totalSeconds) * static_cast<double>(w);
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
return area.left + static_cast<int>(px + 0.5);
return area.x + static_cast<int>(px + 0.5);
}
int gateTimedWidth(const Rect& area) {
const int w = std::max(0, area.width());
const int w = std::max(0, area.width);
if (w <= 0) return 0;
const int sustainPx =
static_cast<int>(kGateSustainDisplayFraction * static_cast<double>(w) + 0.5);
@@ -38,24 +42,19 @@ double gatePxPerSecond(const Rect& area) {
}
int levelToY(const Rect& area, double level) {
const int h = std::max(0, area.height());
if (h <= 0) return area.top;
const int h = std::max(0, area.height);
if (h <= 0) return area.y;
if (level < 0.0) level = 0.0;
if (level > 1.0) level = 1.0;
// Level 1 -> top row, level 0 -> bottom row (bottom-1 under the half-open convention). The
// range spans (h-1) pixels so both endpoints land ON a drawable row.
const int span = h - 1;
const long dy = static_cast<long>((1.0 - level) * static_cast<double>(span) + 0.5);
return area.top + static_cast<int>(dy);
return area.y + static_cast<int>(dy);
}
namespace {
double clamp01(double v) {
if (v < 0.0) return 0.0;
if (v > 1.0) return 1.0;
return v;
}
EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, double level) {
EnvVertex v;
@@ -71,12 +70,12 @@ EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, dou
// DOUBLE space to the last in-bounds column BEFORE the integer cast (FA2 bounds invariant; a
// huge px would overflow a 32-bit long on Windows and wrap to the WRONG edge).
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
const int w = std::max(1, area.width());
const int w = std::max(1, area.width);
if (px < 0.0) px = 0.0;
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
EnvVertex v;
v.node = node;
v.x = area.left + static_cast<int>(px + 0.5);
v.x = area.x + static_cast<int>(px + 0.5);
v.y = levelToY(area, level);
v.level = level;
return v;
@@ -95,7 +94,7 @@ std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area) {
// gets a kGateNodeSepPx base so consecutive nodes never coincide (every node individually
// grabbable at any params, incl. the tier-0 zero-hold/zero-decay defaults). The sustain
// plateau is the fixed reserve between DecayEnd and ReleaseStart.
const int W = std::max(1, area.width());
const int W = std::max(1, area.width);
const double sustainPx = static_cast<double>(W - gateTimedWidth(area));
const double sep = static_cast<double>(kGateNodeSepPx);
const double pps = gatePxPerSecond(area);
@@ -162,7 +161,7 @@ std::vector<EnvVertex> triggerPolyline(const AmpEnvelope& env, const Rect& area,
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
double totalSeconds) {
if (area.width() <= 0 || area.height() <= 0 || totalSeconds <= 0.0) {
if (area.width <= 0 || area.height <= 0 || totalSeconds <= 0.0) {
// Degenerate surface: a two-point flat baseline at level 0 so the shell always has a line.
return {vtx(EnvNode::Origin, area, 1.0, 0.0, 0.0),
vtx(EnvNode::ReleaseEnd, area, 1.0, 1.0, 0.0)};
@@ -173,4 +172,4 @@ std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect&
: triggerPolyline(env, area, totalSeconds);
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -25,7 +25,7 @@
// the vertical axis is LEVEL (0 at rect bottom, 1 at rect top).
//
// BOUNDS INVARIANT (FA2). EVERY vertex of EVERY polyline is clamped inside the canvas:
// x in [area.left, area.right-1], y in [area.top, area.bottom-1] (half-open rect convention).
// x in [area.x, area.right()-1], y in [area.y, area.bottom()-1] (half-open rect convention).
// No node and no drawn segment ever exceeds the canvas — paint-time clipping of handles is no
// longer needed (and never fires) in the shell.
//
@@ -33,8 +33,8 @@
// * The EnvNode enum is UNCHANGED (same node set, same draggable set — Origin + ReleaseStart
// remain the only non-draggable anchors).
// * ALL vertices are now in-bounds (see above). The shell's previous "skip handle when
// v.x >= waveArea.right" clip is dead code: ReleaseEnd (Gate) and FadeOutStart/LengthEnd
// (Trigger, at full length / zero fade-out) now land at area.right-1 and MUST get handles.
// v.x >= waveArea.right()" clip is dead code: ReleaseEnd (Gate) and FadeOutStart/LengthEnd
// (Trigger, at full length / zero fade-out) now land at area.right()-1 and MUST get handles.
// * Gate's x-axis is SCHEMATIC, not PCM-aligned: the timed region is scaled to the param
// domain (4 x kGateStageMaxSeconds), the sustain reserve is a fixed width, and every
// segment carries a kGateNodeSepPx pixel base. The Gate curve does NOT line up with the
@@ -60,9 +60,9 @@
#include <cstdint>
#include <vector>
#include "editor_geometry.h" // Rect — the shared geometry idiom
#include "core/instrument/ui/editor_geometry.h" // Rect — the shared geometry idiom
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// The play mode the overlay draws — a LOCAL mirror of sampler_core's PlayMode kept here so the
// geometry module stays engine-free (the shell maps the zone's PlayMode to this). Same two cases.
@@ -173,7 +173,7 @@ inline constexpr int kGateNodeSepPx = 8;
// (param clamps are caller-supplied in envelope_edit); only layout does.
inline constexpr double kGateStageMaxSeconds = 2.0;
// The pixel width of the Gate timed region: area.width() minus the sustain-plateau reserve,
// The pixel width of the Gate timed region: area.width minus the sustain-plateau reserve,
// floored at 1 px so the px<->seconds scale never degenerates for a non-empty area. Returns 0
// for a zero/negative-width area. Shared by gatePolyline and envelope_edit's gate drag scale.
int gateTimedWidth(const Rect& area);
@@ -188,7 +188,7 @@ double gatePxPerSecond(const Rect& area);
// Map an amp envelope to its polyline vertices inside `area`, over a sample of `totalSeconds`
// wall-clock duration. `area` is the waveform rect (left/top inclusive, right/bottom exclusive);
// y maps level 0..1 across [area.bottom-1 .. area.top] (level 1 at the TOP). The polyline reads
// y maps level 0..1 across [area.bottom()-1 .. area.y] (level 1 at the TOP). The polyline reads
// left-to-right in draw order, Origin first.
//
// TIME BASE (FA2).
@@ -205,25 +205,25 @@ double gatePxPerSecond(const Rect& area);
// lengthFraction * totalSeconds; fade-in/out are fractions OF that played span. Nodes past
// the played span never appear (FadeOutStart/LengthEnd sit at the played span's right edge).
//
// BOUNDS: every vertex is inside the canvas — x in [area.left, area.right-1], y in
// [area.top, area.bottom-1]. Nothing maps past area.right (the pre-FA2 release tail is gone). A
// BOUNDS: every vertex is inside the canvas — x in [area.x, area.right()-1], y in
// [area.y, area.bottom()-1]. Nothing maps past area.right() (the pre-FA2 release tail is gone). A
// degenerate area (zero width/height) or totalSeconds <= 0 yields the two-point flat baseline
// [Origin, end at level 0] so the shell always has a drawable line. Pure — same inputs, same
// polyline.
std::vector<EnvVertex> buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area,
double totalSeconds);
// Map a time (seconds) to a pixel x inside `area`: t=0 -> area.left, t=totalSeconds ->
// area.right-1, linear, CLAMPED on both sides (t < 0 pins to area.left; t past totalSeconds pins
// to area.right-1 — the in-bounds invariant, FA2). A zero-width area or totalSeconds <= 0 yields
// area.left. Pure — the shared time->x map the Trigger polyline and the node hit-test
// Map a time (seconds) to a pixel x inside `area`: t=0 -> area.x, t=totalSeconds ->
// area.right()-1, linear, CLAMPED on both sides (t < 0 pins to area.x; t past totalSeconds pins
// to area.right()-1 — the in-bounds invariant, FA2). A zero-width area or totalSeconds <= 0 yields
// area.x. Pure — the shared time->x map the Trigger polyline and the node hit-test
// (envelope_edit) use, so the drawn handle and its grab region agree.
int timeToX(const Rect& area, double totalSeconds, double t);
// Map a level (0..1) to a pixel y inside `area`: level 1 -> area.top, level 0 -> area.bottom-1
// Map a level (0..1) to a pixel y inside `area`: level 1 -> area.y, level 0 -> area.bottom()-1
// (so the full-amplitude line sits at the top edge and silence at the bottom pixel row). level is
// clamped to [0,1]. A zero-height area yields area.top. Pure — the shared level->y map the polyline
// clamped to [0,1]. A zero-height area yields area.y. Pure — the shared level->y map the polyline
// and the node hit-test share.
int levelToY(const Rect& area, double level);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,10 +1,10 @@
// keyboard_strip.cpp — see keyboard_strip.h. Pure math; no host types.
#include "keyboard_strip.h"
#include "core/instrument/ui/keyboard_strip.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
@@ -30,24 +30,24 @@ StripLayout layoutStrip(int w, int h) {
const int cw = std::max(0, w);
const int ch = std::max(0, h);
StripLayout out;
out.keys = Rect{0, 0, cw, ch};
out.keys = Rect::ltrb(0, 0, cw, ch);
return out;
}
int keyLeftX(const StripLayout& layout, int note) {
const Rect& band = layout.keys;
const int bandWidth = std::max(0, band.width());
const int bandWidth = std::max(0, band.width);
// note is a KEY here (0..127); its left edge is boundary `note`. Callers pass note+1 to
// get a key's right edge, and 128 maps to the band right.
const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
return keyEdgeToX(band.left, bandWidth, edge);
return keyEdgeToX(band.x, bandWidth, edge);
}
Rect keyRect(const StripLayout& layout, int note) {
const int n = clampNote(note);
const int leftX = keyLeftX(layout, n);
const int rightX = keyLeftX(layout, n + 1);
return Rect{leftX, layout.keys.top, std::max(leftX, rightX), layout.keys.bottom};
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
}
Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
@@ -57,11 +57,11 @@ Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
int keyAtPoint(const StripLayout& layout, int x, int y) {
const Rect& band = layout.keys;
if (!contains(band, x, y)) return -1;
const int bandWidth = std::max(0, band.width());
const int bandWidth = std::max(0, band.width);
if (bandWidth <= 0) return -1;
// Invert keyEdgeToX: the key whose half-open [leftX, rightX) contains x. Floor-divide
// the pixel offset back to a key; clamp defensively (a point on band.right-1 maps to 127).
const int offset = x - band.left;
// the pixel offset back to a key; clamp defensively (a point on band.right()-1 maps to 127).
const int offset = x - band.x;
int note = (offset * kStripKeyCount) / bandWidth;
return clampNote(note);
}
@@ -72,22 +72,22 @@ Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote) {
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
const int leftX = keyLeftX(layout, lo);
const int rightX = keyLeftX(layout, hi + 1);
return Rect{leftX, layout.keys.top, std::max(leftX, rightX), layout.keys.bottom};
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
}
ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y) {
const Rect bar = zoneBarRect(layout, lowNote, highNote);
if (!contains(bar, x, y)) return ZoneGrab::kNone;
const int barW = bar.width();
const int barW = bar.width;
// A narrow bar (< 2*edge) has no body: split at the midpoint, LOW edge wins the tie so
// a click exactly on the midpoint resizes low (deterministic).
if (barW < 2 * kStripEdgeGrabWidth) {
const int mid = bar.left + barW / 2;
const int mid = bar.x + barW / 2;
return x <= mid ? ZoneGrab::kLowEdge : ZoneGrab::kHighEdge;
}
if (x < bar.left + kStripEdgeGrabWidth) return ZoneGrab::kLowEdge;
if (x >= bar.right - kStripEdgeGrabWidth) return ZoneGrab::kHighEdge;
if (x < bar.x + kStripEdgeGrabWidth) return ZoneGrab::kLowEdge;
if (x >= bar.right() - kStripEdgeGrabWidth) return ZoneGrab::kHighEdge;
return ZoneGrab::kBody;
}
@@ -127,7 +127,7 @@ bool isNaturalKey(int note) {
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
if (dxPixels == 0) return clampNote(startNote);
const int bandWidth = std::max(0, layout.keys.width());
const int bandWidth = std::max(0, layout.keys.width);
if (bandWidth <= 0) return clampNote(startNote); // zero-width -> no motion
// Proportional shift: same linear mapping as keyAtPoint/keyEdgeToX so click and drag
// agree across the full strip, even on non-divisible-by-128 widths. The proportional
@@ -146,4 +146,4 @@ int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
return clampNote(startNote + shift);
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -23,9 +23,9 @@
#pragma once
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// The full MIDI key span the strip maps across its width: 128 keys (0..127). Named
// distinctly from embed_strip's kEmbedKeyCount (same value) so the two strips stay
@@ -42,7 +42,7 @@ inline constexpr int kStripEdgeGrabWidth = 6;
// takes the whole area today (a future octave-label lane can carve a sub-band here without
// changing callers). Clamped so a degenerate (tiny/zero) size never yields an inverted rect.
struct StripLayout {
Rect keys; // the key band: the 128-key span maps linearly across keys.width()
Rect keys; // the key band: the 128-key span maps linearly across keys.width
};
// Divide a (w x h) strip area into its regions. Pure: same inputs -> same layout. A zero or
@@ -50,7 +50,7 @@ struct StripLayout {
StripLayout layoutStrip(int w, int h);
// The x pixel (inside the keys band) of the LEFT edge of key `note` (0..127). The 128-key
// span maps linearly across keys.width(); key N occupies the half-open pixel range
// span maps linearly across keys.width; key N occupies the half-open pixel range
// [keyLeftX(N), keyLeftX(N+1)). Notes are clamped to [0,127]; note==128 maps to the band's
// right edge (so a key's right edge is keyLeftX(note+1)). Pure.
int keyLeftX(const StripLayout& layout, int note);
@@ -128,4 +128,4 @@ int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels);
// pastel spectral fill (S-VIEW-7). Pure — no layout required, no host types.
bool isNaturalKey(int note);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,10 +1,10 @@
// knob_deck.cpp — see knob_deck.h. Pure arithmetic; no LICE/VST3/REAPER includes.
#include "knob_deck.h"
#include "core/instrument/ui/knob_deck.h"
#include <algorithm>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
@@ -33,19 +33,20 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
out.id = g.id;
out.box = box;
const int captionTop = box.top + kDeckGroupPadY;
const int innerLeft = box.left + kDeckGroupPadX;
const int innerRight = box.right - kDeckGroupPadX;
const int captionTop = box.y + kDeckGroupPadY;
const int innerLeft = box.x + kDeckGroupPadX;
const int innerRight = box.right() - kDeckGroupPadX;
// Caption row: text left, compact toggle right-anchored (r11 — the not-full-width home).
out.caption = Rect{innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH};
out.caption = Rect::ltrb(innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH);
if (g.captionToggle.id >= 0) {
const int segW = g.captionToggle.segWidth;
const int togTop = captionTop + (kDeckCaptionH - kDeckToggleH) / 2;
const Rect seg1{innerRight - segW, togTop, innerRight, togTop + kDeckToggleH};
const Rect seg0{seg1.left - segW, togTop, seg1.left, togTop + kDeckToggleH};
const Rect seg1 = Rect::ltrb(innerRight - segW, togTop, innerRight, togTop + kDeckToggleH);
const Rect seg0 = Rect::ltrb(seg1.x - segW, togTop, seg1.x, togTop + kDeckToggleH);
out.captionToggle = DeckToggleLayout{g.captionToggle.id, seg0, seg1};
out.caption.right = seg0.left - kDeckToggleGap; // caption text stops at the toggle
// Caption text stops at the toggle: pull the right edge in (XYWH: shrink width).
out.caption.width = (seg0.x - kDeckToggleGap) - out.caption.x;
}
// Knob row: fixed cells left-to-right, then the optional row toggle.
@@ -54,12 +55,12 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
for (int id : g.cellIds) {
DeckCellLayout c;
c.id = id;
c.cell = Rect{x, cellTop, x + kDeckCellW, cellTop + kDeckCellH};
c.cell = Rect::ltrb(x, cellTop, x + kDeckCellW, cellTop + kDeckCellH);
const int knobLeft = x + (kDeckCellW - kDeckKnobSize) / 2;
const int knobTop = cellTop + 4;
c.knob = Rect{knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize};
c.knob = Rect::ltrb(knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize);
const int labelTop = knobTop + kDeckKnobSize + 4;
c.label = Rect{c.cell.left, labelTop, c.cell.right, labelTop + kDeckCellLabelH};
c.label = Rect::ltrb(c.cell.x, labelTop, c.cell.right(), labelTop + kDeckCellLabelH);
out.cells.push_back(c);
x += kDeckCellW;
}
@@ -67,8 +68,8 @@ DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
if (!g.cellIds.empty()) x += kDeckToggleGap;
const int segW = g.rowToggle.segWidth;
const int togTop = cellTop + (kDeckCellH - kDeckToggleH) / 2;
const Rect seg0{x, togTop, x + segW, togTop + kDeckToggleH};
const Rect seg1{seg0.right, togTop, seg0.right + segW, togTop + kDeckToggleH};
const Rect seg0 = Rect::ltrb(x, togTop, x + segW, togTop + kDeckToggleH);
const Rect seg1 = Rect::ltrb(seg0.right(), togTop, seg0.right() + segW, togTop + kDeckToggleH);
out.rowToggle = DeckToggleLayout{g.rowToggle.id, seg0, seg1};
}
return out;
@@ -120,9 +121,9 @@ DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int to
rowHasGroup = false;
}
if (rowHasGroup) x += kDeckGroupGap;
const Rect box{x, y, x + w, y + kDeckGroupH};
const Rect box = Rect::ltrb(x, y, x + w, y + kDeckGroupH);
out.groups.push_back(layoutGroup(g, box));
x = box.right;
x = box.right();
rowHasGroup = true;
}
out.height = out.rowCount * kDeckGroupH + (out.rowCount - 1) * kDeckRowGap;
@@ -152,4 +153,4 @@ DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) {
return {};
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -27,9 +27,9 @@
#include <vector>
#include "editor_geometry.h" // Rect, contains — the shared geometry idiom
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — the shared geometry idiom
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// Fixed deck metrics (spec r11), exposed so the shell and tests agree.
inline constexpr int kDeckCellW = 48; // one knob cell
@@ -130,4 +130,4 @@ struct DeckHit {
// Pure — the shell's routing entry point.
DeckHit hitTestDeck(const DeckLayout& layout, int x, int y);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,30 +1,34 @@
// param_slider.cpp — see param_slider.h. PURE control-surface geometry for the S12/S15/S16
// editor parameter panel. No host types; only the shared Rect + contains().
#include "param_slider.h"
#include "core/instrument/ui/param_slider.h"
#include "core/util/clamp01.h"
#include <algorithm>
#include <cmath>
namespace reasampler::vst {
namespace reasampler::instrument::ui {
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
std::vector<ControlRow> layoutControls(const Rect& panel,
const std::vector<ControlDesc>& controls) {
std::vector<ControlRow> out;
if (controls.empty() || panel.width() <= 0 || panel.height() <= 0) return out;
if (controls.empty() || panel.width <= 0 || panel.height <= 0) return out;
out.reserve(controls.size());
// The label column is clamped so a narrow panel still leaves a control column.
const int labelW = (std::min)(kControlLabelWidth, (std::max)(0, panel.width() / 2));
int rowTop = panel.top;
const int labelW = (std::min)(kControlLabelWidth, (std::max)(0, panel.width / 2));
int rowTop = panel.y;
for (const ControlDesc& d : controls) {
ControlRow r;
r.id = d.id;
r.kind = d.kind;
const int rowBottom = rowTop + kControlRowHeight;
r.row = Rect{panel.left, rowTop, panel.right, rowBottom};
r.label = Rect{panel.left, rowTop, panel.left + labelW, rowBottom};
r.control = Rect{panel.left + labelW, rowTop, panel.right, rowBottom};
r.row = Rect::ltrb(panel.x, rowTop, panel.right(), rowBottom);
r.label = Rect::ltrb(panel.x, rowTop, panel.x + labelW, rowBottom);
r.control = Rect::ltrb(panel.x + labelW, rowTop, panel.right(), rowBottom);
out.push_back(r);
rowTop = rowBottom + kControlRowGap;
}
@@ -33,13 +37,13 @@ std::vector<ControlRow> layoutControls(const Rect& panel,
Rect toggleSegmentRect(const Rect& control, int seg) {
if (seg < 0 || seg >= kToggleSegments) return Rect{};
const int w = control.width();
if (w <= 0 || control.height() <= 0) return Rect{};
const int w = control.width;
if (w <= 0 || control.height <= 0) return Rect{};
const int segW = w / kToggleSegments;
const int left = control.left + seg * segW;
const int left = control.x + seg * segW;
// The last segment absorbs the width remainder so the segments tile the whole control.
const int right = (seg == kToggleSegments - 1) ? control.right : left + segW;
return Rect{left, control.top, right, control.bottom};
const int right = (seg == kToggleSegments - 1) ? control.right() : left + segW;
return Rect::ltrb(left, control.y, right, control.bottom());
}
int toggleSegmentHitTest(const Rect& control, int x, int y) {
@@ -52,31 +56,31 @@ int toggleSegmentHitTest(const Rect& control, int x, int y) {
Rect sliderTrackRect(const Rect& control) {
// Inset a half-handle at each end so the handle stays fully inside the control at value
// 0 and 1. The handle CENTER ranges across [track.left, track.right].
// 0 and 1. The handle CENTER ranges across [track.x, track.right()].
const int half = kSliderHandleWidth / 2;
if (control.width() <= kSliderHandleWidth || control.height() <= 0) return Rect{};
return Rect{control.left + half, control.top, control.right - half, control.bottom};
if (control.width <= kSliderHandleWidth || control.height <= 0) return Rect{};
return Rect::ltrb(control.x + half, control.y, control.right() - half, control.bottom());
}
Rect sliderHandleRect(const Rect& control, double value) {
const Rect track = sliderTrackRect(control);
if (track.width() <= 0) return Rect{};
if (track.width <= 0) return Rect{};
if (value < 0.0) value = 0.0;
if (value > 1.0) value = 1.0;
const int span = track.width(); // handle-center movable span
const int centerX = track.left + static_cast<int>(value * span + 0.5);
const int span = track.width; // handle-center movable span
const int centerX = track.x + static_cast<int>(value * span + 0.5);
const int half = kSliderHandleWidth / 2;
return Rect{centerX - half, control.top, centerX - half + kSliderHandleWidth,
control.bottom};
return Rect::ltrb(centerX - half, control.y, centerX - half + kSliderHandleWidth,
control.bottom());
}
double valueAtPoint(const Rect& control, int x) {
const Rect track = sliderTrackRect(control);
const int span = track.width();
const int span = track.width;
if (span <= 0) return 0.0;
if (x <= track.left) return 0.0;
if (x >= track.right) return 1.0;
return static_cast<double>(x - track.left) / static_cast<double>(span);
if (x <= track.x) return 0.0;
if (x >= track.right()) return 1.0;
return static_cast<double>(x - track.x) / static_cast<double>(span);
}
// --- Radial knob (Wave A FA4) ---------------------------------------------------------
@@ -95,16 +99,15 @@ double normDeg(double deg) {
return deg;
}
double clamp01(double v) { return (std::min)(1.0, (std::max)(0.0, v)); }
} // namespace
KnobGeometry computeKnob(const Rect& cell) {
if (cell.width() <= 0 || cell.height() <= 0) return KnobGeometry{};
if (cell.width <= 0 || cell.height <= 0) return KnobGeometry{};
KnobGeometry g;
g.centerX = (cell.left + cell.right) / 2.0;
g.centerY = (cell.top + cell.bottom) / 2.0;
g.radius = (std::min)(cell.width(), cell.height()) / 2.0;
g.centerX = (cell.x + cell.right()) / 2.0;
g.centerY = (cell.y + cell.bottom()) / 2.0;
g.radius = (std::min)(cell.width, cell.height) / 2.0;
return g;
}
@@ -154,4 +157,4 @@ int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y) {
return -1;
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -24,9 +24,9 @@
#include <vector>
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
namespace reasampler::vst {
namespace reasampler::instrument::ui {
// Fixed control-panel metrics, exposed so the shell and tests agree.
inline constexpr int kControlRowHeight = 22; // one control row (incl. its inter-row gap)
@@ -81,7 +81,7 @@ int toggleSegmentHitTest(const Rect& control, int x, int y);
// The slider track sub-rect inside a slider control's `control` rect: the control inset so the
// handle (kSliderHandleWidth) stays fully within the control at value 0 and 1 (a half-handle
// margin at each end). The handle CENTER ranges across [track.left, track.right] as the value
// margin at each end). The handle CENTER ranges across [track.x, track.right()] as the value
// ranges [0,1]. The shell draws the track fill + handle here. A degenerate control yields an
// empty rect. Pure.
Rect sliderTrackRect(const Rect& control);
@@ -177,4 +177,4 @@ double knobDragValue(double startValue, int dyPixels,
// toggleSegmentHitTest / knobDragValue over the ensuing drag) and commits.
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -1,11 +1,11 @@
// waveform_view.cpp — see waveform_view.h. Pure math; no host types.
#include "waveform_view.h"
#include "core/instrument/ui/waveform_view.h"
#include <algorithm>
#include <cstdlib> // std::abs (int overload)
namespace reasampler::vst {
namespace reasampler::instrument::ui {
namespace {
@@ -18,21 +18,21 @@ std::int64_t clampFrame(std::int64_t f, std::int64_t frameCount) {
} // namespace
int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame) {
const int w = std::max(0, area.width());
if (frameCount <= 0 || w <= 0) return area.left;
const int w = std::max(0, area.width);
if (frameCount <= 0 || w <= 0) return area.x;
const std::int64_t f = clampFrame(frame, frameCount);
// Linear map: x = left + round(f * w / frameCount). Rounding keeps the marker line
// visually centered on its frame; the divide is exact rational (multiply first).
const std::int64_t num = f * static_cast<std::int64_t>(w) + frameCount / 2;
return area.left + static_cast<int>(num / frameCount);
return area.x + static_cast<int>(num / frameCount);
}
std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x) {
const int w = std::max(0, area.width());
const int w = std::max(0, area.width);
if (frameCount <= 0 || w <= 0) return 0;
if (x <= area.left) return 0;
if (x >= area.right) return frameCount;
const std::int64_t dx = static_cast<std::int64_t>(x - area.left);
if (x <= area.x) return 0;
if (x >= area.right()) return frameCount;
const std::int64_t dx = static_cast<std::int64_t>(x - area.x);
// Inverse of frameToX: frame = round(dx * frameCount / w). Round so click and marker draw
// agree at bin granularity.
const std::int64_t num = dx * frameCount + static_cast<std::int64_t>(w) / 2;
@@ -54,7 +54,7 @@ std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::in
int dxPixels) {
const std::int64_t start = clampFrame(startFrame, frameCount);
if (dxPixels == 0) return start;
const int w = std::max(0, area.width());
const int w = std::max(0, area.width);
if (frameCount <= 0 || w <= 0) return start; // no room to move
// Proportional shift, rounded to the nearest frame (same linear map as frameToX/xToFrame).
const std::int64_t magnitude =
@@ -96,4 +96,4 @@ std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
return t; // no sign change in the whole buffer -> keep the raw (clamped) target
}
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
@@ -24,10 +24,12 @@
#include <cstdint>
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
#include "peaks.h" // AudioSample (float), the mono PCM the snap scans
#include "core/instrument/ui/editor_geometry.h" // Rect, contains — one shared geometry idiom
#include "core/audio/peaks.h" // AudioSample (float), the mono PCM the snap scans
namespace reasampler::vst {
namespace reasampler::instrument::ui {
using audio::AudioSample;
// The width (px) of a marker's grab region either side of its x line: a grab within this many
// pixels of a marker's drawn x is a grab OF that marker. Mirrors keyboard_strip's edge-grab
@@ -35,14 +37,14 @@ namespace reasampler::vst {
// distinguishable.
inline constexpr int kMarkerGrabWidth = 5;
// The x pixel (inside `area`) of frame `frame` under the linear map: frame 0 -> area.left,
// frame frameCount -> area.right. A frame is clamped to [0, frameCount] before mapping, so an
// The x pixel (inside `area`) of frame `frame` under the linear map: frame 0 -> area.x,
// frame frameCount -> area.right(). A frame is clamped to [0, frameCount] before mapping, so an
// out-of-range frame pins to an edge rather than escaping the rect. frameCount <= 0 or a
// zero-width area pins every frame to area.left (a degenerate, non-inverting result). Pure.
// zero-width area pins every frame to area.x (a degenerate, non-inverting result). Pure.
int frameToX(const Rect& area, std::int64_t frameCount, std::int64_t frame);
// The frame a point x (inside `area`) maps to under the inverse linear map, clamped to
// [0, frameCount]. A point left of area.left yields 0; right of area.right yields frameCount.
// [0, frameCount]. A point left of area.x yields 0; right of area.right() yields frameCount.
// frameCount <= 0 or a zero-width area yields 0. Pure — the inverse of frameToX (round-trips
// to the same frame at bin granularity).
std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x);
@@ -80,4 +82,4 @@ std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::in
std::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
std::int64_t target);
} // namespace reasampler::vst
} // namespace reasampler::instrument::ui
+1 -1
View File
@@ -138,7 +138,7 @@ public:
// Captures the raw source text of one value verbatim (string-aware brace
// matching), so a nested blob can be handed to its own parser — the
// bank_book -> BankIndex::deserialize seam.
// bank_book -> BankModel::deserialize seam.
bool captureValue(std::string& raw);
private:
@@ -1,4 +1,4 @@
#include "bank_book.h"
#include "core/model/bank_book.h"
#include <algorithm>
#include <unordered_set>
@@ -9,136 +9,16 @@
//
// JSON rides on the shared core/json lexical layer (Q-W1), matching bank_model
// and view_mode_model. The book blob nests one bank object per bank, each carrying that
// bank's BankIndex serialized by bank_model's OWN writer (BankIndex::serialize),
// bank's BankModel serialized by bank_model's OWN writer (BankModel::serialize),
// so per-bank sample serialization stays owned by bank_model and is not duplicated
// here. The book writer emits the bank envelope (id / displayName / ordinal) plus a
// raw "index" member whose value is the BankIndex blob verbatim; the parser splits
// raw "index" member whose value is the BankModel blob verbatim; the parser splits
// the book envelope, then hands each nested index blob straight to
// BankIndex::deserialize. Ints use %d; strings are escaped by writeEscaped.
// BankModel::deserialize. Ints use %d; strings are escaped by writeEscaped.
namespace reasampler {
// ===========================================================================
// SlotMap — the L7 gap-preserving display-position carrier (pure). See bank_book.h.
// The invariant: entries_ is kept sorted ascending by slot, one id per slot, one
// slot per id. Every mutator restores it; queries assume it.
// ===========================================================================
void SlotMap::sortBySlot() {
std::stable_sort(entries_.begin(), entries_.end(),
[](const Entry& a, const Entry& b) { return a.slot < b.slot; });
}
int SlotMap::slotOf(const std::string& id) const {
for (const auto& e : entries_)
if (e.id == id) return e.slot;
return -1;
}
std::string SlotMap::idAt(int slot) const {
for (const auto& e : entries_)
if (e.slot == slot) return e.id;
return {};
}
int SlotMap::maxSlot() const {
int m = -1;
for (const auto& e : entries_)
if (e.slot > m) m = e.slot;
return m;
}
std::vector<std::string> SlotMap::orderedIds() const {
// entries_ is sorted ascending by slot, so a straight walk is display order.
std::vector<std::string> out;
out.reserve(entries_.size());
for (const auto& e : entries_) out.push_back(e.id);
return out;
}
void SlotMap::append(const std::string& id) {
if (id.empty()) return;
remove(id); // an existing id is re-appended, not left in place
entries_.push_back(Entry{id, maxSlot() + 1}); // next free slot after the last occupied
sortBySlot();
}
bool SlotMap::remove(const std::string& id) {
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
if (it->id == id) {
entries_.erase(it); // leaves the slot empty — no re-pack
return true;
}
}
return false;
}
bool SlotMap::reorder(const std::string& id, int targetSlot) {
if (slotOf(id) < 0) return false; // not mapped -> no mutation
if (targetSlot < 0) targetSlot = 0;
if (slotOf(id) == targetSlot) return false; // already there — true no-op
// Detach the moving id first so the occupancy test below sees the post-move world.
remove(id);
const bool occupied = !idAt(targetSlot).empty();
if (occupied) {
// Insert-before-and-shift: every occupant at slot >= targetSlot shifts up by one,
// preserving relative order and interior gaps above the target. The moving id then
// takes targetSlot cleanly.
for (auto& e : entries_)
if (e.slot >= targetSlot) ++e.slot;
}
entries_.push_back(Entry{id, targetSlot});
sortBySlot();
return true;
}
void SlotMap::resetDense(const std::vector<std::string>& ids) {
entries_.clear();
int slot = 0;
for (const auto& id : ids) {
if (id.empty()) continue;
if (slotOf(id) >= 0) continue; // skip a duplicate id (one slot per id)
entries_.push_back(Entry{id, slot++});
}
// Already ascending by construction; no sort needed.
}
void SlotMap::reconcile(const std::vector<std::string>& liveIds) {
// Drop markers whose sample left the index.
entries_.erase(
std::remove_if(entries_.begin(), entries_.end(),
[&](const Entry& e) {
return std::find(liveIds.begin(), liveIds.end(), e.id) ==
liveIds.end();
}),
entries_.end());
// Append live ids that have no mapping yet (out-of-band index growth), in liveIds
// order, each to the next free slot after the current frontier.
for (const auto& id : liveIds)
if (slotOf(id) < 0) append(id);
sortBySlot();
}
bool SlotMap::operator==(const SlotMap& o) const {
return entries_ == o.entries_;
}
SlotMap SlotMap::fromEntries(const std::vector<std::pair<std::string, int>>& pairs) {
SlotMap m;
for (const auto& [id, slot] : pairs) {
if (id.empty() || slot < 0) continue; // drop malformed pair
if (m.slotOf(id) >= 0) continue; // duplicate id: first wins
if (!m.idAt(slot).empty()) continue; // slot taken: never double-occupy
m.entries_.push_back(Entry{id, slot});
}
m.sortBySlot();
return m;
}
// SlotMap::serialize is defined in the JSON writer section below (it reuses the
// shared core/json emit helpers).
// SlotMap lives in core/model/slot_map.cpp (extracted Q-W1, T4-05).
// ---------------------------------------------------------------------------
// BankBook — construction + bank lookup
@@ -165,12 +45,12 @@ const Bank* BankBook::bank(const std::string& id) const {
return nullptr;
}
BankIndex* BankBook::index(const std::string& id) {
BankModel* BankBook::index(const std::string& id) {
Bank* b = bank(id);
return b ? &b->index : nullptr;
}
const BankIndex* BankBook::index(const std::string& id) const {
const BankModel* BankBook::index(const std::string& id) const {
const Bank* b = bank(id);
return b ? &b->index : nullptr;
}
@@ -326,13 +206,13 @@ bool BankBook::evacuate(const std::string& id) {
if (src == nullptr) return false;
// Move every member into the pool, index-only, observing destination collapse.
// Snapshot the members first, then clear the source — BankIndex has no bulk move,
// Snapshot the members first, then clear the source — BankModel has no bulk move,
// and adding into the pool must not alias the vector we are draining.
BankIndex& poolIndex = pool().index;
BankModel& poolIndex = pool().index;
const std::vector<Sample> members = src->index.all(); // copy
for (const auto& s : members)
poolIndex.add(s); // Added or Collapsed; either way the pool now holds the hash
src->index = BankIndex{}; // leave the evacuated bank empty
src->index = BankModel{}; // leave the evacuated bank empty
return true;
}
@@ -346,12 +226,12 @@ bool BankBook::setActiveBank(const std::string& id) {
return true;
}
BankIndex& BankBook::activeIndex() {
BankModel& BankBook::activeIndex() {
// activeBankId_ always names a live bank; it falls back to the pool on delete.
return bank(activeBankId_)->index;
}
const BankIndex& BankBook::activeIndex() const {
const BankModel& BankBook::activeIndex() const {
return bank(activeBankId_)->index;
}
@@ -361,11 +241,11 @@ const BankIndex& BankBook::activeIndex() const {
namespace {
// Adds `s` to `dest` and maps the BankIndex outcome onto the transfer outcome for
// Adds `s` to `dest` and maps the BankModel outcome onto the transfer outcome for
// the "gained a NEW entry" case (`gained`) vs the collapse case. Rejected outcomes
// (absolute path / empty id) cannot occur here: the sample already passed add() on
// the source side, so its path and id are already valid.
TransferResult applyDestAdd(BankIndex& dest, const Sample& s, TransferResult gained) {
TransferResult applyDestAdd(BankModel& dest, const Sample& s, TransferResult gained) {
return dest.add(s) == AddResult::Collapsed ? TransferResult::Collapsed : gained;
}
@@ -445,7 +325,7 @@ bool BankBook::updateSampleInPlace(const std::string& sampleId, const Sample& up
namespace {
// The bank's live sample ids in INDEX (insertion) order — the reconcile/migration seed.
std::vector<std::string> indexIds(const BankIndex& idx) {
std::vector<std::string> indexIds(const BankModel& idx) {
std::vector<std::string> ids;
for (const auto& s : idx.all()) ids.push_back(s.id);
return ids;
@@ -557,20 +437,6 @@ using ObjWriter = json::Writer;
} // namespace
std::string SlotMap::serialize() const {
// Array of {id, slot} objects in ascending slot order (entries_ is kept sorted).
std::string out;
out += '[';
for (std::size_t i = 0; i < entries_.size(); ++i) {
if (i) out += ',';
ObjWriter e(out);
e.keyStr("id", entries_[i].id);
e.keyRaw("slot", intToStr(entries_[i].slot));
}
out += ']';
return out;
}
std::string BankBook::serialize() const {
std::string out;
{
@@ -578,7 +444,7 @@ std::string BankBook::serialize() const {
root.keyRaw("version", intToStr(1));
root.keyStr("activeBank", activeBankId_);
// banks: array of { id, displayName, ordinal, index: <BankIndex blob> }.
// banks: array of { id, displayName, ordinal, index: <BankModel blob> }.
// The pool rides in as bank-zero, persisted identically to any named bank.
root.keyBegin("banks");
out += '[';
@@ -589,7 +455,7 @@ std::string BankBook::serialize() const {
b.keyStr("displayName", banks_[i].displayName);
b.keyRaw("ordinal", intToStr(banks_[i].ordinal));
// The nested index is bank_model's own JSON, emitted verbatim so the
// per-sample shape stays owned by BankIndex::serialize (not duplicated).
// per-sample shape stays owned by BankModel::serialize (not duplicated).
b.keyRaw("index", banks_[i].index.serialize());
// L7 display positions (gap-preserving). Absent on a pre-L7 blob; the
// parser defaults such a bank's slots from insertion order on load.
@@ -638,7 +504,7 @@ bool parseBank(json::Reader& r, Bank& b) {
} else if (key == "index") {
std::string raw;
if (!r.captureValue(raw)) return false;
auto idx = BankIndex::deserialize(raw);
auto idx = BankModel::deserialize(raw);
if (!idx) return false; // a malformed nested index fails the whole parse
b.index = std::move(*idx);
haveIndex = true;
@@ -718,7 +584,7 @@ bool parseBook(json::Reader& r, const std::string& raw, std::vector<Bank>& banks
if (!r.parseString(activeBank)) return false;
} else if (key == "samples") {
// Legacy marker. The legacy index is re-parsed from the whole input below
// (BankIndex::deserialize owns that shape); here we only skip the value to
// (BankModel::deserialize owns that shape); here we only skip the value to
// keep the scan well-formed and note that we saw it.
sawSamples = true;
if (!r.skipValue()) return false;
@@ -734,7 +600,7 @@ bool parseBook(json::Reader& r, const std::string& raw, std::vector<Bank>& banks
// --- Legacy migration: a bare bank_index (samples, no banks) → pool. ---
if (!sawBanks) {
if (!sawSamples) return false; // neither shape's marker → malformed
auto legacy = BankIndex::deserialize(raw);
auto legacy = BankModel::deserialize(raw);
if (!legacy) return false;
Bank pool;
pool.id = kPoolBankId;
+19 -102
View File
@@ -10,9 +10,9 @@
// -- What it is --------------------------------------------------------------
//
// An ordered registry of banks. Each bank = { stable id, display name, ordinal,
// BankIndex }. The book WRAPS N BankIndex instances — bank_model / BankIndex are
// BankModel }. The book WRAPS N BankModel instances — bank_model / BankModel are
// UNTOUCHED (additive: no bankId on Sample). Movement of samples between banks is
// index-only (remove from source's BankIndex, add to destination's); files never
// index-only (remove from source's BankModel, add to destination's); files never
// relocate — banks are logical groupings over one shared file pool.
//
// -- The pool (privileged, not special-cased) --------------------------------
@@ -42,113 +42,30 @@
#include <utility>
#include <vector>
#include "bank_model.h"
#include "core/model/bank_model.h"
#include "core/model/slot_map.h"
namespace reasampler {
// Q-W1 interim: this god module re-namespaces in its own split wave; until then the
// clean model types it wraps live in reasampler::model.
using namespace model;
// The pool's fixed identity. The id is reserved: createBank rejects it, and the
// pool is always bank-zero. The name is fixed: renameBank rejects the pool.
inline constexpr const char* kPoolBankId = "pool";
inline constexpr const char* kPoolBankName = "Pool";
// SlotMap — the L7 gap-preserving display-position carrier for ONE bank (F2 settled:
// plain interchangeable slots, NOT M9 fixed/addressable slots). A slot is just a
// display position a sample id occupies; the map is sample id -> slot (>= 0). Gaps
// are first-class: a bank may have a sample at slot 1 with slot 0 empty (an empty
// first row above an occupied second row). At most one id per slot (a slot is never
// double-occupied) and at most one slot per id (an id sits in exactly one place).
//
// Position lives HERE, not on Sample (CLAUDE.md wrapping discipline): a copy of one
// sample into two banks may sit at different slots, so position is a per-bank display
// concern owned by the bank's membership. bank_model / Sample stay untouched.
//
// PURE: standard library only. Hard-tested to the bar of BankIndex's round-trip.
class SlotMap {
public:
// The slot an id occupies, or -1 if the id is not mapped. O(N).
int slotOf(const std::string& id) const;
// The id occupying `slot`, or "" if the slot is empty. O(N).
std::string idAt(int slot) const;
// The highest occupied slot, or -1 when the map is empty. Defines the append
// frontier and (with trailing-empty trim) the content extent.
int maxSlot() const;
// Ids in ASCENDING slot order (the deterministic display order). Empty slots
// produce no entry — the caller iterates occupants; sparse layout is a draw
// concern that reads slotOf/idAt, not this list.
std::vector<std::string> orderedIds() const;
// Places `id` at the next free slot after the last occupied one (append). If the
// id is already mapped it is first removed (leaving its old slot empty), then
// appended — an append never fills an earlier gap. No-op guard: empty id ignored.
void append(const std::string& id);
// Drops `id`'s mapping, LEAVING ITS SLOT EMPTY (no re-pack) so every other id
// keeps its position. Returns true if the id was mapped.
bool remove(const std::string& id);
// Moves `id` to `targetSlot`, gap-preserving (F3 reorder semantics):
// * target slot EMPTY -> `id` moves there; its old slot is left empty.
// * target slot OCCUPIED -> insert-before-and-shift: `id` takes targetSlot and
// every occupant at slot >= targetSlot (except `id` itself) shifts up by one,
// preserving their relative order and never colliding. Matches file-manager
// reorder. Interior gaps between shifted occupants are preserved as-is
// (shift is +1 on each occupant, so the gap structure above the target is kept).
// * negative targetSlot is clamped to 0.
// Returns false (no mutation) if `id` is not mapped. Deterministic.
bool reorder(const std::string& id, int targetSlot);
// Rebuilds the map densely from `ids` in the given order (slot i = ids[i]),
// dropping any prior state. The migration path: a pre-L7 bank with no persisted
// slot data is seeded from its BankIndex insertion order, densely packed (no gaps),
// so it is visually identical on first post-L7 load. Empty/duplicate ids skipped.
void resetDense(const std::vector<std::string>& ids);
// Drops any mapping whose id is NOT in `liveIds` (a stale marker whose sample left
// the index) and appends any live id that has NO mapping yet (a sample the index
// gained out-of-band). Slots of surviving ids are untouched (gaps preserved). Keeps
// the map consistent with the bank's membership without a re-pack. Deterministic:
// orphan appends follow `liveIds` order.
void reconcile(const std::vector<std::string>& liveIds);
bool empty() const { return entries_.empty(); }
std::size_t size() const { return entries_.size(); }
bool operator==(const SlotMap& o) const;
// JSON fragment (an array of {id, slot} objects, ascending slot). Emitted as the
// bank envelope's "slots" member by BankBook::serialize; parsed back by its parser.
// Round-trips losslessly with the rest of the bank.
std::string serialize() const;
// Builds a map from explicit (id, slot) pairs parsed from persisted JSON. Enforces
// the map invariants defensively against a hand-edited blob: a duplicate id keeps
// its FIRST occurrence; a slot already taken by a kept id drops the later pair
// (never double-occupies); an empty id or negative slot is dropped. The result is
// sorted ascending by slot. reconcile() against live membership runs afterward, so
// a lossy repair here degrades gracefully rather than corrupting lookup.
static SlotMap fromEntries(const std::vector<std::pair<std::string, int>>& pairs);
private:
struct Entry {
std::string id;
int slot = 0;
bool operator==(const Entry& o) const { return id == o.id && slot == o.slot; }
};
std::vector<Entry> entries_; // kept sorted ascending by slot (invariant)
void sortBySlot();
};
// SlotMap — extracted to its own TU/header pair (Q-W1, T4-05): core/model/slot_map.h.
// Included above because Bank carries one per bank.
// One bank: a stable id, a display name, an ordinal (tab/display order), and its
// own BankIndex. The pool is the bank whose id == kPoolBankId.
// own BankModel. The pool is the bank whose id == kPoolBankId.
struct Bank {
std::string id; // stable, persisted; the pool's is kPoolBankId
std::string displayName; // mutable for named banks; fixed "Pool" for the pool
int ordinal = 0; // display order; pool is 0, named banks 1..N
BankIndex index; // this bank's samples
BankModel index; // this bank's samples
SlotMap slots; // L7 display positions of this bank's samples (gap-preserving)
bool isPool() const { return id == kPoolBankId; }
@@ -251,10 +168,10 @@ public:
// bank — an invalid set never corrupts state.
bool setActiveBank(const std::string& id);
// The active bank's BankIndex — the index the capture layer adds to. Always
// The active bank's BankModel — the index the capture layer adds to. Always
// valid (the active id always names a live bank; it falls back to the pool).
BankIndex& activeIndex();
const BankIndex& activeIndex() const;
BankModel& activeIndex();
const BankModel& activeIndex() const;
// -- Sample movement (index-only; files never relocate) ------------------
@@ -334,7 +251,7 @@ public:
// Refreshes a sample IN PLACE wherever it lives in the book (M10 re-capture):
// finds the bank holding `sampleId` and replaces its entry with `updated`
// (order-preserving, no dedup — see BankIndex::updateInPlace). Scans banks in
// (order-preserving, no dedup — see BankModel::updateInPlace). Scans banks in
// ordinal order and updates the FIRST holder (a sample id is unique within a
// bank; the same id living in two banks via copy would update the earliest, which
// is acceptable — re-capture operates on the panel's focused single selection).
@@ -372,9 +289,9 @@ public:
Bank* bank(const std::string& id);
const Bank* bank(const std::string& id) const;
// The bank's BankIndex by id, or nullptr. Convenience over bank()->index.
BankIndex* index(const std::string& id);
const BankIndex* index(const std::string& id) const;
// The bank's BankModel by id, or nullptr. Convenience over bank()->index.
BankModel* index(const std::string& id);
const BankModel* index(const std::string& id) const;
// The pool (always present). Never null.
Bank& pool();
@@ -1,4 +1,4 @@
#include "bank_model.h"
#include "core/model/bank_model.h"
#include <cctype>
@@ -14,7 +14,7 @@
// shortest form that round-trips every IEEE-754 double exactly, so the
// deserialize(serialize(x)) == x invariant holds bit-for-bit.
namespace reasampler {
namespace reasampler::model {
// ---------------------------------------------------------------------------
// equality
@@ -74,10 +74,10 @@ static bool isAbsolutePath(const std::string& p) {
}
// ---------------------------------------------------------------------------
// BankIndex
// BankModel
// ---------------------------------------------------------------------------
AddResult BankIndex::add(const Sample& sample) {
AddResult BankModel::add(const Sample& sample) {
if (sample.id.empty()) return AddResult::RejectedEmptyId;
if (isAbsolutePath(sample.relativePath)) return AddResult::RejectedAbsolutePath;
@@ -88,7 +88,7 @@ AddResult BankIndex::add(const Sample& sample) {
return AddResult::Added;
}
bool BankIndex::remove(const std::string& id) {
bool BankModel::remove(const std::string& id) {
for (auto it = samples_.begin(); it != samples_.end(); ++it) {
if (it->id == id) {
samples_.erase(it);
@@ -98,7 +98,7 @@ bool BankIndex::remove(const std::string& id) {
return false;
}
bool BankIndex::updateInPlace(const std::string& id, const Sample& updated) {
bool BankModel::updateInPlace(const std::string& id, const Sample& updated) {
if (isAbsolutePath(updated.relativePath)) return false; // invariant still holds
for (auto& s : samples_) {
if (s.id == id) {
@@ -109,20 +109,20 @@ bool BankIndex::updateInPlace(const std::string& id, const Sample& updated) {
return false;
}
const Sample* BankIndex::query(const std::string& id) const {
const Sample* BankModel::query(const std::string& id) const {
for (const auto& s : samples_)
if (s.id == id) return &s;
return nullptr;
}
const Sample* BankIndex::findByHash(const std::string& contentHash) const {
const Sample* BankModel::findByHash(const std::string& contentHash) const {
if (contentHash.empty()) return nullptr; // empty hashes never dedup
for (const auto& s : samples_)
if (s.contentHash == contentHash) return &s;
return nullptr;
}
bool BankIndex::moveTier(const std::string& id, Tier tier) {
bool BankModel::moveTier(const std::string& id, Tier tier) {
for (auto& s : samples_) {
if (s.id == id) {
s.tier = tier;
@@ -132,7 +132,7 @@ bool BankIndex::moveTier(const std::string& id, Tier tier) {
return false;
}
std::vector<Sample> BankIndex::byTier(Tier tier) const {
std::vector<Sample> BankModel::byTier(Tier tier) const {
std::vector<Sample> out;
for (const auto& s : samples_)
if (s.tier == tier) out.push_back(s);
@@ -223,7 +223,7 @@ void writeSample(std::string& out, const Sample& s) {
} // namespace
std::string BankIndex::serialize() const {
std::string BankModel::serialize() const {
std::string out;
{
ObjWriter root(out);
@@ -410,7 +410,7 @@ bool parseSample(json::Reader& r, Sample& s) {
return r.consume('}');
}
bool parseIndex(json::Reader& r, BankIndex& out) {
bool parseIndex(json::Reader& r, BankModel& out) {
if (!r.consume('{')) return false;
r.skipWs();
if (r.consume('}')) return true; // empty object — vacuously an empty index
@@ -451,11 +451,11 @@ bool parseIndex(json::Reader& r, BankIndex& out) {
} // namespace
std::optional<BankIndex> BankIndex::deserialize(const std::string& blob) {
BankIndex idx;
std::optional<BankModel> BankModel::deserialize(const std::string& blob) {
BankModel idx;
json::Reader r(blob);
if (!parseIndex(r, idx)) return std::nullopt;
return idx;
}
} // namespace reasampler
} // namespace reasampler::model
@@ -1,7 +1,7 @@
#pragma once
// bank_model — the HEART of ReaSampler, deliberately free of any REAPER type so
// it compiles and unit-tests OUTSIDE the DAW. It owns the per-project sample
// bank: the `Sample` metadata struct and the `BankIndex` (add / remove / query /
// bank: the `Sample` metadata struct and the `BankModel` (add / remove / query /
// tier moves / dedup-by-hash + JSON round-trip to/from std::string).
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
@@ -12,7 +12,7 @@
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::model {
// How the source audio was obtained. Kept in the pure core (no REAPER coupling);
// the capture backends (M3/M8) map their own notion onto these.
@@ -81,7 +81,7 @@ struct LoopPoints {
// The metadata record for one captured sample. The audio itself lives in a
// project-relative file; `relativePath` is ALWAYS relative (enforced at the
// BankIndex::add boundary — see AddResult).
// BankModel::add boundary — see AddResult).
struct Sample {
std::string id; // stable unique id (assigned by the caller)
std::string displayName;
@@ -128,7 +128,7 @@ struct Sample {
Tier tier = Tier::Scratch;
std::string contentHash; // dedup key (see BankIndex)
std::string contentHash; // dedup key (see BankModel)
std::optional<Provenance> provenance; // set only when resampled
@@ -141,7 +141,7 @@ struct Sample {
bool isAutoPrunable() const { return tier == Tier::Scratch; }
};
// Outcome of BankIndex::add. `add` rejects rather than silently mutating:
// Outcome of BankModel::add. `add` rejects rather than silently mutating:
// - RejectedAbsolutePath: relativePath was absolute (precision invariant).
// - RejectedEmptyId: id was empty (the collection is keyed by id).
// - Collapsed: content hash matched an existing entry; the existing
@@ -157,7 +157,7 @@ enum class AddResult {
// An ordered, id-keyed collection of Samples with content-hash dedup, tier
// moves/filtering, and lossless JSON round-trip. Insertion order is preserved
// so a future panel (M5) can iterate in stable order.
class BankIndex {
class BankModel {
public:
// Adds a sample. Enforces the relative-paths-only invariant and dedups by
// content hash (an equal-hash add collapses onto the existing entry rather
@@ -200,7 +200,7 @@ public:
std::size_t size() const { return samples_.size(); }
bool empty() const { return samples_.empty(); }
bool operator==(const BankIndex& o) const { return samples_ == o.samples_; }
bool operator==(const BankModel& o) const { return samples_ == o.samples_; }
// Serializes the whole index to a JSON string (lossless round-trip).
std::string serialize() const;
@@ -208,10 +208,10 @@ public:
// Parses a JSON string produced by serialize(). Returns std::nullopt on
// malformed / truncated input (error signaled, never UB). On success the
// returned index satisfies deserialize(serialize(x)) == x.
static std::optional<BankIndex> deserialize(const std::string& json);
static std::optional<BankModel> deserialize(const std::string& json);
private:
std::vector<Sample> samples_; // insertion order preserved
};
} // namespace reasampler
} // namespace reasampler::model
@@ -1,4 +1,4 @@
#include "owned_manifest.h"
#include "core/model/owned_manifest.h"
#include <cctype>
@@ -13,7 +13,7 @@
//
// so a compact writer + a focused string-array domain parse is all it needs.
namespace reasampler {
namespace reasampler::model {
// ---------------------------------------------------------------------------
// path invariant (mirror of bank_model's isAbsolutePath)
@@ -110,4 +110,4 @@ std::optional<OwnedFileManifest> OwnedFileManifest::deserialize(const std::strin
return m;
}
} // namespace reasampler
} // namespace reasampler::model
@@ -23,17 +23,17 @@
// -- The relative-paths-only invariant ---------------------------------------
//
// A manifest path is ALWAYS project-relative (same invariant as Sample.relativePath
// and the persisted BankIndex). add() rejects an absolute path rather than guess a
// and the persisted BankModel). add() rejects an absolute path rather than guess a
// relativization — the pure model has no project root, so a "normalization" would be
// a guess that could point at the wrong file (mirror of BankIndex::add's rejection).
// a guess that could point at the wrong file (mirror of BankModel::add's rejection).
#include <optional>
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::model {
// Outcome of an add(). Mirrors BankIndex::AddResult's honesty — the op reports what
// Outcome of an add(). Mirrors BankModel::AddResult's honesty — the op reports what
// happened rather than silently mutating on a bad request.
// - Added: the path was new and recorded.
// - AlreadyPresent: the path was already in the manifest (dedup no-op).
@@ -88,4 +88,4 @@ private:
std::vector<std::string> paths_; // insertion order; deduplicated
};
} // namespace reasampler
} // namespace reasampler::model
@@ -1,4 +1,4 @@
#include "provenance.h"
#include "core/model/provenance.h"
#include <cstdio>
@@ -25,7 +25,7 @@
// injection-proof on its own and can be embedded whole as one more length-prefixed
// field of the fingerprint.
namespace reasampler {
namespace reasampler::model {
bool CaptureRecipe::operator==(const CaptureRecipe& o) const {
return scope == o.scope && sourceMode == o.sourceMode &&
@@ -159,4 +159,4 @@ std::optional<std::string> detectParent(
return parent;
}
} // namespace reasampler
} // namespace reasampler::model
@@ -35,7 +35,7 @@
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::model {
// Capture scope, mirrored from render_settings' CaptureScope but kept independent
// here so the pure provenance module does not pull the whole render_settings graph
@@ -146,4 +146,4 @@ std::optional<std::string> detectParent(
const std::vector<std::string>& sourceItemFiles,
const std::vector<BankFileRef>& bankFiles);
} // namespace reasampler
} // namespace reasampler::model
+145
View File
@@ -0,0 +1,145 @@
#include "core/model/slot_map.h"
#include <algorithm>
#include "core/json/json.h"
// slot_map implementation (extracted from bank_book, Q-W1 T4-05).
//
// The invariant: entries_ is kept sorted ascending by slot, one id per slot, one
// slot per id. Every mutator restores it; queries assume it. serialize rides the
// shared core/json emit helpers — the emitted fragment is byte-identical to the
// pre-extraction bank_book writer.
namespace reasampler::model {
void SlotMap::sortBySlot() {
std::stable_sort(entries_.begin(), entries_.end(),
[](const Entry& a, const Entry& b) { return a.slot < b.slot; });
}
int SlotMap::slotOf(const std::string& id) const {
for (const auto& e : entries_)
if (e.id == id) return e.slot;
return -1;
}
std::string SlotMap::idAt(int slot) const {
for (const auto& e : entries_)
if (e.slot == slot) return e.id;
return {};
}
int SlotMap::maxSlot() const {
int m = -1;
for (const auto& e : entries_)
if (e.slot > m) m = e.slot;
return m;
}
std::vector<std::string> SlotMap::orderedIds() const {
// entries_ is sorted ascending by slot, so a straight walk is display order.
std::vector<std::string> out;
out.reserve(entries_.size());
for (const auto& e : entries_) out.push_back(e.id);
return out;
}
void SlotMap::append(const std::string& id) {
if (id.empty()) return;
remove(id); // an existing id is re-appended, not left in place
entries_.push_back(Entry{id, maxSlot() + 1}); // next free slot after the last occupied
sortBySlot();
}
bool SlotMap::remove(const std::string& id) {
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
if (it->id == id) {
entries_.erase(it); // leaves the slot empty — no re-pack
return true;
}
}
return false;
}
bool SlotMap::reorder(const std::string& id, int targetSlot) {
if (slotOf(id) < 0) return false; // not mapped -> no mutation
if (targetSlot < 0) targetSlot = 0;
if (slotOf(id) == targetSlot) return false; // already there — true no-op
// Detach the moving id first so the occupancy test below sees the post-move world.
remove(id);
const bool occupied = !idAt(targetSlot).empty();
if (occupied) {
// Insert-before-and-shift: every occupant at slot >= targetSlot shifts up by one,
// preserving relative order and interior gaps above the target. The moving id then
// takes targetSlot cleanly.
for (auto& e : entries_)
if (e.slot >= targetSlot) ++e.slot;
}
entries_.push_back(Entry{id, targetSlot});
sortBySlot();
return true;
}
void SlotMap::resetDense(const std::vector<std::string>& ids) {
entries_.clear();
int slot = 0;
for (const auto& id : ids) {
if (id.empty()) continue;
if (slotOf(id) >= 0) continue; // skip a duplicate id (one slot per id)
entries_.push_back(Entry{id, slot++});
}
// Already ascending by construction; no sort needed.
}
void SlotMap::reconcile(const std::vector<std::string>& liveIds) {
// Drop markers whose sample left the index.
entries_.erase(
std::remove_if(entries_.begin(), entries_.end(),
[&](const Entry& e) {
return std::find(liveIds.begin(), liveIds.end(), e.id) ==
liveIds.end();
}),
entries_.end());
// Append live ids that have no mapping yet (out-of-band index growth), in liveIds
// order, each to the next free slot after the current frontier.
for (const auto& id : liveIds)
if (slotOf(id) < 0) append(id);
sortBySlot();
}
bool SlotMap::operator==(const SlotMap& o) const {
return entries_ == o.entries_;
}
SlotMap SlotMap::fromEntries(const std::vector<std::pair<std::string, int>>& pairs) {
SlotMap m;
for (const auto& [id, slot] : pairs) {
if (id.empty() || slot < 0) continue; // drop malformed pair
if (m.slotOf(id) >= 0) continue; // duplicate id: first wins
if (!m.idAt(slot).empty()) continue; // slot taken: never double-occupy
m.entries_.push_back(Entry{id, slot});
}
m.sortBySlot();
return m;
}
std::string SlotMap::serialize() const {
// Array of {id, slot} objects in ascending slot order (entries_ is kept sorted).
// json::Writer + numToStr are the same emit path the pre-extraction writer used,
// so the fragment is byte-identical.
std::string out;
out += '[';
for (std::size_t i = 0; i < entries_.size(); ++i) {
if (i) out += ',';
json::Writer e(out);
e.keyStr("id", entries_[i].id);
e.keyRaw("slot", json::numToStr(entries_[i].slot));
}
out += ']';
return out;
}
} // namespace reasampler::model
+106
View File
@@ -0,0 +1,106 @@
#pragma once
// slot_map — the L7 gap-preserving display-position carrier for ONE bank (F2 settled:
// plain interchangeable slots, NOT M9 fixed/addressable slots). A slot is just a
// display position a sample id occupies; the map is sample id -> slot (>= 0). Gaps
// are first-class: a bank may have a sample at slot 1 with slot 0 empty (an empty
// first row above an occupied second row). At most one id per slot (a slot is never
// double-occupied) and at most one slot per id (an id sits in exactly one place).
//
// Position lives HERE, not on Sample (CLAUDE.md wrapping discipline): a copy of one
// sample into two banks may sit at different slots, so position is a per-bank display
// concern owned by the bank's membership. bank_model / Sample stay untouched.
//
// Extracted from bank_book (Q-W1, T4-05): a self-contained ordered-slot container
// with its own serialize, distinct from the multi-bank registry that carries it.
// Behavior covered by bank_book_tests (the round-trip + reorder/reconcile suites);
// a dedicated slot_map_tests target is a welcome follow-up, not a Q-W1 requirement.
//
// PURE: standard library + core/json (serialize) only.
#include <cstddef>
#include <string>
#include <utility>
#include <vector>
namespace reasampler::model {
class SlotMap {
public:
// The slot an id occupies, or -1 if the id is not mapped. O(N).
int slotOf(const std::string& id) const;
// The id occupying `slot`, or "" if the slot is empty. O(N).
std::string idAt(int slot) const;
// The highest occupied slot, or -1 when the map is empty. Defines the append
// frontier and (with trailing-empty trim) the content extent.
int maxSlot() const;
// Ids in ASCENDING slot order (the deterministic display order). Empty slots
// produce no entry — the caller iterates occupants; sparse layout is a draw
// concern that reads slotOf/idAt, not this list.
std::vector<std::string> orderedIds() const;
// Places `id` at the next free slot after the last occupied one (append). If the
// id is already mapped it is first removed (leaving its old slot empty), then
// appended — an append never fills an earlier gap. No-op guard: empty id ignored.
void append(const std::string& id);
// Drops `id`'s mapping, LEAVING ITS SLOT EMPTY (no re-pack) so every other id
// keeps its position. Returns true if the id was mapped.
bool remove(const std::string& id);
// Moves `id` to `targetSlot`, gap-preserving (F3 reorder semantics):
// * target slot EMPTY -> `id` moves there; its old slot is left empty.
// * target slot OCCUPIED -> insert-before-and-shift: `id` takes targetSlot and
// every occupant at slot >= targetSlot (except `id` itself) shifts up by one,
// preserving their relative order and never colliding. Matches file-manager
// reorder. Interior gaps between shifted occupants are preserved as-is
// (shift is +1 on each occupant, so the gap structure above the target is kept).
// * negative targetSlot is clamped to 0.
// Returns false (no mutation) if `id` is not mapped. Deterministic.
bool reorder(const std::string& id, int targetSlot);
// Rebuilds the map densely from `ids` in the given order (slot i = ids[i]),
// dropping any prior state. The migration path: a pre-L7 bank with no persisted
// slot data is seeded from its BankModel insertion order, densely packed (no gaps),
// so it is visually identical on first post-L7 load. Empty/duplicate ids skipped.
void resetDense(const std::vector<std::string>& ids);
// Drops any mapping whose id is NOT in `liveIds` (a stale marker whose sample left
// the index) and appends any live id that has NO mapping yet (a sample the index
// gained out-of-band). Slots of surviving ids are untouched (gaps preserved). Keeps
// the map consistent with the bank's membership without a re-pack. Deterministic:
// orphan appends follow `liveIds` order.
void reconcile(const std::vector<std::string>& liveIds);
bool empty() const { return entries_.empty(); }
std::size_t size() const { return entries_.size(); }
bool operator==(const SlotMap& o) const;
// JSON fragment (an array of {id, slot} objects, ascending slot). Emitted as the
// bank envelope's "slots" member by BankBook::serialize; parsed back by its parser.
// Round-trips losslessly with the rest of the bank.
std::string serialize() const;
// Builds a map from explicit (id, slot) pairs parsed from persisted JSON. Enforces
// the map invariants defensively against a hand-edited blob: a duplicate id keeps
// its FIRST occurrence; a slot already taken by a kept id drops the later pair
// (never double-occupies); an empty id or negative slot is dropped. The result is
// sorted ascending by slot. reconcile() against live membership runs afterward, so
// a lossy repair here degrades gracefully rather than corrupting lookup.
static SlotMap fromEntries(const std::vector<std::pair<std::string, int>>& pairs);
private:
struct Entry {
std::string id;
int slot = 0;
bool operator==(const Entry& o) const { return id == o.id && slot == o.slot; }
};
std::vector<Entry> entries_; // kept sorted ascending by slot (invariant)
void sortBySlot();
};
} // namespace reasampler::model
+48
View File
@@ -0,0 +1,48 @@
#pragma once
// core/namespaces.h — Q-W1 INTERIM flat-namespace shim for the not-yet-split
// god/shell TUs (bank_panel / actions / persist / ingest / main / view / capture
// shells / the VST editor+processor). The Q-W1 sub-namespaces move every clean pure
// module's symbols out of the flat `reasampler` namespace; the god modules keep their
// pre-split internals, which reference those symbols unqualified (or qualified as
// `reasampler::X`). Nominating every sub-namespace inside `reasampler` restores both
// forms ([namespace.qual]p2 routes qualified lookup through using-directives), so the
// god internals stay untouched until their own split waves.
//
// SCOPE CONTRACT: included ONLY by god/shell TUs pending their split wave
// (Q-W2/Q-W2v/Q-W3/Q-W4/Q-W5). Clean core modules must NOT include this — they
// reference cross-subsystem symbols by their real namespace homes. Each split wave
// drops this include from the TUs it rewrites; when the last split lands, delete
// this header.
namespace reasampler {
namespace model {}
namespace view {}
namespace capture {}
namespace audio {}
namespace ui {}
namespace reclaim {}
namespace version {}
namespace json {}
namespace util {}
namespace wire {}
namespace instrument {
namespace engine {}
namespace map {}
namespace ui {}
} // namespace instrument
using namespace model;
using namespace view;
using namespace capture;
using namespace audio;
using namespace ui;
using namespace reclaim;
using namespace version;
using namespace util;
using namespace wire;
using namespace instrument::engine;
using namespace instrument::map;
using namespace instrument::ui;
} // namespace reasampler
@@ -1,4 +1,4 @@
#include "prune_reconcile.h"
#include "core/reclaim/prune_reconcile.h"
#include <unordered_set>
@@ -7,7 +7,7 @@
// keeping a path iff it is owned AND not referenced. Walking `present` (not owned)
// gives the ∩-present clause for free and yields output in folder-enumeration order.
namespace reasampler {
namespace reasampler::reclaim {
std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
const std::vector<std::string>& referenced,
@@ -83,4 +83,4 @@ std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirm
return plan;
}
} // namespace reasampler
} // namespace reasampler::reclaim
@@ -32,7 +32,7 @@
// -- Path representation: EXACT-STRING match (safety-critical) -----------------
//
// Every path in the model is a project-relative string compared VERBATIM: Sample.
// relativePath, OwnedFileManifest::contains (p == relativePath), and BankIndex all
// relativePath, OwnedFileManifest::contains (p == relativePath), and BankModel all
// use raw std::string equality — no separator normalization, no case-folding, no
// trailing-slash trimming. This core MATCHES that convention exactly: it compares
// the raw strings the shell supplies. Feeding a consistent spelling across the three
@@ -46,7 +46,7 @@
#include <unordered_map>
#include <vector>
namespace reasampler {
namespace reasampler::reclaim {
// The dry-run prune result (Phase R, Wave 2 — report only, no deletion). The thin
// prune shell (persist) fills this from pruneOrphans() + a per-file size stat and hands
@@ -175,4 +175,4 @@ PruneReport buildPruneReport(const std::vector<std::string>& orphans,
std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed,
const std::vector<std::string>& freshOrphans);
} // namespace reasampler
} // namespace reasampler::reclaim
@@ -1,10 +1,10 @@
// action_bar — pure implementation. See action_bar.h. NO REAPER / SWELL / LICE / vendor.
#include "action_bar.h"
#include "core/ui/action_bar.h"
#include <cstddef>
namespace reasampler {
namespace reasampler::ui {
namespace {
@@ -151,4 +151,4 @@ int hitTestActionBar(int px, int py, const ActionBarRect& bar,
return -1; // inter-button/cluster gap or the overflow dead-zone — a clean miss
}
} // namespace reasampler
} // namespace reasampler::ui
+4 -12
View File
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// action_bar — the REAPER-free, LICE-free layout + hit-test math behind the bank_panel's
// TASK-GROUPED toolbars (Phase L, L2 + L4 + L6). L2's dock-panel layout redesign (DS-3: a
// thorough layout, not a re-skin) groups the action-trigger button inventory BY TASK — a compact
@@ -35,7 +36,7 @@
#include <vector>
namespace reasampler {
namespace reasampler::ui {
// The task cluster a button belongs to (the L2 "group by task" mandate). The order here is
// NOT itself the bar order — the caller passes ClusterSpecs in the order it wants; this enum
@@ -58,16 +59,7 @@ enum class ActionCluster {
// The bar the clusters are drawn into, top-left origin (SWELL/LICE convention). (x, y) is the
// top-left corner; width/height are the bar extents. The panel reserves this as a fixed-height
// band (its own judgment where — above the tail footer, below the split body).
struct ActionBarRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const ActionBarRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using ActionBarRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// One visible button's placement within the bar, top-left origin. `index` is the button's
// position in the caller's flat action list (the caller supplies actions in cluster order, so
@@ -156,4 +148,4 @@ std::vector<ActionBarSlot> computeBarSlots(const ActionBarRect& bar,
int hitTestActionBar(int px, int py, const ActionBarRect& bar,
const std::vector<ClusterSpec>& clusters, const ActionBarSpec& spec);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,11 +1,11 @@
// bank_grid — pure implementation. See bank_grid.h. NO REAPER / SWELL / vendor.
#include "bank_grid.h"
#include "core/ui/bank_grid.h"
#include <algorithm>
#include <cmath>
namespace reasampler {
namespace reasampler::ui {
namespace {
@@ -224,4 +224,4 @@ float compressAmplitudeForDisplay(float linear) {
return linear < 0.0f ? -clamped : clamped;
}
} // namespace reasampler
} // namespace reasampler::ui
+4 -12
View File
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// bank_grid — the REAPER-free layout math and cache-key logic behind the docked
// bank_panel (M5, Wave A). The panel shell (bank_panel.cpp) owns the SWELL window,
// LICE drawing, and PCM reads; ALL of that is REAPER-bound and DAW-verified. What
@@ -14,22 +15,13 @@
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::ui {
// A single cell's pixel rectangle within the panel, top-left origin (SWELL/LICE
// convention). (x, y) is the top-left corner; width/height are the cell extents.
// These are the draw bounds for one sample's thumbnail; the panel draws its
// waveform envelope inside this rect (minus any internal padding it applies).
struct CellRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const CellRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using CellRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// Fixed inputs that shape the grid. All in pixels. cellWidth/cellHeight are the
// TARGET cell size; the layout fits as many whole columns as the panel width
@@ -183,4 +175,4 @@ constexpr float kDisplayFloorDb = -60.0f;
// (stays on the midline). Full-scale (|linear| == 1.0f) returns exactly ±1.0f.
float compressAmplitudeForDisplay(float linear);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,8 +1,8 @@
// card_drag — pure implementation. See card_drag.h. NO REAPER / SWELL / LICE / OS / vendor.
#include "card_drag.h"
#include "core/ui/card_drag.h"
namespace reasampler {
namespace reasampler::ui {
namespace {
@@ -93,4 +93,4 @@ int hitTestSlot(int px, int py, const std::vector<SlotCellRect>& rects) {
return -1;
}
} // namespace reasampler
} // namespace reasampler::ui
+4 -4
View File
@@ -30,10 +30,10 @@
#include <vector>
#include "bank_grid.h" // CellRect
#include "drag_out.h" // PanelClientRect, DragState
#include "core/ui/bank_grid.h" // CellRect
#include "core/ui/drag_out.h" // PanelClientRect, DragState
namespace reasampler {
namespace reasampler::ui {
// Which drop region the pointer currently sits over WITHIN the client rect. The shell
// classifies the live pointer against its own region geometry (tab strip / other bank
@@ -144,4 +144,4 @@ std::vector<SlotCellRect> computeSlotRectsForDrop(int maxSlot, int panelWidth,
// callers reason in model slots.
int hitTestSlot(int px, int py, const std::vector<SlotCellRect>& rects);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,11 +1,11 @@
// card_meta — pure implementation. See card_meta.h. NO REAPER / SWELL / LICE / vendor.
#include "card_meta.h"
#include "core/ui/card_meta.h"
#include <cmath>
#include <cstdio>
namespace reasampler {
namespace reasampler::ui {
std::string formatBarsBeats(const MusicalLength& m) {
// No derivable musical read-out without a positive tempo AND a stamped meter.
@@ -61,4 +61,4 @@ std::string formatSecondsMs(double lengthSeconds) {
return buf;
}
} // namespace reasampler
} // namespace reasampler::ui
+2 -2
View File
@@ -11,7 +11,7 @@
#include <string>
namespace reasampler {
namespace reasampler::ui {
// The musical length inputs, taken straight off a Sample (L7 F1 capture-time stamp):
// lengthSeconds — captured length in wall-clock seconds (>= 0).
@@ -52,4 +52,4 @@ std::string formatBarsBeats(const MusicalLength& m);
// * negative length is clamped to "0.000" (a length is never negative; defensive).
std::string formatSecondsMs(double lengthSeconds);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,9 +1,9 @@
// component_geometry — pure implementation. See component_geometry.h. NO REAPER / SWELL /
// LICE / vendor. Standard library only.
#include "component_geometry.h"
#include "core/ui/component_geometry.h"
namespace reasampler {
namespace reasampler::ui {
bool hitTestBox(int px, int py, const KitBox& box) {
if (box.empty()) return false;
@@ -105,4 +105,4 @@ int waveformColumnCount(const KitBox& box) {
return w > 0 ? w : 0;
}
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// component_geometry — the REAPER-free, LICE-free geometry + hit-test math for the shared
// drawing kit's generic components (Phase L, L1): a button box, a slider's track/handle,
// and a list row. These are the kit-level primitives that DON'T already have a pure owner:
@@ -20,23 +21,12 @@
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library
// only. Builds and unit-tests without REAPER. Mirror of mode_switch / prune_button.
namespace reasampler {
namespace reasampler::ui {
// A generic pixel box, top-left origin (SWELL/LICE convention). Shared shape for the kit
// component rects below. A zero-area box (empty()) means "nothing to draw / hit" — the
// same graceful-suppression convention prune_button uses.
struct KitBox {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0; }
bool operator==(const KitBox& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using KitBox = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// True iff (px, py) falls inside `box`, half-open bounds [x, x+width) x [y, y+height) —
// the same discipline as every sibling hit-test so draw and hit-test never double-claim a
@@ -136,4 +126,4 @@ int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCou
// identical whether bins == columns or bins == k*columns) and wastes memory and CPU.
int waveformColumnCount(const KitBox& box);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,10 +1,10 @@
// drag_out — pure implementation. See drag_out.h. NO REAPER / SWELL / OS / vendor.
#include "drag_out.h"
#include "core/ui/drag_out.h"
#include <unordered_set>
namespace reasampler {
namespace reasampler::ui {
namespace {
@@ -51,4 +51,4 @@ PathList assemblePathList(const std::vector<ResolvedSample>& resolved) {
return out;
}
} // namespace reasampler
} // namespace reasampler::ui
+4 -12
View File
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// drag_out — the REAPER-free / OS-free decision logic behind the bank_panel's native OS
// drag-out (Milestone 11, the final polish point). Two pure concerns live here so they are
// unit-tested outside the DAW (CLAUDE.md §load-bearing split); the OLE / SWELL initiation
@@ -29,7 +30,7 @@
#include <string>
#include <vector>
namespace reasampler {
namespace reasampler::ui {
// --- Gesture boundary ---------------------------------------------------------
@@ -37,16 +38,7 @@ namespace reasampler {
// LICE convention). width/height are the extents; a point (px, py) is INSIDE when
// x <= px < x + width and y <= py < y + height (half-open, matching the panel's other
// hit-tests so the edge is claimed consistently).
struct PanelClientRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const PanelClientRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using PanelClientRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// The live drag state the shell tracks, reduced to what the boundary decision needs:
// whether a drag is currently active (threshold crossed) and whether the armed payload
@@ -137,4 +129,4 @@ struct PathList {
// case-insensitive dedup on Windows — the pure layer does not guess a platform rule).
PathList assemblePathList(const std::vector<ResolvedSample>& resolved);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,8 +1,8 @@
// footer_bar — pure implementation. See footer_bar.h. NO REAPER / SWELL / LICE / vendor.
#include "footer_bar.h"
#include "core/ui/footer_bar.h"
namespace reasampler {
namespace reasampler::ui {
namespace {
@@ -66,4 +66,4 @@ FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout) {
return FooterHit::None;
}
} // namespace reasampler
} // namespace reasampler::ui
+9 -19
View File
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// footer_bar — the REAPER-free, LICE-free layout + hit-test math for the bank_panel's L4
// footer LEFT group: the narrowed [Arrange|Design] mode toggle, its compact per-mode count
// label, and the Tail button, laid out left-to-right at the footer's left. The panel shell
@@ -24,32 +25,21 @@
// tiling and hit-test, so mode_switch stays the ONE owner of segment geometry. footer_bar
// decides the toggle's placement + overall width; mode_switch subdivides it.
//
// NAME NOTE (brief §name-collision): ButtonRect / FooterRect / SegmentRect / ActionBarRect /
// KitBox / KitButtonBox are already owned in this namespace; grep-checked FooterBar* / FooterHit
// FREE before minting. FooterRect (prune_button) is the input strip type and is REUSED here
// (same concept — the footer strip); the new output/spec/hit types carry the FooterBar* prefix.
// Naming: the rect-role family (ButtonRect / FooterRect / FooterBarRect / ...) is unified on
// the ONE concrete ui::Rect (core/ui/rect.h, Q-W1 T2-05) — the per-role names are aliases, so
// the former hand-collision bookkeeping is retired. FooterRect (prune_button) remains the
// shared input-strip spelling; this module's output/spec/hit types carry the FooterBar* prefix.
//
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library only.
#include "prune_button.h" // FooterRect — the footer strip input type (shared, not re-minted)
#include "core/ui/prune_button.h" // FooterRect — the footer strip input type (shared, not re-minted)
namespace reasampler {
namespace reasampler::ui {
// One placed affordance's pixel rectangle within the footer, top-left origin. A zero-area rect
// (empty()) means "not placed" (the footer was too narrow to host it after the ones before it),
// so the shell draws/hit-tests nothing for it — graceful degradation, mirroring prune_button.
struct FooterBarRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0; }
bool operator==(const FooterBarRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using FooterBarRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// The laid-out footer LEFT group: the mode toggle box, the count label box, and the Tail
// button box, in left-to-right order. Any box may be empty (suppressed) when the footer is
@@ -113,4 +103,4 @@ FooterBarLayout computeFooterBar(const FooterRect& footer, const FooterBarSpec&
// mode_switch over the toggle box), then the Tail hit; this returns which region was struck.
FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,10 +1,10 @@
// mode_enable — pure implementation. See mode_enable.h. NO REAPER / SWELL / LICE / vendor.
#include "mode_enable.h"
#include "core/ui/mode_enable.h"
#include "view_mode_model.h" // kArrangeModeId / kDesignModeId — the ONE home for the mode ids
#include "core/view/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ONE home for the mode ids
namespace reasampler {
namespace reasampler::ui {
bool tagButtonEnabled(const std::string& activeModeId, TagTarget target) {
// The target's own mode id, so the rule is a single "target != active" compare.
@@ -18,4 +18,4 @@ bool tagButtonEnabled(const std::string& activeModeId, TagTarget target) {
return activeModeId != targetId;
}
} // namespace reasampler
} // namespace reasampler::ui
@@ -17,7 +17,7 @@
#include <string>
namespace reasampler {
namespace reasampler::ui {
// A tag button's TARGET mode — the mode it sends the selection to when fired. Arrange = the
// untagged default (returning the selection to Arrange), Design = tagged into the Design mode.
@@ -36,4 +36,4 @@ enum class TagTarget {
// disable an action the user can still reach), so a future added mode never dead-locks the bar.
bool tagButtonEnabled(const std::string& activeModeId, TagTarget target);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,8 +1,8 @@
// overflow_menu — pure implementation. See overflow_menu.h. NO REAPER / SWELL / LICE / vendor.
#include "overflow_menu.h"
#include "core/ui/overflow_menu.h"
namespace reasampler {
namespace reasampler::ui {
int menuButtonReserve(const MenuBarRect& bar, const MenuButtonSpec& spec) {
if (bar.width <= 0 || bar.height <= 0 || spec.buttonWidth <= 0) return 0;
@@ -39,4 +39,4 @@ bool hitTestMenuButton(int px, int py, const MenuButtonRect& button) {
py >= button.y && py < button.y + button.height;
}
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// overflow_menu — the REAPER-free layout math behind the bank_panel TOP toolbar's "⋯ / More"
// overflow-menu button (Phase L, L5, refinement 1). The rare capture variants (Batch Items /
// Batch Razor / Capture RT) move OFF the always-visible top bar into a popup opened by a small
@@ -15,39 +16,19 @@
// Mirror of prune_button / mode_switch. The bar rect type it consumes mirrors action_bar's
// ActionBarRect shape but is named distinctly to avoid coupling the two modules.
namespace reasampler {
namespace reasampler::ui {
// The toolbar band the button is drawn into, top-left origin (SWELL/LICE convention). The
// shell derives this from topToolbarRect(). A distinct type from action_bar::ActionBarRect so
// this module stands alone (same shape; deliberate — the two modules are not coupled).
struct MenuBarRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const MenuBarRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using MenuBarRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// The More button's pixel rectangle within the band, top-left origin. A zero-area rect
// (width <= 0 or height <= 0) means "no button" — the band is degenerate or too narrow to
// place the button clear of its left inset; the caller must not draw or hit-test it. The
// three variants stay reachable via their bindable commands, so a suppressed button is
// graceful, not a lost affordance.
struct MenuButtonRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0; }
bool operator==(const MenuButtonRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using MenuButtonRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// Layout inputs for the More button, in pixels. Defaults match the bank_panel top-toolbar
// metrics; the shell passes its own so draw and hit-test share one source of truth.
@@ -84,4 +65,4 @@ MenuButtonRect computeMenuButton(const MenuBarRect& bar, const MenuButtonSpec& s
// hit-test agree on the same pixels. An empty button never claims a point (always false).
bool hitTestMenuButton(int px, int py, const MenuButtonRect& button);
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,11 +1,11 @@
#include "prune_button.h"
#include "core/ui/prune_button.h"
// prune_button implementation — right-anchored button placement in the footer strip,
// with a left-collision suppression rule. Trivially auditable arithmetic; the safety
// property (a suppressed/empty button never claims a click) is a pure predicate tested
// outside the DAW.
namespace reasampler {
namespace reasampler::ui {
ButtonRect computePruneButton(const FooterRect& footer, const PruneButtonSpec& spec) {
if (footer.width <= 0 || footer.height <= 0) return ButtonRect{}; // degenerate footer
@@ -36,4 +36,4 @@ bool hitTestPruneButton(int px, int py, const ButtonRect& button) {
py >= button.y && py < button.y + button.height;
}
} // namespace reasampler
} // namespace reasampler::ui
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// prune_button — the REAPER-free layout math behind the bank_panel's Prune button
// (Phase R, Wave 3 — R3, fork R-E). A single labelled button drawn in the panel's
// tail-footer strip that fires the "Prune bank folder" command. The panel shell
@@ -24,37 +25,17 @@
// is always reachable via its bindable command, so a hidden button is a graceful
// degradation, not a lost affordance.
namespace reasampler {
namespace reasampler::ui {
// The footer strip the button is drawn into, top-left origin (SWELL/LICE
// convention). (x, y) is the top-left corner; width/height are the strip extents.
// bank_panel derives this from panelFooter() and passes it here.
struct FooterRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const FooterRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using FooterRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// A button's pixel rectangle within the footer, top-left origin. A zero-area rect
// (width <= 0 or height <= 0) means "no button" — the footer is too narrow to place
// it, or the footer itself is degenerate; the caller must not draw or hit-test it.
struct ButtonRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0; }
bool operator==(const ButtonRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using ButtonRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// Layout inputs for the prune button, in pixels. Defaults match the bank_panel footer
// metrics; the shell passes its own so draw and hit-test share one source of truth.
@@ -98,4 +79,4 @@ ButtonRect computePruneButton(const FooterRect& footer, const PruneButtonSpec& s
// so a suppressed button cannot be accidentally clicked.
bool hitTestPruneButton(int px, int py, const ButtonRect& button);
} // namespace reasampler
} // namespace reasampler::ui
+56
View File
@@ -0,0 +1,56 @@
#pragma once
// rect.h — the ONE concrete pixel rectangle (Q-W1, T2-05 ≡ T4-21).
//
// Before Q-W1 the codebase carried 12+ byte-identical {x, y, width, height} structs
// (ButtonRect / FooterRect / CellRect / KitBox / ...) plus a second LTRB grammar on
// the VST side (editor_geometry's left/top/right/bottom Rect). This is the single
// owner: one CONCRETE type (deliberately NOT a template — the role types differed in
// name only, so a template would model nothing), with per-role aliases at the old
// definition sites so call sites keep their semantic names
// (`using ButtonRect = ui::Rect;`).
//
// Grammar: XYWH storage (the majority grammar — every extension role struct), with
// right()/bottom() accessors and an ltrb() factory so the former LTRB call sites
// convert mechanically. Half-open on both axes: a rect covers
// [x, x+width) × [y, y+height) — the same convention LICE/SWELL RECTs use, and the
// one every hitTest* in the codebase already implements.
//
// PURE MODULE: standard library only. Header-only; behavior is covered by the role
// modules' own test executables (prune_button / footer_bar / bank_grid / ... and the
// instrument-ui suites), which exercise every alias against these semantics.
namespace reasampler::ui {
struct Rect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
// Exclusive edges (half-open convention).
int right() const { return x + width; }
int bottom() const { return y + height; }
// A zero-or-negative-area rect means "not placed / suppressed": the caller must
// not draw or hit-test it (the shared graceful-degradation contract).
bool empty() const { return width <= 0 || height <= 0; }
// The former LTRB grammar's constructor (editor_geometry and friends): edges in,
// extents stored. right/bottom exclusive, matching right()/bottom().
static Rect ltrb(int left, int top, int right, int bottom) {
return Rect{left, top, right - left, bottom - top};
}
bool operator==(const Rect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
bool operator!=(const Rect& o) const { return !(*this == o); }
};
// True iff (px, py) falls inside r under the half-open convention. An empty rect
// contains nothing, so a suppressed affordance can never claim a click.
inline bool contains(const Rect& r, int px, int py) {
return px >= r.x && px < r.x + r.width && py >= r.y && py < r.y + r.height;
}
} // namespace reasampler::ui
@@ -1,10 +1,10 @@
// tab_strip — pure implementation. See tab_strip.h. NO REAPER / SWELL / vendor.
#include "tab_strip.h"
#include "core/ui/tab_strip.h"
#include <cstddef>
namespace reasampler {
namespace reasampler::ui {
TabStripLayout computeTabStripLayout(const TabStripRect& strip, int tabCount,
const TabStripSpec& spec, int scrollOffset) {
@@ -109,4 +109,4 @@ TabHit hitTestTabStrip(int px, int py, const TabStripRect& strip, int tabCount,
return miss; // track dead space (no tab under the point)
}
} // namespace reasampler
} // namespace reasampler::ui
+4 -12
View File
@@ -1,4 +1,5 @@
#pragma once
#include "core/ui/rect.h"
// tab_strip — the REAPER-free layout + hit-test math behind the bank_panel's
// named-banks tab strip (Phase B, Wave 4 — B4). The named-banks region of the
// vertical-split bank window is a LICE-drawn tab strip (one tab per named bank,
@@ -16,21 +17,12 @@
#include <vector>
namespace reasampler {
namespace reasampler::ui {
// The strip the tabs are drawn into, top-left origin (SWELL/LICE convention).
// (x, y) is the top-left corner; width/height are the strip extents. The panel
// reserves this as a fixed-height band at the top of the named-banks region.
struct TabStripRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
bool operator==(const TabStripRect& o) const {
return x == o.x && y == o.y && width == o.width && height == o.height;
}
};
using TabStripRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased
// Fixed inputs that shape the strip. tabWidth is the pixel width of each tab (fixed
// so the strip reads as a uniform segmented control and overflow math stays simple —
@@ -132,4 +124,4 @@ struct TabHit {
TabHit hitTestTabStrip(int px, int py, const TabStripRect& strip, int tabCount,
const TabStripSpec& spec, int scrollOffset);
} // namespace reasampler
} // namespace reasampler::ui
+3 -3
View File
@@ -1,11 +1,11 @@
// theme — pure implementation. See theme.h. NO REAPER / SWELL / LICE / vendor.
#include "theme.h"
#include "core/ui/theme.h"
#include <algorithm>
#include <cmath>
namespace reasampler {
namespace reasampler::ui {
namespace {
@@ -190,4 +190,4 @@ double textFloor(TextClass cls) {
return cls == TextClass::Body ? 4.5 : 3.0;
}
} // namespace reasampler
} // namespace reasampler::ui

Some files were not shown because too many files have changed in this diff Show More