Phase S voices: user-set polyphony, mono stack (retrig|legato), isolated preview card, idle-drain retirement; unity bypass preview-only (v7)

This commit is contained in:
2026-07-27 21:29:30 -04:00
parent d9321eaf3a
commit 885b7f29a2
10 changed files with 1176 additions and 98 deletions
+131
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <cstdint>
#include <cstdio> // snprintf (Phase S voice-count readout)
#include <fstream>
#include <string>
#include <vector>
@@ -63,6 +64,7 @@ constexpr UINT kSyncTimerIntervalMs = 500;
constexpr int kTitleHeight = 26;
constexpr int kHeroWaveformHeight = 150; // the enlarged Sample-face hero (was a 72px strip)
constexpr int kClusterHeight = 52; // root strip + preview + channel toggle
constexpr int kVoiceDeckHeight = 26; // Phase S provisional voice deck band
constexpr int kStripBandHeight = 40; // the keyboard-strip band height (root strip + zone strip)
constexpr int kNavButtonWidth = 62; // Browse / Zone / Back title-band buttons
@@ -161,6 +163,9 @@ void ReaSamplerEditor::refreshFromBank() {
selectedId_ = processor_->selectedSampleId();
map_ = processor_->performanceMap();
channelMode_ = processor_->channelMode();
voiceCount_ = processor_->voiceCount(); // Phase S voice-deck snapshot
voiceMode_ = processor_->voiceMode();
monoTrigger_ = processor_->monoTrigger();
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
// Drop a filter that names a bank no longer present.
if (!activeFilterBankId_.empty()) {
@@ -728,6 +733,7 @@ struct SampleBands {
Rect hero; // the hero waveform + S-VIEW-3 envelope overlay
Rect velCurve; // the S-VIEW-10 velocity-curve editor box (empty when suppressed)
Rect cluster; // root strip + preview-trigger + velocity knob + channel toggle
Rect voice; // Phase S voice deck (PROVISIONAL): voice count + Poly/Mono + Retrig/Legato
Rect control; // the param control strip (Mode / Pitch / AHDSR|Trigger / AD pitch / keyTrack)
};
SampleBands computeSampleBands(int w, int h) {
@@ -759,6 +765,12 @@ SampleBands computeSampleBands(int w, int h) {
const int clusterH = (std::min)(kClusterHeight, (std::max)(0, h - y));
b.cluster = Rect{0, y, w, y + clusterH};
y += clusterH;
// Phase S voice deck: a narrow PROVISIONAL band between the cluster and the param strip
// (voice count stepper + Poly/Mono + Retrig/Legato). The Wave B recompose owns the final
// placement; carving a distinct band keeps the wiring self-contained and easy to relocate.
const int voiceH = (std::min)(kVoiceDeckHeight, (std::max)(0, h - y));
b.voice = Rect{0, y, w, y + voiceH};
y += voiceH;
b.control = Rect{kPad, y, w - kPad, h};
return b;
}
@@ -868,6 +880,43 @@ ChannelToggleRects channelToggleRects(const Rect& area) {
return {mono, stereo};
}
// Phase S voice deck (PROVISIONAL — the Wave B recompose owns the final placement): one row of
// per-instance voice-system controls inside the `voice` band. Left to right: a "Voices" label,
// a [-] step-down button, the count readout, a [+] step-up button, then a two-segment
// [Poly|Mono] toggle, then a two-segment [Retrig|Legato] toggle (live only in Mono). Both draw
// + hit-test derive from this ONE formula so they never drift (the channelToggleRects pattern).
constexpr int kVoiceSegW = 52;
constexpr int kVoiceSegH = 18;
constexpr int kVoiceStepW = 18; // the [-] / [+] stepper buttons
constexpr int kVoiceCountW = 30; // the numeric readout between them
constexpr int kVoiceLabelW = 44; // the "Voices" caption
constexpr int kVoiceGap = 16; // gap between the stepper / toggle groups
struct VoiceDeckRects {
Rect label; // "Voices" caption (decorative)
Rect minus; // step the count down
Rect count; // the numeric readout (decorative)
Rect plus; // step the count up
Rect poly; // VoiceMode::Poly segment
Rect mono; // VoiceMode::Mono segment
Rect retrig; // MonoTrigger::Retrigger segment
Rect legato; // MonoTrigger::Legato segment
};
VoiceDeckRects voiceDeckRects(const Rect& band) {
const int top = band.top + (band.height() - kVoiceSegH) / 2;
const int bot = top + kVoiceSegH;
int x = band.left + kPad;
VoiceDeckRects r;
r.label = Rect{x, top, x + kVoiceLabelW, bot}; x = r.label.right;
r.minus = Rect{x, top, x + kVoiceStepW, bot}; x = r.minus.right + 2;
r.count = Rect{x, top, x + kVoiceCountW, bot}; x = r.count.right + 2;
r.plus = Rect{x, top, x + kVoiceStepW, bot}; x = r.plus.right + kVoiceGap;
r.poly = Rect{x, top, x + kVoiceSegW, bot}; x = r.poly.right;
r.mono = Rect{x, top, x + kVoiceSegW, bot}; x = r.mono.right + kVoiceGap;
r.retrig = Rect{x, top, x + kVoiceSegW, bot}; x = r.retrig.right;
r.legato = Rect{x, top, x + kVoiceSegW, bot};
return r;
}
// Draw the pastel spectral keyboard-strip background (Phase L, L3) — the signature surface.
// Fills each MIDI key column with its spectral hue (spectralColor over note/127), then draws
// faint per-octave hairline ticks for orientation. Shared by the setup face + the Zones strip
@@ -1115,6 +1164,55 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
kitTextCentered(bmp, chan.stereo, "Stereo", Font::Label, isStereo ? Role::BgBase : Role::TextPrimary);
}
// --- Phase S voice deck (PROVISIONAL placement; Wave B owns the final composition) -----
if (bands.voice.height() > 0) {
fillSurface(bmp, toKitBox(bands.voice), Role::BgPanel, InteractionState::Rest);
const VoiceDeckRects vd = voiceDeckRects(bands.voice);
kitTextCentered(bmp, vd.label, "Voices", Font::Micro, Role::TextDim);
// Count stepper: [-] N [+]. The steppers grey out at the range edges (the shared
// pure-core kMin/kMaxVoiceCount — one spelling with the engine + the state bytes).
const bool canDown = voiceCount_ > kMinVoiceCount;
const bool canUp = voiceCount_ < kMaxVoiceCount;
fillSurface(bmp, toKitBox(vd.minus), Role::BgCell,
canDown ? InteractionState::Rest : InteractionState::Disabled);
fillSurface(bmp, toKitBox(vd.plus), Role::BgCell,
canUp ? InteractionState::Rest : InteractionState::Disabled);
kitTextCentered(bmp, vd.minus, "-", Font::Label,
canDown ? Role::TextPrimary : Role::TextDim);
kitTextCentered(bmp, vd.plus, "+", Font::Label,
canUp ? Role::TextPrimary : Role::TextDim);
char countBuf[8];
snprintf(countBuf, sizeof(countBuf), "%d", voiceCount_);
kitTextCentered(bmp, vd.count, countBuf, Font::ValueMono, Role::TextPrimary);
// Poly | Mono voice-mode toggle (the channel-toggle grammar: active segment lit).
const bool isMono = (voiceMode_ == VoiceMode::Mono);
fillSurface(bmp, toKitBox(vd.poly), Role::BgCell,
!isMono ? InteractionState::Active : InteractionState::Rest);
fillSurface(bmp, toKitBox(vd.mono), Role::BgCell,
isMono ? InteractionState::Active : InteractionState::Rest);
kitTextCentered(bmp, vd.poly, "Poly", Font::Label,
!isMono ? Role::BgBase : Role::TextPrimary);
kitTextCentered(bmp, vd.mono, "Mono", Font::Label,
isMono ? Role::BgBase : Role::TextPrimary);
// Retrig | Legato mono-takeover toggle — meaningful only in Mono; drawn Disabled
// (inert) in Poly so the dependency reads at a glance.
const bool isLegato = (monoTrigger_ == MonoTrigger::Legato);
const InteractionState retrigState =
!isMono ? InteractionState::Disabled
: (!isLegato ? InteractionState::Active : InteractionState::Rest);
const InteractionState legatoState =
!isMono ? InteractionState::Disabled
: (isLegato ? InteractionState::Active : InteractionState::Rest);
fillSurface(bmp, toKitBox(vd.retrig), Role::BgCell, retrigState);
fillSurface(bmp, toKitBox(vd.legato), Role::BgCell, legatoState);
kitTextCentered(bmp, vd.retrig, "Retrig", Font::Label,
!isMono ? Role::TextDim
: (!isLegato ? Role::BgBase : Role::TextPrimary));
kitTextCentered(bmp, vd.legato, "Legato", Font::Label,
!isMono ? Role::TextDim
: (isLegato ? Role::BgBase : Role::TextPrimary));
}
// --- The "Modes-and-down" control strip (S-VIEW-2: moved from Zone) --------------------
paintControls(bmp, bands.control, zone);
}
@@ -1873,6 +1971,39 @@ void ReaSamplerEditor::onMouseDown(int x, int y) {
return;
}
// Phase S voice deck (PROVISIONAL). Every edit writes through the processor setter,
// which rebuilds the engine off-thread via the drain-slot swap (ringing tails survive);
// the local snapshot updates in step so the deck repaints without waiting for a sync tick.
if (bands.voice.height() > 0 && contains(bands.voice, x, y)) {
const VoiceDeckRects vd = voiceDeckRects(bands.voice);
if (contains(vd.minus, x, y) && voiceCount_ > kMinVoiceCount) {
voiceCount_ -= 1;
processor_->setVoiceCount(voiceCount_);
invalidate();
} else if (contains(vd.plus, x, y) && voiceCount_ < kMaxVoiceCount) {
voiceCount_ += 1;
processor_->setVoiceCount(voiceCount_);
invalidate();
} else if (contains(vd.poly, x, y)) {
voiceMode_ = VoiceMode::Poly;
processor_->setVoiceMode(VoiceMode::Poly);
invalidate();
} else if (contains(vd.mono, x, y)) {
voiceMode_ = VoiceMode::Mono;
processor_->setVoiceMode(VoiceMode::Mono);
invalidate();
} else if (voiceMode_ == VoiceMode::Mono && contains(vd.retrig, x, y)) {
monoTrigger_ = MonoTrigger::Retrigger; // inert (Disabled) in Poly
processor_->setMonoTrigger(MonoTrigger::Retrigger);
invalidate();
} else if (voiceMode_ == VoiceMode::Mono && contains(vd.legato, x, y)) {
monoTrigger_ = MonoTrigger::Legato;
processor_->setMonoTrigger(MonoTrigger::Legato);
invalidate();
}
return; // the deck band swallows its clicks (no fall-through to the hero/markers)
}
// S-VIEW-10: the velocity-curve editor beside the hero. Every in-box click is an edit
// (grab / Alt-delete / add-at-cursor), so materialize the one-zone site first (the
// mirror of the control strip's ensureSampleZone path).