Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c942cad0c | ||
|
|
8364737d1c | ||
|
|
3be1e42242 | ||
|
|
cd26beca42 | ||
|
|
40dc0d4105 | ||
|
|
e2e66457c2 | ||
|
|
4748bfe768 | ||
|
|
dc6fd88ed1 | ||
|
|
3b2ecc50f2 |
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { pathToFileURL } = require('url');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||||
|
const BAKED = "/usr/lib/node_modules/@nanonets/graft/dist/claude";
|
||||||
|
|
||||||
|
// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
|
||||||
|
function fromPkg(base) {
|
||||||
|
try {
|
||||||
|
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
|
||||||
|
return path.join(path.dirname(pkg), 'dist', 'claude');
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
|
||||||
|
function globalRoot() {
|
||||||
|
try {
|
||||||
|
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
|
||||||
|
return root || null;
|
||||||
|
} catch { return null; /* npm unavailable */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The version of the package a dist/claude dir belongs to, or null if unreadable.
|
||||||
|
function versionOf(distClaude) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
|
||||||
|
function newer(a, b) {
|
||||||
|
if (!a) return false;
|
||||||
|
if (!b) return true;
|
||||||
|
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
|
||||||
|
const pa = p(a), pb = p(b);
|
||||||
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
|
const d = (pa[i] || 0) - (pb[i] || 0);
|
||||||
|
if (d !== 0) return d > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The highest-versioned dir in `dirs` that actually contains `name`, or null.
|
||||||
|
function best(dirs, name) {
|
||||||
|
let bestDir = null, bestVer = null;
|
||||||
|
for (const d of dirs) {
|
||||||
|
if (!d || !fs.existsSync(path.join(d, name))) continue;
|
||||||
|
const v = versionOf(d);
|
||||||
|
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
|
||||||
|
}
|
||||||
|
return bestDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function entry(name) {
|
||||||
|
// Cheap candidates first, and only shell out to npm when every one of them misses.
|
||||||
|
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
|
||||||
|
const hit = best(cheap, name);
|
||||||
|
if (hit) return path.join(hit, name);
|
||||||
|
const gr = globalRoot();
|
||||||
|
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
|
||||||
|
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
|
||||||
|
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
|
||||||
|
}
|
||||||
|
|
||||||
|
import(pathToFileURL(entry("hooks.js")).href).then((m) => m.main(process.argv[2])).catch(() => { /* graft unavailable — no-op */ });
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { pathToFileURL } = require('url');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||||
|
const BAKED = "/usr/lib/node_modules/@nanonets/graft/dist/claude";
|
||||||
|
|
||||||
|
// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
|
||||||
|
function fromPkg(base) {
|
||||||
|
try {
|
||||||
|
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
|
||||||
|
return path.join(path.dirname(pkg), 'dist', 'claude');
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
|
||||||
|
function globalRoot() {
|
||||||
|
try {
|
||||||
|
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
|
||||||
|
return root || null;
|
||||||
|
} catch { return null; /* npm unavailable */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The version of the package a dist/claude dir belongs to, or null if unreadable.
|
||||||
|
function versionOf(distClaude) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
|
||||||
|
function newer(a, b) {
|
||||||
|
if (!a) return false;
|
||||||
|
if (!b) return true;
|
||||||
|
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
|
||||||
|
const pa = p(a), pb = p(b);
|
||||||
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
|
const d = (pa[i] || 0) - (pb[i] || 0);
|
||||||
|
if (d !== 0) return d > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The highest-versioned dir in `dirs` that actually contains `name`, or null.
|
||||||
|
function best(dirs, name) {
|
||||||
|
let bestDir = null, bestVer = null;
|
||||||
|
for (const d of dirs) {
|
||||||
|
if (!d || !fs.existsSync(path.join(d, name))) continue;
|
||||||
|
const v = versionOf(d);
|
||||||
|
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
|
||||||
|
}
|
||||||
|
return bestDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function entry(name) {
|
||||||
|
// Cheap candidates first, and only shell out to npm when every one of them misses.
|
||||||
|
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
|
||||||
|
const hit = best(cheap, name);
|
||||||
|
if (hit) return path.join(hit, name);
|
||||||
|
const gr = globalRoot();
|
||||||
|
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
|
||||||
|
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
|
||||||
|
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
|
||||||
|
}
|
||||||
|
|
||||||
|
import(pathToFileURL(entry("statusline.js")).href).then((m) => m.main()).catch(() => { /* graft unavailable — no-op */ });
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"statusLine": {
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-statusline.cjs\""
|
||||||
|
},
|
||||||
|
"subagentStatusLine": {
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-statusline.cjs\""
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Write|Edit|MultiEdit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" post-edit",
|
||||||
|
"timeout": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash|mcp__graft__|Read|Grep|Glob",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" tool-savings",
|
||||||
|
"timeout": 8000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" prompt",
|
||||||
|
"timeout": 15000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" session-start",
|
||||||
|
"timeout": 8000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" stop",
|
||||||
|
"timeout": 8000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"footerLinksRegexes": [
|
||||||
|
"graft/[\\w./-]+\\.md"
|
||||||
|
],
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(graft:*)",
|
||||||
|
"Bash(npx graft:*)",
|
||||||
|
"Bash(graft-dev:*)",
|
||||||
|
"Bash(node dist/cli.js:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
name: graft
|
||||||
|
description: This repo is indexed by graft/. For ANY task here, whether
|
||||||
|
understanding how something works, finding where code lives, tracing what
|
||||||
|
calls a symbol or what a change breaks, or scoping an edit, get your context
|
||||||
|
from graft before grepping or reading source files.
|
||||||
|
---
|
||||||
|
|
||||||
|
# graft
|
||||||
|
|
||||||
|
`graft/` holds a graph of this repo: small markdown nodes that each explain one
|
||||||
|
part in prose and name the exact `file:line` spans they cover, plus a wiring
|
||||||
|
graph of who-calls-what. Querying a node costs a few hundred tokens; rebuilding
|
||||||
|
that understanding by reading source costs thousands, and misses the edges.
|
||||||
|
|
||||||
|
Every command below is `$0`, needs no API key, and returns in under a second.
|
||||||
|
There are six of them. **Pick the one that fits the task, run it, act on the
|
||||||
|
answer; don't chain tools hoping for more. Most tasks need one call.**
|
||||||
|
|
||||||
|
## The tools
|
||||||
|
|
||||||
|
### 1 · `graft ask "<question>" --source`: locate + understand (the default)
|
||||||
|
Ranked retrieval over the graph, routed automatically between prose nodes and
|
||||||
|
the wiring graph, returning the top hits with exact `file:line`.
|
||||||
|
- `--source` inlines the code at each hit, the ≤8-line **crux** of each
|
||||||
|
definition, so the result IS the code you need, no follow-up file read. Add
|
||||||
|
`--full` only when the crux is too small to act on.
|
||||||
|
- `--in <path>` narrows to a subtree before ranking; `-n N` caps results (default 8).
|
||||||
|
- **Use it when** the question is conceptual or locational: "how does auth
|
||||||
|
work", "where is rate-limiting handled", "what assembles the request pipeline".
|
||||||
|
- One ask usually answers. A genuinely multi-part question needs one ask per
|
||||||
|
distinct sub-aspect, never the same question reworded. Few or weak hits mean
|
||||||
|
switch tool (grep / skeleton / callers), don't re-ask.
|
||||||
|
|
||||||
|
### 2 · `graft grep "<pattern>"`: exhaustive find
|
||||||
|
Regex (or `--fixed` for a literal) over every indexed file, hits **grouped by
|
||||||
|
enclosing symbol** and ranked by coupling; it also reports files it couldn't read.
|
||||||
|
- **Use it when** you need every occurrence: all call sites, all uses of a
|
||||||
|
constant, all providers. `ask` is ranked top-N and *will* miss instances;
|
||||||
|
grep won't. One grep replaces a spray of asks.
|
||||||
|
- Search a **short symbol name or literal**, not a full guessed signature: an
|
||||||
|
over-specific regex (`func (s *Server) GenerateHandler`) returns nothing even
|
||||||
|
when the code is indexed. If a grep misses, **loosen it** (drop the receiver
|
||||||
|
and signature, keep the bare name) and retry `graft grep` — do NOT switch to
|
||||||
|
raw `grep -rn`, which is slower and unranked.
|
||||||
|
- `-i` case-insensitive; `--in <path>` scopes to a subtree. Raw `grep -rn` is
|
||||||
|
only for files graft genuinely doesn't index (docs, configs, brand-new files).
|
||||||
|
|
||||||
|
### 3 · `graft skeleton <file>`: a file's API at a glance
|
||||||
|
Signatures-only view of one file (every function / method / type with its span)
|
||||||
|
in ~200 tokens, ~10x cheaper than reading the file.
|
||||||
|
- **Use it when** you need "what's in this file / what can I call here" before
|
||||||
|
editing or wiring into it. One skeleton is the whole answer for a file; don't
|
||||||
|
re-skeleton the same file, and don't skeleton every file `map` already named.
|
||||||
|
|
||||||
|
### 4 · `graft callers <symbol>`: the exact edges
|
||||||
|
Precomputed call/reference edges, not a text search. Symbol can be bare
|
||||||
|
(`Foo`), qualified (`Class.method`), or package-qualified (`pkg.Fn`).
|
||||||
|
- default `--direction in`: **who calls/references** this; run before you
|
||||||
|
rename, delete, or change its signature.
|
||||||
|
- `--direction out`: **what this symbol itself calls/depends on** (the old `callees`).
|
||||||
|
- `--depth N`: walk transitively N hops for the **full blast radius** (the old
|
||||||
|
`impact`); `--depth 2` is the usual "what breaks if I touch this".
|
||||||
|
- `--depth all`: the **entire connected closure** — every source reachable
|
||||||
|
through the edges. Reach for this before a **refactor, rename, or any
|
||||||
|
multi-file change**: it surfaces the sibling and downstream files (platform
|
||||||
|
variants, a module you must split out) that a single-file edit would miss.
|
||||||
|
|
||||||
|
### 5 · `graft map`: orientation for an unfamiliar repo or area
|
||||||
|
A token-budgeted tour: directory clusters, per-directory hubs, and global
|
||||||
|
hotspots, straight from the wiring graph.
|
||||||
|
- **Use it when** you land in a repo cold or are asked for "the architecture".
|
||||||
|
`map` alone is the answer: read the hub cards it names; do NOT then skeleton
|
||||||
|
or ask your way through every subsystem it lists. `--max-dirs N` widens it.
|
||||||
|
|
||||||
|
### 6 · Lifecycle: `graft build` / `graft check`
|
||||||
|
Every tool above refreshes the graph itself before answering, so what those tools
|
||||||
|
return always describes the code as it is right now — including edits you just made
|
||||||
|
and have not committed. You do **not** need to run `build` after editing.
|
||||||
|
|
||||||
|
One caveat, if you `grep` the markdown under `graft/` directly: those cards are a
|
||||||
|
projection, rebuilt at the end of the turn rather than on each query, so after an edit
|
||||||
|
they can lag. The tools above never do — prefer them, and treat a card's spans as
|
||||||
|
stale if you have edited that file this turn.
|
||||||
|
|
||||||
|
`build` is for the LLM layer (`--deep` adds a concept map; skip unless asked);
|
||||||
|
`check` fails when `graft/` is stale, for CI.
|
||||||
|
|
||||||
|
## Scenarios: the shortest path through a coding task
|
||||||
|
|
||||||
|
| When you're… | Reach for | Calls |
|
||||||
|
|---|---|---|
|
||||||
|
| Onboarding / "explain this codebase" | `graft map`, then read the named hub cards | 1 |
|
||||||
|
| Understanding a flow ("how does X work") | `graft ask "<flow>" --source` | 1 |
|
||||||
|
| Finding where a change belongs | `graft ask "where is <behavior>" --source` | 1 |
|
||||||
|
| Editing a symbol you can already name | `graft grep "<symbol>"`, edit at the `file:line` (skip `ask` — you know where it is) | 1 |
|
||||||
|
| Renaming / deleting / changing a signature | `graft callers <sym> --depth 2` first | 1 |
|
||||||
|
| Refactor / multi-file change (before editing) | `graft callers <sym> --depth all` — map every connected file, don't stop at the first | 1 |
|
||||||
|
| "What does this depend on?" | `graft callers <sym> --direction out` | 1 |
|
||||||
|
| Finding every occurrence of a pattern | `graft grep "<literal>"` | 1 |
|
||||||
|
| "What's the API of this file?" | `graft skeleton <file>` | 1 |
|
||||||
|
| Debugging a failure in area X | `graft ask "<symptom>" --source`, then `callers` on the suspect | 1–2 |
|
||||||
|
| Judging a diff's risk before merge | `graft callers <changed sym> --depth 2` | 1 / symbol |
|
||||||
|
| Working inside one repo of a monorepo | add `--in <scope>/` to ask / grep / callers | n/a |
|
||||||
|
|
||||||
|
In a multi-repo workspace, graft ranks fairly so the biggest repo can't drown
|
||||||
|
the rest, and every hit carries a `[scope/]` label naming its sub-project; when
|
||||||
|
you already know where you're working, narrow with `graft ask "<task>" --in <scope>/`.
|
||||||
|
|
||||||
|
## Spend the fewest calls
|
||||||
|
- A node's `covers:` list already gives exact `file:line` for every symbol, so
|
||||||
|
cite straight from it. The spans are generated from source and authoritative;
|
||||||
|
don't re-open or re-grep files to "double-check".
|
||||||
|
- When the task already names the file or symbol to change, go straight there:
|
||||||
|
`graft grep "<symbol>"` for the exact `file:line`, then edit. Reserve
|
||||||
|
`graft ask` for when you don't yet know where the code lives — an `ask`
|
||||||
|
round-trip is wasted on a target you can already name.
|
||||||
|
- Trust the answer and act. Reach for a second tool only when the first genuinely
|
||||||
|
fell short: weak hits, a truncated span, or a need to be exhaustive.
|
||||||
|
- If graft names a path that isn't on disk, its index is ahead of your checkout
|
||||||
|
(a branch switch or unpulled move). Don't read the missing file — `graft grep`
|
||||||
|
the symbol to find where it lives now, or run `graft build` to refresh.
|
||||||
|
|
||||||
|
## Report what graft saved, every turn
|
||||||
|
Each retrieval tool **opens** its output with a `[graft] tokens saved ≈ N` line:
|
||||||
|
the estimated tokens that call saved versus reading the files it covers whole.
|
||||||
|
Whenever you used any graft tool in a turn, close your reply with a one-line
|
||||||
|
tally summing those numbers across every graft call you made, e.g.
|
||||||
|
`🌱 graft saved ~12,400 tokens this turn (3 calls)`. A call with no such line
|
||||||
|
(tiny files, where the pointers cost as much as the source) saved nothing, so
|
||||||
|
skip it. This is the per-turn figure; the statusline carries the running
|
||||||
|
session total.
|
||||||
|
|
||||||
|
**Never pipe a graft command through `head`, `tail`, or `sed -n`.** Every tool
|
||||||
|
is already capped and states what it dropped; clipping it costs you hits you
|
||||||
|
asked for, and it silently drops the savings line the statusline's running
|
||||||
|
total is parsed from.
|
||||||
|
|
||||||
|
## When graft isn't enough
|
||||||
|
- Span truncated ("+N more lines"): open the file at that exact range.
|
||||||
|
- A node lacks a detail: ask a more specific question; only then read source at
|
||||||
|
the exact `file:line`, never a whole file to rebuild understanding graft gives.
|
||||||
|
- You may also grep / ls / cat inside `graft/` directly (plain markdown;
|
||||||
|
`graft/INDEX.md` indexes the nodes), but the tools above are faster and
|
||||||
|
exhaustive where it matters, so reach for them first.
|
||||||
|
|
||||||
|
When the graft MCP server is connected, these are exposed as tools too:
|
||||||
|
`graft_find_code`, `graft_find_all`, `graft_file_api`, `graft_trace_calls` (with
|
||||||
|
`direction` / `depth`), `graft_repo_map`, `graft_check_freshness`. Use whichever surface is
|
||||||
|
available; the guidance is identical.
|
||||||
@@ -16,3 +16,6 @@ compile_commands.json
|
|||||||
*.user
|
*.user
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
|
# graft's local graph cache — regenerable, not committed (run `graft build`).
|
||||||
|
/graft/
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# graft's cards are gitignored but should stay greppable: ripgrep reads
|
||||||
|
# .ignore before .gitignore, so this re-admits the tree to search only.
|
||||||
|
!graft/
|
||||||
|
graft/.cache/
|
||||||
|
graft/.graph/
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"graft": {
|
||||||
|
"command": "graft",
|
||||||
|
"args": [
|
||||||
|
"mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!-- graft:start -->
|
||||||
|
## Graft — repo context graph
|
||||||
|
|
||||||
|
This repo is indexed in `graft/`: small linked markdown nodes that explain each
|
||||||
|
system and carry exact file:line spans, kept in sync with the code through git.
|
||||||
|
|
||||||
|
For ANY task here — understanding how something works, finding where code lives,
|
||||||
|
or scoping a change — get context from the graph before grepping or opening
|
||||||
|
source files. Re-ask freely (it's cheap) and reuse literal identifiers you
|
||||||
|
already have (symbol, error string, file name) as the query. New to this repo?
|
||||||
|
Run `graft map` first — a token-budgeted orientation (dir clusters, hubs,
|
||||||
|
hotspots), no LLM, no key.
|
||||||
|
|
||||||
|
- Run `graft ask "<your question>" --source` → ranked nodes with the relevant
|
||||||
|
code spans inlined (each hit's ≤8-line crux by default; `--full` for whole
|
||||||
|
definitions when the crux isn't enough). Match the tool to the task shape:
|
||||||
|
for understanding or editing, the top node IS the answer — cite its
|
||||||
|
`covers:` file:line spans and edit straight from `--source`. For
|
||||||
|
exhaustive tasks ("every occurrence / every caller of this pattern"), ranked
|
||||||
|
results are top-N, not complete — run `graft grep "<literal>"` instead
|
||||||
|
(exhaustive over indexed files, grouped by enclosing symbol), falling back
|
||||||
|
to raw `grep -rn` only for unindexed files.
|
||||||
|
- `graft skeleton <file>` → every definition's signature + span, ~10× cheaper
|
||||||
|
than reading the file; use it to skim an API surface.
|
||||||
|
- `graft callers <symbol>` gives precomputed, exact edges — who calls this.
|
||||||
|
Add `--direction out` for what it calls, or `--depth N` to walk
|
||||||
|
transitively for the full blast radius. For structural questions, skip
|
||||||
|
ranking and use this directly.
|
||||||
|
- Or browse: `graft/INDEX.md` lists every node; follow the links.
|
||||||
|
- Monorepos and folders of multiple repos rank fairly across sub-projects —
|
||||||
|
hits carry `[scope/]` labels naming which one they're from. Narrow with
|
||||||
|
`graft ask "<task>" --in <scope>/` once you know where you're working.
|
||||||
|
|
||||||
|
If a returned span is truncated ("+N more lines"), open the file at that exact
|
||||||
|
range before finalizing. Only open source files when a node genuinely lacks a
|
||||||
|
needed detail, and then at the exact file:line the node points to — never
|
||||||
|
re-read whole files.
|
||||||
|
|
||||||
|
After big code changes, refresh the graph with `graft build` (deterministic,
|
||||||
|
no API key, $0).
|
||||||
|
<!-- graft:end -->
|
||||||
+238
-192
@@ -1,22 +1,11 @@
|
|||||||
#include "Engine.h"
|
#include "Engine.h"
|
||||||
|
#include "RAVEButton.h"
|
||||||
|
|
||||||
namespace serum
|
namespace serum
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
inline float v (juce::AudioProcessorValueTreeState& apvts, const char* id)
|
|
||||||
{
|
|
||||||
if (auto* p = apvts.getRawParameterValue (id))
|
|
||||||
return p->load();
|
|
||||||
return 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int vic (juce::AudioProcessorValueTreeState& apvts, const char* id, int maxValue)
|
|
||||||
{
|
|
||||||
return juce::jlimit (0, maxValue, (int) std::llround (v (apvts, id) * maxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline float limit (float x) noexcept
|
inline float limit (float x) noexcept
|
||||||
{
|
{
|
||||||
const float ax = std::fabs (x);
|
const float ax = std::fabs (x);
|
||||||
@@ -26,47 +15,36 @@ namespace
|
|||||||
const float clipped = 0.8f + std::tanh (over) * 0.2f;
|
const float clipped = 0.8f + std::tanh (over) * 0.2f;
|
||||||
return std::copysign (clipped, x);
|
return std::copysign (clipped, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* kEnvAttack[kNumEnvelopes] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
|
||||||
const char* kEnvDecay[kNumEnvelopes] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
|
||||||
const char* kEnvSustain[kNumEnvelopes]= { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
|
||||||
const char* kEnvRelease[kNumEnvelopes]= { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
|
||||||
const char* kEnvCurve[kNumEnvelopes] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
|
||||||
|
|
||||||
const char* kLfoRate[kNumLfos] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
|
||||||
const char* kLfoSync[kNumLfos] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
|
||||||
const char* kLfoBeat[kNumLfos] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
|
||||||
const char* kLfoShape[kNumLfos] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
|
||||||
const char* kLfoPhase[kNumLfos] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
|
||||||
const char* kLfoFade[kNumLfos] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
|
||||||
const char* kLfoDelay[kNumLfos] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
|
||||||
|
|
||||||
const char* kFxType[kNumFxSlots] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
|
||||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
|
||||||
const char* kFxMix[kNumFxSlots] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
|
||||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
|
||||||
const char* kFxP1[kNumFxSlots] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
|
||||||
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
|
||||||
const char* kFxP2[kNumFxSlots] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
|
||||||
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
|
||||||
const char* kFxP3[kNumFxSlots] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
|
||||||
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
|
||||||
const char* kFxP4[kNumFxSlots] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
|
||||||
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::prepare (double sampleRate, int maxBlockSize)
|
Engine::Engine()
|
||||||
{
|
{
|
||||||
|
wavetables.prebuild();
|
||||||
|
audioMatrix.connections.reserve (ModulationMatrix::kMaxConnections);
|
||||||
|
for (auto& assignments : audioMacros.assignments)
|
||||||
|
assignments.reserve (MacroControls::kMaxAssignments);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::prepare (double sampleRate, int maxBlockSize, juce::AudioProcessorValueTreeState& apvts)
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (controlLock);
|
||||||
sr = sampleRate;
|
sr = sampleRate;
|
||||||
blockSize = maxBlockSize;
|
blockSize = juce::jmax (1, maxBlockSize);
|
||||||
|
parameterValues.clear();
|
||||||
|
for (auto* parameter : apvts.processor.getParameters())
|
||||||
|
if (auto* ranged = dynamic_cast<juce::RangedAudioParameter*> (parameter))
|
||||||
|
if (auto* raw = apvts.getRawParameterValue (ranged->paramID))
|
||||||
|
parameterValues.emplace (std::string_view (ranged->paramID.toRawUTF8()),
|
||||||
|
ParameterValue { raw, raw->load() });
|
||||||
|
|
||||||
for (auto& voice : voices)
|
for (auto& voice : voices)
|
||||||
voice.prepare (sampleRate, maxBlockSize);
|
voice.prepare (sampleRate, blockSize);
|
||||||
for (auto& lfo : lfos)
|
for (auto& lfo : lfos)
|
||||||
lfo.prepare (sampleRate);
|
lfo.prepare (sampleRate);
|
||||||
fx.prepare (sampleRate, maxBlockSize);
|
fx.prepare (sampleRate, blockSize);
|
||||||
mixBuffer.setSize (2, maxBlockSize, false, false, true);
|
mixBuffer.setSize (2, blockSize, false, false, true);
|
||||||
reset();
|
reset();
|
||||||
|
captureControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::reset()
|
void Engine::reset()
|
||||||
@@ -78,29 +56,57 @@ void Engine::reset()
|
|||||||
fx.reset();
|
fx.reset();
|
||||||
pitchBend = 0.0f;
|
pitchBend = 0.0f;
|
||||||
modWheel = 0.0f;
|
modWheel = 0.0f;
|
||||||
|
activeVoiceCount.store (0, std::memory_order_relaxed);
|
||||||
mixBuffer.clear();
|
mixBuffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Engine::captureControls()
|
||||||
|
{
|
||||||
|
const juce::ScopedTryLock lock (controlLock);
|
||||||
|
if (! lock.isLocked())
|
||||||
|
return;
|
||||||
|
|
||||||
|
audioMatrix.connections.assign (matrix.connections.begin(),
|
||||||
|
matrix.connections.begin() + juce::jmin (matrix.size(), ModulationMatrix::kMaxConnections));
|
||||||
|
for (int i = 0; i < kNumMacros; ++i)
|
||||||
|
{
|
||||||
|
const auto& source = macros.assignments[(size_t) i];
|
||||||
|
auto& dest = audioMacros.assignments[(size_t) i];
|
||||||
|
dest.assign (source.begin(), source.begin() + juce::jmin ((int) source.size(), MacroControls::kMaxAssignments));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
|
lfos[(size_t) i].setShapeData (controlLfos[(size_t) i].getShapeData(),
|
||||||
|
controlLfos[(size_t) i].getShapeSteps());
|
||||||
|
for (auto& entry : parameterValues)
|
||||||
|
{
|
||||||
|
const float value = entry.second.source->load (std::memory_order_relaxed);
|
||||||
|
entry.second.value = std::isfinite (value) ? juce::jlimit (0.0f, 1.0f, value) : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Engine::setLfoShapeData (int index, const std::vector<float>& data, int steps)
|
void Engine::setLfoShapeData (int index, const std::vector<float>& data, int steps)
|
||||||
{
|
{
|
||||||
|
const juce::ScopedLock lock (controlLock);
|
||||||
index = juce::jlimit (0, kNumLfos - 1, index);
|
index = juce::jlimit (0, kNumLfos - 1, index);
|
||||||
lfos[(size_t) index].setShapeData (data, steps);
|
controlLfos[(size_t) index].setShapeData (data, steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Engine::getActiveVoiceCount() const
|
float Engine::v (const char* id) const
|
||||||
{
|
{
|
||||||
int count = 0;
|
const auto it = parameterValues.find (id);
|
||||||
for (const auto& v : voices)
|
return it != parameterValues.end() ? it->second.value : 0.0f;
|
||||||
if (v.isActive())
|
}
|
||||||
++count;
|
|
||||||
return count;
|
int Engine::vic (const char* id, int maxValue) const
|
||||||
|
{
|
||||||
|
return juce::jlimit (0, maxValue, (int) std::llround (v (id) * maxValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
SynthVoice* Engine::findFreeVoice()
|
SynthVoice* Engine::findFreeVoice()
|
||||||
{
|
{
|
||||||
for (auto& v : voices)
|
for (auto& voice : voices)
|
||||||
if (! v.isActive())
|
if (! voice.isActive())
|
||||||
return &v;
|
return &voice;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,20 +116,20 @@ SynthVoice* Engine::stealVoice()
|
|||||||
SynthVoice* best = nullptr;
|
SynthVoice* best = nullptr;
|
||||||
juce::uint64 bestId = std::numeric_limits<juce::uint64>::max();
|
juce::uint64 bestId = std::numeric_limits<juce::uint64>::max();
|
||||||
|
|
||||||
for (auto& v : voices)
|
for (auto& voice : voices)
|
||||||
if (v.isActive() && v.isReleased() && v.getNoteId() < bestId)
|
if (voice.isActive() && voice.isReleased() && voice.getNoteId() < bestId)
|
||||||
{
|
{
|
||||||
best = &v;
|
best = &voice;
|
||||||
bestId = v.getNoteId();
|
bestId = voice.getNoteId();
|
||||||
}
|
}
|
||||||
if (best != nullptr)
|
if (best != nullptr)
|
||||||
return best;
|
return best;
|
||||||
|
|
||||||
for (auto& v : voices)
|
for (auto& voice : voices)
|
||||||
if (v.isActive() && v.getNoteId() < bestId)
|
if (voice.isActive() && voice.getNoteId() < bestId)
|
||||||
{
|
{
|
||||||
best = &v;
|
best = &voice;
|
||||||
bestId = v.getNoteId();
|
bestId = voice.getNoteId();
|
||||||
}
|
}
|
||||||
return best != nullptr ? best : &voices[0];
|
return best != nullptr ? best : &voices[0];
|
||||||
}
|
}
|
||||||
@@ -140,181 +146,221 @@ void Engine::noteOn (int noteNumber, float velocity01)
|
|||||||
|
|
||||||
void Engine::noteOff (int noteNumber)
|
void Engine::noteOff (int noteNumber)
|
||||||
{
|
{
|
||||||
for (auto& v : voices)
|
for (auto& voice : voices)
|
||||||
if (v.isActive() && v.getNote() == noteNumber && ! v.isReleased())
|
if (voice.isActive() && voice.getNote() == noteNumber && ! voice.isReleased())
|
||||||
v.noteOff();
|
voice.noteOff();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::allNotesOff()
|
void Engine::allNotesOff()
|
||||||
{
|
{
|
||||||
for (auto& v : voices)
|
for (auto& voice : voices)
|
||||||
if (v.isActive())
|
if (voice.isActive())
|
||||||
v.noteOff();
|
voice.noteOff();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& o)
|
void Engine::readOscParams (const paramIds::Oscillator& ids, OscParams& o) const
|
||||||
{
|
{
|
||||||
const juce::String p = prefix;
|
o.enabled = v (ids.on) > 0.5f;
|
||||||
auto g = [&] (const char* suffix) { return v (apvts, (p + suffix).toRawUTF8()); };
|
o.wave = vic (ids.wave, kNumWavetables - 1);
|
||||||
|
o.wtPos = v (ids.wtPos);
|
||||||
o.enabled = g ("On") > 0.5f;
|
o.warp = vic (ids.warp, (int) WarpMode::Count - 1);
|
||||||
o.wave = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (g ("Wave") * (kNumWavetables - 1)));
|
o.warpAmt = v (ids.warpAmt);
|
||||||
o.wtPos = g ("WtPos");
|
o.coarse = vic (ids.coarse, 48) - 24;
|
||||||
o.warp = (int) std::llround (g ("Warp") * 7.0f);
|
o.fine = vic (ids.fine, 200) - 100;
|
||||||
o.warpAmt = g ("WarpAmt");
|
o.level = v (ids.level);
|
||||||
o.coarse = (int) std::llround (g ("Coarse") * 48.0f) - 24;
|
o.pan = v (ids.pan) * 2.0f - 1.0f;
|
||||||
o.fine = (int) std::llround (g ("Fine") * 200.0f) - 100;
|
o.unison = 1 + vic (ids.unison, kMaxUnison - 1);
|
||||||
o.level = g ("Level");
|
o.detune = v (ids.detune);
|
||||||
o.pan = g ("Pan") * 2.0f - 1.0f;
|
o.spread = v (ids.spread);
|
||||||
o.unison = 1 + (int) std::llround (g ("Unison") * 15.0f);
|
o.phase = v (ids.phase);
|
||||||
o.detune = g ("Detune");
|
o.randPhase = v (ids.randPhase);
|
||||||
o.spread = g ("Spread");
|
|
||||||
o.phase = g ("Phase");
|
|
||||||
o.randPhase = g ("RandPh");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi,
|
void Engine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi,
|
||||||
juce::AudioProcessorValueTreeState& apvts,
|
juce::AudioProcessorValueTreeState&, juce::AudioPlayHead* playhead)
|
||||||
juce::AudioPlayHead* playhead)
|
|
||||||
{
|
{
|
||||||
const int n = buffer.getNumSamples();
|
const int n = buffer.getNumSamples();
|
||||||
const int numCh = buffer.getNumChannels();
|
const int numCh = buffer.getNumChannels();
|
||||||
|
captureControls();
|
||||||
|
|
||||||
// Tempo.
|
// Tempo.
|
||||||
if (playhead != nullptr)
|
if (playhead != nullptr)
|
||||||
if (auto pos = playhead->getPosition())
|
if (auto pos = playhead->getPosition())
|
||||||
if (auto b = pos->getBpm())
|
if (auto tempo = pos->getBpm())
|
||||||
bpm = *b;
|
if (std::isfinite (*tempo) && *tempo > 0.0)
|
||||||
|
bpm = *tempo;
|
||||||
|
|
||||||
// MIDI.
|
// Advance LFOs and capture their values at control-rate render boundaries.
|
||||||
for (const auto meta : midi)
|
|
||||||
{
|
|
||||||
const auto m = meta.getMessage();
|
|
||||||
if (m.isNoteOn() && m.getVelocity() > 0)
|
|
||||||
noteOn (m.getNoteNumber(), m.getFloatVelocity());
|
|
||||||
else if (m.isNoteOff() || (m.isNoteOn() && m.getVelocity() == 0))
|
|
||||||
noteOff (m.getNoteNumber());
|
|
||||||
else if (m.isPitchWheel())
|
|
||||||
pitchBend = (m.getPitchWheelValue() - 8192) / 8192.0f;
|
|
||||||
else if (m.isController())
|
|
||||||
{
|
|
||||||
if (m.getControllerNumber() == 1)
|
|
||||||
modWheel = m.getControllerValue() / 127.0f;
|
|
||||||
else if (m.getControllerNumber() == 120 || m.getControllerNumber() == 123)
|
|
||||||
allNotesOff();
|
|
||||||
}
|
|
||||||
else if (m.isAllNotesOff() || m.isAllSoundOff())
|
|
||||||
allNotesOff();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the voice mix buffer.
|
|
||||||
mixBuffer.setSize (2, n, false, false, true);
|
|
||||||
mixBuffer.clear();
|
|
||||||
float* mixL = mixBuffer.getWritePointer (0);
|
|
||||||
float* mixR = mixBuffer.getWritePointer (1);
|
|
||||||
|
|
||||||
// Advance LFOs and capture their values (control rate).
|
|
||||||
float lfoValues[kNumLfos];
|
|
||||||
for (int i = 0; i < kNumLfos; ++i)
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
{
|
{
|
||||||
lfos[(size_t) i].setTempo (bpm);
|
auto& lfo = lfos[(size_t) i];
|
||||||
lfos[(size_t) i].setParams (v (apvts, kLfoRate[i]),
|
lfo.setTempo (bpm);
|
||||||
v (apvts, kLfoSync[i]) > 0.5f,
|
lfo.setParams (v (paramIds::lfoRate[i]), v (paramIds::lfoSync[i]) > 0.5f,
|
||||||
v (apvts, kLfoBeat[i]),
|
v (paramIds::lfoBeat[i]), vic (paramIds::lfoShape[i], (int) LfoShape::Count - 1),
|
||||||
vic (apvts, kLfoShape[i], 6),
|
v (paramIds::lfoPhase[i]), v (paramIds::lfoFade[i]), v (paramIds::lfoDelay[i]));
|
||||||
v (apvts, kLfoPhase[i]),
|
|
||||||
v (apvts, kLfoFade[i]),
|
|
||||||
v (apvts, kLfoDelay[i]));
|
|
||||||
for (int s = 0; s < n; ++s)
|
|
||||||
lfos[(size_t) i].process();
|
|
||||||
lfoValues[i] = lfos[(size_t) i].getValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const float macroValues[kNumMacros] = { v (apvts, ids::macro1), v (apvts, ids::macro2),
|
|
||||||
v (apvts, ids::macro3), v (apvts, ids::macro4) };
|
|
||||||
|
|
||||||
// Build the render context.
|
// Build the render context.
|
||||||
RenderContext ctx;
|
RenderContext ctx;
|
||||||
ctx.sampleRate = sr;
|
ctx.sampleRate = sr;
|
||||||
ctx.wavetables = &wavetables;
|
ctx.wavetables = &wavetables;
|
||||||
for (int i = 0; i < kNumLfos; ++i) ctx.lfoValues[i] = lfoValues[i];
|
ctx.matrix = &audioMatrix;
|
||||||
for (int i = 0; i < kNumMacros; ++i) ctx.macroValues[i] = macroValues[i];
|
ctx.macros = &audioMacros;
|
||||||
ctx.modWheel = modWheel;
|
for (int i = 0; i < kNumMacros; ++i)
|
||||||
ctx.pitchBend = pitchBend;
|
ctx.macroValues[i] = v (paramIds::macros[i]);
|
||||||
ctx.pitchBendRange = 2.0f;
|
|
||||||
ctx.matrix = &matrix;
|
|
||||||
ctx.macros = ¯os;
|
|
||||||
|
|
||||||
readOscParams (apvts, "oscA", ctx.oscA);
|
readOscParams (paramIds::oscillators[0], ctx.oscA);
|
||||||
readOscParams (apvts, "oscB", ctx.oscB);
|
readOscParams (paramIds::oscillators[1], ctx.oscB);
|
||||||
|
|
||||||
ctx.subOn = v (apvts, ids::subOn) > 0.5f;
|
ctx.subOn = v (ids::subOn) > 0.5f;
|
||||||
ctx.subShape = vic (apvts, ids::subShape, 1);
|
ctx.subShape = vic (ids::subShape, (int) SubShape::Count - 1);
|
||||||
ctx.subOct = vic (apvts, ids::subOct, 2) - 2;
|
ctx.subOct = vic (ids::subOct, 2) - 2;
|
||||||
ctx.subLevel = v (apvts, ids::subLevel);
|
ctx.subLevel = v (ids::subLevel);
|
||||||
|
ctx.noiseOn = v (ids::noiseOn) > 0.5f;
|
||||||
|
ctx.noiseType = vic (ids::noiseType, (int) NoiseType::Count - 1);
|
||||||
|
ctx.noiseLevel = v (ids::noiseLevel);
|
||||||
|
|
||||||
ctx.noiseOn = v (apvts, ids::noiseOn) > 0.5f;
|
ctx.filters.f1On = v (ids::f1On) > 0.5f;
|
||||||
ctx.noiseType = vic (apvts, ids::noiseType, 1);
|
ctx.filters.f1Type = vic (ids::f1Type, (int) FilterModel::Count - 1);
|
||||||
ctx.noiseLevel = v (apvts, ids::noiseLevel);
|
ctx.filters.f1Cutoff = v (ids::f1Cutoff);
|
||||||
|
ctx.filters.f1Res = v (ids::f1Res);
|
||||||
ctx.filters.f1On = v (apvts, ids::f1On) > 0.5f;
|
ctx.filters.f1Drive = v (ids::f1Drive);
|
||||||
ctx.filters.f1Type = vic (apvts, ids::f1Type, 6);
|
ctx.filters.f1Key = v (ids::f1Key);
|
||||||
ctx.filters.f1Cutoff = v (apvts, ids::f1Cutoff);
|
ctx.filters.f1Slope = vic (ids::f1Slope, 2);
|
||||||
ctx.filters.f1Res = v (apvts, ids::f1Res);
|
ctx.filters.f2On = v (ids::f2On) > 0.5f;
|
||||||
ctx.filters.f1Drive = v (apvts, ids::f1Drive);
|
ctx.filters.f2Type = vic (ids::f2Type, (int) FilterModel::Count - 1);
|
||||||
ctx.filters.f1Key = v (apvts, ids::f1Key);
|
ctx.filters.f2Cutoff = v (ids::f2Cutoff);
|
||||||
ctx.filters.f1Slope = vic (apvts, ids::f1Slope, 2);
|
ctx.filters.f2Res = v (ids::f2Res);
|
||||||
|
ctx.filters.f2Drive = v (ids::f2Drive);
|
||||||
ctx.filters.f2On = v (apvts, ids::f2On) > 0.5f;
|
ctx.filters.f2Key = v (ids::f2Key);
|
||||||
ctx.filters.f2Type = vic (apvts, ids::f2Type, 6);
|
ctx.filters.f2Slope = vic (ids::f2Slope, 2);
|
||||||
ctx.filters.f2Cutoff = v (apvts, ids::f2Cutoff);
|
ctx.filters.route = vic (ids::fRoute, (int) FilterRoute::Count - 1);
|
||||||
ctx.filters.f2Res = v (apvts, ids::f2Res);
|
ctx.filters.mix = v (ids::fMix);
|
||||||
ctx.filters.f2Drive = v (apvts, ids::f2Drive);
|
ctx.filters.out = v (ids::fOut) * 1.5f;
|
||||||
ctx.filters.f2Key = v (apvts, ids::f2Key);
|
|
||||||
ctx.filters.f2Slope = vic (apvts, ids::f2Slope, 2);
|
|
||||||
|
|
||||||
ctx.filters.route = vic (apvts, ids::fRoute, 2);
|
|
||||||
ctx.filters.mix = v (apvts, ids::fMix);
|
|
||||||
ctx.filters.out = v (apvts, ids::fOut) * 1.5f;
|
|
||||||
|
|
||||||
for (int i = 0; i < kNumEnvelopes; ++i)
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||||
{
|
{
|
||||||
ctx.envAttack[i] = v (apvts, kEnvAttack[i]);
|
ctx.envAttack[i] = v (paramIds::envAttack[i]);
|
||||||
ctx.envDecay[i] = v (apvts, kEnvDecay[i]);
|
ctx.envDecay[i] = v (paramIds::envDecay[i]);
|
||||||
ctx.envSustain[i] = v (apvts, kEnvSustain[i]);
|
ctx.envSustain[i] = v (paramIds::envSustain[i]);
|
||||||
ctx.envRelease[i] = v (apvts, kEnvRelease[i]);
|
ctx.envRelease[i] = v (paramIds::envRelease[i]);
|
||||||
ctx.envCurve[i] = v (apvts, kEnvCurve[i]);
|
ctx.envCurve[i] = v (paramIds::envCurve[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FX rack.
|
||||||
|
std::array<FxSlotParams, kNumFxSlots> slots;
|
||||||
|
for (int i = 0; i < kNumFxSlots; ++i)
|
||||||
|
{
|
||||||
|
auto& slot = slots[(size_t) i];
|
||||||
|
slot.type = vic (paramIds::fxType[i], (int) FxType::Count - 1);
|
||||||
|
slot.mix = v (paramIds::fxMix[i]);
|
||||||
|
slot.p[0] = v (paramIds::fxP1[i]);
|
||||||
|
slot.p[1] = v (paramIds::fxP2[i]);
|
||||||
|
slot.p[2] = v (paramIds::fxP3[i]);
|
||||||
|
slot.p[3] = v (paramIds::fxP4[i]);
|
||||||
|
}
|
||||||
|
if (v (ids::rave) > 0.5f)
|
||||||
|
RaveController::apply (ctx, slots.data(), kNumFxSlots);
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
auto renderUntil = [&] (int end)
|
||||||
|
{
|
||||||
|
while (offset < end)
|
||||||
|
{
|
||||||
|
const int count = juce::jmin (end - offset, juce::jmin (blockSize, 64));
|
||||||
|
ctx.modWheel = modWheel;
|
||||||
|
ctx.pitchBend = pitchBend;
|
||||||
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
|
ctx.lfoValues[i] = lfos[(size_t) i].getValue();
|
||||||
|
|
||||||
|
const SynthVoice* newest = nullptr;
|
||||||
|
for (const auto& voice : voices)
|
||||||
|
if (voice.isActive() && (newest == nullptr || voice.getNoteId() > newest->getNoteId()))
|
||||||
|
newest = &voice;
|
||||||
|
auto sourceValue = [&] (ModSource source)
|
||||||
|
{
|
||||||
|
const int index = (int) source;
|
||||||
|
if (source >= ModSource::Lfo1 && source <= ModSource::Lfo4)
|
||||||
|
return ctx.lfoValues[index - (int) ModSource::Lfo1];
|
||||||
|
if (source >= ModSource::Macro1 && source <= ModSource::Macro4)
|
||||||
|
return ctx.macroValues[index - (int) ModSource::Macro1];
|
||||||
|
if (source == ModSource::ModWheel) return modWheel;
|
||||||
|
if (source == ModSource::PitchBend) return pitchBend;
|
||||||
|
return newest != nullptr ? newest->getModulationValue (source) : 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<float, kNumModTargets> globalMod {};
|
||||||
|
for (const auto& connection : audioMatrix.connections)
|
||||||
|
if (! isPerVoiceTarget (connection.target))
|
||||||
|
{
|
||||||
|
float value = sourceValue (connection.source);
|
||||||
|
if (connection.bipolar && ! isBipolarSource (connection.source))
|
||||||
|
value = value * 2.0f - 1.0f;
|
||||||
|
globalMod[(size_t) connection.target] += value * connection.depth;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < kNumMacros; ++i)
|
||||||
|
for (const auto& assignment : audioMacros.assignments[(size_t) i])
|
||||||
|
if (! isPerVoiceTarget (assignment.target))
|
||||||
|
globalMod[(size_t) assignment.target] += ctx.macroValues[i] * assignment.depth;
|
||||||
|
|
||||||
|
// Prepare the voice mix buffer.
|
||||||
|
juce::AudioBuffer<float> block (mixBuffer.getArrayOfWritePointers(), 2, count);
|
||||||
|
block.clear();
|
||||||
|
|
||||||
// Render all active voices into the mix buffer.
|
// Render all active voices into the mix buffer.
|
||||||
for (auto& voice : voices)
|
for (auto& voice : voices)
|
||||||
if (voice.isActive())
|
if (voice.isActive())
|
||||||
voice.render (mixL, mixR, n, ctx);
|
voice.render (block.getWritePointer (0), block.getWritePointer (1), count, ctx);
|
||||||
|
|
||||||
// FX rack.
|
auto modulatedSlots = slots;
|
||||||
FxSlotParams slots[kNumFxSlots];
|
|
||||||
for (int i = 0; i < kNumFxSlots; ++i)
|
for (int i = 0; i < kNumFxSlots; ++i)
|
||||||
{
|
modulatedSlots[(size_t) i].mix = juce::jlimit (0.0f, 1.0f,
|
||||||
slots[i].type = juce::jlimit (0, (int) FxType::Count - 1, (int) std::llround (v (apvts, kFxType[i]) * ((int) FxType::Count - 1)));
|
slots[(size_t) i].mix + globalMod[(size_t) ModTarget::Fx1Mix + (size_t) i]);
|
||||||
slots[i].mix = v (apvts, kFxMix[i]);
|
fx.process (block, modulatedSlots.data(), kNumFxSlots);
|
||||||
slots[i].p[0] = v (apvts, kFxP1[i]);
|
|
||||||
slots[i].p[1] = v (apvts, kFxP2[i]);
|
|
||||||
slots[i].p[2] = v (apvts, kFxP3[i]);
|
|
||||||
slots[i].p[3] = v (apvts, kFxP4[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fx.process (mixBuffer, slots, kNumFxSlots);
|
|
||||||
|
|
||||||
// Master + soft limiting.
|
// Master + soft limiting.
|
||||||
const float master = v (apvts, ids::master);
|
const float master = juce::jlimit (0.0f, 1.0f, v (ids::master) + globalMod[(size_t) ModTarget::Master]);
|
||||||
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
{
|
{
|
||||||
float* dest = buffer.getWritePointer (ch);
|
float* dest = buffer.getWritePointer (ch, offset);
|
||||||
const float* src = mixBuffer.getReadPointer (ch < 2 ? ch : 0);
|
const float* src = block.getReadPointer (ch < 2 ? ch : 0);
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < count; ++i)
|
||||||
dest[i] = limit (src[i] * master);
|
dest[i] = limit (src[i] * master);
|
||||||
}
|
}
|
||||||
|
for (auto& lfo : lfos)
|
||||||
|
lfo.advance (count);
|
||||||
|
offset += count;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// MIDI.
|
||||||
|
for (const auto meta : midi)
|
||||||
|
{
|
||||||
|
renderUntil (juce::jlimit (offset, n, meta.samplePosition));
|
||||||
|
const auto message = meta.getMessage();
|
||||||
|
if (message.isNoteOn())
|
||||||
|
noteOn (message.getNoteNumber(), message.getFloatVelocity());
|
||||||
|
else if (message.isNoteOff())
|
||||||
|
noteOff (message.getNoteNumber());
|
||||||
|
else if (message.isPitchWheel())
|
||||||
|
pitchBend = (message.getPitchWheelValue() - 8192) / 8192.0f;
|
||||||
|
else if (message.isAllSoundOff())
|
||||||
|
{
|
||||||
|
for (auto& voice : voices)
|
||||||
|
voice.reset();
|
||||||
|
fx.reset();
|
||||||
|
}
|
||||||
|
else if (message.isAllNotesOff())
|
||||||
|
allNotesOff();
|
||||||
|
else if (message.isController() && message.getControllerNumber() == 1)
|
||||||
|
modWheel = message.getControllerValue() / 127.0f;
|
||||||
|
}
|
||||||
|
renderUntil (n);
|
||||||
|
|
||||||
|
int active = 0;
|
||||||
|
for (const auto& voice : voices)
|
||||||
|
active += voice.isActive() ? 1 : 0;
|
||||||
|
activeVoiceCount.store (active, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
+25
-10
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
#include "Params.h"
|
#include "Params.h"
|
||||||
#include "Wavetable.h"
|
#include "Wavetable.h"
|
||||||
#include "SynthVoice.h"
|
#include "SynthVoice.h"
|
||||||
@@ -19,7 +21,8 @@ namespace serum
|
|||||||
class Engine
|
class Engine
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void prepare (double sampleRate, int blockSize);
|
Engine();
|
||||||
|
void prepare (double sampleRate, int blockSize, juce::AudioProcessorValueTreeState& apvts);
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
void processBlock (juce::AudioBuffer<float>& buffer,
|
void processBlock (juce::AudioBuffer<float>& buffer,
|
||||||
@@ -32,22 +35,31 @@ public:
|
|||||||
void noteOff (int noteNumber);
|
void noteOff (int noteNumber);
|
||||||
void allNotesOff();
|
void allNotesOff();
|
||||||
|
|
||||||
// Modulation / DSP accessors (read-only for the GUI).
|
// Control-state accessors: hold getControlLock() while reading or editing.
|
||||||
|
const juce::CriticalSection& getControlLock() const { return controlLock; }
|
||||||
ModulationMatrix& getMatrix() { return matrix; }
|
ModulationMatrix& getMatrix() { return matrix; }
|
||||||
MacroControls& getMacros() { return macros; }
|
MacroControls& getMacros() { return macros; }
|
||||||
WavetableLibrary& getWavetables() { return wavetables; }
|
const std::array<LFO, kNumLfos>& getLfos() const { return controlLfos; }
|
||||||
const std::array<LFO, kNumLfos>& getLfos() const { return lfos; }
|
const WavetableLibrary& getWavetables() const { return wavetables; }
|
||||||
void setLfoShapeData (int index, const std::vector<float>& data, int steps);
|
void setLfoShapeData (int index, const std::vector<float>& data, int steps);
|
||||||
|
|
||||||
int getActiveVoiceCount() const;
|
int getActiveVoiceCount() const { return activeVoiceCount.load (std::memory_order_relaxed); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::array<SynthVoice, kNumVoices> voices;
|
std::array<SynthVoice, kNumVoices> voices;
|
||||||
std::array<LFO, kNumLfos> lfos;
|
std::array<LFO, kNumLfos> lfos, controlLfos;
|
||||||
WavetableLibrary wavetables;
|
WavetableLibrary wavetables;
|
||||||
FXProcessor fx;
|
FXProcessor fx;
|
||||||
ModulationMatrix matrix;
|
ModulationMatrix matrix, audioMatrix;
|
||||||
MacroControls macros;
|
MacroControls macros, audioMacros;
|
||||||
|
juce::CriticalSection controlLock;
|
||||||
|
|
||||||
|
struct ParameterValue
|
||||||
|
{
|
||||||
|
std::atomic<float>* source;
|
||||||
|
float value;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string_view, ParameterValue> parameterValues;
|
||||||
|
|
||||||
double sr = 44100.0;
|
double sr = 44100.0;
|
||||||
int blockSize = 512;
|
int blockSize = 512;
|
||||||
@@ -55,13 +67,16 @@ private:
|
|||||||
float pitchBend = 0.0f;
|
float pitchBend = 0.0f;
|
||||||
float modWheel = 0.0f;
|
float modWheel = 0.0f;
|
||||||
juce::uint64 noteCounter = 0;
|
juce::uint64 noteCounter = 0;
|
||||||
|
std::atomic<int> activeVoiceCount { 0 };
|
||||||
|
|
||||||
juce::AudioBuffer<float> mixBuffer;
|
juce::AudioBuffer<float> mixBuffer;
|
||||||
|
|
||||||
SynthVoice* findFreeVoice();
|
SynthVoice* findFreeVoice();
|
||||||
SynthVoice* stealVoice();
|
SynthVoice* stealVoice();
|
||||||
|
void captureControls();
|
||||||
void readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& out);
|
float v (const char* id) const;
|
||||||
|
int vic (const char* id, int maxValue) const;
|
||||||
|
void readOscParams (const paramIds::Oscillator& ids, OscParams& out) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
+14
-11
@@ -14,24 +14,27 @@ namespace serum
|
|||||||
|
|
||||||
FXProcessor::FXProcessor()
|
FXProcessor::FXProcessor()
|
||||||
{
|
{
|
||||||
units[0] = std::make_unique<HyperUnit>();
|
for (auto& slotUnits : units)
|
||||||
units[1] = std::make_unique<ChorusUnit>();
|
{
|
||||||
units[2] = std::make_unique<FlangerUnit>();
|
slotUnits[0] = std::make_unique<HyperUnit>();
|
||||||
units[3] = std::make_unique<PhaserUnit>();
|
slotUnits[1] = std::make_unique<ChorusUnit>();
|
||||||
units[4] = std::make_unique<DistortionUnit>();
|
slotUnits[2] = std::make_unique<FlangerUnit>();
|
||||||
units[5] = std::make_unique<EQUnit>();
|
slotUnits[3] = std::make_unique<PhaserUnit>();
|
||||||
units[6] = std::make_unique<CompressorUnit>();
|
slotUnits[4] = std::make_unique<DistortionUnit>();
|
||||||
units[7] = std::make_unique<DelayUnit>();
|
slotUnits[5] = std::make_unique<EQUnit>();
|
||||||
units[8] = std::make_unique<ReverbUnit>();
|
slotUnits[6] = std::make_unique<CompressorUnit>();
|
||||||
|
slotUnits[7] = std::make_unique<DelayUnit>();
|
||||||
|
slotUnits[8] = std::make_unique<ReverbUnit>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FXProcessor::~FXProcessor() = default;
|
FXProcessor::~FXProcessor() = default;
|
||||||
|
|
||||||
void FXProcessor::prepare (double sampleRate, int maxBlockSize)
|
void FXProcessor::prepare (double sampleRate, int maxBlockSize)
|
||||||
{
|
{
|
||||||
for (auto& u : units)
|
for (auto& slotUnits : units)
|
||||||
|
for (auto& u : slotUnits)
|
||||||
u->prepare (sampleRate, maxBlockSize);
|
u->prepare (sampleRate, maxBlockSize);
|
||||||
dry.setSize (2, maxBlockSize, false, false, true);
|
|
||||||
wet.setSize (2, maxBlockSize, false, false, true);
|
wet.setSize (2, maxBlockSize, false, false, true);
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct FxSlotParams
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Reorderable effects rack. Slots are processed in list order; reordering is
|
// Reorderable effects rack. Slots are processed in list order; reordering is
|
||||||
// simply swapping FxSlotParams entries. Nine effect unit implementations are
|
// simply swapping FxSlotParams entries. Nine effect unit implementations are
|
||||||
// owned here and shared across slots.
|
// preconstructed per slot with independent DSP history.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class FXProcessor
|
class FXProcessor
|
||||||
{
|
{
|
||||||
@@ -47,8 +47,10 @@ public:
|
|||||||
static juce::String fxTypeName (int type);
|
static juce::String fxTypeName (int type);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::array<std::unique_ptr<FXUnit>, 9> units;
|
static constexpr int kNumEffectTypes = (int) FxType::Count - 1;
|
||||||
juce::AudioBuffer<float> dry, wet;
|
std::array<std::array<std::unique_ptr<FXUnit>, kNumEffectTypes>, kNumFxSlots> units;
|
||||||
|
std::array<int, kNumFxSlots> activeTypes {};
|
||||||
|
juce::AudioBuffer<float> wet;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
+76
-29
@@ -5,7 +5,7 @@ namespace serum
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr double kPi = 3.14159265358979323846;
|
constexpr double kPi = juce::MathConstants<double>::pi;
|
||||||
|
|
||||||
inline float clampF (float v, float lo, float hi) noexcept
|
inline float clampF (float v, float lo, float hi) noexcept
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,9 @@ void Filter::prepare (double sampleRate, int maxBlockSize)
|
|||||||
|
|
||||||
void Filter::reset()
|
void Filter::reset()
|
||||||
{
|
{
|
||||||
|
lastCutoffHz = lastRes = -1.0f;
|
||||||
|
lastType = -1;
|
||||||
|
ladderG = 0.0;
|
||||||
ic1eq = ic2eq = 0.0;
|
ic1eq = ic2eq = 0.0;
|
||||||
lastG = lastK = 0.0;
|
lastG = lastK = 0.0;
|
||||||
a1 = a2 = a3 = 0.0;
|
a1 = a2 = a3 = 0.0;
|
||||||
@@ -40,6 +43,57 @@ void Filter::reset()
|
|||||||
combDamp = 0.0;
|
combDamp = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Filter::updateCoefficients (float cutoffHz, float res, int type) noexcept
|
||||||
|
{
|
||||||
|
if (cutoffHz == lastCutoffHz && res == lastRes && type == lastType)
|
||||||
|
return;
|
||||||
|
lastCutoffHz = cutoffHz;
|
||||||
|
lastRes = res;
|
||||||
|
lastType = type;
|
||||||
|
|
||||||
|
// Ladder stages are cascaded one-poles, which need an exponential coefficient
|
||||||
|
// (always in (0,1]) for unconditional stability. The TPT SVF (formant/screamer)
|
||||||
|
// uses cached tan()-based coefficients instead.
|
||||||
|
switch ((FilterModel) type)
|
||||||
|
{
|
||||||
|
case FilterModel::LadderLP:
|
||||||
|
case FilterModel::LadderHP:
|
||||||
|
case FilterModel::LadderBP:
|
||||||
|
case FilterModel::Diode:
|
||||||
|
{
|
||||||
|
const double fc = clampD (cutoffHz, 20.0, sr * 0.45);
|
||||||
|
ladderG = 1.0 - std::exp (-2.0 * kPi * fc / sr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FilterModel::Formant:
|
||||||
|
{
|
||||||
|
// morph (0..1) sweeps the three bandpass centres to produce vowel-like spectra.
|
||||||
|
const double m = clampD (maps::hzToCutoff (cutoffHz), 0.0, 1.0);
|
||||||
|
const double base[3] = { 400.0, 1200.0, 2600.0 };
|
||||||
|
const double k = clampD (2.0 * (1.0 - (double) res), 0.05, 2.0);
|
||||||
|
for (int i = 0; i < 3; ++i)
|
||||||
|
{
|
||||||
|
const double fc = base[i] * (0.7 + 1.6 * m) * (i == 2 ? 0.9 : 1.0);
|
||||||
|
const double g = std::tan (kPi * clampD (fc, 30.0, sr * 0.45) / sr);
|
||||||
|
auto& c = formantCoefficients[(size_t) i];
|
||||||
|
c[0] = 1.0 / (1.0 + g * (g + k));
|
||||||
|
c[1] = g * c[0];
|
||||||
|
c[2] = g * c[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FilterModel::Screamer:
|
||||||
|
{
|
||||||
|
const double g = std::tan (kPi * clampD (cutoffHz, 30.0, sr * 0.45) / sr);
|
||||||
|
const double k = clampD (2.0 * (1.0 - (double) res), 0.05, 2.0);
|
||||||
|
updateSvf (g, k);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Filter::updateSvf (double g, double k) noexcept
|
void Filter::updateSvf (double g, double k) noexcept
|
||||||
{
|
{
|
||||||
if (g == lastG && k == lastK)
|
if (g == lastG && k == lastK)
|
||||||
@@ -143,22 +197,16 @@ double Filter::comb (double in, double freqHz, double res, double drive) noexcep
|
|||||||
return (double) y;
|
return (double) y;
|
||||||
}
|
}
|
||||||
|
|
||||||
double Filter::formant (double in, double morph, double res) noexcept
|
double Filter::formant (double in) noexcept
|
||||||
{
|
{
|
||||||
// morph (0..1) sweeps the three bandpass centres to produce vowel-like spectra.
|
|
||||||
const double m = clampD (morph, 0.0, 1.0);
|
|
||||||
const double base[3] = { 400.0, 1200.0, 2600.0 };
|
|
||||||
const double k = clampD (2.0 * (1.0 - res), 0.05, 2.0);
|
|
||||||
|
|
||||||
double out = 0.0;
|
double out = 0.0;
|
||||||
const double gains[3] = { 1.0, 0.8, 0.5 };
|
const double gains[3] = { 1.0, 0.8, 0.5 };
|
||||||
for (int i = 0; i < 3; ++i)
|
for (int i = 0; i < 3; ++i)
|
||||||
{
|
{
|
||||||
const double fc = base[i] * (0.7 + 1.6 * m) * (i == 2 ? 0.9 : 1.0);
|
const auto& c = formantCoefficients[(size_t) i];
|
||||||
const double g = std::tan (kPi * clampD (fc, 30.0, sr * 0.45) / sr);
|
const double a1 = c[0];
|
||||||
const double a1 = 1.0 / (1.0 + g * (g + k));
|
const double a2 = c[1];
|
||||||
const double a2 = g * a1;
|
const double a3 = c[2];
|
||||||
const double a3 = g * a2;
|
|
||||||
const double v3 = in - formantState[(size_t) i][1];
|
const double v3 = in - formantState[(size_t) i][1];
|
||||||
const double v1 = a1 * formantState[(size_t) i][0] + a2 * v3;
|
const double v1 = a1 * formantState[(size_t) i][0] + a2 * v3;
|
||||||
const double v2 = formantState[(size_t) i][1] + a2 * formantState[(size_t) i][0] + a3 * v3;
|
const double v2 = formantState[(size_t) i][1] + a2 * formantState[(size_t) i][0] + a3 * v3;
|
||||||
@@ -169,11 +217,9 @@ double Filter::formant (double in, double morph, double res) noexcept
|
|||||||
return clampD (out * 0.5, -8.0, 8.0);
|
return clampD (out * 0.5, -8.0, 8.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
double Filter::screamer (double in, double cutoffHz, double res, double drive) noexcept
|
double Filter::screamer (double in, double drive) noexcept
|
||||||
{
|
{
|
||||||
const double g = std::tan (kPi * clampD (cutoffHz, 30.0, sr * 0.45) / sr);
|
const double band = svfBand (in, lastG, lastK);
|
||||||
const double k = clampD (2.0 * (1.0 - res), 0.05, 2.0);
|
|
||||||
const double band = svfBand (in, g, k);
|
|
||||||
const double driven = std::tanh (band * (1.0 + drive * 12.0));
|
const double driven = std::tanh (band * (1.0 + drive * 12.0));
|
||||||
return driven * (1.0 - drive * 0.4);
|
return driven * (1.0 - drive * 0.4);
|
||||||
}
|
}
|
||||||
@@ -183,11 +229,8 @@ float Filter::processSample (float in, float cutoffHz, float res, float drive, i
|
|||||||
res = clampF (res, 0.0f, 0.98f);
|
res = clampF (res, 0.0f, 0.98f);
|
||||||
drive = clampF (drive, 0.0f, 1.0f);
|
drive = clampF (drive, 0.0f, 1.0f);
|
||||||
|
|
||||||
// Ladder stages are cascaded one-poles, which need an exponential coefficient
|
updateCoefficients (cutoffHz, res, type);
|
||||||
// (always in (0,1]) for unconditional stability. The TPT SVF (formant/screamer)
|
const double g = ladderG;
|
||||||
// computes its own tan()-based g internally.
|
|
||||||
const double fc = clampD (cutoffHz, 20.0, sr * 0.45);
|
|
||||||
const double g = 1.0 - std::exp (-2.0 * kPi * fc / sr);
|
|
||||||
double out = (double) in;
|
double out = (double) in;
|
||||||
|
|
||||||
switch ((FilterModel) type)
|
switch ((FilterModel) type)
|
||||||
@@ -221,10 +264,10 @@ float Filter::processSample (float in, float cutoffHz, float res, float drive, i
|
|||||||
out = comb (in, cutoffHz, res, drive);
|
out = comb (in, cutoffHz, res, drive);
|
||||||
break;
|
break;
|
||||||
case FilterModel::Formant:
|
case FilterModel::Formant:
|
||||||
out = formant (in, maps::hzToCutoff (cutoffHz), res);
|
out = formant (in);
|
||||||
break;
|
break;
|
||||||
case FilterModel::Screamer:
|
case FilterModel::Screamer:
|
||||||
out = screamer (in, cutoffHz, res, drive);
|
out = screamer (in, drive);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -233,18 +276,22 @@ float Filter::processSample (float in, float cutoffHz, float res, float drive, i
|
|||||||
return (float) clampD (out, -8.0, 8.0);
|
return (float) clampD (out, -8.0, 8.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float Filter::getCutoffHz (float cutoffNorm, float keytrack, float noteHz) noexcept
|
||||||
|
{
|
||||||
|
// Keytrack shifts the cutoff with note pitch.
|
||||||
|
const float noteNumber = (noteHz > 0.0f) ? (69.0f + 12.0f * std::log2f (noteHz / 440.0f)) : 60.0f;
|
||||||
|
const float baseHz = maps::cutoffToHz (cutoffNorm);
|
||||||
|
const float keyFactor = std::pow (2.0f, keytrack * (noteNumber - 60.0f) / 12.0f);
|
||||||
|
return clampF (baseHz * keyFactor, 20.0f, 18000.0f);
|
||||||
|
}
|
||||||
|
|
||||||
void Filter::process (float* samples, int numSamples, float cutoffNorm, float res,
|
void Filter::process (float* samples, int numSamples, float cutoffNorm, float res,
|
||||||
float drive, float keytrack, float noteHz, int type, int slope) noexcept
|
float drive, float keytrack, float noteHz, int type, int slope) noexcept
|
||||||
{
|
{
|
||||||
if (samples == nullptr || numSamples <= 0)
|
if (samples == nullptr || numSamples <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Keytrack shifts the cutoff with note pitch.
|
const float cutoffHz = getCutoffHz (cutoffNorm, keytrack, noteHz);
|
||||||
const float noteNumber = (noteHz > 0.0f) ? (69.0f + 12.0f * std::log2f (noteHz / 440.0f)) : 60.0f;
|
|
||||||
const float baseHz = maps::cutoffToHz (cutoffNorm);
|
|
||||||
const float keyFactor = std::pow (2.0f, keytrack * (noteNumber - 60.0f) / 12.0f);
|
|
||||||
const float cutoffHz = clampF (baseHz * keyFactor, 20.0f, 18000.0f);
|
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
samples[i] = processSample (samples[i], cutoffHz, res, drive, type, slope);
|
samples[i] = processSample (samples[i], cutoffHz, res, drive, type, slope);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -23,9 +23,13 @@ public:
|
|||||||
|
|
||||||
// Single-sample version (used by the comb/naive paths where convenient).
|
// Single-sample version (used by the comb/naive paths where convenient).
|
||||||
float processSample (float in, float cutoffHz, float res, float drive, int type, int slope) noexcept;
|
float processSample (float in, float cutoffHz, float res, float drive, int type, int slope) noexcept;
|
||||||
|
static float getCutoffHz (float cutoffNorm, float keytrack, float noteHz) noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
double sr = 44100.0;
|
double sr = 44100.0;
|
||||||
|
float lastCutoffHz = -1.0f, lastRes = -1.0f;
|
||||||
|
int lastType = -1;
|
||||||
|
double ladderG = 0.0;
|
||||||
|
|
||||||
// TPT SVF state (also reused by formant/screamer).
|
// TPT SVF state (also reused by formant/screamer).
|
||||||
double ic1eq = 0.0, ic2eq = 0.0;
|
double ic1eq = 0.0, ic2eq = 0.0;
|
||||||
@@ -42,15 +46,17 @@ private:
|
|||||||
|
|
||||||
// Formant: three parallel bandpass SVFs (state pairs).
|
// Formant: three parallel bandpass SVFs (state pairs).
|
||||||
std::array<std::array<double, 2>, 3> formantState { { { { 0.0, 0.0 } }, { { 0.0, 0.0 } }, { { 0.0, 0.0 } } } };
|
std::array<std::array<double, 2>, 3> formantState { { { { 0.0, 0.0 } }, { { 0.0, 0.0 } }, { { 0.0, 0.0 } } } };
|
||||||
|
std::array<std::array<double, 3>, 3> formantCoefficients {};
|
||||||
|
|
||||||
|
void updateCoefficients (float cutoffHz, float res, int type) noexcept;
|
||||||
void updateSvf (double g, double k) noexcept;
|
void updateSvf (double g, double k) noexcept;
|
||||||
double svfLow (double in, double g, double k) noexcept;
|
double svfLow (double in, double g, double k) noexcept;
|
||||||
double svfBand (double in, double g, double k) noexcept;
|
double svfBand (double in, double g, double k) noexcept;
|
||||||
double svfHigh (double in, double g, double k) noexcept;
|
double svfHigh (double in, double g, double k) noexcept;
|
||||||
double ladder (double in, double g, double res, double drive, int stages, bool diode) noexcept;
|
double ladder (double in, double g, double res, double drive, int stages, bool diode) noexcept;
|
||||||
double comb (double in, double freqHz, double res, double drive) noexcept;
|
double comb (double in, double freqHz, double res, double drive) noexcept;
|
||||||
double formant (double in, double morph, double res) noexcept;
|
double formant (double in) noexcept;
|
||||||
double screamer (double in, double cutoffHz, double res, double drive) noexcept;
|
double screamer (double in, double drive) noexcept;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ void FilterBank::process (float* l, float* r, int numSamples, const FilterBankPa
|
|||||||
else if (p.route == (int) FilterRoute::Parallel)
|
else if (p.route == (int) FilterRoute::Parallel)
|
||||||
{
|
{
|
||||||
// Run both filters on copies and crossfade.
|
// Run both filters on copies and crossfade.
|
||||||
|
const float cutoff1 = p.f1On ? Filter::getCutoffHz (p.f1Cutoff, p.f1Key, noteHz) : 0.0f;
|
||||||
|
const float cutoff2 = p.f2On ? Filter::getCutoffHz (p.f2Cutoff, p.f2Key, noteHz) : 0.0f;
|
||||||
float f1l = 0.0f, f1r = 0.0f, f2l = 0.0f, f2r = 0.0f;
|
float f1l = 0.0f, f1r = 0.0f, f2l = 0.0f, f2r = 0.0f;
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
{
|
{
|
||||||
@@ -46,13 +48,13 @@ void FilterBank::process (float* l, float* r, int numSamples, const FilterBankPa
|
|||||||
f2l = l[i]; f2r = r[i];
|
f2l = l[i]; f2r = r[i];
|
||||||
if (p.f1On)
|
if (p.f1On)
|
||||||
{
|
{
|
||||||
f1l = f1L.processSample (f1l, maps::cutoffToHz (p.f1Cutoff), p.f1Res, p.f1Drive, p.f1Type, p.f1Slope);
|
f1l = f1L.processSample (f1l, cutoff1, p.f1Res, p.f1Drive, p.f1Type, p.f1Slope);
|
||||||
f1r = f1R.processSample (f1r, maps::cutoffToHz (p.f1Cutoff), p.f1Res, p.f1Drive, p.f1Type, p.f1Slope);
|
f1r = f1R.processSample (f1r, cutoff1, p.f1Res, p.f1Drive, p.f1Type, p.f1Slope);
|
||||||
}
|
}
|
||||||
if (p.f2On)
|
if (p.f2On)
|
||||||
{
|
{
|
||||||
f2l = f2L.processSample (f2l, maps::cutoffToHz (p.f2Cutoff), p.f2Res, p.f2Drive, p.f2Type, p.f2Slope);
|
f2l = f2L.processSample (f2l, cutoff2, p.f2Res, p.f2Drive, p.f2Type, p.f2Slope);
|
||||||
f2r = f2R.processSample (f2r, maps::cutoffToHz (p.f2Cutoff), p.f2Res, p.f2Drive, p.f2Type, p.f2Slope);
|
f2r = f2R.processSample (f2r, cutoff2, p.f2Res, p.f2Drive, p.f2Type, p.f2Slope);
|
||||||
}
|
}
|
||||||
const float m = p.mix;
|
const float m = p.mix;
|
||||||
l[i] = f1l * (1.0f - m) + f2l * m;
|
l[i] = f1l * (1.0f - m) + f2l * m;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace serum
|
|||||||
class Display : public juce::Component
|
class Display : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setTitle (const juce::String& t) { title = t; repaint(); }
|
void setTitle (const juce::String& t) { if (title != t) { title = t; repaint(); } }
|
||||||
void setValue (const juce::String& v) { value = v; repaint(); }
|
void setValue (const juce::String& v) { if (value != v) { value = v; repaint(); } }
|
||||||
|
|
||||||
void paint (juce::Graphics& g) override;
|
void paint (juce::Graphics& g) override;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ namespace serum
|
|||||||
|
|
||||||
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
|
void EnvelopeDisplay::setParams (float a, float d, float s, float r, float c)
|
||||||
{
|
{
|
||||||
|
if (attack == a && decay == d && sustain == s && release == r && curve == c)
|
||||||
|
return;
|
||||||
attack = a; decay = d; sustain = s; release = r; curve = c;
|
attack = a; decay = d; sustain = s; release = r; curve = c;
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnvelopeDisplay::paint (juce::Graphics& g)
|
void EnvelopeDisplay::paint (juce::Graphics& g)
|
||||||
@@ -19,7 +22,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
const float atkShape = 0.3f + curve * 2.7f;
|
const float atkShape = 0.3f + curve * 2.7f;
|
||||||
const float decShape = 3.0f - curve * 2.7f;
|
const float decShape = 3.0f - curve * 2.7f;
|
||||||
|
|
||||||
// Normalise durations for display (attack 0..1, decay 0..0.6, release 0..0.6).
|
// Normalise durations for display, allowing a short sustain plateau.
|
||||||
const float aSec = maps::toSeconds (attack);
|
const float aSec = maps::toSeconds (attack);
|
||||||
const float dSec = maps::toSeconds (decay);
|
const float dSec = maps::toSeconds (decay);
|
||||||
const float rSec = maps::toSeconds (release);
|
const float rSec = maps::toSeconds (release);
|
||||||
@@ -36,7 +39,6 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
|
|
||||||
juce::Path path;
|
juce::Path path;
|
||||||
path.startNewSubPath (left, bottom);
|
path.startNewSubPath (left, bottom);
|
||||||
path.lineTo (left, top);
|
|
||||||
|
|
||||||
// Attack (curve-shaped).
|
// Attack (curve-shaped).
|
||||||
const int steps = 48;
|
const int steps = 48;
|
||||||
@@ -44,7 +46,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
for (int i = 0; i <= steps; ++i)
|
for (int i = 0; i <= steps; ++i)
|
||||||
{
|
{
|
||||||
const float p = (float) i / steps;
|
const float p = (float) i / steps;
|
||||||
const float y = top + (bottom - top) * std::pow (p, atkShape);
|
const float y = bottom - (bottom - top) * std::pow (p, atkShape);
|
||||||
path.lineTo (left + p * (peakX - left), y);
|
path.lineTo (left + p * (peakX - left), y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
for (int i = 0; i <= steps; ++i)
|
for (int i = 0; i <= steps; ++i)
|
||||||
{
|
{
|
||||||
const float p = (float) i / steps;
|
const float p = (float) i / steps;
|
||||||
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
const float y = sustainY + (top - sustainY) * std::pow (1.0f - p, decShape);
|
||||||
path.lineTo (peakX + p * (decX - peakX), y);
|
path.lineTo (peakX + p * (decX - peakX), y);
|
||||||
}
|
}
|
||||||
path.lineTo (decX, sustainY);
|
path.lineTo (decX, sustainY);
|
||||||
@@ -64,7 +66,7 @@ void EnvelopeDisplay::paint (juce::Graphics& g)
|
|||||||
for (int i = 0; i <= steps; ++i)
|
for (int i = 0; i <= steps; ++i)
|
||||||
{
|
{
|
||||||
const float p = (float) i / steps;
|
const float p = (float) i / steps;
|
||||||
const float y = sustainY + (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
const float y = bottom - (bottom - sustainY) * std::pow (1.0f - p, decShape);
|
||||||
path.lineTo (relStartX + p * (right - relStartX), y);
|
path.lineTo (relStartX + p * (right - relStartX), y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ float FilterDisplay::magnitude (float freqHz, float cutoffHz, float res, int typ
|
|||||||
|
|
||||||
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
|
void FilterDisplay::setParams (int t, float c, float r, float d, int s)
|
||||||
{
|
{
|
||||||
|
if (type == t && cutoff == c && res == r && drive == d && slope == s)
|
||||||
|
return;
|
||||||
type = t; cutoff = c; res = r; drive = d; slope = s;
|
type = t; cutoff = c; res = r; drive = d; slope = s;
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FilterDisplay::paint (juce::Graphics& g)
|
void FilterDisplay::paint (juce::Graphics& g)
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ namespace serum
|
|||||||
{
|
{
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Filter frequency-response view (magnitude vs log frequency).
|
// Approximate filter frequency-response view (magnitude vs log frequency).
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class FilterDisplay : public juce::Component
|
class FilterDisplay : public juce::Component,
|
||||||
|
public juce::SettableTooltipClient
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
FilterDisplay() { setTooltip ("Approximate response preview; slope, drive and modulation are not modelled."); }
|
||||||
void setParams (int type, float cutoff, float res, float drive, int slope);
|
void setParams (int type, float cutoff, float res, float drive, int slope);
|
||||||
void setEnabled (bool e) { enabled = e; }
|
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
|
||||||
|
|
||||||
void paint (juce::Graphics& g) override;
|
void paint (juce::Graphics& g) override;
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ namespace serum
|
|||||||
|
|
||||||
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
||||||
{
|
{
|
||||||
steps = juce::jlimit (2, 64, s);
|
const int newSteps = juce::jlimit (2, 64, s);
|
||||||
shapeData = data;
|
const size_t dataSize = std::min (data.size(), size_t (64));
|
||||||
if ((int) shapeData.size() < 64)
|
if (steps == newSteps && shapeData.size() == 64
|
||||||
|
&& std::equal (data.begin(), data.begin() + dataSize, shapeData.begin())
|
||||||
|
&& std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return v == 0.0f; }))
|
||||||
|
return;
|
||||||
|
|
||||||
|
steps = newSteps;
|
||||||
|
shapeData.assign (data.begin(), data.begin() + dataSize);
|
||||||
shapeData.resize (64, 0.0f);
|
shapeData.resize (64, 0.0f);
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace serum
|
|||||||
class LFODisplay : public juce::Component
|
class LFODisplay : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setShape (int s) { shape = s; repaint(); }
|
void setShape (int s) { if (shape != s) { shape = s; repaint(); } }
|
||||||
void setShapeData (const std::vector<float>& data, int steps);
|
void setShapeData (const std::vector<float>& data, int steps);
|
||||||
void setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
|
void setOnShapeEdited (std::function<void (const std::vector<float>&, int)> cb) { onEdited = std::move (cb); }
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ ToggleButton::ToggleButton (const juce::String& lbl) : label (lbl)
|
|||||||
|
|
||||||
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
|
void ToggleButton::attach (juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId)
|
||||||
{
|
{
|
||||||
param = apvts.getParameter (paramId);
|
attachment.reset();
|
||||||
if (param != nullptr)
|
if (auto* param = apvts.getParameter (paramId))
|
||||||
{
|
{
|
||||||
state = param->getValue() > 0.5f;
|
|
||||||
attachment = std::make_unique<juce::ParameterAttachment> (*param,
|
attachment = std::make_unique<juce::ParameterAttachment> (*param,
|
||||||
[this] (float newValue) { setToggleState (newValue > 0.5f); });
|
[this] (float newValue) { setToggleState (newValue > 0.5f); });
|
||||||
|
attachment->sendInitialUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +60,9 @@ void ToggleButton::mouseDown (const juce::MouseEvent&)
|
|||||||
{
|
{
|
||||||
onClick (newState);
|
onClick (newState);
|
||||||
}
|
}
|
||||||
else if (param != nullptr)
|
else if (attachment != nullptr)
|
||||||
{
|
{
|
||||||
param->setValueNotifyingHost (newState ? 1.0f : 0.0f);
|
attachment->setValueAsCompleteGesture (newState ? 1.0f : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
setToggleState (newState);
|
setToggleState (newState);
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool state = false;
|
bool state = false;
|
||||||
juce::String label;
|
juce::String label;
|
||||||
juce::RangedAudioParameter* param = nullptr;
|
|
||||||
std::unique_ptr<juce::ParameterAttachment> attachment;
|
std::unique_ptr<juce::ParameterAttachment> attachment;
|
||||||
std::function<void (bool)> onClick;
|
std::function<void (bool)> onClick;
|
||||||
juce::Colour onColour = theme::accent;
|
juce::Colour onColour = theme::accent;
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ namespace serum
|
|||||||
class WaveformDisplay : public juce::Component
|
class WaveformDisplay : public juce::Component
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setWavetables (const WavetableLibrary* lib) { wtLib = lib; }
|
void setWavetables (const WavetableLibrary* lib) { if (wtLib != lib) { wtLib = lib; repaint(); } }
|
||||||
void setWaveIndex (int index) { wave = index; }
|
void setWaveIndex (int index) { if (wave != index) { wave = index; repaint(); } }
|
||||||
void setFramePosition (float pos) { wtPos = pos; }
|
void setFramePosition (float pos) { if (wtPos != pos) { wtPos = pos; repaint(); } }
|
||||||
void setEnabled (bool e) { enabled = e; }
|
void setEnabled (bool e) { if (enabled != e) { enabled = e; repaint(); } }
|
||||||
|
|
||||||
void paint (juce::Graphics& g) override;
|
void paint (juce::Graphics& g) override;
|
||||||
|
|
||||||
|
|||||||
+33
-20
@@ -10,15 +10,17 @@ void LFO::reset()
|
|||||||
delayCounter = 0.0;
|
delayCounter = 0.0;
|
||||||
fadeCounter = 0.0;
|
fadeCounter = 0.0;
|
||||||
fadeVal = 1.0f;
|
fadeVal = 1.0f;
|
||||||
holdValue = 0.0f;
|
holdValue = rng.nextFloat() * 2.0f - 1.0f;
|
||||||
prevPhase = 0.0;
|
|
||||||
prevDelayParam = -1.0f;
|
prevDelayParam = -1.0f;
|
||||||
prevFadeParam = -1.0f;
|
prevFadeParam = -1.0f;
|
||||||
shapeBuffer.assign ((size_t) kShapePoints, 0.0f);
|
if (shapeBuffer.empty())
|
||||||
|
{
|
||||||
|
shapeBuffer.resize ((size_t) kShapePoints);
|
||||||
// default step sequence
|
// default step sequence
|
||||||
for (int i = 0; i < kShapePoints; ++i)
|
for (int i = 0; i < kShapePoints; ++i)
|
||||||
shapeBuffer[(size_t) i] = (i % 2 == 0) ? 1.0f : -1.0f;
|
shapeBuffer[(size_t) i] = (i % 2 == 0) ? 1.0f : -1.0f;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
|
void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
|
||||||
float fade, float delay)
|
float fade, float delay)
|
||||||
@@ -26,10 +28,10 @@ void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
|
|||||||
sync = s;
|
sync = s;
|
||||||
beat = b;
|
beat = b;
|
||||||
shape = juce::jlimit (0, (int) LfoShape::Count - 1, shp);
|
shape = juce::jlimit (0, (int) LfoShape::Count - 1, shp);
|
||||||
phase = juce::jlimit (0.0f, 1.0f, ph);
|
phaseOffset = juce::jlimit (0.0f, 1.0f, ph);
|
||||||
|
|
||||||
if (sync)
|
if (sync)
|
||||||
rateHz = maps::beatToMultiplier (beat) * (tempo / 60.0);
|
rateHz = (tempo / 60.0) / maps::beatToMultiplier (beat);
|
||||||
else
|
else
|
||||||
rateHz = maps::rateToHz (rateNorm);
|
rateHz = maps::rateToHz (rateNorm);
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
|
|||||||
prevDelayParam = delay;
|
prevDelayParam = delay;
|
||||||
prevFadeParam = fade;
|
prevFadeParam = fade;
|
||||||
}
|
}
|
||||||
|
value = delayCounter > 0.0 ? 0.0f : shapeValue() * fadeVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LFO::setShapeData (const std::vector<float>& data, int steps)
|
void LFO::setShapeData (const std::vector<float>& data, int steps)
|
||||||
@@ -54,17 +57,18 @@ void LFO::setShapeData (const std::vector<float>& data, int steps)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
shapeSteps = juce::jlimit (2, kShapePoints, steps);
|
shapeSteps = juce::jlimit (2, kShapePoints, steps);
|
||||||
shapeBuffer.assign (data.begin(), data.end());
|
for (size_t i = 0; i < shapeBuffer.size(); ++i)
|
||||||
shapeBuffer.resize ((size_t) kShapePoints, 0.0f);
|
shapeBuffer[i] = i < data.size() && std::isfinite (data[i])
|
||||||
|
? juce::jlimit (-1.0f, 1.0f, data[i]) : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
float LFO::shapeValue() noexcept
|
float LFO::shapeValue() noexcept
|
||||||
{
|
{
|
||||||
const float p = (float) phase;
|
const float p = getPhase();
|
||||||
switch ((LfoShape) shape)
|
switch ((LfoShape) shape)
|
||||||
{
|
{
|
||||||
case LfoShape::Sine:
|
case LfoShape::Sine:
|
||||||
return std::sin (p * 6.28318530717958647692f);
|
return std::sin (p * juce::MathConstants<float>::twoPi);
|
||||||
case LfoShape::Triangle:
|
case LfoShape::Triangle:
|
||||||
return 1.0f - 4.0f * std::abs (p - 0.5f);
|
return 1.0f - 4.0f * std::abs (p - 0.5f);
|
||||||
case LfoShape::Saw:
|
case LfoShape::Saw:
|
||||||
@@ -72,11 +76,7 @@ float LFO::shapeValue() noexcept
|
|||||||
case LfoShape::Square:
|
case LfoShape::Square:
|
||||||
return (p < 0.5f) ? 1.0f : -1.0f;
|
return (p < 0.5f) ? 1.0f : -1.0f;
|
||||||
case LfoShape::SampleHold:
|
case LfoShape::SampleHold:
|
||||||
{
|
|
||||||
if (phase < prevPhase)
|
|
||||||
holdValue = rng.nextFloat() * 2.0f - 1.0f;
|
|
||||||
return holdValue;
|
return holdValue;
|
||||||
}
|
|
||||||
case LfoShape::StepSeq:
|
case LfoShape::StepSeq:
|
||||||
{
|
{
|
||||||
const int idx = juce::jlimit (0, shapeSteps - 1, (int) (p * shapeSteps));
|
const int idx = juce::jlimit (0, shapeSteps - 1, (int) (p * shapeSteps));
|
||||||
@@ -97,27 +97,40 @@ float LFO::shapeValue() noexcept
|
|||||||
|
|
||||||
float LFO::process() noexcept
|
float LFO::process() noexcept
|
||||||
{
|
{
|
||||||
|
return advance (1);
|
||||||
|
}
|
||||||
|
|
||||||
|
float LFO::advance (int numSamples) noexcept
|
||||||
|
{
|
||||||
|
if (numSamples <= 0)
|
||||||
|
return value;
|
||||||
|
|
||||||
// Start delay.
|
// Start delay.
|
||||||
if (delayCounter > 0.0)
|
if (delayCounter > 0.0)
|
||||||
{
|
{
|
||||||
delayCounter -= 1.0;
|
const int skipped = (int) juce::jmin ((double) numSamples, std::ceil (delayCounter));
|
||||||
value = 0.0f;
|
delayCounter = juce::jmax (0.0, delayCounter - skipped);
|
||||||
return 0.0f;
|
numSamples -= skipped;
|
||||||
|
if (numSamples == 0)
|
||||||
|
return value = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fade-in ramp.
|
// Fade-in ramp.
|
||||||
if (fadeVal < 1.0f)
|
if (fadeVal < 1.0f)
|
||||||
{
|
{
|
||||||
fadeCounter += 1.0;
|
fadeCounter += numSamples;
|
||||||
if (fadeSeconds > 0.0)
|
if (fadeSeconds > 0.0)
|
||||||
fadeVal = (float) juce::jlimit (0.0, 1.0, fadeCounter / (fadeSeconds * sr));
|
fadeVal = (float) juce::jlimit (0.0, 1.0, fadeCounter / (fadeSeconds * sr));
|
||||||
else
|
else
|
||||||
fadeVal = 1.0f;
|
fadeVal = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPhase = phase;
|
phase += rateHz * numSamples / sr;
|
||||||
phase += rateHz / sr;
|
const double cycles = std::floor (phase);
|
||||||
phase -= std::floor (phase);
|
phase -= cycles;
|
||||||
|
if (shape == (int) LfoShape::SampleHold)
|
||||||
|
for (int i = 0; i < (int) cycles; ++i)
|
||||||
|
holdValue = rng.nextFloat() * 2.0f - 1.0f;
|
||||||
|
|
||||||
value = shapeValue() * fadeVal;
|
value = shapeValue() * fadeVal;
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
+4
-2
@@ -15,6 +15,7 @@ class LFO
|
|||||||
public:
|
public:
|
||||||
static constexpr int kShapePoints = 64;
|
static constexpr int kShapePoints = 64;
|
||||||
|
|
||||||
|
LFO() { reset(); }
|
||||||
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
@@ -24,7 +25,8 @@ public:
|
|||||||
void setShapeData (const std::vector<float>& data, int steps);
|
void setShapeData (const std::vector<float>& data, int steps);
|
||||||
|
|
||||||
float process() noexcept; // advance and return current value
|
float process() noexcept; // advance and return current value
|
||||||
float getPhase() const noexcept { return (float) phase; }
|
float advance (int numSamples) noexcept;
|
||||||
|
float getPhase() const noexcept { return (float) (phase + phaseOffset - std::floor (phase + phaseOffset)); }
|
||||||
float getValue() const noexcept { return value; }
|
float getValue() const noexcept { return value; }
|
||||||
|
|
||||||
const std::vector<float>& getShapeData() const noexcept { return shapeBuffer; }
|
const std::vector<float>& getShapeData() const noexcept { return shapeBuffer; }
|
||||||
@@ -35,6 +37,7 @@ private:
|
|||||||
double tempo = 120.0;
|
double tempo = 120.0;
|
||||||
|
|
||||||
double phase = 0.0;
|
double phase = 0.0;
|
||||||
|
double phaseOffset = 0.0;
|
||||||
double rateHz = 1.0;
|
double rateHz = 1.0;
|
||||||
float value = 0.0f;
|
float value = 0.0f;
|
||||||
|
|
||||||
@@ -55,7 +58,6 @@ private:
|
|||||||
|
|
||||||
juce::Random rng;
|
juce::Random rng;
|
||||||
float holdValue = 0.0f;
|
float holdValue = 0.0f;
|
||||||
double prevPhase = 0.0;
|
|
||||||
|
|
||||||
float shapeValue() noexcept;
|
float shapeValue() noexcept;
|
||||||
};
|
};
|
||||||
|
|||||||
+26
-11
@@ -5,10 +5,11 @@ namespace serum
|
|||||||
|
|
||||||
bool MacroControls::addAssignment (int macro, ModTarget target, float depth)
|
bool MacroControls::addAssignment (int macro, ModTarget target, float depth)
|
||||||
{
|
{
|
||||||
macro = juce::jlimit (0, kNumMacros - 1, macro);
|
if (macro < 0 || macro >= kNumMacros
|
||||||
if (target == ModTarget::None || (int) assignments[(size_t) macro].size() >= kMaxAssignments)
|
|| (int) target < 0 || (int) target >= kNumModTargets
|
||||||
|
|| ! std::isfinite (depth) || (int) assignments[(size_t) macro].size() >= kMaxAssignments)
|
||||||
return false;
|
return false;
|
||||||
assignments[(size_t) macro].push_back ({ target, depth });
|
assignments[(size_t) macro].push_back ({ target, juce::jlimit (-1.0f, 1.0f, depth) });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,12 +41,21 @@ juce::String MacroControls::macroName (int index)
|
|||||||
|
|
||||||
juce::ValueTree MacroControls::toValueTree() const
|
juce::ValueTree MacroControls::toValueTree() const
|
||||||
{
|
{
|
||||||
|
MacroControls validated;
|
||||||
|
for (int m = 0; m < kNumMacros; ++m)
|
||||||
|
for (const auto& a : assignments[(size_t) m])
|
||||||
|
{
|
||||||
|
if ((int) validated.assignments[(size_t) m].size() >= kMaxAssignments)
|
||||||
|
break;
|
||||||
|
if (! validated.addAssignment (m, a.target, a.depth))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
juce::ValueTree tree ("MACROS");
|
juce::ValueTree tree ("MACROS");
|
||||||
for (int m = 0; m < kNumMacros; ++m)
|
for (int m = 0; m < kNumMacros; ++m)
|
||||||
{
|
{
|
||||||
juce::ValueTree mac ("MACRO");
|
juce::ValueTree mac ("MACRO");
|
||||||
mac.setProperty ("index", m, nullptr);
|
mac.setProperty ("index", m, nullptr);
|
||||||
for (const auto& a : assignments[(size_t) m])
|
for (const auto& a : validated.assignments[(size_t) m])
|
||||||
{
|
{
|
||||||
juce::ValueTree asg ("ASSIGN");
|
juce::ValueTree asg ("ASSIGN");
|
||||||
asg.setProperty ("target", modTargetToString (a.target), nullptr);
|
asg.setProperty ("target", modTargetToString (a.target), nullptr);
|
||||||
@@ -60,23 +70,28 @@ juce::ValueTree MacroControls::toValueTree() const
|
|||||||
void MacroControls::fromValueTree (const juce::ValueTree& tree)
|
void MacroControls::fromValueTree (const juce::ValueTree& tree)
|
||||||
{
|
{
|
||||||
clear();
|
clear();
|
||||||
if (! tree.isValid())
|
if (! tree.hasType ("MACROS"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (const auto& mac : tree)
|
for (const auto& mac : tree)
|
||||||
{
|
{
|
||||||
if (! mac.hasType ("MACRO"))
|
if (! mac.hasType ("MACRO"))
|
||||||
continue;
|
continue;
|
||||||
const int index = juce::jlimit (0, kNumMacros - 1, (int) mac.getProperty ("index", 0));
|
const double savedIndex = (double) mac.getProperty ("index", -1);
|
||||||
|
if (! std::isfinite (savedIndex) || savedIndex < 0.0 || savedIndex >= kNumMacros
|
||||||
|
|| std::floor (savedIndex) != savedIndex)
|
||||||
|
continue;
|
||||||
|
const int index = (int) savedIndex;
|
||||||
for (const auto& asg : mac)
|
for (const auto& asg : mac)
|
||||||
{
|
{
|
||||||
|
if ((int) assignments[(size_t) index].size() >= kMaxAssignments)
|
||||||
|
break;
|
||||||
if (! asg.hasType ("ASSIGN"))
|
if (! asg.hasType ("ASSIGN"))
|
||||||
continue;
|
continue;
|
||||||
MacroAssignment a;
|
if (! addAssignment (index,
|
||||||
a.target = modTargetFromString (asg.getProperty ("target").toString());
|
modTargetFromString (asg.getProperty ("target").toString()),
|
||||||
a.depth = (float) asg.getProperty ("depth", 0.0);
|
(float) asg.getProperty ("depth", 0.0)))
|
||||||
if (a.target != ModTarget::None)
|
continue;
|
||||||
assignments[(size_t) index].push_back (a);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-11
@@ -5,9 +5,11 @@ namespace serum
|
|||||||
|
|
||||||
bool ModulationMatrix::addConnection (ModSource source, ModTarget target, float depth, bool bipolar)
|
bool ModulationMatrix::addConnection (ModSource source, ModTarget target, float depth, bool bipolar)
|
||||||
{
|
{
|
||||||
if (target == ModTarget::None || (int) connections.size() >= kMaxConnections)
|
if ((int) source < 0 || (int) source >= kNumModSources
|
||||||
|
|| (int) target < 0 || (int) target >= kNumModTargets
|
||||||
|
|| ! std::isfinite (depth) || (int) connections.size() >= kMaxConnections)
|
||||||
return false;
|
return false;
|
||||||
connections.push_back ({ source, target, depth, bipolar });
|
connections.push_back ({ source, target, juce::jlimit (-1.0f, 1.0f, depth), bipolar });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +28,16 @@ void ModulationMatrix::removeAllWithTarget (ModTarget target)
|
|||||||
|
|
||||||
juce::ValueTree ModulationMatrix::toValueTree() const
|
juce::ValueTree ModulationMatrix::toValueTree() const
|
||||||
{
|
{
|
||||||
juce::ValueTree tree ("MODMATRIX");
|
ModulationMatrix validated;
|
||||||
for (const auto& c : connections)
|
for (const auto& c : connections)
|
||||||
|
{
|
||||||
|
if (validated.size() >= kMaxConnections)
|
||||||
|
break;
|
||||||
|
if (! validated.addConnection (c.source, c.target, c.depth, c.bipolar))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
juce::ValueTree tree ("MODMATRIX");
|
||||||
|
for (const auto& c : validated.connections)
|
||||||
{
|
{
|
||||||
juce::ValueTree con ("CONNECTION");
|
juce::ValueTree con ("CONNECTION");
|
||||||
con.setProperty ("source", modSourceToString (c.source), nullptr);
|
con.setProperty ("source", modSourceToString (c.source), nullptr);
|
||||||
@@ -42,20 +52,24 @@ juce::ValueTree ModulationMatrix::toValueTree() const
|
|||||||
void ModulationMatrix::fromValueTree (const juce::ValueTree& tree)
|
void ModulationMatrix::fromValueTree (const juce::ValueTree& tree)
|
||||||
{
|
{
|
||||||
connections.clear();
|
connections.clear();
|
||||||
if (! tree.isValid())
|
if (! tree.hasType ("MODMATRIX"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (const auto& con : tree)
|
for (const auto& con : tree)
|
||||||
{
|
{
|
||||||
|
if ((int) connections.size() >= kMaxConnections)
|
||||||
|
break;
|
||||||
if (! con.hasType ("CONNECTION"))
|
if (! con.hasType ("CONNECTION"))
|
||||||
continue;
|
continue;
|
||||||
ModConnection c;
|
const auto sourceName = con.getProperty ("source").toString();
|
||||||
c.source = modSourceFromString (con.getProperty ("source").toString());
|
const auto source = modSourceFromString (sourceName);
|
||||||
c.target = modTargetFromString (con.getProperty ("target").toString());
|
if (sourceName.isEmpty() || modSourceToString (source) != sourceName)
|
||||||
c.depth = (float) con.getProperty ("depth", 0.0);
|
continue;
|
||||||
c.bipolar = (bool) con.getProperty ("bipolar", false);
|
if (! addConnection (source,
|
||||||
if (c.target != ModTarget::None && (int) connections.size() < kMaxConnections)
|
modTargetFromString (con.getProperty ("target").toString()),
|
||||||
connections.push_back (c);
|
(float) con.getProperty ("depth", 0.0),
|
||||||
|
(bool) con.getProperty ("bipolar", false)))
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-22
@@ -6,49 +6,78 @@ namespace serum
|
|||||||
void Oscillator::reset()
|
void Oscillator::reset()
|
||||||
{
|
{
|
||||||
for (auto& v : voices)
|
for (auto& v : voices)
|
||||||
{
|
v = SubVoice {};
|
||||||
v.phase = 0.0;
|
|
||||||
v.detuneRatio = 1.0;
|
|
||||||
v.pan = 0.0f;
|
|
||||||
v.level = 1.0f;
|
|
||||||
}
|
|
||||||
activeUnison = 1;
|
activeUnison = 1;
|
||||||
|
initializedUnison = 0;
|
||||||
|
paramsValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Oscillator::noteOn (double freqHz, const OscParams& p, juce::uint32 seed)
|
void Oscillator::noteOn (double freqHz, const OscParams& p, juce::uint32 seed)
|
||||||
{
|
{
|
||||||
jassert (freqHz > 0.0);
|
jassert (freqHz > 0.0);
|
||||||
rng.setSeed (seed);
|
rng.setSeed (seed);
|
||||||
|
initializedUnison = 0;
|
||||||
|
paramsValid = false;
|
||||||
|
setParams (p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Oscillator::setParams (const OscParams& p) noexcept
|
||||||
|
{
|
||||||
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
||||||
activeUnison = uni;
|
const bool countChanged = ! paramsValid || uni != activeUnison;
|
||||||
|
const bool detuneChanged = countChanged || p.detune != cachedParams.detune;
|
||||||
|
const bool panChanged = countChanged || p.pan != cachedParams.pan || p.spread != cachedParams.spread;
|
||||||
|
if (! detuneChanged && ! panChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
const double basePhase = (double) p.phase * kTwoPi;
|
const double basePhase = (double) p.phase * kTwoPi;
|
||||||
|
for (int v = initializedUnison; v < uni; ++v)
|
||||||
for (int v = 0; v < uni; ++v)
|
|
||||||
{
|
{
|
||||||
// Even phase spacing prevents cancellation across unison voices.
|
// Even phase spacing distributes the initial unison phases.
|
||||||
double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0;
|
double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0;
|
||||||
double random = rng.nextFloat() * (double) p.randPhase * kTwoPi;
|
double random = rng.nextFloat() * (double) p.randPhase * kTwoPi;
|
||||||
voices[(size_t) v].phase = basePhase + offset + random;
|
voices[(size_t) v].phase = basePhase + offset + random;
|
||||||
|
}
|
||||||
|
initializedUnison = juce::jmax (initializedUnison, uni);
|
||||||
|
|
||||||
|
const float unisonLevel = 1.0f / std::sqrt ((float) uni);
|
||||||
|
for (int v = 0; v < uni; ++v)
|
||||||
|
{
|
||||||
|
auto& sv = voices[(size_t) v];
|
||||||
|
if (detuneChanged)
|
||||||
|
{
|
||||||
// Detune: linear spread in cents, 0..50 cents at full depth.
|
// Detune: linear spread in cents, 0..50 cents at full depth.
|
||||||
double detuneCents = 0.0;
|
double detuneCents = 0.0;
|
||||||
if (uni > 1)
|
if (uni > 1)
|
||||||
detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0));
|
detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0));
|
||||||
voices[(size_t) v].detuneRatio = std::pow (2.0, detuneCents / 1200.0);
|
sv.detuneRatio = std::pow (2.0, detuneCents / 1200.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panChanged)
|
||||||
|
{
|
||||||
// Stereo spread.
|
// Stereo spread.
|
||||||
float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f;
|
float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f;
|
||||||
voices[(size_t) v].pan = panPos;
|
panPos = juce::jlimit (-1.0f, 1.0f, panPos + p.pan);
|
||||||
|
|
||||||
|
// Constant-power pan.
|
||||||
|
const float panAngle = (panPos + 1.0f) * 0.5f * juce::MathConstants<float>::halfPi;
|
||||||
|
sv.panL = std::cos (panAngle);
|
||||||
|
sv.panR = std::sin (panAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countChanged)
|
||||||
|
{
|
||||||
// Gain scaling with a centre emphasis for odd unison counts.
|
// Gain scaling with a centre emphasis for odd unison counts.
|
||||||
float lvl = 1.0f / std::sqrt ((float) uni);
|
float lvl = unisonLevel;
|
||||||
if ((uni & 1) && v == uni / 2)
|
if ((uni & 1) && v == uni / 2)
|
||||||
lvl *= 1.3f;
|
lvl *= 1.3f;
|
||||||
voices[(size_t) v].level = lvl;
|
sv.level = lvl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
activeUnison = uni;
|
||||||
|
cachedParams = p;
|
||||||
|
paramsValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept
|
float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept
|
||||||
{
|
{
|
||||||
@@ -96,7 +125,7 @@ void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double fre
|
|||||||
if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0)
|
if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
const int uni = activeUnison;
|
||||||
const float framePos = p.wtPos * 255.0f;
|
const float framePos = p.wtPos * 255.0f;
|
||||||
const double phaseInc = kTwoPi * freqHz / sr;
|
const double phaseInc = kTwoPi * freqHz / sr;
|
||||||
|
|
||||||
@@ -116,14 +145,9 @@ void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double fre
|
|||||||
float sample = wt.readSafe (framePos, warpPhase (phase01, p));
|
float sample = wt.readSafe (framePos, warpPhase (phase01, p));
|
||||||
sample = warpSample (sample, p);
|
sample = warpSample (sample, p);
|
||||||
|
|
||||||
// Constant-power pan.
|
|
||||||
const float panAngle = (sv.pan + 1.0f) * 0.5f * 1.5707963267948966f;
|
|
||||||
const float panL = std::cos (panAngle);
|
|
||||||
const float panR = std::sin (panAngle);
|
|
||||||
|
|
||||||
const float gain = sv.level * p.level;
|
const float gain = sv.level * p.level;
|
||||||
accL += sample * gain * panL;
|
accL += sample * gain * sv.panL;
|
||||||
accR += sample * gain * panR;
|
accR += sample * gain * sv.panR;
|
||||||
}
|
}
|
||||||
|
|
||||||
outL += accL;
|
outL += accL;
|
||||||
|
|||||||
+8
-3
@@ -41,8 +41,9 @@ public:
|
|||||||
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// (Re)configure unison sub-voices: phase offsets, detune, pan and gain.
|
// Initialise a fresh note's unison sub-voices: phase offsets, detune, pan and gain.
|
||||||
void noteOn (double freqHz, const OscParams& p, juce::uint32 seed);
|
void noteOn (double freqHz, const OscParams& p, juce::uint32 seed);
|
||||||
|
void setParams (const OscParams& p) noexcept;
|
||||||
|
|
||||||
// Accumulate this oscillator's contribution into outL/outR.
|
// Accumulate this oscillator's contribution into outL/outR.
|
||||||
void processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
|
void processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
|
||||||
@@ -55,16 +56,20 @@ private:
|
|||||||
{
|
{
|
||||||
double phase = 0.0;
|
double phase = 0.0;
|
||||||
double detuneRatio = 1.0;
|
double detuneRatio = 1.0;
|
||||||
float pan = 0.0f;
|
float panL = 0.70710678f;
|
||||||
|
float panR = 0.70710678f;
|
||||||
float level = 1.0f;
|
float level = 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::array<SubVoice, kMaxUnison> voices;
|
std::array<SubVoice, kMaxUnison> voices;
|
||||||
int activeUnison = 1;
|
int activeUnison = 1;
|
||||||
|
int initializedUnison = 0;
|
||||||
|
bool paramsValid = false;
|
||||||
|
OscParams cachedParams;
|
||||||
double sr = 44100.0;
|
double sr = 44100.0;
|
||||||
juce::Random rng;
|
juce::Random rng;
|
||||||
|
|
||||||
static constexpr double kTwoPi = 6.28318530717958647692;
|
static constexpr double kTwoPi = juce::MathConstants<double>::twoPi;
|
||||||
|
|
||||||
float warpPhase (float phase, const OscParams& p) const noexcept;
|
float warpPhase (float phase, const OscParams& p) const noexcept;
|
||||||
float warpSample (float sample, const OscParams& p) const noexcept;
|
float warpSample (float sample, const OscParams& p) const noexcept;
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ ModSource modSourceFromString (const juce::String& s)
|
|||||||
for (int i = 0; i < kNumModSources; ++i)
|
for (int i = 0; i < kNumModSources; ++i)
|
||||||
if (s == modSourceToString ((ModSource) i))
|
if (s == modSourceToString ((ModSource) i))
|
||||||
return (ModSource) i;
|
return (ModSource) i;
|
||||||
return ModSource::Lfo1;
|
return ModSource::Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
juce::String modTargetName (ModTarget t)
|
juce::String modTargetName (ModTarget t)
|
||||||
|
|||||||
+46
-20
@@ -226,6 +226,50 @@ inline constexpr int kNumFxSlots = 8;
|
|||||||
inline constexpr int kMaxUnison = 16;
|
inline constexpr int kMaxUnison = 16;
|
||||||
inline constexpr int kNumVoices = 32;
|
inline constexpr int kNumVoices = 32;
|
||||||
|
|
||||||
|
namespace paramIds
|
||||||
|
{
|
||||||
|
struct Oscillator
|
||||||
|
{
|
||||||
|
const char *on, *wave, *wtPos, *warp, *warpAmt, *coarse, *fine, *level;
|
||||||
|
const char *pan, *unison, *detune, *spread, *phase, *randPhase;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr Oscillator oscillators[kNumOscillators] = {
|
||||||
|
{ ids::oscAOn, ids::oscAWave, ids::oscAWtPos, ids::oscAWarp, ids::oscAWarpAmt,
|
||||||
|
ids::oscACoarse, ids::oscAFine, ids::oscALevel, ids::oscAPan, ids::oscAUnison,
|
||||||
|
ids::oscADetune, ids::oscASpread, ids::oscAPhase, ids::oscARandPh },
|
||||||
|
{ ids::oscBOn, ids::oscBWave, ids::oscBWtPos, ids::oscBWarp, ids::oscBWarpAmt,
|
||||||
|
ids::oscBCoarse, ids::oscBFine, ids::oscBLevel, ids::oscBPan, ids::oscBUnison,
|
||||||
|
ids::oscBDetune, ids::oscBSpread, ids::oscBPhase, ids::oscBRandPh }
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr const char* envAttack[] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
||||||
|
inline constexpr const char* envDecay[] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
||||||
|
inline constexpr const char* envSustain[] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
||||||
|
inline constexpr const char* envRelease[] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
||||||
|
inline constexpr const char* envCurve[] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
||||||
|
inline constexpr const char* lfoRate[] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
||||||
|
inline constexpr const char* lfoSync[] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
||||||
|
inline constexpr const char* lfoBeat[] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
||||||
|
inline constexpr const char* lfoShape[] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
||||||
|
inline constexpr const char* lfoPhase[] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
||||||
|
inline constexpr const char* lfoFade[] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
||||||
|
inline constexpr const char* lfoDelay[] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
||||||
|
inline constexpr const char* fxType[] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
||||||
|
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
||||||
|
inline constexpr const char* fxMix[] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
||||||
|
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
||||||
|
inline constexpr const char* fxP1[] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
||||||
|
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
||||||
|
inline constexpr const char* fxP2[] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
||||||
|
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
||||||
|
inline constexpr const char* fxP3[] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
||||||
|
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
||||||
|
inline constexpr const char* fxP4[] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
||||||
|
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
||||||
|
inline constexpr const char* macros[] = { ids::macro1, ids::macro2, ids::macro3, ids::macro4 };
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Modulation sources / destinations
|
// Modulation sources / destinations
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -261,14 +305,7 @@ inline constexpr int kNumModSources = (int) ModSource::Count;
|
|||||||
// Is a given source per-voice (i.e. needs a value for each voice)?
|
// Is a given source per-voice (i.e. needs a value for each voice)?
|
||||||
inline bool isPerVoiceSource (ModSource s)
|
inline bool isPerVoiceSource (ModSource s)
|
||||||
{
|
{
|
||||||
switch (s)
|
return (s >= ModSource::Env1 && s <= ModSource::Note) || s == ModSource::Random;
|
||||||
{
|
|
||||||
case ModSource::Env1: case ModSource::Env2:
|
|
||||||
case ModSource::Env3: case ModSource::Env4:
|
|
||||||
case ModSource::Velocity: case ModSource::Note:
|
|
||||||
case ModSource::Random: return true;
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Does a source already span -1..1 (bipolar range)?
|
// Does a source already span -1..1 (bipolar range)?
|
||||||
@@ -280,18 +317,7 @@ inline bool isBipolarSource (ModSource s)
|
|||||||
// Is a given target a per-voice parameter?
|
// Is a given target a per-voice parameter?
|
||||||
inline bool isPerVoiceTarget (ModTarget t)
|
inline bool isPerVoiceTarget (ModTarget t)
|
||||||
{
|
{
|
||||||
switch (t)
|
return t != ModTarget::Master && ! (t >= ModTarget::Fx1Mix && t <= ModTarget::Fx8Mix);
|
||||||
{
|
|
||||||
case ModTarget::Master:
|
|
||||||
case ModTarget::FilterMix: case ModTarget::FilterOut:
|
|
||||||
case ModTarget::Fx1Mix: case ModTarget::Fx2Mix:
|
|
||||||
case ModTarget::Fx3Mix: case ModTarget::Fx4Mix:
|
|
||||||
case ModTarget::Fx5Mix: case ModTarget::Fx6Mix:
|
|
||||||
case ModTarget::Fx7Mix: case ModTarget::Fx8Mix:
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
juce::String modSourceName (ModSource s);
|
juce::String modSourceName (ModSource s);
|
||||||
|
|||||||
+137
-51
@@ -94,18 +94,25 @@ namespace
|
|||||||
knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h);
|
knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* kFxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
constexpr auto& kFxType = paramIds::fxType;
|
||||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
constexpr auto& kFxMix = paramIds::fxMix;
|
||||||
const char* kFxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
constexpr auto& kFxP1 = paramIds::fxP1;
|
||||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
constexpr auto& kFxP2 = paramIds::fxP2;
|
||||||
const char* kFxP1[8] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
constexpr auto& kFxP3 = paramIds::fxP3;
|
||||||
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
constexpr auto& kFxP4 = paramIds::fxP4;
|
||||||
const char* kFxP2[8] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
|
||||||
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
constexpr const char* kFxParamNames[(int) FxType::Count][4] = {
|
||||||
const char* kFxP3[8] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
{ "P1", "P2", "P3", "P4" },
|
||||||
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
{ "Intensity", "Low Amount", "High Amount", "Output" },
|
||||||
const char* kFxP4[8] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
{ "Rate", "Depth", "Width", "Unused" },
|
||||||
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
{ "Rate", "Depth", "Feedback", "Unused" },
|
||||||
|
{ "Rate", "Depth", "Feedback", "Stages" },
|
||||||
|
{ "Drive", "Shape", "Tone", "Output" },
|
||||||
|
{ "Low Gain", "Mid Gain", "High Gain", "Mid Freq" },
|
||||||
|
{ "Threshold", "Ratio", "Attack", "Release" },
|
||||||
|
{ "Time", "Feedback", "Damping", "Ping-Pong" },
|
||||||
|
{ "Size", "Damping", "Width", "Predelay" }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -131,7 +138,6 @@ PluginEditor::PluginEditor (SerumAltAudioProcessor& p)
|
|||||||
buildMacroTab();
|
buildMacroTab();
|
||||||
|
|
||||||
setTab (0);
|
setTab (0);
|
||||||
applyUiScale();
|
|
||||||
startTimerHz (30);
|
startTimerHz (30);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +169,11 @@ Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name,
|
|||||||
const juce::String& paramId, std::function<juce::String (float)> fmt,
|
const juce::String& paramId, std::function<juce::String (float)> fmt,
|
||||||
const juce::String& tooltip)
|
const juce::String& tooltip)
|
||||||
{
|
{
|
||||||
auto* k = new Knob (name, std::move (fmt));
|
auto control = std::make_unique<Knob> (name, std::move (fmt));
|
||||||
|
auto* k = control.get();
|
||||||
|
ownedControls.push_back (std::move (control));
|
||||||
parent->addAndMakeVisible (k);
|
parent->addAndMakeVisible (k);
|
||||||
if (paramId.isNotEmpty())
|
if (paramId.isNotEmpty() && processor.parameters.getParameter (paramId) != nullptr)
|
||||||
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
sliderAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (
|
||||||
processor.parameters, paramId, *k));
|
processor.parameters, paramId, *k));
|
||||||
k->setTooltip (tooltip.isNotEmpty() ? tooltip : name);
|
k->setTooltip (tooltip.isNotEmpty() ? tooltip : name);
|
||||||
@@ -175,11 +183,13 @@ Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name,
|
|||||||
juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId,
|
juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId,
|
||||||
const juce::StringArray& items, const juce::String& tooltip)
|
const juce::StringArray& items, const juce::String& tooltip)
|
||||||
{
|
{
|
||||||
auto* c = new juce::ComboBox();
|
auto control = std::make_unique<juce::ComboBox>();
|
||||||
|
auto* c = control.get();
|
||||||
|
ownedControls.push_back (std::move (control));
|
||||||
c->addItemList (items, 1);
|
c->addItemList (items, 1);
|
||||||
c->setSelectedItemIndex (0, juce::dontSendNotification);
|
c->setSelectedItemIndex (0, juce::dontSendNotification);
|
||||||
parent->addAndMakeVisible (c);
|
parent->addAndMakeVisible (c);
|
||||||
if (paramId.isNotEmpty())
|
if (paramId.isNotEmpty() && processor.parameters.getParameter (paramId) != nullptr)
|
||||||
comboAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (
|
comboAttachments.push_back (std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (
|
||||||
processor.parameters, paramId, *c));
|
processor.parameters, paramId, *c));
|
||||||
if (tooltip.isNotEmpty())
|
if (tooltip.isNotEmpty())
|
||||||
@@ -190,7 +200,9 @@ juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::St
|
|||||||
ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label,
|
ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label,
|
||||||
const juce::String& paramId, const juce::String& tooltip)
|
const juce::String& paramId, const juce::String& tooltip)
|
||||||
{
|
{
|
||||||
auto* t = new ToggleButton (label);
|
auto control = std::make_unique<ToggleButton> (label);
|
||||||
|
auto* t = control.get();
|
||||||
|
ownedControls.push_back (std::move (control));
|
||||||
parent->addAndMakeVisible (t);
|
parent->addAndMakeVisible (t);
|
||||||
if (paramId.isNotEmpty())
|
if (paramId.isNotEmpty())
|
||||||
t->attach (processor.parameters, paramId);
|
t->attach (processor.parameters, paramId);
|
||||||
@@ -430,11 +442,11 @@ void PluginEditor::buildModTab()
|
|||||||
envDisplays[(size_t) i].setBounds (layout::padding, layout::titleHeight,
|
envDisplays[(size_t) i].setBounds (layout::padding, layout::titleHeight,
|
||||||
colW - 2 * layout::padding, layout::envDisplayHeight);
|
colW - 2 * layout::padding, layout::envDisplayHeight);
|
||||||
|
|
||||||
const char* idsA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
const auto& idsA = paramIds::envAttack;
|
||||||
const char* idsD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
const auto& idsD = paramIds::envDecay;
|
||||||
const char* idsS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
const auto& idsS = paramIds::envSustain;
|
||||||
const char* idsR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
const auto& idsR = paramIds::envRelease;
|
||||||
const char* idsC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
const auto& idsC = paramIds::envCurve;
|
||||||
|
|
||||||
std::vector<Knob*> knobs = {
|
std::vector<Knob*> knobs = {
|
||||||
makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time (0.5 ms - 12 s)"),
|
makeKnob (&envPanels[(size_t) i], "Attack", idsA[i], formatSeconds, "Attack time (0.5 ms - 12 s)"),
|
||||||
@@ -455,13 +467,13 @@ void PluginEditor::buildModTab()
|
|||||||
lfoPanels[(size_t) i].setBounds (gridX (i, colW, colGap), lfoTop, colW, lfoH);
|
lfoPanels[(size_t) i].setBounds (gridX (i, colW, colGap), lfoTop, colW, lfoH);
|
||||||
modView.addAndMakeVisible (lfoPanels[(size_t) i]);
|
modView.addAndMakeVisible (lfoPanels[(size_t) i]);
|
||||||
|
|
||||||
const char* rate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
const auto& rate = paramIds::lfoRate;
|
||||||
const char* sync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
const auto& sync = paramIds::lfoSync;
|
||||||
const char* beat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
const auto& beat = paramIds::lfoBeat;
|
||||||
const char* shape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
const auto& shape = paramIds::lfoShape;
|
||||||
const char* phase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
const auto& phase = paramIds::lfoPhase;
|
||||||
const char* fade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
const auto& fade = paramIds::lfoFade;
|
||||||
const char* delay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
const auto& delay = paramIds::lfoDelay;
|
||||||
|
|
||||||
makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "LFO shape")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight);
|
makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes, "LFO shape")->setBounds (layout::padding, comboRowY(), 110, layout::comboHeight);
|
||||||
makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Tempo-sync LFO")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i], "Tempo-sync LFO")->setBounds (layout::padding + 110 + layout::gap, layout::titleHeight, layout::toggleWidth, layout::toggleHeight);
|
||||||
@@ -533,9 +545,12 @@ void PluginEditor::buildModTab()
|
|||||||
const int tid = modTargetCombo.getSelectedItemIndex();
|
const int tid = modTargetCombo.getSelectedItemIndex();
|
||||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid],
|
||||||
(float) modDepthKnob.getValue(),
|
(float) modDepthKnob.getValue(),
|
||||||
modBipolarToggle.getToggleState());
|
modBipolarToggle.getToggleState());
|
||||||
|
}
|
||||||
updateModList();
|
updateModList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -546,7 +561,12 @@ void PluginEditor::buildModTab()
|
|||||||
modRemoveButton.setTooltip ("Remove last modulation connection");
|
modRemoveButton.setTooltip ("Remove last modulation connection");
|
||||||
modRemoveButton.onClick = [this]
|
modRemoveButton.onClick = [this]
|
||||||
{
|
{
|
||||||
processor.engine.getMatrix().removeConnection (processor.engine.getMatrix().size() - 1);
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
|
auto& matrix = processor.engine.getMatrix();
|
||||||
|
if (matrix.size() > 0)
|
||||||
|
matrix.removeConnection (matrix.size() - 1);
|
||||||
|
}
|
||||||
updateModList();
|
updateModList();
|
||||||
};
|
};
|
||||||
matrixPanel.addAndMakeVisible (modRemoveButton);
|
matrixPanel.addAndMakeVisible (modRemoveButton);
|
||||||
@@ -556,7 +576,10 @@ void PluginEditor::buildModTab()
|
|||||||
modClearButton.setTooltip ("Clear all modulation connections");
|
modClearButton.setTooltip ("Clear all modulation connections");
|
||||||
modClearButton.onClick = [this]
|
modClearButton.onClick = [this]
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
processor.engine.getMatrix().clear();
|
processor.engine.getMatrix().clear();
|
||||||
|
}
|
||||||
updateModList();
|
updateModList();
|
||||||
};
|
};
|
||||||
matrixPanel.addAndMakeVisible (modClearButton);
|
matrixPanel.addAndMakeVisible (modClearButton);
|
||||||
@@ -599,11 +622,15 @@ void PluginEditor::buildFxTab()
|
|||||||
fxTypeCombos[(size_t) i]->setBounds (layout::padding, comboRowY(), 160, layout::comboHeight);
|
fxTypeCombos[(size_t) i]->setBounds (layout::padding, comboRowY(), 160, layout::comboHeight);
|
||||||
|
|
||||||
const int upX = layout::padding + 160 + layout::gap;
|
const int upX = layout::padding + 160 + layout::gap;
|
||||||
fxUp[(size_t) i] = new juce::TextButton ("\xe2\x86\x91");
|
auto up = std::make_unique<juce::TextButton> ("\xe2\x86\x91");
|
||||||
|
fxUp[(size_t) i] = up.get();
|
||||||
|
ownedControls.push_back (std::move (up));
|
||||||
fxUp[(size_t) i]->setBounds (upX, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
fxUp[(size_t) i]->setBounds (upX, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
||||||
fxUp[(size_t) i]->setTooltip ("Move effect up");
|
fxUp[(size_t) i]->setTooltip ("Move effect up");
|
||||||
fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]);
|
fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]);
|
||||||
fxDown[(size_t) i] = new juce::TextButton ("\xe2\x86\x93");
|
auto down = std::make_unique<juce::TextButton> ("\xe2\x86\x93");
|
||||||
|
fxDown[(size_t) i] = down.get();
|
||||||
|
ownedControls.push_back (std::move (down));
|
||||||
fxDown[(size_t) i]->setBounds (upX + layout::arrowWidth + layout::gap, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
fxDown[(size_t) i]->setBounds (upX + layout::arrowWidth + layout::gap, comboRowY(), layout::arrowWidth, layout::comboHeight);
|
||||||
fxDown[(size_t) i]->setTooltip ("Move effect down");
|
fxDown[(size_t) i]->setTooltip ("Move effect down");
|
||||||
fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]);
|
fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]);
|
||||||
@@ -637,8 +664,8 @@ void PluginEditor::buildMacroTab()
|
|||||||
macroPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, macroH);
|
macroPanels[(size_t) i].setBounds (gridX (i, colW, colGap), layout::margin, colW, macroH);
|
||||||
macroView.addAndMakeVisible (macroPanels[(size_t) i]);
|
macroView.addAndMakeVisible (macroPanels[(size_t) i]);
|
||||||
|
|
||||||
const char* ids[4] = { ids::macro1, ids::macro2, ids::macro3, ids::macro4 };
|
const auto& macroIds = paramIds::macros;
|
||||||
macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), ids[i], formatPercent,
|
macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), macroIds[i], formatPercent,
|
||||||
MacroControls::macroName (i) + " macro (0-100%)");
|
MacroControls::macroName (i) + " macro (0-100%)");
|
||||||
macroKnobs[(size_t) i]->setBounds ((colW - layout::macroKnobSize) / 2,
|
macroKnobs[(size_t) i]->setBounds ((colW - layout::macroKnobSize) / 2,
|
||||||
layout::titleHeight + layout::padding,
|
layout::titleHeight + layout::padding,
|
||||||
@@ -684,8 +711,11 @@ void PluginEditor::buildMacroTab()
|
|||||||
const int tid = macroAssignTarget.getSelectedItemIndex();
|
const int tid = macroAssignTarget.getSelectedItemIndex();
|
||||||
if (tid >= 0 && tid < (int) targetEnums.size())
|
if (tid >= 0 && tid < (int) targetEnums.size())
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid],
|
||||||
(float) macroDepthKnob.getValue());
|
(float) macroDepthKnob.getValue());
|
||||||
|
}
|
||||||
updateMacroList();
|
updateMacroList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -696,7 +726,10 @@ void PluginEditor::buildMacroTab()
|
|||||||
macroClearButton.setTooltip ("Clear all macro assignments");
|
macroClearButton.setTooltip ("Clear all macro assignments");
|
||||||
macroClearButton.onClick = [this]
|
macroClearButton.onClick = [this]
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
processor.engine.getMacros().clear();
|
processor.engine.getMacros().clear();
|
||||||
|
}
|
||||||
updateMacroList();
|
updateMacroList();
|
||||||
};
|
};
|
||||||
macroAssignPanel.addAndMakeVisible (macroClearButton);
|
macroAssignPanel.addAndMakeVisible (macroClearButton);
|
||||||
@@ -715,10 +748,15 @@ void PluginEditor::buildMacroTab()
|
|||||||
|
|
||||||
void PluginEditor::swapFxSlots (int a, int b)
|
void PluginEditor::swapFxSlots (int a, int b)
|
||||||
{
|
{
|
||||||
|
if (a < 0 || b < 0 || a >= kNumFxSlots || b >= kNumFxSlots || a == b)
|
||||||
|
return;
|
||||||
|
|
||||||
auto swapParam = [this] (const char* pa, const char* pb)
|
auto swapParam = [this] (const char* pa, const char* pb)
|
||||||
{
|
{
|
||||||
auto* p1 = processor.parameters.getParameter (pa);
|
auto* p1 = processor.parameters.getParameter (pa);
|
||||||
auto* p2 = processor.parameters.getParameter (pb);
|
auto* p2 = processor.parameters.getParameter (pb);
|
||||||
|
if (p1 == nullptr || p2 == nullptr)
|
||||||
|
return;
|
||||||
const float v1 = p1->getValue();
|
const float v1 = p1->getValue();
|
||||||
const float v2 = p2->getValue();
|
const float v2 = p2->getValue();
|
||||||
p1->setValueNotifyingHost (v2);
|
p1->setValueNotifyingHost (v2);
|
||||||
@@ -748,6 +786,7 @@ void PluginEditor::setTab (int index)
|
|||||||
tabMod.setToggleState (currentTab == 2, juce::dontSendNotification);
|
tabMod.setToggleState (currentTab == 2, juce::dontSendNotification);
|
||||||
tabFx.setToggleState (currentTab == 3, juce::dontSendNotification);
|
tabFx.setToggleState (currentTab == 3, juce::dontSendNotification);
|
||||||
tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification);
|
tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification);
|
||||||
|
timerCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::cycleTab (int delta)
|
void PluginEditor::cycleTab (int delta)
|
||||||
@@ -791,11 +830,18 @@ void PluginEditor::applyUiScale()
|
|||||||
void PluginEditor::updateVisuals()
|
void PluginEditor::updateVisuals()
|
||||||
{
|
{
|
||||||
const auto& apvts = processor.parameters;
|
const auto& apvts = processor.parameters;
|
||||||
auto gv = [&] (const char* id) { return apvts.getRawParameterValue (id)->load(); };
|
auto gv = [&] (const char* id)
|
||||||
|
{
|
||||||
|
if (auto* value = apvts.getRawParameterValue (id))
|
||||||
|
return value->load();
|
||||||
|
return 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
masterDisplay.setValue (formatPercent (gv (ids::master)));
|
masterDisplay.setValue (formatPercent (gv (ids::master)));
|
||||||
|
|
||||||
// Waveforms.
|
// Waveforms.
|
||||||
|
if (currentTab == 0)
|
||||||
|
{
|
||||||
const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1)));
|
const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1)));
|
||||||
const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1)));
|
const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1)));
|
||||||
oscAWave.setWaveIndex (waveA);
|
oscAWave.setWaveIndex (waveA);
|
||||||
@@ -804,62 +850,98 @@ void PluginEditor::updateVisuals()
|
|||||||
oscBWave.setWaveIndex (waveB);
|
oscBWave.setWaveIndex (waveB);
|
||||||
oscBWave.setFramePosition (gv (ids::oscBWtPos));
|
oscBWave.setFramePosition (gv (ids::oscBWtPos));
|
||||||
oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f);
|
oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f);
|
||||||
oscAWave.repaint();
|
}
|
||||||
oscBWave.repaint();
|
|
||||||
|
|
||||||
// Filters.
|
// Filters.
|
||||||
|
if (currentTab == 1)
|
||||||
|
{
|
||||||
filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res),
|
filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res),
|
||||||
gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f));
|
gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f));
|
||||||
filter1Display.setEnabled (gv (ids::f1On) > 0.5f);
|
filter1Display.setEnabled (gv (ids::f1On) > 0.5f);
|
||||||
filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res),
|
filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res),
|
||||||
gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f));
|
gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f));
|
||||||
filter2Display.setEnabled (gv (ids::f2On) > 0.5f);
|
filter2Display.setEnabled (gv (ids::f2On) > 0.5f);
|
||||||
filter1Display.repaint();
|
}
|
||||||
filter2Display.repaint();
|
|
||||||
|
|
||||||
// Envelopes.
|
// Envelopes.
|
||||||
const char* a[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
if (currentTab == 2)
|
||||||
const char* d[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
|
||||||
const char* s[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
|
||||||
const char* r[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
|
||||||
const char* c[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
|
||||||
for (int i = 0; i < kNumEnvelopes; ++i)
|
|
||||||
{
|
{
|
||||||
|
const auto& a = paramIds::envAttack;
|
||||||
|
const auto& d = paramIds::envDecay;
|
||||||
|
const auto& s = paramIds::envSustain;
|
||||||
|
const auto& r = paramIds::envRelease;
|
||||||
|
const auto& c = paramIds::envCurve;
|
||||||
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||||
envDisplays[(size_t) i].setParams (gv (a[i]), gv (d[i]), gv (s[i]), gv (r[i]), gv (c[i]));
|
envDisplays[(size_t) i].setParams (gv (a[i]), gv (d[i]), gv (s[i]), gv (r[i]), gv (c[i]));
|
||||||
envDisplays[(size_t) i].repaint();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LFOs.
|
// LFOs.
|
||||||
const char* lshape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
if (currentTab == 2)
|
||||||
|
{
|
||||||
|
const auto& lshape = paramIds::lfoShape;
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
for (int i = 0; i < kNumLfos; ++i)
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
{
|
{
|
||||||
|
const auto& lfo = processor.engine.getLfos()[(size_t) i];
|
||||||
lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f));
|
lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f));
|
||||||
lfoDisplays[(size_t) i].setShapeData (processor.engine.getLfos()[(size_t) i].getShapeData(),
|
lfoDisplays[(size_t) i].setShapeData (lfo.getShapeData(), lfo.getShapeSteps());
|
||||||
processor.engine.getLfos()[(size_t) i].getShapeSteps());
|
}
|
||||||
lfoDisplays[(size_t) i].repaint();
|
}
|
||||||
|
|
||||||
|
if (currentTab == 3)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < kNumFxSlots; ++i)
|
||||||
|
{
|
||||||
|
const int type = juce::jlimit (0, (int) FxType::Count - 1,
|
||||||
|
(int) std::llround (gv (kFxType[i]) * ((int) FxType::Count - 1)));
|
||||||
|
for (int p = 0; p < 4; ++p)
|
||||||
|
{
|
||||||
|
auto* knob = fxKnobs[(size_t) i][(size_t) p + 1];
|
||||||
|
const juce::String name = kFxParamNames[type][p];
|
||||||
|
if (knob->getName() != name)
|
||||||
|
{
|
||||||
|
knob->setName (name);
|
||||||
|
knob->setTooltip (name == "Unused" ? "Not used by this effect"
|
||||||
|
: name + " (normalised 0-100%)");
|
||||||
|
knob->repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAVE state.
|
// RAVE state.
|
||||||
raveButton.setToggleState (processor.isRaveEnabled());
|
raveButton.setToggleState (processor.isRaveEnabled());
|
||||||
|
|
||||||
|
const int program = processor.getCurrentProgram();
|
||||||
|
if (presetCombo.getSelectedItemIndex() != program)
|
||||||
|
presetCombo.setSelectedItemIndex (program, juce::dontSendNotification);
|
||||||
|
|
||||||
// Scale change.
|
// Scale change.
|
||||||
if (processor.getUiScaleIndex() != currentScaleIndex)
|
if (processor.getUiScaleIndex() != currentScaleIndex)
|
||||||
applyUiScale();
|
applyUiScale();
|
||||||
|
if (scaleCombo.getSelectedItemIndex() != currentScaleIndex)
|
||||||
|
scaleCombo.setSelectedItemIndex (currentScaleIndex, juce::dontSendNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::updateModList()
|
void PluginEditor::updateModList()
|
||||||
{
|
{
|
||||||
juce::String text;
|
juce::String text;
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
const auto& cons = processor.engine.getMatrix().connections;
|
const auto& cons = processor.engine.getMatrix().connections;
|
||||||
for (const auto& c : cons)
|
for (const auto& c : cons)
|
||||||
text += modSourceName (c.source) + " -> " + modTargetName (c.target)
|
text += modSourceName (c.source) + " -> " + modTargetName (c.target)
|
||||||
+ " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n";
|
+ " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n";
|
||||||
|
}
|
||||||
|
if (modList.getText() != text)
|
||||||
modList.setText (text, false);
|
modList.setText (text, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::updateMacroList()
|
void PluginEditor::updateMacroList()
|
||||||
{
|
{
|
||||||
juce::String text;
|
juce::String text;
|
||||||
|
{
|
||||||
|
const juce::ScopedLock lock (processor.engine.getControlLock());
|
||||||
for (int m = 0; m < kNumMacros; ++m)
|
for (int m = 0; m < kNumMacros; ++m)
|
||||||
{
|
{
|
||||||
text += MacroControls::macroName (m) + ":\n";
|
text += MacroControls::macroName (m) + ":\n";
|
||||||
@@ -869,13 +951,17 @@ void PluginEditor::updateMacroList()
|
|||||||
for (const auto& a : assigns)
|
for (const auto& a : assigns)
|
||||||
text += " " + modTargetName (a.target) + " [" + juce::String (a.depth, 2) + "]\n";
|
text += " " + modTargetName (a.target) + " [" + juce::String (a.depth, 2) + "]\n";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (macroList.getText() != text)
|
||||||
macroList.setText (text, false);
|
macroList.setText (text, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginEditor::timerCallback()
|
void PluginEditor::timerCallback()
|
||||||
{
|
{
|
||||||
updateVisuals();
|
updateVisuals();
|
||||||
|
if (currentTab == 2)
|
||||||
updateModList();
|
updateModList();
|
||||||
|
else if (currentTab == 4)
|
||||||
updateMacroList();
|
updateMacroList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ private:
|
|||||||
juce::TextButton macroAssignButton, macroClearButton;
|
juce::TextButton macroAssignButton, macroClearButton;
|
||||||
juce::TextEditor macroList;
|
juce::TextEditor macroList;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<juce::Component>> ownedControls;
|
||||||
|
|
||||||
// --- attachments ---
|
// --- attachments ---
|
||||||
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> sliderAttachments;
|
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> sliderAttachments;
|
||||||
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment>> comboAttachments;
|
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment>> comboAttachments;
|
||||||
|
|||||||
+106
-34
@@ -176,7 +176,7 @@ juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::crea
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||||
{
|
{
|
||||||
engine.prepare (sampleRate, samplesPerBlock);
|
engine.prepare (sampleRate, samplesPerBlock, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SerumAltAudioProcessor::releaseResources()
|
void SerumAltAudioProcessor::releaseResources()
|
||||||
@@ -205,14 +205,13 @@ int SerumAltAudioProcessor::getNumPrograms()
|
|||||||
|
|
||||||
int SerumAltAudioProcessor::getCurrentProgram()
|
int SerumAltAudioProcessor::getCurrentProgram()
|
||||||
{
|
{
|
||||||
return currentProgram;
|
return currentProgram.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SerumAltAudioProcessor::setCurrentProgram (int index)
|
void SerumAltAudioProcessor::setCurrentProgram (int index)
|
||||||
{
|
{
|
||||||
index = juce::jlimit (0, getNumPrograms() - 1, index);
|
if (getNumPrograms() > 0)
|
||||||
loadFactoryPreset (index);
|
loadFactoryPreset (juce::jlimit (0, getNumPrograms() - 1, index));
|
||||||
currentProgram = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const juce::String SerumAltAudioProcessor::getProgramName (int index)
|
const juce::String SerumAltAudioProcessor::getProgramName (int index)
|
||||||
@@ -238,24 +237,36 @@ void SerumAltAudioProcessor::loadFactoryPreset (int index)
|
|||||||
if (index < 0 || index >= (int) presets.size())
|
if (index < 0 || index >= (int) presets.size())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
const FactoryPreset& preset = presets[(size_t) index];
|
const FactoryPreset& preset = presets[(size_t) index];
|
||||||
|
const auto* uiScaleParam = parameters.getParameter (ids::uiScale);
|
||||||
// RAVE should start off for a freshly loaded preset.
|
for (auto* param : getParameters())
|
||||||
rave.resetSnapshot();
|
if (param != nullptr && param != uiScaleParam)
|
||||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
param->setValueNotifyingHost (param->getDefaultValue());
|
||||||
raveParam->setValueNotifyingHost (0.0f);
|
|
||||||
|
|
||||||
for (const auto& kv : preset.params)
|
for (const auto& kv : preset.params)
|
||||||
if (auto* param = parameters.getParameter (kv.first))
|
if (auto* param = parameters.getParameter (kv.first))
|
||||||
param->setValueNotifyingHost (kv.second);
|
if (param != uiScaleParam && std::isfinite (kv.second))
|
||||||
|
param->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, kv.second));
|
||||||
|
|
||||||
engine.getMatrix().clear();
|
// RAVE should start off for a freshly loaded preset.
|
||||||
|
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||||
|
raveParam->setValueNotifyingHost (0.0f);
|
||||||
|
|
||||||
|
auto& matrix = engine.getMatrix();
|
||||||
|
matrix.clear();
|
||||||
for (const auto& mod : preset.mods)
|
for (const auto& mod : preset.mods)
|
||||||
engine.getMatrix().addConnection (mod.source, mod.target, mod.depth, mod.bipolar);
|
if (! matrix.addConnection (mod.source, mod.target, mod.depth, mod.bipolar))
|
||||||
|
continue;
|
||||||
|
|
||||||
engine.getMacros().clear();
|
auto& macros = engine.getMacros();
|
||||||
|
macros.clear();
|
||||||
for (const auto& ma : preset.macroAssigns)
|
for (const auto& ma : preset.macroAssigns)
|
||||||
engine.getMacros().addAssignment (ma.macro, ma.target, ma.depth);
|
if (! macros.addAssignment (ma.macro, ma.target, ma.depth))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
restoreLfoShapesFromState ({});
|
||||||
|
currentProgram.store (index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -263,25 +274,36 @@ void SerumAltAudioProcessor::loadFactoryPreset (int index)
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
void SerumAltAudioProcessor::setRaveEnabled (bool enabled)
|
void SerumAltAudioProcessor::setRaveEnabled (bool enabled)
|
||||||
{
|
{
|
||||||
rave.setEnabled (enabled, parameters);
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
if (auto* raveParam = parameters.getParameter (ids::rave))
|
||||||
|
{
|
||||||
|
raveParam->beginChangeGesture();
|
||||||
raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f);
|
raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f);
|
||||||
|
raveParam->endChangeGesture();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SerumAltAudioProcessor::isRaveEnabled() const
|
bool SerumAltAudioProcessor::isRaveEnabled() const
|
||||||
{
|
{
|
||||||
return rave.isEnabled();
|
if (auto* raveParam = parameters.getRawParameterValue (ids::rave))
|
||||||
|
return raveParam->load() > 0.5f;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SerumAltAudioProcessor::getUiScaleIndex() const
|
int SerumAltAudioProcessor::getUiScaleIndex() const
|
||||||
{
|
{
|
||||||
if (auto* p = parameters.getRawParameterValue (ids::uiScale))
|
if (auto* p = parameters.getRawParameterValue (ids::uiScale))
|
||||||
return juce::jlimit (0, 4, (int) std::llround (p->load() * 4.0f));
|
{
|
||||||
|
const float value = p->load();
|
||||||
|
if (std::isfinite (value))
|
||||||
|
return (int) std::llround (juce::jlimit (0.0f, 1.0f, value) * 4.0f);
|
||||||
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SerumAltAudioProcessor::setUiScaleIndex (int index)
|
void SerumAltAudioProcessor::setUiScaleIndex (int index)
|
||||||
{
|
{
|
||||||
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
index = juce::jlimit (0, 4, index);
|
index = juce::jlimit (0, 4, index);
|
||||||
if (auto* p = parameters.getParameter (ids::uiScale))
|
if (auto* p = parameters.getParameter (ids::uiScale))
|
||||||
p->setValueNotifyingHost ((float) index / 4.0f);
|
p->setValueNotifyingHost ((float) index / 4.0f);
|
||||||
@@ -298,7 +320,15 @@ float SerumAltAudioProcessor::getUiScale() const
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||||
{
|
{
|
||||||
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
auto state = parameters.copyState();
|
auto state = parameters.copyState();
|
||||||
|
for (int i = state.getNumChildren(); --i >= 0;)
|
||||||
|
{
|
||||||
|
const auto child = state.getChild (i);
|
||||||
|
if (child.hasType ("MODMATRIX") || child.hasType ("MACROS") || child.hasType ("LFOSHAPES"))
|
||||||
|
state.removeChild (i, nullptr);
|
||||||
|
}
|
||||||
|
state.setProperty ("currentProgram", currentProgram.load(), nullptr);
|
||||||
state.appendChild (engine.getMatrix().toValueTree(), nullptr);
|
state.appendChild (engine.getMatrix().toValueTree(), nullptr);
|
||||||
state.appendChild (engine.getMacros().toValueTree(), nullptr);
|
state.appendChild (engine.getMacros().toValueTree(), nullptr);
|
||||||
saveLfoShapesToState (state);
|
saveLfoShapesToState (state);
|
||||||
@@ -310,39 +340,47 @@ void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
|||||||
|
|
||||||
void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||||
{
|
{
|
||||||
|
if (data == nullptr || sizeInBytes <= 0)
|
||||||
|
return;
|
||||||
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
|
||||||
if (xml == nullptr)
|
if (xml == nullptr || ! xml->hasTagName ("SerumAlt"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
juce::ValueTree state = juce::ValueTree::fromXml (*xml);
|
juce::ValueTree state = juce::ValueTree::fromXml (*xml);
|
||||||
if (! state.isValid())
|
if (! state.hasType ("SerumAlt"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
|
// A persisted RAVE toggle is rendered non-destructively, so retain it.
|
||||||
parameters.replaceState (state);
|
parameters.replaceState (state);
|
||||||
engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX"));
|
engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX"));
|
||||||
engine.getMacros().fromValueTree (state.getChildWithName ("MACROS"));
|
engine.getMacros().fromValueTree (state.getChildWithName ("MACROS"));
|
||||||
restoreLfoShapesFromState (state);
|
restoreLfoShapesFromState (state);
|
||||||
|
|
||||||
// A persisted RAVE toggle has no live snapshot, so start it off.
|
const double savedProgram = (double) state.getProperty ("currentProgram", 0);
|
||||||
rave.resetSnapshot();
|
currentProgram.store (std::isfinite (savedProgram)
|
||||||
if (auto* raveParam = parameters.getParameter (ids::rave))
|
? (int) juce::jlimit (0.0, (double) juce::jmax (0, getNumPrograms() - 1), savedProgram) : 0);
|
||||||
raveParam->setValueNotifyingHost (0.0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
|
void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
|
||||||
{
|
{
|
||||||
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
juce::ValueTree tree ("LFOSHAPES");
|
juce::ValueTree tree ("LFOSHAPES");
|
||||||
for (int i = 0; i < kNumLfos; ++i)
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
{
|
{
|
||||||
const auto& data = engine.getLfos()[(size_t) i].getShapeData();
|
const auto& source = engine.getLfos()[(size_t) i];
|
||||||
|
const auto& data = source.getShapeData();
|
||||||
juce::ValueTree lfo ("LFO");
|
juce::ValueTree lfo ("LFO");
|
||||||
lfo.setProperty ("index", i, nullptr);
|
lfo.setProperty ("index", i, nullptr);
|
||||||
lfo.setProperty ("steps", engine.getLfos()[(size_t) i].getShapeSteps(), nullptr);
|
lfo.setProperty ("steps", juce::jlimit (2, LFO::kShapePoints, source.getShapeSteps()), nullptr);
|
||||||
|
|
||||||
juce::Array<juce::var> arr;
|
juce::Array<juce::var> arr;
|
||||||
for (float vv : data)
|
for (int point = 0; point < juce::jmin (LFO::kShapePoints, (int) data.size()); ++point)
|
||||||
arr.add (vv);
|
{
|
||||||
lfo.setProperty ("data", juce::var (arr), nullptr);
|
const float value = data[(size_t) point];
|
||||||
|
arr.add (std::isfinite (value) ? juce::jlimit (-1.0f, 1.0f, value) : 0.0f);
|
||||||
|
}
|
||||||
|
lfo.setProperty ("data", juce::JSON::toString (juce::var (arr), true), nullptr);
|
||||||
tree.appendChild (lfo, nullptr);
|
tree.appendChild (lfo, nullptr);
|
||||||
}
|
}
|
||||||
state.appendChild (tree, nullptr);
|
state.appendChild (tree, nullptr);
|
||||||
@@ -350,23 +388,57 @@ void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
|
|||||||
|
|
||||||
void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state)
|
void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state)
|
||||||
{
|
{
|
||||||
|
const juce::ScopedLock lock (engine.getControlLock());
|
||||||
|
std::vector<float> defaultShape ((size_t) LFO::kShapePoints);
|
||||||
|
for (int point = 0; point < LFO::kShapePoints; ++point)
|
||||||
|
defaultShape[(size_t) point] = (point % 2 == 0) ? 1.0f : -1.0f;
|
||||||
|
for (int index = 0; index < kNumLfos; ++index)
|
||||||
|
engine.setLfoShapeData (index, defaultShape, 16);
|
||||||
|
|
||||||
const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES");
|
const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES");
|
||||||
if (! tree.isValid())
|
if (! tree.isValid())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
std::array<bool, kNumLfos> restored {};
|
||||||
for (const auto& lfo : tree)
|
for (const auto& lfo : tree)
|
||||||
{
|
{
|
||||||
if (! lfo.hasType ("LFO"))
|
if (! lfo.hasType ("LFO"))
|
||||||
continue;
|
continue;
|
||||||
const int index = juce::jlimit (0, kNumLfos - 1, (int) lfo.getProperty ("index", 0));
|
const double savedIndex = (double) lfo.getProperty ("index", -1);
|
||||||
const int steps = (int) lfo.getProperty ("steps", 16);
|
if (! std::isfinite (savedIndex) || savedIndex < 0.0 || savedIndex >= kNumLfos
|
||||||
|
|| std::floor (savedIndex) != savedIndex)
|
||||||
|
continue;
|
||||||
|
const int index = (int) savedIndex;
|
||||||
|
if (restored[(size_t) index])
|
||||||
|
continue;
|
||||||
|
const double savedSteps = (double) lfo.getProperty ("steps", 16);
|
||||||
|
const int steps = std::isfinite (savedSteps)
|
||||||
|
? (int) juce::jlimit (2.0, (double) LFO::kShapePoints, savedSteps) : 16;
|
||||||
|
|
||||||
|
juce::var shape = lfo.getProperty ("data");
|
||||||
|
if (shape.isString())
|
||||||
|
{
|
||||||
|
const auto text = shape.toString();
|
||||||
|
if (text.length() > 8192)
|
||||||
|
continue;
|
||||||
|
shape = juce::JSON::parse (text);
|
||||||
|
}
|
||||||
|
const auto* arr = shape.getArray();
|
||||||
|
if (arr == nullptr || arr->isEmpty())
|
||||||
|
continue;
|
||||||
|
const int numPoints = juce::jmin (LFO::kShapePoints, arr->size());
|
||||||
std::vector<float> data;
|
std::vector<float> data;
|
||||||
if (auto* arr = lfo.getProperty ("data").getArray())
|
data.reserve ((size_t) numPoints);
|
||||||
for (const auto& vv : *arr)
|
for (int point = 0; point < numPoints; ++point)
|
||||||
data.push_back ((float) vv);
|
{
|
||||||
|
const auto& savedValue = arr->getReference (point);
|
||||||
|
const double value = (savedValue.isDouble() || savedValue.isInt() || savedValue.isInt64())
|
||||||
|
? (double) savedValue : 0.0;
|
||||||
|
data.push_back (std::isfinite (value) ? (float) juce::jlimit (-1.0, 1.0, value) : 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
engine.setLfoShapeData (index, data, steps);
|
engine.setLfoShapeData (index, data, steps);
|
||||||
|
restored[(size_t) index] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
|
#include <atomic>
|
||||||
#include "Params.h"
|
#include "Params.h"
|
||||||
#include "Engine.h"
|
#include "Engine.h"
|
||||||
#include "RAVEButton.h"
|
|
||||||
|
|
||||||
namespace serum
|
namespace serum
|
||||||
{
|
{
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio
|
// SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio
|
||||||
// engine, preset management and the RAVE controller.
|
// engine, preset management and the RAVE toggle.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class SerumAltAudioProcessor : public juce::AudioProcessor
|
class SerumAltAudioProcessor : public juce::AudioProcessor
|
||||||
{
|
{
|
||||||
@@ -52,15 +52,14 @@ public:
|
|||||||
void loadFactoryPreset (int index);
|
void loadFactoryPreset (int index);
|
||||||
int getNumFactoryPresets() const;
|
int getNumFactoryPresets() const;
|
||||||
|
|
||||||
// Public DSP state (read/write from the GUI thread).
|
// Public control state (hold the engine control lock for matrix/macros/LFOs).
|
||||||
juce::AudioProcessorValueTreeState parameters;
|
juce::AudioProcessorValueTreeState parameters;
|
||||||
Engine engine;
|
Engine engine;
|
||||||
RaveController rave;
|
|
||||||
|
|
||||||
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int currentProgram = 0;
|
std::atomic<int> currentProgram { 0 };
|
||||||
|
|
||||||
void restoreLfoShapesFromState (const juce::ValueTree& state);
|
void restoreLfoShapesFromState (const juce::ValueTree& state);
|
||||||
void saveLfoShapesToState (juce::ValueTree& state) const;
|
void saveLfoShapesToState (juce::ValueTree& state) const;
|
||||||
|
|||||||
+22
-75
@@ -1,90 +1,37 @@
|
|||||||
#include "RAVEButton.h"
|
#include "RAVEButton.h"
|
||||||
|
#include "SynthVoice.h"
|
||||||
|
#include "FXProcessor.h"
|
||||||
|
|
||||||
namespace serum
|
namespace serum
|
||||||
{
|
{
|
||||||
|
|
||||||
void RaveController::resetSnapshot()
|
void RaveController::apply (RenderContext& context, FxSlotParams* slots, int numSlots) noexcept
|
||||||
{
|
{
|
||||||
snapshot.clear();
|
// Boost the static rendering parameters.
|
||||||
enabled = false;
|
context.oscA.unison = juce::jlimit (8, kMaxUnison, context.oscA.unison);
|
||||||
}
|
context.oscB.unison = juce::jlimit (8, kMaxUnison, context.oscB.unison);
|
||||||
|
context.oscA.spread = 1.0f;
|
||||||
|
context.oscB.spread = 1.0f;
|
||||||
|
context.oscA.detune = 1.0f;
|
||||||
|
context.oscB.detune = 0.7f;
|
||||||
|
|
||||||
void RaveController::snapshotParam (const juce::String& id, juce::AudioProcessorValueTreeState& apvts)
|
// Apply drive boosts.
|
||||||
{
|
context.filters.f1Drive = 0.6f;
|
||||||
if (auto* p = apvts.getParameter (id))
|
context.filters.f2Drive = 0.6f;
|
||||||
snapshot.emplace_back (id, p->getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
void RaveController::setParam (const juce::String& id, float value, juce::AudioProcessorValueTreeState& apvts)
|
// Boost FX-specific params (Hyper intensity / Reverb mix).
|
||||||
{
|
if (slots == nullptr)
|
||||||
if (auto* p = apvts.getParameter (id))
|
|
||||||
p->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
void RaveController::setEnabled (bool shouldEnable, juce::AudioProcessorValueTreeState& apvts)
|
|
||||||
{
|
|
||||||
if (shouldEnable == enabled)
|
|
||||||
return;
|
return;
|
||||||
|
for (int i = 0; i < juce::jlimit (0, kNumFxSlots, numSlots); ++i)
|
||||||
if (shouldEnable)
|
|
||||||
{
|
{
|
||||||
snapshot.clear();
|
auto& slot = slots[i];
|
||||||
|
if (slot.type == (int) FxType::Hyper)
|
||||||
// Snapshot the static "boost" parameters.
|
slot.p[0] = 1.0f; // OTT intensity 100%
|
||||||
const char* staticParams[] =
|
else if (slot.type == (int) FxType::Reverb)
|
||||||
{
|
{
|
||||||
ids::oscAUnison, ids::oscBUnison,
|
const float mix = std::isfinite (slot.mix) ? slot.mix : 0.0f;
|
||||||
ids::oscASpread, ids::oscBSpread,
|
slot.mix = juce::jlimit (0.0f, 1.0f, mix + 0.4f); // reverb send +6dB-ish
|
||||||
ids::oscADetune, ids::oscBDetune,
|
|
||||||
ids::f1Drive, ids::f2Drive
|
|
||||||
};
|
|
||||||
for (auto id : staticParams)
|
|
||||||
snapshotParam (id, apvts);
|
|
||||||
|
|
||||||
// Snapshot + boost FX-specific params (Hyper intensity / Reverb mix).
|
|
||||||
const char* fxMixIds[kNumFxSlots] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
|
||||||
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
|
||||||
const char* fxP1Ids[kNumFxSlots] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
|
||||||
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
|
||||||
const char* fxTypeIds[kNumFxSlots] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
|
||||||
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
|
||||||
|
|
||||||
for (int i = 0; i < kNumFxSlots; ++i)
|
|
||||||
{
|
|
||||||
const auto* typeParam = apvts.getParameter (fxTypeIds[i]);
|
|
||||||
const int type = typeParam ? (int) (typeParam->getValue() * (int) FxType::Count) : 0;
|
|
||||||
|
|
||||||
if (type == (int) FxType::Hyper)
|
|
||||||
{
|
|
||||||
snapshotParam (fxP1Ids[i], apvts);
|
|
||||||
setParam (fxP1Ids[i], 1.0f, apvts); // OTT intensity 100%
|
|
||||||
}
|
}
|
||||||
else if (type == (int) FxType::Reverb)
|
|
||||||
{
|
|
||||||
snapshotParam (fxMixIds[i], apvts);
|
|
||||||
const float current = apvts.getParameter (fxMixIds[i])->getValue();
|
|
||||||
setParam (fxMixIds[i], current + 0.4f, apvts); // reverb send +6dB-ish
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply boosts.
|
|
||||||
setParam (ids::oscAUnison, 8.0f / 16.0f, apvts);
|
|
||||||
setParam (ids::oscBUnison, 8.0f / 16.0f, apvts);
|
|
||||||
setParam (ids::oscASpread, 1.0f, apvts);
|
|
||||||
setParam (ids::oscBSpread, 1.0f, apvts);
|
|
||||||
setParam (ids::oscADetune, 1.0f, apvts);
|
|
||||||
setParam (ids::oscBDetune, 0.7f, apvts);
|
|
||||||
setParam (ids::f1Drive, 0.6f, apvts);
|
|
||||||
setParam (ids::f2Drive, 0.6f, apvts);
|
|
||||||
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (const auto& entry : snapshot)
|
|
||||||
setParam (entry.first, entry.second, apvts);
|
|
||||||
snapshot.clear();
|
|
||||||
enabled = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-14
@@ -1,29 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
#include "Params.h"
|
|
||||||
|
|
||||||
namespace serum
|
namespace serum
|
||||||
{
|
{
|
||||||
|
|
||||||
|
struct RenderContext;
|
||||||
|
struct FxSlotParams;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// RAVE — one-shot "make it huge" control. Toggling on snapshots the current
|
// RAVE — non-destructive "make it huge" control. While enabled, rendering
|
||||||
// values of the affected parameters and pushes unison, width, drive, OTT and
|
// boosts unison, width, drive, OTT and reverb in the current block's snapshots;
|
||||||
// reverb to their boosted settings; toggling off restores the snapshot.
|
// the underlying parameters remain unchanged when toggling on or off.
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
class RaveController
|
class RaveController
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setEnabled (bool enabled, juce::AudioProcessorValueTreeState& apvts);
|
static void apply (RenderContext& context, FxSlotParams* slots, int numSlots) noexcept;
|
||||||
bool isEnabled() const noexcept { return enabled; }
|
|
||||||
void resetSnapshot();
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool enabled = false;
|
|
||||||
std::vector<std::pair<juce::String, float>> snapshot;
|
|
||||||
|
|
||||||
void snapshotParam (const juce::String& id, juce::AudioProcessorValueTreeState& apvts);
|
|
||||||
void setParam (const juce::String& id, float value, juce::AudioProcessorValueTreeState& apvts);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace serum
|
} // namespace serum
|
||||||
|
|||||||
+21
-8
@@ -35,7 +35,8 @@ void SynthVoice::reset()
|
|||||||
velocity = 0.0f;
|
velocity = 0.0f;
|
||||||
baseFreq = 0.0;
|
baseFreq = 0.0;
|
||||||
active = released = false;
|
active = released = false;
|
||||||
lastUnisonA = lastUnisonB = 1;
|
noteId = 0;
|
||||||
|
oscillatorsNeedNoteOn = false;
|
||||||
noteRandom = 0.5f;
|
noteRandom = 0.5f;
|
||||||
scratch.clear();
|
scratch.clear();
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,8 @@ void SynthVoice::noteOn (int noteNumber, float velocity01, double freqHz, juce::
|
|||||||
active = true;
|
active = true;
|
||||||
released = false;
|
released = false;
|
||||||
seed = noteSeed;
|
seed = noteSeed;
|
||||||
lastUnisonA = lastUnisonB = 0; // force unison reconfigure on first render
|
noteId = noteSeed;
|
||||||
|
oscillatorsNeedNoteOn = true; // initialise fresh oscillator phases on first render
|
||||||
|
|
||||||
juce::Random rng (noteSeed);
|
juce::Random rng (noteSeed);
|
||||||
noteRandom = rng.nextFloat();
|
noteRandom = rng.nextFloat();
|
||||||
@@ -151,10 +153,19 @@ void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderC
|
|||||||
const double freqA = bent * std::pow (2.0, (double) a.coarse / 12.0 + (double) a.fine / 1200.0);
|
const double freqA = bent * std::pow (2.0, (double) a.coarse / 12.0 + (double) a.fine / 1200.0);
|
||||||
const double freqB = bent * std::pow (2.0, (double) b.coarse / 12.0 + (double) b.fine / 1200.0);
|
const double freqB = bent * std::pow (2.0, (double) b.coarse / 12.0 + (double) b.fine / 1200.0);
|
||||||
|
|
||||||
// Reconfigure unison only when the integer count changes (avoids phase reset
|
// Initialise phases only for a fresh note; update held-note unison parameters
|
||||||
// on every block when unison is LFO-modulated at control rate).
|
// without resetting existing phases when modulated at control rate.
|
||||||
if (a.unison != lastUnisonA) { oscA.noteOn (freqA, a, seed); lastUnisonA = a.unison; }
|
if (oscillatorsNeedNoteOn)
|
||||||
if (b.unison != lastUnisonB) { oscB.noteOn (freqB, b, seed + 1); lastUnisonB = b.unison; }
|
{
|
||||||
|
oscA.noteOn (freqA, a, seed);
|
||||||
|
oscB.noteOn (freqB, b, seed + 1);
|
||||||
|
oscillatorsNeedNoteOn = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oscA.setParams (a);
|
||||||
|
oscB.setParams (b);
|
||||||
|
}
|
||||||
|
|
||||||
// 7. Modulated filter parameters.
|
// 7. Modulated filter parameters.
|
||||||
FilterBankParams fb = ctx.filters;
|
FilterBankParams fb = ctx.filters;
|
||||||
@@ -177,6 +188,8 @@ void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderC
|
|||||||
const Wavetable& wtA = ctx.wavetables->getTable (a.wave);
|
const Wavetable& wtA = ctx.wavetables->getTable (a.wave);
|
||||||
const Wavetable& wtB = ctx.wavetables->getTable (b.wave);
|
const Wavetable& wtB = ctx.wavetables->getTable (b.wave);
|
||||||
const double subMult = (ctx.subOct == -2) ? 0.25 : (ctx.subOct == -1) ? 0.5 : 1.0;
|
const double subMult = (ctx.subOct == -2) ? 0.25 : (ctx.subOct == -1) ? 0.5 : 1.0;
|
||||||
|
const float subLevel = clampF (ctx.subLevel + mod[(int) ModTarget::SubLevel], 0.0f, 1.0f);
|
||||||
|
const float noiseLevel = clampF (ctx.noiseLevel + mod[(int) ModTarget::NoiseLevel], 0.0f, 1.0f);
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
{
|
{
|
||||||
@@ -186,9 +199,9 @@ void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderC
|
|||||||
|
|
||||||
float mono = 0.0f;
|
float mono = 0.0f;
|
||||||
if (ctx.subOn)
|
if (ctx.subOn)
|
||||||
sub.processAdd (bent * subMult, ctx.subShape, ctx.subLevel, mono);
|
sub.processAdd (bent * subMult, ctx.subShape, subLevel, mono);
|
||||||
if (ctx.noiseOn)
|
if (ctx.noiseOn)
|
||||||
noise.processAdd (ctx.noiseType, ctx.noiseLevel, mono);
|
noise.processAdd (ctx.noiseType, noiseLevel, mono);
|
||||||
l += mono;
|
l += mono;
|
||||||
r += mono;
|
r += mono;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -87,7 +87,7 @@ private:
|
|||||||
bool released = false;
|
bool released = false;
|
||||||
juce::uint64 noteId = 0;
|
juce::uint64 noteId = 0;
|
||||||
|
|
||||||
int lastUnisonA = 1, lastUnisonB = 1;
|
bool oscillatorsNeedNoteOn = false;
|
||||||
|
|
||||||
juce::uint32 seed = 0;
|
juce::uint32 seed = 0;
|
||||||
float noteRandom = 0.5f;
|
float noteRandom = 0.5f;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcp": {
|
||||||
|
"graft": {
|
||||||
|
"type": "local",
|
||||||
|
"command": [
|
||||||
|
"graft",
|
||||||
|
"mcp"
|
||||||
|
],
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user