diff --git a/CMakeLists.txt b/CMakeLists.txt index 12c69f8..09e9d7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -82,18 +82,40 @@ set(SDK_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/reaper-sdk/sdk) set(WDL_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/WDL/WDL) set(SWELL ${WDL_INC}/swell) +# --------------------------------------------------------------------------- +# 0) core/ — the Q-W1 shared pure substrate. NO REAPER, NO SWELL, NO VST3. +# json: the ONE JSON lexical layer (reader + writer) behind the five +# persisted-blob (de)serializers (bank_model / bank_book / +# view_mode_model / owned_manifest / tail_control). T2-02 / §2. +# wire: the ONE length-prefixed ext-state wire codec (putField + Cursor + +# the guarded decimal accumulate) behind provenance / +# assignment_request / sample_usage / bank_sync. T2-01(b). +# file_bytes: the ONE whole-file byte loader both artifacts link. T2-03. +# Headers are included as "core/json/json.h" etc. (rooted at src/), so the +# include paths survive the Q-W1 part-2 directory relocation unchanged. +# --------------------------------------------------------------------------- +add_library(json STATIC src/core/json/json.cpp) +target_include_directories(json PUBLIC src) + +add_library(wire STATIC src/core/wire/wire.cpp) +target_include_directories(wire PUBLIC src) + +add_library(file_bytes STATIC src/core/util/file_bytes.cpp) +target_include_directories(file_bytes PUBLIC src) + # --------------------------------------------------------------------------- # 1) Pure model library — NO REAPER, NO SWELL. Builds & tests anywhere. # The sampler's heart: Sample metadata + BankIndex (Milestone 1). # --------------------------------------------------------------------------- -add_library(bank_model STATIC src/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) # --------------------------------------------------------------------------- # 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) # --------------------------------------------------------------------------- @@ -101,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) # --------------------------------------------------------------------------- @@ -110,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) # --------------------------------------------------------------------------- @@ -120,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) # --------------------------------------------------------------------------- @@ -131,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) # --------------------------------------------------------------------------- @@ -140,8 +162,9 @@ 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 # durable-key convention in lane_keys (laneNameForMode), so the model depends on that # pure sibling. PUBLIC so every consumer (tests + module) resolves the symbol. @@ -154,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) @@ -166,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) # --------------------------------------------------------------------------- @@ -177,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) # --------------------------------------------------------------------------- @@ -186,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) # --------------------------------------------------------------------------- @@ -197,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) @@ -210,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) # --------------------------------------------------------------------------- @@ -221,9 +244,20 @@ 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) + +# --------------------------------------------------------------------------- +# 2g''') Pure slot_map library — NO REAPER, NO SWELL. The L7 gap-preserving +# display-position carrier for one bank (extracted from bank_book, Q-W1 +# T4-05): sample id -> slot, gap-preserving append/remove/reorder/reconcile, +# JSON round-trip. Mirror of bank_model; wrapped (not merged) by bank_book. +# --------------------------------------------------------------------------- +add_library(slot_map STATIC src/core/model/slot_map.cpp) +target_include_directories(slot_map PUBLIC src) +target_link_libraries(slot_map PRIVATE json) # --------------------------------------------------------------------------- # 2g') Pure bank_book library — NO REAPER, NO SWELL. The multi-bank phase heart @@ -233,9 +267,11 @@ target_link_libraries(tail_control PUBLIC render_settings) # 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) target_include_directories(bank_book PUBLIC src) target_link_libraries(bank_book PUBLIC bank_model) +target_link_libraries(bank_book PUBLIC slot_map) +target_link_libraries(bank_book PRIVATE json) # --------------------------------------------------------------------------- # 2g'') Pure owned_manifest library — NO REAPER, NO SWELL. The owned-file manifest @@ -246,8 +282,9 @@ target_link_libraries(bank_book PUBLIC bank_model) # 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) # --------------------------------------------------------------------------- # 2g''') Pure prune_reconcile library — NO REAPER, NO SWELL, NO filesystem. The @@ -258,7 +295,7 @@ target_include_directories(owned_manifest PUBLIC src) # 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) # --------------------------------------------------------------------------- @@ -269,7 +306,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) # --------------------------------------------------------------------------- @@ -280,7 +317,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) @@ -294,7 +331,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) @@ -308,7 +345,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) # --------------------------------------------------------------------------- @@ -321,8 +358,9 @@ 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) # --------------------------------------------------------------------------- # 2j') Pure assignment_request library — NO REAPER, NO SWELL, NO VST3. The S8 ingest @@ -334,8 +372,9 @@ target_include_directories(provenance PUBLIC src) # 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) # --------------------------------------------------------------------------- # 2j'') Pure sample_usage library — NO REAPER, NO SWELL, NO VST3. The pS-usage seam: @@ -347,8 +386,9 @@ target_include_directories(assignment_request PUBLIC src) # 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) # --------------------------------------------------------------------------- # 2l) Pure drag_out library — NO REAPER, NO SWELL, NO OS/OLE. The Milestone 11 @@ -361,7 +401,7 @@ target_include_directories(sample_usage PUBLIC src) # 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) # --------------------------------------------------------------------------- @@ -375,13 +415,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/core/wire/reasampler_uid.h, SDK-free), channel-selected via the generated # version header — hence the generated include dir. The round-trip test parses # the container and decodes back through the instrument's own reader. Mirror of # assignment_request. # --------------------------------------------------------------------------- -add_library(instrument_drop STATIC src/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) # --------------------------------------------------------------------------- @@ -394,7 +434,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) # --------------------------------------------------------------------------- @@ -407,7 +447,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) # --------------------------------------------------------------------------- @@ -422,7 +462,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) # --------------------------------------------------------------------------- @@ -436,7 +476,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) @@ -449,7 +489,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) # --------------------------------------------------------------------------- @@ -461,7 +501,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) @@ -473,7 +513,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) # --------------------------------------------------------------------------- @@ -484,7 +524,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) # --------------------------------------------------------------------------- @@ -497,7 +537,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) @@ -518,8 +558,8 @@ target_link_libraries(card_drag PUBLIC drag_out bank_grid) # because that header drags (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 @@ -527,17 +567,31 @@ 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) # --------------------------------------------------------------------------- # 3) Standalone tests for the pure modules (run without launching REAPER). # --------------------------------------------------------------------------- enable_testing() + +# core/ (Q-W1): the shared JSON lexical layer, wire codec, and file loader. +add_executable(json_tests tests/test_json.cpp) +target_link_libraries(json_tests PRIVATE json) +add_test(NAME json_tests COMMAND json_tests) + +add_executable(wire_tests tests/test_wire.cpp) +target_link_libraries(wire_tests PRIVATE wire) +add_test(NAME wire_tests COMMAND wire_tests) + +add_executable(file_bytes_tests tests/test_file_bytes.cpp) +target_link_libraries(file_bytes_tests PRIVATE file_bytes) +add_test(NAME file_bytes_tests COMMAND file_bytes_tests) + add_executable(bank_model_tests tests/test_bank_model.cpp) target_link_libraries(bank_model_tests PRIVATE bank_model) add_test(NAME bank_model_tests COMMAND bank_model_tests) @@ -602,6 +656,10 @@ add_executable(bank_book_tests tests/test_bank_book.cpp) target_link_libraries(bank_book_tests PRIVATE bank_book) add_test(NAME bank_book_tests COMMAND bank_book_tests) +add_executable(slot_map_tests tests/test_slot_map.cpp) +target_link_libraries(slot_map_tests PRIVATE slot_map json) +add_test(NAME slot_map_tests COMMAND slot_map_tests) + add_executable(wav_trim_tests tests/test_wav_trim.cpp) target_link_libraries(wav_trim_tests PRIVATE wav_trim) add_test(NAME wav_trim_tests COMMAND wav_trim_tests) @@ -628,7 +686,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 @@ -645,20 +703,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) @@ -757,19 +815,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 @@ -780,8 +838,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) @@ -791,16 +849,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 @@ -808,8 +866,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 @@ -819,24 +877,25 @@ 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) # browser_scroll (Phase S12) — PURE scroll-window + scrollbar-thumb + type-to-filter-search # geometry LAYERED over the S10 capture_browser: the visible-card window, thumb rect + # thumb-drag<->offset mapping, and the name-substring filter that composes with the bank # filter. The mirror of capture_browser; links capture_browser (for BrowserLayout + the card # metrics/cell rect) which pulls editor_geometry transitively. NEITHER SDK. -add_library(browser_scroll STATIC src/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 @@ -844,16 +903,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 . 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 @@ -861,16 +920,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: @@ -878,23 +937,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) @@ -1004,42 +1063,42 @@ 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 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_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}) # OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or # "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels' @@ -1142,16 +1201,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 @@ -1201,10 +1260,13 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp") sample_map capture_paths embed_strip app_version capture_browser keyboard_strip waveform_view bank_sync browser_scroll note_entry param_slider theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit - knob_deck curve_popup master_gain sample_usage) + 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 diff --git a/src/actions.cpp b/src/actions.cpp index 185afea..6f025b3 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -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 #include -#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; diff --git a/src/actions.h b/src/actions.h index e9d9bfe..936b381 100644 --- a/src/actions.h +++ b/src/actions.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // 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 // a mode, tag/untag/show-both the current track selection. Each action mutates the diff --git a/src/main.cpp b/src/app/main.cpp similarity index 99% rename from src/main.cpp rename to src/app/main.cpp index 668eba4..3c5aafb 100644 --- a/src/main.cpp +++ b/src/app/main.cpp @@ -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 #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 // 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 diff --git a/src/assignment_request.cpp b/src/assignment_request.cpp deleted file mode 100644 index 077aeee..0000000 --- a/src/assignment_request.cpp +++ /dev/null @@ -1,147 +0,0 @@ -// assignment_request.cpp — see assignment_request.h. Pure: standard library only. - -#include "assignment_request.h" - -#include -#include - -namespace reasampler { - -namespace { - -constexpr const char* kMagic = "rsassign1"; - -// Append one length-prefixed field: ':' . Mirror of -// provenance's putField so the two seams share one wire idiom. -void putField(std::string& out, const std::string& field) { - out += std::to_string(field.size()); - out += ':'; - out += field; -} - -// Cursor over the encoded string. All reads are bounds-checked; a short read fails -// the whole parse (ok_ latches false). Mirror of provenance's Cursor, trimmed to the -// three field kinds this record needs. -class Cursor { -public: - explicit Cursor(const std::string& s) : s_(s) {} - - bool ok() const { return ok_; } - bool atEnd() const { return pos_ >= s_.size(); } - - // Reads one length-prefixed field into `out`. Fails on a missing ':', an empty or - // non-numeric length, a length that overflows SIZE_MAX, or a length that runs past - // the end. The digit count is capped at 20 (the decimal width of SIZE_MAX on a - // 64-bit host) so a crafted 200-digit length cannot accumulate past SIZE_MAX via - // repeated multiply. "never UB" promise from the header is upheld here. - bool field(std::string& out) { - if (!ok_) return false; - const std::size_t colon = s_.find(':', pos_); - if (colon == std::string::npos) return fail(); - if (colon == pos_) return fail(); // empty length token - // Cap: SIZE_MAX fits in at most 20 decimal digits; a longer run is bogus. - if (colon - pos_ > 20u) return fail(); - std::size_t len = 0; - for (std::size_t i = pos_; i < colon; ++i) { - const char c = s_[i]; - if (c < '0' || c > '9') return fail(); - const std::size_t digit = static_cast(c - '0'); - // Overflow guard: if len would exceed SIZE_MAX after multiply+add, fail. - if (len > (std::numeric_limits::max() - digit) / 10u) - return fail(); - len = len * 10u + digit; - } - const std::size_t start = colon + 1; - // Guard: start may equal s_.size() (empty remainder), in which case only len==0 - // is valid; start > s_.size() cannot happen (colon < s_.size() by find()). - // Use subtraction-first form to avoid start+len wrapping on a huge len. - if (start > s_.size() || len > s_.size() - start) return fail(); - out.assign(s_, start, len); - pos_ = start + len; - return true; - } - - // Reads a length-prefixed field and parses it as a signed 64-bit decimal (an - // optional leading '-'). Fails on empty, non-digit, trailing bytes, or a value - // that would overflow INT64_MAX / underflow INT64_MIN. The digit count is capped - // at 19 (the decimal width of INT64_MAX, plus 1 for the optional sign = 20 - // characters maximum) so a crafted 21-digit field cannot accumulate UB. "never UB" - // promise from the header is upheld: all arithmetic is done on positive digits - // and capped before applying the sign. - bool fieldInt64(std::int64_t& out) { - std::string f; - if (!field(f)) return false; - if (f.empty()) return fail(); - std::size_t i = 0; - bool neg = false; - if (f[0] == '-') { - neg = true; - i = 1; - if (f.size() == 1) return fail(); // bare "-" - } - // Cap at 19 digits (INT64_MAX = 9223372036854775807 — 19 digits). A 20-digit - // positive value would overflow INT64_MAX; a 20-digit negative might be valid - // (INT64_MIN = -9223372036854775808) but we conservatively reject it too: the - // generation field is a unix timestamp, never near INT64 limits in practice. - if (f.size() - i > 19u) return fail(); - std::int64_t v = 0; - for (; i < f.size(); ++i) { - const char c = f[i]; - if (c < '0' || c > '9') return fail(); - const std::int64_t digit = static_cast(c - '0'); - // Overflow guard: v * 10 + digit must not exceed INT64_MAX. - if (v > (std::numeric_limits::max() - digit) / 10) - return fail(); - v = v * 10 + digit; - } - out = neg ? -v : v; - return true; - } - - // Consumes an exact literal at the cursor (the magic tag). Fails if absent. - bool literal(const char* lit) { - if (!ok_) return false; - std::size_t i = 0; - for (; lit[i] != '\0'; ++i) { - if (pos_ + i >= s_.size() || s_[pos_ + i] != lit[i]) return fail(); - } - pos_ += i; - return true; - } - -private: - bool fail() { - ok_ = false; - return false; - } - - const std::string& s_; - std::size_t pos_ = 0; - bool ok_ = true; -}; - -} // namespace - -std::string encodeAssignmentRequest(const AssignmentRequest& req) { - std::string out = kMagic; - putField(out, req.bankId); - putField(out, req.sampleId); - putField(out, std::to_string(req.generation)); - return out; -} - -std::optional decodeAssignmentRequest(const std::string& wire) { - Cursor cur(wire); - if (!cur.literal(kMagic)) return std::nullopt; - - AssignmentRequest req; - if (!cur.field(req.bankId)) return std::nullopt; - if (!cur.field(req.sampleId)) return std::nullopt; - if (!cur.fieldInt64(req.generation)) return std::nullopt; - - // Reject trailing garbage: a well-formed value ends exactly at the last field. - if (!cur.ok() || !cur.atEnd()) return std::nullopt; - return req; -} - -} // namespace reasampler diff --git a/src/bank_model.cpp b/src/bank_model.cpp deleted file mode 100644 index a5286ec..0000000 --- a/src/bank_model.cpp +++ /dev/null @@ -1,767 +0,0 @@ -#include "bank_model.h" - -#include -#include -#include -#include -#include - -// bank_model implementation. -// -// JSON is hand-rolled and self-contained (brief: keep the pure core -// dependency-free — no third-party JSON lib, no WDL coupling). The field set is -// a flat struct of primitives, strings, one enum, a small string array, and a -// few optionals, so a compact writer + recursive-descent parser is the simplest -// thing that works. Doubles are emitted with 17 significant digits (%.17g), the -// shortest form that round-trips every IEEE-754 double exactly, so the -// deserialize(serialize(x)) == x invariant holds bit-for-bit. - -namespace reasampler { - -// --------------------------------------------------------------------------- -// equality -// --------------------------------------------------------------------------- - -bool SourceRange::operator==(const SourceRange& o) const { - return startSeconds == o.startSeconds && endSeconds == o.endSeconds && - startPpq == o.startPpq && endPpq == o.endPpq; -} - -bool Provenance::operator==(const Provenance& o) const { - return parentSampleId == o.parentSampleId && fxChainSnapshot == o.fxChainSnapshot; -} - -bool Levels::operator==(const Levels& o) const { - return peakDb == o.peakDb && rmsDb == o.rmsDb && lufs == o.lufs; -} - -bool LoopPoints::operator==(const LoopPoints& o) const { - return start == o.start && end == o.end; -} - -bool Sample::operator==(const Sample& o) const { - return id == o.id && displayName == o.displayName && relativePath == o.relativePath && - sourceMode == o.sourceMode && sourceRange == o.sourceRange && - trackGuids == o.trackGuids && wetDry == o.wetDry && - channelCount == o.channelCount && sampleRate == o.sampleRate && - lengthSeconds == o.lengthSeconds && lengthBeats == o.lengthBeats && - captureTempo == o.captureTempo && - captureTimeSigNum == o.captureTimeSigNum && - captureTimeSigDenom == o.captureTimeSigDenom && key == o.key && - rootNote == o.rootNote && loop == o.loop && levels == o.levels && - clipped == o.clipped && tier == o.tier && contentHash == o.contentHash && - provenance == o.provenance && createdTimestamp == o.createdTimestamp; -} - -// --------------------------------------------------------------------------- -// path invariant -// --------------------------------------------------------------------------- - -// DECISION: reject absolute paths rather than normalize them. The pure model has -// no knowledge of the project root, so it cannot correctly relativize an absolute -// path — any "normalization" would be a guess that could point at the wrong file. -// Rejecting at the boundary is honest and deterministic; the capture backend (M3) -// is responsible for handing us an already-relative path. Covers POSIX ("/x"), -// Windows drive ("C:\x", "C:/x", "C:foo" drive-relative), and UNC ("\\host\share") -// forms. Any leading : is rejected regardless of the character that follows — -// drive-relative paths ("C:foo.wav") resolve against the drive's current directory, -// not the project root, so they violate the relative-paths-only invariant just as -// much as "C:\foo.wav" does. -static bool isAbsolutePath(const std::string& p) { - if (p.empty()) return false; - if (p[0] == '/' || p[0] == '\\') return true; // POSIX root or UNC - if (p.size() >= 2 && std::isalpha(static_cast(p[0])) && p[1] == ':') - return true; // Windows drive (C:\, C:/, C:foo, C:) - return false; -} - -// --------------------------------------------------------------------------- -// BankIndex -// --------------------------------------------------------------------------- - -AddResult BankIndex::add(const Sample& sample) { - if (sample.id.empty()) return AddResult::RejectedEmptyId; - if (isAbsolutePath(sample.relativePath)) return AddResult::RejectedAbsolutePath; - - if (findByHash(sample.contentHash) != nullptr) - return AddResult::Collapsed; - - samples_.push_back(sample); - return AddResult::Added; -} - -bool BankIndex::remove(const std::string& id) { - for (auto it = samples_.begin(); it != samples_.end(); ++it) { - if (it->id == id) { - samples_.erase(it); - return true; - } - } - return false; -} - -bool BankIndex::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) { - s = updated; // replace in place — position (insertion order) preserved - return true; - } - } - return false; -} - -const Sample* BankIndex::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 { - 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) { - for (auto& s : samples_) { - if (s.id == id) { - s.tier = tier; - return true; - } - } - return false; -} - -std::vector BankIndex::byTier(Tier tier) const { - std::vector out; - for (const auto& s : samples_) - if (s.tier == tier) out.push_back(s); - return out; -} - -// --------------------------------------------------------------------------- -// JSON writer -// --------------------------------------------------------------------------- - -namespace { - -void writeEscaped(std::string& out, const std::string& s) { - out += '"'; - for (char c : s) { - switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\b': out += "\\b"; break; - case '\f': out += "\\f"; break; - case '\n': out += "\\n"; break; - case '\r': out += "\\r"; break; - case '\t': out += "\\t"; break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); - out += buf; - } else { - out += c; - } - } - } - out += '"'; -} - -std::string numToStr(double v) { - char buf[32]; - std::snprintf(buf, sizeof(buf), "%.17g", v); - return buf; -} - -std::string numToStr(std::int64_t v) { - char buf[32]; - std::snprintf(buf, sizeof(buf), "%lld", static_cast(v)); - return buf; -} - -std::string numToStr(int v) { return numToStr(static_cast(v)); } - -class ObjWriter { -public: - explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; } - ~ObjWriter() { out_ += '}'; } - - void keyRaw(const char* key, const std::string& rawValue) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - out_ += rawValue; - } - void keyStr(const char* key, const std::string& value) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - writeEscaped(out_, value); - } - // Begin a nested value; caller writes the value immediately after. - void keyBegin(const char* key) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - } - -private: - void sep() { - if (first_) first_ = false; else out_ += ','; - } - std::string& out_; - bool first_ = true; -}; - -void writeStringArray(std::string& out, const std::vector& v) { - out += '['; - for (std::size_t i = 0; i < v.size(); ++i) { - if (i) out += ','; - writeEscaped(out, v[i]); - } - out += ']'; -} - -void writeSample(std::string& out, const Sample& s) { - ObjWriter w(out); - w.keyStr("id", s.id); - w.keyStr("displayName", s.displayName); - w.keyStr("relativePath", s.relativePath); - w.keyRaw("sourceMode", numToStr(static_cast(s.sourceMode))); - - w.keyBegin("sourceRange"); - { - ObjWriter r(out); - r.keyRaw("startSeconds", numToStr(s.sourceRange.startSeconds)); - r.keyRaw("endSeconds", numToStr(s.sourceRange.endSeconds)); - r.keyRaw("startPpq", numToStr(s.sourceRange.startPpq)); - r.keyRaw("endPpq", numToStr(s.sourceRange.endPpq)); - } - - w.keyBegin("trackGuids"); - writeStringArray(out, s.trackGuids); - - w.keyRaw("wetDry", numToStr(s.wetDry)); - w.keyRaw("channelCount", numToStr(s.channelCount)); - w.keyRaw("sampleRate", numToStr(s.sampleRate)); - w.keyRaw("lengthSeconds", numToStr(s.lengthSeconds)); - w.keyRaw("lengthBeats", numToStr(s.lengthBeats)); - w.keyRaw("captureTempo", numToStr(s.captureTempo)); - w.keyRaw("captureTimeSigNum", numToStr(s.captureTimeSigNum)); - w.keyRaw("captureTimeSigDenom", numToStr(s.captureTimeSigDenom)); - - // Optionals are emitted as null when absent so present/absent round-trips. - w.keyBegin("key"); - if (s.key) writeEscaped(out, *s.key); else out += "null"; - - // Phase S seam fields (D-B). Emitted as null when absent (same shape as `key` - // and `provenance`) so pre-Phase-S JSON — which lacks these keys entirely — - // parses to empty optionals and re-serializes without invention. - w.keyBegin("rootNote"); - if (s.rootNote) out += numToStr(*s.rootNote); else out += "null"; - - w.keyBegin("loop"); - if (s.loop) { - ObjWriter lp(out); - lp.keyRaw("start", numToStr(s.loop->start)); - lp.keyRaw("end", numToStr(s.loop->end)); - } else { - out += "null"; - } - - w.keyBegin("levels"); - { - ObjWriter l(out); - l.keyRaw("peakDb", numToStr(s.levels.peakDb)); - l.keyRaw("rmsDb", numToStr(s.levels.rmsDb)); - l.keyRaw("lufs", numToStr(s.levels.lufs)); - } - - w.keyRaw("clipped", s.clipped ? "true" : "false"); - w.keyRaw("tier", numToStr(static_cast(s.tier))); - w.keyStr("contentHash", s.contentHash); - - w.keyBegin("provenance"); - if (s.provenance) { - ObjWriter p(out); - p.keyStr("parentSampleId", s.provenance->parentSampleId); - p.keyStr("fxChainSnapshot", s.provenance->fxChainSnapshot); - } else { - out += "null"; - } - - w.keyRaw("createdTimestamp", numToStr(s.createdTimestamp)); -} - -} // namespace - -std::string BankIndex::serialize() const { - std::string out; - { - ObjWriter root(out); - root.keyRaw("version", numToStr(1)); - root.keyBegin("samples"); - out += '['; - for (std::size_t i = 0; i < samples_.size(); ++i) { - if (i) out += ','; - writeSample(out, samples_[i]); - } - out += ']'; - } // root closes the object here — not deferred to function return (NRVO would - // otherwise let the caller observe `out` before the closing brace is appended) - return out; -} - -// --------------------------------------------------------------------------- -// JSON parser (recursive descent). Returns false on any malformed input; never -// reads out of bounds. Only supports the subset our writer emits. -// --------------------------------------------------------------------------- - -namespace { - -class Parser { -public: - explicit Parser(const std::string& s) : s_(s) {} - - bool parseIndex(BankIndex& out); - -private: - const std::string& s_; - std::size_t pos_ = 0; - - bool eof() const { return pos_ >= s_.size(); } - char peek() const { return s_[pos_]; } - - void skipWs() { - while (!eof()) { - char c = s_[pos_]; - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; - else break; - } - } - - bool consume(char c) { - skipWs(); - if (eof() || s_[pos_] != c) return false; - ++pos_; - return true; - } - - bool parseString(std::string& out); - bool parseRawScalar(std::string& out); // number / true / false / null token - bool parseDouble(double& out); - bool parseInt64(std::int64_t& out); - bool parseInt(int& out); - bool parseBool(bool& out); - bool expectNullOr(bool& wasNull); // peeks for `null`; consumes if present - - bool parseSample(Sample& out); - bool parseKey(std::string& key); // an object member key + ':' - bool skipValue(); // for forward-compat unknown keys -}; - -// Parses a JSON string literal (with the escapes our writer emits, plus \uXXXX -// for control chars). Positioned at the opening quote after whitespace. -bool Parser::parseString(std::string& out) { - skipWs(); - if (eof() || s_[pos_] != '"') return false; - ++pos_; - out.clear(); - while (!eof()) { - char c = s_[pos_++]; - if (c == '"') return true; - if (c == '\\') { - if (eof()) return false; - char e = s_[pos_++]; - switch (e) { - case '"': out += '"'; break; - case '\\': out += '\\'; break; - case '/': out += '/'; break; - case 'b': out += '\b'; break; - case 'f': out += '\f'; break; - case 'n': out += '\n'; break; - case 'r': out += '\r'; break; - case 't': out += '\t'; break; - case 'u': { - // Decode a \uXXXX escape to its code point. - auto readHex4 = [&](unsigned int& cp) -> bool { - if (pos_ + 4 > s_.size()) return false; - cp = 0; - for (int i = 0; i < 4; ++i) { - char h = s_[pos_++]; - cp <<= 4; - if (h >= '0' && h <= '9') cp |= static_cast(h - '0'); - else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); - else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10); - else return false; - } - return true; - }; - - unsigned int hi = 0; - if (!readHex4(hi)) return false; - - unsigned int codePoint = hi; - if (hi >= 0xD800 && hi <= 0xDBFF) { - // High surrogate — must be followed by \uDC00–\uDFFF. - if (pos_ + 6 > s_.size()) return false; - if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false; - pos_ += 2; - unsigned int lo = 0; - if (!readHex4(lo)) return false; - if (lo < 0xDC00 || lo > 0xDFFF) return false; // unpaired high surrogate - codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00); - } else if (hi >= 0xDC00 && hi <= 0xDFFF) { - return false; // unpaired low surrogate — malformed - } - - // Encode codePoint as UTF-8. - if (codePoint <= 0x7F) { - out += static_cast(codePoint); - } else if (codePoint <= 0x7FF) { - out += static_cast(0xC0 | (codePoint >> 6)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else if (codePoint <= 0xFFFF) { - out += static_cast(0xE0 | (codePoint >> 12)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else { - out += static_cast(0xF0 | (codePoint >> 18)); - out += static_cast(0x80 | ((codePoint >> 12) & 0x3F)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } - break; - } - default: return false; - } - } else { - out += c; - } - } - return false; // unterminated string -} - -// Reads a bare token (number, true, false, null) up to the next structural char. -bool Parser::parseRawScalar(std::string& out) { - skipWs(); - std::size_t start = pos_; - while (!eof()) { - char c = s_[pos_]; - if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' || - c == '\n' || c == '\r') - break; - ++pos_; - } - if (pos_ == start) return false; - out.assign(s_, start, pos_ - start); - return true; -} - -bool Parser::parseDouble(double& out) { - std::string tok; - if (!parseRawScalar(tok)) return false; - const char* b = tok.c_str(); - char* end = nullptr; - errno = 0; - double v = std::strtod(b, &end); - if (end != b + tok.size()) return false; - if (errno == ERANGE) return false; // overflow / underflow → malformed - out = v; - return true; -} - -bool Parser::parseInt64(std::int64_t& out) { - std::string tok; - if (!parseRawScalar(tok)) return false; - const char* b = tok.c_str(); - char* end = nullptr; - errno = 0; - long long v = std::strtoll(b, &end, 10); - if (end != b + tok.size()) return false; - if (errno == ERANGE) return false; // overflow → malformed - out = static_cast(v); - return true; -} - -bool Parser::parseInt(int& out) { - std::int64_t v = 0; - if (!parseInt64(v)) return false; - out = static_cast(v); - return true; -} - -bool Parser::parseBool(bool& out) { - std::string tok; - if (!parseRawScalar(tok)) return false; - if (tok == "true") { out = true; return true; } - if (tok == "false") { out = false; return true; } - return false; -} - -// If the next value is the `null` token, consumes it and sets wasNull=true. -// Otherwise leaves the position untouched and sets wasNull=false. Returns false -// only on eof. -bool Parser::expectNullOr(bool& wasNull) { - skipWs(); - if (eof()) return false; - if (s_.compare(pos_, 4, "null") == 0) { - pos_ += 4; - wasNull = true; - } else { - wasNull = false; - } - return true; -} - -bool Parser::parseKey(std::string& key) { - if (!parseString(key)) return false; - if (!consume(':')) return false; - return true; -} - -// Skips one JSON value (object / array / string / scalar) for forward-compat -// with keys we don't recognize. Assumes position is at the start of the value. -bool Parser::skipValue() { - skipWs(); - if (eof()) return false; - char c = s_[pos_]; - if (c == '"') { - std::string tmp; - return parseString(tmp); - } - if (c == '{' || c == '[') { - char open = c, close = (c == '{') ? '}' : ']'; - ++pos_; - int depth = 1; - while (!eof() && depth > 0) { - char d = s_[pos_]; - if (d == '"') { - std::string tmp; - if (!parseString(tmp)) return false; - continue; - } - if (d == open) ++depth; - else if (d == close) --depth; - ++pos_; - } - return depth == 0; - } - std::string tmp; - return parseRawScalar(tmp); -} - -bool Parser::parseSample(Sample& s) { - if (!consume('{')) return false; - skipWs(); - if (consume('}')) return true; // empty object (shouldn't happen, but valid) - - do { - std::string key; - if (!parseKey(key)) return false; - - if (key == "id") { - if (!parseString(s.id)) return false; - } else if (key == "displayName") { - if (!parseString(s.displayName)) return false; - } else if (key == "relativePath") { - if (!parseString(s.relativePath)) return false; - } else if (key == "sourceMode") { - int v = 0; - if (!parseInt(v)) return false; - // Valid range: MasterMix(0) .. Realtime(5). - if (v < static_cast(SourceMode::MasterMix) || - v > static_cast(SourceMode::Realtime)) - return false; - s.sourceMode = static_cast(v); - } else if (key == "sourceRange") { - if (!consume('{')) return false; - do { - std::string rk; - if (!parseKey(rk)) return false; - double dv = 0.0; - if (!parseDouble(dv)) return false; - if (rk == "startSeconds") s.sourceRange.startSeconds = dv; - else if (rk == "endSeconds") s.sourceRange.endSeconds = dv; - else if (rk == "startPpq") s.sourceRange.startPpq = dv; - else if (rk == "endPpq") s.sourceRange.endPpq = dv; - } while (consume(',')); - if (!consume('}')) return false; - } else if (key == "trackGuids") { - if (!consume('[')) return false; - skipWs(); - if (!consume(']')) { - do { - std::string g; - if (!parseString(g)) return false; - s.trackGuids.push_back(g); - } while (consume(',')); - if (!consume(']')) return false; - } - } else if (key == "wetDry") { - if (!parseDouble(s.wetDry)) return false; - } else if (key == "channelCount") { - if (!parseInt(s.channelCount)) return false; - } else if (key == "sampleRate") { - if (!parseInt(s.sampleRate)) return false; - } else if (key == "lengthSeconds") { - if (!parseDouble(s.lengthSeconds)) return false; - } else if (key == "lengthBeats") { - if (!parseDouble(s.lengthBeats)) return false; - } else if (key == "captureTempo") { - if (!parseDouble(s.captureTempo)) return false; - } else if (key == "captureTimeSigNum") { - if (!parseInt(s.captureTimeSigNum)) return false; - } else if (key == "captureTimeSigDenom") { - if (!parseInt(s.captureTimeSigDenom)) return false; - } else if (key == "key") { - bool wasNull = false; - if (!expectNullOr(wasNull)) return false; - if (wasNull) { - s.key.reset(); - } else { - std::string k; - if (!parseString(k)) return false; - s.key = k; - } - } else if (key == "rootNote") { - bool wasNull = false; - if (!expectNullOr(wasNull)) return false; - if (wasNull) { - s.rootNote.reset(); - } else { - int v = 0; - if (!parseInt(v)) return false; - // Valid MIDI note range: 0..127 inclusive (boundaries valid). - if (v < 0 || v > 127) return false; - s.rootNote = v; - } - } else if (key == "loop") { - bool wasNull = false; - if (!expectNullOr(wasNull)) return false; - if (wasNull) { - s.loop.reset(); - } else { - if (!consume('{')) return false; - LoopPoints lp; - do { - std::string lk; - if (!parseKey(lk)) return false; - std::int64_t lv = 0; - if (!parseInt64(lv)) return false; - if (lk == "start") lp.start = lv; - else if (lk == "end") lp.end = lv; - } while (consume(',')); - if (!consume('}')) return false; - // Invariant: 0 <= start <= end. start == end is a valid zero-length - // marker; a negative index or start > end is malformed, not silently - // clamped (mirrors the enum-range rejection above). - if (lp.start < 0 || lp.end < lp.start) return false; - s.loop = lp; - } - } else if (key == "levels") { - if (!consume('{')) return false; - do { - std::string lk; - if (!parseKey(lk)) return false; - double dv = 0.0; - if (!parseDouble(dv)) return false; - if (lk == "peakDb") s.levels.peakDb = dv; - else if (lk == "rmsDb") s.levels.rmsDb = dv; - else if (lk == "lufs") s.levels.lufs = dv; - } while (consume(',')); - if (!consume('}')) return false; - } else if (key == "clipped") { - if (!parseBool(s.clipped)) return false; - } else if (key == "tier") { - int v = 0; - if (!parseInt(v)) return false; - // Valid range: Scratch(0) .. Archive(1). - if (v < static_cast(Tier::Scratch) || - v > static_cast(Tier::Archive)) - return false; - s.tier = static_cast(v); - } else if (key == "contentHash") { - if (!parseString(s.contentHash)) return false; - } else if (key == "provenance") { - bool wasNull = false; - if (!expectNullOr(wasNull)) return false; - if (wasNull) { - s.provenance.reset(); - } else { - if (!consume('{')) return false; - Provenance p; - do { - std::string pk; - if (!parseKey(pk)) return false; - std::string pv; - if (!parseString(pv)) return false; - if (pk == "parentSampleId") p.parentSampleId = pv; - else if (pk == "fxChainSnapshot") p.fxChainSnapshot = pv; - } while (consume(',')); - if (!consume('}')) return false; - s.provenance = p; - } - } else if (key == "createdTimestamp") { - if (!parseInt64(s.createdTimestamp)) return false; - } else { - if (!skipValue()) return false; // forward-compat: ignore unknown - } - } while (consume(',')); - - return consume('}'); -} - -bool Parser::parseIndex(BankIndex& out) { - if (!consume('{')) return false; - skipWs(); - if (consume('}')) return true; // empty object — vacuously an empty index - - std::vector parsed; - do { - std::string key; - if (!parseKey(key)) return false; - - if (key == "samples") { - if (!consume('[')) return false; - skipWs(); - if (!consume(']')) { - do { - Sample s; - if (!parseSample(s)) return false; - parsed.push_back(std::move(s)); - } while (consume(',')); - if (!consume(']')) return false; - } - } else { - if (!skipValue()) return false; // version, or unknown keys - } - } while (consume(',')); - - if (!consume('}')) return false; - - // Trailing garbage after the root object is malformed. - skipWs(); - if (!eof()) return false; - - // Rebuild via add() so the same invariants (relative-path, dedup) that guard - // live inserts also guard deserialized data. Rejected/collapsed entries are - // dropped silently — a well-formed serialized index never triggers them. - for (auto& s : parsed) out.add(s); - return true; -} - -} // namespace - -std::optional BankIndex::deserialize(const std::string& json) { - BankIndex idx; - Parser p(json); - if (!p.parseIndex(idx)) return std::nullopt; - return idx; -} - -} // namespace reasampler diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index 5a0b618..94d6fd2 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -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 #include -#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(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(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& 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& sampleIds, const std::string& srcBankId) { @@ -2190,7 +2191,7 @@ void removeSamples(const std::vector& 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 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 ids; const RegionDisplay disp = focusedDisplay(); @@ -2212,7 +2213,7 @@ std::vector resolveDragPathsForOs() { std::vector 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(idx->size()) : 0; const int focus = g_panel.selection.focus; if (focus >= 0) diff --git a/src/bank_panel.h b/src/bank_panel.h index 8a3b5c8..1d5d52b 100644 --- a/src/bank_panel.h +++ b/src/bank_panel.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // 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 // bank as a grid of LICE-drawn waveform thumbnails. The panel itself NEVER inserts @@ -13,7 +14,7 @@ #include #include -#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 { diff --git a/src/peaks.cpp b/src/core/audio/peaks.cpp similarity index 98% rename from src/peaks.cpp rename to src/core/audio/peaks.cpp index b2a695a..1d79625 100644 --- a/src/peaks.cpp +++ b/src/core/audio/peaks.cpp @@ -1,4 +1,4 @@ -#include "peaks.h" +#include "core/audio/peaks.h" #include #include @@ -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& interleaved, std::size_t channelCount, @@ -123,4 +123,4 @@ std::size_t lastFrameAboveThreshold(const std::vector& interleaved, return kNoFrameAboveThreshold; } -} // namespace reasampler +} // namespace reasampler::audio diff --git a/src/peaks.h b/src/core/audio/peaks.h similarity index 99% rename from src/peaks.h rename to src/core/audio/peaks.h index dc1365a..6f3d5b6 100644 --- a/src/peaks.h +++ b/src/core/audio/peaks.h @@ -11,7 +11,7 @@ #include #include -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& interleaved, std::size_t frameCount, AudioSample linearThreshold); -} // namespace reasampler +} // namespace reasampler::audio diff --git a/src/batch_capture.cpp b/src/core/capture/batch_capture.cpp similarity index 95% rename from src/batch_capture.cpp rename to src/core/capture/batch_capture.cpp index 3e46851..3f5ff1d 100644 --- a/src/batch_capture.cpp +++ b/src/core/capture/batch_capture.cpp @@ -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 -namespace reasampler { +namespace reasampler::capture { std::vector planCaptureUnits(const std::vector& ranges) { std::vector units; @@ -73,4 +73,4 @@ std::string BatchOutcome::summaryLine(const std::string& noun) const { return line; } -} // namespace reasampler +} // namespace reasampler::capture diff --git a/src/batch_capture.h b/src/core/capture/batch_capture.h similarity index 98% rename from src/batch_capture.h rename to src/core/capture/batch_capture.h index 213eb9e..e1353a7 100644 --- a/src/batch_capture.h +++ b/src/core/capture/batch_capture.h @@ -29,7 +29,7 @@ #include #include -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 results_; }; -} // namespace reasampler +} // namespace reasampler::capture diff --git a/src/capture_paths.cpp b/src/core/capture/capture_paths.cpp similarity index 99% rename from src/capture_paths.cpp rename to src/core/capture/capture_paths.cpp index 33bfbce..175f40a 100644 --- a/src/capture_paths.cpp +++ b/src/core/capture/capture_paths.cpp @@ -1,4 +1,4 @@ -#include "capture_paths.h" +#include "core/capture/capture_paths.h" #include #include @@ -8,7 +8,7 @@ #include #include -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 diff --git a/src/capture_paths.h b/src/core/capture/capture_paths.h similarity index 98% rename from src/capture_paths.h rename to src/core/capture/capture_paths.h index 4ab8026..1cb9f6c 100644 --- a/src/capture_paths.h +++ b/src/core/capture/capture_paths.h @@ -17,7 +17,7 @@ #include #include -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; // /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 "[_].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 diff --git a/src/insert_plan.cpp b/src/core/capture/insert_plan.cpp similarity index 93% rename from src/insert_plan.cpp rename to src/core/capture/insert_plan.cpp index 9c3983e..b58d56c 100644 --- a/src/insert_plan.cpp +++ b/src/core/capture/insert_plan.cpp @@ -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 diff --git a/src/insert_plan.h b/src/core/capture/insert_plan.h similarity index 98% rename from src/insert_plan.h rename to src/core/capture/insert_plan.h index 7a6e2cb..2bb45e9 100644 --- a/src/insert_plan.h +++ b/src/core/capture/insert_plan.h @@ -22,7 +22,7 @@ #include -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 diff --git a/src/realtime_record.cpp b/src/core/capture/realtime_record.cpp similarity index 98% rename from src/realtime_record.cpp rename to src/core/capture/realtime_record.cpp index 9b287d1..9a754c4 100644 --- a/src/realtime_record.cpp +++ b/src/core/capture/realtime_record.cpp @@ -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 diff --git a/src/realtime_record.h b/src/core/capture/realtime_record.h similarity index 98% rename from src/realtime_record.h rename to src/core/capture/realtime_record.h index 82fec4c..76e551a 100644 --- a/src/realtime_record.h +++ b/src/core/capture/realtime_record.h @@ -24,9 +24,13 @@ #include #include -#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 diff --git a/src/render_settings.cpp b/src/core/capture/render_settings.cpp similarity index 98% rename from src/render_settings.cpp rename to src/core/capture/render_settings.cpp index 041cfa9..a83c5ca 100644 --- a/src/render_settings.cpp +++ b/src/core/capture/render_settings.cpp @@ -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 #include #include -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& captureActionTable() { return table; } -} // namespace reasampler +} // namespace reasampler::capture diff --git a/src/render_settings.h b/src/core/capture/render_settings.h similarity index 98% rename from src/render_settings.h rename to src/core/capture/render_settings.h index 8d337eb..4c88323 100644 --- a/src/render_settings.h +++ b/src/core/capture/render_settings.h @@ -25,9 +25,11 @@ #include #include -#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& captureActionTable(); -} // namespace reasampler +} // namespace reasampler::capture diff --git a/src/tail_control.cpp b/src/core/capture/tail_control.cpp similarity index 55% rename from src/tail_control.cpp rename to src/core/capture/tail_control.cpp index 1d4063a..01b9ad8 100644 --- a/src/tail_control.cpp +++ b/src/core/capture/tail_control.cpp @@ -1,14 +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 -#include #include -#include -#include -namespace reasampler { +#include "core/json/json.h" + +namespace reasampler::capture { TailMode cycleTailMode(TailMode current) { switch (current) { @@ -51,13 +50,13 @@ std::string tailToggleLabel(const TailSetting& setting) { // JSON round-trip // --------------------------------------------------------------------------- // -// The setting is a flat object of one enum + one double, so a compact hand-rolled -// writer + a tolerant minimal reader is the simplest thing that works (mirroring -// bank_model's dependency-free JSON choice). manualMs is emitted with 17 significant -// digits (%.17g) — the shortest form that round-trips every IEEE-754 double exactly — -// so deserialize(serialize(x)) == x holds bit-for-bit. deserialize is deliberately -// forgiving: any parse failure returns nullopt so the caller falls back to a default, -// exactly as an absent ext-state key does. +// The setting is a flat object of one enum + one double, riding the shared +// core/json layer (Q-W1, T2-02: the former substring-scan valueAfterKey reader — +// the fifth hand-rolled JSON decoder — is retired). manualMs is emitted with 17 +// significant digits (%.17g) — the shortest form that round-trips every IEEE-754 +// double exactly — so deserialize(serialize(x)) == x holds bit-for-bit. +// deserialize stays forgiving in outcome: any parse failure returns nullopt so +// the caller falls back to a default, exactly as an absent ext-state key does. namespace { @@ -81,52 +80,52 @@ std::optional modeFromInt(int v) { } } -// Find the value token following `"key":` in `json`. Returns a pointer just past the -// colon (skipping whitespace) or nullptr if the key is absent. Minimal: the writer -// emits exactly one flat object with unique keys, so a substring search is sufficient -// and there is no nesting to confuse it. -const char* valueAfterKey(const std::string& json, const char* key) { - const std::string needle = std::string("\"") + key + "\""; - const std::size_t pos = json.find(needle); - if (pos == std::string::npos) return nullptr; - const char* p = json.c_str() + pos + needle.size(); - while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p; - if (*p != ':') return nullptr; - ++p; - while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p; - return p; -} - } // namespace std::string serializeTailSetting(const TailSetting& setting) { - char buf[128]; - std::snprintf(buf, sizeof(buf), "{\"mode\":%d,\"manualMs\":%.17g}", - modeToInt(setting.mode), setting.manualMs); - return std::string(buf); + // Byte-identical to the former snprintf writer: {"mode":%d,"manualMs":%.17g}. + std::string out; + { + json::Writer w(out); + w.keyRaw("mode", json::numToStr(modeToInt(setting.mode))); + w.keyRaw("manualMs", json::numToStr(setting.manualMs)); + } // Writer closes the object here (see bank_model's NRVO note) + return out; } -std::optional deserializeTailSetting(const std::string& json) { - const char* modeTok = valueAfterKey(json, "mode"); - const char* msTok = valueAfterKey(json, "manualMs"); - if (!modeTok || !msTok) return std::nullopt; // absent key -> malformed -> default +std::optional deserializeTailSetting(const std::string& blob) { + json::Reader r(blob); + if (!r.consume('{')) return std::nullopt; - char* end = nullptr; - errno = 0; - const long modeVal = std::strtol(modeTok, &end, 10); - if (end == modeTok || errno != 0) return std::nullopt; - const std::optional mode = modeFromInt(static_cast(modeVal)); + int modeInt = 0; + double ms = 0.0; + bool haveMode = false, haveMs = false; + r.skipWs(); + if (!r.consume('}')) { + do { + std::string key; + if (!r.parseKey(key)) return std::nullopt; + if (key == "mode") { + if (!r.parseInt(modeInt)) return std::nullopt; + haveMode = true; + } else if (key == "manualMs") { + if (!r.parseDouble(ms)) return std::nullopt; + haveMs = true; + } else { + if (!r.skipValue()) return std::nullopt; // forward-compat + } + } while (r.consume(',')); + if (!r.consume('}')) return std::nullopt; + } + if (!haveMode || !haveMs) return std::nullopt; // absent key -> malformed -> default + + const std::optional mode = modeFromInt(modeInt); if (!mode) return std::nullopt; - end = nullptr; - errno = 0; - const double ms = std::strtod(msTok, &end); - if (end == msTok || errno != 0) return std::nullopt; - TailSetting out; out.mode = *mode; out.manualMs = ms; return out; } -} // namespace reasampler +} // namespace reasampler::capture diff --git a/src/tail_control.h b/src/core/capture/tail_control.h similarity index 95% rename from src/tail_control.h rename to src/core/capture/tail_control.h index 1e7ab93..9430dba 100644 --- a/src/tail_control.h +++ b/src/core/capture/tail_control.h @@ -12,9 +12,9 @@ #include #include -#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 deserializeTailSetting(const std::string& json); -} // namespace reasampler +} // namespace reasampler::capture diff --git a/src/wav_trim.cpp b/src/core/capture/wav_trim.cpp similarity index 98% rename from src/wav_trim.cpp rename to src/core/capture/wav_trim.cpp index e8265ae..3fbe945 100644 --- a/src/wav_trim.cpp +++ b/src/core/capture/wav_trim.cpp @@ -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 // 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 diff --git a/src/wav_trim.h b/src/core/capture/wav_trim.h similarity index 97% rename from src/wav_trim.h rename to src/core/capture/wav_trim.h index 7acf5ee..497790d 100644 --- a/src/wav_trim.h +++ b/src/core/capture/wav_trim.h @@ -29,9 +29,11 @@ #include #include -#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 diff --git a/src/vst/master_gain.cpp b/src/core/instrument/engine/master_gain.cpp similarity index 85% rename from src/vst/master_gain.cpp rename to src/core/instrument/engine/master_gain.cpp index 00413e4..02957dc 100644 --- a/src/vst/master_gain.cpp +++ b/src/core/instrument/engine/master_gain.cpp @@ -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 #include #include #include -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 diff --git a/src/vst/master_gain.h b/src/core/instrument/engine/master_gain.h similarity index 97% rename from src/vst/master_gain.h rename to src/core/instrument/engine/master_gain.h index 9ba20aa..42e2b3b 100644 --- a/src/vst/master_gain.h +++ b/src/core/instrument/engine/master_gain.h @@ -19,7 +19,7 @@ #include -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 diff --git a/src/vst/pitch_shift.cpp b/src/core/instrument/engine/pitch_shift.cpp similarity index 99% rename from src/vst/pitch_shift.cpp rename to src/core/instrument/engine/pitch_shift.cpp index dd73d16..6bcdae3 100644 --- a/src/vst/pitch_shift.cpp +++ b/src/core/instrument/engine/pitch_shift.cpp @@ -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 #include #include -namespace reasampler { +namespace reasampler::instrument::engine { namespace { @@ -431,4 +431,4 @@ AudioSample PitchShifter::processImpl(AudioSample in, const SpliceEvent* linked) return static_cast(out); } -} // namespace reasampler +} // namespace reasampler::instrument::engine diff --git a/src/vst/pitch_shift.h b/src/core/instrument/engine/pitch_shift.h similarity index 98% rename from src/vst/pitch_shift.h rename to src/core/instrument/engine/pitch_shift.h index aed90d6..2a73b8d 100644 --- a/src/vst/pitch_shift.h +++ b/src/core/instrument/engine/pitch_shift.h @@ -66,9 +66,11 @@ #include #include -#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 diff --git a/src/vst/sampler_core.cpp b/src/core/instrument/engine/sampler_core.cpp similarity index 99% rename from src/vst/sampler_core.cpp rename to src/core/instrument/engine/sampler_core.cpp index c0920cd..0edf418 100644 --- a/src/vst/sampler_core.cpp +++ b/src/core/instrument/engine/sampler_core.cpp @@ -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 @@ -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 diff --git a/src/vst/sampler_core.h b/src/core/instrument/engine/sampler_core.h similarity index 98% rename from src/vst/sampler_core.h rename to src/core/instrument/engine/sampler_core.h index c2c68bc..354845c 100644 --- a/src/vst/sampler_core.h +++ b/src/core/instrument/engine/sampler_core.h @@ -22,12 +22,19 @@ #include #include -#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 diff --git a/src/vst/velocity_curve.cpp b/src/core/instrument/engine/velocity_curve.cpp similarity index 97% rename from src/vst/velocity_curve.cpp rename to src/core/instrument/engine/velocity_curve.cpp index 248184d..50af883 100644 --- a/src/vst/velocity_curve.cpp +++ b/src/core/instrument/engine/velocity_curve.cpp @@ -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 // std::max, std::min, std::abs, std::stable_sort #include // std::fabs #include // 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 diff --git a/src/vst/velocity_curve.h b/src/core/instrument/engine/velocity_curve.h similarity index 99% rename from src/vst/velocity_curve.h rename to src/core/instrument/engine/velocity_curve.h index 6c761e9..a2de2dc 100644 --- a/src/vst/velocity_curve.h +++ b/src/core/instrument/engine/velocity_curve.h @@ -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 points_; }; -} // namespace reasampler::vst +} // namespace reasampler::instrument::engine diff --git a/src/vst/bank_sync.cpp b/src/core/instrument/map/bank_sync.cpp similarity index 71% rename from src/vst/bank_sync.cpp rename to src/core/instrument/map/bank_sync.cpp index 8ca894b..10532e9 100644 --- a/src/vst/bank_sync.cpp +++ b/src/core/instrument/map/bank_sync.cpp @@ -1,29 +1,22 @@ // bank_sync.cpp — see bank_sync.h. Pure; standard library only. -#include "bank_sync.h" +#include "core/instrument/map/bank_sync.h" #include -#include #include -namespace reasampler::vst { +#include "core/wire/wire.h" + +namespace reasampler::instrument::map { std::int64_t parseBankGeneration(const std::string& raw) { - if (raw.empty()) return kBankGenerationAbsent; - - // Whole-string, non-negative decimal parse WITHOUT exceptions or locale surprises. - // A leading '+' / '-' , any non-digit, an empty digit run, or overflow past int64 max - // all reject to the absent default (0). Manual accumulation with an overflow guard so a + // Whole-string, non-negative decimal parse WITHOUT exceptions or locale + // surprises — the shared core/wire accumulate (Q-W1, T2-01b). A leading + // '+' / '-', any non-digit, an empty string, or overflow past int64 max all + // reject to the absent default (0); the guarded accumulate means a // pathologically long digit run can never wrap into a bogus small value. std::int64_t value = 0; - constexpr std::int64_t kMax = std::numeric_limits::max(); - for (const char c : raw) { - if (c < '0' || c > '9') return kBankGenerationAbsent; // any non-digit -> reject whole - const int digit = c - '0'; - // Guard value*10 + digit against overflow before performing it. - if (value > (kMax - digit) / 10) return kBankGenerationAbsent; // would overflow -> reject - value = value * 10 + digit; - } + if (!wire::parseUnsignedDecimal(raw, value)) return kBankGenerationAbsent; return value; } @@ -67,4 +60,4 @@ AssignConsumeDecision consumeDecision(const std::optional& re return d; } -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/bank_sync.h b/src/core/instrument/map/bank_sync.h similarity index 96% rename from src/vst/bank_sync.h rename to src/core/instrument/map/bank_sync.h index cb1002d..856bfe5 100644 --- a/src/vst/bank_sync.h +++ b/src/core/instrument/map/bank_sync.h @@ -21,9 +21,11 @@ #include #include -#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& re std::int64_t lastConsumed, bool resolves, bool isFocusedTarget); -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/bridge_marshal.cpp b/src/core/instrument/map/bridge_marshal.cpp similarity index 80% rename from src/vst/bridge_marshal.cpp rename to src/core/instrument/map/bridge_marshal.cpp index 23d908c..2163b55 100644 --- a/src/vst/bridge_marshal.cpp +++ b/src/core/instrument/map/bridge_marshal.cpp @@ -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 decodeGetProjExtState(int apiReturn, const std::string& buffer) { @@ -13,4 +13,4 @@ std::optional decodeGetProjExtState(int apiReturn, return buffer; } -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/bridge_marshal.h b/src/core/instrument/map/bridge_marshal.h similarity index 95% rename from src/vst/bridge_marshal.h rename to src/core/instrument/map/bridge_marshal.h index 2c75068..9e918f4 100644 --- a/src/vst/bridge_marshal.h +++ b/src/core/instrument/map/bridge_marshal.h @@ -22,7 +22,7 @@ #include #include -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 decodeGetProjExtState(int apiReturn, const std::string& buffer); -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/note_entry.cpp b/src/core/instrument/map/note_entry.cpp similarity index 96% rename from src/vst/note_entry.cpp rename to src/core/instrument/map/note_entry.cpp index 99cb354..3a3e2a1 100644 --- a/src/vst/note_entry.cpp +++ b/src/core/instrument/map/note_entry.cpp @@ -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 #include -namespace reasampler::vst { +namespace reasampler::instrument::map { namespace { char asciiUpper(char c) { @@ -110,4 +110,4 @@ std::optional parseNoteEntry(const std::string& text) { return parseNoteName(s); } -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/note_entry.h b/src/core/instrument/map/note_entry.h similarity index 95% rename from src/vst/note_entry.h rename to src/core/instrument/map/note_entry.h index de3ae0a..b1e909d 100644 --- a/src/vst/note_entry.h +++ b/src/core/instrument/map/note_entry.h @@ -22,7 +22,7 @@ #include #include -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 parseNoteEntry(const std::string& text); -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/sample_map.cpp b/src/core/instrument/map/sample_map.cpp similarity index 98% rename from src/vst/sample_map.cpp rename to src/core/instrument/map/sample_map.cpp index 959582d..7ae5813 100644 --- a/src/vst/sample_map.cpp +++ b/src/core/instrument/map/sample_map.cpp @@ -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 // std::min #include // assert @@ -9,10 +9,12 @@ #include // std::memcpy #include // 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& 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& pts = z.velocityCurve.points(); + const std::vector& pts = z.velocityCurve.points(); putU32le(out, static_cast(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 pts; + std::vector 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 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& 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; } diff --git a/src/vst/sample_map.h b/src/core/instrument/map/sample_map.h similarity index 98% rename from src/vst/sample_map.h rename to src/core/instrument/map/sample_map.h index d8703b9..76764b6 100644 --- a/src/vst/sample_map.h +++ b/src/core/instrument/map/sample_map.h @@ -23,12 +23,18 @@ #include #include -#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) diff --git a/src/vst/trigger_seam.cpp b/src/core/instrument/map/trigger_seam.cpp similarity index 87% rename from src/vst/trigger_seam.cpp rename to src/core/instrument/map/trigger_seam.cpp index 0ee61a4..812fb3b 100644 --- a/src/vst/trigger_seam.cpp +++ b/src/core/instrument/map/trigger_seam.cpp @@ -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 -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(fadeFraction * static_cast(playLength) + 0.5); } -} // namespace reasampler::vst +} // namespace reasampler::instrument::map diff --git a/src/vst/trigger_seam.h b/src/core/instrument/map/trigger_seam.h similarity index 96% rename from src/vst/trigger_seam.h rename to src/core/instrument/map/trigger_seam.h index d7ce3c3..6539589 100644 --- a/src/vst/trigger_seam.h +++ b/src/core/instrument/map/trigger_seam.h @@ -25,7 +25,7 @@ #include -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 diff --git a/src/vst/browser_scroll.cpp b/src/core/instrument/ui/browser_scroll.cpp similarity index 88% rename from src/vst/browser_scroll.cpp rename to src/core/instrument/ui/browser_scroll.cpp index 85be0f8..3a7f9ad 100644 --- a/src/vst/browser_scroll.cpp +++ b/src/core/instrument/ui/browser_scroll.cpp @@ -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 #include -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( static_cast(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 filterNameIndices(const std::vector& names, return out; } -} // namespace reasampler::vst +} // namespace reasampler::instrument::ui diff --git a/src/vst/browser_scroll.h b/src/core/instrument/ui/browser_scroll.h similarity index 97% rename from src/vst/browser_scroll.h rename to src/core/instrument/ui/browser_scroll.h index 81aa831..641d409 100644 --- a/src/vst/browser_scroll.h +++ b/src/core/instrument/ui/browser_scroll.h @@ -24,9 +24,9 @@ #include #include -#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 filterNameIndices(const std::vector& names, const std::string& query); -} // namespace reasampler::vst +} // namespace reasampler::instrument::ui diff --git a/src/vst/capture_browser.cpp b/src/core/instrument/ui/capture_browser.cpp similarity index 68% rename from src/vst/capture_browser.cpp rename to src/core/instrument/ui/capture_browser.cpp index a36f63e..5ed9517 100644 --- a/src/vst/capture_browser.cpp +++ b/src/core/instrument/ui/capture_browser.cpp @@ -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 -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 diff --git a/src/vst/capture_browser.h b/src/core/instrument/ui/capture_browser.h similarity index 96% rename from src/vst/capture_browser.h rename to src/core/instrument/ui/capture_browser.h index 864f378..73ecbe7 100644 --- a/src/vst/capture_browser.h +++ b/src/core/instrument/ui/capture_browser.h @@ -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 diff --git a/src/core/instrument/ui/curve_popup.cpp b/src/core/instrument/ui/curve_popup.cpp new file mode 100644 index 0000000..29451e1 --- /dev/null +++ b/src/core/instrument/ui/curve_popup.cpp @@ -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 + +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 diff --git a/src/vst/curve_popup.h b/src/core/instrument/ui/curve_popup.h similarity index 94% rename from src/vst/curve_popup.h rename to src/core/instrument/ui/curve_popup.h index 3909b85..3213ec2 100644 --- a/src/vst/curve_popup.h +++ b/src/core/instrument/ui/curve_popup.h @@ -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 diff --git a/src/vst/editor_geometry.cpp b/src/core/instrument/ui/editor_geometry.cpp similarity index 64% rename from src/vst/editor_geometry.cpp rename to src/core/instrument/ui/editor_geometry.cpp index f838420..90ccfe5 100644 --- a/src/vst/editor_geometry.cpp +++ b/src/core/instrument/ui/editor_geometry.cpp @@ -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 -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 diff --git a/src/vst/editor_geometry.h b/src/core/instrument/ui/editor_geometry.h similarity index 91% rename from src/vst/editor_geometry.h rename to src/core/instrument/ui/editor_geometry.h index 8ca0615..a0c7ff9 100644 --- a/src/vst/editor_geometry.h +++ b/src/core/instrument/ui/editor_geometry.h @@ -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 diff --git a/src/vst/embed_strip.cpp b/src/core/instrument/ui/embed_strip.cpp similarity index 78% rename from src/vst/embed_strip.cpp rename to src/core/instrument/ui/embed_strip.cpp index e1bf0d9..7b8d827 100644 --- a/src/vst/embed_strip.cpp +++ b/src/core/instrument/ui/embed_strip.cpp @@ -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 -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(l * band.width()); + const int fillW = static_cast(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 diff --git a/src/vst/embed_strip.h b/src/core/instrument/ui/embed_strip.h similarity index 95% rename from src/vst/embed_strip.h rename to src/core/instrument/ui/embed_strip.h index b16a88d..1c28193 100644 --- a/src/vst/embed_strip.h +++ b/src/core/instrument/ui/embed_strip.h @@ -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 diff --git a/src/vst/envelope_edit.cpp b/src/core/instrument/ui/envelope_edit.cpp similarity index 88% rename from src/vst/envelope_edit.cpp rename to src/core/instrument/ui/envelope_edit.cpp index 56381fa..9b050f1 100644 --- a/src/vst/envelope_edit.cpp +++ b/src/core/instrument/ui/envelope_edit.cpp @@ -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 #include // 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(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(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(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 diff --git a/src/vst/envelope_edit.h b/src/core/instrument/ui/envelope_edit.h similarity index 96% rename from src/vst/envelope_edit.h rename to src/core/instrument/ui/envelope_edit.h index abd7c62..7d7699c 100644 --- a/src/vst/envelope_edit.h +++ b/src/core/instrument/ui/envelope_edit.h @@ -42,10 +42,10 @@ #include #include -#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 diff --git a/src/vst/envelope_overlay.cpp b/src/core/instrument/ui/envelope_overlay.cpp similarity index 90% rename from src/vst/envelope_overlay.cpp rename to src/core/instrument/ui/envelope_overlay.cpp index f413e41..a4bf59d 100644 --- a/src/vst/envelope_overlay.cpp +++ b/src/core/instrument/ui/envelope_overlay.cpp @@ -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 -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(w); if (px > static_cast(w - 1)) px = static_cast(w - 1); - return area.left + static_cast(px + 0.5); + return area.x + static_cast(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(kGateSustainDisplayFraction * static_cast(w) + 0.5); @@ -38,25 +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((1.0 - level) * static_cast(span) + 0.5); - return area.top + static_cast(dy); + return area.y + static_cast(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; v.node = node; @@ -71,12 +69,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(w - 1)) px = static_cast(w - 1); EnvVertex v; v.node = node; - v.x = area.left + static_cast(px + 0.5); + v.x = area.x + static_cast(px + 0.5); v.y = levelToY(area, level); v.level = level; return v; @@ -95,7 +93,7 @@ std::vector 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(W - gateTimedWidth(area)); const double sep = static_cast(kGateNodeSepPx); const double pps = gatePxPerSecond(area); @@ -162,7 +160,7 @@ std::vector triggerPolyline(const AmpEnvelope& env, const Rect& area, std::vector 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 +171,4 @@ std::vector buildEnvelopePolyline(const AmpEnvelope& env, const Rect& : triggerPolyline(env, area, totalSeconds); } -} // namespace reasampler::vst +} // namespace reasampler::instrument::ui diff --git a/src/vst/envelope_overlay.h b/src/core/instrument/ui/envelope_overlay.h similarity index 92% rename from src/vst/envelope_overlay.h rename to src/core/instrument/ui/envelope_overlay.h index fc3780e..0d06945 100644 --- a/src/vst/envelope_overlay.h +++ b/src/core/instrument/ui/envelope_overlay.h @@ -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 #include -#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 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 diff --git a/src/vst/keyboard_strip.cpp b/src/core/instrument/ui/keyboard_strip.cpp similarity index 86% rename from src/vst/keyboard_strip.cpp rename to src/core/instrument/ui/keyboard_strip.cpp index 98638b1..7b64af4 100644 --- a/src/vst/keyboard_strip.cpp +++ b/src/core/instrument/ui/keyboard_strip.cpp @@ -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 -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 diff --git a/src/vst/keyboard_strip.h b/src/core/instrument/ui/keyboard_strip.h similarity index 96% rename from src/vst/keyboard_strip.h rename to src/core/instrument/ui/keyboard_strip.h index ac8b4c3..2f843a3 100644 --- a/src/vst/keyboard_strip.h +++ b/src/core/instrument/ui/keyboard_strip.h @@ -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 diff --git a/src/vst/knob_deck.cpp b/src/core/instrument/ui/knob_deck.cpp similarity index 79% rename from src/vst/knob_deck.cpp rename to src/core/instrument/ui/knob_deck.cpp index 9df5b8c..6eae276 100644 --- a/src/vst/knob_deck.cpp +++ b/src/core/instrument/ui/knob_deck.cpp @@ -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 -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& 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 diff --git a/src/vst/knob_deck.h b/src/core/instrument/ui/knob_deck.h similarity index 97% rename from src/vst/knob_deck.h rename to src/core/instrument/ui/knob_deck.h index 5a1a890..2d07b70 100644 --- a/src/vst/knob_deck.h +++ b/src/core/instrument/ui/knob_deck.h @@ -27,9 +27,9 @@ #include -#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 diff --git a/src/vst/param_slider.cpp b/src/core/instrument/ui/param_slider.cpp similarity index 71% rename from src/vst/param_slider.cpp rename to src/core/instrument/ui/param_slider.cpp index a080e43..6aa17cc 100644 --- a/src/vst/param_slider.cpp +++ b/src/core/instrument/ui/param_slider.cpp @@ -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 #include -namespace reasampler::vst { +namespace reasampler::instrument::ui { + +using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24) std::vector layoutControls(const Rect& panel, const std::vector& controls) { std::vector 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 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(value * span + 0.5); + const int span = track.width; // handle-center movable span + const int centerX = track.x + static_cast(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(x - track.left) / static_cast(span); + if (x <= track.x) return 0.0; + if (x >= track.right()) return 1.0; + return static_cast(x - track.x) / static_cast(span); } // --- Radial knob (Wave A FA4) --------------------------------------------------------- @@ -95,16 +99,14 @@ 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 +156,4 @@ int controlAtPoint(const std::vector& rows, int x, int y) { return -1; } -} // namespace reasampler::vst +} // namespace reasampler::instrument::ui diff --git a/src/vst/param_slider.h b/src/core/instrument/ui/param_slider.h similarity index 97% rename from src/vst/param_slider.h rename to src/core/instrument/ui/param_slider.h index b853eaa..0720388 100644 --- a/src/vst/param_slider.h +++ b/src/core/instrument/ui/param_slider.h @@ -24,9 +24,9 @@ #include -#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& rows, int x, int y); -} // namespace reasampler::vst +} // namespace reasampler::instrument::ui diff --git a/src/vst/waveform_view.cpp b/src/core/instrument/ui/waveform_view.cpp similarity index 88% rename from src/vst/waveform_view.cpp rename to src/core/instrument/ui/waveform_view.cpp index 2b6d70c..fc004b9 100644 --- a/src/vst/waveform_view.cpp +++ b/src/core/instrument/ui/waveform_view.cpp @@ -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 #include // 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(w) + frameCount / 2; - return area.left + static_cast(num / frameCount); + return area.x + static_cast(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(x - area.left); + if (x <= area.x) return 0; + if (x >= area.right()) return frameCount; + const std::int64_t dx = static_cast(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(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 diff --git a/src/vst/waveform_view.h b/src/core/instrument/ui/waveform_view.h similarity index 89% rename from src/vst/waveform_view.h rename to src/core/instrument/ui/waveform_view.h index 4831cbf..171b39a 100644 --- a/src/vst/waveform_view.h +++ b/src/core/instrument/ui/waveform_view.h @@ -24,10 +24,12 @@ #include -#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 diff --git a/src/core/json/json.cpp b/src/core/json/json.cpp new file mode 100644 index 0000000..ad3d6bb --- /dev/null +++ b/src/core/json/json.cpp @@ -0,0 +1,319 @@ +// core/json implementation — see json.h. The bodies are the (previously +// quintuplicated) bank_model / view_mode_model lexical layer, verbatim; any +// behavioral change here changes five persisted-blob parsers at once. + +#include "core/json/json.h" + +#include +#include +#include +#include + +namespace reasampler::json { + +// --------------------------------------------------------------------------- +// emit helpers +// --------------------------------------------------------------------------- + +void writeEscaped(std::string& out, const std::string& s) { + out += '"'; + for (char c : s) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\b': out += "\\b"; break; + case '\f': out += "\\f"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: + if (static_cast(c) < 0x20) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); + out += buf; + } else { + out += c; + } + } + } + out += '"'; +} + +std::string numToStr(double v) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.17g", v); + return buf; +} + +std::string numToStr(std::int64_t v) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%lld", static_cast(v)); + return buf; +} + +std::string numToStr(int v) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%d", v); + return buf; +} + +void writeStringArray(std::string& out, const std::vector& v) { + out += '['; + for (std::size_t i = 0; i < v.size(); ++i) { + if (i) out += ','; + writeEscaped(out, v[i]); + } + out += ']'; +} + +void writeIntArray(std::string& out, const std::vector& v) { + out += '['; + for (std::size_t i = 0; i < v.size(); ++i) { + if (i) out += ','; + out += numToStr(v[i]); + } + out += ']'; +} + +// --------------------------------------------------------------------------- +// Reader +// --------------------------------------------------------------------------- + +void Reader::skipWs() { + while (!eof()) { + char c = s_[pos_]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; + else break; + } +} + +bool Reader::consume(char c) { + skipWs(); + if (eof() || s_[pos_] != c) return false; + ++pos_; + return true; +} + +// Parses a JSON string literal (with the escapes our writers emit, plus \uXXXX +// for control chars). Positioned before the opening quote (skips leading ws). +bool Reader::parseString(std::string& out) { + skipWs(); + if (eof() || s_[pos_] != '"') return false; + ++pos_; + out.clear(); + while (!eof()) { + char c = s_[pos_++]; + if (c == '"') return true; + if (c == '\\') { + if (eof()) return false; + char e = s_[pos_++]; + switch (e) { + case '"': out += '"'; break; + case '\\': out += '\\'; break; + case '/': out += '/'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case 'n': out += '\n'; break; + case 'r': out += '\r'; break; + case 't': out += '\t'; break; + case 'u': { + // Decode a \uXXXX escape to its code point. + auto readHex4 = [&](unsigned int& cp) -> bool { + if (pos_ + 4 > s_.size()) return false; + cp = 0; + for (int i = 0; i < 4; ++i) { + char h = s_[pos_++]; + cp <<= 4; + if (h >= '0' && h <= '9') cp |= static_cast(h - '0'); + else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); + else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10); + else return false; + } + return true; + }; + + unsigned int hi = 0; + if (!readHex4(hi)) return false; + + unsigned int codePoint = hi; + if (hi >= 0xD800 && hi <= 0xDBFF) { + // High surrogate — must be followed by \uDC00–\uDFFF. + if (pos_ + 6 > s_.size()) return false; + if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false; + pos_ += 2; + unsigned int lo = 0; + if (!readHex4(lo)) return false; + if (lo < 0xDC00 || lo > 0xDFFF) return false; // unpaired high surrogate + codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00); + } else if (hi >= 0xDC00 && hi <= 0xDFFF) { + return false; // unpaired low surrogate — malformed + } + + // Encode codePoint as UTF-8. + if (codePoint <= 0x7F) { + out += static_cast(codePoint); + } else if (codePoint <= 0x7FF) { + out += static_cast(0xC0 | (codePoint >> 6)); + out += static_cast(0x80 | (codePoint & 0x3F)); + } else if (codePoint <= 0xFFFF) { + out += static_cast(0xE0 | (codePoint >> 12)); + out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); + out += static_cast(0x80 | (codePoint & 0x3F)); + } else { + out += static_cast(0xF0 | (codePoint >> 18)); + out += static_cast(0x80 | ((codePoint >> 12) & 0x3F)); + out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); + out += static_cast(0x80 | (codePoint & 0x3F)); + } + break; + } + default: return false; + } + } else { + out += c; + } + } + return false; // unterminated string +} + +bool Reader::parseRawScalar(std::string& out) { + skipWs(); + std::size_t start = pos_; + while (!eof()) { + char c = s_[pos_]; + if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' || + c == '\n' || c == '\r') + break; + ++pos_; + } + if (pos_ == start) return false; + out.assign(s_, start, pos_ - start); + return true; +} + +bool Reader::parseDouble(double& out) { + std::string tok; + if (!parseRawScalar(tok)) return false; + const char* b = tok.c_str(); + char* end = nullptr; + errno = 0; + double v = std::strtod(b, &end); + if (end != b + tok.size()) return false; + if (errno == ERANGE) return false; // overflow / underflow -> malformed + out = v; + return true; +} + +bool Reader::parseInt64(std::int64_t& out) { + std::string tok; + if (!parseRawScalar(tok)) return false; + const char* b = tok.c_str(); + char* end = nullptr; + errno = 0; + long long v = std::strtoll(b, &end, 10); + if (end != b + tok.size()) return false; + if (errno == ERANGE) return false; // overflow -> malformed + out = static_cast(v); + return true; +} + +bool Reader::parseInt(int& out) { + std::int64_t v = 0; + if (!parseInt64(v)) return false; + if (v < INT_MIN || v > INT_MAX) return false; + out = static_cast(v); + return true; +} + +bool Reader::parseBool(bool& out) { + std::string tok; + if (!parseRawScalar(tok)) return false; + if (tok == "true") { out = true; return true; } + if (tok == "false") { out = false; return true; } + return false; +} + +bool Reader::expectNullOr(bool& wasNull) { + skipWs(); + if (eof()) return false; + if (s_.compare(pos_, 4, "null") == 0) { + pos_ += 4; + wasNull = true; + } else { + wasNull = false; + } + return true; +} + +bool Reader::parseKey(std::string& key) { + if (!parseString(key)) return false; + return consume(':'); +} + +bool Reader::parseStringArray(std::vector& out) { + if (!consume('[')) return false; + skipWs(); + if (consume(']')) return true; // empty array + do { + std::string s; + if (!parseString(s)) return false; + out.push_back(std::move(s)); + } while (consume(',')); + return consume(']'); +} + +bool Reader::parseIntArray(std::vector& out) { + if (!consume('[')) return false; + skipWs(); + if (consume(']')) return true; + do { + int v = 0; + if (!parseInt(v)) return false; + out.push_back(v); + } while (consume(',')); + return consume(']'); +} + +bool Reader::skipValue() { + std::string raw; + return captureValue(raw); +} + +// Records the raw source span of one JSON value starting at the current position +// (after whitespace). Handles nested objects/arrays with string-aware brace +// matching (braces inside strings ignored). +bool Reader::captureValue(std::string& raw) { + skipWs(); + if (eof()) return false; + std::size_t start = pos_; + char c = s_[pos_]; + if (c == '"') { + std::string tmp; + if (!parseString(tmp)) return false; + raw.assign(s_, start, pos_ - start); + return true; + } + if (c == '{' || c == '[') { + char open = c, close = (c == '{') ? '}' : ']'; + ++pos_; + int depth = 1; + while (!eof() && depth > 0) { + char d = s_[pos_]; + if (d == '"') { + std::string tmp; + if (!parseString(tmp)) return false; // advances past the string + continue; + } + if (d == open) ++depth; + else if (d == close) --depth; + ++pos_; + } + if (depth != 0) return false; + raw.assign(s_, start, pos_ - start); + return true; + } + // bare scalar (number / true / false / null) + return parseRawScalar(raw); +} + +} // namespace reasampler::json diff --git a/src/core/json/json.h b/src/core/json/json.h new file mode 100644 index 0000000..2af28c3 --- /dev/null +++ b/src/core/json/json.h @@ -0,0 +1,149 @@ +// core/json — the ONE hand-rolled JSON lexical layer (Q-W1; audit T2-02 / §2 +// "Parser ×4"). Pure: standard library only — NO REAPER, NO SWELL, NO VST3. +// +// This module owns the lexical half of the house JSON dialect: the escape-aware +// string literal (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), the bare +// scalar tokens, the number parses (strtod/strtoll with full-token + ERANGE +// rejection), key+':' consumption, unknown-value skipping, and the emit side +// (escaping, %.17g / %d / %lld number rendering, the scoped object writer). +// The DOMAIN grammars — which keys exist, what shape each value takes, what is +// rejected at the model boundary — stay in the consumers (bank_model, bank_book, +// view_mode_model, owned_manifest, tail_control). One lexical definition means +// the five decoders can no longer drift on tolerance or escaping. +// +// Byte-compatibility contract (load-bearing): the emit helpers reproduce the +// prior per-module writers EXACTLY — writeEscaped's escape set, %.17g for +// doubles (shortest form that round-trips every IEEE-754 double bit-for-bit), +// plain decimal for ints — so a re-serialized blob is byte-identical to what +// the pre-extraction writers produced. This was a structural dedupe, not a +// format change; persisted .rpp ext-state must not shift by a byte. + +#pragma once + +#include +#include +#include + +namespace reasampler::json { + +// --------------------------------------------------------------------------- +// emit helpers (writer side) +// --------------------------------------------------------------------------- + +// Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX +// for remaining control chars, everything else verbatim (UTF-8 passes through). +void writeEscaped(std::string& out, const std::string& s); + +// Number rendering. %.17g is the shortest form that round-trips every IEEE-754 +// double exactly, so deserialize(serialize(x)) == x holds bit-for-bit. +std::string numToStr(double v); +std::string numToStr(std::int64_t v); +std::string numToStr(int v); + +// Flat homogeneous arrays: ["a","b"] / [1,2]. Empty vector -> "[]". +void writeStringArray(std::string& out, const std::vector& v); +void writeIntArray(std::string& out, const std::vector& v); + +// Scoped object writer: appends '{' on construction and '}' on destruction, with +// comma separation handled internally. Nested values are written by keyBegin() +// followed by the caller emitting the value (e.g. a nested Writer scope or an +// array). NOTE the destructor-close means an enclosing scope must END (brace +// block) before the built string is returned — see the NRVO note in the +// consumers' serialize() implementations. +class Writer { +public: + explicit Writer(std::string& out) : out_(out) { out_ += '{'; } + ~Writer() { out_ += '}'; } + + Writer(const Writer&) = delete; + Writer& operator=(const Writer&) = delete; + + // "key": — rawValue appended verbatim (numbers, bools, null, + // pre-serialized nested blobs). + void keyRaw(const char* key, const std::string& rawValue) { + sep(); + writeEscaped(out_, key); + out_ += ':'; + out_ += rawValue; + } + // "key":"value" — value escaped. + void keyStr(const char* key, const std::string& value) { + sep(); + writeEscaped(out_, key); + out_ += ':'; + writeEscaped(out_, value); + } + // "key": — caller writes the value immediately after. + void keyBegin(const char* key) { + sep(); + writeEscaped(out_, key); + out_ += ':'; + } + +private: + void sep() { + if (first_) first_ = false; else out_ += ','; + } + std::string& out_; + bool first_ = true; +}; + +// --------------------------------------------------------------------------- +// Reader — the lexical cursor (parser side) +// --------------------------------------------------------------------------- +// +// Every method returns false on malformed input and never reads out of bounds. +// Only the subset the house writers emit is supported. The reader borrows the +// input string — it must outlive the Reader. +class Reader { +public: + explicit Reader(const std::string& s) : s_(s) {} + + bool eof() const { return pos_ >= s_.size(); } + void skipWs(); + + // Consumes `c` (after whitespace). False without advancing past `c` if the + // next non-ws char differs. + bool consume(char c); + + // JSON string literal (escapes + \uXXXX incl. surrogate pairs -> UTF-8). + bool parseString(std::string& out); + + // Bare token (number / true / false / null) up to the next structural char. + bool parseRawScalar(std::string& out); + + // Numbers: full-token parse; trailing bytes or ERANGE reject. parseInt + // additionally rejects values outside [INT_MIN, INT_MAX]. + bool parseDouble(double& out); + bool parseInt64(std::int64_t& out); + bool parseInt(int& out); + + bool parseBool(bool& out); + + // Peeks for the `null` token; consumes it if present (wasNull=true), + // otherwise leaves the position untouched (wasNull=false). Returns false + // only on eof. + bool expectNullOr(bool& wasNull); + + // An object member key + ':'. + bool parseKey(std::string& key); + + // Homogeneous arrays. Appends to `out`; empty array is valid. + bool parseStringArray(std::vector& out); + bool parseIntArray(std::vector& out); + + // Skips one value of any shape (string / object / array / bare scalar) — + // forward-compat for unknown keys. + bool skipValue(); + + // 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 -> BankModel::deserialize seam. + bool captureValue(std::string& raw); + +private: + const std::string& s_; + std::size_t pos_ = 0; +}; + +} // namespace reasampler::json diff --git a/src/bank_book.cpp b/src/core/model/bank_book.cpp similarity index 61% rename from src/bank_book.cpp rename to src/core/model/bank_book.cpp index 786e265..20372ac 100644 --- a/src/bank_book.cpp +++ b/src/core/model/bank_book.cpp @@ -1,144 +1,24 @@ -#include "bank_book.h" +#include "core/model/bank_book.h" #include -#include #include +#include "core/json/json.h" + // bank_book implementation. // -// JSON is hand-rolled and self-contained, matching the house style of bank_model -// and view_mode_model (brief: keep the pure core dependency-free — no third-party -// JSON lib). The book blob nests one bank object per bank, each carrying that -// bank's BankIndex serialized by bank_model's OWN writer (BankIndex::serialize), +// 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 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 SlotMap::orderedIds() const { - // entries_ is sorted ascending by slot, so a straight walk is display order. - std::vector 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& 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& 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>& 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 -// file-local ObjWriter / intToStr 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 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 indexIds(const BankIndex& idx) { +std::vector indexIds(const BankModel& idx) { std::vector ids; for (const auto& s : idx.all()) ids.push_back(s.id); return ids; @@ -550,81 +430,13 @@ std::vector BankBook::referencedPaths() const { namespace { -void writeEscaped(std::string& out, const std::string& s) { - out += '"'; - for (char c : s) { - switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\b': out += "\\b"; break; - case '\f': out += "\\f"; break; - case '\n': out += "\\n"; break; - case '\r': out += "\\r"; break; - case '\t': out += "\\t"; break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); - out += buf; - } else { - out += c; - } - } - } - out += '"'; -} - -std::string intToStr(int v) { - char buf[16]; - std::snprintf(buf, sizeof(buf), "%d", v); - return buf; -} - -class ObjWriter { -public: - explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; } - ~ObjWriter() { out_ += '}'; } - - void keyRaw(const char* key, const std::string& rawValue) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - out_ += rawValue; - } - void keyStr(const char* key, const std::string& value) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - writeEscaped(out_, value); - } - void keyBegin(const char* key) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - } - -private: - void sep() { if (first_) first_ = false; else out_ += ','; } - std::string& out_; - bool first_ = true; -}; +// Shared core/json emit helpers (Q-W1): the same escape set + %d rendering the +// prior file-local writer carried, so the emitted blob is byte-identical. +std::string intToStr(int v) { return json::numToStr(v); } +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; { @@ -632,7 +444,7 @@ std::string BankBook::serialize() const { root.keyRaw("version", intToStr(1)); root.keyStr("activeBank", activeBankId_); - // banks: array of { id, displayName, ordinal, index: }. + // banks: array of { id, displayName, ordinal, index: }. // The pool rides in as bank-zero, persisted identically to any named bank. root.keyBegin("banks"); out += '['; @@ -643,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. @@ -660,224 +472,39 @@ std::string BankBook::serialize() const { namespace { -class Parser { -public: - explicit Parser(const std::string& s) : s_(s) {} +// The book DOMAIN grammar over the shared core/json lexical layer (Q-W1). +// parseBank parses one bank object; parseSlots the "slots" array ([{id, slot}, +// ...]) into (id, slot) pairs (empty array valid; the pair-level defensive +// repair — dupes/conflicts — lives in SlotMap::fromEntries); parseBook the root +// blob, distinguishing the legacy shape (a bare bank_index object: has +// "samples", no "banks") from the book shape (has "banks"): a legacy blob +// yields a single pool bank carrying the migrated index and an empty active id +// (⇒ pool). The member deserialize() adopts the result (ordinal normalize + +// active resolve). +bool parseSlots(json::Reader& r, std::vector>& out); - // Parses a book blob into a bank set + active id. On success fills the out-params - // and returns true. Distinguishes the legacy shape (a bare bank_index object: has - // "samples", no "banks") from the book shape (has "banks"): a legacy blob yields a - // single pool bank carrying the migrated index and an empty active id (⇒ pool). The - // member deserialize() adopts the result (ordinal normalize + active resolve). - bool parseBook(std::vector& banks, std::string& activeBank); - -private: - const std::string& s_; - std::size_t pos_ = 0; - - bool eof() const { return pos_ >= s_.size(); } - - void skipWs() { - while (!eof()) { - char c = s_[pos_]; - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; - else break; - } - } - - bool consume(char c) { - skipWs(); - if (eof() || s_[pos_] != c) return false; - ++pos_; - return true; - } - - bool parseString(std::string& out); - bool parseInt(int& out); - bool parseKey(std::string& key); - bool skipValue(); - // Captures the raw source text of one JSON value (object / array / string / - // scalar) verbatim, so a nested BankIndex blob can be handed to its own parser. - bool captureValue(std::string& raw); - - bool parseBank(Bank& out); - // Parses the "slots" array ([{id, slot}, ...]) into (id, slot) pairs. An empty - // array is valid (an empty bank). Malformed structure fails the whole parse; the - // pair-level defensive repair (dupes/conflicts) lives in SlotMap::fromEntries. - bool parseSlots(std::vector>& out); -}; - -bool Parser::parseString(std::string& out) { - skipWs(); - if (eof() || s_[pos_] != '"') return false; - ++pos_; - out.clear(); - while (!eof()) { - char c = s_[pos_++]; - if (c == '"') return true; - if (c == '\\') { - if (eof()) return false; - char e = s_[pos_++]; - switch (e) { - case '"': out += '"'; break; - case '\\': out += '\\'; break; - case '/': out += '/'; break; - case 'b': out += '\b'; break; - case 'f': out += '\f'; break; - case 'n': out += '\n'; break; - case 'r': out += '\r'; break; - case 't': out += '\t'; break; - case 'u': { - auto readHex4 = [&](unsigned int& cp) -> bool { - if (pos_ + 4 > s_.size()) return false; - cp = 0; - for (int i = 0; i < 4; ++i) { - char h = s_[pos_++]; - cp <<= 4; - if (h >= '0' && h <= '9') cp |= static_cast(h - '0'); - else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); - else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10); - else return false; - } - return true; - }; - unsigned int hi = 0; - if (!readHex4(hi)) return false; - unsigned int codePoint = hi; - if (hi >= 0xD800 && hi <= 0xDBFF) { - if (pos_ + 6 > s_.size()) return false; - if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false; - pos_ += 2; - unsigned int lo = 0; - if (!readHex4(lo)) return false; - if (lo < 0xDC00 || lo > 0xDFFF) return false; - codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00); - } else if (hi >= 0xDC00 && hi <= 0xDFFF) { - return false; // unpaired low surrogate - } - if (codePoint <= 0x7F) { - out += static_cast(codePoint); - } else if (codePoint <= 0x7FF) { - out += static_cast(0xC0 | (codePoint >> 6)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else if (codePoint <= 0xFFFF) { - out += static_cast(0xE0 | (codePoint >> 12)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else { - out += static_cast(0xF0 | (codePoint >> 18)); - out += static_cast(0x80 | ((codePoint >> 12) & 0x3F)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } - break; - } - default: return false; - } - } else { - out += c; - } - } - return false; // unterminated -} - -bool Parser::parseInt(int& out) { - skipWs(); - std::size_t start = pos_; - if (!eof() && (s_[pos_] == '-' || s_[pos_] == '+')) ++pos_; - std::size_t digitsStart = pos_; - while (!eof() && s_[pos_] >= '0' && s_[pos_] <= '9') ++pos_; - if (pos_ == digitsStart) return false; // no digits - long v = 0; - try { - v = std::stol(s_.substr(start, pos_ - start)); - } catch (...) { - return false; // out of long range → malformed - } - if (v < INT_MIN || v > INT_MAX) return false; - out = static_cast(v); - return true; -} - -bool Parser::parseKey(std::string& key) { - if (!parseString(key)) return false; - if (!consume(':')) return false; - return true; -} - -bool Parser::skipValue() { - std::string raw; - return captureValue(raw); -} - -// Records the raw source span of one JSON value starting at the current position -// (after whitespace) so it can be re-parsed by a nested parser. Handles nested -// objects/arrays with string-aware brace matching (braces inside strings ignored). -bool Parser::captureValue(std::string& raw) { - skipWs(); - if (eof()) return false; - std::size_t start = pos_; - char c = s_[pos_]; - if (c == '"') { - std::string tmp; - if (!parseString(tmp)) return false; - raw.assign(s_, start, pos_ - start); - return true; - } - if (c == '{' || c == '[') { - char open = c, close = (c == '{') ? '}' : ']'; - ++pos_; - int depth = 1; - while (!eof() && depth > 0) { - char d = s_[pos_]; - if (d == '"') { - std::string tmp; - if (!parseString(tmp)) return false; // advances past the string - continue; - } - if (d == open) ++depth; - else if (d == close) --depth; - ++pos_; - } - if (depth != 0) return false; - raw.assign(s_, start, pos_ - start); - return true; - } - // bare scalar (number / true / false / null) - while (!eof()) { - char d = s_[pos_]; - if (d == ',' || d == '}' || d == ']' || d == ' ' || d == '\t' || - d == '\n' || d == '\r') - break; - ++pos_; - } - if (pos_ == start) return false; - raw.assign(s_, start, pos_ - start); - return true; -} - -bool Parser::parseBank(Bank& b) { - if (!consume('{')) return false; - skipWs(); - if (consume('}')) return false; // a bank object must at least carry an id +bool parseBank(json::Reader& r, Bank& b) { + if (!r.consume('{')) return false; + r.skipWs(); + if (r.consume('}')) return false; // a bank object must at least carry an id bool haveId = false; bool haveIndex = false; do { std::string key; - if (!parseKey(key)) return false; + if (!r.parseKey(key)) return false; if (key == "id") { - if (!parseString(b.id)) return false; + if (!r.parseString(b.id)) return false; haveId = true; } else if (key == "displayName") { - if (!parseString(b.displayName)) return false; + if (!r.parseString(b.displayName)) return false; } else if (key == "ordinal") { - if (!parseInt(b.ordinal)) return false; + if (!r.parseInt(b.ordinal)) return false; } else if (key == "index") { std::string raw; - if (!captureValue(raw)) return false; - auto idx = BankIndex::deserialize(raw); + if (!r.captureValue(raw)) return false; + auto idx = BankModel::deserialize(raw); if (!idx) return false; // a malformed nested index fails the whole parse b.index = std::move(*idx); haveIndex = true; @@ -886,49 +513,50 @@ bool Parser::parseBank(Bank& b) { // nothing because the key never appears); when present it drives the // bank's SlotMap. reconcileSlots() (post-adopt) squares it with membership. std::vector> pairs; - if (!parseSlots(pairs)) return false; + if (!parseSlots(r, pairs)) return false; b.slots = SlotMap::fromEntries(pairs); } else { - if (!skipValue()) return false; // forward-compat unknown keys + if (!r.skipValue()) return false; // forward-compat unknown keys } - } while (consume(',')); + } while (r.consume(',')); - if (!consume('}')) return false; + if (!r.consume('}')) return false; if (!haveId || b.id.empty()) return false; // id keys the registry if (!haveIndex) return false; // every bank persists its index return true; } -bool Parser::parseSlots(std::vector>& out) { +bool parseSlots(json::Reader& r, std::vector>& out) { out.clear(); - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; // empty slot array — a bank with no positions yet + if (!r.consume('[')) return false; + r.skipWs(); + if (r.consume(']')) return true; // empty slot array — a bank with no positions yet do { - if (!consume('{')) return false; + if (!r.consume('{')) return false; std::string id; int slot = 0; bool haveId = false, haveSlot = false; do { std::string k; - if (!parseKey(k)) return false; - if (k == "id") { if (!parseString(id)) return false; haveId = true; } - else if (k == "slot") { if (!parseInt(slot)) return false; haveSlot = true; } - else { if (!skipValue()) return false; } // forward-compat - } while (consume(',')); - if (!consume('}')) return false; + if (!r.parseKey(k)) return false; + if (k == "id") { if (!r.parseString(id)) return false; haveId = true; } + else if (k == "slot") { if (!r.parseInt(slot)) return false; haveSlot = true; } + else { if (!r.skipValue()) return false; } // forward-compat + } while (r.consume(',')); + if (!r.consume('}')) return false; if (!haveId || !haveSlot) return false; // a slot entry needs both out.emplace_back(std::move(id), slot); - } while (consume(',')); - return consume(']'); + } while (r.consume(',')); + return r.consume(']'); } -bool Parser::parseBook(std::vector& banks, std::string& activeBank) { +bool parseBook(json::Reader& r, const std::string& raw, std::vector& banks, + std::string& activeBank) { banks.clear(); activeBank.clear(); - if (!consume('{')) return false; - skipWs(); - if (consume('}')) return false; // an empty object is neither shape → malformed + if (!r.consume('{')) return false; + r.skipWs(); + if (r.consume('}')) return false; // an empty object is neither shape → malformed // Decide the shape by which structural key we saw. A "banks" key ⇒ book shape; a // "samples" key with no "banks" ⇒ legacy shape (promote into the pool). @@ -938,41 +566,41 @@ bool Parser::parseBook(std::vector& banks, std::string& activeBank) { do { std::string key; - if (!parseKey(key)) return false; + if (!r.parseKey(key)) return false; if (key == "banks") { sawBanks = true; - if (!consume('[')) return false; - skipWs(); - if (!consume(']')) { + if (!r.consume('[')) return false; + r.skipWs(); + if (!r.consume(']')) { do { Bank b; - if (!parseBank(b)) return false; + if (!parseBank(r, b)) return false; parsedBanks.push_back(std::move(b)); - } while (consume(',')); - if (!consume(']')) return false; + } while (r.consume(',')); + if (!r.consume(']')) return false; } } else if (key == "activeBank") { - if (!parseString(activeBank)) return false; + 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 (!skipValue()) return false; + if (!r.skipValue()) return false; } else { - if (!skipValue()) return false; // version, or unknown + if (!r.skipValue()) return false; // version, or unknown } - } while (consume(',')); + } while (r.consume(',')); - if (!consume('}')) return false; - skipWs(); - if (!eof()) return false; // trailing garbage + if (!r.consume('}')) return false; + r.skipWs(); + if (!r.eof()) return false; // trailing garbage // --- 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(s_); + auto legacy = BankModel::deserialize(raw); if (!legacy) return false; Bank pool; pool.id = kPoolBankId; @@ -1059,11 +687,11 @@ void BankBook::adoptBanks(std::vector&& banks, const std::string& activeBa activeBankId_ = (bank(activeBank) != nullptr) ? activeBank : std::string(kPoolBankId); } -std::optional BankBook::deserialize(const std::string& json) { +std::optional BankBook::deserialize(const std::string& blob) { std::vector banks; std::string activeBank; - Parser p(json); - if (!p.parseBook(banks, activeBank)) return std::nullopt; + json::Reader r(blob); + if (!parseBook(r, blob, banks, activeBank)) return std::nullopt; BankBook book; book.adoptBanks(std::move(banks), activeBank); diff --git a/src/bank_book.h b/src/core/model/bank_book.h similarity index 78% rename from src/bank_book.h rename to src/core/model/bank_book.h index 9b4c6f8..6a66fe6 100644 --- a/src/bank_book.h +++ b/src/core/model/bank_book.h @@ -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 #include -#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 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& 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& 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>& 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 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(); diff --git a/src/core/model/bank_model.cpp b/src/core/model/bank_model.cpp new file mode 100644 index 0000000..dee8731 --- /dev/null +++ b/src/core/model/bank_model.cpp @@ -0,0 +1,461 @@ +#include "core/model/bank_model.h" + +#include + +#include "core/json/json.h" + +// bank_model implementation. +// +// JSON rides on the shared core/json lexical layer (Q-W1: one reader/writer, +// no per-module Parser copy). The field set is a flat struct of primitives, +// strings, one enum, a small string array, and a few optionals, so a compact +// writer + recursive-descent DOMAIN parser over json::Reader is the simplest +// thing that works. Doubles are emitted with 17 significant digits (%.17g), the +// shortest form that round-trips every IEEE-754 double exactly, so the +// deserialize(serialize(x)) == x invariant holds bit-for-bit. + +namespace reasampler::model { + +// --------------------------------------------------------------------------- +// equality +// --------------------------------------------------------------------------- + +bool SourceRange::operator==(const SourceRange& o) const { + return startSeconds == o.startSeconds && endSeconds == o.endSeconds && + startPpq == o.startPpq && endPpq == o.endPpq; +} + +bool Provenance::operator==(const Provenance& o) const { + return parentSampleId == o.parentSampleId && fxChainSnapshot == o.fxChainSnapshot; +} + +bool Levels::operator==(const Levels& o) const { + return peakDb == o.peakDb && rmsDb == o.rmsDb && lufs == o.lufs; +} + +bool LoopPoints::operator==(const LoopPoints& o) const { + return start == o.start && end == o.end; +} + +bool Sample::operator==(const Sample& o) const { + return id == o.id && displayName == o.displayName && relativePath == o.relativePath && + sourceMode == o.sourceMode && sourceRange == o.sourceRange && + trackGuids == o.trackGuids && wetDry == o.wetDry && + channelCount == o.channelCount && sampleRate == o.sampleRate && + lengthSeconds == o.lengthSeconds && lengthBeats == o.lengthBeats && + captureTempo == o.captureTempo && + captureTimeSigNum == o.captureTimeSigNum && + captureTimeSigDenom == o.captureTimeSigDenom && key == o.key && + rootNote == o.rootNote && loop == o.loop && levels == o.levels && + clipped == o.clipped && tier == o.tier && contentHash == o.contentHash && + provenance == o.provenance && createdTimestamp == o.createdTimestamp; +} + +// --------------------------------------------------------------------------- +// path invariant +// --------------------------------------------------------------------------- + +// DECISION: reject absolute paths rather than normalize them. The pure model has +// no knowledge of the project root, so it cannot correctly relativize an absolute +// path — any "normalization" would be a guess that could point at the wrong file. +// Rejecting at the boundary is honest and deterministic; the capture backend (M3) +// is responsible for handing us an already-relative path. Covers POSIX ("/x"), +// Windows drive ("C:\x", "C:/x", "C:foo" drive-relative), and UNC ("\\host\share") +// forms. Any leading : is rejected regardless of the character that follows — +// drive-relative paths ("C:foo.wav") resolve against the drive's current directory, +// not the project root, so they violate the relative-paths-only invariant just as +// much as "C:\foo.wav" does. +static bool isAbsolutePath(const std::string& p) { + if (p.empty()) return false; + if (p[0] == '/' || p[0] == '\\') return true; // POSIX root or UNC + if (p.size() >= 2 && std::isalpha(static_cast(p[0])) && p[1] == ':') + return true; // Windows drive (C:\, C:/, C:foo, C:) + return false; +} + +// --------------------------------------------------------------------------- +// BankModel +// --------------------------------------------------------------------------- + +AddResult BankModel::add(const Sample& sample) { + if (sample.id.empty()) return AddResult::RejectedEmptyId; + if (isAbsolutePath(sample.relativePath)) return AddResult::RejectedAbsolutePath; + + if (findByHash(sample.contentHash) != nullptr) + return AddResult::Collapsed; + + samples_.push_back(sample); + return AddResult::Added; +} + +bool BankModel::remove(const std::string& id) { + for (auto it = samples_.begin(); it != samples_.end(); ++it) { + if (it->id == id) { + samples_.erase(it); + return true; + } + } + return false; +} + +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) { + s = updated; // replace in place — position (insertion order) preserved + return true; + } + } + return false; +} + +const Sample* BankModel::query(const std::string& id) const { + for (const auto& s : samples_) + if (s.id == id) return &s; + return nullptr; +} + +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 BankModel::moveTier(const std::string& id, Tier tier) { + for (auto& s : samples_) { + if (s.id == id) { + s.tier = tier; + return true; + } + } + return false; +} + +std::vector BankModel::byTier(Tier tier) const { + std::vector out; + for (const auto& s : samples_) + if (s.tier == tier) out.push_back(s); + return out; +} + +// --------------------------------------------------------------------------- +// JSON writer +// --------------------------------------------------------------------------- + +namespace { + +using json::numToStr; +using json::writeEscaped; +using json::writeStringArray; +using ObjWriter = json::Writer; + +void writeSample(std::string& out, const Sample& s) { + ObjWriter w(out); + w.keyStr("id", s.id); + w.keyStr("displayName", s.displayName); + w.keyStr("relativePath", s.relativePath); + w.keyRaw("sourceMode", numToStr(static_cast(s.sourceMode))); + + w.keyBegin("sourceRange"); + { + ObjWriter r(out); + r.keyRaw("startSeconds", numToStr(s.sourceRange.startSeconds)); + r.keyRaw("endSeconds", numToStr(s.sourceRange.endSeconds)); + r.keyRaw("startPpq", numToStr(s.sourceRange.startPpq)); + r.keyRaw("endPpq", numToStr(s.sourceRange.endPpq)); + } + + w.keyBegin("trackGuids"); + writeStringArray(out, s.trackGuids); + + w.keyRaw("wetDry", numToStr(s.wetDry)); + w.keyRaw("channelCount", numToStr(s.channelCount)); + w.keyRaw("sampleRate", numToStr(s.sampleRate)); + w.keyRaw("lengthSeconds", numToStr(s.lengthSeconds)); + w.keyRaw("lengthBeats", numToStr(s.lengthBeats)); + w.keyRaw("captureTempo", numToStr(s.captureTempo)); + w.keyRaw("captureTimeSigNum", numToStr(s.captureTimeSigNum)); + w.keyRaw("captureTimeSigDenom", numToStr(s.captureTimeSigDenom)); + + // Optionals are emitted as null when absent so present/absent round-trips. + w.keyBegin("key"); + if (s.key) writeEscaped(out, *s.key); else out += "null"; + + // Phase S seam fields (D-B). Emitted as null when absent (same shape as `key` + // and `provenance`) so pre-Phase-S JSON — which lacks these keys entirely — + // parses to empty optionals and re-serializes without invention. + w.keyBegin("rootNote"); + if (s.rootNote) out += numToStr(*s.rootNote); else out += "null"; + + w.keyBegin("loop"); + if (s.loop) { + ObjWriter lp(out); + lp.keyRaw("start", numToStr(s.loop->start)); + lp.keyRaw("end", numToStr(s.loop->end)); + } else { + out += "null"; + } + + w.keyBegin("levels"); + { + ObjWriter l(out); + l.keyRaw("peakDb", numToStr(s.levels.peakDb)); + l.keyRaw("rmsDb", numToStr(s.levels.rmsDb)); + l.keyRaw("lufs", numToStr(s.levels.lufs)); + } + + w.keyRaw("clipped", s.clipped ? "true" : "false"); + w.keyRaw("tier", numToStr(static_cast(s.tier))); + w.keyStr("contentHash", s.contentHash); + + w.keyBegin("provenance"); + if (s.provenance) { + ObjWriter p(out); + p.keyStr("parentSampleId", s.provenance->parentSampleId); + p.keyStr("fxChainSnapshot", s.provenance->fxChainSnapshot); + } else { + out += "null"; + } + + w.keyRaw("createdTimestamp", numToStr(s.createdTimestamp)); +} + +} // namespace + +std::string BankModel::serialize() const { + std::string out; + { + ObjWriter root(out); + root.keyRaw("version", numToStr(1)); + root.keyBegin("samples"); + out += '['; + for (std::size_t i = 0; i < samples_.size(); ++i) { + if (i) out += ','; + writeSample(out, samples_[i]); + } + out += ']'; + } // root closes the object here — not deferred to function return (NRVO would + // otherwise let the caller observe `out` before the closing brace is appended) + return out; +} + +// --------------------------------------------------------------------------- +// JSON parser (recursive descent over the shared json::Reader). Returns false +// on any malformed input; never reads out of bounds. Only supports the subset +// our writer emits. The lexical layer (strings, numbers, skip) lives in +// core/json; only the Sample/index DOMAIN grammar lives here. +// --------------------------------------------------------------------------- + +namespace { + +bool parseSample(json::Reader& r, Sample& s) { + if (!r.consume('{')) return false; + r.skipWs(); + if (r.consume('}')) return true; // empty object (shouldn't happen, but valid) + + do { + std::string key; + if (!r.parseKey(key)) return false; + + if (key == "id") { + if (!r.parseString(s.id)) return false; + } else if (key == "displayName") { + if (!r.parseString(s.displayName)) return false; + } else if (key == "relativePath") { + if (!r.parseString(s.relativePath)) return false; + } else if (key == "sourceMode") { + int v = 0; + if (!r.parseInt(v)) return false; + // Valid range: MasterMix(0) .. Realtime(5). + if (v < static_cast(SourceMode::MasterMix) || + v > static_cast(SourceMode::Realtime)) + return false; + s.sourceMode = static_cast(v); + } else if (key == "sourceRange") { + if (!r.consume('{')) return false; + do { + std::string rk; + if (!r.parseKey(rk)) return false; + double dv = 0.0; + if (!r.parseDouble(dv)) return false; + if (rk == "startSeconds") s.sourceRange.startSeconds = dv; + else if (rk == "endSeconds") s.sourceRange.endSeconds = dv; + else if (rk == "startPpq") s.sourceRange.startPpq = dv; + else if (rk == "endPpq") s.sourceRange.endPpq = dv; + } while (r.consume(',')); + if (!r.consume('}')) return false; + } else if (key == "trackGuids") { + if (!r.consume('[')) return false; + r.skipWs(); + if (!r.consume(']')) { + do { + std::string g; + if (!r.parseString(g)) return false; + s.trackGuids.push_back(g); + } while (r.consume(',')); + if (!r.consume(']')) return false; + } + } else if (key == "wetDry") { + if (!r.parseDouble(s.wetDry)) return false; + } else if (key == "channelCount") { + if (!r.parseInt(s.channelCount)) return false; + } else if (key == "sampleRate") { + if (!r.parseInt(s.sampleRate)) return false; + } else if (key == "lengthSeconds") { + if (!r.parseDouble(s.lengthSeconds)) return false; + } else if (key == "lengthBeats") { + if (!r.parseDouble(s.lengthBeats)) return false; + } else if (key == "captureTempo") { + if (!r.parseDouble(s.captureTempo)) return false; + } else if (key == "captureTimeSigNum") { + if (!r.parseInt(s.captureTimeSigNum)) return false; + } else if (key == "captureTimeSigDenom") { + if (!r.parseInt(s.captureTimeSigDenom)) return false; + } else if (key == "key") { + bool wasNull = false; + if (!r.expectNullOr(wasNull)) return false; + if (wasNull) { + s.key.reset(); + } else { + std::string k; + if (!r.parseString(k)) return false; + s.key = k; + } + } else if (key == "rootNote") { + bool wasNull = false; + if (!r.expectNullOr(wasNull)) return false; + if (wasNull) { + s.rootNote.reset(); + } else { + int v = 0; + if (!r.parseInt(v)) return false; + // Valid MIDI note range: 0..127 inclusive (boundaries valid). + if (v < 0 || v > 127) return false; + s.rootNote = v; + } + } else if (key == "loop") { + bool wasNull = false; + if (!r.expectNullOr(wasNull)) return false; + if (wasNull) { + s.loop.reset(); + } else { + if (!r.consume('{')) return false; + LoopPoints lp; + do { + std::string lk; + if (!r.parseKey(lk)) return false; + std::int64_t lv = 0; + if (!r.parseInt64(lv)) return false; + if (lk == "start") lp.start = lv; + else if (lk == "end") lp.end = lv; + } while (r.consume(',')); + if (!r.consume('}')) return false; + // Invariant: 0 <= start <= end. start == end is a valid zero-length + // marker; a negative index or start > end is malformed, not silently + // clamped (mirrors the enum-range rejection above). + if (lp.start < 0 || lp.end < lp.start) return false; + s.loop = lp; + } + } else if (key == "levels") { + if (!r.consume('{')) return false; + do { + std::string lk; + if (!r.parseKey(lk)) return false; + double dv = 0.0; + if (!r.parseDouble(dv)) return false; + if (lk == "peakDb") s.levels.peakDb = dv; + else if (lk == "rmsDb") s.levels.rmsDb = dv; + else if (lk == "lufs") s.levels.lufs = dv; + } while (r.consume(',')); + if (!r.consume('}')) return false; + } else if (key == "clipped") { + if (!r.parseBool(s.clipped)) return false; + } else if (key == "tier") { + int v = 0; + if (!r.parseInt(v)) return false; + // Valid range: Scratch(0) .. Archive(1). + if (v < static_cast(Tier::Scratch) || + v > static_cast(Tier::Archive)) + return false; + s.tier = static_cast(v); + } else if (key == "contentHash") { + if (!r.parseString(s.contentHash)) return false; + } else if (key == "provenance") { + bool wasNull = false; + if (!r.expectNullOr(wasNull)) return false; + if (wasNull) { + s.provenance.reset(); + } else { + if (!r.consume('{')) return false; + Provenance p; + do { + std::string pk; + if (!r.parseKey(pk)) return false; + std::string pv; + if (!r.parseString(pv)) return false; + if (pk == "parentSampleId") p.parentSampleId = pv; + else if (pk == "fxChainSnapshot") p.fxChainSnapshot = pv; + } while (r.consume(',')); + if (!r.consume('}')) return false; + s.provenance = p; + } + } else if (key == "createdTimestamp") { + if (!r.parseInt64(s.createdTimestamp)) return false; + } else { + if (!r.skipValue()) return false; // forward-compat: ignore unknown + } + } while (r.consume(',')); + + return r.consume('}'); +} + +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 + + std::vector parsed; + do { + std::string key; + if (!r.parseKey(key)) return false; + + if (key == "samples") { + if (!r.consume('[')) return false; + r.skipWs(); + if (!r.consume(']')) { + do { + Sample s; + if (!parseSample(r, s)) return false; + parsed.push_back(std::move(s)); + } while (r.consume(',')); + if (!r.consume(']')) return false; + } + } else { + if (!r.skipValue()) return false; // version, or unknown keys + } + } while (r.consume(',')); + + if (!r.consume('}')) return false; + + // Trailing garbage after the root object is malformed. + r.skipWs(); + if (!r.eof()) return false; + + // Rebuild via add() so the same invariants (relative-path, dedup) that guard + // live inserts also guard deserialized data. Rejected/collapsed entries are + // dropped silently — a well-formed serialized index never triggers them. + for (auto& s : parsed) out.add(s); + return true; +} + +} // namespace + +std::optional BankModel::deserialize(const std::string& blob) { + BankModel idx; + json::Reader r(blob); + if (!parseIndex(r, idx)) return std::nullopt; + return idx; +} + +} // namespace reasampler::model diff --git a/src/bank_model.h b/src/core/model/bank_model.h similarity index 95% rename from src/bank_model.h rename to src/core/model/bank_model.h index 5ad8472..c7612bc 100644 --- a/src/bank_model.h +++ b/src/core/model/bank_model.h @@ -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 #include -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; // 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 deserialize(const std::string& json); + static std::optional deserialize(const std::string& json); private: std::vector samples_; // insertion order preserved }; -} // namespace reasampler +} // namespace reasampler::model diff --git a/src/core/model/owned_manifest.cpp b/src/core/model/owned_manifest.cpp new file mode 100644 index 0000000..e2ae900 --- /dev/null +++ b/src/core/model/owned_manifest.cpp @@ -0,0 +1,113 @@ +#include "core/model/owned_manifest.h" + +#include + +#include "core/json/json.h" + +// owned_manifest implementation. +// +// JSON rides on the shared core/json lexical layer (Q-W1, mirror of bank_model / +// bank_book / tail_control). The shape is a single object with one string array: +// +// {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]} +// +// so a compact writer + a focused string-array domain parse is all it needs. + +namespace reasampler::model { + +// --------------------------------------------------------------------------- +// path invariant (mirror of bank_model's isAbsolutePath) +// --------------------------------------------------------------------------- + +namespace { + +// Any leading '/' or '\' (POSIX root / UNC), or a leading : (Windows drive, +// incl. drive-relative "C:foo") is absolute. Same rejection bank_model applies to +// Sample.relativePath — the manifest holds the SAME kind of path, so the invariant +// must match exactly (a path the index accepts must be recordable, and vice versa). +bool isAbsolutePath(const std::string& p) { + if (p.empty()) return false; + if (p[0] == '/' || p[0] == '\\') return true; + if (p.size() >= 2 && std::isalpha(static_cast(p[0])) && p[1] == ':') + return true; + return false; +} + +} // namespace + +// --------------------------------------------------------------------------- +// mutation / query +// --------------------------------------------------------------------------- + +ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) { + if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath; + if (isAbsolutePath(relativePath)) return ManifestAddResult::RejectedAbsolutePath; + if (contains(relativePath)) return ManifestAddResult::AlreadyPresent; + paths_.push_back(relativePath); + return ManifestAddResult::Added; +} + +bool OwnedFileManifest::contains(const std::string& relativePath) const { + for (const auto& p : paths_) + if (p == relativePath) return true; + return false; +} + +// --------------------------------------------------------------------------- +// JSON writer (shared core/json escape — byte-identical to the prior local one) +// --------------------------------------------------------------------------- + +std::string OwnedFileManifest::serialize() const { + std::string out = "{\"owned\":["; + for (std::size_t i = 0; i < paths_.size(); ++i) { + if (i) out += ','; + json::writeEscaped(out, paths_[i]); + } + out += "]}"; + return out; +} + +// --------------------------------------------------------------------------- +// JSON parser (string-array-only DOMAIN grammar over the shared core/json +// lexical layer). Tolerates unknown keys (forward-compat) and requires the +// "owned" value to be an array of strings. +// --------------------------------------------------------------------------- + +namespace { + +bool parseManifest(json::Reader& r, OwnedFileManifest& out) { + if (!r.consume('{')) return false; + r.skipWs(); + if (r.consume('}')) return true; // empty object -> empty manifest + for (;;) { + std::string key; + if (!r.parseKey(key)) return false; + if (key == "owned") { + std::vector paths; + if (!r.parseStringArray(paths)) return false; + for (auto& p : paths) { + // Feed through add() so the persisted invariants (dedup, reject + // empty/absolute) are re-asserted on load — a hand-edited or corrupt + // blob cannot smuggle an absolute or duplicate path into the manifest. + out.add(p); + } + } else { + if (!r.skipValue()) return false; // forward-compat: tolerate unknown keys + } + r.skipWs(); + if (r.consume(',')) continue; + if (r.consume('}')) return true; + return false; + } +} + +} // namespace + +std::optional OwnedFileManifest::deserialize(const std::string& blob) { + OwnedFileManifest m; + json::Reader r(blob); + if (!parseManifest(r, m)) return std::nullopt; + return m; +} + +} // namespace reasampler::model diff --git a/src/owned_manifest.h b/src/core/model/owned_manifest.h similarity index 93% rename from src/owned_manifest.h rename to src/core/model/owned_manifest.h index fe3a3d7..9135b75 100644 --- a/src/owned_manifest.h +++ b/src/core/model/owned_manifest.h @@ -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 #include #include -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 paths_; // insertion order; deduplicated }; -} // namespace reasampler +} // namespace reasampler::model diff --git a/src/provenance.cpp b/src/core/model/provenance.cpp similarity index 57% rename from src/provenance.cpp rename to src/core/model/provenance.cpp index 89cd816..329b66a 100644 --- a/src/provenance.cpp +++ b/src/core/model/provenance.cpp @@ -1,8 +1,8 @@ -#include "provenance.h" +#include "core/model/provenance.h" #include -#include -#include + +#include "core/wire/wire.h" // provenance implementation — pure, self-contained (no third-party lib, mirror of // bank_model's hand-rolled encoding discipline). @@ -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 && @@ -39,12 +39,12 @@ namespace { constexpr const char* kMagic = "rsprov1"; -// Append one length-prefixed field: ':' -void putField(std::string& out, const std::string& field) { - out += std::to_string(field.size()); - out += ':'; - out += field; -} +// The shared core/wire codec (Q-W1, T2-01b) carries the field grammar + the full +// hardening (incl. the fixed fieldInt range check that closes the old strtol +// silent-narrowing TODO). Only the %.17g double rendering stays local — it is +// this writer's convention, shared with the bank model's JSON doubles. +using wire::putField; +using Cursor = wire::Cursor; std::string dblToStr(double v) { char buf[32]; @@ -52,111 +52,6 @@ std::string dblToStr(double v) { return buf; } -// Cursor over the encoded string. All reads are bounds-checked; any short read -// fails the whole parse (ok_ latches false). -class Cursor { -public: - explicit Cursor(const std::string& s) : s_(s) {} - - bool ok() const { return ok_; } - bool atEnd() const { return pos_ >= s_.size(); } - - // Reads one length-prefixed field into `out`. Fails on a missing ':', an empty or - // non-numeric length, a length that overflows SIZE_MAX, or a length that runs past - // the end. Hardened form backported from the assignment_request / sample_usage - // siblings (Q-W0 T2-01a): the digit count is capped at 20 (the decimal width of - // SIZE_MAX on a 64-bit host) so a crafted 200-digit length cannot accumulate past - // SIZE_MAX via repeated multiply, and the bounds check is subtraction-first so a - // huge `len` cannot wrap `start + len` past the end test. - bool field(std::string& out) { - if (!ok_) return false; - const std::size_t colon = s_.find(':', pos_); - if (colon == std::string::npos) return fail(); - if (colon == pos_) return fail(); // empty length token - // Cap: SIZE_MAX fits in at most 20 decimal digits; a longer run is bogus. - if (colon - pos_ > 20u) return fail(); - std::size_t len = 0; - for (std::size_t i = pos_; i < colon; ++i) { - const char c = s_[i]; - if (c < '0' || c > '9') return fail(); - const std::size_t digit = static_cast(c - '0'); - // Overflow guard: if len would exceed SIZE_MAX after multiply+add, fail. - if (len > (std::numeric_limits::max() - digit) / 10u) - return fail(); - len = len * 10u + digit; - } - const std::size_t start = colon + 1; - // Subtraction-first form: start + len cannot wrap on a huge len. - if (start > s_.size() || len > s_.size() - start) return fail(); - out.assign(s_, start, len); - pos_ = start + len; - return true; - } - - bool fieldInt(int& out) { - std::string f; - if (!field(f)) return false; - return toInt(f, out); - } - - // A length-prefixed unsigned decimal (the GUID count). Hardened (Q-W0 T2-01a, the - // sample_usage fieldCount pattern): fails on empty, non-digit, a digit run past 20 - // (SIZE_MAX's decimal width), or an accumulate that would overflow SIZE_MAX. - bool fieldSizeT(std::size_t& out) { - std::string f; - if (!field(f)) return false; - if (f.empty() || f.size() > 20u) return fail(); - std::size_t v = 0; - for (const char c : f) { - if (c < '0' || c > '9') return fail(); - const std::size_t digit = static_cast(c - '0'); - if (v > (std::numeric_limits::max() - digit) / 10u) - return fail(); - v = v * 10u + digit; - } - out = v; - return true; - } - - bool fieldDouble(double& out) { - std::string f; - if (!field(f)) return false; - const char* b = f.c_str(); - char* end = nullptr; - double v = std::strtod(b, &end); - if (end != b + f.size()) return fail(); - out = v; - return true; - } - - // Consumes an exact literal at the cursor (the magic tag). Fails if absent. - bool literal(const char* lit) { - if (!ok_) return false; - const std::string l(lit); - if (s_.compare(pos_, l.size(), l) != 0) return fail(); - pos_ += l.size(); - return true; - } - -private: - bool fail() { ok_ = false; return false; } - // TODO(Q-W1): strtol does not check errno/range here, so an out-of-range field narrows - // silently to LONG_MAX (then truncates into `int`) instead of failing parse. Flagged for - // the Q-W1 wire-codec collapse rather than fixed in place. - static bool toInt(const std::string& f, int& out) { - const char* b = f.c_str(); - char* end = nullptr; - long v = std::strtol(b, &end, 10); - if (end != b + f.size() || f.empty()) return false; - out = static_cast(v); - return true; - } - - const std::string& s_; - std::size_t pos_ = 0; - bool ok_ = true; -}; - } // namespace std::string fxChainIdentity(const std::vector& entries) { @@ -264,4 +159,4 @@ std::optional detectParent( return parent; } -} // namespace reasampler +} // namespace reasampler::model diff --git a/src/provenance.h b/src/core/model/provenance.h similarity index 99% rename from src/provenance.h rename to src/core/model/provenance.h index d8f5a66..cd6255d 100644 --- a/src/provenance.h +++ b/src/core/model/provenance.h @@ -35,7 +35,7 @@ #include #include -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 detectParent( const std::vector& sourceItemFiles, const std::vector& bankFiles); -} // namespace reasampler +} // namespace reasampler::model diff --git a/src/core/model/slot_map.cpp b/src/core/model/slot_map.cpp new file mode 100644 index 0000000..2d60da7 --- /dev/null +++ b/src/core/model/slot_map.cpp @@ -0,0 +1,145 @@ +#include "core/model/slot_map.h" + +#include + +#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 SlotMap::orderedIds() const { + // entries_ is sorted ascending by slot, so a straight walk is display order. + std::vector 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& 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& 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>& 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 diff --git a/src/core/model/slot_map.h b/src/core/model/slot_map.h new file mode 100644 index 0000000..59f550a --- /dev/null +++ b/src/core/model/slot_map.h @@ -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 +#include +#include +#include + +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 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& 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& 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>& 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 entries_; // kept sorted ascending by slot (invariant) + + void sortBySlot(); +}; + +} // namespace reasampler::model diff --git a/src/core/namespaces.h b/src/core/namespaces.h new file mode 100644 index 0000000..fd5431d --- /dev/null +++ b/src/core/namespaces.h @@ -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 diff --git a/src/prune_reconcile.cpp b/src/core/reclaim/prune_reconcile.cpp similarity index 97% rename from src/prune_reconcile.cpp rename to src/core/reclaim/prune_reconcile.cpp index d605aad..464c6b9 100644 --- a/src/prune_reconcile.cpp +++ b/src/core/reclaim/prune_reconcile.cpp @@ -1,4 +1,4 @@ -#include "prune_reconcile.h" +#include "core/reclaim/prune_reconcile.h" #include @@ -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 pruneOrphans(const std::vector& present, const std::vector& referenced, @@ -83,4 +83,4 @@ std::vector pruneDeletePlan(const std::vector& confirm return plan; } -} // namespace reasampler +} // namespace reasampler::reclaim diff --git a/src/prune_reconcile.h b/src/core/reclaim/prune_reconcile.h similarity index 99% rename from src/prune_reconcile.h rename to src/core/reclaim/prune_reconcile.h index a44660c..e744827 100644 --- a/src/prune_reconcile.h +++ b/src/core/reclaim/prune_reconcile.h @@ -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 #include -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& orphans, std::vector pruneDeletePlan(const std::vector& confirmed, const std::vector& freshOrphans); -} // namespace reasampler +} // namespace reasampler::reclaim diff --git a/src/action_bar.cpp b/src/core/ui/action_bar.cpp similarity index 98% rename from src/action_bar.cpp rename to src/core/ui/action_bar.cpp index 3f7535c..36da30b 100644 --- a/src/action_bar.cpp +++ b/src/core/ui/action_bar.cpp @@ -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 -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 diff --git a/src/action_bar.h b/src/core/ui/action_bar.h similarity index 96% rename from src/action_bar.h rename to src/core/ui/action_bar.h index 8e48a7e..bb754c2 100644 --- a/src/action_bar.h +++ b/src/core/ui/action_bar.h @@ -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 -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 computeBarSlots(const ActionBarRect& bar, int hitTestActionBar(int px, int py, const ActionBarRect& bar, const std::vector& clusters, const ActionBarSpec& spec); -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/bank_grid.cpp b/src/core/ui/bank_grid.cpp similarity index 98% rename from src/bank_grid.cpp rename to src/core/ui/bank_grid.cpp index 1cc3a53..2522527 100644 --- a/src/bank_grid.cpp +++ b/src/core/ui/bank_grid.cpp @@ -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 #include -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 diff --git a/src/bank_grid.h b/src/core/ui/bank_grid.h similarity index 97% rename from src/bank_grid.h rename to src/core/ui/bank_grid.h index 3616e96..6c4dd00 100644 --- a/src/bank_grid.h +++ b/src/core/ui/bank_grid.h @@ -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 #include -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 diff --git a/src/card_drag.cpp b/src/core/ui/card_drag.cpp similarity index 97% rename from src/card_drag.cpp rename to src/core/ui/card_drag.cpp index afe5971..6f14fd9 100644 --- a/src/card_drag.cpp +++ b/src/core/ui/card_drag.cpp @@ -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& rects) { return -1; } -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/card_drag.h b/src/core/ui/card_drag.h similarity index 98% rename from src/card_drag.h rename to src/core/ui/card_drag.h index ef0681b..e64ce05 100644 --- a/src/card_drag.h +++ b/src/core/ui/card_drag.h @@ -30,10 +30,10 @@ #include -#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 computeSlotRectsForDrop(int maxSlot, int panelWidth, // callers reason in model slots. int hitTestSlot(int px, int py, const std::vector& rects); -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/card_meta.cpp b/src/core/ui/card_meta.cpp similarity index 96% rename from src/card_meta.cpp rename to src/core/ui/card_meta.cpp index b2e44e9..8987dbf 100644 --- a/src/card_meta.cpp +++ b/src/core/ui/card_meta.cpp @@ -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 #include -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 diff --git a/src/card_meta.h b/src/core/ui/card_meta.h similarity index 98% rename from src/card_meta.h rename to src/core/ui/card_meta.h index 1c97fad..a561cd3 100644 --- a/src/card_meta.h +++ b/src/core/ui/card_meta.h @@ -11,7 +11,7 @@ #include -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 diff --git a/src/component_geometry.cpp b/src/core/ui/component_geometry.cpp similarity index 97% rename from src/component_geometry.cpp rename to src/core/ui/component_geometry.cpp index 65c1d0e..3e9d521 100644 --- a/src/component_geometry.cpp +++ b/src/core/ui/component_geometry.cpp @@ -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 diff --git a/src/component_geometry.h b/src/core/ui/component_geometry.h similarity index 95% rename from src/component_geometry.h rename to src/core/ui/component_geometry.h index ff8e30f..7455c47 100644 --- a/src/component_geometry.h +++ b/src/core/ui/component_geometry.h @@ -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 diff --git a/src/drag_out.cpp b/src/core/ui/drag_out.cpp similarity index 95% rename from src/drag_out.cpp rename to src/core/ui/drag_out.cpp index db58c65..06a10e3 100644 --- a/src/drag_out.cpp +++ b/src/core/ui/drag_out.cpp @@ -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 -namespace reasampler { +namespace reasampler::ui { namespace { @@ -51,4 +51,4 @@ PathList assemblePathList(const std::vector& resolved) { return out; } -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/drag_out.h b/src/core/ui/drag_out.h similarity index 96% rename from src/drag_out.h rename to src/core/ui/drag_out.h index eacf47f..a24dd2c 100644 --- a/src/drag_out.h +++ b/src/core/ui/drag_out.h @@ -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 #include -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& resolved); -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/footer_bar.cpp b/src/core/ui/footer_bar.cpp similarity index 96% rename from src/footer_bar.cpp rename to src/core/ui/footer_bar.cpp index b1e6824..e4b683f 100644 --- a/src/footer_bar.cpp +++ b/src/core/ui/footer_bar.cpp @@ -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 diff --git a/src/footer_bar.h b/src/core/ui/footer_bar.h similarity index 88% rename from src/footer_bar.h rename to src/core/ui/footer_bar.h index bc24342..4d1e58f 100644 --- a/src/footer_bar.h +++ b/src/core/ui/footer_bar.h @@ -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 diff --git a/src/mode_enable.cpp b/src/core/ui/mode_enable.cpp similarity index 77% rename from src/mode_enable.cpp rename to src/core/ui/mode_enable.cpp index 84df3ce..0f31bd8 100644 --- a/src/mode_enable.cpp +++ b/src/core/ui/mode_enable.cpp @@ -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 diff --git a/src/mode_enable.h b/src/core/ui/mode_enable.h similarity index 97% rename from src/mode_enable.h rename to src/core/ui/mode_enable.h index 8391a46..0132f5e 100644 --- a/src/mode_enable.h +++ b/src/core/ui/mode_enable.h @@ -17,7 +17,7 @@ #include -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 diff --git a/src/overflow_menu.cpp b/src/core/ui/overflow_menu.cpp similarity index 94% rename from src/overflow_menu.cpp rename to src/core/ui/overflow_menu.cpp index 04b1d3b..e6dd9b6 100644 --- a/src/overflow_menu.cpp +++ b/src/core/ui/overflow_menu.cpp @@ -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 diff --git a/src/overflow_menu.h b/src/core/ui/overflow_menu.h similarity index 87% rename from src/overflow_menu.h rename to src/core/ui/overflow_menu.h index 88764fc..31972ee 100644 --- a/src/overflow_menu.h +++ b/src/core/ui/overflow_menu.h @@ -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 diff --git a/src/prune_button.cpp b/src/core/ui/prune_button.cpp similarity index 94% rename from src/prune_button.cpp rename to src/core/ui/prune_button.cpp index 424c327..71db3a2 100644 --- a/src/prune_button.cpp +++ b/src/core/ui/prune_button.cpp @@ -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 diff --git a/src/prune_button.h b/src/core/ui/prune_button.h similarity index 90% rename from src/prune_button.h rename to src/core/ui/prune_button.h index ff0e414..fbd5b3a 100644 --- a/src/prune_button.h +++ b/src/core/ui/prune_button.h @@ -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 diff --git a/src/core/ui/rect.h b/src/core/ui/rect.h new file mode 100644 index 0000000..92e64bd --- /dev/null +++ b/src/core/ui/rect.h @@ -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 diff --git a/src/tab_strip.cpp b/src/core/ui/tab_strip.cpp similarity index 98% rename from src/tab_strip.cpp rename to src/core/ui/tab_strip.cpp index 68c57a2..c0d4db7 100644 --- a/src/tab_strip.cpp +++ b/src/core/ui/tab_strip.cpp @@ -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 -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 diff --git a/src/tab_strip.h b/src/core/ui/tab_strip.h similarity index 95% rename from src/tab_strip.h rename to src/core/ui/tab_strip.h index 7932070..4f05ee9 100644 --- a/src/tab_strip.h +++ b/src/core/ui/tab_strip.h @@ -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 -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 diff --git a/src/theme.cpp b/src/core/ui/theme.cpp similarity index 99% rename from src/theme.cpp rename to src/core/ui/theme.cpp index 5e4bad3..e80ee62 100644 --- a/src/theme.cpp +++ b/src/core/ui/theme.cpp @@ -1,11 +1,11 @@ // theme — pure implementation. See theme.h. NO REAPER / SWELL / LICE / vendor. -#include "theme.h" +#include "core/ui/theme.h" #include #include -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 diff --git a/src/theme.h b/src/core/ui/theme.h similarity index 99% rename from src/theme.h rename to src/core/ui/theme.h index 2137113..0d6f5ba 100644 --- a/src/theme.h +++ b/src/core/ui/theme.h @@ -24,7 +24,7 @@ #include -namespace reasampler { +namespace reasampler::ui { // A straight 8-bit-per-channel RGBA color, LICE-free. The draw shell converts this to a // LICE_pixel via LICE_RGBA at the boundary (draw_kit); nothing here depends on LICE's @@ -115,4 +115,4 @@ double contrastRatio(const KitColor& a, const KitColor& b); // pair the kit actually draws. double textFloor(TextClass cls); -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/tooltip.cpp b/src/core/ui/tooltip.cpp similarity index 95% rename from src/tooltip.cpp rename to src/core/ui/tooltip.cpp index b9d902a..5f09110 100644 --- a/src/tooltip.cpp +++ b/src/core/ui/tooltip.cpp @@ -1,8 +1,8 @@ // tooltip — pure implementation. See tooltip.h. NO REAPER / SWELL / LICE / vendor. -#include "tooltip.h" +#include "core/ui/tooltip.h" -namespace reasampler { +namespace reasampler::ui { std::string stripActionPrefix(const std::string& fullName, const std::string& prefix) { if (prefix.empty()) return fullName; @@ -50,4 +50,4 @@ TooltipBox computeTooltip(int anchorX, int anchorY, int anchorW, int anchorH, return box; } -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/tooltip.h b/src/core/ui/tooltip.h similarity index 98% rename from src/tooltip.h rename to src/core/ui/tooltip.h index 38be963..1682597 100644 --- a/src/tooltip.h +++ b/src/core/ui/tooltip.h @@ -13,7 +13,7 @@ #include -namespace reasampler { +namespace reasampler::ui { // The tooltip's box (top-left origin, SWELL/LICE convention). A zero-area rect means "do not // draw" (degenerate inputs); the caller checks empty() before drawing. @@ -59,4 +59,4 @@ TooltipBox computeTooltip(int anchorX, int anchorY, int anchorW, int anchorH, int textW, int textH, int clientW, int clientH, const TooltipSpec& spec); -} // namespace reasampler +} // namespace reasampler::ui diff --git a/src/core/util/clamp01.h b/src/core/util/clamp01.h new file mode 100644 index 0000000..0a141a0 --- /dev/null +++ b/src/core/util/clamp01.h @@ -0,0 +1,14 @@ +#pragma once +// clamp01 — the ONE unit-interval clamp (Q-W1, T4-24). Replaces the per-module +// static copies (master_gain / param_slider / envelope_overlay / reasampler_editor). +// Deliberately the ternary form: comparisons with NaN are false, so a NaN input +// passes through unchanged rather than silently collapsing to a bound — the +// behavior of the majority of the retired copies. +// +// PURE: standard library only (not even that). + +namespace reasampler::util { + +constexpr double clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); } + +} // namespace reasampler::util diff --git a/src/core/util/file_bytes.cpp b/src/core/util/file_bytes.cpp new file mode 100644 index 0000000..b66608c --- /dev/null +++ b/src/core/util/file_bytes.cpp @@ -0,0 +1,21 @@ +// core/util/file_bytes implementation — see file_bytes.h. + +#include "core/util/file_bytes.h" + +#include + +namespace reasampler::util { + +std::vector readFileBytes(const std::string& path) { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) return {}; + const std::streamoff size = f.tellg(); + if (size <= 0) return {}; + std::vector bytes(static_cast(size)); + f.seekg(0); + f.read(reinterpret_cast(bytes.data()), size); + if (!f) return {}; + return bytes; +} + +} // namespace reasampler::util diff --git a/src/core/util/file_bytes.h b/src/core/util/file_bytes.h new file mode 100644 index 0000000..5214850 --- /dev/null +++ b/src/core/util/file_bytes.h @@ -0,0 +1,19 @@ +// core/util/file_bytes — the ONE whole-file byte loader (Q-W1; audit T2-03). +// Pure standard library — NO REAPER, NO SWELL, NO VST3 — but it does blocking +// file I/O: NEVER call it on the audio thread (off-thread only, the same rule +// every prior hand-rolled copy carried). Linked by both artifacts. + +#pragma once + +#include +#include +#include + +namespace reasampler::util { + +// Reads the whole file at `path` into a byte buffer. Empty on ANY failure — +// unopenable, empty file, or short read — so the caller has exactly one +// "nothing to work with" branch. +std::vector readFileBytes(const std::string& path); + +} // namespace reasampler::util diff --git a/src/app_version.cpp b/src/core/version/app_version.cpp similarity index 98% rename from src/app_version.cpp rename to src/core/version/app_version.cpp index 247c24a..568f02d 100644 --- a/src/app_version.cpp +++ b/src/core/version/app_version.cpp @@ -6,13 +6,13 @@ // parse/compare/classify logic (V1). No #ifdef forks leak beyond this file; the shells // consume the accessors below, so channel identity is one auditable definition. -#include "app_version.h" +#include "core/version/app_version.h" #include #include "version_generated.h" // REASAMPLER_VERSION_STRING + REASAMPLER_CHANNEL_IS_BETA -namespace reasampler { +namespace reasampler::version { namespace { // The one channel predicate every derivation below branches on — the single point the @@ -166,4 +166,4 @@ WritingVersion classifyWritingVersion(const std::string& rawStamp) { return wv; } -} // namespace reasampler +} // namespace reasampler::version diff --git a/src/app_version.h b/src/core/version/app_version.h similarity index 99% rename from src/app_version.h rename to src/core/version/app_version.h index f940e6f..603dcf1 100644 --- a/src/app_version.h +++ b/src/core/version/app_version.h @@ -36,7 +36,7 @@ #include #include -namespace reasampler { +namespace reasampler::version { // --- Channel identity (V4, beta-in-isolation) --------------------------------------- // @@ -199,4 +199,4 @@ struct WritingVersion { // with the raw ext-state read and never has to reason about the cases itself. WritingVersion classifyWritingVersion(const std::string& rawStamp); -} // namespace reasampler +} // namespace reasampler::version diff --git a/src/version_generated.h.in b/src/core/version/version_generated.h.in similarity index 100% rename from src/version_generated.h.in rename to src/core/version/version_generated.h.in diff --git a/src/guid_diff.cpp b/src/core/view/guid_diff.cpp similarity index 94% rename from src/guid_diff.cpp rename to src/core/view/guid_diff.cpp index 9a8253c..303f94e 100644 --- a/src/guid_diff.cpp +++ b/src/core/view/guid_diff.cpp @@ -1,11 +1,11 @@ // guid_diff implementation — pure set arithmetic for new-content detection. See // guid_diff.h. No REAPER, no SWELL — std only. -#include "guid_diff.h" +#include "core/view/guid_diff.h" #include -namespace reasampler { +namespace reasampler::view { std::vector newGuids(const std::set& previous, const std::set& current) { @@ -41,4 +41,4 @@ void GuidBaseline::reset() { primed_ = false; // next observe() re-baselines (first-poll guard re-armed) } -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/guid_diff.h b/src/core/view/guid_diff.h similarity index 98% rename from src/guid_diff.h rename to src/core/view/guid_diff.h index fc4d4c8..5026ec2 100644 --- a/src/guid_diff.h +++ b/src/core/view/guid_diff.h @@ -17,7 +17,7 @@ #include #include -namespace reasampler { +namespace reasampler::view { // The GUIDs present in `current` but absent from `previous` — i.e. new since the // previous poll. Order is the set's ascending order (deterministic; the caller does @@ -59,4 +59,4 @@ private: bool primed_ = false; // false ⇒ next observe() sets the baseline }; -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/lane_keys.cpp b/src/core/view/lane_keys.cpp similarity index 95% rename from src/lane_keys.cpp rename to src/core/view/lane_keys.cpp index 41dcf41..b003925 100644 --- a/src/lane_keys.cpp +++ b/src/core/view/lane_keys.cpp @@ -1,10 +1,10 @@ // lane_keys implementation — pure string convention, no REAPER. See lane_keys.h. -#include "lane_keys.h" +#include "core/view/lane_keys.h" #include -namespace reasampler { +namespace reasampler::view { namespace { // Does `s` start with the managed-lane prefix? @@ -48,4 +48,4 @@ bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName) { return !hasManagedPrefix(laneName); } -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/lane_keys.h b/src/core/view/lane_keys.h similarity index 98% rename from src/lane_keys.h rename to src/core/view/lane_keys.h index a2a6446..36cb133 100644 --- a/src/lane_keys.h +++ b/src/core/view/lane_keys.h @@ -33,7 +33,7 @@ #include #include -namespace reasampler { +namespace reasampler::view { // The prefix the tool stamps on every lane NAME it mints. A lane name carrying this // prefix is a managed lane the tool created; any other name (or an empty/unnamed lane) @@ -82,4 +82,4 @@ std::optional modeIdFromLaneName(const std::string& laneName); // inputs (I_FREEMODE result, P_LANENAME string) and never re-derives this logic. bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName); -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/mode_switch.cpp b/src/core/view/mode_switch.cpp similarity index 96% rename from src/mode_switch.cpp rename to src/core/view/mode_switch.cpp index 204c481..6bc8c57 100644 --- a/src/mode_switch.cpp +++ b/src/core/view/mode_switch.cpp @@ -1,10 +1,10 @@ // mode_switch — pure implementation. See mode_switch.h. NO REAPER / SWELL / vendor. -#include "mode_switch.h" +#include "core/view/mode_switch.h" #include -namespace reasampler { +namespace reasampler::view { namespace { @@ -61,4 +61,4 @@ int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount) { return -1; } -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/mode_switch.h b/src/core/view/mode_switch.h similarity index 84% rename from src/mode_switch.h rename to src/core/view/mode_switch.h index 4813a8c..44b8250 100644 --- a/src/mode_switch.h +++ b/src/core/view/mode_switch.h @@ -1,4 +1,5 @@ #pragma once +#include "core/ui/rect.h" // mode_switch — the REAPER-free layout math behind the bank_panel's Design-View // mode switch (Phase D, Wave 4 — D5). A segmented control `[ Arrange | Design ]` // (N-mode general, one segment per registered mode) drawn in a fixed-height header @@ -13,35 +14,17 @@ #include -namespace reasampler { +namespace reasampler::view { // The header strip the switch is 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 at the top of its client area and offsets the grid below. -struct HeaderRect { - int x = 0; - int y = 0; - int width = 0; - int height = 0; - - bool operator==(const HeaderRect& o) const { - return x == o.x && y == o.y && width == o.width && height == o.height; - } -}; +using HeaderRect = ui::Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased // One segment's pixel rectangle within the header, top-left origin. These are the // draw bounds for one mode's button; the panel draws the mode's display name inside // it and lights it when it is the active mode. -struct SegmentRect { - int x = 0; - int y = 0; - int width = 0; - int height = 0; - - bool operator==(const SegmentRect& o) const { - return x == o.x && y == o.y && width == o.width && height == o.height; - } -}; +using SegmentRect = ui::Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h), role-aliased // Divides `header` into `segmentCount` equal segments left-to-right, in the caller's // order (the panel passes modes in ordinal order). Returns exactly segmentCount @@ -63,4 +46,4 @@ std::vector computeSegmentRects(const HeaderRect& header, // the panel drew there. int hitTestSegment(int px, int py, const HeaderRect& header, int segmentCount); -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/view_mode_model.cpp b/src/core/view/view_mode_model.cpp similarity index 71% rename from src/view_mode_model.cpp rename to src/core/view/view_mode_model.cpp index 9b6af0b..f2cfbfc 100644 --- a/src/view_mode_model.cpp +++ b/src/core/view/view_mode_model.cpp @@ -1,20 +1,17 @@ -#include "view_mode_model.h" +#include "core/view/view_mode_model.h" #include #include -#include -#include -#include -#include #include #include -#include "lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention +#include "core/json/json.h" +#include "core/view/lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention // view_mode_model implementation. // -// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief: -// keep the pure core dependency-free — no third-party JSON lib). A compact writer +// JSON rides on the shared core/json lexical layer (Q-W1), mirroring bank_model. +// A compact writer // plus a recursive-descent parser covers the field set: the mode registry, the // GUID-keyed membership map, per-track snapshots (with a variable-length per-FX // offline vector), and the active mode. Ints are emitted plainly; strings are @@ -22,6 +19,10 @@ namespace reasampler { +// Q-W1 interim: laneNameForMode lives in reasampler::view now; this god module +// re-namespaces in its own split wave. +using view::laneNameForMode; + // --------------------------------------------------------------------------- // equality // --------------------------------------------------------------------------- @@ -493,73 +494,12 @@ bool ViewModeModel::operator==(const ViewModeModel& o) const { namespace { -void writeEscaped(std::string& out, const std::string& s) { - out += '"'; - for (char c : s) { - switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\b': out += "\\b"; break; - case '\f': out += "\\f"; break; - case '\n': out += "\\n"; break; - case '\r': out += "\\r"; break; - case '\t': out += "\\t"; break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); - out += buf; - } else { - out += c; - } - } - } - out += '"'; -} - -std::string intToStr(int v) { - char buf[16]; - std::snprintf(buf, sizeof(buf), "%d", v); - return buf; -} - -void writeIntArray(std::string& out, const std::vector& v) { - out += '['; - for (std::size_t i = 0; i < v.size(); ++i) { - if (i) out += ','; - out += intToStr(v[i]); - } - out += ']'; -} - -class ObjWriter { -public: - explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; } - ~ObjWriter() { out_ += '}'; } - - void keyRaw(const char* key, const std::string& rawValue) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - out_ += rawValue; - } - void keyStr(const char* key, const std::string& value) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - writeEscaped(out_, value); - } - void keyBegin(const char* key) { - sep(); - writeEscaped(out_, key); - out_ += ':'; - } - -private: - void sep() { if (first_) first_ = false; else out_ += ','; } - std::string& out_; - bool first_ = true; -}; +// Shared core/json emit helpers (Q-W1): same escape set + %d rendering as the +// prior file-local writer, so the emitted blob is byte-identical. +using json::writeEscaped; +using json::writeIntArray; +std::string intToStr(int v) { return json::numToStr(v); } +using ObjWriter = json::Writer; } // namespace @@ -660,250 +600,64 @@ std::string ViewModeModel::serialize() const { namespace { -class Parser { -public: - explicit Parser(const std::string& s) : s_(s) {} - bool parseModel(ViewModeModel& out); - -private: - const std::string& s_; - std::size_t pos_ = 0; - - bool eof() const { return pos_ >= s_.size(); } - - void skipWs() { - while (!eof()) { - char c = s_[pos_]; - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; - else break; - } - } - bool consume(char c) { - skipWs(); - if (eof() || s_[pos_] != c) return false; - ++pos_; - return true; - } - bool parseString(std::string& out); - bool parseRawScalar(std::string& out); - bool parseInt(int& out); - bool parseBool(bool& out); - bool parseKey(std::string& key); - bool skipValue(); - - bool parseModes(ModeRegistry& reg); - bool parseMembership(MembershipIndex& idx); - bool parseSnapshots(std::map& snaps); - bool parseLanes(LaneOwnershipIndex& idx); - bool parseIntArray(std::vector& out); -}; - -bool Parser::parseString(std::string& out) { - skipWs(); - if (eof() || s_[pos_] != '"') return false; - ++pos_; - out.clear(); - while (!eof()) { - char c = s_[pos_++]; - if (c == '"') return true; - if (c == '\\') { - if (eof()) return false; - char e = s_[pos_++]; - switch (e) { - case '"': out += '"'; break; - case '\\': out += '\\'; break; - case '/': out += '/'; break; - case 'b': out += '\b'; break; - case 'f': out += '\f'; break; - case 'n': out += '\n'; break; - case 'r': out += '\r'; break; - case 't': out += '\t'; break; - case 'u': { - auto readHex4 = [&](unsigned int& cp) -> bool { - if (pos_ + 4 > s_.size()) return false; - cp = 0; - for (int i = 0; i < 4; ++i) { - char h = s_[pos_++]; - cp <<= 4; - if (h >= '0' && h <= '9') cp |= static_cast(h - '0'); - else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); - else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10); - else return false; - } - return true; - }; - unsigned int hi = 0; - if (!readHex4(hi)) return false; - unsigned int codePoint = hi; - if (hi >= 0xD800 && hi <= 0xDBFF) { - if (pos_ + 6 > s_.size()) return false; - if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false; - pos_ += 2; - unsigned int lo = 0; - if (!readHex4(lo)) return false; - if (lo < 0xDC00 || lo > 0xDFFF) return false; - codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00); - } else if (hi >= 0xDC00 && hi <= 0xDFFF) { - return false; - } - if (codePoint <= 0x7F) { - out += static_cast(codePoint); - } else if (codePoint <= 0x7FF) { - out += static_cast(0xC0 | (codePoint >> 6)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else if (codePoint <= 0xFFFF) { - out += static_cast(0xE0 | (codePoint >> 12)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else { - out += static_cast(0xF0 | (codePoint >> 18)); - out += static_cast(0x80 | ((codePoint >> 12) & 0x3F)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } - break; - } - default: return false; - } - } else { - out += c; - } - } - return false; // unterminated -} - -bool Parser::parseRawScalar(std::string& out) { - skipWs(); - std::size_t start = pos_; - while (!eof()) { - char c = s_[pos_]; - if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' || - c == '\n' || c == '\r') - break; - ++pos_; - } - if (pos_ == start) return false; - out.assign(s_, start, pos_ - start); - return true; -} - -bool Parser::parseInt(int& out) { - std::string tok; - if (!parseRawScalar(tok)) return false; - const char* b = tok.c_str(); - char* end = nullptr; - errno = 0; - long long v = std::strtoll(b, &end, 10); - if (end != b + tok.size()) return false; - if (errno == ERANGE) return false; - if (v < INT_MIN || v > INT_MAX) return false; - out = static_cast(v); - return true; -} - -bool Parser::parseBool(bool& out) { - std::string tok; - if (!parseRawScalar(tok)) return false; - if (tok == "true") { out = true; return true; } - if (tok == "false") { out = false; return true; } - return false; -} - -bool Parser::parseKey(std::string& key) { - if (!parseString(key)) return false; - return consume(':'); -} - -bool Parser::skipValue() { - skipWs(); - if (eof()) return false; - char c = s_[pos_]; - if (c == '"') { std::string tmp; return parseString(tmp); } - if (c == '{' || c == '[') { - char open = c, close = (c == '{') ? '}' : ']'; - ++pos_; - int depth = 1; - while (!eof() && depth > 0) { - char d = s_[pos_]; - if (d == '"') { std::string tmp; if (!parseString(tmp)) return false; continue; } - if (d == open) ++depth; - else if (d == close) --depth; - ++pos_; - } - return depth == 0; - } - std::string tmp; - return parseRawScalar(tmp); -} - -bool Parser::parseIntArray(std::vector& out) { - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; - do { - int v = 0; - if (!parseInt(v)) return false; - out.push_back(v); - } while (consume(',')); - return consume(']'); -} +// The model DOMAIN grammar over the shared core/json lexical layer (Q-W1). // The registry starts seeded (Arrange + Design). Deserialization must reproduce the // serialized set exactly, so we replace the seeded contents with the parsed ones — // add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as // duplicates and the ordinals/names would not round-trip. We therefore parse into a // fresh vector and swap. `reg` is passed empty (see parseModel). -bool Parser::parseModes(ModeRegistry& reg) { - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; // empty array (unusual, but valid) +bool parseModes(json::Reader& r, ModeRegistry& reg) { + if (!r.consume('[')) return false; + r.skipWs(); + if (r.consume(']')) return true; // empty array (unusual, but valid) do { - if (!consume('{')) return false; + if (!r.consume('{')) return false; Mode m; bool haveId = false; do { std::string k; - if (!parseKey(k)) return false; - if (k == "id") { if (!parseString(m.id)) return false; haveId = true; } - else if (k == "displayName") { if (!parseString(m.displayName)) return false; } - else if (k == "ordinal") { if (!parseInt(m.ordinal)) return false; } - else if (!skipValue()) return false; - } while (consume(',')); - if (!consume('}')) return false; + if (!r.parseKey(k)) return false; + if (k == "id") { if (!r.parseString(m.id)) return false; haveId = true; } + else if (k == "displayName") { if (!r.parseString(m.displayName)) return false; } + else if (k == "ordinal") { if (!r.parseInt(m.ordinal)) return false; } + else if (!r.skipValue()) return false; + } while (r.consume(',')); + if (!r.consume('}')) return false; if (!haveId || !reg.add(m)) return false; // malformed / duplicate id - } while (consume(',')); - return consume(']'); + } while (r.consume(',')); + return r.consume(']'); } -bool Parser::parseMembership(MembershipIndex& idx) { - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; +bool parseMembership(json::Reader& r, MembershipIndex& idx) { + if (!r.consume('[')) return false; + r.skipWs(); + if (r.consume(']')) return true; do { - if (!consume('{')) return false; + if (!r.consume('{')) return false; std::string guid; Membership mem; bool haveGuid = false; do { std::string k; - if (!parseKey(k)) return false; - if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; } + if (!r.parseKey(k)) return false; + if (k == "guid") { if (!r.parseString(guid)) return false; haveGuid = true; } else if (k == "modes") { - if (!consume('[')) return false; - skipWs(); - if (!consume(']')) { + if (!r.consume('[')) return false; + r.skipWs(); + if (!r.consume(']')) { do { std::string id; - if (!parseString(id)) return false; + if (!r.parseString(id)) return false; mem.modeIds.insert(id); - } while (consume(',')); - if (!consume(']')) return false; + } while (r.consume(',')); + if (!r.consume(']')) return false; } } - else if (k == "showBoth") { if (!parseBool(mem.showBoth)) return false; } - else if (!skipValue()) return false; - } while (consume(',')); - if (!consume('}')) return false; + else if (k == "showBoth") { if (!r.parseBool(mem.showBoth)) return false; } + else if (!r.skipValue()) return false; + } while (r.consume(',')); + if (!r.consume('}')) return false; if (!haveGuid || guid.empty()) return false; // Install the entry verbatim (tag() would clear a multi-mode set and drop // show-both). A serialized entry is trusted to already satisfy the model's @@ -918,55 +672,55 @@ bool Parser::parseMembership(MembershipIndex& idx) { // mode that no longer exists has an immediate behavioral consequence, so it // is caught and the parse is rejected. if (!idx.restore(guid, mem)) return false; - } while (consume(',')); - return consume(']'); + } while (r.consume(',')); + return r.consume(']'); } -bool Parser::parseSnapshots(std::map& snaps) { - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; +bool parseSnapshots(json::Reader& r, std::map& snaps) { + if (!r.consume('[')) return false; + r.skipWs(); + if (r.consume(']')) return true; do { - if (!consume('{')) return false; + if (!r.consume('{')) return false; std::string guid; TrackSnapshot snap; bool haveGuid = false; do { std::string k; - if (!parseKey(k)) return false; - if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; } - else if (k == "showInTcp") { if (!parseInt(snap.showInTcp)) return false; } - else if (k == "showInMixer") { if (!parseInt(snap.showInMixer)) return false; } - else if (k == "mainSend") { if (!parseInt(snap.mainSend)) return false; } - else if (k == "fxEnable") { if (!parseInt(snap.fxEnable)) return false; } - else if (k == "fxOffline") { if (!parseIntArray(snap.fxOffline)) return false; } - else if (!skipValue()) return false; - } while (consume(',')); - if (!consume('}')) return false; + if (!r.parseKey(k)) return false; + if (k == "guid") { if (!r.parseString(guid)) return false; haveGuid = true; } + else if (k == "showInTcp") { if (!r.parseInt(snap.showInTcp)) return false; } + else if (k == "showInMixer") { if (!r.parseInt(snap.showInMixer)) return false; } + else if (k == "mainSend") { if (!r.parseInt(snap.mainSend)) return false; } + else if (k == "fxEnable") { if (!r.parseInt(snap.fxEnable)) return false; } + else if (k == "fxOffline") { if (!r.parseIntArray(snap.fxOffline)) return false; } + else if (!r.skipValue()) return false; + } while (r.consume(',')); + if (!r.consume('}')) return false; if (!haveGuid || guid.empty()) return false; snaps[guid] = snap; - } while (consume(',')); - return consume(']'); + } while (r.consume(',')); + return r.consume(']'); } -bool Parser::parseLanes(LaneOwnershipIndex& idx) { - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; +bool parseLanes(json::Reader& r, LaneOwnershipIndex& idx) { + if (!r.consume('[')) return false; + r.skipWs(); + if (r.consume(']')) return true; do { - if (!consume('{')) return false; + if (!r.consume('{')) return false; std::string trackGuid, laneKey, mode; bool haveTrack = false, haveLane = false, managed = false, haveManaged = false; do { std::string k; - if (!parseKey(k)) return false; - if (k == "trackGuid") { if (!parseString(trackGuid)) return false; haveTrack = true; } - else if (k == "laneKey") { if (!parseString(laneKey)) return false; haveLane = true; } - else if (k == "managed") { if (!parseBool(managed)) return false; haveManaged = true; } - else if (k == "mode") { if (!parseString(mode)) return false; } - else if (!skipValue()) return false; - } while (consume(',')); - if (!consume('}')) return false; + if (!r.parseKey(k)) return false; + if (k == "trackGuid") { if (!r.parseString(trackGuid)) return false; haveTrack = true; } + else if (k == "laneKey") { if (!r.parseString(laneKey)) return false; haveLane = true; } + else if (k == "managed") { if (!r.parseBool(managed)) return false; haveManaged = true; } + else if (k == "mode") { if (!r.parseString(mode)) return false; } + else if (!r.skipValue()) return false; + } while (r.consume(',')); + if (!r.consume('}')) return false; // Both keys mandatory and non-empty (they form the lane's identity). A managed // lane must carry a non-empty mode; a manual lane must not claim one. Enforcing // this on parse keeps a round-tripped index byte-for-byte identical to the @@ -980,14 +734,14 @@ bool Parser::parseLanes(LaneOwnershipIndex& idx) { if (!mode.empty()) return false; // manual lane must not carry a mode if (!idx.setManual(trackGuid, laneKey)) return false; } - } while (consume(',')); - return consume(']'); + } while (r.consume(',')); + return r.consume(']'); } -bool Parser::parseModel(ViewModeModel& out) { - if (!consume('{')) return false; - skipWs(); - if (consume('}')) return true; // lenient empty root ⇒ default-seeded model +bool parseModel(json::Reader& r, ViewModeModel& out) { + if (!r.consume('{')) return false; + r.skipWs(); + if (r.consume('}')) return true; // lenient empty root ⇒ default-seeded model ModeRegistry reg; // seeded default; REPLACED if a modes array is present bool haveModes = false; @@ -999,33 +753,33 @@ bool Parser::parseModel(ViewModeModel& out) { do { std::string key; - if (!parseKey(key)) return false; + if (!r.parseKey(key)) return false; if (key == "activeMode") { - if (!parseString(activeMode)) return false; + if (!r.parseString(activeMode)) return false; haveActive = true; } else if (key == "modes") { ModeRegistry fresh = ModeRegistry::makeEmpty(); // parse into empty, then own - if (!parseModes(fresh)) return false; + if (!parseModes(r, fresh)) return false; reg = fresh; haveModes = true; } else if (key == "membership") { - if (!parseMembership(membership)) return false; + if (!parseMembership(r, membership)) return false; } else if (key == "snapshots") { - if (!parseSnapshots(snaps)) return false; + if (!parseSnapshots(r, snaps)) return false; } else if (key == "lanes") { - if (!parseLanes(lanes)) return false; + if (!parseLanes(r, lanes)) return false; } else { // Unknown keys and the "version" field are skipped here. // "version" is serialized as a forward-compat placeholder — there is no // active version gate yet; all persisted data is parsed the same way // regardless of the value. A future gate would add a version branch here. - if (!skipValue()) return false; + if (!r.skipValue()) return false; } - } while (consume(',')); + } while (r.consume(',')); - if (!consume('}')) return false; - skipWs(); - if (!eof()) return false; // trailing garbage + if (!r.consume('}')) return false; + r.skipWs(); + if (!r.eof()) return false; // trailing garbage if (haveModes) out.modes() = reg; out.membership() = membership; @@ -1039,10 +793,10 @@ bool Parser::parseModel(ViewModeModel& out) { } // namespace -std::optional ViewModeModel::deserialize(const std::string& json) { +std::optional ViewModeModel::deserialize(const std::string& blob) { ViewModeModel vm; - Parser p(json); - if (!p.parseModel(vm)) return std::nullopt; + json::Reader r(blob); + if (!parseModel(r, vm)) return std::nullopt; return vm; } diff --git a/src/view_mode_model.h b/src/core/view/view_mode_model.h similarity index 100% rename from src/view_mode_model.h rename to src/core/view/view_mode_model.h diff --git a/src/view_tree.cpp b/src/core/view/view_tree.cpp similarity index 93% rename from src/view_tree.cpp rename to src/core/view/view_tree.cpp index 3f5a966..6356ec0 100644 --- a/src/view_tree.cpp +++ b/src/core/view/view_tree.cpp @@ -1,8 +1,8 @@ // view_tree — pure folder-depth walk. See view_tree.h. -#include "view_tree.h" +#include "core/view/view_tree.h" -namespace reasampler { +namespace reasampler::view { FolderTree buildFolderTree(const std::vector& entries) { FolderTree tree; @@ -38,4 +38,4 @@ FolderTree buildFolderTree(const std::vector& entries) { return tree; } -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/view_tree.h b/src/core/view/view_tree.h similarity index 93% rename from src/view_tree.h rename to src/core/view/view_tree.h index f349d84..7d2d968 100644 --- a/src/view_tree.h +++ b/src/core/view/view_tree.h @@ -11,9 +11,9 @@ #include #include -#include "view_mode_model.h" +#include "core/view/view_mode_model.h" -namespace reasampler { +namespace reasampler::view { // One track's contribution to the folder walk, read from REAPER in arrange order. // folderDepth is I_FOLDERDEPTH verbatim: 0 = normal, 1 = folder parent (opens a @@ -30,4 +30,4 @@ struct TrackFolderEntry { // is clamped to empty) so a corrupt/stale project can never fault the shell. FolderTree buildFolderTree(const std::vector& entries); -} // namespace reasampler +} // namespace reasampler::view diff --git a/src/core/wire/assignment_request.cpp b/src/core/wire/assignment_request.cpp new file mode 100644 index 0000000..ab05c53 --- /dev/null +++ b/src/core/wire/assignment_request.cpp @@ -0,0 +1,43 @@ +// assignment_request.cpp — see assignment_request.h. Pure: standard library only. + +#include "core/wire/assignment_request.h" + +#include "core/wire/wire.h" + +namespace reasampler::wire { + +namespace { + +constexpr const char* kMagic = "rsassign1"; + +// The shared core/wire codec (Q-W1, T2-01b) — the same field grammar + hardening +// this file previously carried as its own Cursor copy. "never UB, never a +// partial value" is upheld in the codec. +using wire::putField; +using Cursor = wire::Cursor; + +} // namespace + +std::string encodeAssignmentRequest(const AssignmentRequest& req) { + std::string out = kMagic; + putField(out, req.bankId); + putField(out, req.sampleId); + putField(out, std::to_string(req.generation)); + return out; +} + +std::optional decodeAssignmentRequest(const std::string& wire) { + Cursor cur(wire); + if (!cur.literal(kMagic)) return std::nullopt; + + AssignmentRequest req; + if (!cur.field(req.bankId)) return std::nullopt; + if (!cur.field(req.sampleId)) return std::nullopt; + if (!cur.fieldInt64(req.generation)) return std::nullopt; + + // Reject trailing garbage: a well-formed value ends exactly at the last field. + if (!cur.ok() || !cur.atEnd()) return std::nullopt; + return req; +} + +} // namespace reasampler::wire diff --git a/src/assignment_request.h b/src/core/wire/assignment_request.h similarity index 97% rename from src/assignment_request.h rename to src/core/wire/assignment_request.h index 46c1e03..12e469a 100644 --- a/src/assignment_request.h +++ b/src/core/wire/assignment_request.h @@ -41,11 +41,11 @@ #include #include -namespace reasampler { +namespace reasampler::wire { // One assignment request: the ingested sample's identity + a monotonic disambiguator. // bankId — the bank the sample was ingested into (the active/target bank). -// sampleId — the ingested Sample's stable id (BankIndex key). +// sampleId — the ingested Sample's stable id (BankModel key). // generation — a monotonic value the reader compares to detect a NEW request. The // writer supplies a unix-epoch-seconds stamp; the reader treats it as an // opaque "did this change?" token, not a wall-clock it interprets. @@ -86,4 +86,4 @@ std::string encodeAssignmentRequest(const AssignmentRequest& req); // silently, never crashing or selecting a nonexistent entry. std::optional decodeAssignmentRequest(const std::string& wire); -} // namespace reasampler +} // namespace reasampler::wire diff --git a/src/instrument_drop.cpp b/src/core/wire/instrument_drop.cpp similarity index 92% rename from src/instrument_drop.cpp rename to src/core/wire/instrument_drop.cpp index 9db7320..e475830 100644 --- a/src/instrument_drop.cpp +++ b/src/core/wire/instrument_drop.cpp @@ -1,15 +1,15 @@ // instrument_drop — pure implementation. See instrument_drop.h. // NO REAPER / SWELL / VST3 SDK / vendor. Reuses sample_map's ComponentState serializer and -// the SDK-free UID macros (vst/reasampler_uid.h). +// the SDK-free UID macros (core/wire/reasampler_uid.h). -#include "instrument_drop.h" +#include "core/wire/instrument_drop.h" #include -#include "vst/reasampler_uid.h" // REASAMPLER_ACTIVE_UID_* — the FROZEN, channel-selected class UID -#include "vst/sample_map.h" // ComponentState + serializeComponentState (the SHARED writer) +#include "core/wire/reasampler_uid.h" // REASAMPLER_ACTIVE_UID_* — the FROZEN, channel-selected class UID +#include "core/instrument/map/sample_map.h" // ComponentState + serializeComponentState (the SHARED writer) -namespace reasampler { +namespace reasampler::wire { namespace { @@ -106,4 +106,4 @@ bool infoNamesFxHotspot(const std::string& info) { return startsWith("fx_") || startsWith("tcp.fx") || startsWith("mcp.fx"); } -} // namespace reasampler +} // namespace reasampler::wire diff --git a/src/instrument_drop.h b/src/core/wire/instrument_drop.h similarity index 96% rename from src/instrument_drop.h rename to src/core/wire/instrument_drop.h index 7576114..25fec92 100644 --- a/src/instrument_drop.h +++ b/src/core/wire/instrument_drop.h @@ -3,7 +3,7 @@ // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO VST3 SDK, // NO vendor/ includes. Standard library only (+ the pure sample_map it reuses and the -// SDK-free UID macros in vst/reasampler_uid.h). Unit-tested outside the DAW — the same +// SDK-free UID macros in core/wire/reasampler_uid.h). Unit-tested outside the DAW — the same // "small pure builder + round-trip proof" pattern as assignment_request / provenance. // // -- What it is (the S17 seam, extension side) -------------------------------- @@ -36,14 +36,14 @@ #include #include -namespace reasampler { +namespace reasampler::wire { // The 32-char uppercase-hex class-ID string of THIS build's channel-active ReaSampler 9000 // VST3 class UID — exactly what Steinberg::FUID::toString renders and what a .vstpreset // header carries (public.sdk vstpresetfile: "ASCII-encoded FUID"). On both COM-compatible // (Windows GUID byte order) and plain layouts, FUID::toString reduces to the four // INLINE_UID uint32 words printed "%08X" in order, so this derivation is platform-stable. -// Sourced from the FROZEN macros in vst/reasampler_uid.h (the same constants the factory +// Sourced from the FROZEN macros in core/wire/reasampler_uid.h (the same constants the factory // registers), channel-selected by the one REASAMPLER_CHANNEL_IS_BETA bit — a beta extension // writes presets only the beta VST class accepts, preserving the S18 pairing invariant. std::string vstClassIdHex(); @@ -111,4 +111,4 @@ bool infoNamesFxHotspot(const std::string& info); // its setState expects. Not called by the shell (which uses the .vstpreset image). std::vector instrumentDropStateBytes(const std::string& sampleId); -} // namespace reasampler +} // namespace reasampler::wire diff --git a/src/vst/reasampler_uid.h b/src/core/wire/reasampler_uid.h similarity index 100% rename from src/vst/reasampler_uid.h rename to src/core/wire/reasampler_uid.h diff --git a/src/sample_usage.cpp b/src/core/wire/sample_usage.cpp similarity index 78% rename from src/sample_usage.cpp rename to src/core/wire/sample_usage.cpp index 8414564..0489650 100644 --- a/src/sample_usage.cpp +++ b/src/core/wire/sample_usage.cpp @@ -1,92 +1,24 @@ // sample_usage.cpp — see sample_usage.h. Pure: standard library only. -#include "sample_usage.h" +#include "core/wire/sample_usage.h" #include -#include -#include -namespace reasampler { +#include "core/wire/wire.h" + +namespace reasampler::wire { namespace { constexpr const char* kMagic = "rsusage1"; -// Append one length-prefixed field: ':' . The same wire idiom as -// assignment_request / provenance — one grammar across every ext-state seam. -void putField(std::string& out, const std::string& field) { - out += std::to_string(field.size()); - out += ':'; - out += field; -} - -// Bounds-checked cursor over the encoded string (the assignment_request Cursor, trimmed -// to the two field kinds this record needs). A short read latches ok_ false. -class Cursor { -public: - explicit Cursor(const std::string& s) : s_(s) {} - - bool ok() const { return ok_; } - bool atEnd() const { return pos_ >= s_.size(); } - - bool field(std::string& out) { - if (!ok_) return false; - const std::size_t colon = s_.find(':', pos_); - if (colon == std::string::npos) return fail(); - if (colon == pos_) return fail(); // empty length token - if (colon - pos_ > 20u) return fail(); // SIZE_MAX is 20 decimal digits - std::size_t len = 0; - for (std::size_t i = pos_; i < colon; ++i) { - const char c = s_[i]; - if (c < '0' || c > '9') return fail(); - const std::size_t digit = static_cast(c - '0'); - if (len > (std::numeric_limits::max() - digit) / 10u) - return fail(); - len = len * 10u + digit; - } - const std::size_t start = colon + 1; - if (start > s_.size() || len > s_.size() - start) return fail(); - out.assign(s_, start, len); - pos_ = start + len; - return true; - } - - // Consumes an exact literal at the cursor (the magic tag). Fails if absent. - bool literal(const char* lit) { - if (!ok_) return false; - std::size_t i = 0; - for (; lit[i] != '\0'; ++i) { - if (pos_ + i >= s_.size() || s_[pos_ + i] != lit[i]) return fail(); - } - pos_ += i; - return true; - } - - // A length-prefixed unsigned decimal (the hold count). Fails on empty, non-digit, - // or a value past a sane ceiling (a record cannot hold more entries than bytes). - bool fieldCount(std::size_t& out) { - std::string f; - if (!field(f)) return false; - if (f.empty() || f.size() > 10u) return fail(); - std::size_t v = 0; - for (const char c : f) { - if (c < '0' || c > '9') return fail(); - v = v * 10u + static_cast(c - '0'); - } - out = v; - return true; - } - -private: - bool fail() { - ok_ = false; - return false; - } - - const std::string& s_; - std::size_t pos_ = 0; - bool ok_ = true; -}; +// The shared core/wire codec (Q-W1, T2-01b) — one grammar across every +// ext-state seam. The former local fieldCount (10-digit cap) is subsumed by the +// codec's fieldSizeT (20-digit cap + overflow-guarded accumulate): every count +// the old cap accepted decodes identically, and any larger count is rejected by +// the count-vs-wire-size sanity bound at the call site below. +using wire::putField; +using Cursor = wire::Cursor; } // namespace @@ -115,7 +47,7 @@ std::optional decodeUsageRecord(const std::string& wire) { else if (unionedField == "0") rec.unioned = false; else return std::nullopt; // anything else is corruption -> reject whole std::size_t count = 0; - if (!c.fieldCount(count)) return std::nullopt; + if (!c.fieldSizeT(count)) return std::nullopt; // Each hold needs at least 4 wire bytes ("0:0:"), so a count past wire.size()/4 is // provably bogus — reject before looping rather than iterating a crafted huge count. if (count > wire.size() / 4u + 1u) return std::nullopt; @@ -298,4 +230,4 @@ bool identityMatches(const std::string& identity, const std::string& uidHexUpper return !nameUpper.empty() && up.find(nameUpper) != std::string::npos; } -} // namespace reasampler +} // namespace reasampler::wire diff --git a/src/sample_usage.h b/src/core/wire/sample_usage.h similarity index 99% rename from src/sample_usage.h rename to src/core/wire/sample_usage.h index 3e90fee..8553bf0 100644 --- a/src/sample_usage.h +++ b/src/core/wire/sample_usage.h @@ -105,7 +105,7 @@ #include #include -namespace reasampler { +namespace reasampler::wire { // One held capture: the bank sample id (attribution/debugging) + the project-relative // WAV path (the prune-protection payload — compared by EXACT string against the prune @@ -250,4 +250,4 @@ bool identityMatches(const std::string& identity, const std::string& uidHexUpper // ASCII-only uppercase (shared by the matcher and the shell's needle preparation). std::string toUpperAscii(const std::string& s); -} // namespace reasampler +} // namespace reasampler::wire diff --git a/src/core/wire/wire.cpp b/src/core/wire/wire.cpp new file mode 100644 index 0000000..80c08b4 --- /dev/null +++ b/src/core/wire/wire.cpp @@ -0,0 +1,134 @@ +// core/wire implementation — see wire.h. The bodies are the hardened +// assignment_request / sample_usage / provenance (post Q-W0 T2-01a backport) +// cursor, unified; any behavioral change here changes every ext-state wire +// seam at once. + +#include "core/wire/wire.h" + +#include +#include + +namespace reasampler::wire { + +void putField(std::string& out, const std::string& field) { + out += std::to_string(field.size()); + out += ':'; + out += field; +} + +bool parseUnsignedDecimal(const std::string& s, std::int64_t& out) { + if (s.empty()) return false; + std::int64_t value = 0; + constexpr std::int64_t kMax = std::numeric_limits::max(); + for (const char c : s) { + if (c < '0' || c > '9') return false; // any non-digit -> reject whole + const int digit = c - '0'; + // Guard value*10 + digit against overflow before performing it. + if (value > (kMax - digit) / 10) return false; + value = value * 10 + digit; + } + out = value; + return true; +} + +bool Cursor::literal(const char* lit) { + if (!ok_) return false; + std::size_t i = 0; + for (; lit[i] != '\0'; ++i) { + if (pos_ + i >= s_.size() || s_[pos_ + i] != lit[i]) return fail(); + } + pos_ += i; + return true; +} + +bool Cursor::field(std::string& out) { + if (!ok_) return false; + const std::size_t colon = s_.find(':', pos_); + if (colon == std::string::npos) return fail(); + if (colon == pos_) return fail(); // empty length token + // Cap: SIZE_MAX fits in at most 20 decimal digits; a longer run is bogus. + if (colon - pos_ > 20u) return fail(); + std::size_t len = 0; + for (std::size_t i = pos_; i < colon; ++i) { + const char c = s_[i]; + if (c < '0' || c > '9') return fail(); + const std::size_t digit = static_cast(c - '0'); + // Overflow guard: if len would exceed SIZE_MAX after multiply+add, fail. + if (len > (std::numeric_limits::max() - digit) / 10u) + return fail(); + len = len * 10u + digit; + } + const std::size_t start = colon + 1; + // Subtraction-first form: start + len cannot wrap on a huge len. + if (start > s_.size() || len > s_.size() - start) return fail(); + out.assign(s_, start, len); + pos_ = start + len; + return true; +} + +bool Cursor::fieldInt64(std::int64_t& out) { + std::string f; + if (!field(f)) return false; + if (f.empty()) return fail(); + std::size_t i = 0; + bool neg = false; + if (f[0] == '-') { + neg = true; + i = 1; + if (f.size() == 1) return fail(); // bare "-" + } + // Cap at 19 digits (INT64_MAX = 9223372036854775807 — 19 digits). A 20-digit + // positive value would overflow INT64_MAX; a 20-digit negative might be valid + // (INT64_MIN) but is conservatively rejected too — see header. + if (f.size() - i > 19u) return fail(); + std::int64_t v = 0; + for (; i < f.size(); ++i) { + const char c = f[i]; + if (c < '0' || c > '9') return fail(); + const std::int64_t digit = static_cast(c - '0'); + // Overflow guard: v * 10 + digit must not exceed INT64_MAX. + if (v > (std::numeric_limits::max() - digit) / 10) + return fail(); + v = v * 10 + digit; + } + out = neg ? -v : v; + return true; +} + +bool Cursor::fieldInt(int& out) { + std::int64_t v = 0; + if (!fieldInt64(v)) return false; + if (v < std::numeric_limits::min() || v > std::numeric_limits::max()) + return fail(); + out = static_cast(v); + return true; +} + +bool Cursor::fieldSizeT(std::size_t& out) { + std::string f; + if (!field(f)) return false; + if (f.empty() || f.size() > 20u) return fail(); + std::size_t v = 0; + for (const char c : f) { + if (c < '0' || c > '9') return fail(); + const std::size_t digit = static_cast(c - '0'); + if (v > (std::numeric_limits::max() - digit) / 10u) + return fail(); + v = v * 10u + digit; + } + out = v; + return true; +} + +bool Cursor::fieldDouble(double& out) { + std::string f; + if (!field(f)) return false; + const char* b = f.c_str(); + char* end = nullptr; + double v = std::strtod(b, &end); + if (end != b + f.size()) return fail(); + out = v; + return true; +} + +} // namespace reasampler::wire diff --git a/src/core/wire/wire.h b/src/core/wire/wire.h new file mode 100644 index 0000000..9cdde10 --- /dev/null +++ b/src/core/wire/wire.h @@ -0,0 +1,88 @@ +// core/wire — the ONE length-prefixed ext-state wire codec (Q-W1; audit +// T2-01(b)). Pure: standard library only — NO REAPER, NO SWELL, NO VST3. +// +// The `':'` field grammar ("one grammar across every +// ext-state seam") was previously implemented as three near-identical +// putField + Cursor copies (provenance / assignment_request / sample_usage) +// plus a fourth guarded decimal accumulate (bank_sync::parseBankGeneration) — +// and the copies drifted on the hardening. This is the single survivor, +// carrying the FULL hardening everywhere: +// - length digit-run capped at 20 (SIZE_MAX's decimal width) so a crafted +// digit run cannot accumulate past SIZE_MAX via repeated multiply; +// - overflow guard on every accumulate (multiply+add checked BEFORE applied); +// - subtraction-first bounds check so a huge len cannot wrap `start + len`; +// - fieldInt/fieldInt64 parse sign+digits manually with an INT64 overflow +// guard and an int range check — an out-of-range field FAILS the parse +// (closing the strtol errno/range gap the provenance copy carried). +// +// Wire formats on disk / ext-state are FROZEN: encode is byte-identical to the +// pre-collapse writers (std::to_string length + ':' + bytes), decode is +// tolerant-identical for every value a house writer can emit. "Never UB, never +// a partial value" is the parse-integrity promise. + +#pragma once + +#include +#include +#include + +namespace reasampler::wire { + +// Append one length-prefixed field: ':' +void putField(std::string& out, const std::string& field); + +// Whole-string, non-negative decimal parse WITHOUT exceptions or locale +// surprises (the bank_sync generation-stamp core). False on empty, any +// non-digit (incl. a leading '+'/'-'), or overflow past INT64_MAX; the +// accumulate is overflow-guarded so a pathologically long digit run can never +// wrap into a bogus small value. +bool parseUnsignedDecimal(const std::string& s, std::int64_t& out); + +// Bounds-checked cursor over an encoded string. All reads are bounds-checked; +// any short read fails the whole parse (ok_ latches false — every subsequent +// read also fails, so a caller may check ok() once at the end). +class Cursor { +public: + explicit Cursor(const std::string& s) : s_(s) {} + + bool ok() const { return ok_; } + bool atEnd() const { return pos_ >= s_.size(); } + + // Consumes an exact literal at the cursor (the magic tag). Fails if absent. + bool literal(const char* lit); + + // Reads one length-prefixed field into `out`. Fails on a missing ':', an + // empty or non-numeric length, a length that would overflow SIZE_MAX, or a + // length that runs past the end. + bool field(std::string& out); + + // Length-prefixed signed 64-bit decimal (optional leading '-'). Digit run + // capped at 19 (INT64_MAX's decimal width); overflow fails the parse. A + // 20-digit negative (only INT64_MIN itself) is conservatively rejected — + // house writers emit generation timestamps and small enums, never that. + bool fieldInt64(std::int64_t& out); + + // fieldInt64 narrowed to int; a value outside [INT_MIN, INT_MAX] FAILS the + // parse (the fixed form of the provenance copy's silent strtol narrowing). + bool fieldInt(int& out); + + // Length-prefixed unsigned decimal (element counts). Digit run capped at + // 20; overflow-guarded accumulate. Callers still apply their own + // count-vs-wire-size sanity bound BEFORE any reserve() on the result. + bool fieldSizeT(std::size_t& out); + + // Length-prefixed %.17g double. Full-token strtod; trailing bytes fail. + // Deliberately NO errno/ERANGE rejection: the writers emit %.17g of live + // doubles (incl. "inf"), and those must decode back — same accept set as + // every prior copy. + bool fieldDouble(double& out); + +private: + bool fail() { ok_ = false; return false; } + + const std::string& s_; + std::size_t pos_ = 0; + bool ok_ = true; +}; + +} // namespace reasampler::wire diff --git a/src/ext_keys.h b/src/ext_keys.h index 674e1f9..71d6953 100644 --- a/src/ext_keys.h +++ b/src/ext_keys.h @@ -15,7 +15,7 @@ // stored state. Changing any of them orphans that state. See persist.h for the // per-key retirement / migration semantics — this header only owns the spellings. -#include "app_version.h" +#include "core/version/app_version.h" namespace reasampler { @@ -26,7 +26,7 @@ namespace reasampler { // because the value is fixed by the channel bit at build time. This is the wire-contract // reconciliation between S4 (shared ext_keys) and V4 (channel-isolated namespace): without // it a beta instrument would read the stable namespace and see empty state. -inline const char* kProjExtNamespace() { return extStateNamespace().c_str(); } +inline const char* kProjExtNamespace() { return version::extStateNamespace().c_str(); } // The multi-bank key: the whole serialized BankBook (pool + named banks). This is // the key the VST3 instrument reads to see the live bank (read-only, S4). persist.h diff --git a/src/ingest.cpp b/src/ingest.cpp index 9dd4395..250b201 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // ingest.cpp — the S8 "ingest through the bank" shell (extension side). See ingest.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT @@ -16,17 +17,18 @@ #include #include "actions.h" // persistBankOp — shared undo-block wrapper (R-B path) -#include "app_version.h" // channelCommandId / channelActionName -#include "assignment_request.h" // pure (bankId, sampleId, generation) encode -#include "bank_book.h" // BankBook, Bank, activeBankId / activeIndex -#include "bank_model.h" // Sample, AddResult, findByHash +#include "core/version/app_version.h" // channelCommandId / channelActionName +#include "core/wire/assignment_request.h" // pure (bankId, sampleId, generation) encode +#include "core/model/bank_book.h" // BankBook, Bank, activeBankId / activeIndex +#include "core/model/bank_model.h" // Sample, AddResult, findByHash #include "bank_panel.h" // bankPanelRefresh -#include "capture_paths.h" // deriveBankPaths / projectDirOfRpp / hashWavContent -#include "instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId) -#include "instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block) +#include "core/capture/capture_paths.h" // deriveBankPaths / projectDirOfRpp / hashWavContent +#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03) +#include "core/wire/instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId) +#include "shell/actions/instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block) #include "persist.h" // ReaSamplerSession -#include "wav_trim.h" // parseWavLayout — 32f-float WAV validator for the fast path +#include "core/capture/wav_trim.h" // parseWavLayout — 32f-float WAV validator for the fast path #include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs) @@ -81,19 +83,8 @@ std::string currentProjectDir() { return projectDirOfRpp(std::string(buf.data())); } -// Reads a whole file's bytes. Empty vector on any failure (missing / unreadable). Mirror -// of capture.cpp's readFileBytes — used to read the source and validate/hash the bank copy. -std::vector readFileBytes(const std::string& path) { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) return {}; - const std::streamsize n = f.tellg(); - if (n <= 0) return {}; - std::vector bytes(static_cast(n)); - f.seekg(0); - f.read(reinterpret_cast(bytes.data()), n); - if (!f) return {}; - return bytes; -} +// Whole-file reads (source read + bank-copy validate/hash) go through the shared +// core/util readFileBytes (Q-W1, T2-03): empty on any failure (missing / unreadable). // Writes a byte buffer to a file. Returns true on success. The caller is responsible for // ensuring the directory exists before calling. diff --git a/src/ingest.h b/src/ingest.h index a6b2203..6c74d57 100644 --- a/src/ingest.h +++ b/src/ingest.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // ingest — the S8 "ingest through the bank" shell (EXTENSION side). // // Compiled into the reaper_reasampler MODULE. REAPER-facing (PCM_Source metadata reads, diff --git a/src/owned_manifest.cpp b/src/owned_manifest.cpp deleted file mode 100644 index 08c4798..0000000 --- a/src/owned_manifest.cpp +++ /dev/null @@ -1,315 +0,0 @@ -#include "owned_manifest.h" - -#include -#include - -// owned_manifest implementation. -// -// JSON is hand-rolled and self-contained (project convention: the pure core is -// dependency-free — no third-party JSON lib, mirror of bank_model / bank_book / -// tail_control). The shape is a single object with one string array: -// -// {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]} -// -// so a compact writer + a focused string-array parser is all it needs — far smaller -// than bank_model's full recursive-descent parser, because there is exactly one key -// and one value kind. - -namespace reasampler { - -// --------------------------------------------------------------------------- -// path invariant (mirror of bank_model's isAbsolutePath) -// --------------------------------------------------------------------------- - -namespace { - -// Any leading '/' or '\' (POSIX root / UNC), or a leading : (Windows drive, -// incl. drive-relative "C:foo") is absolute. Same rejection bank_model applies to -// Sample.relativePath — the manifest holds the SAME kind of path, so the invariant -// must match exactly (a path the index accepts must be recordable, and vice versa). -bool isAbsolutePath(const std::string& p) { - if (p.empty()) return false; - if (p[0] == '/' || p[0] == '\\') return true; - if (p.size() >= 2 && std::isalpha(static_cast(p[0])) && p[1] == ':') - return true; - return false; -} - -} // namespace - -// --------------------------------------------------------------------------- -// mutation / query -// --------------------------------------------------------------------------- - -ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) { - if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath; - if (isAbsolutePath(relativePath)) return ManifestAddResult::RejectedAbsolutePath; - if (contains(relativePath)) return ManifestAddResult::AlreadyPresent; - paths_.push_back(relativePath); - return ManifestAddResult::Added; -} - -bool OwnedFileManifest::contains(const std::string& relativePath) const { - for (const auto& p : paths_) - if (p == relativePath) return true; - return false; -} - -// --------------------------------------------------------------------------- -// JSON writer -// --------------------------------------------------------------------------- - -namespace { - -void writeEscaped(std::string& out, const std::string& s) { - out += '"'; - for (char c : s) { - switch (c) { - case '"': out += "\\\""; break; - case '\\': out += "\\\\"; break; - case '\b': out += "\\b"; break; - case '\f': out += "\\f"; break; - case '\n': out += "\\n"; break; - case '\r': out += "\\r"; break; - case '\t': out += "\\t"; break; - default: - if (static_cast(c) < 0x20) { - char buf[8]; - std::snprintf(buf, sizeof(buf), "\\u%04x", - static_cast(c)); - out += buf; - } else { - out += c; - } - } - } - out += '"'; -} - -} // namespace - -std::string OwnedFileManifest::serialize() const { - std::string out = "{\"owned\":["; - for (std::size_t i = 0; i < paths_.size(); ++i) { - if (i) out += ','; - writeEscaped(out, paths_[i]); - } - out += "]}"; - return out; -} - -// --------------------------------------------------------------------------- -// JSON parser (string-array only) -// --------------------------------------------------------------------------- - -namespace { - -class Parser { -public: - explicit Parser(const std::string& s) : s_(s) {} - - // Parse the manifest object into `out`. Tolerates unknown keys (forward-compat) - // and requires the "owned" value to be an array of strings. - bool parseManifest(OwnedFileManifest& out); - -private: - const std::string& s_; - std::size_t pos_ = 0; - - bool eof() const { return pos_ >= s_.size(); } - - void skipWs() { - while (!eof()) { - char c = s_[pos_]; - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; - else break; - } - } - - bool consume(char c) { - skipWs(); - if (eof() || s_[pos_] != c) return false; - ++pos_; - return true; - } - - bool parseString(std::string& out); - bool parseStringArray(std::vector& out); - bool skipValue(); // for forward-compat unknown keys -}; - -// Parses a JSON string literal (with the escapes our writer emits, plus \uXXXX for -// control chars). Positioned before the opening quote (skips leading whitespace). -bool Parser::parseString(std::string& out) { - skipWs(); - if (eof() || s_[pos_] != '"') return false; - ++pos_; - out.clear(); - while (!eof()) { - char c = s_[pos_++]; - if (c == '"') return true; - if (c == '\\') { - if (eof()) return false; - char e = s_[pos_++]; - switch (e) { - case '"': out += '"'; break; - case '\\': out += '\\'; break; - case '/': out += '/'; break; - case 'b': out += '\b'; break; - case 'f': out += '\f'; break; - case 'n': out += '\n'; break; - case 'r': out += '\r'; break; - case 't': out += '\t'; break; - case 'u': { - auto readHex4 = [&](unsigned int& cp) -> bool { - if (pos_ + 4 > s_.size()) return false; - cp = 0; - for (int i = 0; i < 4; ++i) { - char h = s_[pos_++]; - cp <<= 4; - if (h >= '0' && h <= '9') cp |= static_cast(h - '0'); - else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); - else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10); - else return false; - } - return true; - }; - - unsigned int hi = 0; - if (!readHex4(hi)) return false; - - unsigned int codePoint = hi; - if (hi >= 0xD800 && hi <= 0xDBFF) { - if (pos_ + 6 > s_.size()) return false; - if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false; - pos_ += 2; - unsigned int lo = 0; - if (!readHex4(lo)) return false; - if (lo < 0xDC00 || lo > 0xDFFF) return false; - codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00); - } else if (hi >= 0xDC00 && hi <= 0xDFFF) { - return false; // unpaired low surrogate - } - - if (codePoint <= 0x7F) { - out += static_cast(codePoint); - } else if (codePoint <= 0x7FF) { - out += static_cast(0xC0 | (codePoint >> 6)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else if (codePoint <= 0xFFFF) { - out += static_cast(0xE0 | (codePoint >> 12)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } else { - out += static_cast(0xF0 | (codePoint >> 18)); - out += static_cast(0x80 | ((codePoint >> 12) & 0x3F)); - out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); - out += static_cast(0x80 | (codePoint & 0x3F)); - } - break; - } - default: return false; - } - } else { - out += c; - } - } - return false; // unterminated string -} - -bool Parser::parseStringArray(std::vector& out) { - if (!consume('[')) return false; - skipWs(); - if (consume(']')) return true; // empty array - for (;;) { - std::string s; - if (!parseString(s)) return false; - out.push_back(std::move(s)); - skipWs(); - if (consume(',')) continue; - if (consume(']')) return true; - return false; // neither separator nor terminator — malformed - } -} - -// Skip a single JSON value (string / array / object / bare scalar) so an unknown key -// does not abort the parse. Minimal: enough for forward-compat siblings we don't know. -bool Parser::skipValue() { - skipWs(); - if (eof()) return false; - char c = s_[pos_]; - if (c == '"') { - std::string tmp; - return parseString(tmp); - } - if (c == '[' || c == '{') { - // Balance nested brackets of either kind, ignoring bracket chars inside - // strings. Enough to step over an unknown nested value; not a full validator. - int depth = 0; - bool inStr = false; - while (!eof()) { - char d = s_[pos_]; - if (inStr) { - if (d == '\\') { pos_ += 2; continue; } - if (d == '"') inStr = false; - ++pos_; - continue; - } - if (d == '"') { inStr = true; ++pos_; continue; } - if (d == '[' || d == '{') ++depth; - else if (d == ']' || d == '}') { - --depth; - if (depth == 0) { ++pos_; return true; } - } - ++pos_; - } - return false; - } - // bare scalar (number / true / false / null) — read to the next structural char - while (!eof()) { - char d = s_[pos_]; - if (d == ',' || d == '}' || d == ']' || d == ' ' || d == '\t' || - d == '\n' || d == '\r') - break; - ++pos_; - } - return true; -} - -bool Parser::parseManifest(OwnedFileManifest& out) { - if (!consume('{')) return false; - skipWs(); - if (consume('}')) return true; // empty object -> empty manifest - for (;;) { - std::string key; - if (!parseString(key)) return false; - if (!consume(':')) return false; - if (key == "owned") { - std::vector paths; - if (!parseStringArray(paths)) return false; - for (auto& p : paths) { - // Feed through add() so the persisted invariants (dedup, reject - // empty/absolute) are re-asserted on load — a hand-edited or corrupt - // blob cannot smuggle an absolute or duplicate path into the manifest. - out.add(p); - } - } else { - if (!skipValue()) return false; // forward-compat: tolerate unknown keys - } - skipWs(); - if (consume(',')) continue; - if (consume('}')) return true; - return false; - } -} - -} // namespace - -std::optional OwnedFileManifest::deserialize(const std::string& json) { - OwnedFileManifest m; - Parser p(json); - if (!p.parseManifest(m)) return std::nullopt; - return m; -} - -} // namespace reasampler diff --git a/src/persist.cpp b/src/persist.cpp index d553925..dfb3e8f 100644 --- a/src/persist.cpp +++ b/src/persist.cpp @@ -1,4 +1,5 @@ -// persist.cpp — REAPER-facing implementation of the BankIndex <-> project +#include "core/namespaces.h" +// persist.cpp — REAPER-facing implementation of the BankModel <-> project // ext-state bridge (M4). See persist.h for the contract. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h @@ -93,11 +94,11 @@ #include #endif -#include "app_version.h" -#include "capture_paths.h" -#include "prune_reconcile.h" -#include "usage_scan.h" // liveInstanceHeldPaths (pS-usage: instance holds join `referenced`) -#include "vst/bank_sync.h" // parseBankGeneration / formatBankGeneration (SHARED with the instrument reader) +#include "core/version/app_version.h" +#include "core/capture/capture_paths.h" +#include "core/reclaim/prune_reconcile.h" +#include "shell/persist/usage_scan.h" // liveInstanceHeldPaths (pS-usage: instance holds join `referenced`) +#include "core/instrument/map/bank_sync.h" // parseBankGeneration / formatBankGeneration (SHARED with the instrument reader) #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects @@ -258,11 +259,11 @@ bool ReaSamplerSession::saveToActiveProject() { // seam so the counter and MarkProjectDirty stay paired. The value is whatever // bumpBankGeneration() advanced it to since the last save (0 if never bumped / pre-S9), so // every content mutation's own save carries the fresh generation the instrument reads. The - // format is the SHARED pure encoder (vst::formatBankGeneration) so writer and reader agree + // format is the SHARED pure encoder (instrument::map::formatBankGeneration) so writer and reader agree // byte-for-byte — a decimal integer. Additive: does not disturb the blobs above. SetProjExtState(static_cast(proj), projExtNamespace(), kProjExtBankGenKey, - vst::formatBankGeneration(bankGeneration_).c_str()); + instrument::map::formatBankGeneration(bankGeneration_).c_str()); MarkProjectDirty(static_cast(proj)); return true; @@ -636,7 +637,7 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi // rather than resetting to 0 on reopen — a next bump then reads > the stored value. A // project switch reads THAT project's counter, not the previous one's; an absent/malformed // stamp (pre-S9 or corrupt) parses to 0 via the SHARED decoder. proj == nullptr -> 0. - bankGeneration_ = vst::parseBankGeneration( + bankGeneration_ = instrument::map::parseBankGeneration( proj ? getProjExtStateString(static_cast(proj), projExtNamespace(), kProjExtBankGenKey) : std::string{}); @@ -683,7 +684,7 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi // Idempotent, so a fresh empty book is a cheap no-op. book_.reconcileSlots(); - // Project-relative resolution is a READ-time concern: every BankIndex in the book + // Project-relative resolution is a READ-time concern: every BankModel in the book // stores only relative paths (invariant, enforced per-bank at add()), and consumers // (M5 panel, M6 insert) resolve each entry against the CURRENT project dir via // resolveBankFile(projectDir, relativePath). We do NOT rewrite stored paths to diff --git a/src/persist.h b/src/persist.h index 52b5cfc..74a3d37 100644 --- a/src/persist.h +++ b/src/persist.h @@ -1,12 +1,13 @@ #pragma once -// persist — the REAPER-facing bridge between the in-memory BankIndex and project +#include "core/namespaces.h" +// persist — the REAPER-facing bridge between the in-memory BankModel and project // ext state (CLAUDE.md §load-bearing split; CONTEXT.md §Persistence & paths). // -// Save: serialize the BankIndex JSON -> SetProjExtState under namespace +// Save: serialize the BankModel JSON -> SetProjExtState under namespace // "reasampler" (ext state lives inside the .rpp, so the index travels with the // project for free). // Load: on project load, GetProjExtState -> bank_model::deserialize -> in-memory -// BankIndex, then resolve each entry's bank file against the CURRENT project +// BankModel, then resolve each entry's bank file against the CURRENT project // dir (project-relative resolution — a project opened from a new location still // finds its bank). // Save-As: when the project path changes, relocate the physical bank folder so @@ -20,14 +21,14 @@ #include #include -#include "app_version.h" -#include "bank_book.h" -#include "bank_model.h" +#include "core/version/app_version.h" +#include "core/model/bank_book.h" +#include "core/model/bank_model.h" #include "ext_keys.h" -#include "owned_manifest.h" -#include "prune_reconcile.h" -#include "tail_control.h" -#include "view_mode_model.h" +#include "core/model/owned_manifest.h" +#include "core/reclaim/prune_reconcile.h" +#include "core/capture/tail_control.h" +#include "core/view/view_mode_model.h" namespace reasampler { @@ -100,21 +101,21 @@ public: ReaSamplerSession() = default; // The multi-bank book (Phase B): the pool + named banks, each wrapping a - // BankIndex, plus the active-bank id. The action layer (B3) creates / renames / + // BankModel, plus the active-bank id. The action layer (B3) creates / renames / // reorders / deletes banks and moves samples here; the panel (B4) reads it; // persist serializes it under the `banks` key on save and replaces it on load. BankBook& book() { return book_; } const BankBook& book() const { return book_; } - // The capture add-target: the ACTIVE bank's BankIndex (defaults to the pool). + // The capture add-target: the ACTIVE bank's BankModel (defaults to the pool). // The capture path adds a captured Sample through this seam, so a capture lands // in whichever bank is active — the single behavioural change B2 wires in over // M7/M8 (the capture backends are untouched; only the target index moved). The // panel/insert readers that displayed the single index continue to read it here // unchanged; today it resolves to the pool (default active), matching prior // single-bank behaviour, until B3/B4 let the user switch the active bank. - BankIndex& bank() { return book_.activeIndex(); } - const BankIndex& bank() const { return book_.activeIndex(); } + BankModel& bank() { return book_.activeIndex(); } + const BankModel& bank() const { return book_.activeIndex(); } // The in-memory Design-View model. The view/action layer mutates it (tag, // toggle, snapshot); persist serializes it on save and replaces it on project @@ -222,7 +223,7 @@ public: // routing). Non-throwing: every filesystem call uses error_code forms; a per-file // failure (locked, already gone) is recorded and skipped, never thrown across the C ABI. // - // Does NOT modify the BankIndex/book (orphans are unreferenced by definition) and does + // Does NOT modify the BankModel/book (orphans are unreferenced by definition) and does // NOT modify the OwnedFileManifest (a reclaimed file drops out of the (owned ∩ present) // algebra naturally once it is off disk — no persist write, so no undo-point question // and no risk to the referenced/owned safety). Writes NO ext-state at all. diff --git a/src/drag_out_win.cpp b/src/shell/actions/drag_out_win.cpp similarity index 99% rename from src/drag_out_win.cpp rename to src/shell/actions/drag_out_win.cpp index e555f24..3aa6e83 100644 --- a/src/drag_out_win.cpp +++ b/src/shell/actions/drag_out_win.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // drag_out_win — OS/COM initiation of native OS drag-out (M11). See drag_out_win.h. // // Windows path (primary): a hand-rolled minimal IDataObject exposing exactly one format, @@ -9,7 +10,7 @@ // Compiled into the reaper_reasampler MODULE. No REAPER API is used here (pure OS/COM); it // is a leaf the bank_panel calls. -#include "drag_out_win.h" +#include "shell/actions/drag_out_win.h" #ifdef _WIN32 diff --git a/src/drag_out_win.h b/src/shell/actions/drag_out_win.h similarity index 98% rename from src/drag_out_win.h rename to src/shell/actions/drag_out_win.h index 04ccb6b..1a5781b 100644 --- a/src/drag_out_win.h +++ b/src/shell/actions/drag_out_win.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // drag_out_win — the OS/COM initiation half of native OS drag-out (Milestone 11). The pure // gesture-boundary decision and path-list assembly live in drag_out.*; THIS is the platform // shell that hands a resolved, existing-file path list to the operating system's drag-drop diff --git a/src/instrument_drop_win.cpp b/src/shell/actions/instrument_drop_win.cpp similarity index 96% rename from src/instrument_drop_win.cpp rename to src/shell/actions/instrument_drop_win.cpp index 363c9c8..763d3ba 100644 --- a/src/instrument_drop_win.cpp +++ b/src/shell/actions/instrument_drop_win.cpp @@ -1,9 +1,10 @@ +#include "core/namespaces.h" // instrument_drop_win — the REAPER shell for S17 drop-and-load. See instrument_drop_win.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT // REAPERAPI_IMPLEMENT (main.cpp owns the pointers; here they are extern via the WANT list). -#include "instrument_drop_win.h" +#include "shell/actions/instrument_drop_win.h" #include #include @@ -13,8 +14,8 @@ #include #include -#include "app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing) -#include "instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier +#include "core/version/app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing) +#include "core/wire/instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier #include "reaper_plugin.h" diff --git a/src/instrument_drop_win.h b/src/shell/actions/instrument_drop_win.h similarity index 99% rename from src/instrument_drop_win.h rename to src/shell/actions/instrument_drop_win.h index 20a65cb..f9aa016 100644 --- a/src/instrument_drop_win.h +++ b/src/shell/actions/instrument_drop_win.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // instrument_drop_win — the REAPER-facing shell half of S17 drop-and-load. The pure gesture // decision lives in drag_out (DragGesture::InstrumentDrop) and the pure payload construction // in instrument_drop; THIS is the platform shell that (a) resolves a screen point to a track diff --git a/src/capture.cpp b/src/shell/capture/capture.cpp similarity index 97% rename from src/capture.cpp rename to src/shell/capture/capture.cpp index 61c9113..0269c07 100644 --- a/src/capture.cpp +++ b/src/shell/capture/capture.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // capture.cpp — REAPER-facing offline-render backend (OfflineRenderBackend). // // Compiled into the reaper_reasampler MODULE. Includes @@ -33,7 +34,7 @@ // is the realtime-record backend (M8), which captures the master bus output to a // temp track during playback and never invokes the offline render pipeline. -#include "capture.h" +#include "shell/capture/capture.h" #include #include @@ -42,8 +43,9 @@ #include #include -#include "capture_paths.h" -#include "render_settings.h" +#include "core/capture/capture_paths.h" +#include "core/util/file_bytes.h" +#include "core/capture/render_settings.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects @@ -226,20 +228,9 @@ std::string makeUniqueTag() { return std::to_string(static_cast(now)); } -// Reads the whole file into a byte buffer. Returns an empty vector on any I/O -// failure (the caller then leaves contentHash empty — the safe, confirm-eliciting -// direction for an unreadable file). -std::vector readFileBytes(const std::string& path) { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) return {}; - const std::streamoff size = f.tellg(); - if (size <= 0) return {}; - std::vector bytes(static_cast(size)); - f.seekg(0); - f.read(reinterpret_cast(bytes.data()), size); - if (!f) return {}; - return bytes; -} +// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03): +// empty on any I/O failure (the caller then leaves contentHash empty — the safe, +// confirm-eliciting direction for an unreadable file). } // namespace diff --git a/src/capture.h b/src/shell/capture/capture.h similarity index 98% rename from src/capture.h rename to src/shell/capture/capture.h index 03b61ca..fd9a54d 100644 --- a/src/capture.h +++ b/src/shell/capture/capture.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // capture — the REAPER-facing capture shell (CLAUDE.md §load-bearing split). // // This header declares the capture *seam* the later milestones fill: @@ -19,8 +20,8 @@ #include #include -#include "bank_model.h" -#include "render_settings.h" // TailMode (pure) — the three-state tail contract +#include "core/model/bank_model.h" +#include "core/capture/render_settings.h" // TailMode (pure) — the three-state tail contract // MediaTrack is forward-declared (like track_guid.h) so this header stays // REAPER-free while RealtimeRecordBackend::begin can take the resolved source diff --git a/src/capture_realtime.cpp b/src/shell/capture/capture_realtime.cpp similarity index 97% rename from src/capture_realtime.cpp rename to src/shell/capture/capture_realtime.cpp index 4a5e16f..21c0612 100644 --- a/src/capture_realtime.cpp +++ b/src/shell/capture/capture_realtime.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // capture_realtime.cpp — REAPER-facing realtime-record backend (RealtimeRecordBackend). // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h @@ -67,7 +68,7 @@ // Item realtime is deferred (UnsupportedMode): item scope would need per-item take // isolation on top of the tap, which is a separate increment. -#include "capture.h" +#include "shell/capture/capture.h" #include #include @@ -78,11 +79,12 @@ #include #include -#include "capture_paths.h" // hashBytes, deriveBankPaths -#include "peaks.h" // lastFrameAboveThreshold, AudioSample -#include "realtime_record.h" -#include "render_settings.h" // autoTrimEndRatio, realtimeRecordWindowEnd -#include "wav_trim.h" // parseWavLayout, extractFloatFrames, planWavTruncate +#include "core/capture/capture_paths.h" // hashBytes, deriveBankPaths +#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03) +#include "core/audio/peaks.h" // lastFrameAboveThreshold, AudioSample +#include "core/capture/realtime_record.h" +#include "core/capture/render_settings.h" // autoTrimEndRatio, realtimeRecordWindowEnd +#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames, planWavTruncate #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects @@ -323,20 +325,9 @@ private: namespace { -// Reads the whole file into a byte buffer. Empty vector on any I/O failure — the -// caller treats an unreadable file as "skip the trim" (keep the untrimmed window), -// never as a corruption of the recorded audio. -std::vector readAllBytes(const std::string& path) { - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) return {}; - const std::streamoff size = f.tellg(); - if (size <= 0) return {}; - std::vector bytes(static_cast(size)); - f.seekg(0); - f.read(reinterpret_cast(bytes.data()), size); - if (!f) return {}; - return bytes; -} +// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03): +// empty on any I/O failure — the caller treats an unreadable file as "skip the +// trim" (keep the untrimmed window), never as a corruption of the recorded audio. // Patches a little-endian uint32 into a byte buffer at `off` (the header size fields). void writeU32LE(std::vector& bytes, std::size_t off, std::uint32_t v) { @@ -371,7 +362,7 @@ double trimAutoTailInPlace(const std::string& path, double rangeEndSeconds) { constexpr double kNoTrim = -1.0; - std::vector bytes = readAllBytes(path); + std::vector bytes = readFileBytes(path); if (bytes.empty()) return kNoTrim; const reasampler::WavLayout layout = parseWavLayout(bytes); @@ -533,7 +524,7 @@ CaptureResult finalizeRecording(RealtimeCaptureState& st) { // contentHash empty — the safe, confirm-eliciting direction (bank_model treats // "" as non-participating). { - const std::vector fileBytes = readAllBytes(destPath); + const std::vector fileBytes = readFileBytes(destPath); if (!fileBytes.empty()) { result.sample.contentHash = hashWavContent(fileBytes); } diff --git a/src/insert.cpp b/src/shell/capture/insert.cpp similarity index 97% rename from src/insert.cpp rename to src/shell/capture/insert.cpp index 509200b..d6344dc 100644 --- a/src/insert.cpp +++ b/src/shell/capture/insert.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // insert.cpp — REAPER-facing placement shell (M6). See insert.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h @@ -30,15 +31,15 @@ // doc-comment says "Set exactly one track selected, deselect all others" — // this is the strongest confirmation we have; flagged for DAW-verification. -#include "insert.h" +#include "shell/capture/insert.h" #include #include #include -#include "bank_model.h" +#include "core/model/bank_model.h" #include "bank_panel.h" -#include "capture_paths.h" +#include "core/capture/capture_paths.h" #include "persist.h" #define REAPERAPI_MINIMAL @@ -130,8 +131,8 @@ InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request) // necessarily the active/capture-target bank. Fall back to the active bank when // the source id names no bank (defensive). const std::string srcBankId = bankPanelSelectedSourceBankId(); - const BankIndex* srcIndex = session->book().index(srcBankId); - const BankIndex& bank = srcIndex ? *srcIndex : session->bank(); + const BankModel* srcIndex = session->book().index(srcBankId); + const BankModel& bank = srcIndex ? *srcIndex : session->bank(); const Sample* sample = bank.query(id); if (!sample) { result.status = InsertStatus::NothingResolved; return result; } diff --git a/src/insert.h b/src/shell/capture/insert.h similarity index 97% rename from src/insert.h rename to src/shell/capture/insert.h index b61aee9..18aa2c7 100644 --- a/src/insert.h +++ b/src/shell/capture/insert.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // insert — placement of bank samples into the arrange (M6). REAPER-facing shell: // it reads the bank_panel's current selection, resolves each selected sample's // file, and drops it into the arrange at the edit cursor via InsertMedia, wrapped @@ -17,7 +18,7 @@ // The header is SDK-free: all REAPER API use lives in insert.cpp. The pure // mode-bit arithmetic lives in insert_plan (unit-tested outside the DAW). -#include "insert_plan.h" +#include "core/capture/insert_plan.h" namespace reasampler { diff --git a/src/item_read.cpp b/src/shell/capture/item_read.cpp similarity index 94% rename from src/item_read.cpp rename to src/shell/capture/item_read.cpp index 40edc65..fb84fa6 100644 --- a/src/item_read.cpp +++ b/src/shell/capture/item_read.cpp @@ -1,9 +1,10 @@ +#include "core/namespaces.h" // item_read.cpp — the single MediaItem* read seam (GUID + fixed-lane name). See // item_read.h. Compiled into the reaper_reasampler MODULE; includes // reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU that // defines the API pointers — CLAUDE.md §contract). -#include "item_read.h" +#include "shell/capture/item_read.h" #include diff --git a/src/item_read.h b/src/shell/capture/item_read.h similarity index 98% rename from src/item_read.h rename to src/shell/capture/item_read.h index 7bb74e9..4c8da44 100644 --- a/src/item_read.h +++ b/src/shell/capture/item_read.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // item_read — the ONE place a MediaItem* is read for its canonical GUID string and for // the durable P_LANENAME of the fixed lane it sits on. Before this seam, view.cpp and // bank_panel.cpp each carried a near-identical private itemGuid / itemLaneName pair diff --git a/src/provenance_shell.cpp b/src/shell/capture/provenance_shell.cpp similarity index 95% rename from src/provenance_shell.cpp rename to src/shell/capture/provenance_shell.cpp index 111ae6c..f0a1319 100644 --- a/src/provenance_shell.cpp +++ b/src/shell/capture/provenance_shell.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // provenance_shell.cpp — the REAPER reads behind Milestone 10. See provenance_shell.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h @@ -19,13 +20,13 @@ // * CountTracks / GetTrack (track scan) // * guidToString (via track_guid) -#include "provenance_shell.h" +#include "shell/capture/provenance_shell.h" #include -#include "bank_book.h" // BankBook, Bank, BankIndex::all -#include "capture_paths.h" // resolveBankFile, normalizeSlashes -#include "track_guid.h" // guidString — the ONE canonical GUID key formatter +#include "core/model/bank_book.h" // BankBook, Bank, BankModel::all +#include "core/capture/capture_paths.h" // resolveBankFile, normalizeSlashes +#include "shell/capture/track_guid.h" // guidString — the ONE canonical GUID key formatter #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_TrackFX_GetCount diff --git a/src/provenance_shell.h b/src/shell/capture/provenance_shell.h similarity index 98% rename from src/provenance_shell.h rename to src/shell/capture/provenance_shell.h index e53888b..1a1dab5 100644 --- a/src/provenance_shell.h +++ b/src/shell/capture/provenance_shell.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // provenance_shell — the REAPER-facing reads Milestone 10 needs, in one place. // // The PURE provenance module (provenance.h) owns the fingerprint encoding, the @@ -19,7 +20,7 @@ #include #include -#include "provenance.h" +#include "core/model/provenance.h" class MediaTrack; class MediaItem; diff --git a/src/track_guid.cpp b/src/shell/capture/track_guid.cpp similarity index 91% rename from src/track_guid.cpp rename to src/shell/capture/track_guid.cpp index 8a0de5b..4a47d0b 100644 --- a/src/track_guid.cpp +++ b/src/shell/capture/track_guid.cpp @@ -1,9 +1,10 @@ +#include "core/namespaces.h" // track_guid.cpp — the single MediaTrack* -> canonical GUID key formatter. See // track_guid.h. Compiled into the reaper_reasampler MODULE; includes // reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU // that defines the API pointers — CLAUDE.md §contract). -#include "track_guid.h" +#include "shell/capture/track_guid.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_GetTrackGUID diff --git a/src/track_guid.h b/src/shell/capture/track_guid.h similarity index 97% rename from src/track_guid.h rename to src/shell/capture/track_guid.h index f13bac3..ec28cae 100644 --- a/src/track_guid.h +++ b/src/shell/capture/track_guid.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID // string used as a membership-index key. Both the Design View shell (view.cpp) and // the actions layer (actions.cpp) key membership on this exact string, so the key diff --git a/src/vst/reaper_bridge.cpp b/src/shell/instrument/reaper_bridge.cpp similarity index 97% rename from src/vst/reaper_bridge.cpp rename to src/shell/instrument/reaper_bridge.cpp index 0c5dc49..91fa451 100644 --- a/src/vst/reaper_bridge.cpp +++ b/src/shell/instrument/reaper_bridge.cpp @@ -1,11 +1,12 @@ +#include "core/namespaces.h" // reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin. -#include "reaper_bridge.h" +#include "shell/instrument/reaper_bridge.h" #include -#include "bridge_marshal.h" -#include "capture_paths.h" // projectDirOfRpp (shared M4 project-dir derivation) +#include "core/instrument/map/bridge_marshal.h" +#include "core/capture/capture_paths.h" // projectDirOfRpp (shared M4 project-dir derivation) #include "ext_keys.h" // kProjExtNamespace (shared wire contract) // The VST3 base types must be included before REAPER's VST3 interface header, which diff --git a/src/vst/reaper_bridge.h b/src/shell/instrument/reaper_bridge.h similarity index 99% rename from src/vst/reaper_bridge.h rename to src/shell/instrument/reaper_bridge.h index 78582de..ef7183b 100644 --- a/src/vst/reaper_bridge.h +++ b/src/shell/instrument/reaper_bridge.h @@ -16,6 +16,7 @@ // reaper_vst3_interfaces.h + reaper_plugin_functions.h at the spike. #pragma once +#include "core/namespaces.h" #include #include diff --git a/src/vst/reasampler_embed.cpp b/src/shell/instrument/reasampler_embed.cpp similarity index 87% rename from src/vst/reasampler_embed.cpp rename to src/shell/instrument/reasampler_embed.cpp index 6e739fd..e3c46bf 100644 --- a/src/vst/reasampler_embed.cpp +++ b/src/shell/instrument/reasampler_embed.cpp @@ -1,22 +1,23 @@ +#include "core/namespaces.h" // reasampler_embed.cpp — see reasampler_embed.h. The IReaperUIEmbedInterface shell. // Windows-only (D5); guarded so a non-Windows build degrades to a stub that reports // "not supported" and draws nothing. -#include "reasampler_embed.h" +#include "shell/instrument/reasampler_embed.h" #include #include -#include "app_version.h" // vstPluginName (channel-derived embed label, S18) -#include "bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh) -#include "component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3) -#include "draw_kit.h" // the L1 draw kit: fillSurface/text (L3) -#include "editor_geometry.h" // Rect (shared with embed_strip) -#include "embed_strip.h" // the pure strip layout + hit-test +#include "core/version/app_version.h" // vstPluginName (channel-derived embed label, S18) +#include "core/instrument/map/bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh) +#include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3) +#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text (L3) +#include "core/instrument/ui/editor_geometry.h" // Rect (shared with embed_strip) +#include "core/instrument/ui/embed_strip.h" // the pure strip layout + hit-test #include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey -#include "reaper_bridge.h" +#include "shell/instrument/reaper_bridge.h" #include "reasampler_processor.h" -#include "theme.h" // Role / InteractionState / spectralColor (L3) +#include "core/ui/theme.h" // Role / InteractionState / spectralColor (L3) // wdltypes.h first: it defines INT_PTR portably (and pulls on Windows), which // reaper_plugin_fx_embed.h's REAPER_FXEMBED_IBitmap::Extended needs as its return type. @@ -48,7 +49,7 @@ namespace { // (component_geometry). Every embed surface now draws by palette ROLE via the L1 kit, retiring // the local pre-L1 forest-green palette + raw GDI DrawTextA. KitBox toKitBox(const Rect& r) { - return KitBox{r.left, r.top, r.width(), r.height()}; + return KitBox{r.x, r.y, r.width, r.height}; } // A short display name for a bank sample id, from the snapshotted list (the editor's helper, @@ -198,12 +199,12 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { if (map_.zones.empty()) { // No opt-in zones authored: a faint bg/cell band spanning the keymap area so the strip // reads as "present, no zones" — the default single-capture face lives in the editor. - LICE_FillRect(bmp, layout.keymap.left, layout.keymap.top, layout.keymap.width(), - layout.keymap.height(), toLice(roleColor(Role::BgCell)), 0.5f, 0); + LICE_FillRect(bmp, layout.keymap.x, layout.keymap.y, layout.keymap.width, + layout.keymap.height, toLice(roleColor(Role::BgCell)), 0.5f, 0); const std::string label = reasampler::vstPluginName() + // channel-derived (S18) (samples_.empty() ? " (bank empty)" : " (no zones)"); - const Rect labelR{layout.keymap.left + 4, layout.keymap.top, layout.keymap.right, - layout.keymap.bottom}; + const Rect labelR = Rect::ltrb(layout.keymap.x + 4, layout.keymap.y, layout.keymap.right(), + layout.keymap.bottom()); text(bmp, toKitBox(labelR), label.c_str(), Font::Label, Role::TextPrimary, Align::Left); } else { // Draw each zone as a segment across the keymap span, first-match order (so the painted @@ -214,26 +215,26 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { for (int i = 0; i < static_cast(map_.zones.size()); ++i) { const PerformanceZone& z = map_.zones[i]; const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote); - if (r.width() <= 0) continue; + if (r.width <= 0) continue; const bool sel = (i == selectedZone_); if (sel) { // Static glow halo, then the crisp accent-primary fill. - LICE_FillRect(bmp, r.left - 2, r.top, r.width() + 4, r.height(), + LICE_FillRect(bmp, r.x - 2, r.y, r.width + 4, r.height, toLice(roleColor(Role::AccentHot)), 0.30f, 0); - LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(), + LICE_FillRect(bmp, r.x, r.y, r.width, r.height, toLice(roleColor(Role::AccentPrimary)), 1.0f, 0); } else { const double t = ((z.lowNote + z.highNote) * 0.5) / 127.0; - LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(), + LICE_FillRect(bmp, r.x, r.y, r.width, r.height, toLice(spectralColor(t)), 0.65f, 0); } - LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, + LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); // Label the segment with the sample name when it is wide enough to read. The // selected (accent-fill) segment draws its label in bg/base for contrast (the // tight text-on-pastel pair, §4); the rest in text/primary. - if (r.width() >= 24) { - const Rect lr{r.left + 3, r.top, r.right - 2, r.bottom}; + if (r.width >= 24) { + const Rect lr = Rect::ltrb(r.x + 3, r.y, r.right() - 2, r.bottom()); text(bmp, toKitBox(lr), sampleLabel(samples_, z.sampleId).c_str(), Font::Label, sel ? Role::BgBase : Role::TextPrimary, Align::Left); } @@ -242,12 +243,12 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { // The level band: a recessed bg/cell channel with an accent-primary fill following the // live activity level (a direct level follow — the one permitted "motion", §3.5). - if (layout.levelBand.height() > 0) { + if (layout.levelBand.height > 0) { fillSurface(bmp, toKitBox(layout.levelBand), Role::BgCell, InteractionState::Pressed); const double level = processor_ ? processor_->embedActivityLevel() : 0.0; const Rect fill = levelFillRect(layout, level); - if (fill.width() > 0) { - LICE_FillRect(bmp, fill.left, fill.top, fill.width(), fill.height(), + if (fill.width > 0) { + LICE_FillRect(bmp, fill.x, fill.y, fill.width, fill.height, toLice(roleColor(Role::AccentPrimary)), 1.0f, 0); } } diff --git a/src/vst/reasampler_embed.h b/src/shell/instrument/reasampler_embed.h similarity index 97% rename from src/vst/reasampler_embed.h rename to src/shell/instrument/reasampler_embed.h index 33a017c..23e9b56 100644 --- a/src/vst/reasampler_embed.h +++ b/src/shell/instrument/reasampler_embed.h @@ -31,6 +31,7 @@ // REAPER's messages to/from it and draws with the same LICE idiom as reasampler_editor. #pragma once +#include "core/namespaces.h" #include #include @@ -38,7 +39,7 @@ #include "pluginterfaces/base/funknown.h" -#include "sample_map.h" // SampleChoice, PerformanceMap (the state the strip reflects) +#include "core/instrument/map/sample_map.h" // SampleChoice, PerformanceMap (the state the strip reflects) // REAPER's VST3-side embed interface (vendored). Uses UNQUALIFIED Steinberg types, so it is // pulled into the Steinberg namespace the same way reaper_bridge.cpp includes the host diff --git a/src/vst/reasampler_vst.h b/src/shell/instrument/reasampler_vst.h similarity index 95% rename from src/vst/reasampler_vst.h rename to src/shell/instrument/reasampler_vst.h index abd5fc8..5e99b76 100644 --- a/src/vst/reasampler_vst.h +++ b/src/shell/instrument/reasampler_vst.h @@ -18,10 +18,11 @@ // binary UID identity — the string identity lives in the pure module). #pragma once +#include "core/namespaces.h" #include "pluginterfaces/base/funknown.h" -#include "reasampler_uid.h" // the FROZEN UID macros + channel selection (SDK-free values) +#include "core/wire/reasampler_uid.h" // the FROZEN UID macros + channel selection (SDK-free values) namespace reasampler::vst { diff --git a/src/vst/vst_entry.cpp b/src/shell/instrument/vst_entry.cpp similarity index 95% rename from src/vst/vst_entry.cpp rename to src/shell/instrument/vst_entry.cpp index ea49a15..4b32533 100644 --- a/src/vst/vst_entry.cpp +++ b/src/shell/instrument/vst_entry.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // vst_entry.cpp — the VST3 module class factory (Phase S1). Enumerates the one class // this module offers (the ReaSampler instrument) via the SDK's factory macros. The // Windows module exports — GetPluginFactory (here, via BEGIN_FACTORY) and @@ -21,10 +22,10 @@ #include "pluginterfaces/vst/ivstaudioprocessor.h" // kVstAudioEffectClass, PlugType -#include "app_version.h" // vstPluginName / appVersion — the channel-derived identity +#include "core/version/app_version.h" // vstPluginName / appVersion — the channel-derived identity #include "ext_keys.h" // kProjExtNamespace — the pairing-surface assertion target #include "reasampler_processor.h" -#include "reasampler_vst.h" // channel-selected class UID (REASAMPLER_ACTIVE_UID_*) +#include "shell/instrument/reasampler_vst.h" // channel-selected class UID (REASAMPLER_ACTIVE_UID_*) // CHANNEL PAIRING INVARIANT (S18). The instrument's PLUGIN identity forks by the ONE channel // bit (REASAMPLER_CHANNEL_IS_BETA — the class UID selected in reasampler_vst.h, the filename diff --git a/src/draw_kit.cpp b/src/shell/panel/draw_kit.cpp similarity index 98% rename from src/draw_kit.cpp rename to src/shell/panel/draw_kit.cpp index 4c935fa..bc7587e 100644 --- a/src/draw_kit.cpp +++ b/src/shell/panel/draw_kit.cpp @@ -1,14 +1,15 @@ +#include "core/namespaces.h" // draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h. // // Compiled into the reaper_reasampler MODULE. SHELL layer: it is the only kit file that // touches LICE + SWELL. All colors come from the pure `theme` module; all geometry from // the pure `component_geometry` module. DAW-verified, not unit-tested. -#include "draw_kit.h" +#include "shell/panel/draw_kit.h" #include -#include "bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure) +#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure) // SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is // provided by the host. Mirrors bank_panel.cpp's include discipline. diff --git a/src/draw_kit.h b/src/shell/panel/draw_kit.h similarity index 96% rename from src/draw_kit.h rename to src/shell/panel/draw_kit.h index ba69dae..c8696d0 100644 --- a/src/draw_kit.h +++ b/src/shell/panel/draw_kit.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // draw_kit — the LICE-facing SHELL half of the shared drawing kit (Phase L, L1). This is // the ONE source of drawing for the whole system: every surface (bank_panel now; the VST // editor + embed strip at L3) fills, buttons, rows, sliders, waveforms, and — above all — @@ -23,9 +24,9 @@ // DOUBLE-BUFFER DISCIPLINE (§3.5 "zero-jank"): every function here draws into the caller's // offscreen LICE_IBitmap; the caller BitBlt's once. Nothing here draws direct-to-DC. -#include "component_geometry.h" // KitBox / SliderGeometry — the pure geometry the shell draws -#include "peaks.h" // Envelope — the waveform primitive's input -#include "theme.h" // Role / InteractionState / KitColor / TextClass +#include "core/ui/component_geometry.h" // KitBox / SliderGeometry — the pure geometry the shell draws +#include "core/audio/peaks.h" // Envelope — the waveform primitive's input +#include "core/ui/theme.h" // Role / InteractionState / KitColor / TextClass // LICE types at the boundary (this is the shell half). LICE_IBitmap is forward-declared // to keep the header light. LICE_pixel is a typedef (unsigned int) — not forward-declarable diff --git a/src/usage_scan.cpp b/src/shell/persist/usage_scan.cpp similarity index 96% rename from src/usage_scan.cpp rename to src/shell/persist/usage_scan.cpp index 49b91d8..416ab52 100644 --- a/src/usage_scan.cpp +++ b/src/shell/persist/usage_scan.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // usage_scan.cpp — see usage_scan.h. The REAPER reads behind the pS-usage prune // protection; every decision is in the pure sample_usage module, this TU only reads. // @@ -16,7 +17,7 @@ // * TakeFX_GetCount / TakeFX_GetNamedConfigParm (~6710/6774) // * guidToString (via track_guid::guidString) -#include "usage_scan.h" +#include "shell/persist/usage_scan.h" #include #include @@ -25,11 +26,11 @@ #include #include -#include "app_version.h" // vstPluginName / vstOutputName (channel name needles) +#include "core/version/app_version.h" // vstPluginName / vstOutputName (channel name needles) #include "ext_keys.h" // kProjExtNamespace / kProjExtUsageKeyPrefix -#include "instrument_drop.h" // vstClassIdHex — the frozen channel class-UID hex -#include "sample_usage.h" // identityMatches, foldUsageRecords (the pure decisions) -#include "track_guid.h" // guidString — the ONE canonical GUID key formatter +#include "core/wire/instrument_drop.h" // vstClassIdHex — the frozen channel class-UID hex +#include "core/wire/sample_usage.h" // identityMatches, foldUsageRecords (the pure decisions) +#include "shell/capture/track_guid.h" // guidString — the ONE canonical GUID key formatter #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjExtState diff --git a/src/usage_scan.h b/src/shell/persist/usage_scan.h similarity index 99% rename from src/usage_scan.h rename to src/shell/persist/usage_scan.h index e9fd5d8..c1bfe0b 100644 --- a/src/usage_scan.h +++ b/src/shell/persist/usage_scan.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // usage_scan — the EXTENSION-side shell of the pS-usage seam (see sample_usage.h for // the pure core, the fail-safe folds, and the full design note). At prune-scan time it // answers ONE question: which project-relative bank paths are held by a LIVE ReaSampler diff --git a/src/view.cpp b/src/shell/view/view.cpp similarity index 99% rename from src/view.cpp rename to src/shell/view/view.cpp index a6c4321..3c6f5fd 100644 --- a/src/view.cpp +++ b/src/shell/view/view.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // view.cpp — REAPER-facing Design View shell (Phase D2). See view.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h @@ -8,7 +9,7 @@ // module so it is unit-tested outside the DAW; this file owns only the REAPER // reads/writes and the snapshot-before-park ordering. -#include "view.h" +#include "shell/view/view.h" #include #include @@ -18,10 +19,10 @@ #include #include -#include "item_read.h" -#include "lane_keys.h" -#include "track_guid.h" -#include "view_tree.h" +#include "shell/capture/item_read.h" +#include "core/view/lane_keys.h" +#include "shell/capture/track_guid.h" +#include "core/view/view_tree.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountTracks diff --git a/src/view.h b/src/shell/view/view.h similarity index 98% rename from src/view.h rename to src/shell/view/view.h index ae5feb0..4ad8c58 100644 --- a/src/view.h +++ b/src/shell/view/view.h @@ -1,4 +1,5 @@ #pragma once +#include "core/namespaces.h" // view — the REAPER-facing shell of the Design View feature (Phase D2). It is the // mirror of the capture shell: the ViewModeModel (pure, D1) holds the mode/ // membership/snapshot state and emits the toggle plan; this shell reads the live @@ -24,7 +25,7 @@ #include -#include "view_mode_model.h" +#include "core/view/view_mode_model.h" // REAPER's opaque project handle. Forward-declared to keep this header SDK-free; // the .cpp includes reaper_plugin_functions.h and sees the real class. diff --git a/src/vst/curve_popup.cpp b/src/vst/curve_popup.cpp deleted file mode 100644 index 07ffe53..0000000 --- a/src/vst/curve_popup.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// curve_popup.cpp — see curve_popup.h. Pure arithmetic; no LICE/VST3/REAPER includes. - -#include "curve_popup.h" - -#include - -namespace reasampler::vst { - -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{left, top, left + sheetW, top + sheetH}; - - const int titleBottom = out.sheet.top + kCurvePopupTitleH; - const int closeTop = out.sheet.top + (kCurvePopupTitleH - kCurvePopupCloseSize) / 2; - out.close = Rect{out.sheet.right - kCurvePopupPad - kCurvePopupCloseSize, closeTop, - out.sheet.right - kCurvePopupPad, closeTop + kCurvePopupCloseSize}; - out.title = Rect{out.sheet.left + kCurvePopupPad, out.sheet.top, - out.close.left - kCurvePopupPad, titleBottom}; - - out.curveBox = Rect{out.sheet.left + 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::vst diff --git a/src/vst/reasampler_editor.cpp b/src/vst/reasampler_editor.cpp index 13f8a6c..0bf74fb 100644 --- a/src/vst/reasampler_editor.cpp +++ b/src/vst/reasampler_editor.cpp @@ -1,3 +1,5 @@ +#include "core/namespaces.h" +#include "core/util/clamp01.h" // reasampler_editor.cpp — see reasampler_editor.h. The IPlugView<->LICE bridge for the // ReaSampler 9000 capture-first editor (Phase S10). Windows-only (D5); the whole file is // guarded so a non-Windows build (not a target) degrades to the CPluginView defaults. @@ -11,27 +13,28 @@ #include #include -#include "browser_scroll.h" // S12 scroll-window + thumb + type-to-filter search geometry -#include "capture_browser.h" -#include "capture_paths.h" // resolveBankFile (shared M4 path resolution) -#include "component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3) -#include "curve_popup.h" // r11 centered curve-popup sheet geometry (FB1) -#include "draw_kit.h" // the L1 draw kit: fillSurface/drawButton/text/drawWaveform (L3) -#include "editor_geometry.h" // Rect, contains +#include "core/instrument/ui/browser_scroll.h" // S12 scroll-window + thumb + type-to-filter search geometry +#include "core/instrument/ui/capture_browser.h" +#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution) +#include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3) +#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03) +#include "core/instrument/ui/curve_popup.h" // r11 centered curve-popup sheet geometry (FB1) +#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/drawButton/text/drawWaveform (L3) +#include "core/instrument/ui/editor_geometry.h" // Rect, contains #include "ext_keys.h" -#include "keyboard_strip.h" -#include "master_gain.h" // r11 master-gain dB<->linear<->knob taper (FB1) -#include "theme.h" // Role / InteractionState / KitColor / spectralColor (L3) -#include "note_entry.h" // S12 direct numeric note-entry parse -#include "param_slider.h" // the FA4 radial-knob primitive (value<->needle map, drag delta) -#include "peaks.h" // computeEnvelope -#include "reaper_bridge.h" +#include "core/instrument/ui/keyboard_strip.h" +#include "core/instrument/engine/master_gain.h" // r11 master-gain dB<->linear<->knob taper (FB1) +#include "core/ui/theme.h" // Role / InteractionState / KitColor / spectralColor (L3) +#include "core/instrument/map/note_entry.h" // S12 direct numeric note-entry parse +#include "core/instrument/ui/param_slider.h" // the FA4 radial-knob primitive (value<->needle map, drag delta) +#include "core/audio/peaks.h" // computeEnvelope +#include "shell/instrument/reaper_bridge.h" #include "reasampler_processor.h" -#include "app_version.h" // vstPluginName (channel-derived editor title band, S18) -#include "sample_map.h" -#include "wav_trim.h" // parseWavLayout, extractFloatFrames -#include "trigger_seam.h" // triggerPlayLength / framesToFadeFraction / fadeFractionToFrames (S-VIEW-3) -#include "waveform_view.h" // frame<->pixel markers + zero-crossing snap (S11) +#include "core/version/app_version.h" // vstPluginName (channel-derived editor title band, S18) +#include "core/instrument/map/sample_map.h" +#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames +#include "core/instrument/map/trigger_seam.h" // triggerPlayLength / framesToFadeFraction / fadeFractionToFrames (S-VIEW-3) +#include "core/instrument/ui/waveform_view.h" // frame<->pixel markers + zero-crossing snap (S11) #ifdef _WIN32 #include // GET_X_LPARAM / GET_Y_LPARAM @@ -85,7 +88,7 @@ constexpr Role kRoleLoopMarker = Role::AccentTertiary; // through the L1 kit (theme roles + draw_kit), retiring the shell's raw LICE_RGBA palette + // GDI DrawTextA path. KitBox toKitBox(const Rect& r) { - return KitBox{r.left, r.top, r.width(), r.height()}; + return KitBox{r.x, r.y, r.width, r.height}; } // Kit text in a palette ROLE (the common case). Left/Right/Center via Align. @@ -399,7 +402,6 @@ constexpr double kFadeMaxSeconds = 2.0; // Trigger fade throw ceili constexpr double kPitchDepthMaxSemis = 24.0; // AD pitch depth throw: +/-24 st, centered constexpr double kKeyTrackMax = 2.0; // S-VIEW-6 key-track slider ceiling (0..200%) -double clamp01(double v) { return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v); } } // namespace double ReaSamplerEditor::controlValue(int id, const ZonePlaySeconds& play) const { @@ -770,16 +772,8 @@ const std::vector& ReaSamplerEditor::monoPcmFor(const std::string& if (!relativePath.empty()) { const std::string projectDir = processor_->bridge().activeProjectDir(); const std::string abs = resolveBankFile(projectDir, relativePath); - std::vector bytes; - std::ifstream f(abs, std::ios::binary | std::ios::ate); - if (f) { - const std::streamoff size = f.tellg(); - if (size > 0) { - f.seekg(0, std::ios::beg); - bytes.resize(static_cast(size)); - if (!f.read(reinterpret_cast(bytes.data()), size)) bytes.clear(); - } - } + // Shared core/util whole-file loader (Q-W1, T2-03): empty on any failure. + const std::vector bytes = readFileBytes(abs); const WavLayout layout = parseWavLayout(bytes); if (layout.valid) { std::vector interleaved = @@ -959,12 +953,12 @@ struct SampleBands { SampleBands computeSampleBands(int w, int h, int deckH) { SampleBands b; const int titleH = (std::min)(kTitleHeight, h); - b.title = Rect{0, 0, w, titleH}; + b.title = Rect::ltrb(0, 0, w, titleH); // Two nav buttons right-anchored in the title band (Browse then Zone). const int navTop = 2; const int navBot = (std::max)(navTop, titleH - 2); - const Rect zone{w - kPad - kNavButtonWidth, navTop, w - kPad, navBot}; - const Rect browse{zone.left - 4 - kNavButtonWidth, navTop, zone.left - 4, navBot}; + const Rect zone = Rect::ltrb(w - kPad - kNavButtonWidth, navTop, w - kPad, navBot); + const Rect browse = Rect::ltrb(zone.x - 4 - kNavButtonWidth, navTop, zone.x - 4, navBot); b.navBrowse = browse; b.navZone = zone; @@ -976,9 +970,9 @@ SampleBands computeSampleBands(int w, int h, int deckH) { clusterTop = heroBottom + 4; deckTop = clusterTop + kClusterHeight + 4; } - b.hero = Rect{kPad, titleH, w - kPad, heroBottom}; - b.cluster = Rect{0, clusterTop, w, clusterTop + kClusterHeight}; - b.deck = Rect{kPad, deckTop, w - kPad, deckTop + deckH}; + b.hero = Rect::ltrb(kPad, titleH, w - kPad, heroBottom); + b.cluster = Rect::ltrb(0, clusterTop, w, clusterTop + kClusterHeight); + b.deck = Rect::ltrb(kPad, deckTop, w - kPad, deckTop + deckH); return b; } @@ -995,49 +989,49 @@ struct ClusterRects { }; ClusterRects clusterRects(const Rect& cluster, const Rect& chanMono) { ClusterRects r; - const int stripTop = cluster.top + (cluster.height() - kStripBandHeight) / 2; + const int stripTop = cluster.y + (cluster.height - kStripBandHeight) / 2; const int stripBot = stripTop + kStripBandHeight; - const int curveTop = cluster.top + (cluster.height() - kCurveBtnSize) / 2; - r.curveBtn = Rect{chanMono.left - kPad - kCurveBtnSize, curveTop, - chanMono.left - kPad, curveTop + kCurveBtnSize}; - r.velCell = Rect{r.curveBtn.left - kPad - kVelCellW, stripTop, - r.curveBtn.left - kPad, stripBot}; - const int knobLeft = r.velCell.left + (kVelCellW - kDeckKnobSize) / 2; - r.velKnob = Rect{knobLeft, r.velCell.top, knobLeft + kDeckKnobSize, - r.velCell.top + kDeckKnobSize}; - r.velLabel = Rect{r.velCell.left, r.velKnob.bottom, r.velCell.right, r.velCell.bottom}; - r.preview = Rect{r.velCell.left - kPad - kPreviewBtnW, stripTop, - r.velCell.left - kPad, stripBot}; - r.rootStrip = Rect{cluster.left + kPad, stripTop, r.preview.left - kPad, stripBot}; + const int curveTop = cluster.y + (cluster.height - kCurveBtnSize) / 2; + r.curveBtn = Rect::ltrb(chanMono.x - kPad - kCurveBtnSize, curveTop, + chanMono.x - kPad, curveTop + kCurveBtnSize); + r.velCell = Rect::ltrb(r.curveBtn.x - kPad - kVelCellW, stripTop, + r.curveBtn.x - kPad, stripBot); + const int knobLeft = r.velCell.x + (kVelCellW - kDeckKnobSize) / 2; + r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + kDeckKnobSize, + r.velCell.y + kDeckKnobSize); + r.velLabel = Rect::ltrb(r.velCell.x, r.velKnob.bottom(), r.velCell.right(), r.velCell.bottom()); + r.preview = Rect::ltrb(r.velCell.x - kPad - kPreviewBtnW, stripTop, + r.velCell.x - kPad, stripBot); + r.rootStrip = Rect::ltrb(cluster.x + kPad, stripTop, r.preview.x - kPad, stripBot); return r; } // The Zone-view keyboard strip rect. Zone content sits below the "+ Add Zone" affordance // (top+4, height 20) with a 12px gap, padded 8px horizontally. All call sites use this formula. Rect zonesStripArea(const Rect& content) { - const int stripTop = content.top + 4 + 20 + 12; // addR.bottom + 12 - return Rect{content.left + kPad, stripTop, content.right - kPad, - stripTop + kStripBandHeight}; + const int stripTop = content.y + 4 + 20 + 12; // addR.bottom() + 12 + return Rect::ltrb(content.x + kPad, stripTop, content.right() - kPad, + stripTop + kStripBandHeight); } // The S12 numeric-entry field ROW area inside the Zones legend: a band to the right of the // sample label on the legend row. Three equal fields (low/high/root) tile it. Both draw + -// hit-test use this single formula so they never drift. Anchored off zonesStripArea.bottom so +// hit-test use this single formula so they never drift. Anchored off zonesStripArea.bottom() so // the legend top tracks the strip bottom without re-inlining the strip arithmetic here. Rect noteEntryFieldsArea(const Rect& content) { - const int stripBottom = zonesStripArea(content).bottom; - const int top = stripBottom + 8; // legendTop (== zonesStripArea.bottom + 8) - return Rect{content.left + 8 + 128, top, content.right - 8, top + 18}; + const int stripBottom = zonesStripArea(content).bottom(); + const int top = stripBottom + 8; // legendTop (== zonesStripArea.bottom() + 8) + return Rect::ltrb(content.x + 8 + 128, top, content.right() - 8, top + 18); } // The rect of note-entry field `f` (0=low, 1=high, 2=root) within the fields area: three equal // segments left-to-right. An out-of-range index yields an empty rect. Rect noteEntryFieldRect(const Rect& fields, int f) { - if (f < 0 || f > 2 || fields.width() <= 0) return Rect{}; - const int segW = fields.width() / 3; - const int left = fields.left + f * segW + (f > 0 ? 4 : 0); // small inter-field gap - const int right = (f == 2) ? fields.right : fields.left + (f + 1) * segW; - return Rect{left, fields.top, right, fields.bottom}; + if (f < 0 || f > 2 || fields.width <= 0) return Rect{}; + const int segW = fields.width / 3; + const int left = fields.x + f * segW + (f > 0 ? 4 : 0); // small inter-field gap + const int right = (f == 2) ? fields.right() : fields.x + (f + 1) * segW; + return Rect::ltrb(left, fields.y, right, fields.bottom()); } // The S12/S15/S16 parameter-control panel rect inside the Zones content: below the strip + @@ -1045,9 +1039,9 @@ Rect noteEntryFieldRect(const Rect& fields, int f) { // Zones mode-content area. Both draw + hit-test use this single formula so they never drift. Rect zonesControlPanel(const Rect& content) { const Rect strip = zonesStripArea(content); - const int panelTop = strip.bottom + 8 + 18 + 8; // strip + the 18px legend row + gap - return Rect{content.left + kPad, panelTop, content.right - kPad, - content.bottom - 4}; + const int panelTop = strip.bottom() + 8 + 18 + 8; // strip + the 18px legend row + gap + return Rect::ltrb(content.x + kPad, panelTop, content.right() - kPad, + content.bottom() - 4); } // FB2 (R11-F2 parity): the Zone panel's per-zone controls render as the SAME knob deck the @@ -1059,22 +1053,22 @@ Rect zonesControlPanel(const Rect& content) { // drift. Rect zonesDeckArea(const Rect& content) { const Rect panel = zonesControlPanel(content); - return Rect{panel.left, panel.top, panel.right - kCurveBtnSize - kPad, panel.bottom}; + return Rect::ltrb(panel.x, panel.y, panel.right() - kCurveBtnSize - kPad, panel.bottom()); } // The Zone panel's mini curve-preview button (opens the SAME popup editor as the Sample // cluster's button): the cluster's 28px square, right-anchored at the panel top. Rect zonesCurveButton(const Rect& content) { const Rect panel = zonesControlPanel(content); - return Rect{panel.right - kCurveBtnSize, panel.top, panel.right, panel.top + kCurveBtnSize}; + return Rect::ltrb(panel.right() - kCurveBtnSize, panel.y, panel.right(), panel.y + kCurveBtnSize); } // The pure-module mapping Box for a drawn curve rect: inset from the border so node handles and // the pick radius stay inside the box. Every consumer (paint, hit-test, add, drag) derives the // Box through this ONE formula, so drawn nodes and grabs can never drift apart. VelocityCurve::Box curveBoxFromRect(const Rect& r) { - return VelocityCurve::Box{r.left + kVelCurveInset, r.top + kVelCurveInset, - (std::max)(0, r.width() - 2 * kVelCurveInset), - (std::max)(0, r.height() - 2 * kVelCurveInset)}; + return VelocityCurve::Box{r.x + kVelCurveInset, r.y + kVelCurveInset, + (std::max)(0, r.width - 2 * kVelCurveInset), + (std::max)(0, r.height - 2 * kVelCurveInset)}; } // The S7 mono/stereo toggle (S-VIEW-2: moved here from Browse to the Sample cluster band — it is @@ -1085,10 +1079,10 @@ constexpr int kChanSegW = 52; constexpr int kChanSegH = 18; struct ChannelToggleRects { Rect mono; Rect stereo; }; ChannelToggleRects channelToggleRects(const Rect& area) { - const int top = area.top + (area.height() - kChanSegH) / 2; - const int right = area.right - kPad; - const Rect stereo{right - kChanSegW, top, right, top + kChanSegH}; - const Rect mono{stereo.left - kChanSegW, top, stereo.left, top + kChanSegH}; + const int top = area.y + (area.height - kChanSegH) / 2; + const int right = area.right() - kPad; + const Rect stereo = Rect::ltrb(right - kChanSegW, top, right, top + kChanSegH); + const Rect mono = Rect::ltrb(stereo.x - kChanSegW, top, stereo.x, top + kChanSegH); return {mono, stereo}; } @@ -1142,11 +1136,11 @@ void drawKnobFace(LICE_IBitmap* bmp, const Rect& knobRect, double value01, // faint per-octave hairline ticks for orientation. Shared by the setup face + the Zones strip // so both read as the same spectrum. `stripArea` is the absolute strip rect. void drawSpectralStrip(LICE_IBitmap* bmp, const Rect& stripArea) { - if (stripArea.width() <= 0 || stripArea.height() <= 0) return; - const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); - const int sx = stripArea.left; - const int sy = stripArea.top; - const int h = stripArea.height(); + if (stripArea.width <= 0 || stripArea.height <= 0) return; + const StripLayout sl = layoutStrip(stripArea.width, stripArea.height); + const int sx = stripArea.x; + const int sy = stripArea.y; + const int h = stripArea.height; // A pastel spectral column per key. Each key's local x from keyRect; fill from this key's // left to the next key's left so the sweep tiles with no gaps. Low alpha keeps it a quiet // backdrop the root/zone marks sit over. S-VIEW-7: OVERLAY the two-tone piano-key pattern — @@ -1156,8 +1150,8 @@ void drawSpectralStrip(LICE_IBitmap* bmp, const Rect& stripArea) { const LICE_pixel darkKey = toLice(roleColor(Role::BgBase)); for (int n = 0; n <= 127; ++n) { const Rect k = keyRect(sl, n); - const int x0 = k.left + sx; - const int x1 = (n < 127) ? keyRect(sl, n + 1).left + sx : stripArea.right; + const int x0 = k.x + sx; + const int x1 = (n < 127) ? keyRect(sl, n + 1).x + sx : stripArea.right(); const int cw = (std::max)(1, x1 - x0); const KitColor hue = spectralColor(static_cast(n) / 127.0); LICE_FillRect(bmp, x0, sy, cw, h, toLice(hue), 0.55f, 0); @@ -1171,23 +1165,23 @@ void drawSpectralStrip(LICE_IBitmap* bmp, const Rect& stripArea) { const LICE_pixel tick = toLice(roleColor(Role::LineHairline)); for (int n = 0; n <= 127; n += 12) { const Rect k = keyRect(sl, n); - LICE_Line(bmp, k.left + sx, sy, k.left + sx, sy + h, tick, 1.0f, 0, false); + LICE_Line(bmp, k.x + sx, sy, k.x + sx, sy + h, tick, 1.0f, 0, false); } } // Draw the single-capture root marker on the strip: an accent-primary bar with a soft STATIC // glow (a wider, lower-alpha accent bar behind it) — the "this is live" mark. Never animated. void drawRootMarker(LICE_IBitmap* bmp, const Rect& stripArea, const StripLayout& sl, int root) { - const int sx = stripArea.left; - const int sy = stripArea.top; - const int h = stripArea.height(); + const int sx = stripArea.x; + const int sy = stripArea.y; + const int h = stripArea.height; const Rect marker = rootMarkerRect(sl, root); - const int mw = (std::max)(2, marker.width()); + const int mw = (std::max)(2, marker.width); const LICE_pixel accent = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel glow = toLice(roleColor(Role::AccentHot)); // Static glow: a wider low-alpha halo behind the crisp bar (a drawn state, not a pulse). - LICE_FillRect(bmp, marker.left + sx - 3, sy, mw + 6, h, glow, 0.30f, 0); - LICE_FillRect(bmp, marker.left + sx, sy, mw, h, accent, 1.0f, 0); + LICE_FillRect(bmp, marker.x + sx - 3, sy, mw + 6, h, glow, 0.30f, 0); + LICE_FillRect(bmp, marker.x + sx, sy, mw, h, accent, 1.0f, 0); } } // namespace @@ -1217,7 +1211,7 @@ void ReaSamplerEditor::paint(HDC hdc) { if (dropHintTicks_ > 0) { const int bannerTop = (std::min)(kTitleHeight, h); const int bannerH = (std::min)(kTitleHeight + 8, (std::max)(0, h - bannerTop)); - Rect banner{0, bannerTop, w, bannerTop + bannerH}; + Rect banner = Rect::ltrb(0, bannerTop, w, bannerTop + bannerH); // A transient notice, not the live layer — draw it on the accent-tertiary categorical // hue with a dark label so it reads as "attention, not action". fillSurface(&bmp, toKitBox(banner), Role::AccentTertiary, InteractionState::Rest); @@ -1234,7 +1228,7 @@ void ReaSamplerEditor::paint(HDC hdc) { namespace { void drawTitleBand(LICE_IBitmap* bmp, const Rect& title, const std::string& readout) { fillSurface(bmp, toKitBox(title), Role::BgPanel, InteractionState::Rest); - Rect titleText{title.left + 8, title.top, title.right - 8, title.bottom}; + Rect titleText = Rect::ltrb(title.x + 8, title.y, title.right() - 8, title.bottom()); kitText(bmp, titleText, readout.c_str(), Font::Title, Role::TextPrimary); } } // namespace @@ -1285,7 +1279,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { // Nothing loaded yet: the Sample face is the empty state — a "pick a capture" prompt pointing // at Browse (which is lit above). No hero waveform / controls to draw. if (empty) { - Rect body{bands.hero.left, bands.hero.top, bands.hero.right, bands.deck.bottom}; + Rect body = Rect::ltrb(bands.hero.x, bands.hero.y, bands.hero.right(), bands.deck.bottom()); paintEmptyState(bmp, body); return; } @@ -1300,7 +1294,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { const std::int64_t frames = static_cast(pcm.size()); const Rect waveArea = bands.hero; fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest); - if (frames > 0 && waveArea.width() > 0) { + if (frames > 0 && waveArea.width > 0) { // FA3 gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this // multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact // partition — extra bins produce no visible change. Clamped to frame count below. @@ -1317,7 +1311,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { const int lx = frameToX(waveArea, frames, m.loopStart); const int rx = frameToX(waveArea, frames, m.loopEnd); if (rx > lx) { - LICE_FillRect(bmp, lx, waveArea.top, rx - lx, waveArea.height(), + LICE_FillRect(bmp, lx, waveArea.y, rx - lx, waveArea.height, toLice(roleColor(kRoleLoopMarker)), 0.20f, 0); } } @@ -1327,7 +1321,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { const int mx = frameToX(waveArea, frames, markerFrames[i]); const bool loopMarker = (i != 0); const float alpha = (loopMarker && !m.hasLoop) ? 0.4f : 1.0f; - LICE_FillRect(bmp, mx - 1, waveArea.top, 2, waveArea.height(), + LICE_FillRect(bmp, mx - 1, waveArea.y, 2, waveArea.height, toLice(roleColor(markerRoles[i])), alpha, 0); } @@ -1343,9 +1337,9 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { const ChannelToggleRects chan = channelToggleRects(bands.cluster); const ClusterRects cr = clusterRects(bands.cluster, chan.mono); int root = effectiveRoot(); - if (cr.rootStrip.width() > 0) { + if (cr.rootStrip.width > 0) { drawSpectralStrip(bmp, cr.rootStrip); - const StripLayout sl = layoutStrip(cr.rootStrip.width(), cr.rootStrip.height()); + const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height); drawRootMarker(bmp, cr.rootStrip, sl, root); } @@ -1399,7 +1393,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone, std::int64_t frames) { - if (frames <= 0 || waveArea.width() <= 0 || waveArea.height() <= 0) return; + if (frames <= 0 || waveArea.width <= 0 || waveArea.height <= 0) return; const double rate = liveSampleRate(); if (rate <= 0.0) return; const double totalSeconds = static_cast(frames) / rate; @@ -1411,14 +1405,14 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveA // curve over the waveform. Clip x to the wave rect (a Gate release tail maps past the right). const LICE_pixel line = toLice(roleColor(Role::AccentSecondary)); for (std::size_t i = 1; i < poly.size(); ++i) { - const int x0 = (std::max)(waveArea.left, (std::min)(waveArea.right - 1, poly[i - 1].x)); - const int x1 = (std::max)(waveArea.left, (std::min)(waveArea.right - 1, poly[i].x)); + const int x0 = (std::max)(waveArea.x, (std::min)(waveArea.right() - 1, poly[i - 1].x)); + const int x1 = (std::max)(waveArea.x, (std::min)(waveArea.right() - 1, poly[i].x)); LICE_Line(bmp, x0, poly[i - 1].y, x1, poly[i].y, line, 1.0f, 0, true); } // Draggable node handles: a small square per DRAGGABLE node (Origin + ReleaseStart are draw- // only). Lit accent-hot when this node is the grabbed one. FA2 guarantees every vertex is // in-bounds (the pre-FA2 right-edge clip is dead and removed — edge nodes like ReleaseEnd - // at area.right-1 MUST get handles); the handle SQUARE is additionally clamped inside the + // at area.right()-1 MUST get handles); the handle SQUARE is additionally clamped inside the // hero rect so a 6px box on an edge node never overhangs into the neighbouring bands. const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); @@ -1426,21 +1420,21 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveA if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseStart) continue; const bool grabbed = (drag_ == DragKind::kEnvNode && envNode_ == v.node); const int r = 3; - const int hx = (std::max)(waveArea.left + r, (std::min)(waveArea.right - 1 - r, v.x)); - const int hy = (std::max)(waveArea.top + r, (std::min)(waveArea.bottom - 1 - r, v.y)); + const int hx = (std::max)(waveArea.x + r, (std::min)(waveArea.right() - 1 - r, v.x)); + const int hy = (std::max)(waveArea.y + r, (std::min)(waveArea.bottom() - 1 - r, v.y)); LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, grabbed ? handleHot : handle, 1.0f, 0); } } void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone) { - if (r.width() <= 0 || r.height() <= 0) return; // defensive (degenerate rect) + if (r.width <= 0 || r.height <= 0) return; // defensive (degenerate rect) // The bordered box: a panel surface + hairline border, drawn by palette role. No corner // caption — the popup sheet's own "VELOCITY -> AMP" title labels this context (FB2: the // popup is the only host). fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest); - LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, + LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); const VelocityCurve::Box box = curveBoxFromRect(r); @@ -1469,12 +1463,12 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); const LICE_pixel handleWarn = toLice(roleColor(Role::Warn)); // Drag-off check: during a kCurveNode drag on THIS box, is the live cursor beyond the margin? - const bool dragOffArmed = (drag_ == DragKind::kCurveNode && dragCurveRect_.left == r.left && - dragCurveRect_.top == r.top) && - (dragCurX_ < r.left - kCurveDragOffMargin || - dragCurX_ > r.right + kCurveDragOffMargin || - dragCurY_ < r.top - kCurveDragOffMargin || - dragCurY_ > r.bottom + kCurveDragOffMargin); + const bool dragOffArmed = (drag_ == DragKind::kCurveNode && dragCurveRect_.x == r.x && + dragCurveRect_.y == r.y) && + (dragCurX_ < r.x - kCurveDragOffMargin || + dragCurX_ > r.right() + kCurveDragOffMargin || + dragCurY_ < r.y - kCurveDragOffMargin || + dragCurY_ > r.bottom() + kCurveDragOffMargin); for (std::size_t i = 0; i < curve.points().size(); ++i) { const auto np = VelocityCurve::pixelFromPoint(box, curve.points()[i]); const bool grabbed = (drag_ == DragKind::kCurveNode && @@ -1491,8 +1485,8 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, const PerformanceZone& zone, const std::vector& descs) { - if (deckArea.width() <= 0 || deckArea.height() <= 0) return; - const DeckLayout dl = layoutDeck(descs, deckArea.left, deckArea.top, deckArea.width()); + if (deckArea.width <= 0 || deckArea.height <= 0) return; + const DeckLayout dl = layoutDeck(descs, deckArea.x, deckArea.y, deckArea.width); const ZonePlaySeconds& play = zone.play; const bool isMono = (voiceMode_ == VoiceMode::Mono); const LICE_pixel hairline = toLice(roleColor(Role::LineHairline)); @@ -1545,7 +1539,7 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, for (const DeckGroupLayout& g : dl.groups) { // The fence: a bg/panel box with a hairline border, caption micro-caps left. fillSurface(bmp, toKitBox(g.box), Role::BgPanel, InteractionState::Rest); - LICE_DrawRect(bmp, g.box.left, g.box.top, g.box.width() - 1, g.box.height() - 1, + LICE_DrawRect(bmp, g.box.x, g.box.y, g.box.width - 1, g.box.height - 1, hairline, 1.0f, 0); const char* caption = ""; switch (g.id) { @@ -1606,7 +1600,7 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone) { - if (r.width() <= 0 || r.height() <= 0) return; + if (r.width <= 0 || r.height <= 0) return; // The mini curve-preview button (r11/FB2 — shared by the Sample cluster and the Zone // panel): a hairline-bordered bg/cell square with the zone's live velocity curve traced // in miniature (no node markers at this scale). Hover lifts it; it draws ACTIVE @@ -1617,11 +1611,11 @@ void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, hov ? InteractionState::Hover : InteractionState::Rest); const KitColor border = curvePopupOpen_ ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline); - LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, toLice(border), 1.0f, 0); + LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(border), 1.0f, 0); const VelocityCurve& curve = zone.velocityCurve; const int inset = 3; - const VelocityCurve::Box mini{r.left + inset, r.top + inset, r.width() - 2 * inset, - r.height() - 2 * inset}; + const VelocityCurve::Box mini{r.x + inset, r.y + inset, r.width - 2 * inset, + r.height - 2 * inset}; if (mini.width > 1 && mini.height > 1) { const LICE_pixel trace = toLice(roleColor(Role::AccentSecondary)); int prevX = 0, prevY = 0; @@ -1642,8 +1636,8 @@ void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) { LICE_FillRect(bmp, 0, 0, w, h, toLice(roleColor(Role::BgBase)), 0.50f, 0); const CurvePopupLayout pl = computeCurvePopup(w, h); fillSurface(bmp, toKitBox(pl.sheet), Role::BgPanel, InteractionState::Rest); - LICE_DrawRect(bmp, pl.sheet.left, pl.sheet.top, pl.sheet.width() - 1, - pl.sheet.height() - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); + LICE_DrawRect(bmp, pl.sheet.x, pl.sheet.y, pl.sheet.width - 1, + pl.sheet.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); kitText(bmp, pl.title, "VELOCITY -> AMP", Font::Micro, Role::TextDim); { const KitButtonBox box{toKitBox(pl.close)}; @@ -1766,8 +1760,8 @@ void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) { // Split the area so the primary line sits centered and the S13 ingest affordance sits just // below it. The affordance is the SHIPPED ingest gesture (drop onto the docked panel) — kept // discoverable here regardless of whether a drop ever lands on THIS window. - Rect primary{area.left, area.top, area.right, area.top + area.height() / 2}; - Rect hint{area.left, primary.bottom, area.right, area.bottom}; + Rect primary = Rect::ltrb(area.x, area.y, area.right(), area.y + area.height / 2); + Rect hint = Rect::ltrb(area.x, primary.bottom(), area.right(), area.bottom()); kitTextCentered(bmp, primary, msg, Font::Label, Role::TextDim); kitTextCentered(bmp, hint, "To add a sample: drop a file onto the ReaSampler bank panel (the docked window).", @@ -1791,18 +1785,18 @@ constexpr int kBrowseFooterH = 30; BrowseModal computeBrowseModal(int w, int h) { BrowseModal m; const int titleH = (std::min)(kTitleHeight, h); - m.title = Rect{0, 0, w, titleH}; - m.back = Rect{w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, titleH - 2)}; + m.title = Rect::ltrb(0, 0, w, titleH); + m.back = Rect::ltrb(w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, titleH - 2)); // Search box below the title, spanning the width (searchBoxRect lays it out from 0). const Rect sb = searchBoxRect(w); - m.search = Rect{kPad, titleH, w - kPad, titleH + sb.height()}; - const int footerTop = (std::max)(m.search.bottom, h - kBrowseFooterH); - m.content = Rect{0, m.search.bottom, w, footerTop}; + m.search = Rect::ltrb(kPad, titleH, w - kPad, titleH + sb.height); + const int footerTop = (std::max)(m.search.bottom(), h - kBrowseFooterH); + m.content = Rect::ltrb(0, m.search.bottom(), w, footerTop); // Footer: Cancel (left) + Load (right). const int fTop = footerTop + 3; const int fBot = (std::max)(fTop, h - 3); - m.cancel = Rect{kPad, fTop, kPad + 90, fBot}; - m.confirm = Rect{w - kPad - 90, fTop, w - kPad, fBot}; + m.cancel = Rect::ltrb(kPad, fTop, kPad + 90, fBot); + m.confirm = Rect::ltrb(w - kPad - 90, fTop, w - kPad, fBot); return m; } } // namespace @@ -1830,29 +1824,29 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { : InteractionState::Rest); fillSurface(bmp, toKitBox(searchAbs), Role::BgCell, searchState); if (searchFocused_) { - LICE_DrawRect(bmp, searchAbs.left, searchAbs.top, searchAbs.width() - 1, - searchAbs.height() - 1, toLice(roleColor(Role::TextPrimary)), 1.0f, 0); + LICE_DrawRect(bmp, searchAbs.x, searchAbs.y, searchAbs.width - 1, + searchAbs.height - 1, toLice(roleColor(Role::TextPrimary)), 1.0f, 0); } { std::string sb = searchQuery_.empty() ? std::string("Search captures...") : ("Search: " + searchQuery_ + (searchFocused_ ? "_" : "")); - Rect sbText{searchAbs.left + 6, searchAbs.top, searchAbs.right - 6, searchAbs.bottom}; + Rect sbText = Rect::ltrb(searchAbs.x + 6, searchAbs.y, searchAbs.right() - 6, searchAbs.bottom()); kitText(bmp, sbText, sb.c_str(), Font::Label, searchQuery_.empty() ? Role::TextDim : Role::TextPrimary); } // Tabs + card grid, laid out over the content sub-area by the pure module (origin-offset). const Rect browserArea = bm.content; - const BrowserLayout bl = layoutBrowser(browserArea.width(), browserArea.height()); - const int ox = browserArea.left; - const int oy = browserArea.top; + const BrowserLayout bl = layoutBrowser(browserArea.width, browserArea.height); + const int ox = browserArea.x; + const int oy = browserArea.y; scrollOffset_ = clampScrollOffset(bl, static_cast(visible_.size()), scrollOffset_); const int tabCount = static_cast(banks_.size()) + 1; for (int i = 0; i < tabCount; ++i) { Rect t = filterTabRect(bl, tabCount, i); - t = Rect{t.left + ox, t.top + oy, t.right + ox, t.bottom + oy}; + t = Rect::ltrb(t.x + ox, t.y + oy, t.right() + ox, t.bottom() + oy); const std::string label = (i == 0) ? "All" : banks_[static_cast(i - 1)].displayName; const bool active = (i == 0) ? activeFilterBankId_.empty() : (banks_[static_cast(i - 1)].id == activeFilterBankId_); @@ -1874,12 +1868,12 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { Rect content = cardContentRect(bl, i); Rect thumb = cardThumbnailRect(bl, i); Rect labelR = cardLabelRect(bl, i); - content = Rect{content.left + ox, content.top + oy - scrollOffset_, - content.right + ox, content.bottom + oy - scrollOffset_}; - thumb = Rect{thumb.left + ox, thumb.top + oy - scrollOffset_, - thumb.right + ox, thumb.bottom + oy - scrollOffset_}; - labelR = Rect{labelR.left + ox, labelR.top + oy - scrollOffset_, - labelR.right + ox, labelR.bottom + oy - scrollOffset_}; + content = Rect::ltrb(content.x + ox, content.y + oy - scrollOffset_, + content.right() + ox, content.bottom() + oy - scrollOffset_); + thumb = Rect::ltrb(thumb.x + ox, thumb.y + oy - scrollOffset_, + thumb.right() + ox, thumb.bottom() + oy - scrollOffset_); + labelR = Rect::ltrb(labelR.x + ox, labelR.y + oy - scrollOffset_, + labelR.right() + ox, labelR.bottom() + oy - scrollOffset_); const SampleChoice& s = visible_[static_cast(i)]; const bool pending = (s.id == browsePendingId_); @@ -1890,13 +1884,13 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { const KitColor cardBorder = pending ? roleColor(Role::AccentPrimary) : (loaded ? roleColor(Role::AccentTertiary) : roleColor(Role::LineHairline)); - LICE_DrawRect(bmp, content.left, content.top, content.width() - 1, content.height() - 1, + LICE_DrawRect(bmp, content.x, content.y, content.width - 1, content.height - 1, toLice(cardBorder), 1.0f, 0); drawEnvelope(bmp, thumb, thumbnailFor(s.id, bins)); std::string caption = s.displayName.empty() ? s.id : s.displayName; - Rect nameR{labelR.left + 3, labelR.top, labelR.right - 3, labelR.top + labelR.height() / 2}; - Rect badgeR{labelR.left + 3, nameR.bottom, labelR.right - 3, labelR.bottom}; + Rect nameR = Rect::ltrb(labelR.x + 3, labelR.y, labelR.right() - 3, labelR.y + labelR.height / 2); + Rect badgeR = Rect::ltrb(labelR.x + 3, nameR.bottom(), labelR.right() - 3, labelR.bottom()); kitText(bmp, nameR, caption.c_str(), Font::Label, Role::TextPrimary); std::string badge; if (s.rootNote) badge = "root " + noteLabel(*s.rootNote); @@ -1908,10 +1902,10 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { // Scrollbar thumb. { const Rect thumb = scrollThumbRect(bl, cardCount, scrollOffset_); - if (thumb.height() > 0) { + if (thumb.height > 0) { const bool dragging = (drag_ == DragKind::kScrollThumb); const KitColor tc = roleColor(dragging ? Role::AccentHot : Role::AccentPrimary); - LICE_FillRect(bmp, thumb.left + ox, thumb.top + oy, thumb.width(), thumb.height(), + LICE_FillRect(bmp, thumb.x + ox, thumb.y + oy, thumb.width, thumb.height, toLice(tc), 0.8f, 0); } } @@ -1920,7 +1914,7 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { // Footer: Cancel (discard, return to Sample) + Load (commit the pending pick). Load is inert // (no accent) until a card is picked. Draw a footer strip so the buttons read as a modal bar. - Rect footer{0, bm.content.bottom, w, h}; + Rect footer = Rect::ltrb(0, bm.content.bottom(), w, h); fillSurface(bmp, toKitBox(footer), Role::BgPanel, InteractionState::Rest); { const KitButtonBox box{toKitBox(bm.cancel)}; @@ -1942,17 +1936,17 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { namespace { Rect zoneContentArea(int w, int h) { const int titleH = (std::min)(kTitleHeight, h); - return Rect{0, titleH, w, h}; + return Rect::ltrb(0, titleH, w, h); } } // namespace void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { // Title band + Back button (returns to Sample). The Zone surface is button-summoned and returns // to the Sample home on close. - const Rect title{0, 0, w, (std::min)(kTitleHeight, h)}; + const Rect title = Rect::ltrb(0, 0, w, (std::min)(kTitleHeight, h)); drawTitleBand(bmp, title, "Zone - keyboard map"); { - const Rect back{w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, title.bottom - 2)}; + const Rect back = Rect::ltrb(w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, title.bottom() - 2)); const KitButtonBox box{toKitBox(back)}; const InteractionState st = isHovered(HoverKind::kBack, -1) ? InteractionState::Hover : InteractionState::Rest; @@ -1964,8 +1958,8 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { // A single "+ Add Zone" affordance at the top of the content, then the keyboard strip // with one bar per zone. Delete is a small × on the selected zone (keystroke also). - Rect addR{content.left + pad, content.top + 4, content.left + pad + 96, - content.top + 4 + 20}; + Rect addR = Rect::ltrb(content.x + pad, content.y + 4, content.x + pad + 96, + content.y + 4 + 20); { const KitButtonBox box{toKitBox(addR)}; const InteractionState state = @@ -1973,7 +1967,7 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { drawButton(bmp, box, "+ Add Zone", state, /*warn=*/false); } - Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom}; + Rect delR = Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom()); if (selectedZone_ >= 0) { const KitButtonBox box{toKitBox(delR)}; const InteractionState state = @@ -1988,22 +1982,22 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { // zone is live"); the rest take the categorical secondary hue at low alpha. const Rect stripArea = zonesStripArea(content); drawSpectralStrip(bmp, stripArea); - const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); - const int sx = stripArea.left; - const int sy = stripArea.top; + const StripLayout sl = layoutStrip(stripArea.width, stripArea.height); + const int sx = stripArea.x; + const int sy = stripArea.y; for (int i = 0; i < static_cast(map_.zones.size()); ++i) { const PerformanceZone& z = map_.zones[static_cast(i)]; Rect bar = zoneBarRect(sl, z.lowNote, z.highNote); - const int bw = (std::max)(2, bar.width()); + const int bw = (std::max)(2, bar.width); const bool sel = (i == selectedZone_); if (sel) { // Static glow halo behind the live zone, then the crisp accent-primary bar. - LICE_FillRect(bmp, bar.left + sx - 2, sy, bw + 4, stripArea.height(), + LICE_FillRect(bmp, bar.x + sx - 2, sy, bw + 4, stripArea.height, toLice(roleColor(Role::AccentHot)), 0.30f, 0); - LICE_FillRect(bmp, bar.left + sx, sy, bw, stripArea.height(), + LICE_FillRect(bmp, bar.x + sx, sy, bw, stripArea.height, toLice(roleColor(Role::AccentPrimary)), 1.0f, 0); } else { - LICE_FillRect(bmp, bar.left + sx, sy, bw, stripArea.height(), + LICE_FillRect(bmp, bar.x + sx, sy, bw, stripArea.height, toLice(roleColor(Role::AccentSecondary)), 0.55f, 0); } } @@ -2011,11 +2005,11 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { // A one-line legend of the selected zone below the strip, with three click-to-type numeric // entry fields (low / high / root) — S12 direct numeric entry. Clicking a field focuses it // (entryField_) and typed text commits via parseNoteEntry on Enter. - const int legendTop = stripArea.bottom + 8; - Rect infoR{stripArea.left, legendTop, stripArea.right, legendTop + 18}; + const int legendTop = stripArea.bottom() + 8; + Rect infoR = Rect::ltrb(stripArea.x, legendTop, stripArea.right(), legendTop + 18); if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { const PerformanceZone& z = map_.zones[static_cast(selectedZone_)]; - kitText(bmp, Rect{infoR.left, infoR.top, infoR.left + 120, infoR.bottom}, + kitText(bmp, Rect::ltrb(infoR.x, infoR.y, infoR.x + 120, infoR.bottom()), sampleLabel(samples_, processor_ ? processor_->sampleRefs() : SampleRefs{}, z.sampleId) .c_str(), @@ -2034,11 +2028,11 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { editing ? InteractionState::Focus : InteractionState::Rest); const KitColor border = editing ? roleColor(Role::TextPrimary) : roleColor(Role::LineHairline); - LICE_DrawRect(bmp, fr.left, fr.top, fr.width() - 1, fr.height() - 1, + LICE_DrawRect(bmp, fr.x, fr.y, fr.width - 1, fr.height - 1, toLice(border), 1.0f, 0); std::string cap = std::string(names[f]) + ": " + (editing ? (entryText_ + "_") : vals[f]); - kitText(bmp, Rect{fr.left + 4, fr.top, fr.right - 2, fr.bottom}, cap.c_str(), + kitText(bmp, Rect::ltrb(fr.x + 4, fr.y, fr.right() - 2, fr.bottom()), cap.c_str(), Font::ValueMono, Role::TextPrimary); } } else if (map_.zones.empty()) { @@ -2084,9 +2078,9 @@ void ReaSamplerEditor::resolveHover(int x, int y) { else if (contains(bm.confirm, x, y)) h = {HoverKind::kBrowseConfirm, -1}; else if (contains(bm.search, x, y)) h = {HoverKind::kSearchBox, -1}; else { - const BrowserLayout bl = layoutBrowser(bm.content.width(), bm.content.height()); - const int bx = x - bm.content.left; - const int by = y - bm.content.top; + const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height); + const int bx = x - bm.content.x; + const int by = y - bm.content.y; const int tabCount = static_cast(banks_.size()) + 1; const int tab = filterTabHitTest(bl, tabCount, bx, by); const int card = (tab >= 0) @@ -2106,11 +2100,11 @@ void ReaSamplerEditor::resolveHover(int x, int y) { if (idx >= 0) h = {HoverKind::kCurveNode, idx}; } } else if (view_ == View::kZone) { - const Rect back{w - kPad - kNavButtonWidth, 2, - w - kPad, (std::max)(2, (std::min)(kTitleHeight, hgt) - 2)}; + const Rect back = Rect::ltrb(w - kPad - kNavButtonWidth, 2, + w - kPad, (std::max)(2, (std::min)(kTitleHeight, hgt) - 2)); const Rect content = zoneContentArea(w, hgt); - Rect addR{content.left + kPad, content.top + 4, content.left + kPad + 96, content.top + 4 + 20}; - Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom}; + Rect addR = Rect::ltrb(content.x + kPad, content.y + 4, content.x + kPad + 96, content.y + 4 + 20); + Rect delR = Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom()); if (contains(back, x, y)) { h = {HoverKind::kBack, -1}; } else if (contains(addR, x, y)) { @@ -2126,8 +2120,8 @@ void ReaSamplerEditor::resolveHover(int x, int y) { const ZonePlaySeconds& play = map_.zones[static_cast(selectedZone_)].play; const Rect deckArea = zonesDeckArea(content); - const DeckLayout dl = layoutDeck(zoneDeckGroupDescs(play), deckArea.left, - deckArea.top, deckArea.width()); + const DeckLayout dl = layoutDeck(zoneDeckGroupDescs(play), deckArea.x, + deckArea.y, deckArea.width); const DeckHit dh = hitTestDeck(dl, x, y); if (dh.kind != DeckHitKind::None) h = {HoverKind::kControl, dh.id}; } @@ -2154,7 +2148,7 @@ void ReaSamplerEditor::resolveHover(int x, int y) { else if (contains(bands.deck, x, y)) { // A deck knob/toggle under the pointer: knobs light + swap label->value. const DeckLayout dl = - layoutDeck(descs, bands.deck.left, bands.deck.top, bands.deck.width()); + layoutDeck(descs, bands.deck.x, bands.deck.y, bands.deck.width); const DeckHit dh = hitTestDeck(dl, x, y); if (dh.kind != DeckHitKind::None) h = {HoverKind::kControl, dh.id}; } @@ -2201,9 +2195,9 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { if (contains(bm.search, x, y)) { searchFocused_ = true; invalidate(); return; } searchFocused_ = false; - const BrowserLayout bl = layoutBrowser(bm.content.width(), bm.content.height()); - const int bx = x - bm.content.left; - const int by = y - bm.content.top; + const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height); + const int bx = x - bm.content.x; + const int by = y - bm.content.y; const int tabCount = static_cast(banks_.size()) + 1; const int tab = filterTabHitTest(bl, tabCount, bx, by); if (tab >= 0) { @@ -2214,9 +2208,9 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { return; } const Rect thumb = scrollThumbRect(bl, static_cast(visible_.size()), scrollOffset_); - if (thumb.height() > 0 && - contains(Rect{thumb.left + bm.content.left, thumb.top + bm.content.top, - thumb.right + bm.content.left, thumb.bottom + bm.content.top}, x, y)) { + if (thumb.height > 0 && + contains(Rect::ltrb(thumb.x + bm.content.x, thumb.y + bm.content.y, + thumb.right() + bm.content.x, thumb.bottom() + bm.content.y), x, y)) { drag_ = DragKind::kScrollThumb; dragStartY_ = y; dragStartScrollOffset_ = scrollOffset_; @@ -2315,8 +2309,8 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { // precedent); knobs start a grab-anchored vertical drag. The deck band swallows its // clicks (no fall-through to the hero/markers). if (contains(bands.deck, x, y)) { - const DeckLayout dl = layoutDeck(deckDescs, bands.deck.left, bands.deck.top, - bands.deck.width()); + const DeckLayout dl = layoutDeck(deckDescs, bands.deck.x, bands.deck.y, + bands.deck.width); const DeckHit hit = hitTestDeck(dl, x, y); if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) { switch (static_cast(hit.id)) { @@ -2429,9 +2423,9 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { } // Fenced root strip: grab the root marker (remainder-width since r11). - if (cr.rootStrip.width() > 0) { - const StripLayout sl = layoutStrip(cr.rootStrip.width(), cr.rootStrip.height()); - const int note = keyAtPoint(sl, x - cr.rootStrip.left, y - cr.rootStrip.top); + if (cr.rootStrip.width > 0) { + const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height); + const int note = keyAtPoint(sl, x - cr.rootStrip.x, y - cr.rootStrip.y); if (note >= 0) { drag_ = DragKind::kRootMarker; dragStartX_ = x; @@ -2448,13 +2442,13 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { // The curve popup is modal over the Zone surface too (FB2) — it owns every click while // open, checked before every Zone affordance (incl. Back). if (handlePopupMouseDown(w, h, x, y)) return; - const Rect back{w - kPad - kNavButtonWidth, 2, - w - kPad, (std::max)(2, (std::min)(kTitleHeight, h) - 2)}; + const Rect back = Rect::ltrb(w - kPad - kNavButtonWidth, 2, + w - kPad, (std::max)(2, (std::min)(kTitleHeight, h) - 2)); if (contains(back, x, y)) { view_ = View::kSample; invalidate(); return; } const Rect content = zoneContentArea(w, h); const int pad = 8; - Rect addR{content.left + pad, content.top + 4, content.left + pad + 96, - content.top + 4 + 20}; + Rect addR = Rect::ltrb(content.x + pad, content.y + 4, content.x + pad + 96, + content.y + 4 + 20); if (contains(addR, x, y)) { // Add a narrow default zone for the picked capture (or the first visible sample as a // sensible seed). No pick -> nothing to add. If a full-keyboard zone for the seed id @@ -2490,7 +2484,7 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { commitAndReload(); return; } - Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom}; + Rect delR = Rect::ltrb(addR.right() + 8, addR.y, addR.right() + 8 + 64, addR.bottom()); if (selectedZone_ >= 0 && contains(delR, x, y)) { map_.zones.erase(map_.zones.begin() + selectedZone_); selectedZone_ = -1; @@ -2501,9 +2495,9 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { // The zones strip: hit-test a bar edge/body to start a drag, or a bare key to set the // selected zone's root. const Rect stripArea = zonesStripArea(content); - const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); - const int lx = x - stripArea.left; - const int ly = y - stripArea.top; + const StripLayout sl = layoutStrip(stripArea.width, stripArea.height); + const int lx = x - stripArea.x; + const int ly = y - stripArea.y; std::vector lows, highs; lows.reserve(map_.zones.size()); @@ -2565,8 +2559,8 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { } const ZonePlaySeconds& play = map_.zones[static_cast(selectedZone_)].play; const Rect deckArea = zonesDeckArea(content); - const DeckLayout dl = layoutDeck(zoneDeckGroupDescs(play), deckArea.left, deckArea.top, - deckArea.width()); + const DeckLayout dl = layoutDeck(zoneDeckGroupDescs(play), deckArea.x, deckArea.y, + deckArea.width); const DeckHit hit = hitTestDeck(dl, x, y); if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) { // Zone-param toggles (play mode / pitch engine / pitch-env enable): a discrete, @@ -2640,7 +2634,7 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { // upsert by id so a repeated drag edits the same zone rather than stacking duplicates. const ChannelToggleRects chan = channelToggleRects(bands.cluster); const Rect stripArea = clusterRects(bands.cluster, chan.mono).rootStrip; - const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); + const StripLayout sl = layoutStrip(stripArea.width, stripArea.height); const int note = resolveDragNote(sl, dragStartRoot_, dx); bool found = false; for (int i = 0; i < static_cast(map_.zones.size()); ++i) { @@ -2754,7 +2748,7 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { // at paint from scrollOffset_. const int dyThumb = y - dragStartY_; const BrowseModal bm = computeBrowseModal(w, h); - const BrowserLayout bl = layoutBrowser(bm.content.width(), bm.content.height()); + const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height); scrollOffset_ = thumbDragToOffset(bl, static_cast(visible_.size()), dragStartScrollOffset_, dyThumb); invalidate(); @@ -2765,7 +2759,7 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { // in the Zone surface where selectedZone_ is set + the strip lives under its content area. if (selectedZone_ < 0 || selectedZone_ >= static_cast(map_.zones.size())) return; const Rect stripArea = zonesStripArea(zoneContentArea(w, h)); - const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); + const StripLayout sl = layoutStrip(stripArea.width, stripArea.height); PerformanceZone& z = map_.zones[static_cast(selectedZone_)]; if (drag_ == DragKind::kZoneLow) { z.lowNote = (std::min)(resolveDragNote(sl, dragStartLow_, dx), z.highNote); @@ -2825,10 +2819,10 @@ void ReaSamplerEditor::onMouseUp(int x, int y) { // move — its amp keeps the last clamped drag value). if (kind == DragKind::kCurveNode && curveIdx >= 0 && curveZone >= 0 && curveZone < static_cast(map_.zones.size())) { - const bool off = x < curveRect.left - kCurveDragOffMargin || - x > curveRect.right + kCurveDragOffMargin || - y < curveRect.top - kCurveDragOffMargin || - y > curveRect.bottom + kCurveDragOffMargin; + const bool off = x < curveRect.x - kCurveDragOffMargin || + x > curveRect.right() + kCurveDragOffMargin || + y < curveRect.y - kCurveDragOffMargin || + y > curveRect.bottom() + kCurveDragOffMargin; if (off) { map_.zones[static_cast(curveZone)].velocityCurve.deletePoint( static_cast(curveIdx)); diff --git a/src/vst/reasampler_editor.h b/src/vst/reasampler_editor.h index 91e0082..18a26ea 100644 --- a/src/vst/reasampler_editor.h +++ b/src/vst/reasampler_editor.h @@ -22,6 +22,7 @@ // to create/destroy the child window and onSize to resize it. #pragma once +#include "core/namespaces.h" #include #include @@ -30,13 +31,13 @@ #include "public.sdk/source/common/pluginview.h" -#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules) -#include "envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit) -#include "envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam) -#include "knob_deck.h" // DeckGroupDesc / DeckLayout (r11 knob deck — Sample FB1, Zone FB2) -#include "peaks.h" // Envelope (the cached peak thumbnail) -#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot) -#include "velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state) +#include "core/instrument/ui/editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules) +#include "core/instrument/ui/envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit) +#include "core/instrument/ui/envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam) +#include "core/instrument/ui/knob_deck.h" // DeckGroupDesc / DeckLayout (r11 knob deck — Sample FB1, Zone FB2) +#include "core/audio/peaks.h" // Envelope (the cached peak thumbnail) +#include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot) +#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state) #ifdef _WIN32 #include diff --git a/src/vst/reasampler_processor.cpp b/src/vst/reasampler_processor.cpp index eb8f2d4..4cce960 100644 --- a/src/vst/reasampler_processor.cpp +++ b/src/vst/reasampler_processor.cpp @@ -1,3 +1,4 @@ +#include "core/namespaces.h" // reasampler_processor.cpp — see reasampler_processor.h. #include "reasampler_processor.h" @@ -17,16 +18,17 @@ #include "pluginterfaces/vst/ivstmidicontrollers.h" // kCtrlAllNotesOff / kCtrlAllSoundsOff (panic) #include "pluginterfaces/vst/vstspeaker.h" -#include "assignment_request.h" // decodeAssignmentRequest (S8 request wire parse) -#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision -#include "capture_paths.h" // resolveBankFile (shared M4 path resolution) +#include "core/wire/assignment_request.h" // decodeAssignmentRequest (S8 request wire parse) +#include "core/instrument/map/bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision +#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution) +#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03) #include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey (shared wire contract) -#include "master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp) +#include "core/instrument/engine/master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp) #include "reasampler_editor.h" -#include "reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there) -#include "sample_map.h" // refs resolve, buildZonedKeymap, state (de)ser (pS self-contained) -#include "sample_usage.h" // pS-usage publish plan + wire (prune-protection seam) -#include "wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse) +#include "shell/instrument/reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there) +#include "core/instrument/map/sample_map.h" // refs resolve, buildZonedKeymap, state (de)ser (pS self-contained) +#include "core/wire/sample_usage.h" // pS-usage publish plan + wire (prune-protection seam) +#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse) using namespace Steinberg; using namespace Steinberg::Vst; @@ -69,19 +71,9 @@ std::string mintUsageInstanceGuid() { return std::string(buf); } -// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on -// any failure — the caller treats an unreadable WAV as "nothing to play". -std::vector readFileBytes(const std::string& path) { - std::vector bytes; - std::ifstream f(path, std::ios::binary | std::ios::ate); - if (!f) return bytes; - const std::streamoff size = f.tellg(); - if (size <= 0) return bytes; - f.seekg(0, std::ios::beg); - bytes.resize(static_cast(size)); - if (!f.read(reinterpret_cast(bytes.data()), size)) bytes.clear(); - return bytes; -} +// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03). +// Off-thread only (blocking file I/O). Empty on any failure — the caller treats +// an unreadable WAV as "nothing to play". // Resolve a project-relative WAV path (the M4 way persist does), read + decode it (file // I/O — off-thread only), and apply the S7 cross-mode channel policy for `mode`: mono mode diff --git a/src/vst/reasampler_processor.h b/src/vst/reasampler_processor.h index 73f0e85..e339048 100644 --- a/src/vst/reasampler_processor.h +++ b/src/vst/reasampler_processor.h @@ -25,6 +25,7 @@ // single atomic pointer swap. See the LoadedInstrument handoff below. #pragma once +#include "core/namespaces.h" #include #include @@ -35,9 +36,9 @@ #include "public.sdk/source/vst/vstsinglecomponenteffect.h" -#include "reaper_bridge.h" -#include "sample_map.h" // PerformanceMap (the instrument's owned zoned keymap) -#include "sampler_core.h" +#include "shell/instrument/reaper_bridge.h" +#include "core/instrument/map/sample_map.h" // PerformanceMap (the instrument's owned zoned keymap) +#include "core/instrument/engine/sampler_core.h" namespace reasampler::vst { diff --git a/tests/test_action_bar.cpp b/tests/test_action_bar.cpp index cda6d88..7eba7b1 100644 --- a/tests/test_action_bar.cpp +++ b/tests/test_action_bar.cpp @@ -15,13 +15,14 @@ // degenerate/too-narrow bar handled without crash or overlap. // * Resize: no inventory item cut off or overlapping across a representative width range. -#include "../src/action_bar.h" +#include "../src/core/ui/action_bar.h" #include #include #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_app_version.cpp b/tests/test_app_version.cpp index 0fca97e..2f8c1e3 100644 --- a/tests/test_app_version.cpp +++ b/tests/test_app_version.cpp @@ -5,12 +5,13 @@ // back from ext state (PreVersioning / Unknown / Stamped). The ext-state I/O (persist) and // the show-version action (main) are DAW-verified shell. -#include "../src/app_version.h" +#include "../src/core/version/app_version.h" #include #include using namespace reasampler; +using namespace reasampler::version; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_app_version_padding.cpp b/tests/test_app_version_padding.cpp index 4e89dfd..295cc4d 100644 --- a/tests/test_app_version_padding.cpp +++ b/tests/test_app_version_padding.cpp @@ -25,12 +25,13 @@ // configure_file input in CMakeLists.txt. They are NOT the shipped version: never bump // them on release — their padded shape is the entire point. -#include "../src/app_version.h" +#include "../src/core/version/app_version.h" #include #include using namespace reasampler; +using namespace reasampler::version; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_assignment_request.cpp b/tests/test_assignment_request.cpp index 9dfe12c..45292e6 100644 --- a/tests/test_assignment_request.cpp +++ b/tests/test_assignment_request.cpp @@ -9,12 +9,13 @@ // trailing-garbage input -> nullopt (the reader's "no pending request" fallback hinges // on it). -#include "../src/assignment_request.h" +#include "../src/core/wire/assignment_request.h" #include #include using namespace reasampler; +using namespace reasampler::wire; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_bank_book.cpp b/tests/test_bank_book.cpp index 0e8674c..c25ddaf 100644 --- a/tests/test_bank_book.cpp +++ b/tests/test_bank_book.cpp @@ -10,13 +10,14 @@ // pool, set named, invalid id); JSON round-trip lossless (full book); legacy // bank_index → pool migration. -#include "../src/bank_book.h" +#include "../src/core/model/bank_book.h" #include #include #include using namespace reasampler; +using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -41,6 +42,29 @@ static Sample sampleWith(const std::string& seed) { return sampleWith(seed, "has // --------------------------------------------------------------------------- +// Golden byte-literal (Q-W1 follow-up): pins the EXACT serialized bytes for a +// small fixture (a freshly-seeded book: pool only, one sample), not just +// self-consistent re-serialization — a format drift that both writer and +// reader agree on would slip past the round-trip tests but not this. The +// format is frozen as-shipped; the literal below is the captured current +// output. +static void testSerializeGoldenLiteral() { + BankBook book; + CHECK(book.pool().index.add(sampleWith("g1")) == AddResult::Added); + CHECK(book.serialize() == + "{\"version\":1,\"activeBank\":\"pool\",\"banks\":[{\"id\":\"pool\"," + "\"displayName\":\"Pool\",\"ordinal\":0,\"index\":{\"version\":1," + "\"samples\":[{\"id\":\"id-g1\",\"displayName\":\"sample g1\"," + "\"relativePath\":\"bank/g1.wav\",\"sourceMode\":0,\"sourceRange\":{" + "\"startSeconds\":0,\"endSeconds\":0,\"startPpq\":0,\"endPpq\":0}," + "\"trackGuids\":[],\"wetDry\":1,\"channelCount\":2,\"sampleRate\":48000," + "\"lengthSeconds\":0,\"lengthBeats\":0,\"captureTempo\":0," + "\"captureTimeSigNum\":0,\"captureTimeSigDenom\":0,\"key\":null," + "\"rootNote\":null,\"loop\":null,\"levels\":{\"peakDb\":0,\"rmsDb\":0," + "\"lufs\":0},\"clipped\":false,\"tier\":0,\"contentHash\":\"hash-g1\"," + "\"provenance\":null,\"createdTimestamp\":1753080000}]},\"slots\":[]}]}"); +} + static void testPoolSeededAndDefaults() { BankBook book; // Pool present as bank-zero with fixed id + name + ordinal 0. @@ -339,9 +363,9 @@ static void testJsonEmptyBookRoundTrip() { } static void testLegacyMigration() { - // A bare legacy bank_index JSON (BankIndex::serialize output — has "samples", no + // A bare legacy bank_index JSON (BankModel::serialize output — has "samples", no // "banks") must promote into the pool: a book of { pool } with zero named banks. - BankIndex legacy; + BankModel legacy; CHECK(legacy.add(sampleWith("old1")) == AddResult::Added); CHECK(legacy.add(sampleWith("old2")) == AddResult::Added); std::string legacyJson = legacy.serialize(); @@ -361,7 +385,7 @@ static void testLegacyMigration() { } // An EMPTY legacy index ("{\"samples\":[]}" style via serialize) also migrates. - BankIndex emptyLegacy; + BankModel emptyLegacy; auto back2 = BankBook::deserialize(emptyLegacy.serialize()); CHECK(back2.has_value()); CHECK(back2 && back2->size() == 1 && back2->pool().index.empty()); @@ -408,7 +432,7 @@ static void testLoadPrefersBanksBlob() { CHECK(src.bank("drums")->index.add(sampleWith("new1")) == AddResult::Added); const std::string banksJson = src.serialize(); - BankIndex stale; + BankModel stale; CHECK(stale.add(sampleWith("stale-old")) == AddResult::Added); const std::string legacyJson = stale.serialize(); @@ -424,7 +448,7 @@ static void testLoadPrefersBanksBlob() { static void testLoadMigratesLegacyWhenNoBanks() { // No `banks` key, a legacy `bank_index` present: migrate into the pool, zero named. - BankIndex legacy; + BankModel legacy; CHECK(legacy.add(sampleWith("l1")) == AddResult::Added); CHECK(legacy.add(sampleWith("l2")) == AddResult::Added); const std::string legacyJson = legacy.serialize(); @@ -449,7 +473,7 @@ static void testLoadEmptyWhenNeither() { static void testLoadMalformedBanksDegradesWithoutLegacyFallback() { // A present-but-malformed `banks` blob must degrade to an empty book and must NOT // resurrect the stale legacy key (that would revive superseded single-bank state). - BankIndex stale; + BankModel stale; CHECK(stale.add(sampleWith("stale")) == AddResult::Added); const std::string legacyJson = stale.serialize(); @@ -789,123 +813,13 @@ static void testUpdateSampleInPlace() { // =========================================================================== // L7 — SlotMap (gap-preserving display positions) + BankBook ordering/reorder/replace // =========================================================================== - -// --- SlotMap unit behaviour -------------------------------------------------- - -static void testSlotMapDenseAppend() { - SlotMap m; - m.append("a"); - m.append("b"); - m.append("c"); - CHECK(m.slotOf("a") == 0); - CHECK(m.slotOf("b") == 1); - CHECK(m.slotOf("c") == 2); - CHECK(m.maxSlot() == 2); - CHECK((m.orderedIds() == std::vector{"a", "b", "c"})); - CHECK(m.idAt(1) == "b"); - CHECK(m.slotOf("nope") == -1); -} - -static void testSlotMapRemoveLeavesGap() { - SlotMap m; - m.append("a"); m.append("b"); m.append("c"); // 0,1,2 - CHECK(m.remove("b")); // slot 1 now EMPTY (no re-pack) - CHECK(m.slotOf("a") == 0); - CHECK(m.slotOf("c") == 2); // c did NOT shift down - CHECK(m.idAt(1).empty()); // gap preserved - CHECK((m.orderedIds() == std::vector{"a", "c"})); - CHECK(!m.remove("b")); // already gone -} - -static void testSlotMapAppendAfterGapGoesToFrontier() { - SlotMap m; - m.append("a"); m.append("b"); m.append("c"); // 0,1,2 - m.remove("a"); // slot 0 empty - m.append("d"); // append goes AFTER last occupied (2) -> 3 - CHECK(m.slotOf("d") == 3); // did NOT fill the slot-0 gap - CHECK(m.idAt(0).empty()); -} - -static void testSlotMapReorderIntoEmpty() { - SlotMap m; - m.append("a"); m.append("b"); m.append("c"); // 0,1,2 - m.remove("b"); // slot 1 empty - CHECK(m.reorder("c", 1)); // c -> empty slot 1; its slot 2 empties - CHECK(m.slotOf("c") == 1); - CHECK(m.idAt(2).empty()); - CHECK(m.slotOf("a") == 0); // untouched -} - -static void testSlotMapReorderOntoOccupiedInsertsAndShifts() { - SlotMap m; - m.append("a"); m.append("b"); m.append("c"); m.append("d"); // 0,1,2,3 - CHECK(m.reorder("d", 1)); // d onto occupied slot 1 -> insert-before, shift b,c up - CHECK(m.slotOf("a") == 0); // before the target: unchanged - CHECK(m.slotOf("d") == 1); // took the target slot - CHECK(m.slotOf("b") == 2); // shifted +1 - CHECK(m.slotOf("c") == 3); // shifted +1 - CHECK((m.orderedIds() == std::vector{"a", "d", "b", "c"})); -} - -static void testSlotMapReorderPreservesInteriorGapAboveTarget() { - SlotMap m; - m.append("a"); m.append("b"); m.append("c"); // 0,1,2 - m.remove("b"); // gap at 1: a@0, c@2 - m.append("d"); // d@3 - CHECK(m.reorder("d", 0)); // d onto occupied slot 0 -> a shifts to 1, c shifts to 3 - CHECK(m.slotOf("d") == 0); - CHECK(m.slotOf("a") == 1); // shifted from 0 -> 1 - CHECK(m.slotOf("c") == 3); // shifted from 2 -> 3 (gap at 2 preserved as a +1 of its own) - CHECK(m.idAt(2).empty()); // interior gap above the target survives -} - -static void testSlotMapReorderUnmappedIsNoOp() { - SlotMap m; - m.append("a"); - CHECK(!m.reorder("ghost", 0)); // not mapped -> false, no mutation - CHECK(m.slotOf("a") == 0); -} - -static void testSlotMapNegativeTargetClampsToZero() { - SlotMap m; - m.append("a"); m.append("b"); // 0,1 - CHECK(m.reorder("b", -3)); // clamp to 0 -> insert-before a - CHECK(m.slotOf("b") == 0); - CHECK(m.slotOf("a") == 1); -} - -static void testSlotMapResetDenseSkipsDupesAndEmpties() { - SlotMap m; - m.resetDense({"a", "", "b", "a", "c"}); // "" and the second "a" dropped - CHECK((m.orderedIds() == std::vector{"a", "b", "c"})); - CHECK(m.slotOf("a") == 0); - CHECK(m.slotOf("c") == 2); -} - -static void testSlotMapReconcileDropsStaleAppendsNew() { - SlotMap m; - m.append("a"); m.append("b"); m.append("c"); // 0,1,2 - m.reconcile({"a", "c", "d"}); // b left the index (drop), d is new (append) - CHECK(m.slotOf("a") == 0); // kept at its slot - CHECK(m.slotOf("c") == 2); // kept at its slot (gap where b was) - CHECK(m.slotOf("b") == -1); // stale marker dropped - CHECK(m.slotOf("d") == 3); // appended after the frontier - CHECK(m.idAt(1).empty()); // b's slot stays empty -} - -static void testSlotMapEqualityAndFromEntries() { - SlotMap a; - a.append("x"); a.append("y"); - SlotMap b = SlotMap::fromEntries({{"x", 0}, {"y", 1}}); - CHECK(a == b); - // Defensive repair: duplicate id (first wins), slot conflict (later dropped), - // empty id / negative slot dropped. - SlotMap c = SlotMap::fromEntries({{"x", 0}, {"x", 5}, {"y", 0}, {"", 9}, {"z", -1}, {"w", 2}}); - CHECK(c.slotOf("x") == 0); // first x wins - CHECK(c.slotOf("y") == -1); // slot 0 already taken -> dropped - CHECK(c.slotOf("w") == 2); // valid - CHECK(c.slotOf("z") == -1); // negative slot dropped -} +// +// Pure SlotMap-only unit behaviour (add/remove/query, reorder gap-preservation, +// resetDense/reconcile, equality/fromEntries, serialize golden literal + round +// trip) now lives in test_slot_map.cpp (Q-W1 follow-up), extracted per the house +// every-pure-module-has-a-_tests rule. This file keeps the BankBook-level +// integration coverage below: reorderSample / reconcileSlots / JSON round-trip +// WITH a full book. // --- BankBook L7: JSON round-trip WITH positions ----------------------------- @@ -936,7 +850,7 @@ static void testBankBookSlotsRoundTrip() { static void testMigrationDefaultsToInsertionOrderDense() { // A pre-L7 legacy bank_index blob carries no slot data. On load -> reconcileSlots // seeds dense insertion order (no gaps), so it is visually identical. - BankIndex legacy; + BankModel legacy; CHECK(legacy.add(sampleWith("o1")) == AddResult::Added); CHECK(legacy.add(sampleWith("o2")) == AddResult::Added); CHECK(legacy.add(sampleWith("o3")) == AddResult::Added); @@ -1080,6 +994,7 @@ static void testReplaceSampleInPoolPassesGuard() { } int main() { + testSerializeGoldenLiteral(); testPoolSeededAndDefaults(); testPoolPrivileges(); testCreateRenameReorder(); @@ -1116,18 +1031,8 @@ int main() { testRemoveAllBanksLatentScope(); testUpdateSampleInPlace(); - // L7 — SlotMap + ordering/reorder/replace + slot round-trip/migration. - testSlotMapDenseAppend(); - testSlotMapRemoveLeavesGap(); - testSlotMapAppendAfterGapGoesToFrontier(); - testSlotMapReorderIntoEmpty(); - testSlotMapReorderOntoOccupiedInsertsAndShifts(); - testSlotMapReorderPreservesInteriorGapAboveTarget(); - testSlotMapReorderUnmappedIsNoOp(); - testSlotMapNegativeTargetClampsToZero(); - testSlotMapResetDenseSkipsDupesAndEmpties(); - testSlotMapReconcileDropsStaleAppendsNew(); - testSlotMapEqualityAndFromEntries(); + // L7 — BankBook ordering/reorder/replace + slot round-trip/migration. + // (Pure SlotMap-only unit behaviour lives in slot_map_tests.) testBankBookSlotsRoundTrip(); testMigrationDefaultsToInsertionOrderDense(); testOrderedSampleIdsReconcilesLazily(); diff --git a/tests/test_bank_grid.cpp b/tests/test_bank_grid.cpp index 9d178b8..2c0b07e 100644 --- a/tests/test_bank_grid.cpp +++ b/tests/test_bank_grid.cpp @@ -13,7 +13,7 @@ // keyboard nav (arrow clamp, row moves, shift-extend, partial-last-row clamp, // fresh-panel focus). -#include "../src/bank_grid.h" +#include "../src/core/ui/bank_grid.h" #include #include @@ -21,6 +21,7 @@ #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_bank_model.cpp b/tests/test_bank_model.cpp index 5de98db..299aef0 100644 --- a/tests/test_bank_model.cpp +++ b/tests/test_bank_model.cpp @@ -6,12 +6,13 @@ // present AND absent), dedup-by-hash collapse, tier filter + tier move, // relative-path invariant, empty-index round-trip, malformed/truncated JSON. -#include "../src/bank_model.h" +#include "../src/core/model/bank_model.h" #include #include using namespace reasampler; +using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -65,12 +66,12 @@ static Sample minimalSample(const std::string& seed) { } static void testFullFieldRoundTrip() { - BankIndex idx; + BankModel idx; CHECK(idx.add(fullSample("a")) == AddResult::Added); CHECK(idx.add(minimalSample("b")) == AddResult::Added); std::string json = idx.serialize(); - auto back = BankIndex::deserialize(json); + auto back = BankModel::deserialize(json); CHECK(back.has_value()); CHECK(back && *back == idx); @@ -103,8 +104,32 @@ static void testFullFieldRoundTrip() { } } +// Golden byte-literal (Q-W1 T?-05 follow-up): pins the EXACT serialized bytes for +// a small fixture, not just self-consistent re-serialization — a format drift +// that round-trips losslessly (e.g. a renamed key both writer and reader agree +// on) would slip past testFullFieldRoundTrip but not this. The format is frozen +// as-shipped; the literal below is the captured current output. +static void testSerializeGoldenLiteral() { + BankModel idx; + Sample s; + s.id = "g1"; + s.relativePath = "bank/g1.wav"; + s.contentHash = "hash-g1"; + CHECK(idx.add(s) == AddResult::Added); + CHECK(idx.serialize() == + "{\"version\":1,\"samples\":[{\"id\":\"g1\",\"displayName\":\"\"," + "\"relativePath\":\"bank/g1.wav\",\"sourceMode\":0,\"sourceRange\":{" + "\"startSeconds\":0,\"endSeconds\":0,\"startPpq\":0,\"endPpq\":0}," + "\"trackGuids\":[],\"wetDry\":1,\"channelCount\":0,\"sampleRate\":0," + "\"lengthSeconds\":0,\"lengthBeats\":0,\"captureTempo\":0," + "\"captureTimeSigNum\":0,\"captureTimeSigDenom\":0,\"key\":null," + "\"rootNote\":null,\"loop\":null,\"levels\":{\"peakDb\":0,\"rmsDb\":0," + "\"lufs\":0},\"clipped\":false,\"tier\":0,\"contentHash\":\"hash-g1\"," + "\"provenance\":null,\"createdTimestamp\":0}]}"); +} + static void testDedupByHash() { - BankIndex idx; + BankModel idx; Sample a = fullSample("x"); CHECK(idx.add(a) == AddResult::Added); @@ -128,7 +153,7 @@ static void testDedupByHash() { } static void testTierFilterAndMove() { - BankIndex idx; + BankModel idx; Sample scratch = minimalSample("s"); scratch.tier = Tier::Scratch; Sample archive = fullSample("a"); archive.tier = Tier::Archive; CHECK(idx.add(scratch) == AddResult::Added); @@ -153,7 +178,7 @@ static void testTierFilterAndMove() { } static void testRelativePathInvariant() { - BankIndex idx; + BankModel idx; // POSIX absolute, Windows drive, Windows backslash, UNC — all rejected. const char* absolutes[] = { @@ -184,7 +209,7 @@ static void testRelativePathInvariant() { // bypasses dedup (an in-place refresh is not a new insert). The relative-paths-only // invariant still guards the replacement. static void testUpdateInPlace() { - BankIndex idx; + BankModel idx; CHECK(idx.add(minimalSample("a")) == AddResult::Added); // id "min-a" CHECK(idx.add(minimalSample("b")) == AddResult::Added); // id "min-b" CHECK(idx.add(minimalSample("c")) == AddResult::Added); // id "min-c" @@ -221,10 +246,10 @@ static void testUpdateInPlace() { } static void testEmptyIndexRoundTrip() { - BankIndex idx; + BankModel idx; CHECK(idx.empty()); std::string json = idx.serialize(); - auto back = BankIndex::deserialize(json); + auto back = BankModel::deserialize(json); CHECK(back.has_value()); CHECK(back && back->empty()); CHECK(back && *back == idx); @@ -244,17 +269,17 @@ static void testMalformedJson() { "{\"samples\":[{\"createdTimestamp\":notanumber}]}", }; for (const char* j : bad) { - auto r = BankIndex::deserialize(j); + auto r = BankModel::deserialize(j); CHECK(!r.has_value()); // signaled as nullopt, no crash / UB } // A well-formed empty object deserializes to an empty index (lenient root). - auto ok = BankIndex::deserialize("{}"); + auto ok = BankModel::deserialize("{}"); CHECK(ok.has_value() && ok->empty()); } static void testRemoveAndQuery() { - BankIndex idx; + BankModel idx; CHECK(idx.add(fullSample("1")) == AddResult::Added); CHECK(idx.add(fullSample("2")) == AddResult::Added); CHECK(idx.query("id-1") != nullptr); @@ -267,7 +292,7 @@ static void testRemoveAndQuery() { // Fix 1: drive-relative and bare-drive forms must be rejected by add(). static void testAbsolutePathDriveRelative() { - BankIndex idx; + BankModel idx; // Drive-relative: resolves against the drive's CWD, not the project root. Sample dr = minimalSample("dr"); @@ -295,7 +320,7 @@ static void testAbsolutePathDriveRelative() { static void testUnicodeEscapeDecoding() { // é = U+00E9 → 2-byte UTF-8: 0xC3 0xA9 // JSON: "é" - auto r1 = BankIndex::deserialize( + auto r1 = BankModel::deserialize( "{\"samples\":[{\"id\":\"u1\",\"relativePath\":\"bank/u.wav\"," "\"displayName\":\"\\u00e9\"," "\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -319,7 +344,7 @@ static void testUnicodeEscapeDecoding() { // 中 = U+4E2D → 3-byte UTF-8: 0xE4 0xB8 0xAD // JSON: "中" - auto r2 = BankIndex::deserialize( + auto r2 = BankModel::deserialize( "{\"samples\":[{\"id\":\"u2\",\"relativePath\":\"bank/u.wav\"," "\"displayName\":\"\\u4e2d\"," "\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -343,7 +368,7 @@ static void testUnicodeEscapeDecoding() { } // 😀 = U+1F600 → surrogate pair 😀 → 4-byte UTF-8: 0xF0 0x9F 0x98 0x80 - auto r3 = BankIndex::deserialize( + auto r3 = BankModel::deserialize( "{\"samples\":[{\"id\":\"u3\",\"relativePath\":\"bank/u.wav\"," "\"displayName\":\"\\uD83D\\uDE00\"," "\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -368,7 +393,7 @@ static void testUnicodeEscapeDecoding() { } // Unpaired high surrogate (no following \uDCxx) → nullopt. - auto r4 = BankIndex::deserialize( + auto r4 = BankModel::deserialize( "{\"samples\":[{\"id\":\"u4\",\"relativePath\":\"bank/u.wav\"," "\"displayName\":\"\\uD83D\"," "\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -384,7 +409,7 @@ static void testUnicodeEscapeDecoding() { // Fix 3: strtoll overflow must reject the value, not clamp it silently. static void testIntegerOverflow() { // A timestamp value that overflows int64_t (> 9223372036854775807). - auto r = BankIndex::deserialize( + auto r = BankModel::deserialize( "{\"samples\":[{\"id\":\"ov1\",\"relativePath\":\"bank/ov.wav\"," "\"displayName\":\"\"," "\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -400,7 +425,7 @@ static void testIntegerOverflow() { // Fix 4: out-of-range enum values must reject the sample, not produce invalid enum. static void testEnumRangeValidation() { // tier: 99 is not a valid Tier enumerator. - auto r1 = BankIndex::deserialize( + auto r1 = BankModel::deserialize( "{\"samples\":[{\"id\":\"en1\",\"relativePath\":\"bank/en.wav\"," "\"displayName\":\"\"," "\"sourceMode\":0,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -413,7 +438,7 @@ static void testEnumRangeValidation() { CHECK(!r1.has_value()); // sourceMode: 99 is not a valid SourceMode enumerator. - auto r2 = BankIndex::deserialize( + auto r2 = BankModel::deserialize( "{\"samples\":[{\"id\":\"en2\",\"relativePath\":\"bank/en.wav\"," "\"displayName\":\"\"," "\"sourceMode\":99,\"sourceRange\":{\"startSeconds\":0.0,\"endSeconds\":0.0," @@ -442,7 +467,7 @@ static void testLegacyJsonDefaults() { "\"key\":null,\"levels\":{\"peakDb\":0.0,\"rmsDb\":0.0,\"lufs\":0.0}," "\"clipped\":false,\"tier\":0,\"contentHash\":\"h-leg1\"," "\"provenance\":null,\"createdTimestamp\":0}]}"; - auto r = BankIndex::deserialize(legacy); + auto r = BankModel::deserialize(legacy); CHECK(r.has_value()); if (r) { const Sample* s = r->query("leg1"); @@ -453,7 +478,7 @@ static void testLegacyJsonDefaults() { // Re-serialize is lossless: parsing it again yields an equal index. This // proves the absent fields did not silently gain values on the way out. std::string out = r->serialize(); - auto again = BankIndex::deserialize(out); + auto again = BankModel::deserialize(out); CHECK(again.has_value()); CHECK(again && *again == *r); if (again) { @@ -471,7 +496,7 @@ static void testLegacyJsonDefaults() { // storing a bogus value. static void testSeamFieldBoundaries() { // rootNote at both MIDI edges + equal-and-end-anchored loop points round-trip. - BankIndex idx; + BankModel idx; Sample lo = minimalSample("lo"); lo.contentHash = "h-lo"; lo.rootNote = 0; lo.loop = LoopPoints{0, 0}; // zero-length marker at frame 0 @@ -484,7 +509,7 @@ static void testSeamFieldBoundaries() { CHECK(idx.add(hi) == AddResult::Added); CHECK(idx.add(end) == AddResult::Added); - auto back = BankIndex::deserialize(idx.serialize()); + auto back = BankModel::deserialize(idx.serialize()); CHECK(back.has_value()); CHECK(back && *back == idx); if (back) { @@ -514,21 +539,21 @@ static void testSeamFieldBoundaries() { "\"clipped\":false,\"tier\":0,\"contentHash\":\"h-bad\"," "\"provenance\":null,\"createdTimestamp\":0}]}"; - CHECK(!BankIndex::deserialize(std::string(head) + "\"rootNote\":128," + tail).has_value()); - CHECK(!BankIndex::deserialize(std::string(head) + "\"rootNote\":-1," + tail).has_value()); - CHECK(!BankIndex::deserialize( + CHECK(!BankModel::deserialize(std::string(head) + "\"rootNote\":128," + tail).has_value()); + CHECK(!BankModel::deserialize(std::string(head) + "\"rootNote\":-1," + tail).has_value()); + CHECK(!BankModel::deserialize( std::string(head) + "\"loop\":{\"start\":10,\"end\":5}," + tail).has_value()); // start > end - CHECK(!BankIndex::deserialize( + CHECK(!BankModel::deserialize( std::string(head) + "\"loop\":{\"start\":-1,\"end\":5}," + tail).has_value()); // negative start } // S2 test case 3: the seam-field addition is purely additive — dedup-by-hash, tier -// moves/filtering, and BankIndex ordering are byte-for-byte unchanged by the +// moves/filtering, and BankModel ordering are byte-for-byte unchanged by the // presence (or absence) of rootNote/loop. Two samples differing ONLY in seam fields // but sharing a content hash still collapse; a seam-populated sample tiers exactly // like any other. static void testSeamFieldsAdditiveInvariant() { - BankIndex idx; + BankModel idx; Sample a = fullSample("z"); // has rootNote + loop populated CHECK(idx.add(a) == AddResult::Added); @@ -550,6 +575,7 @@ static void testSeamFieldsAdditiveInvariant() { int main() { testFullFieldRoundTrip(); + testSerializeGoldenLiteral(); testDedupByHash(); testTierFilterAndMove(); testRelativePathInvariant(); diff --git a/tests/test_bank_sync.cpp b/tests/test_bank_sync.cpp index cdcad2a..4195ecb 100644 --- a/tests/test_bank_sync.cpp +++ b/tests/test_bank_sync.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::bank_sync — no REAPER, no VST3, no framework. +// Standalone tests for reasampler::instrument::map::bank_sync — no REAPER, no VST3, no framework. // The S9 bank-generation change-detection + the S8 assignment-request consume DECISION // (the yes/no maths the instrument's off-audio-thread poll runs). The shell owns the // cadence + side effects; this proves the decision rules without a host. @@ -8,7 +8,7 @@ // (no request / not-newer / non-target / unresolvable-drop / apply), asserting both the // apply flag AND the advanced-marker value so a stale request is never re-evaluated. -#include "../src/vst/bank_sync.h" +#include "../src/core/instrument/map/bank_sync.h" #include #include @@ -16,7 +16,7 @@ #include using namespace reasampler; -using namespace reasampler::vst; +using namespace reasampler::instrument::map; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_batch_capture.cpp b/tests/test_batch_capture.cpp index 3cb8e93..f8dbd3b 100644 --- a/tests/test_batch_capture.cpp +++ b/tests/test_batch_capture.cpp @@ -9,13 +9,14 @@ // * batch result aggregation: all-success; partial-failure ORDERING (failed // ordinals reported in unit order); all-failed; single-unit noun singular. -#include "../src/batch_capture.h" +#include "../src/core/capture/batch_capture.h" #include #include #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_bridge_marshal.cpp b/tests/test_bridge_marshal.cpp index 8c6abbb..926f1d2 100644 --- a/tests/test_bridge_marshal.cpp +++ b/tests/test_bridge_marshal.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::bridge_marshal — no VST3, no REAPER, no test +// Standalone tests for reasampler::instrument::map::bridge_marshal — no VST3, no REAPER, no test // framework. Same fast assert loop as the sibling pure tests: assert the REAPER // bridge-read marshalling (GetProjExtState result decode) directly, so the DAW-facing // shell only has to invoke the API. @@ -8,11 +8,12 @@ // (the instrument now parses the bank through the shared bank_book JSON path), so its // cases are gone with it. -#include "../src/vst/bridge_marshal.h" +#include "../src/core/instrument/map/bridge_marshal.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::map; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_browser_scroll.cpp b/tests/test_browser_scroll.cpp index 9448f52..a1114cf 100644 --- a/tests/test_browser_scroll.cpp +++ b/tests/test_browser_scroll.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::browser_scroll — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::browser_scroll — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure editor tests: assert the S12 scroll-window + // scrollbar-thumb + type-to-filter-search geometry LAYERED over the S10 capture_browser. // @@ -11,13 +11,14 @@ // (case-insensitive substring, empty-query identity, no-match); filterNameIndices preserving order // and returning every index for an empty query. -#include "../src/vst/browser_scroll.h" +#include "../src/core/instrument/ui/browser_scroll.h" #include #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -45,7 +46,7 @@ static void testMaxOffsetFitsAndOverflows() { CHECK(scrollMaxOffset(L, L.columns) == 0); // Many rows overflow -> max = content - gridHeight. const int many = L.columns * 20; - const int expect = scrollContentHeight(L, many) - L.grid.height(); + const int expect = scrollContentHeight(L, many) - L.grid.height; CHECK(scrollMaxOffset(L, many) == expect); CHECK(expect > 0); } @@ -67,7 +68,7 @@ static void testVisibleRangeTop() { const VisibleRange vr = visibleCardRange(L, many, 0); CHECK(vr.first == 0); // At offset 0, the last visible row is the one containing (gridH-1). - const int expectedLastRow = (L.grid.height() - 1) / kBrowserCardHeight + 1; + const int expectedLastRow = (L.grid.height - 1) / kBrowserCardHeight + 1; CHECK(vr.last == expectedLastRow * L.columns); } @@ -89,30 +90,30 @@ static void testScrolledCellShiftsUp() { const BrowserLayout L = wideLayout(); const Rect base = cardCellRect(L, 3); const Rect shifted = scrolledCardCellRect(L, 3, 40); - CHECK(shifted.top == base.top - 40); - CHECK(shifted.bottom == base.bottom - 40); - CHECK(shifted.left == base.left); + CHECK(shifted.y == base.y - 40); + CHECK(shifted.bottom() == base.bottom() - 40); + CHECK(shifted.x == base.x); } // --- scrollbar thumb ---------------------------------------------------------- static void testThumbEmptyWhenFits() { const BrowserLayout L = wideLayout(); - CHECK(scrollThumbRect(L, L.columns, 0).height() == 0); // one row fits -> no thumb + CHECK(scrollThumbRect(L, L.columns, 0).height == 0); // one row fits -> no thumb } static void testThumbProportionalAndClamped() { const BrowserLayout L = wideLayout(); const int many = L.columns * 20; const Rect atTop = scrollThumbRect(L, many, 0); - CHECK(atTop.height() > 0); - CHECK(atTop.top == L.grid.top); // at offset 0 the thumb starts at the track top - CHECK(atTop.width() == kScrollbarWidth); - CHECK(atTop.right == L.grid.right); + CHECK(atTop.height > 0); + CHECK(atTop.y == L.grid.y); // at offset 0 the thumb starts at the track top + CHECK(atTop.width == kScrollbarWidth); + CHECK(atTop.right() == L.grid.right()); // At max offset, the thumb bottom reaches the grid bottom (pinned to the end). const int maxOff = scrollMaxOffset(L, many); const Rect atMax = scrollThumbRect(L, many, maxOff); - CHECK(atMax.bottom == L.grid.top + L.grid.height()); + CHECK(atMax.bottom() == L.grid.y + L.grid.height); } static void testThumbDragIsInverse() { @@ -126,7 +127,7 @@ static void testThumbDragIsInverse() { CHECK(thumbDragToOffset(L, many, maxOff, -100000) == 0); // Dragging the thumb by the whole track span from top reaches (near) max. const Rect thumb = scrollThumbRect(L, many, 0); - const int trackSpan = L.grid.height() - thumb.height(); + const int trackSpan = L.grid.height - thumb.height; const int off = thumbDragToOffset(L, many, 0, trackSpan); CHECK(off >= maxOff - 2 && off <= maxOff); } @@ -135,8 +136,8 @@ static void testThumbDragIsInverse() { static void testSearchBoxRect() { const Rect r = searchBoxRect(200); - CHECK(r.left == 0 && r.top == 0 && r.right == 200 && r.height() == kSearchBoxHeight); - CHECK(searchBoxRect(0).width() == 0); + CHECK(r.x == 0 && r.y == 0 && r.right() == 200 && r.height == kSearchBoxHeight); + CHECK(searchBoxRect(0).width == 0); } static void testNameMatch() { diff --git a/tests/test_capture_browser.cpp b/tests/test_capture_browser.cpp index 9f8a7a0..9c0239e 100644 --- a/tests/test_capture_browser.cpp +++ b/tests/test_capture_browser.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::capture_browser — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::capture_browser — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests (embed_strip / editor_geometry): assert // the capture-first browser's card-grid + bank-filter-tab layout and hit-testing directly. // @@ -10,11 +10,12 @@ // the strip into equal segments with the last tab absorbing the remainder; filterTabHitTest // hitting each tab and missing off-strip. -#include "../src/vst/capture_browser.h" +#include "../src/core/instrument/ui/capture_browser.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -25,12 +26,12 @@ static int g_fail = 0; static void testLayoutNormalArea() { // Wide enough for several columns of the fixed-width card. const BrowserLayout L = layoutBrowser(560, 300); - CHECK(L.tabStrip.left == 0 && L.tabStrip.top == 0 && L.tabStrip.right == 560); - CHECK(L.tabStrip.height() == kBrowserTabHeight); + CHECK(L.tabStrip.x == 0 && L.tabStrip.y == 0 && L.tabStrip.right() == 560); + CHECK(L.tabStrip.height == kBrowserTabHeight); // The grid starts right below the tab strip and fills the rest, contiguous. - CHECK(L.grid.top == L.tabStrip.bottom); - CHECK(L.grid.bottom == 300 && L.grid.right == 560); - // columns = grid.width() / cardWidth (>= 1). + CHECK(L.grid.y == L.tabStrip.bottom()); + CHECK(L.grid.bottom() == 300 && L.grid.right() == 560); + // columns = grid.width / cardWidth (>= 1). CHECK(L.columns == 560 / kBrowserCardWidth); CHECK(L.columns >= 1); } @@ -39,22 +40,22 @@ static void testLayoutNarrowAreaSingleColumn() { // Narrower than one card: still a single column, no inversion. const BrowserLayout L = layoutBrowser(kBrowserCardWidth - 10, 200); CHECK(L.columns == 1); - CHECK(L.grid.width() >= 0); - CHECK(L.tabStrip.height() == kBrowserTabHeight); + CHECK(L.grid.width >= 0); + CHECK(L.tabStrip.height == kBrowserTabHeight); } static void testLayoutZeroArea() { const BrowserLayout L = layoutBrowser(0, 0); - CHECK(L.tabStrip.width() == 0 && L.tabStrip.height() == 0); - CHECK(L.grid.width() == 0); + CHECK(L.tabStrip.width == 0 && L.tabStrip.height == 0); + CHECK(L.grid.width == 0); CHECK(L.columns == 1); // never zero (avoids a divide-by-zero in card layout) } static void testLayoutTinyHeightClampsTabStrip() { // A height below the tab band: the tab strip clamps to the area, the grid is empty. const BrowserLayout L = layoutBrowser(560, kBrowserTabHeight - 6); - CHECK(L.tabStrip.height() == kBrowserTabHeight - 6); - CHECK(L.grid.height() <= 0); // no room left for cards + CHECK(L.tabStrip.height == kBrowserTabHeight - 6); + CHECK(L.grid.height <= 0); // no room left for cards } // --- card rects --------------------------------------------------------------- @@ -64,32 +65,32 @@ static void testCardCellsTileRowMajor() { const int cols = L.columns; // Card 0 is top-left of the grid. const Rect c0 = cardCellRect(L, 0); - CHECK(c0.left == L.grid.left && c0.top == L.grid.top); - CHECK(c0.width() == kBrowserCardWidth && c0.height() == kBrowserCardHeight); + CHECK(c0.x == L.grid.x && c0.y == L.grid.y); + CHECK(c0.width == kBrowserCardWidth && c0.height == kBrowserCardHeight); // Card 1 is one card-width to the right, same row. const Rect c1 = cardCellRect(L, 1); - CHECK(c1.left == L.grid.left + kBrowserCardWidth); - CHECK(c1.top == c0.top); + CHECK(c1.x == L.grid.x + kBrowserCardWidth); + CHECK(c1.y == c0.y); // The first card of the SECOND row wraps back to the left, one card-height down. const Rect wrap = cardCellRect(L, cols); - CHECK(wrap.left == L.grid.left); - CHECK(wrap.top == L.grid.top + kBrowserCardHeight); + CHECK(wrap.x == L.grid.x); + CHECK(wrap.y == L.grid.y + kBrowserCardHeight); } static void testCardCellNegativeIndex() { const BrowserLayout L = layoutBrowser(560, 300); const Rect r = cardCellRect(L, -1); - CHECK(r.left == 0 && r.top == 0 && r.right == 0 && r.bottom == 0); + CHECK(r.x == 0 && r.y == 0 && r.right() == 0 && r.bottom() == 0); } static void testCardContentInsetByGutter() { const BrowserLayout L = layoutBrowser(560, 300); const Rect cell = cardCellRect(L, 0); const Rect content = cardContentRect(L, 0); - CHECK(content.left == cell.left + kBrowserCardGutter); - CHECK(content.top == cell.top + kBrowserCardGutter); - CHECK(content.right == cell.right - kBrowserCardGutter); - CHECK(content.bottom == cell.bottom - kBrowserCardGutter); + CHECK(content.x == cell.x + kBrowserCardGutter); + CHECK(content.y == cell.y + kBrowserCardGutter); + CHECK(content.right() == cell.right() - kBrowserCardGutter); + CHECK(content.bottom() == cell.bottom() - kBrowserCardGutter); } static void testThumbnailAboveLabel() { @@ -98,12 +99,12 @@ static void testThumbnailAboveLabel() { const Rect thumb = cardThumbnailRect(L, 0); const Rect label = cardLabelRect(L, 0); // Thumbnail is the top band of the content; the label is the remainder below it, contiguous. - CHECK(thumb.left == content.left && thumb.right == content.right); - CHECK(thumb.top == content.top); - CHECK(thumb.height() == kBrowserThumbHeight); - CHECK(label.top == thumb.bottom); - CHECK(label.bottom == content.bottom); - CHECK(label.left == content.left && label.right == content.right); + CHECK(thumb.x == content.x && thumb.right() == content.right()); + CHECK(thumb.y == content.y); + CHECK(thumb.height == kBrowserThumbHeight); + CHECK(label.y == thumb.bottom()); + CHECK(label.bottom() == content.bottom()); + CHECK(label.x == content.x && label.right() == content.right()); } // --- cardHitTest -------------------------------------------------------------- @@ -111,8 +112,8 @@ static void testThumbnailAboveLabel() { static void testCardHitCenterOfCard() { const BrowserLayout L = layoutBrowser(560, 300); const Rect content = cardContentRect(L, 3); - const int cx = content.left + content.width() / 2; - const int cy = content.top + content.height() / 2; + const int cx = content.x + content.width / 2; + const int cy = content.y + content.height / 2; CHECK(cardHitTest(L, 12, cx, cy) == 3); } @@ -121,21 +122,21 @@ static void testCardHitMissesGutter() { // A point in the gutter between the content and the cell edge (top-left corner of cell 0) // is a miss — only the card CONTENT counts. const Rect cell = cardCellRect(L, 0); - CHECK(cardHitTest(L, 12, cell.left, cell.top) == -1); + CHECK(cardHitTest(L, 12, cell.x, cell.y) == -1); } static void testCardHitMissesPastLastCard() { const BrowserLayout L = layoutBrowser(560, 300); // Only 2 cards exist; a point on where card 5 WOULD be is a miss. const Rect content = cardContentRect(L, 5); - const int cx = content.left + content.width() / 2; - const int cy = content.top + content.height() / 2; + const int cx = content.x + content.width / 2; + const int cy = content.y + content.height / 2; CHECK(cardHitTest(L, 2, cx, cy) == -1); } static void testCardHitMissesTabStrip() { const BrowserLayout L = layoutBrowser(560, 300); - CHECK(cardHitTest(L, 12, 10, L.tabStrip.top + 2) == -1); + CHECK(cardHitTest(L, 12, 10, L.tabStrip.y + 2) == -1); } static void testCardHitZeroCards() { @@ -150,21 +151,21 @@ static void testFilterTabsTileStrip() { const int n = 4; // "All" + 3 banks const Rect t0 = filterTabRect(L, n, 0); const Rect tLast = filterTabRect(L, n, n - 1); - CHECK(t0.left == L.tabStrip.left); + CHECK(t0.x == L.tabStrip.x); // Adjacent tabs share an exact edge (no gap). - CHECK(filterTabRect(L, n, 0).right == filterTabRect(L, n, 1).left); - CHECK(filterTabRect(L, n, 1).right == filterTabRect(L, n, 2).left); + CHECK(filterTabRect(L, n, 0).right() == filterTabRect(L, n, 1).x); + CHECK(filterTabRect(L, n, 1).right() == filterTabRect(L, n, 2).x); // The last tab reaches the strip's right edge exactly (absorbs the remainder). - CHECK(tLast.right == L.tabStrip.right); + CHECK(tLast.right() == L.tabStrip.right()); // All tabs share the strip's height. - CHECK(t0.top == L.tabStrip.top && t0.bottom == L.tabStrip.bottom); + CHECK(t0.y == L.tabStrip.y && t0.bottom() == L.tabStrip.bottom()); } static void testFilterTabOutOfRange() { const BrowserLayout L = layoutBrowser(560, 300); - CHECK(filterTabRect(L, 3, -1).width() == 0); - CHECK(filterTabRect(L, 3, 3).width() == 0); - CHECK(filterTabRect(L, 0, 0).width() == 0); + CHECK(filterTabRect(L, 3, -1).width == 0); + CHECK(filterTabRect(L, 3, 3).width == 0); + CHECK(filterTabRect(L, 0, 0).width == 0); } static void testFilterTabHit() { @@ -172,12 +173,12 @@ static void testFilterTabHit() { const int n = 3; for (int i = 0; i < n; ++i) { const Rect t = filterTabRect(L, n, i); - const int cx = t.left + t.width() / 2; - const int cy = t.top + t.height() / 2; + const int cx = t.x + t.width / 2; + const int cy = t.y + t.height / 2; CHECK(filterTabHitTest(L, n, cx, cy) == i); } // Below the strip (in the grid) -> no tab. - CHECK(filterTabHitTest(L, n, 20, L.grid.top + 4) == -1); + CHECK(filterTabHitTest(L, n, 20, L.grid.y + 4) == -1); } int main() { diff --git a/tests/test_capture_paths.cpp b/tests/test_capture_paths.cpp index aff2b5e..3d253c9 100644 --- a/tests/test_capture_paths.cpp +++ b/tests/test_capture_paths.cpp @@ -1,9 +1,9 @@ // Standalone tests for reasampler::capture_paths — no REAPER, no framework. // The capture shell is DAW-bound and only verifiable in REAPER; this covers the // one genuinely pure piece: the bank-folder / unique-name / project-relative -// path arithmetic that feeds BankIndex::add's relative-only invariant. +// path arithmetic that feeds BankModel::add's relative-only invariant. -#include "../src/capture_paths.h" +#include "../src/core/capture/capture_paths.h" #include #include @@ -12,6 +12,7 @@ #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -74,7 +75,7 @@ static void testDeriveRelativePathIsProjectRelative() { BankPaths p = deriveBankPaths("C:\\Users\\d\\proj", "master mix", "1753080000"); // Relative path is under the fixed bank subfolder, forward-slashed, .wav. CHECK(p.relativePath == "reasampler_bank/master_mix_1753080000.wav"); - // It must NOT be absolute by any of BankIndex::add's rejection rules: + // It must NOT be absolute by any of BankModel::add's rejection rules: // no leading '/', no drive letter, no backslash, no UNC prefix. CHECK(p.relativePath.find(':') == std::string::npos); CHECK(p.relativePath.find('\\') == std::string::npos); diff --git a/tests/test_card_drag.cpp b/tests/test_card_drag.cpp index a7018f4..473c6f1 100644 --- a/tests/test_card_drag.cpp +++ b/tests/test_card_drag.cpp @@ -7,11 +7,12 @@ // -> None); cursor-cue mapping (incl. Replace only for Replace); slot rects include empties // (gap layout), dense layout matches a plain grid, slot hit-test returns slot index + miss. -#include "../src/card_drag.h" +#include "../src/core/ui/card_drag.h" #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_card_meta.cpp b/tests/test_card_meta.cpp index 37fcdbf..b94974b 100644 --- a/tests/test_card_meta.cpp +++ b/tests/test_card_meta.cpp @@ -6,12 +6,13 @@ // long capture, non-4/4 meters (3/4 and 6/8), unstamped meter -> blank, unknown tempo -> // blank (s.ms still derivable); s.ms zero / sub-second / multi-second / ms carry / negative. -#include "../src/card_meta.h" +#include "../src/core/ui/card_meta.h" #include #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_component_geometry.cpp b/tests/test_component_geometry.cpp index 3d97095..a793ac4 100644 --- a/tests/test_component_geometry.cpp +++ b/tests/test_component_geometry.cpp @@ -7,11 +7,12 @@ // row, hover hit-test returns the right row and "no hit" outside/past the last row; and the // shared half-open box hit-test agrees with layout (no double-claimed pixel). -#include "../src/component_geometry.h" +#include "../src/core/ui/component_geometry.h" #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_curve_popup.cpp b/tests/test_curve_popup.cpp index 9e6c5c2..262e560 100644 --- a/tests/test_curve_popup.cpp +++ b/tests/test_curve_popup.cpp @@ -1,14 +1,15 @@ -// Standalone tests for reasampler::vst::curve_popup — no VST3, no REAPER, no framework. Same +// Standalone tests for reasampler::instrument::ui::curve_popup — no VST3, no REAPER, no framework. Same // fast assert loop as the sibling pure tests. Assert the r11 popup-sheet geometry at the size // clamps (the spec's width clamp(60%, 360..520) / height clamp(55%, 260..380)), the centering, // the title-row/close-button placement, the curve-box remainder, and the outside-sheet // dismissal test. -#include "../src/vst/curve_popup.h" +#include "../src/core/instrument/ui/curve_popup.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -17,66 +18,66 @@ static int g_fail = 0; static void testDefaultWindowMidClamp() { // 840x620: 60% = 504 (inside 360..520), 55% = 341 (inside 260..380). const CurvePopupLayout pl = computeCurvePopup(840, 620); - CHECK(pl.sheet.width() == 504); - CHECK(pl.sheet.height() == 341); + CHECK(pl.sheet.width == 504); + CHECK(pl.sheet.height == 341); // Centered (within the integer-division pixel). - CHECK(pl.sheet.left == (840 - 504) / 2); - CHECK(pl.sheet.top == (620 - 341) / 2); + CHECK(pl.sheet.x == (840 - 504) / 2); + CHECK(pl.sheet.y == (620 - 341) / 2); } static void testMinClamp() { // The 560x460 constraint floor: 60% = 336 -> clamps UP to 360; 55% = 253 -> up to 260. const CurvePopupLayout pl = computeCurvePopup(560, 460); - CHECK(pl.sheet.width() == kCurvePopupMinW); - CHECK(pl.sheet.height() == kCurvePopupMinH); - CHECK(pl.sheet.left >= 0 && pl.sheet.right <= 560); - CHECK(pl.sheet.top >= 0 && pl.sheet.bottom <= 460); + CHECK(pl.sheet.width == kCurvePopupMinW); + CHECK(pl.sheet.height == kCurvePopupMinH); + CHECK(pl.sheet.x >= 0 && pl.sheet.right() <= 560); + CHECK(pl.sheet.y >= 0 && pl.sheet.bottom() <= 460); } static void testMaxClamp() { // A large window: 60% of 1600 = 960 -> clamps DOWN to 520; 55% of 900 = 495 -> down to 380. const CurvePopupLayout pl = computeCurvePopup(1600, 900); - CHECK(pl.sheet.width() == kCurvePopupMaxW); - CHECK(pl.sheet.height() == kCurvePopupMaxH); + CHECK(pl.sheet.width == kCurvePopupMaxW); + CHECK(pl.sheet.height == kCurvePopupMaxH); } static void testDegenerateWindowNeverOverhangs() { // A window smaller than the min clamp: the sheet caps at the window dimension (defensive — // below checkSizeConstraint, but geometry must stay sane). const CurvePopupLayout pl = computeCurvePopup(300, 200); - CHECK(pl.sheet.width() == 300); - CHECK(pl.sheet.height() == 200); - CHECK(pl.sheet.left == 0 && pl.sheet.top == 0); + CHECK(pl.sheet.width == 300); + CHECK(pl.sheet.height == 200); + CHECK(pl.sheet.x == 0 && pl.sheet.y == 0); } static void testTitleRowAndCurveBox() { const CurvePopupLayout pl = computeCurvePopup(840, 620); // Close: 18x18, right-anchored inside the title row. - CHECK(pl.close.width() == kCurvePopupCloseSize && pl.close.height() == kCurvePopupCloseSize); - CHECK(pl.close.right == pl.sheet.right - kCurvePopupPad); - CHECK(pl.close.top >= pl.sheet.top); - CHECK(pl.close.bottom <= pl.sheet.top + kCurvePopupTitleH); + CHECK(pl.close.width == kCurvePopupCloseSize && pl.close.height == kCurvePopupCloseSize); + CHECK(pl.close.right() == pl.sheet.right() - kCurvePopupPad); + CHECK(pl.close.y >= pl.sheet.y); + CHECK(pl.close.bottom() <= pl.sheet.y + kCurvePopupTitleH); // Title text: left of the close button, in the title row. - CHECK(pl.title.left == pl.sheet.left + kCurvePopupPad); - CHECK(pl.title.right <= pl.close.left); + CHECK(pl.title.x == pl.sheet.x + kCurvePopupPad); + CHECK(pl.title.right() <= pl.close.x); // Curve box: fills the remainder below the title row, inside the sheet margins. - CHECK(pl.curveBox.top >= pl.sheet.top + kCurvePopupTitleH); - CHECK(pl.curveBox.left == pl.sheet.left + kCurvePopupPad); - CHECK(pl.curveBox.right == pl.sheet.right - kCurvePopupPad); - CHECK(pl.curveBox.bottom == pl.sheet.bottom - kCurvePopupPad); - CHECK(pl.curveBox.width() > 0 && pl.curveBox.height() > 0); + CHECK(pl.curveBox.y >= pl.sheet.y + kCurvePopupTitleH); + CHECK(pl.curveBox.x == pl.sheet.x + kCurvePopupPad); + CHECK(pl.curveBox.right() == pl.sheet.right() - kCurvePopupPad); + CHECK(pl.curveBox.bottom() == pl.sheet.bottom() - kCurvePopupPad); + CHECK(pl.curveBox.width > 0 && pl.curveBox.height > 0); } static void testOutsideSheetDismissTest() { const CurvePopupLayout pl = computeCurvePopup(840, 620); // On the wash: outside. CHECK(popupOutsideSheet(pl, 0, 0)); - CHECK(popupOutsideSheet(pl, pl.sheet.left - 1, pl.sheet.top + 10)); - CHECK(popupOutsideSheet(pl, pl.sheet.right, pl.sheet.top + 10)); // half-open right edge + CHECK(popupOutsideSheet(pl, pl.sheet.x - 1, pl.sheet.y + 10)); + CHECK(popupOutsideSheet(pl, pl.sheet.right(), pl.sheet.y + 10)); // half-open right edge // On the sheet (title row, curve box, padding): inside. - CHECK(!popupOutsideSheet(pl, pl.sheet.left, pl.sheet.top)); - CHECK(!popupOutsideSheet(pl, pl.curveBox.left + 5, pl.curveBox.top + 5)); - CHECK(!popupOutsideSheet(pl, pl.sheet.right - 1, pl.sheet.bottom - 1)); + CHECK(!popupOutsideSheet(pl, pl.sheet.x, pl.sheet.y)); + CHECK(!popupOutsideSheet(pl, pl.curveBox.x + 5, pl.curveBox.y + 5)); + CHECK(!popupOutsideSheet(pl, pl.sheet.right() - 1, pl.sheet.bottom() - 1)); } int main() { diff --git a/tests/test_drag_out.cpp b/tests/test_drag_out.cpp index 8da3977..3511c44 100644 --- a/tests/test_drag_out.cpp +++ b/tests/test_drag_out.cpp @@ -9,13 +9,14 @@ // * Path-list assembly: single, multi, dedupe (cross-bank copy case), skip-missing, // skip-unresolved, empty selection, order preservation, mixed tallies. -#include "../src/drag_out.h" +#include "../src/core/ui/drag_out.h" #include #include #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_editor_geometry.cpp b/tests/test_editor_geometry.cpp index 2d363bf..858fcb7 100644 --- a/tests/test_editor_geometry.cpp +++ b/tests/test_editor_geometry.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::editor_geometry — no VST3, no REAPER, no test +// Standalone tests for reasampler::instrument::ui::editor_geometry — no VST3, no REAPER, no test // framework. Same fast assert loop as the sibling pure tests (mode_switch et al.): // assert the IPlugView LICE editor's layout math + hit-testing directly. // @@ -8,11 +8,12 @@ // the button, missing on the title/canvas, missing outside the surface, and boundary // pixels; layout<->hit-test agreement (a click on the drawn button rect hits it). -#include "../src/vst/editor_geometry.h" +#include "../src/core/instrument/ui/editor_geometry.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -21,7 +22,7 @@ static int g_fail = 0; // --- contains() --------------------------------------------------------------- static void testContainsHalfOpen() { - Rect r{10, 20, 50, 40}; // [10,50) x [20,40) + Rect r = Rect::ltrb(10, 20, 50, 40); // [10,50) x [20,40) CHECK(contains(r, 10, 20)); // top-left inclusive CHECK(contains(r, 49, 39)); // bottom-right exclusive edge, inside CHECK(!contains(r, 50, 30)); // right edge excluded @@ -31,9 +32,9 @@ static void testContainsHalfOpen() { } static void testContainsDegenerate() { - CHECK(!contains(Rect{10, 10, 10, 20}, 10, 15)); // zero width - CHECK(!contains(Rect{10, 10, 20, 10}, 15, 10)); // zero height - CHECK(!contains(Rect{20, 10, 10, 20}, 15, 15)); // inverted (right < left) + CHECK(!contains(Rect::ltrb(10, 10, 10, 20), 10, 15)); // zero width + CHECK(!contains(Rect::ltrb(10, 10, 20, 10), 15, 10)); // zero height + CHECK(!contains(Rect::ltrb(20, 10, 10, 20), 15, 15)); // inverted (right < left) } // --- layoutEditor: normal view ------------------------------------------------ @@ -43,20 +44,20 @@ static void testLayoutNormalView() { // rest; button sits inside the canvas, inset by the margin. const EditorLayout L = layoutEditor(400, 260); - CHECK(L.titleBar.left == 0 && L.titleBar.top == 0); - CHECK(L.titleBar.right == 400); - CHECK(L.titleBar.height() > 0 && L.titleBar.height() <= 260); + CHECK(L.titleBar.x == 0 && L.titleBar.y == 0); + CHECK(L.titleBar.right() == 400); + CHECK(L.titleBar.height > 0 && L.titleBar.height <= 260); // Canvas begins right below the title bar and reaches the bottom-right. - CHECK(L.canvas.top == L.titleBar.bottom); - CHECK(L.canvas.right == 400 && L.canvas.bottom == 260); + CHECK(L.canvas.y == L.titleBar.bottom()); + CHECK(L.canvas.right() == 400 && L.canvas.bottom() == 260); // Button is inside the canvas (does not overhang any edge). - CHECK(L.button.left >= L.canvas.left); - CHECK(L.button.top >= L.canvas.top); - CHECK(L.button.right <= L.canvas.right); - CHECK(L.button.bottom <= L.canvas.bottom); - CHECK(L.button.width() > 0 && L.button.height() > 0); + CHECK(L.button.x >= L.canvas.x); + CHECK(L.button.y >= L.canvas.y); + CHECK(L.button.right() <= L.canvas.right()); + CHECK(L.button.bottom() <= L.canvas.bottom()); + CHECK(L.button.width > 0 && L.button.height > 0); } // --- layoutEditor: tiny view (clamping) --------------------------------------- @@ -65,25 +66,25 @@ static void testLayoutTinyViewClampsButton() { // A view narrower/shorter than the button's natural size: the button must clamp to // the canvas and never produce an inverted or overhanging rect. const EditorLayout L = layoutEditor(40, 40); - CHECK(L.button.right <= L.canvas.right); - CHECK(L.button.bottom <= L.canvas.bottom); - CHECK(L.button.right >= L.button.left); // never inverted - CHECK(L.button.bottom >= L.button.top); + CHECK(L.button.right() <= L.canvas.right()); + CHECK(L.button.bottom() <= L.canvas.bottom()); + CHECK(L.button.right() >= L.button.x); // never inverted + CHECK(L.button.bottom() >= L.button.y); // Title bar clamps to the client height when the view is shorter than its height. - CHECK(L.titleBar.bottom <= 40); + CHECK(L.titleBar.bottom() <= 40); } // --- layoutEditor: zero view (all empty, no inversion) ------------------------ static void testLayoutZeroView() { const EditorLayout L = layoutEditor(0, 0); - CHECK(L.titleBar.width() <= 0 || L.titleBar.height() <= 0); - CHECK(L.canvas.width() <= 0 || L.canvas.height() <= 0); + CHECK(L.titleBar.width <= 0 || L.titleBar.height <= 0); + CHECK(L.canvas.width <= 0 || L.canvas.height <= 0); // No rect is inverted. - CHECK(L.button.right >= L.button.left); - CHECK(L.button.bottom >= L.button.top); - CHECK(L.canvas.right >= L.canvas.left); - CHECK(L.canvas.bottom >= L.canvas.top); + CHECK(L.button.right() >= L.button.x); + CHECK(L.button.bottom() >= L.button.y); + CHECK(L.canvas.right() >= L.canvas.x); + CHECK(L.canvas.bottom() >= L.canvas.y); // A click anywhere on an empty layout hits nothing. CHECK(hitTest(L, 0, 0) == HitTarget::kNone); CHECK(hitTest(L, 5, 5) == HitTarget::kNone); @@ -94,15 +95,15 @@ static void testLayoutZeroView() { static void testHitTestButton() { const EditorLayout L = layoutEditor(400, 260); // Center of the button hits it. - const int cx = (L.button.left + L.button.right) / 2; - const int cy = (L.button.top + L.button.bottom) / 2; + const int cx = (L.button.x + L.button.right()) / 2; + const int cy = (L.button.y + L.button.bottom()) / 2; CHECK(hitTest(L, cx, cy) == HitTarget::kButton); } static void testHitTestMissesNonButton() { const EditorLayout L = layoutEditor(400, 260); // Title bar is inert in the spike. - CHECK(hitTest(L, 200, L.titleBar.top + 1) == HitTarget::kNone); + CHECK(hitTest(L, 200, L.titleBar.y + 1) == HitTarget::kNone); // Empty canvas away from the button. CHECK(hitTest(L, 380, 240) == HitTarget::kNone); // Outside the surface entirely. @@ -113,9 +114,9 @@ static void testHitTestMissesNonButton() { static void testHitTestButtonBoundary() { const EditorLayout L = layoutEditor(400, 260); // Top-left corner of the button is inclusive; the right/bottom edges are excluded. - CHECK(hitTest(L, L.button.left, L.button.top) == HitTarget::kButton); - CHECK(hitTest(L, L.button.right, L.button.top) == HitTarget::kNone); - CHECK(hitTest(L, L.button.left, L.button.bottom) == HitTarget::kNone); + CHECK(hitTest(L, L.button.x, L.button.y) == HitTarget::kButton); + CHECK(hitTest(L, L.button.right(), L.button.y) == HitTarget::kNone); + CHECK(hitTest(L, L.button.x, L.button.bottom()) == HitTarget::kNone); } // --- layout<->hit-test agreement ---------------------------------------------- @@ -124,8 +125,8 @@ static void testHitTestButtonBoundary() { // load-bearing consistency invariant between what the shell draws and what it routes. static void testHitTestMatchesDrawnButton() { const EditorLayout L = layoutEditor(320, 200); - for (int y = L.button.top; y < L.button.bottom; ++y) { - for (int x = L.button.left; x < L.button.right; ++x) { + for (int y = L.button.y; y < L.button.bottom(); ++y) { + for (int x = L.button.x; x < L.button.right(); ++x) { CHECK(hitTest(L, x, y) == HitTarget::kButton); } } @@ -138,14 +139,14 @@ static void testSampleRowRectStacks() { const Rect r0 = sampleRowRect(L, 0); const Rect r1 = sampleRowRect(L, 1); // Row 0 starts at the canvas top and spans its full width. - CHECK(r0.top == L.canvas.top); - CHECK(r0.left == L.canvas.left && r0.right == L.canvas.right); - CHECK(r0.height() == kSampleRowHeight); + CHECK(r0.y == L.canvas.y); + CHECK(r0.x == L.canvas.x && r0.right() == L.canvas.right()); + CHECK(r0.height == kSampleRowHeight); // Row 1 sits directly below row 0 (no gap, no overlap). - CHECK(r1.top == r0.bottom); - CHECK(r1.height() == kSampleRowHeight); + CHECK(r1.y == r0.bottom()); + CHECK(r1.height == kSampleRowHeight); // A negative index is an empty rect. - CHECK(sampleRowRect(L, -1).width() == 0 && sampleRowRect(L, -1).height() == 0); + CHECK(sampleRowRect(L, -1).width == 0 && sampleRowRect(L, -1).height == 0); } static void testSampleRowHitTestMapsClickToRow() { @@ -153,35 +154,35 @@ static void testSampleRowHitTestMapsClickToRow() { const int rows = 5; // A click in the vertical middle of row 2 resolves to index 2. const Rect r2 = sampleRowRect(L, 2); - const int midY = (r2.top + r2.bottom) / 2; + const int midY = (r2.y + r2.bottom()) / 2; CHECK(sampleRowHitTest(L, rows, 200, midY) == 2); // Row 0's top-left corner hits row 0. const Rect r0 = sampleRowRect(L, 0); - CHECK(sampleRowHitTest(L, rows, r0.left, r0.top) == 0); + CHECK(sampleRowHitTest(L, rows, r0.x, r0.y) == 0); } static void testSampleRowHitTestMisses() { const EditorLayout L = layoutEditor(400, 260); const int rows = 3; // Above the first row (in the title bar) -> no row. - CHECK(sampleRowHitTest(L, rows, 200, L.titleBar.top) == -1); + CHECK(sampleRowHitTest(L, rows, 200, L.titleBar.y) == -1); // Below the last row -> no row. const Rect last = sampleRowRect(L, rows - 1); - CHECK(sampleRowHitTest(L, rows, 200, last.bottom + 1) == -1); + CHECK(sampleRowHitTest(L, rows, 200, last.bottom() + 1) == -1); // Left of the canvas -> no row. - CHECK(sampleRowHitTest(L, rows, L.canvas.left - 1, last.top) == -1); + CHECK(sampleRowHitTest(L, rows, L.canvas.x - 1, last.y) == -1); // Zero rows -> always -1. - CHECK(sampleRowHitTest(L, 0, 200, L.canvas.top + 1) == -1); - // At or below canvas.bottom -> always -1, even if rowCount would cover that y. + CHECK(sampleRowHitTest(L, 0, 200, L.canvas.y + 1) == -1); + // At or below canvas.bottom() -> always -1, even if rowCount would cover that y. // This guards paint<->hit-test agreement: sampleRowRect does not clamp to canvas, - // so without this clip a row that extends past canvas.bottom would hit-test but + // so without this clip a row that extends past canvas.bottom() would hit-test but // never be drawn (or vice versa). - CHECK(sampleRowHitTest(L, rows, 200, L.canvas.bottom) == -1); + CHECK(sampleRowHitTest(L, rows, 200, L.canvas.bottom()) == -1); // Use a large rowCount so index arithmetic would return a valid row without the - // canvas.bottom guard — proving the guard fires independently of rowCount. + // canvas.bottom() guard — proving the guard fires independently of rowCount. const int bigRows = 1000; - CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom) == -1); - CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom + 5) == -1); + CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom()) == -1); + CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom() + 5) == -1); } // The drawn-row <-> hit-test agreement: every pixel inside a row rect must resolve to @@ -191,10 +192,10 @@ static void testSampleRowHitTestMatchesDrawnRows() { const int rows = 4; for (int i = 0; i < rows; ++i) { const Rect r = sampleRowRect(L, i); - if (r.top >= L.canvas.bottom) break; // clipped rows aren't clickable targets - const int y = (r.top + r.bottom) / 2; - if (y >= L.canvas.bottom) continue; - CHECK(sampleRowHitTest(L, rows, r.left + 1, y) == i); + if (r.y >= L.canvas.bottom()) break; // clipped rows aren't clickable targets + const int y = (r.y + r.bottom()) / 2; + if (y >= L.canvas.bottom()) continue; + CHECK(sampleRowHitTest(L, rows, r.x + 1, y) == i); } } @@ -204,30 +205,30 @@ static void testKeymapLayoutSplitsCanvas() { const KeymapEditorLayout L = layoutKeymapEditor(600, 300); // The left sample list and right zone panel partition the canvas with no overlap and // no gap: the list's right edge is the panel's left edge. - CHECK(L.sampleList.left == L.base.canvas.left); - CHECK(L.sampleList.right == L.zonePanel.left); - CHECK(L.zonePanel.right == L.base.canvas.right); - CHECK(L.sampleList.top == L.base.canvas.top); - CHECK(L.zonePanel.top == L.base.canvas.top); - CHECK(L.sampleList.bottom == L.base.canvas.bottom); - CHECK(L.zonePanel.bottom == L.base.canvas.bottom); - CHECK(L.sampleList.width() > 0 && L.zonePanel.width() > 0); + CHECK(L.sampleList.x == L.base.canvas.x); + CHECK(L.sampleList.right() == L.zonePanel.x); + CHECK(L.zonePanel.right() == L.base.canvas.right()); + CHECK(L.sampleList.y == L.base.canvas.y); + CHECK(L.zonePanel.y == L.base.canvas.y); + CHECK(L.sampleList.bottom() == L.base.canvas.bottom()); + CHECK(L.zonePanel.bottom() == L.base.canvas.bottom()); + CHECK(L.sampleList.width > 0 && L.zonePanel.width > 0); // Add-Zone button caps the panel; zone rows stack below it. - CHECK(L.addZoneButton.top == L.zonePanel.top); - CHECK(L.addZoneButton.left == L.zonePanel.left && L.addZoneButton.right == L.zonePanel.right); - CHECK(L.zoneRowArea.top == L.addZoneButton.bottom); - CHECK(L.zoneRowArea.bottom == L.zonePanel.bottom); + CHECK(L.addZoneButton.y == L.zonePanel.y); + CHECK(L.addZoneButton.x == L.zonePanel.x && L.addZoneButton.right() == L.zonePanel.right()); + CHECK(L.zoneRowArea.y == L.addZoneButton.bottom()); + CHECK(L.zoneRowArea.bottom() == L.zonePanel.bottom()); } static void checkNoInversion(const KeymapEditorLayout& L) { - CHECK(L.sampleList.right >= L.sampleList.left); - CHECK(L.zonePanel.right >= L.zonePanel.left); - CHECK(L.addZoneButton.right >= L.addZoneButton.left); - CHECK(L.addZoneButton.bottom >= L.addZoneButton.top); - CHECK(L.zoneRowArea.right >= L.zoneRowArea.left); - CHECK(L.zoneRowArea.bottom >= L.zoneRowArea.top); + CHECK(L.sampleList.right() >= L.sampleList.x); + CHECK(L.zonePanel.right() >= L.zonePanel.x); + CHECK(L.addZoneButton.right() >= L.addZoneButton.x); + CHECK(L.addZoneButton.bottom() >= L.addZoneButton.y); + CHECK(L.zoneRowArea.right() >= L.zoneRowArea.x); + CHECK(L.zoneRowArea.bottom() >= L.zoneRowArea.y); // Regions stay within the client area. - CHECK(L.zonePanel.right <= L.base.canvas.right); + CHECK(L.zonePanel.right() <= L.base.canvas.right()); } static void testKeymapLayoutTinyAndZeroNoInversion() { @@ -243,36 +244,36 @@ static void testKeymapSampleRowInLeftColumn() { const KeymapEditorLayout L = layoutKeymapEditor(600, 300); const Rect r0 = keymapSampleRowRect(L, 0); // Rows live in the LEFT column (not the full canvas width). - CHECK(r0.left == L.sampleList.left && r0.right == L.sampleList.right); - CHECK(r0.right < L.base.canvas.right); // strictly left of the zone panel - CHECK(r0.top == L.sampleList.top && r0.height() == kSampleRowHeight); + CHECK(r0.x == L.sampleList.x && r0.right() == L.sampleList.right()); + CHECK(r0.right() < L.base.canvas.right()); // strictly left of the zone panel + CHECK(r0.y == L.sampleList.y && r0.height == kSampleRowHeight); // Hit-test maps a left-column click to the row and rejects a click in the zone panel. - const int midY = (r0.top + r0.bottom) / 2; - CHECK(keymapSampleRowHitTest(L, 3, r0.left + 2, midY) == 0); - CHECK(keymapSampleRowHitTest(L, 3, L.zonePanel.left + 2, midY) == -1); + const int midY = (r0.y + r0.bottom()) / 2; + CHECK(keymapSampleRowHitTest(L, 3, r0.x + 2, midY) == 0); + CHECK(keymapSampleRowHitTest(L, 3, L.zonePanel.x + 2, midY) == -1); } static void testAddZoneHitTest() { const KeymapEditorLayout L = layoutKeymapEditor(600, 300); - const int cx = (L.addZoneButton.left + L.addZoneButton.right) / 2; - const int cy = (L.addZoneButton.top + L.addZoneButton.bottom) / 2; + const int cx = (L.addZoneButton.x + L.addZoneButton.right()) / 2; + const int cy = (L.addZoneButton.y + L.addZoneButton.bottom()) / 2; CHECK(addZoneHitTest(L, cx, cy)); // A click in the zone-row area below the button is NOT the Add button. - CHECK(!addZoneHitTest(L, cx, L.zoneRowArea.top + 2)); + CHECK(!addZoneHitTest(L, cx, L.zoneRowArea.y + 2)); // A click in the left list is NOT the Add button. - CHECK(!addZoneHitTest(L, L.sampleList.left + 2, L.sampleList.top + 2)); + CHECK(!addZoneHitTest(L, L.sampleList.x + 2, L.sampleList.y + 2)); } static void testZoneRowStacksAndSelects() { const KeymapEditorLayout L = layoutKeymapEditor(600, 300); const Rect z0 = zoneRowRect(L, 0); const Rect z1 = zoneRowRect(L, 1); - CHECK(z0.top == L.zoneRowArea.top && z0.height() == kZoneRowHeight); - CHECK(z1.top == z0.bottom); // stacked, no gap - CHECK(z0.left == L.zoneRowArea.left && z0.right == L.zoneRowArea.right); + CHECK(z0.y == L.zoneRowArea.y && z0.height == kZoneRowHeight); + CHECK(z1.y == z0.bottom()); // stacked, no gap + CHECK(z0.x == L.zoneRowArea.x && z0.right() == L.zoneRowArea.right()); // A click on the LABEL area (left part of a zone row) selects the zone with no field. - const int labelX = z0.left + 2; // far left = label, not a control - const int midY = (z0.top + z0.bottom) / 2; + const int labelX = z0.x + 2; // far left = label, not a control + const int midY = (z0.y + z0.bottom()) / 2; const ZoneHit h = zoneHitTest(L, 2, labelX, midY); CHECK(h.zoneIndex == 0 && h.field == ZoneField::kZoneNone); } @@ -280,10 +281,10 @@ static void testZoneRowStacksAndSelects() { static void testZoneRowControlsMapToFields() { const KeymapEditorLayout L = layoutKeymapEditor(600, 300); const Rect row = zoneRowRect(L, 0); - const int midY = (row.top + row.bottom) / 2; + const int midY = (row.y + row.bottom()) / 2; // The seven controls occupy the rightmost 7*kZoneCtrlWidth px, left-to-right: // low-, low+, high-, high+, root-, root+, delete. - const int block = row.right - 7 * kZoneCtrlWidth; + const int block = row.right() - 7 * kZoneCtrlWidth; const ZoneField expected[7] = { ZoneField::kLowDown, ZoneField::kLowUp, ZoneField::kHighDown, ZoneField::kHighUp, ZoneField::kRootDown, ZoneField::kRootUp, @@ -300,14 +301,14 @@ static void testZoneRowControlsMapToFields() { static void testZoneHitTestMisses() { const KeymapEditorLayout L = layoutKeymapEditor(600, 300); const Rect row = zoneRowRect(L, 0); - const int midY = (row.top + row.bottom) / 2; + const int midY = (row.y + row.bottom()) / 2; // Zero zones -> always miss. - CHECK(zoneHitTest(L, 0, row.left + 2, midY).zoneIndex == -1); + CHECK(zoneHitTest(L, 0, row.x + 2, midY).zoneIndex == -1); // Below the last zone row -> miss. const Rect last = zoneRowRect(L, 2); - CHECK(zoneHitTest(L, 3, row.left + 2, last.bottom + 1).zoneIndex == -1); + CHECK(zoneHitTest(L, 3, row.x + 2, last.bottom() + 1).zoneIndex == -1); // Left of the zone panel (in the sample list) -> miss. - CHECK(zoneHitTest(L, 3, L.sampleList.left + 2, midY).zoneIndex == -1); + CHECK(zoneHitTest(L, 3, L.sampleList.x + 2, midY).zoneIndex == -1); } int main() { diff --git a/tests/test_embed_strip.cpp b/tests/test_embed_strip.cpp index 4ce3c22..9d3ea93 100644 --- a/tests/test_embed_strip.cpp +++ b/tests/test_embed_strip.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::embed_strip — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::embed_strip — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests (editor_geometry et al.): assert the // embedded TCP/MCP strip's layout math + zone hit-testing + level fill directly. // @@ -9,11 +9,12 @@ // on overlap, missing on uncovered keys and off-band, and rejecting a null/empty list; // levelFillRect clamping 0..1 and its endpoints. -#include "../src/vst/embed_strip.h" +#include "../src/core/instrument/ui/embed_strip.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -24,34 +25,34 @@ static int g_fail = 0; static void testLayoutNormalArea() { // A comfortable inline strip: keymap band on top, thin level band pinned to the bottom. const EmbedLayout L = layoutEmbed(300, 40); - CHECK(L.keymap.left == 0 && L.keymap.top == 0 && L.keymap.right == 300); - CHECK(L.levelBand.left == 0 && L.levelBand.right == 300); + CHECK(L.keymap.x == 0 && L.keymap.y == 0 && L.keymap.right() == 300); + CHECK(L.levelBand.x == 0 && L.levelBand.right() == 300); // Level band is the fixed height at the very bottom; keymap fills the rest, contiguous. - CHECK(L.levelBand.height() == kEmbedLevelBandHeight); - CHECK(L.levelBand.bottom == 40); - CHECK(L.keymap.bottom == L.levelBand.top); - CHECK(L.keymap.height() == 40 - kEmbedLevelBandHeight); + CHECK(L.levelBand.height == kEmbedLevelBandHeight); + CHECK(L.levelBand.bottom() == 40); + CHECK(L.keymap.bottom() == L.levelBand.y); + CHECK(L.keymap.height == 40 - kEmbedLevelBandHeight); } static void testLayoutTinyAreaKeepsKeymap() { // A very short area: the level band must yield so the keymap keeps its minimum, and no // rect inverts. const EmbedLayout L = layoutEmbed(300, 8); - CHECK(L.keymap.height() >= 0); - CHECK(L.levelBand.height() >= 0); - CHECK(L.keymap.bottom == L.levelBand.top); - CHECK(L.levelBand.bottom == 8); + CHECK(L.keymap.height >= 0); + CHECK(L.levelBand.height >= 0); + CHECK(L.keymap.bottom() == L.levelBand.y); + CHECK(L.levelBand.bottom() == 8); // The keymap is not starved below its floor when the area allows it. - CHECK(L.keymap.height() >= kEmbedKeymapMinHeight || 8 < kEmbedKeymapMinHeight); + CHECK(L.keymap.height >= kEmbedKeymapMinHeight || 8 < kEmbedKeymapMinHeight); } static void testLayoutZeroArea() { const EmbedLayout L = layoutEmbed(0, 0); - CHECK(L.keymap.width() <= 0 && L.keymap.height() <= 0); - CHECK(L.levelBand.width() <= 0 && L.levelBand.height() <= 0); + CHECK(L.keymap.width <= 0 && L.keymap.height <= 0); + CHECK(L.levelBand.width <= 0 && L.levelBand.height <= 0); // Negative dimensions clamp to a zero-area, non-inverted rect. const EmbedLayout N = layoutEmbed(-50, -50); - CHECK(N.keymap.right >= N.keymap.left && N.keymap.bottom >= N.keymap.top); + CHECK(N.keymap.right() >= N.keymap.x && N.keymap.bottom() >= N.keymap.y); } // --- zoneSegmentRect ---------------------------------------------------------- @@ -60,9 +61,9 @@ static void testZoneSegmentFullSpan() { // A zone covering the whole keyboard spans the entire keymap band width. const EmbedLayout L = layoutEmbed(256, 40); const Rect r = zoneSegmentRect(L, 0, 127); - CHECK(r.left == L.keymap.left); - CHECK(r.right == L.keymap.right); - CHECK(r.top == L.keymap.top && r.bottom == L.keymap.bottom); + CHECK(r.x == L.keymap.x); + CHECK(r.right() == L.keymap.right()); + CHECK(r.y == L.keymap.y && r.bottom() == L.keymap.bottom()); } static void testAdjacentZonesTileSeamlessly() { @@ -71,10 +72,10 @@ static void testAdjacentZonesTileSeamlessly() { const EmbedLayout L = layoutEmbed(256, 40); const Rect lo = zoneSegmentRect(L, 0, 59); const Rect hi = zoneSegmentRect(L, 60, 127); - CHECK(lo.left == L.keymap.left); - CHECK(hi.right == L.keymap.right); - CHECK(lo.right == hi.left); // seamless tile — the load-bearing assertion - CHECK(lo.right == L.keymap.left + 60 * 2); // 60 keys * 2px + CHECK(lo.x == L.keymap.x); + CHECK(hi.right() == L.keymap.right()); + CHECK(lo.right() == hi.x); // seamless tile — the load-bearing assertion + CHECK(lo.right() == L.keymap.x + 60 * 2); // 60 keys * 2px } static void testZoneSegmentClampsBadNotes() { @@ -82,9 +83,9 @@ static void testZoneSegmentClampsBadNotes() { // Out-of-range notes clamp into the band; an inverted zone (low > high) collapses to a // zero-or-positive-width rect, never inverts. const Rect over = zoneSegmentRect(L, -10, 200); - CHECK(over.left == L.keymap.left && over.right == L.keymap.right); + CHECK(over.x == L.keymap.x && over.right() == L.keymap.right()); const Rect inv = zoneSegmentRect(L, 100, 20); - CHECK(inv.right >= inv.left); + CHECK(inv.right() >= inv.x); } // --- zoneAtPoint -------------------------------------------------------------- @@ -95,31 +96,31 @@ static void testZoneAtPointHits() { // A point inside the low zone's segment resolves to zone 0; inside the high zone, 1. const Rect lo = zoneSegmentRect(L, 0, 59); const Rect hi = zoneSegmentRect(L, 60, 127); - const int yMid = (L.keymap.top + L.keymap.bottom) / 2; - CHECK(zoneAtPoint(L, zones, 2, lo.left + 1, yMid) == 0); - CHECK(zoneAtPoint(L, zones, 2, hi.right - 1, yMid) == 1); + const int yMid = (L.keymap.y + L.keymap.bottom()) / 2; + CHECK(zoneAtPoint(L, zones, 2, lo.x + 1, yMid) == 0); + CHECK(zoneAtPoint(L, zones, 2, hi.right() - 1, yMid) == 1); } static void testZoneAtPointFirstMatchOnOverlap() { const EmbedLayout L = layoutEmbed(256, 40); // Two overlapping zones; the FIRST in order must win the contested keys. const EmbedZone zones[2] = {{0, 127}, {40, 80}}; - const int yMid = (L.keymap.top + L.keymap.bottom) / 2; + const int yMid = (L.keymap.y + L.keymap.bottom()) / 2; const Rect contested = zoneSegmentRect(L, 40, 80); - CHECK(zoneAtPoint(L, zones, 2, contested.left + 1, yMid) == 0); // zone 0 wins + CHECK(zoneAtPoint(L, zones, 2, contested.x + 1, yMid) == 0); // zone 0 wins } static void testZoneAtPointMisses() { const EmbedLayout L = layoutEmbed(256, 40); const EmbedZone zones[1] = {{60, 72}}; // a narrow zone; most keys uncovered - const int yMid = (L.keymap.top + L.keymap.bottom) / 2; + const int yMid = (L.keymap.y + L.keymap.bottom()) / 2; // A key left of the zone is uncovered -> -1. - CHECK(zoneAtPoint(L, zones, 1, L.keymap.left + 1, yMid) == -1); + CHECK(zoneAtPoint(L, zones, 1, L.keymap.x + 1, yMid) == -1); // A point in the level band (below the keymap) is off the keymap -> -1. - CHECK(zoneAtPoint(L, zones, 1, L.levelBand.left + 4, L.levelBand.top) == -1); + CHECK(zoneAtPoint(L, zones, 1, L.levelBand.x + 4, L.levelBand.y) == -1); // Empty / null list -> -1. - CHECK(zoneAtPoint(L, zones, 0, L.keymap.left + 1, yMid) == -1); - CHECK(zoneAtPoint(L, nullptr, 3, L.keymap.left + 1, yMid) == -1); + CHECK(zoneAtPoint(L, zones, 0, L.keymap.x + 1, yMid) == -1); + CHECK(zoneAtPoint(L, nullptr, 3, L.keymap.x + 1, yMid) == -1); } // --- levelFillRect ------------------------------------------------------------ @@ -127,16 +128,16 @@ static void testZoneAtPointMisses() { static void testLevelFillClamps() { const EmbedLayout L = layoutEmbed(200, 40); // Zero / negative -> empty. - CHECK(levelFillRect(L, 0.0).width() <= 0); - CHECK(levelFillRect(L, -1.0).width() <= 0); + CHECK(levelFillRect(L, 0.0).width <= 0); + CHECK(levelFillRect(L, -1.0).width <= 0); // Full / over-full -> the whole band width. - CHECK(levelFillRect(L, 1.0).width() == L.levelBand.width()); - CHECK(levelFillRect(L, 5.0).width() == L.levelBand.width()); + CHECK(levelFillRect(L, 1.0).width == L.levelBand.width); + CHECK(levelFillRect(L, 5.0).width == L.levelBand.width); // Half -> ~half the band, pinned to the band's left and vertical extent. const Rect half = levelFillRect(L, 0.5); - CHECK(half.left == L.levelBand.left); - CHECK(half.top == L.levelBand.top && half.bottom == L.levelBand.bottom); - CHECK(half.width() == L.levelBand.width() / 2); + CHECK(half.x == L.levelBand.x); + CHECK(half.y == L.levelBand.y && half.bottom() == L.levelBand.bottom()); + CHECK(half.width == L.levelBand.width / 2); } int main() { diff --git a/tests/test_envelope_edit.cpp b/tests/test_envelope_edit.cpp index 6b40cac..37f92a3 100644 --- a/tests/test_envelope_edit.cpp +++ b/tests/test_envelope_edit.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::envelope_edit — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::envelope_edit — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. Assert the S-VIEW-3 draggable-node INVERSE // map: node hit-test + pixel-delta -> clamped/monotonic param set, HARD at the clamp + monotonic // boundaries (the load-bearing "a drag can never produce a param a slider couldn't" invariant). @@ -14,14 +14,15 @@ // pixel delta; zero-fade-out node grabbable at the right edge and draggable inward — FA2); // degenerate area/duration + non-draggable node + cross-mode node -> no motion. -#include "../src/vst/envelope_edit.h" +#include "../src/core/instrument/ui/envelope_edit.h" #include #include #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -41,7 +42,7 @@ static bool findNode(const std::vector& poly, EnvNode node, EnvVertex // scale (FA2 param-domain schematic — sample-length-free): (850-1-32)px over the 8.0s schematic // domain => 102.125 px/s, each segment prefixed by the 8px separation base; the gateEnv() nodes // draw at A x@28, H x@47, D x@85, RS x@235, RE x@284. -static Rect wideArea() { return Rect{20, 10, 1020, 110}; } +static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 110); } static constexpr double kTotal = 2.0; static const double kGateSecPerPx = 1.0 / gatePxPerSecond(wideArea()); @@ -71,10 +72,10 @@ static void testHitGrabsDrawnHandle() { const AmpEnvelope e = gateEnv(); const Rect a = wideArea(); // AttackEnd draws at x = left+28 (8px base + 0.2s * 102.125 px/s), y = top (level 1). - NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 28, a.top); + NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 28, a.y); CHECK(h.hit && h.node == EnvNode::AttackEnd); // The sustain node (DecayEnd) at left+85, level 0.5 -> ~top+50. - NodeHit s = nodeAtPoint(e, a, kTotal, a.left + 85, a.top + 50); + NodeHit s = nodeAtPoint(e, a, kTotal, a.x + 85, a.y + 50); CHECK(s.hit && s.node == EnvNode::DecayEnd); } @@ -82,7 +83,7 @@ static void testHitMissesOffEveryNode() { const AmpEnvelope e = gateEnv(); const Rect a = wideArea(); // A point far from any drawn handle (right of the release ramp, well away from a node). - NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 700, a.top + 5); + NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 700, a.y + 5); CHECK(!h.hit); } @@ -90,11 +91,11 @@ static void testHitSkipsNonDraggableAnchors() { const AmpEnvelope e = gateEnv(); const Rect a = wideArea(); // Origin draws at (left, bottom-1). Even a pixel-perfect grab there is NOT a draggable node. - NodeHit o = nodeAtPoint(e, a, kTotal, a.left, a.bottom - 1); + NodeHit o = nodeAtPoint(e, a, kTotal, a.x, a.bottom() - 1); CHECK(!o.hit); // ReleaseStart draws at (left+235, sustain level ~top+50) — the fixed plateau end. It is // drawing-only -> not grabbable; no other node is within the radius, so this grab misses. - NodeHit rs = nodeAtPoint(e, a, kTotal, a.left + 235, a.top + 50); + NodeHit rs = nodeAtPoint(e, a, kTotal, a.x + 235, a.y + 50); CHECK(!rs.hit); } @@ -106,7 +107,7 @@ static void testHitNearestNodeWinsOverDrawOrder() { AmpEnvelope e = gateEnv(); e.holdSeconds = 0.01; const Rect a = wideArea(); - NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 33, a.top); + NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 33, a.y); CHECK(h.hit && h.node == EnvNode::HoldEnd); } @@ -199,11 +200,11 @@ static void testGateTimeOnlyNodeIgnoresY() { static void testGateReleaseEndGrabAndDrag() { // The FA2 fix: ReleaseEnd is a drawn, IN-BOUNDS, grabbable handle (pre-FA2 it mapped past - // area.right and could never be grabbed). gateEnv() draws it at x@284, level 0 (bottom row). + // area.right() and could never be grabbed). gateEnv() draws it at x@284, level 0 (bottom row). const AmpEnvelope e = gateEnv(); const Rect a = wideArea(); EnvClampBounds b; - NodeHit h = nodeAtPoint(e, a, kTotal, a.left + 284, a.bottom - 1); + NodeHit h = nodeAtPoint(e, a, kTotal, a.x + 284, a.bottom() - 1); CHECK(h.hit && h.node == EnvNode::ReleaseEnd); // Dragging it RIGHT lengthens the release at the gate timed scale; only release changes. AmpEnvelope out = resolveNodeDrag(e, EnvNode::ReleaseEnd, a, kTotal, b, 85, 0); @@ -289,14 +290,14 @@ static void testTriggerZeroFadeOutGrabbableAtRightEdge() { e.fadeOutFraction = 0.0; const Rect a = wideArea(); EnvClampBounds b; - NodeHit h = nodeAtPoint(e, a, kTotal, a.right - 1, a.top); + NodeHit h = nodeAtPoint(e, a, kTotal, a.right() - 1, a.y); CHECK(h.hit && h.node == EnvNode::FadeOutStart); // -100px = -0.2s on the 2.0s played span, applied OPPOSITE -> fadeOut 0.0 -> 0.1. AmpEnvelope out = resolveNodeDrag(e, EnvNode::FadeOutStart, a, kTotal, b, -100, 0); CHECK(near(out.fadeOutFraction, 0.1)); CHECK(near(out.lengthFraction, e.lengthFraction)); // length untouched // LengthEnd sits at the same x but level 0 (bottom row) — grabbable at ITS drawn point. - NodeHit le = nodeAtPoint(e, a, kTotal, a.right - 1, a.bottom - 1); + NodeHit le = nodeAtPoint(e, a, kTotal, a.right() - 1, a.bottom() - 1); CHECK(le.hit && le.node == EnvNode::LengthEnd); } @@ -327,7 +328,7 @@ static void testNonDraggableNodeNoMotion() { static void testDegenerateAreaNoMotion() { const AmpEnvelope e = gateEnv(); EnvClampBounds b; - const Rect zeroW = Rect{0, 0, 0, 100}; + const Rect zeroW = Rect::ltrb(0, 0, 0, 100); AmpEnvelope o1 = resolveNodeDrag(e, EnvNode::AttackEnd, zeroW, kTotal, b, 500, 0); CHECK(near(o1.attackSeconds, e.attackSeconds)); AmpEnvelope o2 = resolveNodeDrag(e, EnvNode::AttackEnd, wideArea(), 0.0, b, 500, 0); // no time @@ -347,7 +348,7 @@ static void testCrossModeNodeNoMotion() { out = resolveNodeDrag(g, EnvNode::FadeInEnd, wideArea(), kTotal, b, 50, 0); CHECK(near(out.fadeInFraction, g.fadeInFraction)); // And the zero-height baseline's ReleaseEnd is not even reported grabbable in Trigger mode. - const Rect flat = Rect{0, 0, 100, 0}; + const Rect flat = Rect::ltrb(0, 0, 100, 0); const NodeHit h = nodeAtPoint(t, flat, kTotal, 99, 0); CHECK(!h.hit); } diff --git a/tests/test_envelope_overlay.cpp b/tests/test_envelope_overlay.cpp index bd09245..6839bec 100644 --- a/tests/test_envelope_overlay.cpp +++ b/tests/test_envelope_overlay.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::envelope_overlay — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::envelope_overlay — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. Assert the S-VIEW-3/FA2 amp-envelope -> // polyline FORWARD map: the Gate BOUNDED-SCHEMATIC AHDSR shape (attack ramp / hold plateau / // decay-to-sustain / fixed-width sustain plateau / in-bounds release) and the Trigger @@ -14,12 +14,13 @@ // the played span, overlap clamp, full-length/zero-fade-out nodes in-bounds at right-1); // degenerate flat baseline. -#include "../src/vst/envelope_overlay.h" +#include "../src/core/instrument/ui/envelope_overlay.h" #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -27,7 +28,7 @@ static int g_fail = 0; // A comfortable overlay area: 1000px wide, 100px tall, offset so left/top != 0 (catches origin // bugs). Under levelToY the level span is height-1 = 99 rows. -static Rect wideArea() { return Rect{20, 10, 1020, 110}; } // width 1000, height 100 +static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 110); } // width 1000, height 100 // Find the first vertex with a given node in a polyline; asserts presence via the returned bool. static bool findNode(const std::vector& poly, EnvNode node, EnvVertex& out) { @@ -41,32 +42,32 @@ static bool findNode(const std::vector& poly, EnvNode node, EnvVertex static void testTimeToXEndpoints() { const Rect a = wideArea(); - CHECK(timeToX(a, 2.0, 0.0) == a.left); // t=0 -> left - CHECK(timeToX(a, 2.0, 2.0) == a.right - 1); // t=total -> last in-bounds column - CHECK(timeToX(a, 2.0, 1.0) == a.left + 500); // midpoint + CHECK(timeToX(a, 2.0, 0.0) == a.x); // t=0 -> left + CHECK(timeToX(a, 2.0, 2.0) == a.right() - 1); // t=total -> last in-bounds column + CHECK(timeToX(a, 2.0, 1.0) == a.x + 500); // midpoint } static void testTimeToXNegativePinsLeft() { const Rect a = wideArea(); - CHECK(timeToX(a, 2.0, -0.5) == a.left); // t<0 pins left + CHECK(timeToX(a, 2.0, -0.5) == a.x); // t<0 pins left } static void testTimeToXPastEndClamps() { // FA2 bounds invariant: t past total pins to the last in-bounds column, never past right. const Rect a = wideArea(); - CHECK(timeToX(a, 2.0, 3.0) == a.right - 1); - CHECK(timeToX(a, 2.0, 1000.0) == a.right - 1); + CHECK(timeToX(a, 2.0, 3.0) == a.right() - 1); + CHECK(timeToX(a, 2.0, 1000.0) == a.right() - 1); // A HUGE t must clamp in double space, not overflow the integer cast (32-bit long on // Windows would wrap to LONG_MIN and pin to the WRONG edge). - CHECK(timeToX(a, 2.0, 1e15) == a.right - 1); + CHECK(timeToX(a, 2.0, 1e15) == a.right() - 1); } static void testGateTimedWidth() { // 15% of the 1000px canvas is reserved for the sustain plateau -> 850px timed region. CHECK(gateTimedWidth(wideArea()) == 850); // Zero-width area -> 0; a tiny area still yields >= 1 so the px<->s scale never degenerates. - CHECK(gateTimedWidth(Rect{5, 5, 5, 45}) == 0); - CHECK(gateTimedWidth(Rect{0, 0, 1, 10}) == 1); + CHECK(gateTimedWidth(Rect::ltrb(5, 5, 5, 45)) == 0); + CHECK(gateTimedWidth(Rect::ltrb(0, 0, 1, 10)) == 1); } static void testGatePxPerSecond() { @@ -74,30 +75,30 @@ static void testGatePxPerSecond() { // 1000px canvas: (850 - 1 - 32) / 8.0s = 817/8 px/s. Independent of any sample duration. const double expected = 817.0 / (4.0 * kGateStageMaxSeconds); CHECK(gatePxPerSecond(wideArea()) == expected); - CHECK(gatePxPerSecond(Rect{5, 5, 5, 45}) == 0.0); // zero-width area -> 0 - CHECK(gatePxPerSecond(Rect{0, 0, 10, 10}) > 0.0); // tiny area: usable floors at 1px, > 0 + CHECK(gatePxPerSecond(Rect::ltrb(5, 5, 5, 45)) == 0.0); // zero-width area -> 0 + CHECK(gatePxPerSecond(Rect::ltrb(0, 0, 10, 10)) > 0.0); // tiny area: usable floors at 1px, > 0 } static void testTimeToXDegenerate() { const Rect a = wideArea(); - CHECK(timeToX(a, 0.0, 1.0) == a.left); // no duration -> left - const Rect z = Rect{5, 5, 5, 45}; // zero width - CHECK(timeToX(z, 2.0, 1.0) == z.left); + CHECK(timeToX(a, 0.0, 1.0) == a.x); // no duration -> left + const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width + CHECK(timeToX(z, 2.0, 1.0) == z.x); } static void testLevelToYEndpoints() { const Rect a = wideArea(); - CHECK(levelToY(a, 1.0) == a.top); // level 1 -> top row - CHECK(levelToY(a, 0.0) == a.bottom - 1); // level 0 -> bottom row - CHECK(levelToY(a, 0.5) == a.top + 50); // mid: round((1-0.5)*99)=round(49.5)=50 + CHECK(levelToY(a, 1.0) == a.y); // level 1 -> top row + CHECK(levelToY(a, 0.0) == a.bottom() - 1); // level 0 -> bottom row + CHECK(levelToY(a, 0.5) == a.y + 50); // mid: round((1-0.5)*99)=round(49.5)=50 } static void testLevelToYClamps() { const Rect a = wideArea(); - CHECK(levelToY(a, 2.0) == a.top); // >1 clamps to top - CHECK(levelToY(a, -1.0) == a.bottom - 1); // <0 clamps to bottom - const Rect z = Rect{5, 5, 45, 5}; // zero height - CHECK(levelToY(z, 0.5) == z.top); + CHECK(levelToY(a, 2.0) == a.y); // >1 clamps to top + CHECK(levelToY(a, -1.0) == a.bottom() - 1); // <0 clamps to bottom + const Rect z = Rect::ltrb(5, 5, 45, 5); // zero height + CHECK(levelToY(z, 0.5) == z.y); } // --- Gate polyline ------------------------------------------------------------ @@ -149,11 +150,11 @@ static void testGateSchematicPlacement() { const std::vector poly = buildEnvelopePolyline(env, a, 2.0); EnvVertex v; - CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.left + 28); - CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.left + 47); - CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.left + 85); - CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.left + 235); - CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.left + 284); + CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.x + 28); + CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.x + 47); + CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.x + 85); + CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.x + 235); + CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.x + 284); } static void testGateLayoutIndependentOfSampleDuration() { @@ -195,7 +196,7 @@ static void testGateSustainPlateauFixedWidth() { env.sustainLevel = 0.6; env.releaseSeconds = 0.3; const Rect a = wideArea(); - const int plateauPx = a.width() - gateTimedWidth(a); // 150 + const int plateauPx = a.width - gateTimedWidth(a); // 150 const std::vector poly = buildEnvelopePolyline(env, a, 2.0); EnvVertex decay, plateauEnd; @@ -207,7 +208,7 @@ static void testGateSustainPlateauFixedWidth() { static void testGateReleaseVisibleInBounds() { // The FA2 fix: Release is a VISIBLE, in-bounds segment — ReleaseEnd sits strictly right of - // the plateau end and strictly inside the canvas (pre-FA2 it mapped past area.right and the + // the plateau end and strictly inside the canvas (pre-FA2 it mapped past area.right() and the // shell clipped its handle away). AmpEnvelope env; env.mode = EnvMode::Gate; @@ -223,7 +224,7 @@ static void testGateReleaseVisibleInBounds() { CHECK(findNode(poly, EnvNode::ReleaseStart, plateauEnd)); CHECK(findNode(poly, EnvNode::ReleaseEnd, rel)); CHECK(rel.x > plateauEnd.x); // a visible ramp, not a collapsed point - CHECK(rel.x < a.right); // strictly in-bounds + CHECK(rel.x < a.right()); // strictly in-bounds CHECK(rel.level == 0.0); } @@ -231,7 +232,7 @@ static void testGateOverrunCompressesFromRight() { // Stages BEYOND the schematic domain (4.0s each > kGateStageMaxSeconds): the layout // compresses from the right preserving the minimum gaps — ReleaseEnd pins to the last // in-bounds column, but the trailing nodes stay strictly increasing and individually - // separated (>= kGateNodeSepPx), NOT piled on one pixel. NOTHING maps past area.right. + // separated (>= kGateNodeSepPx), NOT piled on one pixel. NOTHING maps past area.right(). AmpEnvelope env; env.mode = EnvMode::Gate; env.attackSeconds = 4.0; @@ -246,12 +247,12 @@ static void testGateOverrunCompressesFromRight() { EnvVertex plateauEnd, rel; CHECK(findNode(poly, EnvNode::ReleaseStart, plateauEnd)); CHECK(findNode(poly, EnvNode::ReleaseEnd, rel)); - CHECK(rel.x == a.right - 1); // pinned to the last in-bounds column + CHECK(rel.x == a.right() - 1); // pinned to the last in-bounds column CHECK(plateauEnd.level == 0.7); // still at sustain for (size_t i = 1; i < poly.size(); ++i) { CHECK(poly[i].x > poly[i - 1].x); // strictly monotonic CHECK(poly[i].x - poly[i - 1].x >= kGateNodeSepPx - 1); // min gaps survive compression - CHECK(poly[i].x >= a.left && poly[i].x < a.right); // in-bounds + CHECK(poly[i].x >= a.x && poly[i].x < a.right()); // in-bounds } } @@ -279,8 +280,8 @@ static void testGateAllVerticesInBounds() { for (const AmpEnvelope& env : {base, big, zero, trig, huge}) { for (const EnvVertex& v : buildEnvelopePolyline(env, a, 2.0)) { - CHECK(v.x >= a.left && v.x < a.right); - CHECK(v.y >= a.top && v.y < a.bottom); + CHECK(v.x >= a.x && v.x < a.right()); + CHECK(v.y >= a.y && v.y < a.bottom()); } } } @@ -305,9 +306,9 @@ static void testTriggerShape() { CHECK(poly[3].node == EnvNode::LengthEnd); EnvVertex v; - CHECK(findNode(poly, EnvNode::FadeInEnd, v) && v.x == a.left + 100 && v.level == 1.0); - CHECK(findNode(poly, EnvNode::FadeOutStart, v) && v.x == a.left + 350 && v.level == 1.0); - CHECK(findNode(poly, EnvNode::LengthEnd, v) && v.x == a.left + 500 && v.level == 0.0); + CHECK(findNode(poly, EnvNode::FadeInEnd, v) && v.x == a.x + 100 && v.level == 1.0); + CHECK(findNode(poly, EnvNode::FadeOutStart, v) && v.x == a.x + 350 && v.level == 1.0); + CHECK(findNode(poly, EnvNode::LengthEnd, v) && v.x == a.x + 500 && v.level == 0.0); } static void testTriggerFadeOverlapClamp() { @@ -324,7 +325,7 @@ static void testTriggerFadeOverlapClamp() { CHECK(findNode(poly, EnvNode::FadeInEnd, fin)); CHECK(findNode(poly, EnvNode::FadeOutStart, fout)); CHECK(fin.x == fout.x); // fades meet exactly, never cross - CHECK(fin.x == a.left + 800); + CHECK(fin.x == a.x + 800); } static void testTriggerFullLengthZeroFadeOutInBounds() { @@ -342,8 +343,8 @@ static void testTriggerFullLengthZeroFadeOutInBounds() { EnvVertex fout, lend; CHECK(findNode(poly, EnvNode::FadeOutStart, fout)); CHECK(findNode(poly, EnvNode::LengthEnd, lend)); - CHECK(fout.x == a.right - 1); // present + in-bounds at zero fade-out - CHECK(lend.x == a.right - 1); + CHECK(fout.x == a.right() - 1); // present + in-bounds at zero fade-out + CHECK(lend.x == a.right() - 1); CHECK(fout.level == 1.0 && lend.level == 0.0); } @@ -351,7 +352,7 @@ static void testTriggerFullLengthZeroFadeOutInBounds() { static void testDegenerateFlatBaseline() { AmpEnvelope env; // any params - const Rect zeroW = Rect{0, 0, 0, 100}; + const Rect zeroW = Rect::ltrb(0, 0, 0, 100); const std::vector p1 = buildEnvelopePolyline(env, zeroW, 2.0); CHECK(p1.size() == 2); // always a drawable line CHECK(p1.front().level == 0.0 && p1.back().level == 0.0); @@ -360,7 +361,7 @@ static void testDegenerateFlatBaseline() { const std::vector p2 = buildEnvelopePolyline(env, ok, 0.0); // no duration CHECK(p2.size() == 2); CHECK(p2.front().level == 0.0 && p2.back().level == 0.0); - CHECK(p2.front().x == ok.left && p2.back().x == ok.right - 1); // spans the area, in-bounds + CHECK(p2.front().x == ok.x && p2.back().x == ok.right() - 1); // spans the area, in-bounds } int main() { diff --git a/tests/test_file_bytes.cpp b/tests/test_file_bytes.cpp new file mode 100644 index 0000000..ba6789a --- /dev/null +++ b/tests/test_file_bytes.cpp @@ -0,0 +1,52 @@ +// Standalone tests for reasampler::readFileBytes — no REAPER, no framework. +// The ONE whole-file loader (Q-W1, T2-03) shared by both artifacts. Exercises +// the three-way contract: exact bytes back, empty on a missing file, empty on +// an empty file. Uses a scratch file in the test's working directory. + +#include "../src/core/util/file_bytes.h" + +#include +#include +#include +#include + +using namespace reasampler; +using namespace reasampler::util; + +static int g_fail = 0; +#define CHECK(cond) do { if(!(cond)) { \ + std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) + +static const char* kScratch = "file_bytes_scratch.bin"; + +static void testReadsExactBytesBack() { + // Binary content incl. NUL and 0xFF — the loader must be byte-transparent. + const std::vector payload = {0x00, 0x01, 0xFF, 0x7E, 0x00, 0x0A}; + { + std::ofstream f(kScratch, std::ios::binary | std::ios::trunc); + f.write(reinterpret_cast(payload.data()), + static_cast(payload.size())); + } + CHECK(readFileBytes(kScratch) == payload); + std::remove(kScratch); +} + +static void testMissingFileIsEmpty() { + CHECK(readFileBytes("no_such_file_anywhere.bin").empty()); +} + +static void testEmptyFileIsEmpty() { + { std::ofstream f(kScratch, std::ios::binary | std::ios::trunc); } + CHECK(readFileBytes(kScratch).empty()); + std::remove(kScratch); +} + +int main() { + testReadsExactBytesBack(); + testMissingFileIsEmpty(); + testEmptyFileIsEmpty(); + + if (g_fail == 0) std::printf("file_bytes: all tests passed\n"); + else std::printf("file_bytes: %d CHECK(s) FAILED\n", g_fail); + return g_fail == 0 ? 0 : 1; +} diff --git a/tests/test_footer_bar.cpp b/tests/test_footer_bar.cpp index ba8cb4d..73a2462 100644 --- a/tests/test_footer_bar.cpp +++ b/tests/test_footer_bar.cpp @@ -13,11 +13,12 @@ // * Hit-test: Toggle / Tail returned for in-bounds points, None outside AND on the count // label (a passive readout, never a control); half-open bounds; suppressed box claims none. -#include "../src/footer_bar.h" +#include "../src/core/ui/footer_bar.h" #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_guid_diff.cpp b/tests/test_guid_diff.cpp index 08febad..b888ab2 100644 --- a/tests/test_guid_diff.cpp +++ b/tests/test_guid_diff.cpp @@ -10,7 +10,7 @@ // 5. reset() (project switch) re-arms the first-poll guard: the next observe() // re-baselines and reports nothing new — never diffs across projects. -#include "../src/guid_diff.h" +#include "../src/core/view/guid_diff.h" #include #include @@ -18,6 +18,7 @@ #include using namespace reasampler; +using namespace reasampler::view; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_insert_plan.cpp b/tests/test_insert_plan.cpp index 1339ddb..c118288 100644 --- a/tests/test_insert_plan.cpp +++ b/tests/test_insert_plan.cpp @@ -5,12 +5,13 @@ // proof that the forbidden stretch bit is never set and that native-length insert // carries no tempo bits. -#include "../src/insert_plan.h" +#include "../src/core/capture/insert_plan.h" #include #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_instrument_drop.cpp b/tests/test_instrument_drop.cpp index 08ffaa6..43467c0 100644 --- a/tests/test_instrument_drop.cpp +++ b/tests/test_instrument_drop.cpp @@ -6,8 +6,8 @@ // container -> the instrument's OWN reader -> assert the capture selected) IS the // cross-artifact contract guard — the same pattern assignment_request_tests uses. -#include "../src/instrument_drop.h" -#include "../src/vst/sample_map.h" // deserializeComponentState — the instrument's OWN reader +#include "../src/core/wire/instrument_drop.h" +#include "../src/core/instrument/map/sample_map.h" // deserializeComponentState — the instrument's OWN reader #include "version_generated.h" // REASAMPLER_CHANNEL_IS_BETA — pins the per-channel class ID @@ -17,6 +17,7 @@ #include using namespace reasampler; +using namespace reasampler::wire; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_json.cpp b/tests/test_json.cpp new file mode 100644 index 0000000..0c71cbe --- /dev/null +++ b/tests/test_json.cpp @@ -0,0 +1,297 @@ +// Standalone tests for reasampler::json — no REAPER, no framework. The ONE +// lexical JSON layer (Q-W1) behind bank_model / bank_book / view_mode_model / +// owned_manifest / tail_control. The consumers' own suites prove the domain +// grammars; this suite pins the LEXICAL contract — the escape set, the number +// renderings (byte-exact), the parse tolerances, and the reject paths — so a +// change here is caught before it silently shifts five persisted-blob formats. +// +// NOTE: json::Reader BORROWS its input string, so every test binds a named +// std::string first — never a temporary. + +#include "../src/core/json/json.h" + +#include +#include +#include +#include + +using namespace reasampler; +using namespace reasampler::json; + +static int g_fail = 0; +#define CHECK(cond) do { if(!(cond)) { \ + std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) + +// Convenience: parse helpers over a named buffer per call site. +static bool intFrom(const std::string& s, int& v) { json::Reader r(s); return r.parseInt(v); } +static bool int64From(const std::string& s, std::int64_t& v) { json::Reader r(s); return r.parseInt64(v); } +static bool doubleFrom(const std::string& s, double& v) { json::Reader r(s); return r.parseDouble(v); } +static bool boolFrom(const std::string& s, bool& v) { json::Reader r(s); return r.parseBool(v); } +static bool stringFrom(const std::string& s, std::string& v) { json::Reader r(s); return r.parseString(v); } + +// --- emit: writeEscaped ------------------------------------------------------- + +static void testEscapeExactBytes() { + // The seven short escapes + \u00XX for remaining control chars, verbatim + // pass-through otherwise. Byte-exact: this is the persisted-blob format. + std::string out; + json::writeEscaped(out, "a\"b\\c\n\t\x01z"); + CHECK(out == "\"a\\\"b\\\\c\\n\\t\\u0001z\""); +} + +static void testEscapeUtf8PassesThrough() { + // Multi-byte UTF-8 passes through verbatim; only C0 controls are \u-escaped. + std::string out; + json::writeEscaped(out, "gr\xC3\xBC n"); // "grü n" + CHECK(out == "\"gr\xC3\xBC n\""); +} + +// --- emit: numToStr ----------------------------------------------------------- + +static void testNumToStrIntForms() { + CHECK(json::numToStr(0) == "0"); + CHECK(json::numToStr(-7) == "-7"); + CHECK(json::numToStr(INT_MAX) == "2147483647"); + CHECK(json::numToStr(static_cast(1) << 40) == "1099511627776"); + CHECK(json::numToStr(2000.0) == "2000"); // %.17g drops the trailing .0 + CHECK(json::numToStr(0.5) == "0.5"); +} + +static void testDoubleRoundTripsBitForBit() { + // %.17g is the shortest form that round-trips every IEEE-754 double. + const double v = 3141.592653589793; + double back = 0.0; + CHECK(doubleFrom(json::numToStr(v), back)); + CHECK(back == v); +} + +// --- emit: Writer object grammar ---------------------------------------------- + +static void testWriterEmitsExactObjectBytes() { + std::string out; + { + json::Writer w(out); + w.keyRaw("a", json::numToStr(1)); + w.keyStr("b", "x\"y"); + w.keyBegin("c"); + { + json::Writer nested(out); + nested.keyRaw("d", json::numToStr(2.5)); + } + w.keyBegin("e"); + json::writeStringArray(out, {"p", "q"}); + w.keyBegin("f"); + json::writeIntArray(out, {1, 2}); + } + CHECK(out == "{\"a\":1,\"b\":\"x\\\"y\",\"c\":{\"d\":2.5}," + "\"e\":[\"p\",\"q\"],\"f\":[1,2]}"); +} + +static void testEmptyArraysEmitBrackets() { + std::string s, i; + json::writeStringArray(s, {}); + json::writeIntArray(i, {}); + CHECK(s == "[]"); + CHECK(i == "[]"); +} + +// --- Reader: strings ---------------------------------------------------------- + +static void testParseStringEscapes() { + std::string out; + CHECK(stringFrom(" \"a\\\"b\\\\c\\n\\u0041\"", out)); + CHECK(out == "a\"b\\c\nA"); +} + +static void testParseStringSurrogatePairToUtf8() { + // \uD83D\uDE00 (grinning face) -> F0 9F 98 80. + std::string out; + CHECK(stringFrom("\"\\ud83d\\ude00\"", out)); + CHECK(out == "\xF0\x9F\x98\x80"); +} + +static void testParseStringRejectsMalformed() { + std::string out; + CHECK(!stringFrom("\"unterminated", out)); + CHECK(!stringFrom("\"bad\\qescape\"", out)); + CHECK(!stringFrom("\"\\ud800 alone\"", out)); // unpaired high surrogate + CHECK(!stringFrom("\"\\udc00\"", out)); // unpaired low surrogate + CHECK(!stringFrom("noquote", out)); +} + +// --- Reader: numbers ---------------------------------------------------------- + +static void testParseIntAcceptsAndRejects() { + int v = 0; + CHECK(intFrom("42,", v)); CHECK(v == 42); + CHECK(intFrom("-7}", v)); CHECK(v == -7); + CHECK(intFrom("2147483647]", v)); CHECK(v == INT_MAX); + // Out of int range is REJECTED (the unified guard every consumer now shares). + CHECK(!intFrom("2147483648,", v)); + CHECK(!intFrom("1.5,", v)); + CHECK(!intFrom("x,", v)); + CHECK(!intFrom("", v)); +} + +static void testParseInt64RangeAndReject() { + std::int64_t v = 0; + CHECK(int64From("9223372036854775807,", v)); + CHECK(v == 9223372036854775807LL); + CHECK(!int64From("9223372036854775808,", v)); // ERANGE -> reject +} + +static void testParseDoubleRejectsRangeAndGarbage() { + double v = 0; + CHECK(!doubleFrom("1e999,", v)); // ERANGE + CHECK(!doubleFrom("1.5abc,", v)); // trailing bytes + CHECK(doubleFrom("2.5}", v)); CHECK(v == 2.5); +} + +static void testParseBool() { + bool v = false; + CHECK(boolFrom("true,", v)); CHECK(v); + CHECK(boolFrom("false]", v)); CHECK(!v); + CHECK(!boolFrom("TRUE,", v)); +} + +// --- Reader: structure -------------------------------------------------------- + +static void testExpectNullOr() { + { + const std::string s = "null,"; + json::Reader r(s); + bool wasNull = false; + CHECK(r.expectNullOr(wasNull)); CHECK(wasNull); CHECK(r.consume(',')); + } + { + const std::string s = "\"x\""; + json::Reader r(s); + bool wasNull = true; + std::string v; + CHECK(r.expectNullOr(wasNull)); CHECK(!wasNull); + CHECK(r.parseString(v)); CHECK(v == "x"); + } + { + const std::string s; + json::Reader r(s); + bool wasNull = false; + CHECK(!r.expectNullOr(wasNull)); + } +} + +static void testParseKeyConsumesColon() { + const std::string s = " \"k\" : 1"; + json::Reader r(s); + std::string k; + int v = 0; + CHECK(r.parseKey(k)); + CHECK(k == "k"); + CHECK(r.parseInt(v)); + CHECK(v == 1); +} + +static void testParseArraysAppend() { + { + const std::string s = "[\"a\",\"b\"]"; + json::Reader r(s); + std::vector v; + CHECK(r.parseStringArray(v)); + CHECK(v.size() == 2 && v[0] == "a" && v[1] == "b"); + } + { + const std::string s = "[]"; + json::Reader r(s); + std::vector v; + CHECK(r.parseStringArray(v)); CHECK(v.empty()); + } + { + const std::string s = "[1,2]"; // non-string element + json::Reader r(s); + std::vector v; + CHECK(!r.parseStringArray(v)); + } + { + const std::string s = "[1,2,3]"; + json::Reader r(s); + std::vector v; + CHECK(r.parseIntArray(v)); + CHECK(v.size() == 3 && v[2] == 3); + } + { + const std::string s = "[1,"; // truncated + json::Reader r(s); + std::vector v; + CHECK(!r.parseIntArray(v)); + } +} + +static void testSkipValueOverNestedShapes() { + // Skips a nested object whose strings contain structural chars, then the + // cursor sits exactly on the next separator. + const std::string s = "{\"deep\":[\"}\",{\"x\":\"]\"}]},7"; + json::Reader r(s); + CHECK(r.skipValue()); + CHECK(r.consume(',')); + int v = 0; + CHECK(r.parseInt(v)); + CHECK(v == 7); +} + +static void testCaptureValueVerbatim() { + const std::string s = " {\"a\":[1,\"{\"]} ,tail"; + json::Reader r(s); + std::string raw; + CHECK(r.captureValue(raw)); + CHECK(raw == "{\"a\":[1,\"{\"]}"); + CHECK(r.consume(',')); +} + +static void testWriterOutputParsesBack() { + // The emitted object is consumable by the Reader — the seam the five + // consumers rely on (writer and reader agree on one dialect). + std::string out; + { + json::Writer w(out); + w.keyStr("name", "tab\there"); + w.keyRaw("n", json::numToStr(-3)); + } + json::Reader r(out); + CHECK(r.consume('{')); + std::string k1, v1; + CHECK(r.parseKey(k1) && k1 == "name"); + CHECK(r.parseString(v1) && v1 == "tab\there"); + CHECK(r.consume(',')); + std::string k2; + int v2 = 0; + CHECK(r.parseKey(k2) && k2 == "n"); + CHECK(r.parseInt(v2) && v2 == -3); + CHECK(r.consume('}')); + r.skipWs(); + CHECK(r.eof()); +} + +int main() { + testEscapeExactBytes(); + testEscapeUtf8PassesThrough(); + testNumToStrIntForms(); + testDoubleRoundTripsBitForBit(); + testWriterEmitsExactObjectBytes(); + testEmptyArraysEmitBrackets(); + testParseStringEscapes(); + testParseStringSurrogatePairToUtf8(); + testParseStringRejectsMalformed(); + testParseIntAcceptsAndRejects(); + testParseInt64RangeAndReject(); + testParseDoubleRejectsRangeAndGarbage(); + testParseBool(); + testExpectNullOr(); + testParseKeyConsumesColon(); + testParseArraysAppend(); + testSkipValueOverNestedShapes(); + testCaptureValueVerbatim(); + testWriterOutputParsesBack(); + + if (g_fail == 0) std::printf("json: all tests passed\n"); + else std::printf("json: %d CHECK(s) FAILED\n", g_fail); + return g_fail == 0 ? 0 : 1; +} diff --git a/tests/test_keyboard_strip.cpp b/tests/test_keyboard_strip.cpp index 57596cf..61bbc4b 100644 --- a/tests/test_keyboard_strip.cpp +++ b/tests/test_keyboard_strip.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::keyboard_strip — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::keyboard_strip — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. Assert the capture-first editor's // keyboard-strip layout, root marker, key mapping, zone-bar hit regions, and the drag-delta // note resolver directly — the geometry that backs the single-capture root-set and the opt-in @@ -14,11 +14,12 @@ // no-ops; isNaturalKey across a full octave (C4..B4), at boundary notes 0 and 127, and with // out-of-range inputs that clamp to [0,127]. -#include "../src/vst/keyboard_strip.h" +#include "../src/core/instrument/ui/keyboard_strip.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -32,13 +33,13 @@ static StripLayout wideStrip() { return layoutStrip(1280, 40); } static void testLayoutNormalArea() { const StripLayout L = layoutStrip(640, 40); - CHECK(L.keys.left == 0 && L.keys.top == 0); - CHECK(L.keys.right == 640 && L.keys.bottom == 40); + CHECK(L.keys.x == 0 && L.keys.y == 0); + CHECK(L.keys.right() == 640 && L.keys.bottom() == 40); } static void testLayoutZeroArea() { const StripLayout L = layoutStrip(0, 0); - CHECK(L.keys.width() == 0 && L.keys.height() == 0); + CHECK(L.keys.width == 0 && L.keys.height == 0); } // --- keyLeftX / keyRect / rootMarkerRect -------------------------------------- @@ -46,8 +47,8 @@ static void testLayoutZeroArea() { static void testKeyLeftMonotonicAndBounds() { const StripLayout L = wideStrip(); // Key 0's left edge is the band left; the 128 boundary is the band right. - CHECK(keyLeftX(L, 0) == L.keys.left); - CHECK(keyLeftX(L, 128) == L.keys.right); + CHECK(keyLeftX(L, 0) == L.keys.x); + CHECK(keyLeftX(L, 128) == L.keys.right()); // Strictly non-decreasing across the span. int prev = keyLeftX(L, 0); for (int n = 1; n <= 128; ++n) { @@ -62,17 +63,17 @@ static void testKeyLeftMonotonicAndBounds() { static void testKeyRectHalfOpen() { const StripLayout L = wideStrip(); const Rect k = keyRect(L, 60); - CHECK(k.left == keyLeftX(L, 60)); - CHECK(k.right == keyLeftX(L, 61)); - CHECK(k.top == L.keys.top && k.bottom == L.keys.bottom); - CHECK(k.width() == 10); // 10px/key + CHECK(k.x == keyLeftX(L, 60)); + CHECK(k.right() == keyLeftX(L, 61)); + CHECK(k.y == L.keys.y && k.bottom() == L.keys.bottom()); + CHECK(k.width == 10); // 10px/key } static void testRootMarkerEqualsKeyRect() { const StripLayout L = wideStrip(); const Rect m = rootMarkerRect(L, 64); const Rect k = keyRect(L, 64); - CHECK(m.left == k.left && m.right == k.right && m.top == k.top && m.bottom == k.bottom); + CHECK(m.x == k.x && m.right() == k.right() && m.y == k.y && m.bottom() == k.bottom()); } // --- keyAtPoint --------------------------------------------------------------- @@ -81,17 +82,17 @@ static void testKeyAtPointInverts() { const StripLayout L = wideStrip(); // A point in the middle of key 60's cell resolves to 60. const Rect k = keyRect(L, 60); - CHECK(keyAtPoint(L, k.left + 5, k.top + 2) == 60); + CHECK(keyAtPoint(L, k.x + 5, k.y + 2) == 60); // The very left of the band is key 0; just inside the right edge is key 127. - CHECK(keyAtPoint(L, L.keys.left, 2) == 0); - CHECK(keyAtPoint(L, L.keys.right - 1, 2) == 127); + CHECK(keyAtPoint(L, L.keys.x, 2) == 0); + CHECK(keyAtPoint(L, L.keys.right() - 1, 2) == 127); } static void testKeyAtPointOffBand() { const StripLayout L = wideStrip(); CHECK(keyAtPoint(L, -5, 2) == -1); // left of band - CHECK(keyAtPoint(L, L.keys.right + 5, 2) == -1); // right of band - CHECK(keyAtPoint(L, 100, L.keys.bottom + 5) == -1); // below band + CHECK(keyAtPoint(L, L.keys.right() + 5, 2) == -1); // right of band + CHECK(keyAtPoint(L, 100, L.keys.bottom() + 5) == -1); // below band } // --- zoneBarRect -------------------------------------------------------------- @@ -99,17 +100,17 @@ static void testKeyAtPointOffBand() { static void testZoneBarSpansInclusive() { const StripLayout L = wideStrip(); const Rect bar = zoneBarRect(L, 12, 23); // C1..B1 inclusive - CHECK(bar.left == keyLeftX(L, 12)); - CHECK(bar.right == keyLeftX(L, 24)); // high+1 -> the bar covers key 23 fully - CHECK(bar.width() == 120); // 12 keys * 10px + CHECK(bar.x == keyLeftX(L, 12)); + CHECK(bar.right() == keyLeftX(L, 24)); // high+1 -> the bar covers key 23 fully + CHECK(bar.width == 120); // 12 keys * 10px } static void testZoneBarMalformedCollapses() { const StripLayout L = wideStrip(); // low > high must collapse, never invert. const Rect bar = zoneBarRect(L, 80, 40); - CHECK(bar.width() >= 0); - CHECK(bar.right >= bar.left); + CHECK(bar.width >= 0); + CHECK(bar.right() >= bar.x); } // --- zoneGrabAt --------------------------------------------------------------- @@ -117,23 +118,23 @@ static void testZoneBarMalformedCollapses() { static void testZoneGrabEdgesAndBody() { const StripLayout L = wideStrip(); const Rect bar = zoneBarRect(L, 20, 60); // wide bar with a clear body - const int y = L.keys.top + 2; + const int y = L.keys.y + 2; // Near the left edge -> low; near the right edge -> high; the middle -> body. - CHECK(zoneGrabAt(L, 20, 60, bar.left + 1, y) == ZoneGrab::kLowEdge); - CHECK(zoneGrabAt(L, 20, 60, bar.right - 1, y) == ZoneGrab::kHighEdge); - CHECK(zoneGrabAt(L, 20, 60, bar.left + bar.width() / 2, y) == ZoneGrab::kBody); + CHECK(zoneGrabAt(L, 20, 60, bar.x + 1, y) == ZoneGrab::kLowEdge); + CHECK(zoneGrabAt(L, 20, 60, bar.right() - 1, y) == ZoneGrab::kHighEdge); + CHECK(zoneGrabAt(L, 20, 60, bar.x + bar.width / 2, y) == ZoneGrab::kBody); // Off the bar entirely -> none. - CHECK(zoneGrabAt(L, 20, 60, bar.right + 20, y) == ZoneGrab::kNone); + CHECK(zoneGrabAt(L, 20, 60, bar.right() + 20, y) == ZoneGrab::kNone); } static void testZoneGrabNarrowBarSplitsAtMidpointLowWins() { const StripLayout L = wideStrip(); // A 1-key bar is narrower than 2*edge: no body; the low edge wins the exact midpoint. const Rect bar = zoneBarRect(L, 50, 50); - const int y = L.keys.top + 2; - const int mid = bar.left + bar.width() / 2; + const int y = L.keys.y + 2; + const int mid = bar.x + bar.width / 2; CHECK(zoneGrabAt(L, 50, 50, mid, y) == ZoneGrab::kLowEdge); // tie -> low - CHECK(zoneGrabAt(L, 50, 50, bar.right - 1, y) == ZoneGrab::kHighEdge); + CHECK(zoneGrabAt(L, 50, 50, bar.right() - 1, y) == ZoneGrab::kHighEdge); } // --- zoneBarAtPoint ----------------------------------------------------------- @@ -143,8 +144,8 @@ static void testZoneBarAtPointFirstMatch() { const int lows[2] = {20, 30}; // zone 0 and zone 1 overlap on [30,50] const int highs[2] = {50, 70}; const Rect overlap = zoneBarRect(L, 30, 50); - const int y = L.keys.top + 2; - const int cx = overlap.left + overlap.width() / 2; + const int y = L.keys.y + 2; + const int cx = overlap.x + overlap.width / 2; // A point in the overlap resolves to the FIRST covering zone (draw order). const ZoneBarHit hit = zoneBarAtPoint(L, lows, highs, 2, cx, y); CHECK(hit.zoneIndex == 0); @@ -219,7 +220,7 @@ static void testResolveDragProportionalNonDivisibleWidth() { // a drag from note 0 by (width-1) pixels must land at keyAtPoint(width-1), which is 127. const int width = 544; const StripLayout L = layoutStrip(width, 40); - CHECK(keyAtPoint(L, width - 1, L.keys.top + 1) == 127); + CHECK(keyAtPoint(L, width - 1, L.keys.y + 1) == 127); CHECK(resolveDragNote(L, 0, width - 1) == 127); // Also verify mid-strip coherence: for each key N, a drag from 0 by N's left-edge @@ -227,12 +228,12 @@ static void testResolveDragProportionalNonDivisibleWidth() { // rounding may round down). The critical direction is that it must NOT over-shoot by // more than 0 (it must reach at least the right key). for (int n = 1; n < kStripKeyCount; ++n) { - const int leftPx = keyRect(L, n).left; + const int leftPx = keyRect(L, n).x; const int resolved = resolveDragNote(L, 0, leftPx); // The left edge of key N is the first pixel "in" that key, so we expect resolved == N. // Allow resolved == N-1 only when the pixel is at the exact boundary (keyEdgeToX may // produce the same x for adjacent keys when keys share a pixel). Disallow over-shoot. - const int expected = keyAtPoint(L, leftPx, L.keys.top + 1); + const int expected = keyAtPoint(L, leftPx, L.keys.y + 1); CHECK(resolved >= expected - 1 && resolved <= expected + 1); } } diff --git a/tests/test_knob_deck.cpp b/tests/test_knob_deck.cpp index 695c6fa..4852617 100644 --- a/tests/test_knob_deck.cpp +++ b/tests/test_knob_deck.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::knob_deck — no VST3, no REAPER, no framework. Same fast +// Standalone tests for reasampler::instrument::ui::knob_deck — no VST3, no REAPER, no framework. Same fast // assert loop as the sibling pure tests. Assert the r11 deck layout HARD: // // * group width — caption row vs knob row max + padding; row-toggle and caption-toggle widths. @@ -10,12 +10,13 @@ // * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, blank (-1) cells // and fence padding miss, outside-deck miss. -#include "../src/vst/knob_deck.h" +#include "../src/core/instrument/ui/knob_deck.h" #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -75,20 +76,20 @@ static void testWrapAtNarrowWidthIsDeterministic() { CHECK(dl.groups.size() == 5); // Row membership: groups on row 1 share the first top; the wrapped groups sit one row // pitch lower and restart at the left margin. - const int row0Top = dl.groups[0].box.top; + const int row0Top = dl.groups[0].box.y; const int row1Top = row0Top + kDeckGroupH + kDeckRowGap; - CHECK(dl.groups[0].box.top == row0Top); - CHECK(dl.groups[1].box.top == row0Top); + CHECK(dl.groups[0].box.y == row0Top); + CHECK(dl.groups[1].box.y == row0Top); bool sawWrap = false; for (std::size_t i = 1; i < dl.groups.size(); ++i) { - if (dl.groups[i].box.top == row1Top && dl.groups[i - 1].box.top == row0Top) { - CHECK(dl.groups[i].box.left == 8); // wrapped row restarts at the left edge + if (dl.groups[i].box.y == row1Top && dl.groups[i - 1].box.y == row0Top) { + CHECK(dl.groups[i].box.x == 8); // wrapped row restarts at the left edge sawWrap = true; } } CHECK(sawWrap); // Every box stays within the available width (no group straddles the right edge). - for (const auto& g : dl.groups) CHECK(g.box.right <= 8 + 544); + for (const auto& g : dl.groups) CHECK(g.box.right() <= 8 + 544); } static void testFirstGroupAlwaysPlaces() { @@ -103,33 +104,33 @@ static void testGroupInnerGeometry() { const DeckLayout dl = layoutDeck(deck, 8, 50, 824); const DeckGroupLayout& amp = dl.groups[0]; // Caption row at the top padding; caption toggle right-anchored inside the box. - CHECK(amp.caption.top == amp.box.top + kDeckGroupPadY); + CHECK(amp.caption.y == amp.box.y + kDeckGroupPadY); CHECK(amp.captionToggle.id == 100); - CHECK(amp.captionToggle.seg1.right == amp.box.right - kDeckGroupPadX); - CHECK(amp.captionToggle.seg0.right == amp.captionToggle.seg1.left); - CHECK(amp.captionToggle.seg0.width() == 44 && amp.captionToggle.seg1.width() == 44); - CHECK(amp.captionToggle.seg0.height() == kDeckToggleH); + CHECK(amp.captionToggle.seg1.right() == amp.box.right() - kDeckGroupPadX); + CHECK(amp.captionToggle.seg0.right() == amp.captionToggle.seg1.x); + CHECK(amp.captionToggle.seg0.width == 44 && amp.captionToggle.seg1.width == 44); + CHECK(amp.captionToggle.seg0.height == kDeckToggleH); // The caption text rect stops before the toggle. - CHECK(amp.caption.right <= amp.captionToggle.seg0.left); + CHECK(amp.caption.right() <= amp.captionToggle.seg0.x); // Cells: five, fixed size, abutting, inside the box, below the caption row. CHECK(static_cast(amp.cells.size()) == 5); for (std::size_t i = 0; i < amp.cells.size(); ++i) { const DeckCellLayout& c = amp.cells[i]; - CHECK(c.cell.width() == kDeckCellW && c.cell.height() == kDeckCellH); - CHECK(c.cell.top == amp.box.top + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap); - if (i > 0) CHECK(c.cell.left == amp.cells[i - 1].cell.right); + CHECK(c.cell.width == kDeckCellW && c.cell.height == kDeckCellH); + CHECK(c.cell.y == amp.box.y + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap); + if (i > 0) CHECK(c.cell.x == amp.cells[i - 1].cell.right()); // Knob square centered horizontally, label band beneath it, both inside the cell. - CHECK(c.knob.width() == kDeckKnobSize && c.knob.height() == kDeckKnobSize); - CHECK(c.knob.left - c.cell.left == c.cell.right - c.knob.right); - CHECK(c.label.top >= c.knob.bottom); - CHECK(c.label.bottom <= c.cell.bottom); + CHECK(c.knob.width == kDeckKnobSize && c.knob.height == kDeckKnobSize); + CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right()); + CHECK(c.label.y >= c.knob.bottom()); + CHECK(c.label.bottom() <= c.cell.bottom()); } // VOICE group's row toggle sits after its cell, vertically centered in the cell row. const DeckGroupLayout& voice = dl.groups[3]; CHECK(voice.rowToggle.id == 104); - CHECK(voice.rowToggle.seg0.left == voice.cells[0].cell.right + kDeckToggleGap); - CHECK(voice.rowToggle.seg0.height() == kDeckToggleH); - CHECK(voice.rowToggle.seg0.top > voice.cells[0].cell.top); + CHECK(voice.rowToggle.seg0.x == voice.cells[0].cell.right() + kDeckToggleGap); + CHECK(voice.rowToggle.seg0.height == kDeckToggleH); + CHECK(voice.rowToggle.seg0.y > voice.cells[0].cell.y); // MASTER has no toggles. CHECK(dl.groups[4].captionToggle.id == -1); CHECK(dl.groups[4].rowToggle.id == -1); @@ -142,20 +143,20 @@ static void testHitTest() { // Knob hit: anywhere in the cell (including the label band) resolves to the cell id. const DeckCellLayout& c0 = amp.cells[0]; - DeckHit h = hitTestDeck(dl, c0.cell.left + 1, c0.cell.top + 1); + DeckHit h = hitTestDeck(dl, c0.cell.x + 1, c0.cell.y + 1); CHECK(h.kind == DeckHitKind::Knob && h.id == 1 && h.segment == -1); - h = hitTestDeck(dl, c0.label.left + 2, c0.label.top + 2); + h = hitTestDeck(dl, c0.label.x + 2, c0.label.y + 2); CHECK(h.kind == DeckHitKind::Knob && h.id == 1); // Caption toggle segments 0/1 at their boundary: last px of seg0, first px of seg1. - h = hitTestDeck(dl, amp.captionToggle.seg0.right - 1, amp.captionToggle.seg0.top + 1); + h = hitTestDeck(dl, amp.captionToggle.seg0.right() - 1, amp.captionToggle.seg0.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 0); - h = hitTestDeck(dl, amp.captionToggle.seg1.left, amp.captionToggle.seg1.top + 1); + h = hitTestDeck(dl, amp.captionToggle.seg1.x, amp.captionToggle.seg1.y + 1); CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 1); // Row toggle. const DeckGroupLayout& voice = dl.groups[3]; - h = hitTestDeck(dl, voice.rowToggle.seg1.left + 1, voice.rowToggle.seg1.top + 1); + h = hitTestDeck(dl, voice.rowToggle.seg1.x + 1, voice.rowToggle.seg1.y + 1); CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1); // A blank cell (id -1) misses even though its rect exists. @@ -164,11 +165,11 @@ static void testHitTest() { const DeckLayout tl = layoutDeck(trig, 0, 0, 824); const DeckCellLayout& blank = tl.groups[0].cells[4]; CHECK(blank.id == -1); - h = hitTestDeck(tl, blank.cell.left + 5, blank.cell.top + 5); + h = hitTestDeck(tl, blank.cell.x + 5, blank.cell.y + 5); CHECK(h.kind == DeckHitKind::None); // The fence padding inside the box misses; outside the deck misses. - h = hitTestDeck(dl, amp.box.left + 1, amp.box.bottom - 1); + h = hitTestDeck(dl, amp.box.x + 1, amp.box.bottom() - 1); CHECK(h.kind == DeckHitKind::None); h = hitTestDeck(dl, -50, -50); CHECK(h.kind == DeckHitKind::None); diff --git a/tests/test_lane_keys.cpp b/tests/test_lane_keys.cpp index 82a280b..7cfdd0b 100644 --- a/tests/test_lane_keys.cpp +++ b/tests/test_lane_keys.cpp @@ -9,12 +9,13 @@ // 3. Round-trip: managedLaneKey(laneNameForMode(m)) == "reasampler:" + m, so the // Wave-3 minting path and the read path cannot drift. -#include "../src/lane_keys.h" +#include "../src/core/view/lane_keys.h" #include #include using namespace reasampler; +using namespace reasampler::view; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_master_gain.cpp b/tests/test_master_gain.cpp index 16be668..50b098e 100644 --- a/tests/test_master_gain.cpp +++ b/tests/test_master_gain.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::master_gain — no VST3, no REAPER, no framework. Same +// Standalone tests for reasampler::instrument::engine::master_gain — no VST3, no REAPER, no framework. Same // fast assert loop as the sibling pure tests. Assert the FB1 post-mixer gain taper HARD: // // * -inf bottom — norm 0 maps to -infinity dB and TRUE ZERO linear (silence, not an epsilon); @@ -9,13 +9,14 @@ // * monotonicity — more norm never means less gain. // * label — "-inf" at the bottom, signed one-decimal dB elsewhere. -#include "../src/vst/master_gain.h" +#include "../src/core/instrument/engine/master_gain.h" #include #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::engine; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_mode_enable.cpp b/tests/test_mode_enable.cpp index 2e8effc..d8fe990 100644 --- a/tests/test_mode_enable.cpp +++ b/tests/test_mode_enable.cpp @@ -7,13 +7,14 @@ // buttons are live and "…: Design" buttons are dead; when Arrange is active, the reverse. An // unrecognized active id fails OPEN (every button live) so a future mode never dead-locks the bar. -#include "../src/mode_enable.h" -#include "../src/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ids the rule keys off +#include "../src/core/ui/mode_enable.h" +#include "../src/core/view/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ids the rule keys off #include #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_mode_switch.cpp b/tests/test_mode_switch.cpp index 3110165..42a84c3 100644 --- a/tests/test_mode_switch.cpp +++ b/tests/test_mode_switch.cpp @@ -8,13 +8,14 @@ // evenly); hit-test hits per segment and misses (outside the band above/below/ // left/right, boundary pixels); degenerate widths and counts. -#include "../src/mode_switch.h" +#include "../src/core/view/mode_switch.h" #include #include #include using namespace reasampler; +using namespace reasampler::view; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_note_entry.cpp b/tests/test_note_entry.cpp index 4990001..1107f36 100644 --- a/tests/test_note_entry.cpp +++ b/tests/test_note_entry.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::note_entry — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::map::note_entry — no VST3, no REAPER, no framework. // Assert the S12 direct-numeric-entry parse for a zone's low/high/root MIDI note. // // Covers: plain decimal integers (with +/- sign + surrounding whitespace); note names under the @@ -6,11 +6,12 @@ // [0,127] rather than rejecting; empty / whitespace-only / unparseable input returning nullopt; // the integer path taking precedence over the note-name path for a leading digit. -#include "../src/vst/note_entry.h" +#include "../src/core/instrument/map/note_entry.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::map; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_overflow_menu.cpp b/tests/test_overflow_menu.cpp index 0a064bb..c6912c3 100644 --- a/tests/test_overflow_menu.cpp +++ b/tests/test_overflow_menu.cpp @@ -8,11 +8,12 @@ // a degenerate band reserves nothing; hit-test in/out/edge (half-open bounds); an empty button // claims no point; draw and hit-test agree over the whole rect. -#include "../src/overflow_menu.h" +#include "../src/core/ui/overflow_menu.h" #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_owned_manifest.cpp b/tests/test_owned_manifest.cpp index 77ecf17..d850004 100644 --- a/tests/test_owned_manifest.cpp +++ b/tests/test_owned_manifest.cpp @@ -7,12 +7,13 @@ // semantics, insertion-order preservation, malformed-parse -> nullopt (the persist // shell's warn+fallback hinges on it), and round-trip of paths with JSON metacharacters. -#include "../src/owned_manifest.h" +#include "../src/core/model/owned_manifest.h" #include #include using namespace reasampler; +using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -35,6 +36,18 @@ static void testEmptyManifest() { CHECK(back->empty()); } +// Golden byte-literal (Q-W1 follow-up): pins the EXACT serialized bytes for a +// small fixture (two paths), not just self-consistent re-serialization — a +// format drift that both writer and reader agree on would slip past the +// round-trip tests but not this. The format is frozen as-shipped; the literal +// below is the captured current output. +static void testSerializeGoldenLiteral() { + OwnedFileManifest m; + m.add("reasampler_bank/a.wav"); + m.add("reasampler_bank/b.wav"); + CHECK(m.serialize() == "{\"owned\":[\"reasampler_bank/a.wav\",\"reasampler_bank/b.wav\"]}"); +} + // --- add / contains / order -------------------------------------------------- static void testAddAndContains() { @@ -158,6 +171,7 @@ static void testMalformedParse() { } int main() { + testSerializeGoldenLiteral(); testEmptyManifest(); testAddAndContains(); testDedupRepeatedAdds(); diff --git a/tests/test_param_slider.cpp b/tests/test_param_slider.cpp index 35005a4..48dfaed 100644 --- a/tests/test_param_slider.cpp +++ b/tests/test_param_slider.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::param_slider — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::param_slider — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure editor tests (capture_browser / keyboard_strip): // assert the S12/S15/S16 control-surface layout, toggle-segment split + hit-test, slider // value<->pixel mapping (round-trip + clamping + endpoints), and point->control routing. @@ -16,13 +16,14 @@ // o'clock), wrap-boundary + un-normalized arc inputs, the needle endpoint on the circle, and // the vertical-drag delta->value map (up = increase) with clamping at 0/1. -#include "../src/vst/param_slider.h" +#include "../src/core/instrument/ui/param_slider.h" #include #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -33,7 +34,7 @@ static bool approx(double a, double b) { return (a - b) < 1e-9 && (b - a) < 1e-9 // --- layoutControls ----------------------------------------------------------- static void testLayoutStacksRows() { - const Rect panel{0, 100, 300, 400}; + const Rect panel = Rect::ltrb(0, 100, 300, 400); std::vector ctl{ {1, ControlKind::Toggle}, {2, ControlKind::Slider}, @@ -42,58 +43,58 @@ static void testLayoutStacksRows() { const std::vector rows = layoutControls(panel, ctl); CHECK(rows.size() == 3); // Row 0 sits at the panel top; each subsequent row is one row-height + gap below. - CHECK(rows[0].row.top == 100); - CHECK(rows[0].row.bottom == 100 + kControlRowHeight); - CHECK(rows[1].row.top == rows[0].row.bottom + kControlRowGap); - CHECK(rows[2].row.top == rows[1].row.bottom + kControlRowGap); + CHECK(rows[0].row.y == 100); + CHECK(rows[0].row.bottom() == 100 + kControlRowHeight); + CHECK(rows[1].row.y == rows[0].row.bottom() + kControlRowGap); + CHECK(rows[2].row.y == rows[1].row.bottom() + kControlRowGap); // Ids + kinds carried through in order. CHECK(rows[0].id == 1 && rows[0].kind == ControlKind::Toggle); CHECK(rows[1].id == 2 && rows[1].kind == ControlKind::Slider); // Label column then control column, contiguous, spanning the panel width. - CHECK(rows[0].label.left == panel.left); - CHECK(rows[0].control.left == rows[0].label.right); - CHECK(rows[0].control.right == panel.right); - CHECK(rows[0].label.width() == kControlLabelWidth); + CHECK(rows[0].label.x == panel.x); + CHECK(rows[0].control.x == rows[0].label.right()); + CHECK(rows[0].control.right() == panel.right()); + CHECK(rows[0].label.width == kControlLabelWidth); } static void testLayoutEmptyAndDegenerate() { - CHECK(layoutControls(Rect{0, 0, 300, 300}, {}).empty()); + CHECK(layoutControls(Rect::ltrb(0, 0, 300, 300), {}).empty()); std::vector ctl{{1, ControlKind::Slider}}; - CHECK(layoutControls(Rect{0, 0, 0, 0}, ctl).empty()); - CHECK(layoutControls(Rect{0, 0, 300, 0}, ctl).empty()); + CHECK(layoutControls(Rect::ltrb(0, 0, 0, 0), ctl).empty()); + CHECK(layoutControls(Rect::ltrb(0, 0, 300, 0), ctl).empty()); } static void testLayoutNarrowPanelClampsLabel() { // A panel narrower than 2*labelWidth clamps the label column to half so a control column // survives. - const Rect panel{0, 0, 100, 200}; + const Rect panel = Rect::ltrb(0, 0, 100, 200); const std::vector rows = layoutControls(panel, {{1, ControlKind::Slider}}); CHECK(rows.size() == 1); - CHECK(rows[0].label.width() <= panel.width() / 2 + 1); - CHECK(rows[0].control.width() > 0); + CHECK(rows[0].label.width <= panel.width / 2 + 1); + CHECK(rows[0].control.width > 0); } // --- toggle ------------------------------------------------------------------- static void testToggleSegmentsTile() { - const Rect control{100, 0, 300, 22}; // width 200 + const Rect control = Rect::ltrb(100, 0, 300, 22); // width 200 const Rect s0 = toggleSegmentRect(control, 0); const Rect s1 = toggleSegmentRect(control, 1); - CHECK(s0.left == 100 && s0.right == 200); - CHECK(s1.left == 200 && s1.right == 300); // last absorbs remainder -> reaches control.right + CHECK(s0.x == 100 && s0.right() == 200); + CHECK(s1.x == 200 && s1.right() == 300); // last absorbs remainder -> reaches control.right() // Out of range. - CHECK(toggleSegmentRect(control, 2).width() == 0); - CHECK(toggleSegmentRect(control, -1).width() == 0); + CHECK(toggleSegmentRect(control, 2).width == 0); + CHECK(toggleSegmentRect(control, -1).width == 0); } static void testToggleSegmentRemainderInLast() { - const Rect control{0, 0, 201, 22}; // odd width -> seg0 = 100, seg1 = 101 (absorbs remainder) - CHECK(toggleSegmentRect(control, 0).width() == 100); - CHECK(toggleSegmentRect(control, 1).right == 201); + const Rect control = Rect::ltrb(0, 0, 201, 22); // odd width -> seg0 = 100, seg1 = 101 (absorbs remainder) + CHECK(toggleSegmentRect(control, 0).width == 100); + CHECK(toggleSegmentRect(control, 1).right() == 201); } static void testToggleHitTest() { - const Rect control{100, 0, 300, 22}; + const Rect control = Rect::ltrb(100, 0, 300, 22); CHECK(toggleSegmentHitTest(control, 150, 10) == 0); CHECK(toggleSegmentHitTest(control, 250, 10) == 1); CHECK(toggleSegmentHitTest(control, 50, 10) == -1); // left of control @@ -103,59 +104,59 @@ static void testToggleHitTest() { // --- slider ------------------------------------------------------------------- static void testSliderTrackInsetsHalfHandle() { - const Rect control{100, 0, 300, 22}; + const Rect control = Rect::ltrb(100, 0, 300, 22); const Rect track = sliderTrackRect(control); - CHECK(track.left == control.left + kSliderHandleWidth / 2); - CHECK(track.right == control.right - kSliderHandleWidth / 2); + CHECK(track.x == control.x + kSliderHandleWidth / 2); + CHECK(track.right() == control.right() - kSliderHandleWidth / 2); // A control too narrow for a handle yields an empty track. - CHECK(sliderTrackRect(Rect{0, 0, kSliderHandleWidth - 1, 22}).width() == 0); + CHECK(sliderTrackRect(Rect::ltrb(0, 0, kSliderHandleWidth - 1, 22)).width == 0); } static void testSliderHandleAtEndpointsAndMid() { - const Rect control{100, 0, 300, 22}; + const Rect control = Rect::ltrb(100, 0, 300, 22); const Rect track = sliderTrackRect(control); const int half = kSliderHandleWidth / 2; - // Value 0 -> handle centered at track.left. + // Value 0 -> handle centered at track.x. const Rect h0 = sliderHandleRect(control, 0.0); - CHECK(h0.left + half == track.left); - // Value 1 -> handle centered at track.right. + CHECK(h0.x + half == track.x); + // Value 1 -> handle centered at track.right(). const Rect h1 = sliderHandleRect(control, 1.0); - CHECK(h1.left + half == track.right); + CHECK(h1.x + half == track.right()); // Value 0.5 -> centered at the track middle. const Rect hm = sliderHandleRect(control, 0.5); - CHECK(hm.left + half == track.left + track.width() / 2); + CHECK(hm.x + half == track.x + track.width / 2); } static void testSliderHandleClampsOutOfRange() { - const Rect control{0, 0, 200, 22}; - CHECK(sliderHandleRect(control, -0.5).left == sliderHandleRect(control, 0.0).left); - CHECK(sliderHandleRect(control, 5.0).left == sliderHandleRect(control, 1.0).left); + const Rect control = Rect::ltrb(0, 0, 200, 22); + CHECK(sliderHandleRect(control, -0.5).x == sliderHandleRect(control, 0.0).x); + CHECK(sliderHandleRect(control, 5.0).x == sliderHandleRect(control, 1.0).x); } static void testValueAtPointEndpointsSaturate() { - const Rect control{100, 0, 300, 22}; + const Rect control = Rect::ltrb(100, 0, 300, 22); const Rect track = sliderTrackRect(control); - CHECK(approx(valueAtPoint(control, track.left - 20), 0.0)); - CHECK(approx(valueAtPoint(control, track.left), 0.0)); - CHECK(approx(valueAtPoint(control, track.right + 20), 1.0)); - CHECK(approx(valueAtPoint(control, track.right), 1.0)); + CHECK(approx(valueAtPoint(control, track.x - 20), 0.0)); + CHECK(approx(valueAtPoint(control, track.x), 0.0)); + CHECK(approx(valueAtPoint(control, track.right() + 20), 1.0)); + CHECK(approx(valueAtPoint(control, track.right()), 1.0)); } static void testValueAtPointIsHandleInverse() { // Round-trip: a value -> handle center -> valueAtPoint recovers (within one pixel quantum). - const Rect control{50, 0, 450, 22}; // wide track for pixel resolution + const Rect control = Rect::ltrb(50, 0, 450, 22); // wide track for pixel resolution const Rect track = sliderTrackRect(control); for (double v : {0.1, 0.25, 0.5, 0.75, 0.9}) { const Rect h = sliderHandleRect(control, v); - const int centerX = h.left + kSliderHandleWidth / 2; + const int centerX = h.x + kSliderHandleWidth / 2; const double back = valueAtPoint(control, centerX); CHECK(back >= v - 0.01 && back <= v + 0.01); - CHECK(centerX >= track.left && centerX <= track.right); + CHECK(centerX >= track.x && centerX <= track.right()); } } static void testValueAtPointDegenerateTrack() { - CHECK(approx(valueAtPoint(Rect{0, 0, kSliderHandleWidth - 1, 22}, 5), 0.0)); + CHECK(approx(valueAtPoint(Rect::ltrb(0, 0, kSliderHandleWidth - 1, 22), 5), 0.0)); } // --- knob (FA4) ----------------------------------------------------------------- @@ -164,21 +165,21 @@ static bool nearWithin(double a, double b, double tol) { return (a - b) < tol && static void testKnobGeometryInscribesCell() { // A 44x44 cell at (100,0): center (122,22), radius 22. - const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); + const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44)); CHECK(approx(g.centerX, 122.0)); CHECK(approx(g.centerY, 22.0)); CHECK(approx(g.radius, 22.0)); // A wide cell inscribes on the smaller (vertical) dimension. - const KnobGeometry w = computeKnob(Rect{0, 0, 200, 22}); + const KnobGeometry w = computeKnob(Rect::ltrb(0, 0, 200, 22)); CHECK(approx(w.radius, 11.0)); CHECK(approx(w.centerX, 100.0)); // Degenerate cells yield radius 0. - CHECK(computeKnob(Rect{0, 0, 0, 22}).radius == 0.0); - CHECK(computeKnob(Rect{0, 0, 22, 0}).radius == 0.0); + CHECK(computeKnob(Rect::ltrb(0, 0, 0, 22)).radius == 0.0); + CHECK(computeKnob(Rect::ltrb(0, 0, 22, 0)).radius == 0.0); } static void testKnobHitTestCircle() { - const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); // center (122,22), r 22 + const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44)); // center (122,22), r 22 CHECK(knobHitTest(g, 122, 22)); // center — always hits CHECK(!knobHitTest(g, 122 + 22, 22)); // exactly on the boundary — boundary exclusive CHECK(!knobHitTest(g, 122 + 22, 44)); // cell corner: inside the rect, outside the circle @@ -223,7 +224,7 @@ static void testKnobArcWrapBoundary() { } static void testKnobNeedlePointOnCircle() { - const KnobGeometry g = computeKnob(Rect{100, 0, 144, 44}); // center (122,22), r 22 + const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44)); // center (122,22), r 22 // Default arc, value 0 -> 7 o'clock -> needle points down-left from center. const KnobPoint p7 = knobNeedlePoint(g, KnobArc{}, 0.0); // 210° clockwise from 12: sin(210°)=-0.5, cos(210°)=-√3/2 -> x = cx - r/2, y = cy + r*√3/2 @@ -265,7 +266,7 @@ static void testKnobDragClamps() { // --- controlAtPoint routing --------------------------------------------------- static void testControlAtPointRoutes() { - const Rect panel{0, 0, 300, 400}; + const Rect panel = Rect::ltrb(0, 0, 300, 400); std::vector ctl{ {10, ControlKind::Toggle}, {20, ControlKind::Slider}, @@ -274,26 +275,26 @@ static void testControlAtPointRoutes() { const std::vector rows = layoutControls(panel, ctl); // A point in the toggle's control area routes to the toggle id. const Rect tctl = rows[0].control; - CHECK(controlAtPoint(rows, (tctl.left + tctl.right) / 2, (tctl.top + tctl.bottom) / 2) == 10); + CHECK(controlAtPoint(rows, (tctl.x + tctl.right()) / 2, (tctl.y + tctl.bottom()) / 2) == 10); // A point on the slider's track routes to the slider id. const Rect strack = sliderTrackRect(rows[1].control); - CHECK(controlAtPoint(rows, (strack.left + strack.right) / 2, - (strack.top + strack.bottom) / 2) == 20); + CHECK(controlAtPoint(rows, (strack.x + strack.right()) / 2, + (strack.y + strack.bottom()) / 2) == 20); // A point at the knob's center routes to the knob id; the control-rect corner (outside // the circle) is a miss. const KnobGeometry kg = computeKnob(rows[2].control); CHECK(controlAtPoint(rows, static_cast(kg.centerX), static_cast(kg.centerY)) == 30); - CHECK(controlAtPoint(rows, rows[2].control.left + 1, rows[2].control.top + 1) == -1); + CHECK(controlAtPoint(rows, rows[2].control.x + 1, rows[2].control.y + 1) == -1); } static void testControlAtPointMisses() { - const Rect panel{0, 0, 300, 400}; + const Rect panel = Rect::ltrb(0, 0, 300, 400); const std::vector rows = layoutControls(panel, {{10, ControlKind::Toggle}, {20, ControlKind::Slider}}); // The label column is not interactive. - CHECK(controlAtPoint(rows, rows[0].label.left + 2, rows[0].label.top + 4) == -1); + CHECK(controlAtPoint(rows, rows[0].label.x + 2, rows[0].label.y + 4) == -1); // The gap between rows is a miss. - const int gapY = rows[0].row.bottom + kControlRowGap / 2; + const int gapY = rows[0].row.bottom() + kControlRowGap / 2; CHECK(controlAtPoint(rows, 200, gapY) == -1); // Off-panel below. CHECK(controlAtPoint(rows, 200, 5000) == -1); diff --git a/tests/test_peaks.cpp b/tests/test_peaks.cpp index 713f70b..4ac91b4 100644 --- a/tests/test_peaks.cpp +++ b/tests/test_peaks.cpp @@ -11,13 +11,14 @@ // merge, steep disjoint-span merge, no-bin-dropped spike sweep, no-column-empty // coverage, col clamp, degenerate inputs. -#include "../src/peaks.h" +#include "../src/core/audio/peaks.h" #include #include #include using namespace reasampler; +using namespace reasampler::audio; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_pitch_shift.cpp b/tests/test_pitch_shift.cpp index a92c2fe..768e45e 100644 --- a/tests/test_pitch_shift.cpp +++ b/tests/test_pitch_shift.cpp @@ -27,13 +27,14 @@ // the master's splice decision (jump/lag/frac/fadeLen AND firing frame) exactly, on // decorrelated stereo content where an independent per-channel search provably diverges. -#include "../src/vst/pitch_shift.h" +#include "../src/core/instrument/engine/pitch_shift.h" #include #include #include using namespace reasampler; +using namespace reasampler::instrument::engine; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_provenance.cpp b/tests/test_provenance.cpp index a2b19f3..5c77186 100644 --- a/tests/test_provenance.cpp +++ b/tests/test_provenance.cpp @@ -15,15 +15,16 @@ // The Sample-JSON round-trip of the fingerprint (leveraging M1's existing provenance // round-trip) is exercised in test_bank_model.cpp — see the fingerprint case there. -#include "../src/provenance.h" +#include "../src/core/model/provenance.h" -#include "../src/bank_model.h" // recipe-through-Sample-JSON round-trip (M1 seam) +#include "../src/core/model/bank_model.h" // recipe-through-Sample-JSON round-trip (M1 seam) #include #include #include using namespace reasampler; +using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -246,7 +247,7 @@ static void testHugeGuidCountRejectedBeforeReserve() { // --- recorded-recipe model round-trips through the Sample JSON ---------------- // The fingerprint rides in Provenance.fxChainSnapshot (one string), which M1's -// BankIndex JSON already round-trips. Prove a real recipe survives that path intact. +// BankModel JSON already round-trips. Prove a real recipe survives that path intact. static void testRecipeThroughSampleJson() { const CaptureRecipe r = baseRecipe(); @@ -260,10 +261,10 @@ static void testRecipeThroughSampleJson() { prov.fxChainSnapshot = buildFingerprint(r); s.provenance = prov; - BankIndex idx; + BankModel idx; CHECK(idx.add(s) == AddResult::Added); - auto back = BankIndex::deserialize(idx.serialize()); + auto back = BankModel::deserialize(idx.serialize()); CHECK(back.has_value()); const Sample* child = back ? back->query("child-1") : nullptr; CHECK(child != nullptr); diff --git a/tests/test_prune_button.cpp b/tests/test_prune_button.cpp index 19ff0aa..2803d64 100644 --- a/tests/test_prune_button.cpp +++ b/tests/test_prune_button.cpp @@ -7,11 +7,12 @@ // tail-label inset or is degenerate; hit-test in/out/edge (half-open bounds); a // suppressed/empty button claims no point; draw and hit-test agree over the whole rect. -#include "../src/prune_button.h" +#include "../src/core/ui/prune_button.h" #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_prune_reconcile.cpp b/tests/test_prune_reconcile.cpp index 8a283d1..2fabda3 100644 --- a/tests/test_prune_reconcile.cpp +++ b/tests/test_prune_reconcile.cpp @@ -15,8 +15,8 @@ // Plus: determinism (present-order output), duplicate-`present` de-dup, exact-string // (non-normalizing) match, and the referencedPaths() union query directly. -#include "../src/bank_book.h" -#include "../src/prune_reconcile.h" +#include "../src/core/model/bank_book.h" +#include "../src/core/reclaim/prune_reconcile.h" #include #include @@ -26,6 +26,8 @@ #include using namespace reasampler; +using namespace reasampler::model; +using namespace reasampler::reclaim; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_realtime_record.cpp b/tests/test_realtime_record.cpp index c904ecc..1da6e96 100644 --- a/tests/test_realtime_record.cpp +++ b/tests/test_realtime_record.cpp @@ -3,12 +3,13 @@ // record-mode/recipe bookkeeping (channel count + tap -> I_RECMODE / I_RECMODE_FLAGS) // and the wet/dry -> tap decision, plus the recorded-file -> Sample mapping. -#include "../src/realtime_record.h" +#include "../src/core/capture/realtime_record.h" #include #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_render_settings.cpp b/tests/test_render_settings.cpp index 62001c1..fc30425 100644 --- a/tests/test_render_settings.cpp +++ b/tests/test_render_settings.cpp @@ -6,7 +6,7 @@ // FX-bypass plan (corrects the "items captured through parent FX" defect), and the // capture-action taxonomy table (stable ids, scope x tail-variant matrix). -#include "../src/render_settings.h" +#include "../src/core/capture/render_settings.h" #include #include @@ -14,6 +14,7 @@ #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_sample_map.cpp b/tests/test_sample_map.cpp index 427cbcb..1a7a96d 100644 --- a/tests/test_sample_map.cpp +++ b/tests/test_sample_map.cpp @@ -22,7 +22,7 @@ // stride contract across the seam (that the byte stride wav_trim reports matches the // channel-count stride downmixToMono divides by). -#include "../src/vst/sample_map.h" +#include "../src/core/instrument/map/sample_map.h" #include #include @@ -30,18 +30,21 @@ #include #include -#include "../src/bank_book.h" -#include "../src/bank_model.h" -#include "../src/vst/master_gain.h" // masterGainMaxLinear (the v8 master-gain wire cap) +#include "../src/core/model/bank_book.h" +#include "../src/core/model/bank_model.h" +#include "../src/core/instrument/engine/master_gain.h" // masterGainMaxLinear (the v8 master-gain wire cap) using namespace reasampler; +using namespace reasampler::instrument::engine; +using namespace reasampler::capture; // wav_trim (WavLayout) — sample_map re-exports live in reasampler until Q-W2v +using namespace reasampler::model; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // Build a Sample with the fields sample_map reads. Relative path is required by -// BankIndex::add (relative-only invariant); a content hash is set so dedup does not +// BankModel::add (relative-only invariant); a content hash is set so dedup does not // collapse distinct entries. static Sample makeSample(const std::string& id, const std::string& name, const std::string& rel, std::optional root) { @@ -62,7 +65,7 @@ static std::string bookJson(const std::vector& poolSamples, for (const Sample& s : poolSamples) book.pool().index.add(s); if (!drumSamples.empty()) { book.createBank("drums-id", "Drums"); - BankIndex* di = book.index("drums-id"); + BankModel* di = book.index("drums-id"); for (const Sample& s : drumSamples) di->add(s); } return book.serialize(); @@ -1305,7 +1308,7 @@ static void testComponentStateMasterGainWriterClamps() { hi.masterGainLinear = 1000.0; CHECK(std::fabs(deserializeComponentState(serializeComponentState(hi), 44100.0) .masterGainLinear - - vst::masterGainMaxLinear()) < 1e-9); + instrument::engine::masterGainMaxLinear()) < 1e-9); ComponentState lo; lo.masterGainLinear = -5.0; CHECK(deserializeComponentState(serializeComponentState(lo), 44100.0).masterGainLinear == @@ -1686,7 +1689,7 @@ static void testVelocityCurveRoundTrip() { // A second zone left at the flat default proves the field is per-record and defaults to flat y=1. PerformanceMap m; PerformanceZone z = zone("lead", 20, 100); - z.velocityCurve = vst::VelocityCurve::linear(); + z.velocityCurve = VelocityCurve::linear(); z.velocityCurve.addPoint(60.0, 0.3); // an interior knot to exercise multi-point round-trip m.zones.push_back(z); m.zones.push_back(zone("pad", 0, 19)); // default flat curve @@ -1694,7 +1697,7 @@ static void testVelocityCurveRoundTrip() { CHECK(back.zones.size() == 2); if (back.zones.size() != 2) return; CHECK(back.zones[0].velocityCurve.equals(z.velocityCurve)); // exact point round-trip - CHECK(back.zones[1].velocityCurve.equals(vst::VelocityCurve::flat())); // default preserved + CHECK(back.zones[1].velocityCurve.equals(VelocityCurve::flat())); // default preserved // And the flat default really is unity everywhere (R10-F1 Option A), not the old linear ramp. CHECK(back.zones[1].velocityCurve.eval(1.0) == 1.0); CHECK(back.zones[1].velocityCurve.eval(64.0) == 1.0); @@ -1706,12 +1709,12 @@ static void testVelocityCurveThroughComponentEnvelope() { ComponentState s; s.selectionId = "pick"; PerformanceZone z = zone("pick", 0, 127); - z.velocityCurve = vst::VelocityCurve::linear(); + z.velocityCurve = VelocityCurve::linear(); s.map.zones.push_back(z); const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.map.zones.size() == 1); if (back.map.zones.size() != 1) return; - CHECK(back.map.zones[0].velocityCurve.equals(vst::VelocityCurve::linear())); + CHECK(back.map.zones[0].velocityCurve.equals(VelocityCurve::linear())); } static void testVelocityCurveResolvesToZone() { @@ -1720,12 +1723,12 @@ static void testVelocityCurveResolvesToZone() { const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); PerformanceMap m; PerformanceZone z = zone("a", 0, 127); - z.velocityCurve = vst::VelocityCurve::linear(); + z.velocityCurve = VelocityCurve::linear(); m.zones.push_back(z); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1); if (r.zones.size() != 1) return; - CHECK(r.zones[0].velocityCurve.equals(vst::VelocityCurve::linear())); + CHECK(r.zones[0].velocityCurve.equals(VelocityCurve::linear())); } // FA1 bug 3a — the COMPOSED end-to-end regression, mirroring the processor's reload composition @@ -1740,7 +1743,7 @@ static void testVelocityCurveEndToEndThroughReloadComposition() { ComponentState s; s.selectionId = "a"; PerformanceZone z = zone("a", 0, 127); - z.velocityCurve = vst::VelocityCurve::linear(); + z.velocityCurve = VelocityCurve::linear(); s.map.zones.push_back(z); const ComponentState back = deserializeComponentState(serializeComponentState(s), 48000.0); CHECK(back.map.zones.size() == 1); @@ -1817,7 +1820,7 @@ static void testVelocityCurveV6BackCompatLiftsToFlat() { CHECK(back.zones[0].sampleId == "v6saved"); CHECK(back.zones[0].keyTrack == 0.5); // the v6 field still read correctly // No curve tail -> flat y=1 default (the deliberate behavior change). - CHECK(back.zones[0].velocityCurve.equals(vst::VelocityCurve::flat())); + CHECK(back.zones[0].velocityCurve.equals(VelocityCurve::flat())); CHECK(back.zones[0].velocityCurve.eval(20.0) == 1.0); // a soft hit now plays at unity } diff --git a/tests/test_sample_usage.cpp b/tests/test_sample_usage.cpp index c2d84b0..8c48da5 100644 --- a/tests/test_sample_usage.cpp +++ b/tests/test_sample_usage.cpp @@ -22,7 +22,7 @@ // the pure identity matcher (UID hex / module filename base / display name, beta // over-protect), and the composed pruneOrphans exclusion proof. -#include "../src/sample_usage.h" +#include "../src/core/wire/sample_usage.h" #include #include @@ -30,9 +30,11 @@ #include #include -#include "../src/prune_reconcile.h" // mergeReferenced + pruneOrphans (composed proof) +#include "../src/core/reclaim/prune_reconcile.h" // mergeReferenced + pruneOrphans (composed proof) using namespace reasampler; +using namespace reasampler::reclaim; +using namespace reasampler::wire; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_sampler_core.cpp b/tests/test_sampler_core.cpp index 279953c..ec968c9 100644 --- a/tests/test_sampler_core.cpp +++ b/tests/test_sampler_core.cpp @@ -17,7 +17,7 @@ // by the CMake target linking neither SDK — this file includes only sampler_core.h + // the standard library, which is itself the compile-time proof. -#include "../src/vst/sampler_core.h" +#include "../src/core/instrument/engine/sampler_core.h" #include #include @@ -25,6 +25,7 @@ #include using namespace reasampler; +using namespace reasampler::instrument::engine; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -426,7 +427,7 @@ static void testNoteOffReleasesNewestSameNote() { // A LINEAR velocity curve keeps the two velocities distinguishable (velocity/127). The default // flat y=1 curve (S-VIEW-9 R10-F1) would render both at unity, collapsing the distinction this // note-off-selection test relies on — so we opt this zone back to the linear response. - km.zones[0].velocityCurve = vst::VelocityCurve::linear(); + km.zones[0].velocityCurve = VelocityCurve::linear(); VoiceEngine eng(8, km); std::size_t first = eng.noteOn(60, velOld); // older voice, lower gain @@ -751,7 +752,7 @@ static void testVelocityDefaultCurveIsFlatUnity() { // (not a hardcoded map) drives the gain, and that eval is applied at note-on. static void testVelocityLinearCurveReproducesRamp() { Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0 - km.zones[0].velocityCurve = vst::VelocityCurve::linear(); + km.zones[0].velocityCurve = VelocityCurve::linear(); { VoiceEngine eng(1, km); eng.noteOn(60, 127); @@ -776,7 +777,7 @@ static void testVelocityLinearCurveReproducesRamp() { // curve's shaped value, not the linear one. Proves the whole curve, not just the endpoints, applies. static void testVelocityShapedCurveDrivesGain() { Keymap km = Keymap::singleSampleChromatic(dcSample(100, 60)); // DC 1.0 - vst::VelocityCurve curve = vst::VelocityCurve::linear(); + VelocityCurve curve = VelocityCurve::linear(); curve.addPoint(64.0, 0.9); // pull the mid-velocity response UP to 0.9 km.zones[0].velocityCurve = curve; VoiceEngine eng(1, km); @@ -1429,7 +1430,7 @@ static void testVelocityCurveAppliesUnderPreserve() { SampleData s = dcSample(4000, 60); s.play.pitchEngine = PitchEngine::Preserve; Keymap km = Keymap::singleSampleChromatic(std::move(s)); - km.zones[0].velocityCurve = vst::VelocityCurve::linear(); + km.zones[0].velocityCurve = VelocityCurve::linear(); VoiceEngine eng(1, km, /*preserveCap=*/0, /*window=*/256); eng.noteOn(62, vel); // transposed: the genuine shifter path (not the unity demotion) std::vector out; @@ -1578,7 +1579,7 @@ static void testMonoRepressHeldNoteMovesToTop() { static void testMonoRetriggerFallbackUsesOriginalVelocity() { SampleData s = dcLevelSample(200000, 1.0f, 60); Keymap km = Keymap::singleSampleChromatic(std::move(s)); - km.zones[0].velocityCurve = vst::VelocityCurve::linear(); // gain = velocity/127 + km.zones[0].velocityCurve = VelocityCurve::linear(); // gain = velocity/127 VoiceEngine eng(4, km, 0, 0, VoiceMode::Mono, MonoTrigger::Retrigger); eng.noteOn(60, 32); // soft first note CHECK(approx(probeFrame(eng), 32.0 / 127.0, 1e-4)); @@ -1645,7 +1646,7 @@ static void testMonoLegatoRetunesWithoutReadRestart() { s.rootNote = 60; s.play.adsr = flatAdsr(); Keymap km = Keymap::singleSampleChromatic(std::move(s)); - km.zones[0].velocityCurve = vst::VelocityCurve::linear(); + km.zones[0].velocityCurve = VelocityCurve::linear(); VoiceEngine eng(2, km, 0, 0, VoiceMode::Mono, MonoTrigger::Legato); eng.noteOn(60, 127); // unity: read advances 1/frame, full gain std::vector out; diff --git a/tests/test_slot_map.cpp b/tests/test_slot_map.cpp new file mode 100644 index 0000000..72fce65 --- /dev/null +++ b/tests/test_slot_map.cpp @@ -0,0 +1,228 @@ +// Standalone tests for reasampler::model::SlotMap — no REAPER, no test framework. +// SlotMap is the L7 gap-preserving display-position carrier for one bank, extracted +// from bank_book (Q-W1, T4-05). These are the pure SlotMap-only assertions that +// previously lived inline in test_bank_book.cpp (the L7 "SlotMap unit behaviour" +// block); test_bank_book.cpp keeps its BankBook-level integration coverage +// (reorderSample / reconcileSlots / JSON round-trip WITH a full book), this file +// owns the module's own contract: add/remove/query, reorder gap-preservation, +// resetDense/reconcile, equality/fromEntries, and the serialize wire shape. + +#include "../src/core/model/slot_map.h" + +#include +#include +#include +#include + +#include "../src/core/json/json.h" + +using namespace reasampler::model; +namespace json = reasampler::json; + +static int g_fail = 0; +#define CHECK(cond) do { if(!(cond)) { \ + std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) + +// --- SlotMap unit behaviour -------------------------------------------------- + +static void testSlotMapDenseAppend() { + SlotMap m; + m.append("a"); + m.append("b"); + m.append("c"); + CHECK(m.slotOf("a") == 0); + CHECK(m.slotOf("b") == 1); + CHECK(m.slotOf("c") == 2); + CHECK(m.maxSlot() == 2); + CHECK((m.orderedIds() == std::vector{"a", "b", "c"})); + CHECK(m.idAt(1) == "b"); + CHECK(m.slotOf("nope") == -1); +} + +static void testSlotMapRemoveLeavesGap() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); // 0,1,2 + CHECK(m.remove("b")); // slot 1 now EMPTY (no re-pack) + CHECK(m.slotOf("a") == 0); + CHECK(m.slotOf("c") == 2); // c did NOT shift down + CHECK(m.idAt(1).empty()); // gap preserved + CHECK((m.orderedIds() == std::vector{"a", "c"})); + CHECK(!m.remove("b")); // already gone +} + +static void testSlotMapAppendAfterGapGoesToFrontier() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); // 0,1,2 + m.remove("a"); // slot 0 empty + m.append("d"); // append goes AFTER last occupied (2) -> 3 + CHECK(m.slotOf("d") == 3); // did NOT fill the slot-0 gap + CHECK(m.idAt(0).empty()); +} + +static void testSlotMapReorderIntoEmpty() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); // 0,1,2 + m.remove("b"); // slot 1 empty + CHECK(m.reorder("c", 1)); // c -> empty slot 1; its slot 2 empties + CHECK(m.slotOf("c") == 1); + CHECK(m.idAt(2).empty()); + CHECK(m.slotOf("a") == 0); // untouched +} + +static void testSlotMapReorderOntoOccupiedInsertsAndShifts() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); m.append("d"); // 0,1,2,3 + CHECK(m.reorder("d", 1)); // d onto occupied slot 1 -> insert-before, shift b,c up + CHECK(m.slotOf("a") == 0); // before the target: unchanged + CHECK(m.slotOf("d") == 1); // took the target slot + CHECK(m.slotOf("b") == 2); // shifted +1 + CHECK(m.slotOf("c") == 3); // shifted +1 + CHECK((m.orderedIds() == std::vector{"a", "d", "b", "c"})); +} + +static void testSlotMapReorderPreservesInteriorGapAboveTarget() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); // 0,1,2 + m.remove("b"); // gap at 1: a@0, c@2 + m.append("d"); // d@3 + CHECK(m.reorder("d", 0)); // d onto occupied slot 0 -> a shifts to 1, c shifts to 3 + CHECK(m.slotOf("d") == 0); + CHECK(m.slotOf("a") == 1); // shifted from 0 -> 1 + CHECK(m.slotOf("c") == 3); // shifted from 2 -> 3 (gap at 2 preserved as a +1 of its own) + CHECK(m.idAt(2).empty()); // interior gap above the target survives +} + +static void testSlotMapReorderUnmappedIsNoOp() { + SlotMap m; + m.append("a"); + CHECK(!m.reorder("ghost", 0)); // not mapped -> false, no mutation + CHECK(m.slotOf("a") == 0); +} + +static void testSlotMapNegativeTargetClampsToZero() { + SlotMap m; + m.append("a"); m.append("b"); // 0,1 + CHECK(m.reorder("b", -3)); // clamp to 0 -> insert-before a + CHECK(m.slotOf("b") == 0); + CHECK(m.slotOf("a") == 1); +} + +static void testSlotMapResetDenseSkipsDupesAndEmpties() { + SlotMap m; + m.resetDense({"a", "", "b", "a", "c"}); // "" and the second "a" dropped + CHECK((m.orderedIds() == std::vector{"a", "b", "c"})); + CHECK(m.slotOf("a") == 0); + CHECK(m.slotOf("c") == 2); +} + +static void testSlotMapReconcileDropsStaleAppendsNew() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); // 0,1,2 + m.reconcile({"a", "c", "d"}); // b left the index (drop), d is new (append) + CHECK(m.slotOf("a") == 0); // kept at its slot + CHECK(m.slotOf("c") == 2); // kept at its slot (gap where b was) + CHECK(m.slotOf("b") == -1); // stale marker dropped + CHECK(m.slotOf("d") == 3); // appended after the frontier + CHECK(m.idAt(1).empty()); // b's slot stays empty +} + +static void testSlotMapEqualityAndFromEntries() { + SlotMap a; + a.append("x"); a.append("y"); + SlotMap b = SlotMap::fromEntries({{"x", 0}, {"y", 1}}); + CHECK(a == b); + // Defensive repair: duplicate id (first wins), slot conflict (later dropped), + // empty id / negative slot dropped. + SlotMap c = SlotMap::fromEntries({{"x", 0}, {"x", 5}, {"y", 0}, {"", 9}, {"z", -1}, {"w", 2}}); + CHECK(c.slotOf("x") == 0); // first x wins + CHECK(c.slotOf("y") == -1); // slot 0 already taken -> dropped + CHECK(c.slotOf("w") == 2); // valid + CHECK(c.slotOf("z") == -1); // negative slot dropped +} + +// --- serialize: golden byte-literal + round-trip ----------------------------- + +// Pins the exact wire shape (an array of {"id":..,"slot":..} objects, ascending +// slot, no whitespace) so a future format drift is caught here rather than only +// as a downstream bank_book diff. Mirrors the pre-extraction bank_book writer +// byte-for-byte (core/json emit helpers are shared, not reimplemented). +static void testSlotMapSerializeGoldenLiteral() { + SlotMap empty; + CHECK(empty.serialize() == "[]"); + + SlotMap m; + m.append("a"); + m.append("b"); + CHECK(m.serialize() == "[{\"id\":\"a\",\"slot\":0},{\"id\":\"b\",\"slot\":1}]"); +} + +// A local mirror of bank_book's private parseSlots (the "slots" array grammar): +// [{id, slot}, ...]. slot_map.cpp itself only emits — JSON parsing is a consumer +// concern (see slot_map.h) — so the round-trip proof below parses the emitted +// text back into pairs the same way bank_book does, then rebuilds via +// SlotMap::fromEntries and checks equality against the original. +static bool parseSlotsArray(json::Reader& r, std::vector>& out) { + out.clear(); + if (!r.consume('[')) return false; + r.skipWs(); + if (r.consume(']')) return true; // empty array + do { + if (!r.consume('{')) return false; + std::string id; + int slot = 0; + bool haveId = false, haveSlot = false; + do { + std::string k; + if (!r.parseKey(k)) return false; + if (k == "id") { if (!r.parseString(id)) return false; haveId = true; } + else if (k == "slot") { if (!r.parseInt(slot)) return false; haveSlot = true; } + else { if (!r.skipValue()) return false; } + } while (r.consume(',')); + if (!r.consume('}')) return false; + if (!haveId || !haveSlot) return false; + out.emplace_back(std::move(id), slot); + } while (r.consume(',')); + return r.consume(']'); +} + +static void testSlotMapSerializeRoundTrip() { + SlotMap m; + m.append("a"); m.append("b"); m.append("c"); + m.remove("b"); // leave a gap: a@0, c@2 + m.append("d"); // d@3 + + const std::string blob = m.serialize(); + json::Reader r(blob); + std::vector> pairs; + CHECK(parseSlotsArray(r, pairs)); + + SlotMap round = SlotMap::fromEntries(pairs); + CHECK(round == m); + CHECK(round.slotOf("a") == 0); + CHECK(round.idAt(1).empty()); // gap survives the round trip + CHECK(round.slotOf("c") == 2); + CHECK(round.slotOf("d") == 3); +} + +int main() { + testSlotMapDenseAppend(); + testSlotMapRemoveLeavesGap(); + testSlotMapAppendAfterGapGoesToFrontier(); + testSlotMapReorderIntoEmpty(); + testSlotMapReorderOntoOccupiedInsertsAndShifts(); + testSlotMapReorderPreservesInteriorGapAboveTarget(); + testSlotMapReorderUnmappedIsNoOp(); + testSlotMapNegativeTargetClampsToZero(); + testSlotMapResetDenseSkipsDupesAndEmpties(); + testSlotMapReconcileDropsStaleAppendsNew(); + testSlotMapEqualityAndFromEntries(); + testSlotMapSerializeGoldenLiteral(); + testSlotMapSerializeRoundTrip(); + + if (g_fail == 0) { + std::printf("slot_map_tests: all passed\n"); + return 0; + } + std::printf("slot_map_tests: %d failure(s)\n", g_fail); + return 1; +} diff --git a/tests/test_tab_strip.cpp b/tests/test_tab_strip.cpp index ab32613..833cfb5 100644 --- a/tests/test_tab_strip.cpp +++ b/tests/test_tab_strip.cpp @@ -10,13 +10,14 @@ // left/right chevron precedence at the ends, dead space between visible tabs, // outside the band above/below/left/right, half-open boundary pixels). -#include "../src/tab_strip.h" +#include "../src/core/ui/tab_strip.h" #include #include #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_tail_control.cpp b/tests/test_tail_control.cpp index 09d20b0..20e6e27 100644 --- a/tests/test_tail_control.cpp +++ b/tests/test_tail_control.cpp @@ -3,12 +3,13 @@ // (None -> Auto -> Manual -> None), the manual-length clamp to the 8 s cap, and the // toggle label text. The drawing / click hit-testing is DAW-verified in bank_panel. -#include "../src/tail_control.h" +#include "../src/core/capture/tail_control.h" #include #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -135,6 +136,20 @@ static void testRoundTripAuto() { CHECK(back && settingsEqual(*back, s)); } +static void testSerializeByteIdentity() { + // Q-W1 structural-dedupe guard: the core/json-backed writer must emit the + // EXACT bytes the former snprintf writer produced ({"mode":%d,"manualMs":%.17g}) + // and re-serializing a round-tripped setting must be byte-identical — the blob + // lives in the .rpp, so a byte shift would dirty every saved project. + TailSetting s; // None + 2000.0 default + CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000}"); + TailSetting man; man.mode = TailMode::Manual; man.manualMs = 3141.592653589793; + const std::string json = serializeTailSetting(man); + auto back = deserializeTailSetting(json); + CHECK(back.has_value()); + CHECK(back && serializeTailSetting(*back) == json); // stable second round-trip +} + static void testDeserializeEmptyIsDefault() { // An absent/empty stored value (older project) -> nullopt, so the caller falls // back to the default. This is the graceful-old-project path the brief requires. @@ -164,6 +179,7 @@ int main() { testRoundTripNoneDefault(); testRoundTripManualArbitraryMs(); testRoundTripAuto(); + testSerializeByteIdentity(); testDeserializeEmptyIsDefault(); testDeserializeMalformedIsDefault(); diff --git a/tests/test_theme.cpp b/tests/test_theme.cpp index 3885752..545d91c 100644 --- a/tests/test_theme.cpp +++ b/tests/test_theme.cpp @@ -8,7 +8,7 @@ // interaction-state transform behaves, the WCAG math is correct against known anchors, and // the Direction C spectral ramp interpolates its endpoints. -#include "../src/theme.h" +#include "../src/core/ui/theme.h" #include #include @@ -16,6 +16,7 @@ #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_tooltip.cpp b/tests/test_tooltip.cpp index 2d7cf11..fef6765 100644 --- a/tests/test_tooltip.cpp +++ b/tests/test_tooltip.cpp @@ -5,12 +5,13 @@ // match); placement BELOW the anchor centred; horizontal clamp at both client edges; the // bottom-edge flip to ABOVE; the both-clip clamp for a tall tooltip; degenerate inputs -> empty. -#include "../src/tooltip.h" +#include "../src/core/ui/tooltip.h" #include #include using namespace reasampler; +using namespace reasampler::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_trigger_seam.cpp b/tests/test_trigger_seam.cpp index 58cbcb5..702c44d 100644 --- a/tests/test_trigger_seam.cpp +++ b/tests/test_trigger_seam.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::trigger_seam — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::map::trigger_seam — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. // // Covers: triggerPlayLength (zero play length, startFrame set, startFrame past frameCount, @@ -6,11 +6,12 @@ // (zero play length, rounding); round-trip fidelity; the Finding 1 regression (start-point // set — the case that was broken before this module existed). -#include "../src/vst/trigger_seam.h" +#include "../src/core/instrument/map/trigger_seam.h" #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::map; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_velocity_curve.cpp b/tests/test_velocity_curve.cpp index 9baeaab..7e642cc 100644 --- a/tests/test_velocity_curve.cpp +++ b/tests/test_velocity_curve.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::velocity_curve — no VST3, no REAPER, no framework. Same fast +// Standalone tests for reasampler::instrument::engine::velocity_curve — no VST3, no REAPER, no framework. Same fast // assert loop as the sibling pure tests. Assert the S-VIEW-9 velocity->amp transfer curve HARD: // // * eval — flat y=1 default (R10-F1 Option A: EVERY velocity -> 1.0), linear ramp, curved shape @@ -11,12 +11,13 @@ // * fromPoints — the deserialization repair: sorts by X, box-clamps, forces endpoints, and falls // back to flat() for a sub-2-point list. -#include "../src/vst/velocity_curve.h" +#include "../src/core/instrument/engine/velocity_curve.h" #include #include -using namespace reasampler::vst; +using namespace reasampler; +using namespace reasampler::instrument::engine; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_view_mode_model.cpp b/tests/test_view_mode_model.cpp index 94ce056..bb57611 100644 --- a/tests/test_view_mode_model.cpp +++ b/tests/test_view_mode_model.cpp @@ -15,14 +15,15 @@ // (park-while-parked) so untagged leaves return to visible after toggling back; // guards the in-DAW "all leaves hidden after toggling twice" regression. -#include "../src/view_mode_model.h" -#include "../src/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys +#include "../src/core/view/view_mode_model.h" +#include "../src/core/view/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys #include #include #include using namespace reasampler; +using namespace reasampler::view; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ @@ -55,6 +56,20 @@ static int flagValue(const TrackPlan& p, Flag f) { return -999; // sentinel: flag absent } +// Golden byte-literal (Q-W1 follow-up): pins the EXACT serialized bytes for the +// default-seeded model (Arrange + Design, no membership), not just self- +// consistent re-serialization — a format drift that both writer and reader +// agree on would slip past the round-trip tests but not this. The format is +// frozen as-shipped; the literal below is the captured current output. +static void testSerializeGoldenLiteral() { + ViewModeModel vm; + CHECK(vm.serialize() == + "{\"version\":1,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\"," + "\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\"," + "\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[]," + "\"snapshots\":[],\"lanes\":[]}"); +} + // -- 1. N-mode proven -------------------------------------------------------- static void testNModeRegistryAndMembership() { @@ -1786,6 +1801,7 @@ static void testLaneMalformedJson() { } int main() { + testSerializeGoldenLiteral(); testNModeRegistryAndMembership(); testParentDerivationMultiMode(); testParentOwnMembershipVisibility(); diff --git a/tests/test_view_tree.cpp b/tests/test_view_tree.cpp index f52c131..9a7e06e 100644 --- a/tests/test_view_tree.cpp +++ b/tests/test_view_tree.cpp @@ -3,12 +3,13 @@ // one genuinely pure piece: the I_FOLDERDEPTH walk that turns REAPER's linear // track stream into the parent<->child FolderTree the pure model consumes. -#include "../src/view_tree.h" +#include "../src/core/view/view_tree.h" #include #include using namespace reasampler; +using namespace reasampler::view; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_wav_trim.cpp b/tests/test_wav_trim.cpp index a615be2..10b651d 100644 --- a/tests/test_wav_trim.cpp +++ b/tests/test_wav_trim.cpp @@ -7,7 +7,7 @@ // extraction (whole / tail window / clamp / out-of-range); truncate plan (kept #include @@ -15,6 +15,7 @@ #include using namespace reasampler; +using namespace reasampler::capture; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ diff --git a/tests/test_waveform_view.cpp b/tests/test_waveform_view.cpp index 2969cd9..d5cfd60 100644 --- a/tests/test_waveform_view.cpp +++ b/tests/test_waveform_view.cpp @@ -1,4 +1,4 @@ -// Standalone tests for reasampler::vst::waveform_view — no VST3, no REAPER, no framework. +// Standalone tests for reasampler::instrument::ui::waveform_view — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. Assert the S11 waveform surface's // frame<->pixel mapping, marker grab regions, drag-delta frame resolver (with clamps), and // the zero-crossing snap — the geometry + snap that back the draggable start/loop markers. @@ -9,61 +9,62 @@ // no-ops); nearestZeroCrossing (nearest sign-change, sample-on-zero, equidistant-tie-to-lower, // no-crossing keeps target, target clamp, degenerate buffers). -#include "../src/vst/waveform_view.h" +#include "../src/core/instrument/ui/waveform_view.h" #include #include -using namespace reasampler::vst; -using reasampler::AudioSample; +using namespace reasampler; +using namespace reasampler::instrument::ui; +using reasampler::audio::AudioSample; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // A comfortable waveform area: 1000px wide, offset so left != 0 (catches origin bugs). -static Rect wideArea() { return Rect{20, 10, 1020, 90}; } // width 1000 +static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 90); } // width 1000 // --- frameToX / xToFrame ------------------------------------------------------ static void testFrameToXEndpoints() { const Rect a = wideArea(); - CHECK(frameToX(a, 1000, 0) == a.left); // frame 0 -> left edge - CHECK(frameToX(a, 1000, 1000) == a.right); // frameCount -> right edge - CHECK(frameToX(a, 1000, 500) == a.left + 500); // midpoint (1:1 here) + CHECK(frameToX(a, 1000, 0) == a.x); // frame 0 -> left edge + CHECK(frameToX(a, 1000, 1000) == a.right()); // frameCount -> right edge + CHECK(frameToX(a, 1000, 500) == a.x + 500); // midpoint (1:1 here) } static void testFrameToXClampsOutOfRange() { const Rect a = wideArea(); - CHECK(frameToX(a, 1000, -50) == a.left); // below 0 pins left - CHECK(frameToX(a, 1000, 5000) == a.right); // above count pins right + CHECK(frameToX(a, 1000, -50) == a.x); // below 0 pins left + CHECK(frameToX(a, 1000, 5000) == a.right()); // above count pins right } static void testFrameToXDegenerate() { const Rect a = wideArea(); - CHECK(frameToX(a, 0, 100) == a.left); // no frames -> left - const Rect z = Rect{5, 5, 5, 45}; // zero width - CHECK(frameToX(z, 1000, 500) == z.left); + CHECK(frameToX(a, 0, 100) == a.x); // no frames -> left + const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width + CHECK(frameToX(z, 1000, 500) == z.x); } static void testXToFrameInverse() { const Rect a = wideArea(); - CHECK(xToFrame(a, 1000, a.left) == 0); - CHECK(xToFrame(a, 1000, a.right) == 1000); - CHECK(xToFrame(a, 1000, a.left + 250) == 250); // 1:1 map here + CHECK(xToFrame(a, 1000, a.x) == 0); + CHECK(xToFrame(a, 1000, a.right()) == 1000); + CHECK(xToFrame(a, 1000, a.x + 250) == 250); // 1:1 map here } static void testXToFrameClampsOutside() { const Rect a = wideArea(); - CHECK(xToFrame(a, 1000, a.left - 100) == 0); // left of area -> 0 - CHECK(xToFrame(a, 1000, a.right + 100) == 1000); // right of area -> frameCount - CHECK(xToFrame(a, 0, a.left + 10) == 0); // no frames -> 0 + CHECK(xToFrame(a, 1000, a.x - 100) == 0); // left of area -> 0 + CHECK(xToFrame(a, 1000, a.right() + 100) == 1000); // right of area -> frameCount + CHECK(xToFrame(a, 0, a.x + 10) == 0); // no frames -> 0 } static void testFrameToXRoundTrip() { // Round-trip at a non-1:1 scale: 800px area over 2000 frames (2.5 frames/px). frameToX then // xToFrame should land within a couple frames (rounding both directions). - const Rect a = Rect{0, 0, 800, 60}; + const Rect a = Rect::ltrb(0, 0, 800, 60); for (std::int64_t f = 0; f <= 2000; f += 137) { const int x = frameToX(a, 2000, f); const std::int64_t back = xToFrame(a, 2000, x); @@ -77,39 +78,39 @@ static void testMarkerAtPointGrabsWithinBand() { const Rect a = wideArea(); // Markers at frames 100, 500, 900 -> x = left+100, left+500, left+900. const std::int64_t frames[3] = {100, 500, 900}; - const int midY = a.top + a.height() / 2; - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 100, midY) == 0); - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500, midY) == 1); - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 900, midY) == 2); + const int midY = a.y + a.height / 2; + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 100, midY) == 0); + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, midY) == 1); + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 900, midY) == 2); // Within the grab band on either side of the line. - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500 + kMarkerGrabWidth, midY) == 1); - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500 - kMarkerGrabWidth, midY) == 1); + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1); + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1); } static void testMarkerAtPointMissesBetween() { const Rect a = wideArea(); const std::int64_t frames[3] = {100, 500, 900}; - const int midY = a.top + a.height() / 2; + const int midY = a.y + a.height / 2; // Well away from any marker line. - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 300, midY) == -1); + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 300, midY) == -1); // Off the area vertically. - CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500, a.top - 5) == -1); + CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, a.y - 5) == -1); } static void testMarkerAtPointFirstMatchOnOverlap() { const Rect a = wideArea(); // Two markers at the same frame -> first in order wins. const std::int64_t frames[2] = {400, 400}; - const int midY = a.top + a.height() / 2; - CHECK(markerAtPoint(a, 1000, frames, 2, a.left + 400, midY) == 0); + const int midY = a.y + a.height / 2; + CHECK(markerAtPoint(a, 1000, frames, 2, a.x + 400, midY) == 0); } static void testMarkerAtPointRejectsNullEmpty() { const Rect a = wideArea(); - const int midY = a.top + a.height() / 2; - CHECK(markerAtPoint(a, 1000, nullptr, 3, a.left + 100, midY) == -1); + const int midY = a.y + a.height / 2; + CHECK(markerAtPoint(a, 1000, nullptr, 3, a.x + 100, midY) == -1); const std::int64_t frames[1] = {100}; - CHECK(markerAtPoint(a, 1000, frames, 0, a.left + 100, midY) == -1); + CHECK(markerAtPoint(a, 1000, frames, 0, a.x + 100, midY) == -1); } // --- resolveDragFrame --------------------------------------------------------- @@ -130,7 +131,7 @@ static void testResolveDragFrameClamps() { static void testResolveDragFrameRounds() { // 500px area over 1000 frames -> 2 frames/px. A +3px drag -> round(6.0)=6; the rounding is // at the frame centre. Use a scale where a fractional result appears. - const Rect a = Rect{0, 0, 300, 60}; // 1000 frames / 300px = 3.33 frames/px + const Rect a = Rect::ltrb(0, 0, 300, 60); // 1000 frames / 300px = 3.33 frames/px // +3px -> 3*1000/300 = 10.0 -> 10 frames. CHECK(resolveDragFrame(a, 1000, 100, 3) == 110); // +1px -> 1000/300 = 3.33 -> rounds to 3. @@ -138,7 +139,7 @@ static void testResolveDragFrameRounds() { } static void testResolveDragFrameDegenerate() { - const Rect z = Rect{0, 0, 0, 60}; // zero width + const Rect z = Rect::ltrb(0, 0, 0, 60); // zero width CHECK(resolveDragFrame(z, 1000, 300, 100) == 300); // pinned to start const Rect a = wideArea(); CHECK(resolveDragFrame(a, 0, 300, 100) == 0); // no frames -> clamp(start)=0 diff --git a/tests/test_wire.cpp b/tests/test_wire.cpp new file mode 100644 index 0000000..d2420b4 --- /dev/null +++ b/tests/test_wire.cpp @@ -0,0 +1,191 @@ +// Standalone tests for reasampler::wire — no REAPER, no framework. The ONE +// length-prefixed ext-state wire codec (Q-W1, T2-01b) behind provenance / +// assignment_request / sample_usage / bank_sync. The consumers' own suites +// prove their record grammars round-trip; this suite pins the CODEC contract — +// byte-exact encode, the full hardening (length caps, overflow guards, +// subtraction-first bounds), and the fixed fieldInt range rejection. +// +// NOTE: wire::Cursor BORROWS its input string, so every helper takes a named / +// reference-bound std::string — the Cursor never outlives its buffer. + +#include "../src/core/wire/wire.h" + +#include +#include + +using namespace reasampler; +using namespace reasampler::wire; + +static int g_fail = 0; +#define CHECK(cond) do { if(!(cond)) { \ + std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) + +// One length-prefixed field around `v` — the writer-side convention. +static std::string enc(const std::string& v) { + std::string out; + wire::putField(out, v); + return out; +} + +// Single-field decode helpers (Cursor + buffer share the call's lifetime). +static bool fieldFrom(const std::string& s, std::string& out) { + wire::Cursor c(s); + return c.field(out); +} +static bool i64From(const std::string& s, std::int64_t& out) { + wire::Cursor c(s); + return c.fieldInt64(out); +} +static bool intFrom(const std::string& s, int& out) { + wire::Cursor c(s); + return c.fieldInt(out); +} +static bool sizeFrom(const std::string& s, std::size_t& out) { + wire::Cursor c(s); + return c.fieldSizeT(out); +} +static bool dblFrom(const std::string& s, double& out) { + wire::Cursor c(s); + return c.fieldDouble(out); +} + +// --- putField: byte-exact encode ---------------------------------------------- + +static void testPutFieldExactBytes() { + std::string out; + wire::putField(out, "abc"); + CHECK(out == "3:abc"); + wire::putField(out, ""); // empty field is legal: "0:" + CHECK(out == "3:abc0:"); + wire::putField(out, "a:b"); // ':' inside a value cannot shift the parse + CHECK(out == "3:abc0:3:a:b"); +} + +// --- Cursor: round-trip + literal --------------------------------------------- + +static void testFieldRoundTripIncludingSeparators() { + std::string out = "magic"; + wire::putField(out, "12:34"); // digits + colons in the value + wire::putField(out, ""); + wire::putField(out, "tail"); + wire::Cursor c(out); + std::string a, b, t; + CHECK(c.literal("magic")); + CHECK(c.field(a) && a == "12:34"); + CHECK(c.field(b) && b.empty()); + CHECK(c.field(t) && t == "tail"); + CHECK(c.ok() && c.atEnd()); +} + +static void testLiteralMismatchFails() { + const std::string good = "rsprov1x"; + const std::string wrong = "rsprov0x"; + const std::string truncated = "rspro"; + { wire::Cursor c(good); CHECK(c.literal("rsprov1")); } + { wire::Cursor c(wrong); CHECK(!c.literal("rsprov1")); CHECK(!c.ok()); } + { wire::Cursor c(truncated); CHECK(!c.literal("rsprov1")); } +} + +// --- Cursor: field hardening --------------------------------------------------- + +static void testFieldRejectsMalformedLengths() { + std::string f; + CHECK(!fieldFrom("abc", f)); // no colon + CHECK(!fieldFrom(":x", f)); // empty length + CHECK(!fieldFrom("2x:ab", f)); // non-digit length + CHECK(!fieldFrom("9:ab", f)); // runs past end + // A 200-digit length cannot accumulate past SIZE_MAX (digit-run cap). + CHECK(!fieldFrom(std::string(200, '9') + ":x", f)); + // Exactly-20-digit values: SIZE_MAX itself passes the accumulate but fails the + // bounds check; one past SIZE_MAX trips the overflow guard. + CHECK(!fieldFrom("18446744073709551615:x", f)); + CHECK(!fieldFrom("18446744073709551616:x", f)); +} + +static void testFailureLatchesOk() { + // After one failed read every subsequent read fails too — the caller may + // check ok() once at the end (the "never a partial value" discipline). + const std::string s = "3:abc"; + wire::Cursor c(s); + std::string f; + CHECK(!c.literal("nope")); + CHECK(!c.field(f)); + CHECK(!c.ok()); +} + +// --- Cursor: fieldInt64 / fieldInt -------------------------------------------- + +static void testFieldInt64AcceptsAndRejects() { + std::int64_t v = 0; + CHECK(i64From(enc("12345"), v)); CHECK(v == 12345); + CHECK(i64From(enc("-42"), v)); CHECK(v == -42); + CHECK(i64From(enc("9223372036854775807"), v)); // INT64_MAX + CHECK(v == 9223372036854775807LL); + CHECK(!i64From(enc("9223372036854775808"), v)); // overflow + CHECK(!i64From(enc("12345678901234567890"), v)); // 20-digit cap + CHECK(!i64From(enc("-"), v)); // bare sign + CHECK(!i64From(enc("1a"), v)); // non-digit + CHECK(!i64From(enc(""), v)); // empty +} + +static void testFieldIntRejectsOutOfIntRange() { + // The fixed form of the old provenance strtol TODO: an out-of-int-range field + // FAILS the parse instead of silently narrowing. + int v = 0; + CHECK(intFrom(enc("2147483647"), v)); CHECK(v == 2147483647); + CHECK(intFrom(enc("-2147483648"), v)); CHECK(v == -2147483647 - 1); + CHECK(!intFrom(enc("2147483648"), v)); + CHECK(!intFrom(enc("3000000000"), v)); +} + +// --- Cursor: fieldSizeT / fieldDouble ------------------------------------------ + +static void testFieldSizeT() { + std::size_t v = 1; + CHECK(sizeFrom(enc("0"), v)); CHECK(v == 0); + CHECK(sizeFrom(enc("4096"), v)); CHECK(v == 4096); + CHECK(!sizeFrom(enc("-1"), v)); // sign = non-digit + CHECK(!sizeFrom(enc(std::string(21, '9')), v)); // 21-digit cap + CHECK(!sizeFrom(enc("18446744073709551616"), v)); // overflow guard +} + +static void testFieldDoubleRoundTrip() { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.17g", 3141.592653589793); + double v = 0; + CHECK(dblFrom(enc(buf), v)); + CHECK(v == 3141.592653589793); + CHECK(!dblFrom(enc("1.5x"), v)); // trailing bytes +} + +// --- parseUnsignedDecimal (the bank_sync generation core) ----------------------- + +static void testParseUnsignedDecimal() { + std::int64_t v = 0; + CHECK(wire::parseUnsignedDecimal("0", v) && v == 0); + CHECK(wire::parseUnsignedDecimal("1721947293", v) && v == 1721947293); + CHECK(wire::parseUnsignedDecimal("9223372036854775807", v) && v == 9223372036854775807LL); + CHECK(!wire::parseUnsignedDecimal("", v)); + CHECK(!wire::parseUnsignedDecimal("+5", v)); // sign rejected (non-digit) + CHECK(!wire::parseUnsignedDecimal("-5", v)); + CHECK(!wire::parseUnsignedDecimal("12a", v)); + CHECK(!wire::parseUnsignedDecimal("9223372036854775808", v)); // overflow + CHECK(!wire::parseUnsignedDecimal(std::string(40, '9'), v)); // long run cannot wrap +} + +int main() { + testPutFieldExactBytes(); + testFieldRoundTripIncludingSeparators(); + testLiteralMismatchFails(); + testFieldRejectsMalformedLengths(); + testFailureLatchesOk(); + testFieldInt64AcceptsAndRejects(); + testFieldIntRejectsOutOfIntRange(); + testFieldSizeT(); + testFieldDoubleRoundTrip(); + testParseUnsignedDecimal(); + + if (g_fail == 0) std::printf("wire: all tests passed\n"); + else std::printf("wire: %d CHECK(s) FAILED\n", g_fail); + return g_fail == 0 ? 0 : 1; +}