Skip to content
DakaGoaPublic

About

GoaSynth

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

GoaSynth — Goa Trance VST3 Synthesizer

Version 1.6.0. Numbering: every functional change bumps the minor (1.2 → 1.3 → …); the major stays 1 until the owner explicitly says otherwise, and updates to this website or repository never change it — only VST changes do. The header's MENU dropdown carries an About card (version, license state, machine ID), Check for updates — which compares the running build against docs/version.json on the product site and always answers with a dialog, even when the feed is unreachable — it shows what changed, when it shipped and how big the download is, and when a new version is available its download link fetches the new installer straight from the release (GoaSynth-Setup-<version>.exe, named on the row), a View full change log link opens the release notes (deep-linked to the new version when one is available) — and Change Log, which opens the release notes on the product site.

The GoaSynth mark: the sine swoosh in the UV GOA palette

A polyphonic virtual-analog synthesizer plugin (VST3) designed for Goa trance production: TB-303-style acid bass, supersaw leads, hoover stabs, FM bleeps, PWM pads — with tempo-synced ping-pong delay, chorus, phaser and reverb built in.

The GoaSynth site → — the full feature tour, screenshots, install steps and the 24-hour full trial. €15 one-time for Windows, macOS and Linux, activated offline. The site lives in docs/ in this repository and is published from it by GitHub Pages.

Scale quantizer & microtuning

The arp sequencer (and, with LOCK, live playing) snaps into one of nine scales — MINOR, PHRYGIAN, HARM MIN, HUNG MIN, DBL HARM, DORIAN, MAJOR, PENTA MIN — against a selectable root (ROOT C..B; SCALE and LOCK live next to the voicing controls). CHROMATIC = off.

Microtuning: SCL (bottom right) loads a Scala .scl file; the tuning applies to every voice and persists across sessions (a copy is kept in %APPDATA%\GoaSynth\tuning.scl). Click SCL again to return to 12-TET, and FINE trims ±100 cents (432 Hz ≈ −32).

Master quality (2x oversampling)

QUALITY (bottom right, default off) renders every voice's oscillator + filter chain twice per output sample at half spacing and averages the two — classic 2x oversampling. Waveform edges and resonant sweeps alias roughly an octave less; CPU roughly doubles, which is why it's a switch, not a constant. Filters, DC blockers, the drive/feedback path and the vowel bank each keep independent per-parity state so the handoff between modes is click-free, and cutoff smoothing runs in the log domain (rate-normalised), so the filter character is identical at 44.1k and 96k. All 20 factory LEAD and 19 RISER patches enable QUALITY (they benefit most from the cleaner highs); every other category stays at the fast default path.

Sidechain pump, analog character, filter drive & zoom

  • PUMP / DEPTH (bottom right): host-synced sidechain-style volume dip — pick the beat division (1/4 … 1/16 T) and depth; the synth ducks on the beat and recovers exponentially, no DAW sidechain routing needed.
  • ANALOG: tape-style wow (0.4 Hz) + flutter (5.3 Hz) plus a slow random per-voice drift for wobbly old-machine character.
  • F-DRIVE / F-FB (FILTER panel): pre-filter saturation and a one-sample resonant feedback path — screeching Goa leads at high settings, with a hard safety clamp so it can never run away.
  • FILL (arp strip): writes 3–6 in-scale steps into rests, leaving your programmed pattern untouched — instant psy rolls in the current SCALE/ROOT.
  • Zoom (bottom right button, or Ctrl + Plus / Minus, or Ctrl + scroll): 100–200% interface scaling in 25% steps, remembered in %APPDATA%\GoaSynth\zoom.txt.

Vowel / formant filter

The FILTER B panel hosts a morphing formant bank (AH → EH → EE → OH → OO): VOWEL enables it, MORPH walks the vowel sequence, V-RES sharpens the formant peaks, V-MIX blends dry/filtered. LFO target VOWEL moves the morph for talking-lead and forest-style textures; AI patches can use all of it (vowelOn, vowelMorph, arpScale, tuningFine...).

Built with JUCE 8 (fetched automatically by CMake). The interface follows a Serum-style dark workflow with six switchable skins (the THEME button): UV GOA (violet / teal / magenta over purple-black — the house look), STEEL (studio blue-greys with a restrained cyan-steel primary and amber secondary), WARM (analog rack: cream face, orange/olive/rust accents), NEON (cyberpunk cyan / hot magenta / neon yellow on deep black), PAPER (warm off-white with cobalt, emerald and amber accents — a light mode for daytime sessions) and OLED (pure black with a single red accent for maximum contrast and battery life). The choice is stored with the session and restored on reopen. Behind the panels an animated psychedelic swirl counter-rotates at ~30 fps — two pre-rendered log-spiral layers in the UV palette — and scale-pulses subtly with the smoothed output level, so the room breathes with the music. The surface is three panel rows — oscillators, filters and sub; envelopes and LFOs; then the FX row — over the gate, arp and keyboard strips, with waveform and filter response displays, draggable ADSR envelopes, animated LFO scopes, a preset browser with previous/next arrows, and a built-in octave-shifted keyboard.

The AI patch designer re-rolls every time: the same prompt always yields a different but musically-related patch (waveform flavours, octave placement, unison/detune rolls, filter offset, pan spread, delay division, fresh gate/arp grooves and humanised envelope jitter), with a variant suffix (II, NEO, PRIME...) in the title so generations are easy to tell apart in the browser. Save the ones you like.

The overlay's AI ENGINE button switches between the offline designer and real LLM-backed generation:

  • LOCAL (default) — the built-in offline designer, no key or network.
  • GEMINI — Google Gemini (gemini-3.6-flash default): paste an API key from aistudio.google.com into the key box.
  • OPENAI — OpenAI chat completions (gpt-5.6-luna default: the cheap, fast tier; CUSTOM covers other OpenAI-compatible providers).
  • CUSTOM — base URL of an OpenAI-compatible server (Ollama http://localhost:11434/v1, LM Studio, OpenRouter...). No key needed for local servers; leave the box as your URL.

The MODEL button next to the key box cycles the newest models per engine (Gemini: 3.6 Flash → 3.5 Flash → 3.5 Flash-Lite; OpenAI: 5.6 Luna → 5 Mini → 4o Mini legacy; Custom: the server's default). The choice is remembered in ai.txt, and hand-typed model ids are honoured too — paste one into the file's third line if a provider ships something newer than this build knows.

TEST KEY (next to MODEL) sends a tiny live request so you can confirm the key and model actually work before spending a full generation. It reports OK • <model> accepted the key and answered on success, or the provider's own error (401 key rejected, 429 rate limit, unreachable host...) on failure.

Keys are stored per-user in %APPDATA%\GoaSynth\ai.txt via SAVE KEY and sent only to the provider you chose. Cloud replies run through the same hardened sanitizer as the offline designer — unknown ids, out-of-range values and bogus choice names are dropped or clamped before anything touches the synth. When a reply fails validation, the plugin automatically sends ONE correction follow-up — the model sees its own rejected JSON plus exactly what was dropped or clamped — and the retry usually comes back clean; only a second failure surfaces an error. The offline designer remains the automatic fallback when the network or the provider fails.

LOCAL keeps learning. Every successful cloud patch is archived with its brief in %APPDATA%\GoaSynth\Learned. Offline generations blend in the best-matching archived entries (brief/title word match), so the offline designer gradually inherits the character of the cloud models you actually used — even months later with no network and no key. The bank is capped at the 150 most recent patches and stores no credentials or raw replies.

A VARIATION selector in the overlay scales how far re-rolls stray: SUBTLE keeps the family's core choices and lays classic grooves (3-on/1-off gate, 1/16 minor roller) with half-strength jitter; NORMAL is the default free re-roll; WILD doubles the jitter, widens the filter/FX ranges and may redesign waves, octaves and groove density wholesale.

Sound engine

  • 2 oscillators (A/B) per voice: Saw / Square / PWM / Triangle / Sine, or a drawable 8-frame wavetable per oscillator, each with octave (+/-2), fine detune (+/-50 cents), level, pan, start phase (+ phase randomization); oscillator B can phase-modulate oscillator A (FM). PULSE W sets the PWM duty cycle directly (both oscillators share it, so a two-oscillator PWM patch stays width-coherent) and is a mod destination, so an LFO can sweep the width without spending the LFO's own PWM target row. Two inter-oscillator modes sit alongside FM: SYNC (hard sync — OSC A's phase is reset by every OSC B cycle, the classic metallic sweep) and RING (blends the product of the two oscillators into OSC A, 0 = off, 1 = pure ring).
  • Wavetable morphing: a WT POS knob sweeps through the drawn frames with linear frame interpolation; both LFOs offer "WT POS A/B" targets for evolving spectral movement. Frame editing: pick a frame in the strip under each display, sketch it (left-drag draw, right-drag erase, double-click reset); the other frames show as ghost outlines and the display shows the live morphed output. Tables are band-limited per frame (partial phases preserved, raised-cosine band fade) so hand-drawn shapes don't alias up the keyboard.
  • Wavetable edit tools: each OSC display has a one-click tool row (SMOOTH blur, FLIP polarity, NORM re-level, and the P25 / P50 / FORMANT / SPIKES shape generators) that applies to the selected frame; transforms remove DC and normalize, so even scratchy doodles play at full level. The row ends with WAV: pick an audio file and it is sliced into the oscillator's eight frames, each resampled to 256 points and peak-normalised, and the oscillator switches to its User wave so the import is audible immediately. Dropping a .wav onto the OSC A or OSC B panel does the same thing (onto B for B, anywhere else for A).
  • Sub oscillator (square / sine / triangle, 0-2 octaves below the note) and white noise.
  • Unison up to 7 detuned voices around both oscillators with stereo spread — classic supersaw/trance lead. UNI / DETUNE / WIDTH in the OSC panels and DRIFT / DETUNE / WIDTH in the MOVEMENT panel edit the same global stack, and the SUB panel's CHAR cell reshapes the detune/pan field itself (CLASSIC linear spread, PHASED centre bunching, HYPER widened outer voices).
  • Chord memory: the SUB panel's CHORD cell makes every key also sound scale-snapped intervals (5TH / MINOR / MAJOR / OCT) in POLY voicing. Notes are reference counted, so a key that is both held and sounded as another key's chord tone only stops when the last claim on it is released.
  • Sound quality: saw/square/PWM are polyBLEP band-limited, the sub square too; wavetables are mip-mapped with raised-cosine band fades so nothing aliases up the keyboard. Cutoff smoothing is log-domain (identical glide character at 44.1k or 192k), a DC blocker after the drive stage keeps heavy saturation from thumping the limiter, and the tempo delay feeds back through a 5 kHz lowpass for analog-style darkening repeats.
  • Analog drift — slow random pitch wander per voice.
  • Filter: 12dB/24dB lowpass, highpass, bandpass, notch (TPT state-variable), resonance, drive, key tracking, filter envelope (+/- 5 octaves).
  • Filter B + routing: a second independent filter (same type list, its own cutoff/reso) with a ROUTE selector — SERIAL (B follows A; the 24 dB LP keeps the classic cascade), PARALLEL (A and B summed 50/50 — e.g. LP+HP = band limiter), and SPLIT (A filtered to the left channel, B to the right, for wide stereo filter movement).
  • Mod matrix (MOD button): 8 freely routable slots with A/B routing banks (edit bank B from the card header, flip the live bank with the BANK B switch in the bottom bar), a per-slot curve (LIN/EXP/SIN) and lag (slew) cell. Sources: LFO 1/2, filter env, amp env, velocity, modwheel, S&H (block-rate random), aftertouch, and MACRO A/B (also MIDI CC 14 / CC 15 out of the box). Destinations: cutoff (±4 oct), FILTER B cutoff and resonance, osc fine tune (±1200 ct), osc levels, wavetable positions, reso, drive, pulse width, vowel morph, analog character, osc pan, LFO rates, and the shared FX (delay time ±2 oct, feedback, mix, phaser mix, reverb size/mix, OTT depth, sidechain pump depth — the deepest voice wins). Click a cell to cycle values (right-click goes backwards), click the amount bar to jump the amount straight to that point, drag it vertically (shift-drag for fine changes), double-click amount/src/dst to reset it (0 / OFF); negative amounts reverse the response. Faster assigning: click a row's DST cell and then click any knob in the synth UI to route it (same DST again or ESC cancels; non-routable knobs are refused), or a SRC cell and an LFO scope, ENV graph or MACRO knob.
  • 2 ADSR envelopes (amp + filter), velocity-sensitive, with draggable graphs.
  • 2 LFOs (sine/triangle/square/random S&H) routable to Pitch, Cutoff, PWM width or Volume, with animated displays; free HZ or host-tempo BPM sync (1/1 ... 1/16, 3/16).
  • Voicing: POLY (1-16 voice limit with stealing) / MONO (retriggering) / LEGATO (slide) — full 303 behaviour with glide and pitch bend, mod wheel -> cutoff, sustain pedal support.
  • FX chain: chorus -> phaser -> OTT multiband compressor -> tempo-synced ping-pong delay (Off / 1/16 / 1/8D / 1/8 / 1/4 or free ms) -> reverb -> master gain -> master 3-band EQ -> limiter. The OTT splits the signal at 220 Hz / 2 kHz (4th-order crossovers) and applies the classic fast upward+downward squeeze to each band — DEPTH blends from transparent to full psy squeeze, per-band LOW / MID / HIGH trim the response, OUT is an output pad. Depth 0 is a true DSP bypass. The reverb grows a SHIM (octave-up pitch-shifted tail) and the delay + reverb duck under the dry signal with DUCK, so leads stay clear while the tails swell in the gaps. The master EQ (bottom-right, next to the pump/analog row) is a low shelf at 200 Hz, a peaking mid with its own frequency and a high shelf at 4 kHz, all ±15 dB — it sits after MASTER and before the limiter, and an all-0 dB setting is a true bypass (no filtering at all).
  • Trancegate: 16-step volume pattern with 1/32 ... 1/4 step lengths (incl. dotted/triplet), sample-accurate and phase-locked to the host transport bar — start playback mid-pattern and the gate is already on the right step. Click or drag on the strip to toggle steps; right-click cycles the division, the second cell sets chop depth. A PATTERN cell launches classic trancegate shapes — UPLIFT (3-on/1-off), OFF-BEAT house, ROLLER 8ths, SLOWROCK half-time, 16THS, OFF+4TH swell — cycling with left/right click; hand edits flip it back to MANUAL. The SHAPE cell sets the edge contour for every step: SQUARE (the hard gate), SMOOTH (half-cosine rise/fall), SAW (instant-on, falling ramp) and TRIANGLE (sine swell) — the whole pattern hard-chops or swoons together. All steps on = bypass. The COPY cell mirrors the arp strip's active/rest pattern onto the gate in one click, so a programmed arp run gets an identically gated trance chop.
  • Arp sequencer: a second 16-step strip plays the held note as a programmed semitone run (each step: Rest or +0...+11), with 0-3 octave range and the same sync divisions — classic Goa rolling arp lines. Each step has a velocity bar (drag it up/down; right-click toggles a full-velocity accent) that the synth plays back exactly, for pumping off-beat dynamics, plus a gate band (drag left/right) that sets the step's note length from 5% to 100% of the step. The playhead cell lights up while the transport runs. The DIR cell cycles playback direction: UP, DOWN, UP-DOWN (ping-pong), RANDOM (fresh roll every step), CONVERGE (outer pair inward, 0-15-1-14-...) and STRUM (every held note fires at each boundary, staggered ~18 ms bottom-up — a strummed chord, with the step's semitone transposing the whole chord). The arp also emits the notes it plays to the host's MIDI output, so a generated line can be recorded to a track or fed to another instrument; only the arp's own events are sent (the incoming notes are never echoed back).

Patch workflow

Five buttons sit in the bottom bar beside the octave keys:

  • RAND — randomises the patch. The draw is musically biased rather than uniform (envelope times favour the short end, wet amounts stay moderate, drive and resonance stay clear of the extremes, cutoff stays mid-forward) because a uniform value in every cell produces 5-second attacks and full-wet reverb. Structural parameters are left alone: the mod matrix, the gate/arp step patterns, master gain, fine tune, voicing, polyphony, bend range, quality and the phase-randomisation switches. Booleans get a coin flip.
  • UNDO / REDO — a 32-step stack of whole-patch snapshots. Pushed by preset loads, randomise, A/B swaps and knob/selector gestures (one snapshot per gesture, taken at drag start). It deliberately does not try to undo host automation ramps: those are the host's business, and pushing on every parameter change would fill the stack within seconds.
  • A / B — two slots holding two versions of the patch. The first press puts both slots on the current sound and moves you to B (nothing changes audibly); edit, then press again to flip. The button label shows the live slot.
  • LOCK — lock mode. Click any knob to protect it: locked knobs are skipped by RAND and by preset loads, and show a small filled mark in their top-left corner. A click in lock mode toggles the lock instead of moving the value, so the knob does not jump. Press LOCK again to leave lock mode.

The header carries a stereo output meter next to the MASTER knob: true per-channel peaks (green to about -6 dBFS, amber to -1, red above) with DAW-style peak-hold ticks that freeze for about a second and then fall, so transient levels are readable against fast programme material.

The noise oscillator offers white or pink generation (pink is the natural -3 dB/oct tilt of cymbals, air and wind) with a decorrelated stereo image, the trancegate renders every step edge click-free — a ~1 ms slew on the hard SQUARE shape, half-cosine, falling-ramp and sine contours on SMOOTH, SAW and TRIANGLE — while staying sample-accurate on the grid, and the arp sequencer accents any step whose velocity is pulled to 100% (louder + a longer gate, TB-303 style).

Host integration

  • Program list: the host's own patch menu lists the whole factory bank — program 0 is Init Patch (all defaults) and programs 1..175 are the factory patches in bank order, so a DAW can browse and automate the bank without opening the plugin's browser. Loading a program and clicking a preset in the browser produce the same sound (both reset to defaults, then apply the patch). Program names are read-only (the bank is compiled in).
  • MIDI output: the plugin declares a MIDI output and emits the arp's generated notes on it.
  • Formats: VST3 on Windows, macOS and Linux, plus AU / AUv3 on macOS (Logic) and LV2 on Linux. CLAP is not available in the JUCE 8.0.8 release this project pins, so it is not built; adding it would need a newer JUCE or a third-party wrapper.

UI polish and micro-interactions

  • Knob hover glow: a soft radial halo appears under each rotary when the pointer is over it.
  • Panel lift: hovering a module panel brightens its top edge, adding depth without breaking the flat aesthetic.
  • Filter response glow: the FILTER graph draws the magnitude curve with a soft glow stroke behind the clean line.
  • Active step glow: the gate/arp playhead step gets a bright halo + crown highlight so you can follow the rhythm at a glance.
  • Breathing logo: the GOA header logo slowly pulses its glow at ~4 s period.
  • Grain texture: a subtle film-grain dot pattern is painted over the background gradient, adding organic texture to the vector surface.
  • Rounded corners: panel corner radius increased from 3 px to 6 px for a softer, more modern feel.

Factory presets

175 patches in 9 categories — 20 each of ACID (303-family squelch), BASS (rollers, subs, growls), LEAD (supersaws, hoovers, screeches) and PAD (swirls, drones, choirs), and 19 each of FM (bells, metallics, vocal forms), FX (sweeps, noise, zaps), PLUCK (trance fingers, kotos, glass), RISER (builds, sweeps, noise jets) and SYNTH (organs, EPs, strings, clavs). Each preset carries its category as a tag, so the browser's TAG filter doubles as a category menu — pick "ACID" or "PAD" to see just that family. Names are prefixed by category (ACID .../ BASS ...), which also makes search work as a category filter.

Preset browser

The header shows the current patch name plus < / > arrows, and BROWSE opens a Serum-style overlay: folder tabs (ALL / FACTORY / USER plus a favourites star tab), a live search box that matches names and tags, a TAG filter built from every tag in the bank, a SORT menu (A-Z / Z-A / NEWEST / BANK ORDER) and a live "shown of total" count. The list is grouped (FACTORY / USER PATCHES), zebra-striped, stars favourites, shows up to three tags per entry and a teal SHARED badge on machine-wide patches. Click a row to audition it with the browser still open; double-click or Return loads and closes; ESC closes without rolling back the patch you already auditioned. Click a tag to filter by it, right-click a row for load / rename / duplicate / delete / reveal. DEL removes the selected user preset; factory patches are never touched.

User presets

SAVE stores the complete current patch — every parameter plus both drawn user waves — as a .goapreset file in %APPDATA%/GoaSynth/Presets. The save dialog takes space-separated tags (e.g. "acid bass night") and a SHARED toggle: shared patches land in C:\Users\Public\Documents\GoaSynth and are visible to every Windows account on the machine (a same-named preset in your own bank shadows the shared copy). Saved patches appear under the USER PATCHES section, load into any session, and AI-generated patches are just a SAVE click away from becoming permanent. In the browser, presets from the shared bank wear a teal SHARED badge so you can always tell which bank a patch lives in (and whether DEL will touch other users' sounds).

Preset packs

The preset browser has EXPORT PACK and IMPORT PACK buttons for moving whole banks in and out of the plugin. A pack is a .goapack file — a plain ZIP containing .goapreset files — so it's easy to email, torrent or archive.

  • EXPORT writes every patch in the merged bank (your own presets, plus shared-bank patches and their tags) into a single pack. A native overwrite prompt protects existing files.
  • IMPORT merges a pack into your user bank: existing presets are skipped (the pack can't silently clobber what you have), new ones land in the browser immediately, and junk entries (READMEs, __MACOSX folders) are ignored. Path-flattening on import means hostile or nested zip entries can never write outside the presets folder.
  • Packs round-trip: export → send to another machine/account → import → play.

Website

The landing/documentation site lives in docs/ as plain static files — no build step, no dependencies, no external requests. It is published at https://dakagoa.github.io/GoaSynth/. The site is branded DakaGoaAudio. Its homepage at docs/index.html shows only the two plugin overviews. Clicking a plugin title, screenshot or Explore button opens the full page: docs/goasynth/index.html for GoaSynth, or docs/gacidbase/index.html for G-AcidBase. Both product pages link back to the homepage. Existing root release-note/install fragments are forwarded to GoaSynth's page; the two version feeds keep their existing paths.

Open docs/index.html directly, or point any static host (GitHub Pages, Netlify, Cloudflare Pages) at the docs folder. docs/.nojekyll is already there for GitHub Pages.

It covers the whole plugin — engine, wavetable drawing, filters and routing (including F-DRIVE / F-FB and the vowel bank), envelopes/LFOs, the mod matrix, the FX chain and OTT, trancegate and arp (with the scale quantiser and the FILL helper), the tuning section (nine scales, LOCK, Scala .scl microtuning), pump / analog character / QUALITY, the AI patch designer (the offline LOCAL engine, the optional cloud engines, the correction round and the learned memory), preset browser/packs, the three skins, full specifications, per-OS install steps, the activation and 24-hour trial flow (machine ID → serial → .goalicense), the pricing and licence position, and an FAQ — plus an interactive Play section (the trancegate demo, synthesised in the browser with the Web Audio API, no samples) — and embeds five screenshots in docs/assets/.

The plugin is sold: one personal licence for €15, one-time, with the source staying public under the AGPLv3. The site therefore has a Pricing section and Buy buttons everywhere, and every price in the copy is stamped from CONFIG.price, so that is a one-line edit — but the legal pages carry the figure as plain text (Terms §3 and the EULA), so change those too. DocsCheck (below) fails when they disagree.

Generated G-AcidBase update feed

docs/gacidbase/version.json and the Change Log section of docs/gacidbase/index.html are generated, not hand-maintained. Every released changelog section gets a stable gacidbase/#vX.Y.Z anchor, newest first. The plugin's update dialog links the offered version; TOOLS → CHANGE LOG opens #changelog. Unreleased entries are excluded from both outputs; HTML notes are escaped before rendering. Its five-field schema and existing URL are unchanged, so installed plugins need no rebuild.

Sources (both are outside the published docs/ folder):

# After publishing the corresponding release and adding its changelog section:
python tools/make-gacidbase-feed.py --refresh --tag vX.Y.Z
# Regenerate offline from the saved release record:
python tools/make-gacidbase-feed.py
# CI/local read-only drift check (no network):
python tools/make-gacidbase-feed.py --check
python -m unittest discover -s tests -p test_gacidbase_feed.py -v

The newest released changelog version must match the GitHub tag. Missing/duplicate notes, invalid dates or tags, draft/prerelease releases, and missing/invalid ZIP sizes fail before writes. --tag is an optional exact-match safeguard, not a way to select an old release. The date comes from the changelog; size is derived from release asset bytes using the existing MiB-rounded-as-MB presentation. Commit the changelog, refreshed release record, and generated feed and generated product page together. Refreshing also updates the size after an asset is replaced under the same tag.

The G-AcidBase update feed workflow and CTest's GAcidBaseFeedCheck/GAcidBaseFeedTest reject hand edits. Offline checks compare with the saved release snapshot; they do not claim to detect a newer remote release or an asset replacement until you run --refresh. This workflow only checks: it never commits, pushes, publishes, or builds either plugin. The plugin's existing packaging gate still checks the live feed against the binary version.

Configure the links in the CONFIG block at the top of docs/app.js:

Key What it is
price Displayed price, e.g. '€15' — written into every <span data-price>
store Store name, used in the "handled by …" sentence, e.g. 'Lemon Squeezy'
vatIncluded true for tax-inclusive store pricing, false to advertise "VAT added at checkout"
checkout Hosted checkout URL from your merchant of record
ordersEmail Fallback while the store is offline: Buy buttons become a pre-filled order email
support Support address; falls back to ordersEmail
download Customer download page, sent in the purchase receipt
repo Public source repository; the Source/Repository links hide themselves while it is empty (instead of falling back to the current page)
machineIdField true when checkout collects the buyer's machine ID (the serial is signed against it); set false to hide the note on the pricing card

The page runs in exactly one of three modes — live store, order-by-email, or neither — and only ever promises the one that is configured, so it can never advertise a checkout that doesn't exist. Leaving repo empty on GitHub Pages still derives github.com/<user>/<repo> from the Pages URL. Clicking the three skin cards repaints the whole site (UV GOA / STEEL / WARM), the same skins the plugin ships with.

Because the plugin is activated with a machine-bound serial, the site's checkout copy, install steps, spec table and FAQ all describe that flow: install the trial build, copy the machine ID from the activation screen, give it at checkout, then double-click the .goalicense file that comes back. The order-by-email fallback pre-fills that request. Fulfil/CHECKOUT.md (§3 and §3b) has the fulfilment workflow, including the keypair backup that has no recovery if you skip it.

Setting up the store itself — merchant of record, tax handling, the product fields, the delivery ZIP and the pre-launch test pass — is documented in Fulfil/CHECKOUT.md. That file sits outside docs/ on purpose: everything under docs/ is served by GitHub Pages, and the playbook is seller-side notes (fees, key handling, the refund procedure), not a page for visitors.

Being found: robots, sitemap and one canonical address

The site ships the files a search engine looks for, so a page becomes crawlable without anyone remembering to submit it:

  • docs/robots.txt allows the whole tree and names the sitemap — there is no private area, no admin route and nothing to exclude.
  • docs/sitemap.xml lists every published page.
  • Every page declares a <link rel="canonical"> (the landing page and the four legal pages), and the landing page carries Open Graph / Twitter cards plus a SoftwareApplication JSON-LD block, so a link preview and a rich result read the price, the platforms and the preset count from data rather than guessing them from prose.

Neither file is written by hand. The pages say where they live — each page's canonical link — and the tool reads that and writes the other two:

python tools/make-site-index.py           # rewrite robots.txt and sitemap.xml
python tools/make-site-index.py --check   # report whether they have drifted (exit 1 if so)

It reads every .html file under docs/, refuses a page with no canonical link (or with two, or with a relative one), refuses two pages claiming different origins, and writes the sitemap from the canonicals that remain — so a new page is picked up by being a page, and there is no second list to keep in step. Search-engine verification files (google*.html) are recognised as tokens and skipped. The output is deterministic, so re-running it over an unchanged tree is a no-op.

DocsCheck checks the same three files at test time, which is the backstop: it compares every page's canonical address against the sitemap and robots.txt, fails when a published page is missing from the sitemap, and fails when the sitemap lists a URL that nothing serves. So a forgotten page fails the build, and this tool is the one-command fix.

It also re-derives the absolute URLs in each page's head: og:url becomes that page's own canonical, and og:image / twitter:image are re-rooted at the site root — including social cards, which is the one place a stale host stayed invisible until somebody shared a link and got a broken image. The default screenshot's path lives in SITE_IMAGE at the top of the tool. Each product page keeps its own assets/ screenshot path while its host is re-rooted.

The site remains at https://dakagoa.github.io/GoaSynth/ — the GitHub Pages address of this repository — and the pages are the only place that address is written down. So moving to a custom domain is: edit the canonical links, run the tool, commit. DocsCheck fails if a page is left behind.

Each product page's SoftwareApplication JSON-LD is re-rooted the same way, and it needs no list of paths: the block names its own site URL in the WebSite node's url, so the tool replaces that prefix wherever the block uses it — the @id fragments, the offer URL, the app URL, the image and the three screenshots. The block is parsed before and after the rewrite, so a rewrite can never leave invalid structured data behind. Screenshot paths stay in the page, where a person reads them, and adding a fourth screenshot is a page edit rather than a tool edit.

One piece of structured data is deliberately absent: there is no FAQPage block for the FAQ section. Google retired FAQ rich results — its FAQPage documentation now redirects to the Search changelog, which records the removal on 15 June 2026 ("the FAQ rich result feature is no longer shown in Google Search results") — so the markup would show nothing in Google while duplicating fifteen answers in a second place to keep in step. The FAQ stays visible prose, which still serves readers and AI answer engines. Add it only if something that reads it starts paying off; don't add it expecting a rich result.

One file under docs/ is deliberately not a page: docs/googlebe8101dec58dcb76.html is Google Search Console's proof that the site is ours. It has to be served from the site root, carries no canonical and belongs in no sitemap, so DocsCheck recognises google*.html as a token and exempts it from the rules above — and from nothing else, so any other new page still needs a canonical link and a sitemap entry. Its contents, though, are pinned rather than derived: it is the one fact under docs/ that no file in this repository decides, so DocsCheck holds it to the exact bytes it was issued with. The failure it guards is invisible — Google fetches the file and looks for its own name inside it, so a re-save that adds a trailing newline, writes a UTF-8 byte order mark or "corrects" a quote still answers 200 while the console quietly stops recognising the site. DocsCheck therefore fails on a byte that differs (showing the bytes, escaped, and both lengths), on a token that does not name its own file, and on the file being deleted at all. If the property is ever re-created, land the new token file and update the pin beside it (and the name on PublishCheck's allowlist, which every served file must be on).

Once deployed, submit the site once in Google Search Console and Bing Webmaster Tools — the Google token above is already live, so verification is a click. A sitemap can only be discovered from robots.txt after a crawler has been to the site at least once, and the first crawl of a new site can take days — so submit it rather than waiting. There is also nothing to hide: if a /private/ or /orders/ folder is ever served from docs/, add a Disallow to robots.txt and a note there saying why, because access control is not what a robots file does.

Buyer reviews, and the star rating they earn

Stars in a search result need an aggregateRating, and Google's review snippet guidelines are narrow about what may back one: the reviews have to be visible on the page that carries the markup, the count has to be the reviews a reader can see, and reviews cannot be aggregated in from another website. So the reviews are collected from buyers directly and published to the site by one tool:

python tools/make-reviews.py           # write the reviews section and the aggregateRating
python tools/make-reviews.py --check   # fail if the page and the store disagree
  • The store is Fulfil/reviews.json, gitignored — it holds buyers' names, order references and email addresses, and this repository is public. Only a review the buyer agreed to have quoted, in writing, is ever published.
  • tools/make-reviews.py writes both halves: a reviews section between <!-- reviews:begin --> and <!-- reviews:end -->, and the aggregateRating inside the SoftwareApplication JSON-LD. One run, so they cannot disagree.
  • Withdrawal works: set consent to false and the section and the rating come back out, leaving docs/goasynth/index.html byte-identical to what it was before the review was published.
  • DocsCheck fails the suite when the two halves drift apart — a count larger than the reviews on the page, a rating the visible reviews do not average to, an incentivized review that does not disclose it, or stars with no reviews under them at all.

Fulfil/REVIEWS.md has the capture process: when to ask, the email to send, what to record, why a bad review gets published too, and the guidelines this is built around. A review written in exchange for a free copy or a discount has to say so, clearly and prominently — since 24 July 2026 that is a guideline rather than good manners.

Before you publish: the preflight check

Because docs/ is the website, a file dropped in there is public the moment you deploy — which is how a stray order export, a fulfilment manifest or a pasted keypair ends up on the internet. Run this before you push, or let it run with the suite:

./build/Release/PublishCheck           # or: ctest -R PublishCheck

It fails on three things: a file in docs/ that is not on its allowlist (the allowlist is the list of things meant to be public, so a new page has to be added deliberately); seller-side or secret material inside an allowed page (keypair and ledger filenames, the generated key header, a real signed serial, API-key shapes, an auth token, someone's own C:\Users\<name> profile path — while the plugin's documented C:\Users\Public\... bank is fine); and a served page linking to something outside docs/, which would 404 or point at the rest of the repository. Failures print the file and line number. PublishCheckTest proves each of those cases is actually caught, and that a fresh copy of the real site passes.

Before you publish: the drift guard

PublishCheck guards leaks. Nothing guarded accuracy — and the site has already gone false once: the activation gate and the optional cloud AI engines both landed in Source/ after the pages were written, leaving about twenty statements on the site (two of them in a published privacy policy) describing behaviour the plugin no longer had. So a second checker reads the plugin's own source for the facts the pages restate, and fails when they disagree:

./build/Release/DocsCheck            # or: ctest -R DocsCheck

It compares five things: the price (CONFIG.price in docs/app.js against every euro amount on every page, legal pages included); the preset bank (the // FAMILY (n) headers in Source/Presets.h against the preset grid, plus the N patches headline, and every preset the page cites by name — the site's "Patch of the week" card marks its patches with data-preset="NAME", which is checked against the real bank; the counts alone once let a card advertise PSYTRANCE BASS 4 and UPLIFTING LEAD 2, neither of which ships); the AI defaults (the first model in each cloud table in Source/AiCloudGen.cpp, so a retired default like the 2.0 Flash era cannot linger on the page); local storage (every path the plugin creates via getChildFile() against the privacy policy, which is what makes the policy self-maintaining); and the trial length (License.h's trialHours). DocsCheckTest injects each kind of drift into a scratch copy of the tree and asserts the guard catches it — a checker whose failure mode is a silent pass needs that proof.

The policy documents stores ask for at signup live in docs/legal/: the Licence Agreement, Terms of Sale, Refund Policy and Privacy Policy. They are drafts: every value you must supply is marked with a highlighted class="ph" placeholder, and each file opens with a comment describing what to replace.

After you push: is the live site this commit?

Every check above looks at the repository. None of them can see the deploy, because there is no deploy until GitHub Pages picks the push up — so "the commit is pushed, therefore the site is that commit" is an assumption, and it is the one that has actually been wrong here: a page still being served from the previous deploy looks correct to every local check there is.

tools/check-deployed.py checks it rather than assuming it. It reads the address out of the pages' canonical links — the same source the generators use, so moving to a custom domain needs no edit here — walks everything under docs/, and fetches each file at the URL it is published at:

python tools/check-deployed.py              # is the live site this repository's docs/ tree?
python tools/check-deployed.py --wait 60    # ...right after a push, let Pages catch up
python tools/check-deployed.py -v           # list every file and page compared

It compares every file byte for byte, printing both digests on a mismatch so it is clear which side is stale; each page's canonical link and og:url against the address the page was actually served at; the verification token's bytes and its exact form; robots.txt's Sitemap: line and every <loc> in the sitemap, each of which has to be served, byte-identical to the file it names, and name something the repository publishes; and the one-address property — the address without its trailing slash has to redirect to the canonical one, and the index.html spelling has to serve the same bytes rather than a second copy of the landing page.

Every request carries a throwaway query parameter, because Pages serves with max-age=600 and a plain check would report the previous deploy as drift for ten minutes after a push. It refuses to run while docs/ has uncommitted changes, since a difference would then have two possible causes and the tool cannot tell you which. Pointing --base at a host that is not the address the pages declare (a staging copy, say) skips the address comparisons and keeps the file ones, rather than reporting a page's production canonical as a failure.

python tools/check-deployed.py --base https://staging.example/GoaSynth/   # any host

It exits 0 when the deployed site is this tree, 1 when it is not, and 2 when it could not be checked at all.

Building

Requires CMake 3.22+ and MSVC 2022 (Windows), Xcode (macOS) or a g++/clang toolchain (Linux).

Brand assets

The logo and app icon live in Assets/ and docs/favicon.ico, all generated by one deterministic script (no font files, pure Pillow):

python tools/make-brand.py

It draws the site's sine-swoosh mark in the plugin's house palette (violet/teal/magenta over purple-black) and writes the tile icons the build embeds (ICON_BIG/ICON_SMALL in CMakeLists.txt — juceaide turns them into the Windows .ico, the macOS .icns and the VST3 bundle's Plugin.ico for the plugin, standalone, installer and licence helper), plus the website favicon and a vector SVG mark/lockup. Output is byte-stable for a given script, so re-running it never churns the repository.

cmake -B build -G "Visual Studio 17 2022" -A x64 -DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build --config Release --target GoaSynth_VST3 --parallel

The plugin is produced at:

build/GoaSynth_artefacts/Release/VST3/GoaSynth.vst3

Run the tests with ctest --test-dir build -C Release — or, so a stale binary cannot pass for a fresh one, with the checks runner below. Either way there are seven: RoundTripTest (preset and state round-trip, the licensing paths, cloud-reply sanitising, scale quantiser, Scala microtuning, vowel filter, pump and filter drive, and note-off symmetry — that Scale Lock and chord memory never strand or silence a voice), OverlayTest (builds the real editor offscreen: every overlay must open on screen with laid-out children, the MOD pick/flash/dot workflow, and a unit-map sweep over every knob's value formatting), FulfilTest (the fulfilment tool, against a sandbox keypair), PublishCheck / PublishCheckTest (nothing seller-side or secret is about to be published with the website — see Before you publish), and DocsCheck / DocsCheckTest (the site's claims still match the plugin — see the drift guard).

RoundTripTest prints a [phase] ... line before each block and runs unbuffered, so if it ever dies mid-run the log names the block it was in rather than being empty.

Running the checks in one command

ctest runs whatever executable is already on disk, and the one thing a test suite cannot be asked to notice about itself is that the binary is not the code: a build that failed leaves the previous executable in place, and an executable older than the source it tests reports on the old code. Both look exactly like a pass, and one of them has already cost time here — a DOCS CHECK OK printed by a binary from a build that had failed.

So the suite is normally run through one script, which builds the test targets first, refuses to continue if the build failed, and then refuses to run any binary that is behind the files it was compiled from:

python tools/run-checks.py                 # build, verify, run all seven
python tools/run-checks.py -R DocsCheck    # one test, still built and still verified
python tools/run-checks.py --list -v       # what would run, and what each was built from
python tools/run-checks.py --no-build      # verify and run what is already there

The target list comes from CMake (ctest --show-only=json-v1), so a test is included by being a test; each binary's inputs come from MSBuild's own dependency logs, so RoundTripTest is compared against every file it compiles — JUCE and all — and DocsCheck against the handful it does. Neither list is written down here, because a list is what drifts. Entries outside the tree (the MSVC headers, the Windows DLLs the compiler reads) are ignored: those are the toolchain's business, and MSBuild tracks them itself. On a generator that writes no dependency logs the comparison falls back to tests/ plus CMakeLists.txt, and says so rather than pretending to be exact.

Its exit codes are separate on purpose — 1 the build failed, 2 a setup problem, 3 a binary is behind its sources, 4 the tests failed — because each one sends you somewhere different.

One check is deliberately not in here, and not in ctest either: the site itself is only checkable over the network, and everything above stays offline and deterministic. It has its own section, after you push.

Installing

Copy the whole GoaSynth.vst3 folder (bundle) to:

  • Windows: C:\Program Files\Common Files\VST3\ (or your DAW's VST3 folder)
  • macOS: /Library/Audio/Plug-Ins/VST3/
  • Linux: ~/.vst3/

Then rescan plugins in your DAW, create an instrument track, and play.

Licensing (serial keys)

The plugin ships with a 24-hour trial: the first launch stamps a per-machine trial file (in the shared public folder, so a second Windows account can't reset it) and everything works for 24 hours — full sound engine, FX, AI designer, presets. A TRIAL 23 h 59 m badge in the header counts down and clicks through to the activation screen. After expiry instances render silence until activated; a serial (or the master key) supersedes the trial entirely.

Each serial is an RSA-signed machine fingerprint, so one serial = one machine — keys shared between people simply fail to activate. There is no online check; everything works offline.

For buyers

On first open the plugin shows an activation screen with a MACHINE ID (20 hex chars). Send it to the seller, receive a serial back, paste it in — the plugin unlocks and stays unlocked. Re-installing Windows or changing hardware changes the machine id and needs a fresh serial.

For the seller

GoaSynthKeygen is a separate console app (built alongside the plugin, never shipped to buyers). Double-clicking it opens an interactive menu — issue files, list/verify/unregister serials, all numbered options; commands below are the equivalent one-liners:

GoaSynthKeygen --init <master-key>   # ONCE: creates the RSA keypair + master digest
GoaSynthKeygen --rotate-master [newKey]
                                     # change ONLY the master key: the RSA keypair
                                     # is kept, so every serial already issued
                                     # stays valid. No argument = generate a
                                     # random 12-character key
GoaSynthKeygen --machine-id          # your own machine id
GoaSynthKeygen --gen <machineId> "John's studio PC"   # issue one serial
GoaSynthKeygen --genfile            # issue a .goalicense file, asked step
                                     # by step (no arguments to remember)
GoaSynthKeygen --file <machineId> "John's studio PC"  # issue one serial AND
                                     # export <GoaSynth-<id>>.goalicense
GoaSynthKeygen --list                # active serials (and how many are revoked)
GoaSynthKeygen --verify <serial>     # check a signature; flags a revoked one
GoaSynthKeygen --revoke <serial> [reason]
                                     # refund: pull a serial from the active
                                     # ledger and record why in
                                     # revoked_serials.txt (unlike
                                     # --unregister, which erases the line)
GoaSynthKeygen --master-info         # show the embedded master digest

Keep the master key out of the repository. It is a universal unlock for every shipped copy of the plugin, so it belongs in a password manager — never in a commit, an issue, a build log, or a screenshot. Source/LicenseKeys.h (the public key + master digest) is gitignored for the same reason, and the test suite activates with a throwaway key in tests/TestMasterKey.h so it never needs the real one.

If it does get out, GoaSynthKeygen --rotate-master retires it without touching the keypair, so no serial has to be re-issued: it rewrites the stretched digest in keys.txt and in Source/LicenseKeys.h, and records the change in master_rotations.txt (digests only, never the key). Rebuild and republish afterwards — a build that already shipped keeps accepting the old key, because the old digest is inside it, so rotation protects the next build rather than the copies already sold. The digest ships in every binary, so prefer the generated key over something memorable: an offline guess costs the attacker one attempt per 64 SHA-256 rounds.

Fulfilling orders in bulk

GoaSynthFulfil (also built alongside the plugin, also never shipped to buyers) turns the store's order export into signed licences: it scans a folder of *.csv/*.json exports, finds each buyer's machine ID, signs through the same Keygen/KeygenCore.h the keygen uses, and writes the .goalicense file plus a reply-ready .eml with it attached.

cmake --build build --config Release --target GoaSynthFulfil --parallel

GoaSynthFulfil --inbox C:\goasynth\orders --out C:\goasynth\fulfilled \
               --from "GoaSynth <orders@yourdomain>" --repo https://github.com/you/goasynth
GoaSynthFulfil --inbox C:\goasynth\orders --watch --interval 60   # or leave it running
GoaSynthFulfil --inbox C:\goasynth\orders --dry-run               # report only, sign nothing
GoaSynthFulfil --status --out C:\goasynth\fulfilled               # everything issued so far

A manifest.tsv in the outbox makes each pass idempotent — a re-export, a crash or a long --watch session cannot issue a second serial — and anything ambiguous (no machine ID, or several plausible ones) lands in needs-attention.tsv rather than being signed. Refunds are handled end to end: a refunded order is taken out of the active ledger and recorded in revoked_serials.txt with the order number as its reason, its manifest row flips to revoked, and activity.log keeps the event. A buyer who buys again puts the same serial back in the active ledger (the signature is deterministic) and the pass appends a restored event to revoked_serials.txt, so --list and --verify stop calling a paying customer's serial revoked. The refund that pulled it stays on the record - the log is an append-only event stream, and a serial's state is its newest line. GoaSynthKeygen --verify <serial> on such a serial says so: was revoked on - refund - order 7001, later restored.

Revocation is bookkeeping, not enforcement: an activated copy keeps working because activation is offline. What it changes is which serials count as live licences you support and re-issue — and it is what lets you answer "was order 7001 refunded?" months later, which --unregister (a transfer: erase the line) cannot. --revoke <serial> [reason] is the same operation by hand.

Both seller tools take the keygen folder from GOASYNTH_KEYGEN_DIR when it is set, which is how FulfilTest runs against a throwaway keypair instead of the real one. CHECKOUT.md (in this folder) §3b has the workflow; --help lists the flags and the email placeholders.

The keypair lives in %APPDATA%\GoaSynth\Keygen\ (outside the repo). The plugin embeds only the public key and a stretched SHA-256 of the master key — the private key and master key never ship. Source/LicenseKeys.h (gitignored) holds the generated constants; a fresh checkout builds with a placeholder header that rejects all serials.

Buyer-ready license files (.goalicense): --file wraps the issued serial in a GOA-LICENSE-1 text document. Send the buyer that one file instead of a serial — they double-click it and GoaSynth activates on the spot, or they press IMPORT on the activation screen and pick it. A file is interchangeable with its plain serial: same signature, same machine binding, same ledger — tampering with the informational lines cannot widen what it activates, and a file bound to another machine is refused.

Double-click helper (GoaSynthLicense.exe, built alongside the plugin, ships to buyers): a VST3 is a DLL, so Windows cannot open files "with" the plugin — this small companion app owns the association instead. It registers .goalicense for the current user under HKCU (no admin, no installer) on first run, then double-clicks launch it and the file is activated through exactly the same code path as the plugin's IMPORT button, with a small window confirming the result (or showing the MACHINE ID to send you on failure). GoaSynthLicense.exe --unregister removes the association again; --selftest exercises register → verify → unregister for CI. If Windows still asks how to open the file, picking "GoaSynth license file" once makes the choice stick.

The trial file lives beside the machine ledger in the shared public folder (C:\Users\Public\Documents\GoaSynth\License\trial.txt); GOASYNTH_TRIAL_FILE redirects it for tests. It is a plain timestamp, so it is hardening against honest users, not a determined cracker — the RSA-verified serial remains the real gate.

The master key activates any machine when typed into the serial box — keep it private (it is never stored in plain text anywhere; only its salted, stretched digest is embedded, so the key cannot be extracted from binaries). If it is ever exposed, GoaSynthKeygen --rotate-master retires it while keeping every serial in the wild valid; only a leaked keypair would force a re-issue. An activation ledger in the shared public folder (C:\Users\Public\Documents\ GoaSynth\License\) binds each serial to the first machine that activated it, so a second Windows account cannot re-use the same serial. The machine fingerprint mixes hostname, user, MAC addresses and the OS device id, so disk images that only clone hostname + accounts still get their own identity.

Transfers: if a buyer re-installs Windows, changes hardware or sells the license on, lift the old binding with GoaSynthKeygen --unregister <serial> and issue a fresh serial for the new MACHINE ID. Inside the plugin, the licence file is only refreshed through activate(), and a deliberate deactivation also clears that machine's ledger line — so a serial can never be permanently locked by an old binding.

Notes

UI conventions (for contributors)

How hover info and modulation indicators work in the editor — keep new controls consistent with this. The published copy of this section (with the screenshot inline) lives at GoaSynth → Contributing:

Tooltips. Every interactive control has hover text, shown by the single juce::TooltipWindow owned by the editor (without it, setTooltip() calls are silent no-ops — this shipped broken once).

  • Knobs and toggles (goaui::Ctl / goaui::ToggleCtl) build their tooltip automatically: parameter name + live value in engine units (Cutoff (Hz) — 1500 Hz, Attack — 0.01 s, RESO — 25 %, toggles show — ON/OFF). The unit map lives in liveValueText() (PluginEditor.cpp) — add a branch there for any new parameter unit. If a new knob shows a bare decimal or a wrong unit, the unit-map sweep in OverlayTest fails and names the id.
  • The text is rebuilt on every paint (~30 fps while the backdrop animates), so values track drags, host automation and preset loads while hovering — don't cache tooltip strings.
  • Hand-written tooltips (buttons, graphs, strips, keyboard) follow the house style: what it is, then an em dash, then the gestures/behaviour (OSC A wavetable editor — left-drag draws, right-drag erases …).
  • Ctl/ToggleCtl hold no tooltip themselves (plain Component): text is applied to the child widgets via their setTip() helpers.

Mod dots. A knob targeted by an active mod-matrix slot shows small dots in its top-right corner — one per source slot (max 3), colour-coded: teal LFO 1, magenta LFO 2, accent envelope, dim velocity/modwheel. The dot pulses with the live source value × amount (an LFO dot breathes at its LFO's rate); a slot only counts when its amount is non-zero, and depth over ±0.6 adds a ring. Hovering the knob lists the routings: MOD 2: LFO 1 -> CUTOFF (+40).

A CUTOFF knob with a velocity mod dot and its 1.5k live readout

The snapshot is regenerated by the test suite — run GOASYNTH_KNOB_SHOT=1 ./build/Release/OverlayTest (a routing to CUTOFF is armed for the capture, then disarmed), and the test fails if the file comes out blank.

  • Scan + colours: modDotSources() / paintModDot() (PluginEditor.cpp).
  • Dots refresh on matrix edits, overlay open and preset loads — if you add another path that changes slots, call goaui::repaintCtlPaints().
  • The live pulse reads GoaSynth::uiLfo1 / uiLfo2 / uiEnvF / uiEnvA / uiVelocity (relaxed atomics, last-voice-wins — indicator only).

Pick-to-assign. In the MOD matrix, click a DST cell then a knob (or a SRC cell then an LFO/ENV graph) to route by pointing; ESC or re-click cancels. The just-assigned cell flashes inside the card and a brief arrow points from it at the clicked control, so the landed routing is confirmed (the knob flash under the overlay's dim backdrop would be invisible, so the card carries the confirmation). Source kinds are identified by parameter ids on the widgets (Ctl::paramId, LfoGraph::rateParamId, EnvGraph::attackParamId()) — set those ids when adding new routable controls or sources.

Pick-to-assign: the just-routed DST cell flashing, with a brief arrow pointing at the target knob

Both illustrations regenerate with the suite: GOASYNTH_KNOB_SHOT=1 ./build/Release/OverlayTest (a routing to CUTOFF is armed for the capture, then disarmed), and the test fails if the file comes out blank.

  • JUCE is used under the AGPLv3 license; the VST3 interface code is used under Steinberg's GPLv3 dual license. Because the source stays public and builds are distributed under the AGPLv3, no paid JUCE or Steinberg license is needed in order to sell binaries — buyers receive the corresponding source along with the plugin. Choosing to ship closed source would change that and would need JUCE's proprietary option plus a Steinberg agreement.

About

GoaSynth

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages