Files
reasampler/src/core/wire/instrument_drop.h
T

115 lines
7.5 KiB
C++

#pragma once
// instrument_drop — the PURE payload-construction core of S17 drop-and-load.
//
// 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 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) --------------------------------
//
// S17 drops a bank capture onto a track's FX surface, which instantiates ReaSampler 9000 on
// that track ALREADY PLAYING that capture. The injection mechanism (S-GA-DropFX revision of
// PLAN.md §S17 mechanism (B)): after TrackFX_AddByName creates the instance, the extension
// writes a Steinberg-format .vstpreset file whose 'Comp' chunk is the instrument's own
// component state (the dragged capture pre-selected) and applies it via
// TrackFX_SetPreset(track, fx, "<absolute path>.vstpreset")
// which the SDK documents as accepting full .vstpreset paths for VST3 plug-ins.
//
// WHY NOT vst_chunk (the S-GA-DropFX diagnosis): TrackFX_SetNamedConfigParm's "vst_chunk"
// is "base64-encoded VST-specific chunk" — for a VST3 that is REAPER's OWN wrapper framing
// of the plugin state (the bytes REAPER round-trips into the RPP <VST block), NOT the raw
// IComponent::setState stream. Writing raw component-state bytes there "succeeds" (the parm
// write returns true) but REAPER's VST3 wrapper cannot apply the unframed blob, so the
// instance silently stayed at defaults — the observed blank-on-drop. The .vstpreset path
// replaces that undocumented framing with a Steinberg-DOCUMENTED container this module can
// construct byte-exactly and prove in a unit test (public.sdk/source/vst/vstpresetfile.cpp
// is the reference reader/writer; layout verified against it).
//
// The component-state bytes inside the preset are still produced by the instrument's OWN
// serializer, sample_map::serializeComponentState (the single source of truth for the byte
// layout — the same function the processor's getState calls), so the cross-artifact
// contract cannot drift: if the instrument's format changes, this module changes with it
// because it CALLS it.
#include <cstdint>
#include <string>
#include <vector>
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 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();
// Build a Steinberg VST3 preset file image (the bytes of a .vstpreset) carrying exactly one
// 'Comp' chunk = `componentState`, addressed to class `classIdHex32` (32 hex chars, see
// vstClassIdHex). Layout per public.sdk/source/vst/vstpresetfile.cpp, all integers
// little-endian on disk:
// [0] 'VST3' — header magic
// [4] int32 version = 1
// [8] 32-char ASCII class ID
// [40] int64 chunk-list offset (= 48 + componentState.size())
// [48] the component-state bytes — the one 'Comp' chunk's data
// then 'List', int32 entry count = 1, then the entry: 'Comp', int64 offset 48, int64 size.
// No 'Cont' chunk is written: the instrument is a SingleComponentEffect whose whole state is
// the component stream; a controller-state chunk is optional in the container format.
// Returns an empty vector when classIdHex32 is not exactly 32 chars (contract violation).
std::vector<std::uint8_t> buildVstPresetBytes(const std::string& classIdHex32,
const std::vector<std::uint8_t>& componentState);
// The drop payload: a .vstpreset image for the channel-active class whose component state is
// the instrument's default face with just `sampleId` picked — {selectionId = sampleId, no
// zones, mono, generation 0}, exactly what a fresh instance would hold after the user
// clicked that capture in the browser. The keymap builds under the product defaults (Gate +
// Preserve) from the bank's own S2 intrinsics, so the sample plays MIDI-triggered
// immediately (the S17 "loaded, selected, playable" verify).
//
// An EMPTY sampleId yields the empty-state preset ({"", no zones}) — a drop of nothing
// selects nothing (the S10 silent empty state); the shell guards against this upstream, but
// the pure contract is defined.
//
// Deterministic: the same sampleId always yields the same bytes.
std::vector<std::uint8_t> buildInstrumentDropPreset(const std::string& sampleId);
// -- FX-drop-target classification (S-VIEW-BUG-1 / S-GA-DropFX) ----------------
//
// Pure classifier for GetThingFromPoint's info string: is the point over a surface where an
// instrument drop should instantiate ReaSampler 9000 on the resolved track? This is string
// logic (no REAPER types), so it lives here and is unit-tested outside the DAW — the shell
// (instrument_drop_win) only supplies the info bytes GetThingFromPoint filled.
//
// The SDK (reaper_plugin_functions.h §GetThingFromPoint) documents "fx_chain"/"fx_N" for
// the FX-chain and floating-FX windows, and "tcp"/"mcp"-prefixed strings with sub-element
// tokens ("tcp.mute" is the doc's example) for track-panel hits — WITH the explicit warning
// that "future versions may append additional information". The FX-button sub-token itself
// is undocumented; the WALTER element family names the TCP/MCP FX surfaces "tcp.fx",
// "tcp.fxbyp", "tcp.fxparm", "tcp.fxembed", "mcp.fxlist", ... — all beginning "tcp.fx" /
// "mcp.fx". So the hotspot rule is PREFIX-based (S-GA-DropFX: the earlier exact-token match
// on "tcp.fx"/"mcp.fx" was too strict for appended info and sibling FX elements):
// * "fx_" prefix — the FX-chain and floating-FX windows
// * "tcp.fx" / "mcp.fx" prefix — the TCP/MCP FX button + sibling FX sub-elements
// EXCEPT "tcp.fxembed" / "mcp.fxembed" — the embed-strip surface where a ReaSampler 9000
// instance draws inside the TCP/MCP. Dropping onto the existing instance's own UI must NOT
// add a second instance; the embed surface is explicitly excluded even though it starts
// with "tcp.fx". All other "tcp.fx*" / "mcp.fx*" tokens (fxbyp, fxparm, fxlist, ...) are
// hotspots — they are FX-chain controls, not a running instance's own surface.
// Bare "tcp"/"mcp" and non-FX sub-elements (e.g. "tcp.mute", "tcp.vol") are NOT hotspots.
// The exact live token over the FX button remains a DAW-only fact — confirm in REAPER (a
// deferred ReaScript around reaper.GetThingFromPoint(reaper.GetMousePosition()) prints it).
bool infoNamesFxHotspot(const std::string& info);
// The raw component-state bytes the preset carries — exposed so the round-trip test can
// decode them back through the instrument's OWN reader (sample_map::deserializeComponentState)
// and assert the capture is selected, proving the preset feeds the instrument exactly what
// its setState expects. Not called by the shell (which uses the .vstpreset image).
std::vector<std::uint8_t> instrumentDropStateBytes(const std::string& sampleId);
} // namespace reasampler::wire