@mcptoolshop/claude-sfx 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/guard.d.ts +8 -0
- package/dist/guard.js +1 -1
- package/dist/hook-handler.js +14 -1
- package/dist/mix.d.ts +42 -0
- package/dist/mix.js +119 -0
- package/dist/synth.d.ts +5 -0
- package/dist/synth.js +15 -0
- package/dist/verbs.d.ts +4 -0
- package/dist/verbs.js +51 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Density-aware ducking** (`mix.ts`) — rolling 3s window classifies event density as sparse/medium/bursty. Gain and release are scaled per density level. High-priority verbs (commit, sync) duck less; low-priority (navigate) ducks more during bursts.
|
|
14
|
+
- **Repetition guard** — same verb firing 2+ times in 3s triggers variant rotation, gain reduction (×0.95→×0.85), and release shortening. Prevents search/grep spam from becoming a chirp wall.
|
|
15
|
+
- **Remote low-pass softening** — one-pole RC filter (6dB/octave rolloff at 2500 Hz) applied to all remote-scope sounds. Mimics distance by absorbing high frequencies, complementing the existing envelope/gain changes.
|
|
16
|
+
- **Per-verb ducking priorities** — commit/sync are "high" (protected from heavy ducking), navigate is "low" (maximally ducked during bursts), others are "normal"
|
|
17
|
+
- **Gain floor** at 0.40 prevents sounds from disappearing under worst-case stacking (bursty + many repeats + low priority)
|
|
18
|
+
- `applyLowPass(buffer, cutoffHz)` in synth.ts — reusable one-pole LP filter
|
|
19
|
+
- `MixAdvice` type and `getMixAdvice()` in mix.ts — pure function returning ducking adjustments
|
|
20
|
+
- `mixGain` and `mixRelease` fields in `PlayOptions` for density/repetition adjustments
|
|
21
|
+
- 59 new tests (262 total)
|
|
22
|
+
|
|
10
23
|
## [1.2.1] - 2026-03-30
|
|
11
24
|
|
|
12
25
|
### Fixed
|
package/dist/guard.d.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* Each play call reads/writes a small JSON ledger.
|
|
7
7
|
*/
|
|
8
8
|
import { type SfxConfig } from "./config.js";
|
|
9
|
+
export interface LedgerEntry {
|
|
10
|
+
verb: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Ledger {
|
|
14
|
+
entries: LedgerEntry[];
|
|
15
|
+
}
|
|
16
|
+
export declare function readLedger(): Ledger;
|
|
9
17
|
export interface GuardResult {
|
|
10
18
|
allowed: boolean;
|
|
11
19
|
reason?: string;
|
package/dist/guard.js
CHANGED
|
@@ -18,7 +18,7 @@ const RATE_LIMIT_MAX = 8;
|
|
|
18
18
|
const RATE_LIMIT_WINDOW_MS = 10_000;
|
|
19
19
|
/** Ledger file — tiny JSON tracking recent plays. */
|
|
20
20
|
const LEDGER_FILE = join(tmpdir(), "claude-sfx-ledger.json");
|
|
21
|
-
function readLedger() {
|
|
21
|
+
export function readLedger() {
|
|
22
22
|
if (!existsSync(LEDGER_FILE)) {
|
|
23
23
|
return { entries: [] };
|
|
24
24
|
}
|
package/dist/hook-handler.js
CHANGED
|
@@ -14,7 +14,8 @@ import { applyVolume } from "./synth.js";
|
|
|
14
14
|
import { playSync } from "./player.js";
|
|
15
15
|
import { resolveProfile } from "./profiles.js";
|
|
16
16
|
import { loadConfig, resolveProfileName, volumeToGain } from "./config.js";
|
|
17
|
-
import { guardPlay } from "./guard.js";
|
|
17
|
+
import { guardPlay, readLedger } from "./guard.js";
|
|
18
|
+
import { getMixAdvice } from "./mix.js";
|
|
18
19
|
import { resolveAmbient, isAmbientRunning } from "./ambient.js";
|
|
19
20
|
import { recordSuccess, recordError, getSessionOutcome, resetStreak } from "./streak.js";
|
|
20
21
|
/** Map a PostToolUse tool_name to a verb + options. */
|
|
@@ -122,6 +123,18 @@ function playVerb(verb, options = {}) {
|
|
|
122
123
|
const { intensity } = recordSuccess(verb);
|
|
123
124
|
options = { ...options, intensity };
|
|
124
125
|
}
|
|
126
|
+
// Mix intelligence — density ducking + repetition guard
|
|
127
|
+
const ledger = readLedger();
|
|
128
|
+
const advice = getMixAdvice(verb, ledger.entries);
|
|
129
|
+
if (advice.gainMultiplier < 1) {
|
|
130
|
+
options = { ...options, mixGain: advice.gainMultiplier };
|
|
131
|
+
}
|
|
132
|
+
if (advice.releaseMultiplier < 1) {
|
|
133
|
+
options = { ...options, mixRelease: advice.releaseMultiplier };
|
|
134
|
+
}
|
|
135
|
+
if (advice.forcedVariantIndex !== undefined && options.variantIndex === undefined) {
|
|
136
|
+
options = { ...options, variantIndex: advice.forcedVariantIndex };
|
|
137
|
+
}
|
|
125
138
|
const profileName = resolveProfileName(config);
|
|
126
139
|
const profile = resolveProfile(profileName);
|
|
127
140
|
let buffer = generateVerb(profile, verb, options);
|
package/dist/mix.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mix intelligence — density-aware ducking and repetition guard.
|
|
3
|
+
* Pure functions that read the guard ledger and return adjustment advice.
|
|
4
|
+
* No side effects, no writes — the guard module owns the ledger.
|
|
5
|
+
*/
|
|
6
|
+
import type { LedgerEntry } from "./guard.js";
|
|
7
|
+
export type DensityLevel = "sparse" | "medium" | "bursty";
|
|
8
|
+
export type DuckingPriority = "high" | "normal" | "low";
|
|
9
|
+
export interface MixAdvice {
|
|
10
|
+
/** Gain multiplier (0.4–1.0), applied on top of existing gain. */
|
|
11
|
+
gainMultiplier: number;
|
|
12
|
+
/** Release multiplier (< 1.0 shortens tails under density). */
|
|
13
|
+
releaseMultiplier: number;
|
|
14
|
+
/** If set, force this variant index (for repetition rotation). */
|
|
15
|
+
forcedVariantIndex?: number;
|
|
16
|
+
/** Current density level (for diagnostics/testing). */
|
|
17
|
+
density: DensityLevel;
|
|
18
|
+
/** How many times the same verb repeated recently. */
|
|
19
|
+
recentRepeatCount: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Classify event density based on recent entries in the window.
|
|
23
|
+
* 0–2 events = sparse, 3–5 = medium, 6+ = bursty.
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyDensity(entries: LedgerEntry[], now?: number): DensityLevel;
|
|
26
|
+
/**
|
|
27
|
+
* Count how many times the same verb fired in the recent window.
|
|
28
|
+
* Returns the count of prior occurrences (0 = first time).
|
|
29
|
+
*/
|
|
30
|
+
export declare function countRecentRepeats(entries: LedgerEntry[], verb: string, now?: number, windowMs?: number): number;
|
|
31
|
+
/**
|
|
32
|
+
* Get the ducking priority for a verb.
|
|
33
|
+
* High = important culmination events (less ducking).
|
|
34
|
+
* Low = frequent scan events (more ducking during bursts).
|
|
35
|
+
* Normal = everything else.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getDuckingPriority(verb: string): DuckingPriority;
|
|
38
|
+
/**
|
|
39
|
+
* Compute mix advice for a verb given the current ledger state.
|
|
40
|
+
* Returns gain/release multipliers, optional forced variant, and diagnostics.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getMixAdvice(verb: string, entries: LedgerEntry[], now?: number): MixAdvice;
|
package/dist/mix.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mix intelligence — density-aware ducking and repetition guard.
|
|
3
|
+
* Pure functions that read the guard ledger and return adjustment advice.
|
|
4
|
+
* No side effects, no writes — the guard module owns the ledger.
|
|
5
|
+
*/
|
|
6
|
+
// --- Constants ---
|
|
7
|
+
/** Window for density classification and repeat counting. */
|
|
8
|
+
const DENSITY_WINDOW_MS = 3000;
|
|
9
|
+
/** Minimum gain multiplier — prevents sounds from disappearing entirely. */
|
|
10
|
+
const GAIN_FLOOR = 0.40;
|
|
11
|
+
// --- Density classification ---
|
|
12
|
+
/**
|
|
13
|
+
* Classify event density based on recent entries in the window.
|
|
14
|
+
* 0–2 events = sparse, 3–5 = medium, 6+ = bursty.
|
|
15
|
+
*/
|
|
16
|
+
export function classifyDensity(entries, now = Date.now()) {
|
|
17
|
+
const cutoff = now - DENSITY_WINDOW_MS;
|
|
18
|
+
const recent = entries.filter((e) => e.timestamp > cutoff);
|
|
19
|
+
if (recent.length <= 2)
|
|
20
|
+
return "sparse";
|
|
21
|
+
if (recent.length <= 5)
|
|
22
|
+
return "medium";
|
|
23
|
+
return "bursty";
|
|
24
|
+
}
|
|
25
|
+
// --- Repetition counting ---
|
|
26
|
+
/**
|
|
27
|
+
* Count how many times the same verb fired in the recent window.
|
|
28
|
+
* Returns the count of prior occurrences (0 = first time).
|
|
29
|
+
*/
|
|
30
|
+
export function countRecentRepeats(entries, verb, now = Date.now(), windowMs = DENSITY_WINDOW_MS) {
|
|
31
|
+
const cutoff = now - windowMs;
|
|
32
|
+
return entries.filter((e) => e.timestamp > cutoff && e.verb === verb).length;
|
|
33
|
+
}
|
|
34
|
+
// --- Priority tiers ---
|
|
35
|
+
/**
|
|
36
|
+
* Get the ducking priority for a verb.
|
|
37
|
+
* High = important culmination events (less ducking).
|
|
38
|
+
* Low = frequent scan events (more ducking during bursts).
|
|
39
|
+
* Normal = everything else.
|
|
40
|
+
*/
|
|
41
|
+
export function getDuckingPriority(verb) {
|
|
42
|
+
switch (verb) {
|
|
43
|
+
case "commit":
|
|
44
|
+
case "sync":
|
|
45
|
+
return "high";
|
|
46
|
+
case "navigate":
|
|
47
|
+
return "low";
|
|
48
|
+
default:
|
|
49
|
+
return "normal";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// --- Density ducking tables ---
|
|
53
|
+
const DENSITY_GAIN = {
|
|
54
|
+
sparse: { high: 1.0, normal: 1.0, low: 1.0 },
|
|
55
|
+
medium: { high: 0.95, normal: 0.88, low: 0.82 },
|
|
56
|
+
bursty: { high: 0.90, normal: 0.75, low: 0.65 },
|
|
57
|
+
};
|
|
58
|
+
const DENSITY_RELEASE = {
|
|
59
|
+
sparse: 1.0,
|
|
60
|
+
medium: 0.85,
|
|
61
|
+
bursty: 0.70,
|
|
62
|
+
};
|
|
63
|
+
// --- Repetition ducking ---
|
|
64
|
+
function getRepetitionGain(repeatCount) {
|
|
65
|
+
if (repeatCount <= 1)
|
|
66
|
+
return 1.0;
|
|
67
|
+
if (repeatCount === 2)
|
|
68
|
+
return 0.95;
|
|
69
|
+
if (repeatCount === 3)
|
|
70
|
+
return 0.90;
|
|
71
|
+
return 0.85; // 4+
|
|
72
|
+
}
|
|
73
|
+
function getRepetitionRelease(repeatCount) {
|
|
74
|
+
if (repeatCount <= 1)
|
|
75
|
+
return 1.0;
|
|
76
|
+
if (repeatCount === 2)
|
|
77
|
+
return 1.0;
|
|
78
|
+
if (repeatCount === 3)
|
|
79
|
+
return 0.85;
|
|
80
|
+
return 0.80; // 4+
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Determine variant rotation for repeated verbs.
|
|
84
|
+
* After 2+ repeats, force rotation to the next variant.
|
|
85
|
+
*/
|
|
86
|
+
function getRepetitionVariant(entries, verb, now, repeatCount) {
|
|
87
|
+
if (repeatCount < 2)
|
|
88
|
+
return undefined;
|
|
89
|
+
// Rotate based on repeat count (simple modular rotation across 3 variants)
|
|
90
|
+
return repeatCount % 3;
|
|
91
|
+
}
|
|
92
|
+
// --- Main entry point ---
|
|
93
|
+
/**
|
|
94
|
+
* Compute mix advice for a verb given the current ledger state.
|
|
95
|
+
* Returns gain/release multipliers, optional forced variant, and diagnostics.
|
|
96
|
+
*/
|
|
97
|
+
export function getMixAdvice(verb, entries, now = Date.now()) {
|
|
98
|
+
const density = classifyDensity(entries, now);
|
|
99
|
+
const priority = getDuckingPriority(verb);
|
|
100
|
+
const repeatCount = countRecentRepeats(entries, verb, now);
|
|
101
|
+
// Density ducking
|
|
102
|
+
const densityGain = DENSITY_GAIN[density][priority];
|
|
103
|
+
const densityRelease = DENSITY_RELEASE[density];
|
|
104
|
+
// Repetition ducking (composes multiplicatively)
|
|
105
|
+
const repGain = getRepetitionGain(repeatCount);
|
|
106
|
+
const repRelease = getRepetitionRelease(repeatCount);
|
|
107
|
+
// Compose
|
|
108
|
+
const gainMultiplier = Math.max(densityGain * repGain, GAIN_FLOOR);
|
|
109
|
+
const releaseMultiplier = Math.max(densityRelease * repRelease, 0.50);
|
|
110
|
+
// Variant rotation
|
|
111
|
+
const forcedVariantIndex = getRepetitionVariant(entries, verb, now, repeatCount);
|
|
112
|
+
return {
|
|
113
|
+
gainMultiplier,
|
|
114
|
+
releaseMultiplier,
|
|
115
|
+
forcedVariantIndex,
|
|
116
|
+
density,
|
|
117
|
+
recentRepeatCount: repeatCount,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/synth.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export declare function mixBuffers(buffers: Float64Array[]): Float64Array;
|
|
|
46
46
|
export declare function concatBuffers(buffers: Float64Array[], gapSeconds?: number): Float64Array;
|
|
47
47
|
/** Encode a Float64 audio buffer as a WAV file (PCM 16-bit mono). */
|
|
48
48
|
export declare function encodeWav(buffer: Float64Array): Buffer;
|
|
49
|
+
/**
|
|
50
|
+
* One-pole RC low-pass filter.
|
|
51
|
+
* Gentle 6dB/octave rolloff — mimics distance (air absorbs highs).
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyLowPass(buffer: Float64Array, cutoffHz: number): Float64Array;
|
|
49
54
|
/** Apply a hard loudness cap with gentle compression to a buffer. */
|
|
50
55
|
export declare function limitLoudness(buffer: Float64Array, ceiling?: number): Float64Array;
|
|
51
56
|
/** Apply a volume gain (0.0–1.0) to a buffer. */
|
package/dist/synth.js
CHANGED
|
@@ -187,6 +187,21 @@ export function encodeWav(buffer) {
|
|
|
187
187
|
}
|
|
188
188
|
return wav;
|
|
189
189
|
}
|
|
190
|
+
// --- Low-Pass Filter ---
|
|
191
|
+
/**
|
|
192
|
+
* One-pole RC low-pass filter.
|
|
193
|
+
* Gentle 6dB/octave rolloff — mimics distance (air absorbs highs).
|
|
194
|
+
*/
|
|
195
|
+
export function applyLowPass(buffer, cutoffHz) {
|
|
196
|
+
const a = Math.exp(-2 * Math.PI * cutoffHz / SAMPLE_RATE);
|
|
197
|
+
const result = new Float64Array(buffer.length);
|
|
198
|
+
let prev = 0;
|
|
199
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
200
|
+
prev = (1 - a) * buffer[i] + a * prev;
|
|
201
|
+
result[i] = prev;
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
190
205
|
// --- Loudness Limiter ---
|
|
191
206
|
/** Apply a hard loudness cap with gentle compression to a buffer. */
|
|
192
207
|
export function limitLoudness(buffer, ceiling = 0.85) {
|
package/dist/verbs.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface PlayOptions {
|
|
|
19
19
|
escalation?: number;
|
|
20
20
|
/** Override variant selection (0-indexed). For testing determinism. */
|
|
21
21
|
variantIndex?: number;
|
|
22
|
+
/** Mix-layer gain multiplier from density/repetition analysis (0.4–1.0). */
|
|
23
|
+
mixGain?: number;
|
|
24
|
+
/** Mix-layer release multiplier from density/repetition analysis. */
|
|
25
|
+
mixRelease?: number;
|
|
22
26
|
}
|
|
23
27
|
export type Verb = "intake" | "transform" | "commit" | "navigate" | "execute" | "move" | "sync";
|
|
24
28
|
export declare const VERB_LABELS: Record<Verb, string>;
|
package/dist/verbs.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Architecture: multi-note motifs in A major pentatonic with constrained variation.
|
|
6
6
|
* Each verb has 3 variants. Micro-jitter keeps sounds alive without breaking identity.
|
|
7
7
|
*/
|
|
8
|
-
import { SAMPLE_RATE, generateTone, generateWhoosh, mixBuffers, limitLoudness, } from "./synth.js";
|
|
8
|
+
import { SAMPLE_RATE, generateTone, generateWhoosh, mixBuffers, limitLoudness, applyLowPass, applyVolume, } from "./synth.js";
|
|
9
9
|
import { SCALE, scaleStepDown } from "./profiles.js";
|
|
10
10
|
export const VERB_LABELS = {
|
|
11
11
|
intake: "Intake",
|
|
@@ -335,6 +335,30 @@ function applyWhooshEscalation(params, escalation) {
|
|
|
335
335
|
}
|
|
336
336
|
return p;
|
|
337
337
|
}
|
|
338
|
+
// --- Mix modifiers (density/repetition from Pass B) ---
|
|
339
|
+
/** Scale all note release times in a motif (density/repetition ducking). */
|
|
340
|
+
function applyMotifMixRelease(variant, multiplier) {
|
|
341
|
+
if (multiplier >= 1)
|
|
342
|
+
return variant;
|
|
343
|
+
return {
|
|
344
|
+
...variant,
|
|
345
|
+
notes: variant.notes.map(n => ({
|
|
346
|
+
...n,
|
|
347
|
+
envelope: { ...n.envelope, release: n.envelope.release * multiplier },
|
|
348
|
+
})),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/** Scale whoosh envelope release (density/repetition ducking). */
|
|
352
|
+
function applyWhooshMixRelease(params, multiplier) {
|
|
353
|
+
if (multiplier >= 1)
|
|
354
|
+
return params;
|
|
355
|
+
return {
|
|
356
|
+
...params,
|
|
357
|
+
envelope: { ...params.envelope, release: params.envelope.release * multiplier },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** Low-pass cutoff for remote scope (Hz). Gentle rolloff mimicking distance. */
|
|
361
|
+
const REMOTE_LP_CUTOFF = 2500;
|
|
338
362
|
// --- Main generation ---
|
|
339
363
|
/** Generate a verb sound using the given profile. */
|
|
340
364
|
export function generateVerb(profile, verb, options = {}) {
|
|
@@ -342,6 +366,7 @@ export function generateVerb(profile, verb, options = {}) {
|
|
|
342
366
|
if (!cfg) {
|
|
343
367
|
throw new Error(`Profile "${profile.name}" has no config for verb "${verb}"`);
|
|
344
368
|
}
|
|
369
|
+
const isRemote = options.scope === "remote";
|
|
345
370
|
// ── Whoosh-based verbs (move, sync) ─────────────────────────
|
|
346
371
|
if (cfg.type === "whoosh") {
|
|
347
372
|
const dir = options.direction ?? "up";
|
|
@@ -362,11 +387,13 @@ export function generateVerb(profile, verb, options = {}) {
|
|
|
362
387
|
wp = applyWhooshIntensity(wp, options.intensity);
|
|
363
388
|
if (options.escalation)
|
|
364
389
|
wp = applyWhooshEscalation(wp, options.escalation);
|
|
390
|
+
if (options.mixRelease && options.mixRelease < 1)
|
|
391
|
+
wp = applyWhooshMixRelease(wp, options.mixRelease);
|
|
365
392
|
const whooshBuf = generateWhoosh(wp);
|
|
366
393
|
// Tonal anchor variant
|
|
394
|
+
let result;
|
|
367
395
|
if (cfg.anchorVariants && cfg.anchorVariants.length > 0) {
|
|
368
396
|
let anchor = pickVariant(cfg.anchorVariants, options.variantIndex);
|
|
369
|
-
// Apply motif modifiers to anchor notes too
|
|
370
397
|
let anchorMotif = { notes: anchor.notes };
|
|
371
398
|
if (options.status)
|
|
372
399
|
anchorMotif = applyMotifStatus(anchorMotif, options.status);
|
|
@@ -376,16 +403,26 @@ export function generateVerb(profile, verb, options = {}) {
|
|
|
376
403
|
anchorMotif = applyMotifIntensity(anchorMotif, options.intensity);
|
|
377
404
|
if (options.escalation)
|
|
378
405
|
anchorMotif = applyMotifEscalation(anchorMotif, options.escalation);
|
|
406
|
+
if (options.mixRelease && options.mixRelease < 1)
|
|
407
|
+
anchorMotif = applyMotifMixRelease(anchorMotif, options.mixRelease);
|
|
379
408
|
const anchorBuf = renderMotif(anchorMotif, cfg.gain);
|
|
380
|
-
// Mix: align to same start, extend to longest
|
|
381
409
|
const maxLen = Math.max(whooshBuf.length, anchorBuf.length);
|
|
382
410
|
const whooshPadded = new Float64Array(maxLen);
|
|
383
411
|
whooshPadded.set(whooshBuf, 0);
|
|
384
412
|
const anchorPadded = new Float64Array(maxLen);
|
|
385
413
|
anchorPadded.set(anchorBuf, 0);
|
|
386
|
-
|
|
414
|
+
result = mixBuffers([whooshPadded, anchorPadded]);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
result = whooshBuf;
|
|
387
418
|
}
|
|
388
|
-
|
|
419
|
+
// Remote low-pass softening
|
|
420
|
+
if (isRemote)
|
|
421
|
+
result = applyLowPass(result, REMOTE_LP_CUTOFF);
|
|
422
|
+
// Mix gain ducking
|
|
423
|
+
if (options.mixGain && options.mixGain < 1)
|
|
424
|
+
result = applyVolume(result, options.mixGain);
|
|
425
|
+
return limitLoudness(result);
|
|
389
426
|
}
|
|
390
427
|
// ── Motif-based verbs (intake, transform, commit, navigate, execute) ──
|
|
391
428
|
let variant = pickVariant(cfg.variants, options.variantIndex);
|
|
@@ -397,7 +434,15 @@ export function generateVerb(profile, verb, options = {}) {
|
|
|
397
434
|
variant = applyMotifIntensity(variant, options.intensity);
|
|
398
435
|
if (options.escalation)
|
|
399
436
|
variant = applyMotifEscalation(variant, options.escalation);
|
|
400
|
-
|
|
437
|
+
if (options.mixRelease && options.mixRelease < 1)
|
|
438
|
+
variant = applyMotifMixRelease(variant, options.mixRelease);
|
|
439
|
+
let buffer = renderMotif(variant, cfg.gain);
|
|
440
|
+
// Remote low-pass softening
|
|
441
|
+
if (isRemote)
|
|
442
|
+
buffer = applyLowPass(buffer, REMOTE_LP_CUTOFF);
|
|
443
|
+
// Mix gain ducking
|
|
444
|
+
if (options.mixGain && options.mixGain < 1)
|
|
445
|
+
buffer = applyVolume(buffer, options.mixGain);
|
|
401
446
|
return limitLoudness(buffer);
|
|
402
447
|
}
|
|
403
448
|
// --- Session sounds ---
|