// 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. #include "reasampler_editor.h" #include #include #include #include #include #include "capture_browser.h" #include "capture_paths.h" // resolveBankFile (shared M4 path resolution) #include "editor_geometry.h" // Rect, contains #include "ext_keys.h" #include "keyboard_strip.h" #include "peaks.h" // computeEnvelope #include "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 "waveform_view.h" // frame<->pixel markers + zero-crossing snap (S11) #ifdef _WIN32 #include // GET_X_LPARAM / GET_Y_LPARAM #include "wdltypes.h" #include "lice/lice.h" #endif using namespace Steinberg; namespace reasampler::vst { namespace { #ifdef _WIN32 constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor"; // The S9/S8 change-detection poll (WM_TIMER on the child window). A low-frequency UI-thread // timer: responsive enough that a recapture/ingest/assign refreshes "within a bounded cadence" // (the S9 verify criterion) yet cheap — three small ext-state reads per tick, coalescing many // bumps between ticks into one reload. 500 ms is a deliberate build-time residual: fast enough // to feel hands-free, slow enough to be free. The id is a per-window SetTimer id (any nonzero). constexpr UINT_PTR kSyncTimerId = 1; constexpr UINT kSyncTimerIntervalMs = 500; // Top-level band metrics (shell arithmetic — the load-bearing card/tab/key/zone geometry // is in capture_browser / keyboard_strip). The title band names the plugin + a live // readout; the toggle band carries the Browser/Zones switch; the setup band (single- // capture face) hosts the keyboard strip + level readout under the browser. constexpr int kTitleHeight = 24; constexpr int kToggleHeight = 22; constexpr int kSetupHeight = 176; // the single-capture setup surface (labels + waveform + strip) constexpr int kStripBandHeight = 40; constexpr int kWaveformHeight = 72; // the S11 waveform band inside the setup surface // Palette — house style, mirrored from bank_panel's dark theme so the instrument reads as // the same tool. (Phase L's L1 kit replaces these flat fills later; not gated on it.) const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255); const LICE_pixel kColTitleBg = LICE_RGBA(20, 20, 22, 255); const LICE_pixel kColCardBg = LICE_RGBA(44, 44, 48, 255); const LICE_pixel kColCardSelBg = LICE_RGBA(48, 72, 64, 255); const LICE_pixel kColCardBorder = LICE_RGBA(70, 70, 76, 255); const LICE_pixel kColCardSelBorder = LICE_RGBA(120, 200, 160, 255); const LICE_pixel kColTabBg = LICE_RGBA(36, 36, 40, 255); const LICE_pixel kColTabActiveBg = LICE_RGBA(58, 96, 84, 255); const LICE_pixel kColThumb = LICE_RGBA(120, 200, 160, 255); const LICE_pixel kColStripBg = LICE_RGBA(36, 36, 40, 255); const LICE_pixel kColStripKey = LICE_RGBA(52, 52, 58, 255); const LICE_pixel kColRootMarker = LICE_RGBA(120, 200, 160, 255); const LICE_pixel kColWaveBg = LICE_RGBA(24, 24, 26, 255); const LICE_pixel kColWaveform = LICE_RGBA(120, 200, 160, 255); const LICE_pixel kColStartMarker = LICE_RGBA(230, 200, 120, 255); // start point (amber) const LICE_pixel kColLoopMarker = LICE_RGBA(120, 170, 230, 255); // loop start/end (blue) const LICE_pixel kColLoopRegion = LICE_RGBA(120, 170, 230, 60); // loop span fill (faint) const LICE_pixel kColZoneBar = LICE_RGBA(58, 96, 84, 255); const LICE_pixel kColZoneBarSel = LICE_RGBA(120, 200, 160, 255); const COLORREF kRgbText = RGB(210, 230, 220); const COLORREF kRgbDim = RGB(140, 150, 146); // ANSI path -- string literals must stay ASCII until the Phase L type kit lands. void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col, UINT fmt = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX) { HDC dc = bmp->getDC(); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, col); RECT gr{r.left, r.top, r.right, r.bottom}; DrawTextA(dc, s, -1, &gr, fmt | DT_END_ELLIPSIS); } void drawTextCentered(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) { drawText(bmp, r, s, col, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); } // A short MIDI-note label ("C4", "F#3") for the root badge. Middle C (60) is C4 (the // common DAW convention REAPER uses). std::string noteLabel(int note) { static const char* kNames[12] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; if (note < 0) note = 0; if (note > 127) note = 127; const int octave = note / 12 - 1; // MIDI 0 = C-1; 60 = C4 return std::string(kNames[note % 12]) + std::to_string(octave); } // Draw a mono peak envelope centered vertically in `r` (mirror of bank_panel::drawThumbnail, // single channel). Each bin is a vertical line from its min to its max about the midline. void drawEnvelope(LICE_IBitmap* bmp, const Rect& r, const Envelope& env) { if (r.width() <= 0 || r.height() <= 0 || env.empty() || env[0].empty()) return; const ChannelEnvelope& ch = env[0]; const int mid = r.top + r.height() / 2; const int halfH = r.height() / 2; const int bins = static_cast(ch.size()); for (int x = 0; x < r.width() && x < bins; ++x) { const MinMax& mm = ch[static_cast(x)]; const int yTop = mid - static_cast(mm.max * halfH); const int yBot = mid - static_cast(mm.min * halfH); LICE_Line(bmp, r.left + x, yTop, r.left + x, yBot, kColThumb, 1.0f, 0, false); } } // A display name for a bank sample id from the snapshotted list ("?" if the id no longer // resolves — e.g. a zone naming a deleted sample). std::string sampleLabel(const std::vector& samples, const std::string& id) { for (const SampleChoice& c : samples) { if (c.id == id) return c.displayName.empty() ? c.id : c.displayName; } return "?"; } // The bin count a card's thumbnail is computed at: the card thumbnail width, so one bin // per horizontal pixel. int thumbBins(const BrowserLayout& layout) { return (std::max)(1, cardThumbnailRect(layout, 0).width()); } #endif } // namespace ReaSamplerEditor::ReaSamplerEditor(ReaSamplerProcessor* processor) : CPluginView(nullptr), processor_(processor) { // Default view size — sized to show a couple of card rows + the setup strip. ViewRect r(0, 0, 560, 400); setRect(r); } void ReaSamplerEditor::refreshFromBank() { // Main/UI thread only — reads the live bank over the bridge (allocates, calls REAPER). thumbCache_.clear(); // a bank edit may have re-captured/removed a sample; drop stale peaks pcmCache_.clear(); // and its decoded PCM (the S11 waveform + snap source) if (!processor_) { samples_.clear(); banks_.clear(); visible_.clear(); selectedId_.clear(); map_.zones.clear(); selectedZone_ = -1; return; } auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); samples_ = banksJson ? listSamples(*banksJson) : std::vector{}; banks_ = banksJson ? listBanks(*banksJson) : std::vector{}; selectedId_ = processor_->selectedSampleId(); map_ = processor_->performanceMap(); channelMode_ = processor_->channelMode(); if (selectedZone_ >= static_cast(map_.zones.size())) selectedZone_ = -1; // Drop a filter that names a bank no longer present. if (!activeFilterBankId_.empty()) { bool found = false; for (const BankChoice& b : banks_) if (b.id == activeFilterBankId_) found = true; if (!found) activeFilterBankId_.clear(); } rebuildVisible(); } void ReaSamplerEditor::rebuildVisible() { visible_.clear(); for (const SampleChoice& s : samples_) { if (activeFilterBankId_.empty() || s.bankId == activeFilterBankId_) visible_.push_back(s); } } #ifdef _WIN32 // Windows-only (the WM_TIMER cadence + invalidate() are the Win32 child-window path). Declared // under the same _WIN32 guard in the header; keep the definition guarded to match (D5 makes // Windows the only build target, but the TU must still compile elsewhere). void ReaSamplerEditor::onSyncTimer() { // UI thread (WM_TIMER). Poll the S9 bank generation + the S8 assignment request via the // processor (off the audio thread — the poll itself never touches process()). NEVER while a // drag is in flight: a reload mid-drag would rebuild the instrument and repaint under the // user's cursor, yanking the edit. The next tick (500 ms) picks up the change after release. if (!processor_) return; if (drag_ != DragKind::kNone) return; // defer past the in-flight edit // An open editor marks THIS instance the focused assignment target (the thundering-herd // policy — only an editor-open instance applies a pending assign; see the handoff). Pass // true so this instance consumes the request; instances with no editor open do not poll at // all (the timer is bound to the child window), so they never contend for the request. const ReaSamplerProcessor::BankSyncResult r = processor_->pollBankSync(/*isFocusedTarget=*/true); // Re-snapshot the editor's own view only when something changed (a reload from a bank // content change, or an applied assignment). refreshFromBank re-reads the bank blob + the // processor's (possibly just-updated) selection/map and drops the stale thumbnail/PCM // caches, then repaints — so the browser + setup surface reflect the new bank hands-free. if (r.reloaded || r.applied) { refreshFromBank(); invalidate(); } } #endif // _WIN32 void ReaSamplerEditor::commitAndReload() { // UI thread only. Publish the edited selection + zones to the processor, then rebuild // the instrument off the audio thread (reloadFromBank bakes them into the live Keymap). if (!processor_) return; processor_->setSelectedSampleId(selectedId_); processor_->setPerformanceMap(map_); processor_->reloadFromBank(); #ifdef _WIN32 invalidate(); #endif } ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t frames) const { SetupMarkers m; // Seed from the bank's S2 intrinsic loop (fact about the file), then let a per-zone override // for the picked id win (the instrument's performance choice, D-B). Read the loop intrinsic // from the live bank blob (the same path selectSample uses); the override lives in map_. if (processor_) { auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); if (banksJson) { if (auto sel = selectSample(*banksJson, selectedId_)) { if (sel->loop.hasLoop) { m.hasLoop = true; m.loopStart = sel->loop.start; m.loopEnd = sel->loop.end; } } } } // The override (loop + start) on a zone for the picked id supersedes the intrinsic. for (const PerformanceZone& z : map_.zones) { if (z.sampleId != selectedId_) continue; if (z.loopOverride) { m.hasLoop = z.loopOverride->hasLoop; m.loopStart = z.loopOverride->start; m.loopEnd = z.loopOverride->end; } if (z.startPoint) m.start = *z.startPoint; break; } // Default an unset loop's end to the sample length so the loop markers have somewhere sane // to sit before the user drags (loopStart stays 0). The "no loop" state is m.hasLoop==false; // the markers are still drawn (drag one to CREATE a loop). if (!m.hasLoop && m.loopEnd == 0) m.loopEnd = frames > 0 ? frames : 0; return m; } void ReaSamplerEditor::upsertPickedOverride(const SetupMarkers& m) { // Find-or-append the zone for selectedId_ and write the loop/start override fields. // The bank intrinsic is NEVER written (read-only bank consumer, D-B). selectedId_ must // be non-empty; callers are responsible for that guard. SampleLoop loop; loop.hasLoop = m.hasLoop; loop.start = m.loopStart; loop.end = m.loopEnd; bool found = false; for (PerformanceZone& z : map_.zones) { if (z.sampleId == selectedId_) { z.loopOverride = loop; z.startPoint = m.start; found = true; break; } } if (!found) { PerformanceZone z; z.sampleId = selectedId_; z.lowNote = 0; z.highNote = 127; z.loopOverride = loop; z.startPoint = m.start; map_.zones.push_back(z); } } void ReaSamplerEditor::commitPickedMarkers(const SetupMarkers& m) { // Materialize the edited markers as a per-zone loop/start override on the picked id (upsert, // mirror of the root-marker path): a full-keyboard zone carrying the override. This plays // identically to the un-zoned single capture (one chromatic zone) and round-trips through // the component state; the zone becomes visible if the user opens the Zones panel. The bank // intrinsic is NEVER written (read-only bank consumer, D-B). if (selectedId_.empty()) return; upsertPickedOverride(m); commitAndReload(); } const std::vector& ReaSamplerEditor::monoPcmFor(const std::string& sampleId) { auto it = pcmCache_.find(sampleId); if (it != pcmCache_.end()) return it->second; // SampleChoice is the browser's metadata projection and does NOT carry the WAV path, so // resolve the path from the live bank blob (selectSample) and decode via the shared WAV // parse — the mirror of the processor's decodeRelative. Every failure path caches an EMPTY // vector so a broken/missing file is not re-decoded on every paint. Keyed by id (width- // independent) — the thumbnail bins this at whatever width, the snap scans it directly. std::string relativePath; std::vector mono; if (processor_) { auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); if (banksJson) { if (auto sel = selectSample(*banksJson, sampleId)) relativePath = sel->relativePath; } 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(); } } const WavLayout layout = parseWavLayout(bytes); if (layout.valid) { std::vector interleaved = extractFloatFrames(bytes, layout, 0, layout.frameCount()); mono = downmixToMono(interleaved, layout.channelCount); } } } auto ins = pcmCache_.emplace(sampleId, std::move(mono)); return ins.first->second; } const Envelope& ReaSamplerEditor::thumbnailFor(const std::string& sampleId, int binCount) { const std::string key = sampleId + "|" + std::to_string(binCount); auto it = thumbCache_.find(key); if (it != thumbCache_.end()) return it->second; // Bin the (cached) decoded mono PCM at the requested width — one decode per id, reused by // every thumbnail width AND the S11 waveform surface + snap. const std::vector& mono = monoPcmFor(sampleId); Envelope env; if (!mono.empty()) { env = computeEnvelope(mono, 1, mono.size(), static_cast((std::max)(1, binCount))); } auto ins = thumbCache_.emplace(key, std::move(env)); return ins.first->second; } ReaSamplerEditor::~ReaSamplerEditor() { #ifdef _WIN32 if (childHwnd_) { DestroyWindow(childHwnd_); childHwnd_ = nullptr; } #endif } tresult PLUGIN_API ReaSamplerEditor::isPlatformTypeSupported(FIDString type) { #ifdef _WIN32 if (type && std::string(type) == kPlatformTypeHWND) return kResultTrue; #endif return kResultFalse; } tresult PLUGIN_API ReaSamplerEditor::canResize() { return kResultTrue; } #ifdef _WIN32 void ReaSamplerEditor::invalidate() { if (childHwnd_) InvalidateRect(childHwnd_, nullptr, FALSE); } void ReaSamplerEditor::attachedToParent() { HWND parent = static_cast(systemWindow); if (!parent) return; HINSTANCE hInst = reinterpret_cast(GetWindowLongPtr(parent, GWLP_HINSTANCE)); if (!hInst) hInst = GetModuleHandle(nullptr); static bool classRegistered = false; if (!classRegistered) { WNDCLASSW wc{}; wc.lpfnWndProc = &ReaSamplerEditor::wndProc; wc.hInstance = hInst; wc.lpszClassName = kChildClassName; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.style = CS_HREDRAW | CS_VREDRAW; RegisterClassW(&wc); classRegistered = true; } refreshFromBank(); const ViewRect& r = getRect(); childHwnd_ = CreateWindowExW(0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0, r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr); if (childHwnd_) { SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast(this)); // Start the S9/S8 change-detection poll (UI thread). Tied to the child window's // lifetime — created here, killed in removedFromParent — so an instance whose editor // is closed does NOT poll (the editor-open-only cadence; see the handoff limitation). SetTimer(childHwnd_, kSyncTimerId, kSyncTimerIntervalMs, nullptr); // Poll ONCE immediately so a pending assignment (an S8 ingest fired while this editor // was closed) or a bank change applies the instant the editor opens, rather than waiting // up to one timer interval. refreshFromBank above already primed the view; this folds in // any pending assign/generation so the just-opened editor shows the assigned capture. onSyncTimer(); } } void ReaSamplerEditor::removedFromParent() { if (childHwnd_) { KillTimer(childHwnd_, kSyncTimerId); // stop the poll before the window goes away DestroyWindow(childHwnd_); childHwnd_ = nullptr; } } tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) { tresult res = CPluginView::onSize(newSize); if (childHwnd_ && newSize) { MoveWindow(childHwnd_, 0, 0, newSize->getWidth(), newSize->getHeight(), TRUE); thumbCache_.clear(); // thumbnails are width-bound; a resize invalidates them } return res; } // The client bands: title (top), toggle (below title), then the mode content. In the // browser view the content is the browser grid on top of the single-capture setup band // (when a capture is picked); in the zones view the content is the zones strip + list. namespace { struct EditorBands { Rect title; Rect toggleBrowser; // left half of the toggle band Rect toggleZones; // right half Rect content; // below the toggle band: the mode's own area }; EditorBands computeBands(int w, int h) { EditorBands b; const int titleH = (std::min)(kTitleHeight, h); b.title = Rect{0, 0, w, titleH}; const int toggleTop = titleH; const int toggleBot = (std::min)(h, toggleTop + kToggleHeight); b.toggleBrowser = Rect{0, toggleTop, w / 2, toggleBot}; b.toggleZones = Rect{w / 2, toggleTop, w, toggleBot}; b.content = Rect{0, toggleBot, w, h}; return b; } // The keyboard strip rectangle inside the setup area (single-capture root-drag face). // `area` is the full setup Rect; the strip is anchored at the bottom with an 8px horizontal // pad. All three call sites (paintSetup, onMouseDown, onMouseMove) use this single formula. Rect setupStripArea(const Rect& area) { constexpr int pad = 8; const int stripTop = area.bottom - kStripBandHeight; return Rect{area.left + pad, stripTop, area.right - pad, area.bottom - 4}; } // The S11 waveform rectangle inside the setup area: a band above the keyboard strip, below the // header/hint labels. `area` is the full setup Rect; the waveform is padded 8px horizontally and // anchored above the strip band. All call sites (paintSetup, onMouseDown, onMouseMove) use this // single formula so the draw and the hit-test never drift. Rect setupWaveformArea(const Rect& area) { constexpr int pad = 8; const int waveBottom = area.bottom - kStripBandHeight - 6; // 6px gap above the strip const int waveTop = waveBottom - kWaveformHeight; return Rect{area.left + pad, waveTop, area.right - pad, waveBottom}; } // The keyboard strip rectangle inside the Zones panel content area. `bands.content` is the // mode-content Rect; the strip sits below the "+ Add Zone" affordance (top+4, height 20) // with a 12px gap, padded 8px horizontally. All three call sites (paintZones, onMouseDown, // onMouseMove) use this single formula — the inline arithmetic in onMouseMove was the drift. Rect zonesStripArea(const EditorBands& bands) { constexpr int pad = 8; const int stripTop = bands.content.top + 4 + 20 + 12; // addR.bottom + 12 return Rect{bands.content.left + pad, stripTop, bands.content.right - pad, stripTop + kStripBandHeight}; } // The S7 mono/stereo toggle, a two-segment control anchored to the RIGHT of the setup band's // header row (same y as the sample-name header, so it reads as "this capture's output mode"). // `area` is the full setup Rect. Returns {mono-segment, stereo-segment}; each is kSegW wide, // kSegH tall, side by side. Kept to a small fenced block (S11 owns the waveform region). constexpr int kChanSegW = 52; constexpr int kChanSegH = 18; struct ChannelToggleRects { Rect mono; Rect stereo; }; ChannelToggleRects channelToggleRects(const Rect& area) { constexpr int pad = 8; const int top = area.top + 4; const int right = area.right - pad; const Rect stereo{right - kChanSegW, top, right, top + kChanSegH}; const Rect mono{stereo.left - kChanSegW, top, stereo.left, top + kChanSegH}; return {mono, stereo}; } } // namespace void ReaSamplerEditor::paint(HDC hdc) { RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; if (w <= 0 || h <= 0) return; LICE_SysBitmap bmp(w, h); LICE_Clear(&bmp, kColBackground); const EditorBands bands = computeBands(w, h); // Title band: product name + live readout. LICE_FillRect(&bmp, bands.title.left, bands.title.top, bands.title.width(), bands.title.height(), kColTitleBg, 1.0f, 0); std::string title = reasampler::vstPluginName(); // channel-derived (S18) if (processor_ && processor_->bridge().isConnected()) { if (samples_.empty()) title += " [bank empty]"; else if (selectedId_.empty() && map_.zones.empty()) title += " [pick a capture]"; else if (!map_.zones.empty()) title += " [" + std::to_string(map_.zones.size()) + " zone(s)]"; else title += " [" + sampleLabel(samples_, selectedId_) + "]"; } else { title += " [host: no bridge]"; } Rect titleText{bands.title.left + 8, bands.title.top, bands.title.right - 8, bands.title.bottom}; drawText(&bmp, titleText, title.c_str(), kRgbText); // Toggle band: Browser | Zones. const bool inZones = (view_ == View::kZones); LICE_FillRect(&bmp, bands.toggleBrowser.left, bands.toggleBrowser.top, bands.toggleBrowser.width(), bands.toggleBrowser.height(), inZones ? kColTabBg : kColTabActiveBg, 1.0f, 0); LICE_FillRect(&bmp, bands.toggleZones.left, bands.toggleZones.top, bands.toggleZones.width(), bands.toggleZones.height(), inZones ? kColTabActiveBg : kColTabBg, 1.0f, 0); drawTextCentered(&bmp, bands.toggleBrowser, "Browser", kRgbText); drawTextCentered(&bmp, bands.toggleZones, "Zones", kRgbText); if (view_ == View::kZones) { paintZones(&bmp, w, h); } else { paintBrowser(&bmp, w, h); } BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY); } void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) { // Shown when no card is drawn (nothing to pick): distinguish a genuinely empty bank from // a bank filter that hides everything. Either way it is the "pick a capture" empty state. const char* msg = samples_.empty() ? "No captures in this project yet - capture audio into the bank to play it here." : "No captures in this bank filter. Choose another bank tab above."; drawTextCentered(bmp, area, msg, kRgbDim); } void ReaSamplerEditor::paintBrowser(LICE_IBitmap* bmp, int w, int h) { const EditorBands bands = computeBands(w, h); // When a capture is picked, the setup band takes the bottom; the browser gets the rest. const bool havePick = !selectedId_.empty(); const int setupTop = havePick ? (std::max)(bands.content.top, bands.content.bottom - kSetupHeight) : bands.content.bottom; const Rect browserArea{bands.content.left, bands.content.top, bands.content.right, setupTop}; // The browser tabs + card grid, laid out by the pure module over the browser sub-area. // capture_browser lays out from (0,0); offset the draw by browserArea's origin. const BrowserLayout bl = layoutBrowser(browserArea.width(), browserArea.height()); const int ox = browserArea.left; const int oy = browserArea.top; // Filter tabs: an "All" tab (index 0) + one per named bank. The active tab highlights. 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}; 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_); LICE_FillRect(bmp, t.left, t.top, t.width(), t.height(), active ? kColTabActiveBg : kColTabBg, 1.0f, 0); drawTextCentered(bmp, t, label.c_str(), kRgbText); } // Cards: one per visible sample. Clip at the browser area bottom (scroll is S12). const int bins = thumbBins(bl); for (int i = 0; i < static_cast(visible_.size()); ++i) { Rect content = cardContentRect(bl, i); if (content.top + oy >= browserArea.bottom) break; // past the visible grid Rect thumb = cardThumbnailRect(bl, i); Rect labelR = cardLabelRect(bl, i); content = Rect{content.left + ox, content.top + oy, content.right + ox, content.bottom + oy}; thumb = Rect{thumb.left + ox, thumb.top + oy, thumb.right + ox, thumb.bottom + oy}; labelR = Rect{labelR.left + ox, labelR.top + oy, labelR.right + ox, labelR.bottom + oy}; const SampleChoice& s = visible_[static_cast(i)]; const bool sel = (s.id == selectedId_); LICE_FillRect(bmp, content.left, content.top, content.width(), content.height(), sel ? kColCardSelBg : kColCardBg, 1.0f, 0); LICE_DrawRect(bmp, content.left, content.top, content.width() - 1, content.height() - 1, sel ? kColCardSelBorder : kColCardBorder, 1.0f, 0); drawEnvelope(bmp, thumb, thumbnailFor(s.id, bins)); // Name + root/key badge under the thumbnail. 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}; drawText(bmp, nameR, caption.c_str(), kRgbText); std::string badge; if (s.rootNote) badge = "root " + noteLabel(*s.rootNote); else if (s.key) badge = *s.key; else badge = "root -"; drawText(bmp, badgeR, badge.c_str(), kRgbDim); } if (havePick) { paintSetup(bmp, Rect{bands.content.left, setupTop, bands.content.right, bands.content.bottom}); } else if (visible_.empty()) { paintEmptyState(bmp, browserArea); } } void ReaSamplerEditor::paintSetup(LICE_IBitmap* bmp, const Rect& area) { // The guided single-capture setup: the picked capture's name + root/level, and a // keyboard strip with its root marker (drag to set root). LICE_FillRect(bmp, area.left, area.top, area.width(), area.height(), kColTitleBg, 1.0f, 0); // Effective root: the picked sample's rootNote intrinsic (or middle C when unset). // Read from samples_ (the full unfiltered list) so a bank-filter that hides the // picked sample's bank doesn't mask its intrinsic root with the C4 default. int root = 60; for (const SampleChoice& s : samples_) { if (s.id == selectedId_ && s.rootNote) root = *s.rootNote; } // If a matching one-zone override exists (opt-in from Zones), prefer it as the shown root. for (const PerformanceZone& z : map_.zones) { if (z.sampleId == selectedId_ && z.rootOverride) root = *z.rootOverride; } const int pad = 8; // The mono/stereo toggle sits at the right of the header row; keep the name text clear of it. const ChannelToggleRects chan = channelToggleRects(area); Rect headerR{area.left + pad, area.top + 4, chan.mono.left - 8, area.top + 22}; std::string header = sampleLabel(samples_, selectedId_) + " root " + noteLabel(root); drawText(bmp, headerR, header.c_str(), kRgbText); // S7 mono | stereo output-mode toggle. The active segment highlights (kColTabActiveBg), // the inactive is kColTabBg — the same visual grammar as the Browser/Zones toggle. const bool isStereo = (channelMode_ == ChannelMode::Stereo); LICE_FillRect(bmp, chan.mono.left, chan.mono.top, chan.mono.width(), chan.mono.height(), isStereo ? kColTabBg : kColTabActiveBg, 1.0f, 0); LICE_FillRect(bmp, chan.stereo.left, chan.stereo.top, chan.stereo.width(), chan.stereo.height(), isStereo ? kColTabActiveBg : kColTabBg, 1.0f, 0); drawTextCentered(bmp, chan.mono, "Mono", kRgbText); drawTextCentered(bmp, chan.stereo, "Stereo", kRgbText); Rect hintR{area.left + pad, headerR.bottom, area.right - pad, headerR.bottom + 16}; drawText(bmp, hintR, "Drag the waveform markers to set start + loop; drag the keyboard to set root.", kRgbDim); // --- S11 waveform surface: the picked capture's envelope + draggable markers ---------- const std::vector& pcm = monoPcmFor(selectedId_); const std::int64_t frames = static_cast(pcm.size()); const Rect waveArea = setupWaveformArea(area); LICE_FillRect(bmp, waveArea.left, waveArea.top, waveArea.width(), waveArea.height(), kColWaveBg, 1.0f, 0); if (frames > 0 && waveArea.width() > 0) { // Envelope at one bin per pixel (full-res view of the decoded PCM, S10 read-only view // reused). computeEnvelope over the cached mono frames — no new decode. const int bins = (std::max)(1, waveArea.width()); const Envelope env = computeEnvelope(pcm, 1, pcm.size(), static_cast(bins)); drawEnvelope(bmp, waveArea, env); // reuses the thumbnail envelope draw (kColThumb) const SetupMarkers m = pickedMarkers(frames); // Faint loop-region fill between the loop markers (only when a loop is set). if (m.hasLoop && m.loopEnd > m.loopStart) { 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(), kColLoopRegion, 1.0f, 0); } } // The three markers: start (amber), loop start + loop end (blue). Drawn as 2px vertical // lines the full waveform height. Loop markers dim when no loop is set (the "no loop" // state — draggable to CREATE one). const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd}; const LICE_pixel markerCols[3] = {kColStartMarker, kColLoopMarker, kColLoopMarker}; for (int i = 0; i < 3; ++i) { 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(), markerCols[i], alpha, 0); } } else { drawTextCentered(bmp, waveArea, "(decoding...)", kRgbDim); } // Keyboard strip with the root marker. const Rect stripArea = setupStripArea(area); const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); const int sx = stripArea.left; const int sy = stripArea.top; LICE_FillRect(bmp, stripArea.left, stripArea.top, stripArea.width(), stripArea.height(), kColStripBg, 1.0f, 0); // Faint per-octave key ticks for orientation. for (int n = 0; n <= 127; n += 12) { Rect k = keyRect(sl, n); LICE_Line(bmp, k.left + sx, sy, k.left + sx, sy + stripArea.height(), kColStripKey, 1.0f, 0, false); } Rect marker = rootMarkerRect(sl, root); LICE_FillRect(bmp, marker.left + sx, sy, (std::max)(2, marker.width()), stripArea.height(), kColRootMarker, 1.0f, 0); } void ReaSamplerEditor::paintZones(LICE_IBitmap* bmp, int w, int h) { const EditorBands bands = computeBands(w, h); const int pad = 8; // 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{bands.content.left + pad, bands.content.top + 4, bands.content.left + pad + 96, bands.content.top + 4 + 20}; LICE_FillRect(bmp, addR.left, addR.top, addR.width(), addR.height(), kColCardBg, 1.0f, 0); LICE_DrawRect(bmp, addR.left, addR.top, addR.width() - 1, addR.height() - 1, kColCardSelBorder, 1.0f, 0); drawTextCentered(bmp, addR, "+ Add Zone", kRgbText); Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom}; if (selectedZone_ >= 0) { LICE_FillRect(bmp, delR.left, delR.top, delR.width(), delR.height(), kColCardBg, 1.0f, 0); LICE_DrawRect(bmp, delR.left, delR.top, delR.width() - 1, delR.height() - 1, kColCardSelBorder, 1.0f, 0); drawTextCentered(bmp, delR, "Delete", kRgbText); } // The zones strip. const Rect stripArea = zonesStripArea(bands); const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); const int sx = stripArea.left; const int sy = stripArea.top; LICE_FillRect(bmp, stripArea.left, stripArea.top, stripArea.width(), stripArea.height(), kColStripBg, 1.0f, 0); for (int n = 0; n <= 127; n += 12) { Rect k = keyRect(sl, n); LICE_Line(bmp, k.left + sx, sy, k.left + sx, sy + stripArea.height(), kColStripKey, 1.0f, 0, false); } 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 bool sel = (i == selectedZone_); LICE_FillRect(bmp, bar.left + sx, sy, (std::max)(2, bar.width()), stripArea.height(), sel ? kColZoneBarSel : kColZoneBar, sel ? 1.0f : 0.7f, 0); } // A one-line legend of the selected zone below the strip. Rect infoR{stripArea.left, stripArea.bottom + 8, stripArea.right, stripArea.bottom + 26}; if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { const PerformanceZone& z = map_.zones[static_cast(selectedZone_)]; std::string info = sampleLabel(samples_, z.sampleId) + " " + noteLabel(z.lowNote) + " - " + noteLabel(z.highNote) + " root " + (z.rootOverride ? noteLabel(*z.rootOverride) + "*" : std::string("(bank)")); drawText(bmp, infoR, info.c_str(), kRgbText); } else if (map_.zones.empty()) { drawText(bmp, infoR, "No zones. Add Zone maps the picked capture across the keyboard.", kRgbDim); } } // --- Input: the drag-state machine ------------------------------------------- void ReaSamplerEditor::onMouseDown(int x, int y) { if (!processor_) return; RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; const EditorBands bands = computeBands(w, h); // Toggle band: switch views. if (contains(bands.toggleBrowser, x, y)) { view_ = View::kBrowser; invalidate(); return; } if (contains(bands.toggleZones, x, y)) { view_ = View::kZones; invalidate(); return; } if (view_ == View::kBrowser) { const bool havePick = !selectedId_.empty(); const int setupTop = havePick ? (std::max)(bands.content.top, bands.content.bottom - kSetupHeight) : bands.content.bottom; const Rect browserArea{bands.content.left, bands.content.top, bands.content.right, setupTop}; const BrowserLayout bl = layoutBrowser(browserArea.width(), browserArea.height()); const int bx = x - browserArea.left; const int by = y - browserArea.top; // Filter tabs. const int tabCount = static_cast(banks_.size()) + 1; const int tab = filterTabHitTest(bl, tabCount, bx, by); if (tab >= 0) { activeFilterBankId_ = (tab == 0) ? std::string() : banks_[static_cast(tab - 1)].id; rebuildVisible(); invalidate(); return; } // Cards: pick a capture -> load it (this is the whole time-to-first-note gesture). const int card = cardHitTest(bl, static_cast(visible_.size()), bx, by); if (card >= 0) { selectedId_ = visible_[static_cast(card)].id; commitAndReload(); // publishes the pick + reloads; process() plays it repitched return; } // The setup band: the mono/stereo toggle (header row), the S11 waveform markers, // then the root-marker strip. if (havePick) { const Rect area{bands.content.left, setupTop, bands.content.right, bands.content.bottom}; // S7: a click on a channel-mode segment sets the instance mode (setChannelMode // re-negotiates the bus + reloads; a no-op set for the already-active mode is ignored // by the processor). Snapshot the new mode locally so the paint reflects it at once. const ChannelToggleRects chan = channelToggleRects(area); if (contains(chan.mono, x, y)) { channelMode_ = ChannelMode::Mono; processor_->setChannelMode(ChannelMode::Mono); invalidate(); return; } if (contains(chan.stereo, x, y)) { channelMode_ = ChannelMode::Stereo; processor_->setChannelMode(ChannelMode::Stereo); invalidate(); return; } // S11 waveform markers: grab start / loop-start / loop-end to drag. Hit-test the // waveform band first (it sits above the keyboard strip). markerAtPoint resolves // which marker under the grab; a miss falls through to the keyboard strip. const std::vector& pcm = monoPcmFor(selectedId_); const std::int64_t frames = static_cast(pcm.size()); if (frames > 0) { const Rect waveArea = setupWaveformArea(area); const SetupMarkers m = pickedMarkers(frames); const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd}; const int hit = markerAtPoint(waveArea, frames, markerFrames, 3, x, y); if (hit >= 0) { drag_ = DragKind::kWaveMarker; waveMarker_ = static_cast(hit); dragStartX_ = x; dragStartMarkers_ = m; dragSampleFrames_ = frames; dragStartMap_ = map_; return; // no immediate set — the marker only moves once the cursor drags } } // The setup strip: grab the root marker (drag to set the picked capture's root). const Rect stripArea = setupStripArea(area); const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); const int note = keyAtPoint(sl, x - stripArea.left, y - stripArea.top); if (note >= 0) { drag_ = DragKind::kRootMarker; dragStartX_ = x; dragStartRoot_ = note; dragStartMap_ = map_; // A click sets the root immediately (drag then refines); the override lives on // a one-zone map entry for the picked capture (D-B, never written to the bank). onMouseMove(x, y); // apply the click position as the first delta==0 set return; } } return; } // Zones view. const int pad = 8; Rect addR{bands.content.left + pad, bands.content.top + 4, bands.content.left + pad + 96, bands.content.top + 4 + 20}; if (contains(addR, x, y)) { // Add a full-keyboard 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 // already exists, select it rather than appending a duplicate (mirrors the upsert the // root-marker drag path already performs, preventing overlapping identical zones). std::string seed = !selectedId_.empty() ? selectedId_ : (!visible_.empty() ? visible_.front().id : std::string()); if (seed.empty()) return; for (int i = 0; i < static_cast(map_.zones.size()); ++i) { const PerformanceZone& z = map_.zones[static_cast(i)]; if (z.sampleId == seed && z.lowNote == 0 && z.highNote == 127) { selectedZone_ = i; invalidate(); return; } } PerformanceZone z; z.sampleId = seed; z.lowNote = 0; z.highNote = 127; map_.zones.push_back(z); selectedZone_ = static_cast(map_.zones.size()) - 1; commitAndReload(); return; } Rect delR{addR.right + 8, addR.top, addR.right + 8 + 64, addR.bottom}; if (selectedZone_ >= 0 && contains(delR, x, y)) { map_.zones.erase(map_.zones.begin() + selectedZone_); selectedZone_ = -1; commitAndReload(); return; } // 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(bands); const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); const int lx = x - stripArea.left; const int ly = y - stripArea.top; std::vector lows, highs; lows.reserve(map_.zones.size()); highs.reserve(map_.zones.size()); for (const PerformanceZone& z : map_.zones) { lows.push_back(z.lowNote); highs.push_back(z.highNote); } const ZoneBarHit hit = zoneBarAtPoint(sl, lows.empty() ? nullptr : lows.data(), highs.empty() ? nullptr : highs.data(), static_cast(map_.zones.size()), lx, ly); if (hit.zoneIndex >= 0) { selectedZone_ = hit.zoneIndex; const PerformanceZone& z = map_.zones[static_cast(hit.zoneIndex)]; dragStartX_ = x; dragStartLow_ = z.lowNote; dragStartHigh_ = z.highNote; dragStartMap_ = map_; switch (hit.grab) { case ZoneGrab::kLowEdge: drag_ = DragKind::kZoneLow; break; case ZoneGrab::kHighEdge: drag_ = DragKind::kZoneHigh; break; case ZoneGrab::kBody: drag_ = DragKind::kZoneBody; break; default: drag_ = DragKind::kNone; break; } invalidate(); return; } // A bare key-click inside the strip sets the selected zone's root override. if (contains(stripArea, x, y) && selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { const int note = keyAtPoint(sl, lx, ly); if (note >= 0) { map_.zones[static_cast(selectedZone_)].rootOverride = note; commitAndReload(); } } } void ReaSamplerEditor::onMouseMove(int x, int y) { if (drag_ == DragKind::kNone) return; RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; const EditorBands bands = computeBands(w, h); const int dx = x - dragStartX_; if (drag_ == DragKind::kRootMarker) { // The single-capture root strip lives in the setup band. const int setupTop = (std::max)(bands.content.top, bands.content.bottom - kSetupHeight); const Rect area{bands.content.left, setupTop, bands.content.right, bands.content.bottom}; const Rect stripArea = setupStripArea(area); const StripLayout sl = layoutStrip(stripArea.width(), stripArea.height()); const int note = resolveDragNote(sl, dragStartRoot_, dx); // The performance map is the ONLY D-B override vehicle (rootOverride lives on a zone), // so setting the single capture's root materializes a full-keyboard zone carrying the // override. This plays identically to the un-zoned single-capture path (one chromatic // zone over the whole keyboard) and round-trips through the v3 component state; the // zone becomes visible if the user opens the Zones panel. Upsert by the picked id so a // repeated drag edits the same zone rather than stacking duplicates. bool found = false; for (PerformanceZone& z : map_.zones) { if (z.sampleId == selectedId_) { z.rootOverride = note; found = true; break; } } if (!found) { PerformanceZone z; z.sampleId = selectedId_; z.lowNote = 0; z.highNote = 127; z.rootOverride = note; map_.zones.push_back(z); } invalidate(); // live feedback; the commit lands on WM_LBUTTONUP return; } if (drag_ == DragKind::kWaveMarker) { // S11: resolve the grabbed marker's new frame from the pixel delta, zero-crossing-snap // it against the decoded PCM, apply the inter-marker clamps, and write the override live. const int setupTop = (std::max)(bands.content.top, bands.content.bottom - kSetupHeight); const Rect area{bands.content.left, setupTop, bands.content.right, bands.content.bottom}; const Rect waveArea = setupWaveformArea(area); const std::int64_t frames = dragSampleFrames_; if (frames <= 0) return; // Grabbed frame at grab time, from the snapshot (so the delta is measured from grab). const int idx = static_cast(waveMarker_); const std::int64_t startVals[3] = {dragStartMarkers_.start, dragStartMarkers_.loopStart, dragStartMarkers_.loopEnd}; std::int64_t newFrame = resolveDragFrame(waveArea, frames, startVals[idx], dx); // Snap to the nearest zero crossing in the decoded PCM (the S2 zero-crossing-aware // requirement). Pure over the cached mono frames — no host types, no file I/O. const std::vector& pcm = monoPcmFor(selectedId_); if (!pcm.empty()) { newFrame = nearestZeroCrossing(pcm.data(), static_cast(pcm.size()), newFrame); } // Build the edited marker set from the snapshot, moving only the grabbed marker, then // clamp: loopStart <= loopEnd, start in [0, frames-1]. Dragging a loop marker MAKES a loop. SetupMarkers m = dragStartMarkers_; if (waveMarker_ == WaveMarker::kStart) { m.start = newFrame; } else if (waveMarker_ == WaveMarker::kLoopStart) { m.loopStart = (std::min)(newFrame, m.loopEnd); m.hasLoop = true; } else { // kLoopEnd m.loopEnd = (std::max)(newFrame, m.loopStart); m.hasLoop = true; } if (m.start < 0) m.start = 0; if (m.start > frames - 1) m.start = frames - 1; // Upsert the override on the picked id (mirror of the root-marker path); commit lands on // release, this is live feedback. upsertPickedOverride(m); invalidate(); return; } // Zone edits: recompute the grabbed field(s) against the pure resolver, live. if (selectedZone_ < 0 || selectedZone_ >= static_cast(map_.zones.size())) return; const Rect stripArea = zonesStripArea(bands); 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); } else if (drag_ == DragKind::kZoneHigh) { z.highNote = (std::max)(resolveDragNote(sl, dragStartHigh_, dx), z.lowNote); } else if (drag_ == DragKind::kZoneBody) { // Move the whole span: apply the SAME delta to both edges so the span is preserved, // clamping so neither edge escapes [0,127] (the span shifts, never shrinks). const int newLow = resolveDragNote(sl, dragStartLow_, dx); const int newHigh = resolveDragNote(sl, dragStartHigh_, dx); const int span = dragStartHigh_ - dragStartLow_; if (newLow < 0) { z.lowNote = 0; z.highNote = span; } else if (newHigh > 127) { z.highNote = 127; z.lowNote = 127 - span; } else { z.lowNote = newLow; z.highNote = newHigh; } } invalidate(); } void ReaSamplerEditor::onMouseUp(int /*x*/, int /*y*/) { if (drag_ == DragKind::kNone) return; drag_ = DragKind::kNone; // One coherent edit lands on release: publish the in-flight map + reload off-thread. commitAndReload(); } LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { auto* self = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); switch (msg) { case WM_PAINT: { PAINTSTRUCT ps{}; HDC hdc = BeginPaint(hwnd, &ps); if (self) self->paint(hdc); EndPaint(hwnd, &ps); return 0; } case WM_LBUTTONDOWN: if (self) { SetCapture(hwnd); // keep receiving moves/up if the cursor leaves the child self->onMouseDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); } return 0; case WM_MOUSEMOVE: if (self) self->onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_LBUTTONUP: if (self) { self->onMouseUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); ReleaseCapture(); } return 0; case WM_CAPTURECHANGED: // Capture stolen mid-drag (modal dialog, alt-tab, etc.) — restore map_ to its // pre-grab snapshot so the in-flight live-drag mutation is rolled back, then reset // the drag state machine so stale capture-less WM_MOUSEMOVEs don't keep editing. // Mirror of bank_panel.cpp's WM_CAPTURECHANGED handler. if (self && self->drag_ != DragKind::kNone) { self->map_ = self->dragStartMap_; self->drag_ = DragKind::kNone; self->invalidate(); } return 0; case WM_TIMER: if (self && wParam == kSyncTimerId) self->onSyncTimer(); return 0; case WM_ERASEBKGND: return 1; // fully repaint in WM_PAINT; skip the flicker-inducing erase default: return DefWindowProcW(hwnd, msg, wParam, lParam); } } #else // non-Windows: not a build target (D5), but keep the TU compilable. void ReaSamplerEditor::attachedToParent() {} void ReaSamplerEditor::removedFromParent() {} tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) { return CPluginView::onSize(newSize); } #endif // _WIN32 } // namespace reasampler::vst