#pragma once // insert_plan — the REAPER-free logic behind the `insert` shell (M6): computing // the InsertMedia `mode` bitmask from a small options struct. // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // vendor/ includes. Standard library only. The one genuinely testable-outside-DAW // piece of insert is the mode-bit arithmetic — the InsertMedia bitfield is easy to // get wrong and its bits are load-bearing for the "no silent time-stretch" // invariant, so it is factored here and unit-tested. The REAPER-bound placement // (InsertMedia call, edit-cursor movement, undo block) lives in insert.cpp and is // DAW-verified. // // The bit meanings below are transcribed VERBATIM from the authoritative header // doc-comment (vendor/reaper-sdk/sdk/reaper_plugin_functions.h, InsertMedia): // mode: 0=add to current track, 1=add new track, 3=add to selected items as // takes, &4=stretch/loop to fit time sel, &8=try to match tempo 1x, // &16=try to match tempo 0.5x, &32=try to match tempo 2x, // &64=don't preserve pitch when matching tempo, ... // We intentionally use only the base target (0/1) and the tempo-conform bits // (&8/&16/&32/&64). We NEVER set &4 (stretch/loop to fit time selection) — that is // the silent-time-stretch path the tool forbids (CONTEXT.md §Non-goals). #include namespace reasampler { // 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 // later concern (YAGNI). Both insert AT THE EDIT CURSOR — that is REAPER's // convention for base modes 0/1 (the header names no explicit edit-cursor bit; // see the flagged runtime assumption in insert.cpp). enum class InsertTarget { NewTrack, // mode base 1: add a new track for the item CurrentTrack, // mode base 0: add to the current/selected track }; // Tempo-conform choice. Default is None: insert at the file's native length with // NO stretching (the precision-preserving default). The three ratios are the // explicit opt-in "try to match project tempo" paths — never applied silently. // Ratio1x is the ordinary "conform to tempo"; Half/Double are the octave-shifted // variants REAPER exposes for half/double-time material. enum class TempoConform { None, // no tempo bits set: native length, no stretch (default) Ratio1x, // &8: try to match tempo 1x RatioHalf,// &16: try to match tempo 0.5x RatioDouble,// &32: try to match tempo 2x }; // Options that shape one InsertMedia call. Defaults encode the intended path: // current track (user's selection), no conform, pitch preserved. struct InsertOptions { InsertTarget target = InsertTarget::CurrentTrack; TempoConform conform = TempoConform::None; // Only meaningful when conform != None. When false, adds &64 ("don't preserve // pitch when matching tempo") so a tempo match also shifts pitch (classic // varispeed). Default true = preserve pitch across the tempo match. Ignored // when conform == None (no tempo bits set, so pitch is moot). bool preservePitch = true; }; // Computes the InsertMedia `mode` integer for the given options. // // Guarantees enforced here (and asserted in tests): // * The &4 stretch-to-time-selection bit is NEVER set (no silent stretch). // * When conform == None, NONE of the tempo bits (&8/&16/&32/&64) are set — the // item lands at native length. // * Exactly one base target bit pattern is used (0 or 1), never 3. int computeInsertMode(const InsertOptions& opts); // The forbidden stretch bit, exposed so a test can assert it is never present in // any computed mode (the "no silent time-stretch" invariant, made checkable). inline constexpr int kStretchToTimeSelBit = 4; } // namespace reasampler