@hraness/dawg 0.6.1 → 0.8.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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +82 -32
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/src/audio/strings/bow.ts
CHANGED
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
* force and falls with beta and the mute (`sord`).
|
|
22
22
|
* - Slur: `slurTo` retunes over 7 ms and keeps the bow (no new attack).
|
|
23
23
|
* - Tremolo: `tremhz` reverses the bow that many times a second, with a
|
|
24
|
-
* seeded per-voice rate jitter.
|
|
24
|
+
* seeded per-voice rate jitter. At each reversal the bow force eases off
|
|
25
|
+
* with the bow speed (the friction curve narrows as dir^2), so the
|
|
26
|
+
* string rings through the turn instead of sticking to a slow bow and
|
|
27
|
+
* flattening; the pitch lock averages each stroke's middle (one update
|
|
28
|
+
* per stroke) instead of chasing the turns' transient periods.
|
|
25
29
|
*/
|
|
26
30
|
import { Decimate2 } from "../dsp/oversample.ts";
|
|
27
31
|
import { thiranFor } from "../dsp/interp.ts";
|
|
@@ -52,6 +56,12 @@ export const SLUR_RANGE = 24;
|
|
|
52
56
|
*/
|
|
53
57
|
const LOCK_GAIN = 0.25;
|
|
54
58
|
const LOCK_RANGE = 0.03;
|
|
59
|
+
/** Tremolo: |sin| of the reversal phase past which a stroke is steady. */
|
|
60
|
+
const STROKE_STEADY = 0.7;
|
|
61
|
+
/** Tremolo: lock step per stroke, on the stroke's mean period error. */
|
|
62
|
+
const STROKE_LOCK_GAIN = 0.2;
|
|
63
|
+
/** Tremolo: the friction slope widens at most 1 / FLOOR^2 at a turn. */
|
|
64
|
+
const TURN_FORCE_FLOOR = 0.05;
|
|
55
65
|
/** Speed ratio between dyn 0 and dyn 1 (the review's 8:1). */
|
|
56
66
|
const SPEED_RATIO = 8;
|
|
57
67
|
/** Extra gain law across dyn, in dB (with the 18 dB speed law: 28 dB). */
|
|
@@ -323,6 +333,11 @@ export class BowedString {
|
|
|
323
333
|
private readonly decim?: Decimate2;
|
|
324
334
|
private readonly tremRate: number;
|
|
325
335
|
private readonly tremPhase: number;
|
|
336
|
+
/** Tremolo: between turns, and that stroke's lock measurements. */
|
|
337
|
+
private steady = true;
|
|
338
|
+
private strokeErr = 0;
|
|
339
|
+
private strokeCount = 0;
|
|
340
|
+
private strokeSkip = 0;
|
|
326
341
|
/** Internal-rate sample count. */
|
|
327
342
|
private n = 0;
|
|
328
343
|
private off: number;
|
|
@@ -548,6 +563,20 @@ export class BowedString {
|
|
|
548
563
|
Math.PI * (this.tremRate * (n / this.rate) + this.tremPhase),
|
|
549
564
|
);
|
|
550
565
|
dir = Math.tanh(4 * s);
|
|
566
|
+
const steady = Math.abs(s) > STROKE_STEADY;
|
|
567
|
+
if (steady !== this.steady) {
|
|
568
|
+
// Leaving a stroke's middle: one lock step on its mean error.
|
|
569
|
+
if (this.steady && this.strokeCount > 0)
|
|
570
|
+
this.lock = clamp(
|
|
571
|
+
this.lock - (STROKE_LOCK_GAIN * this.strokeErr) / this.strokeCount,
|
|
572
|
+
-LOCK_RANGE,
|
|
573
|
+
LOCK_RANGE,
|
|
574
|
+
);
|
|
575
|
+
this.steady = steady;
|
|
576
|
+
this.strokeErr = 0;
|
|
577
|
+
this.strokeCount = 0;
|
|
578
|
+
this.strokeSkip = 0;
|
|
579
|
+
}
|
|
551
580
|
}
|
|
552
581
|
// Note-off lifts the bow: the normal force falls over `release` (the
|
|
553
582
|
// friction curve narrows, STK's bow pressure), with the bow still
|
|
@@ -556,7 +585,10 @@ export class BowedString {
|
|
|
556
585
|
// half-reflecting junction that brakes the string.
|
|
557
586
|
const bowVel = this.vtarget * (on ? env : 1) * dir;
|
|
558
587
|
const contact = on || env > 0 ? 1 : 0;
|
|
559
|
-
|
|
588
|
+
let slope = on ? this.slope : this.slope / Math.max(1e-3, env * env);
|
|
589
|
+
// A turning bow presses less: the friction curve narrows with speed.
|
|
590
|
+
if (this.tremRate > 0)
|
|
591
|
+
slope /= Math.max(TURN_FORCE_FLOOR, Math.abs(dir)) ** 2;
|
|
560
592
|
const bOut = bb[(this.bw - this.Nb) & mask]!;
|
|
561
593
|
const raw = nb[(this.nw - this.Nn) & mask]!;
|
|
562
594
|
const nOut = this.a * raw + this.tx1 - this.a * this.ty1;
|
|
@@ -566,7 +598,9 @@ export class BowedString {
|
|
|
566
598
|
const bridgeRefl = -this.sy;
|
|
567
599
|
const nutRefl = -nOut;
|
|
568
600
|
const dv = bowVel - (bridgeRefl + nutRefl);
|
|
569
|
-
|
|
601
|
+
const r = 1 / (Math.abs(dv * slope) + 0.75);
|
|
602
|
+
const r2 = r * r;
|
|
603
|
+
let f = r2 * r2;
|
|
570
604
|
if (f > 1) f = 1;
|
|
571
605
|
const nv = contact * dv * f;
|
|
572
606
|
nb[this.nw & mask] = bridgeRefl + nv;
|
|
@@ -591,8 +625,9 @@ export class BowedString {
|
|
|
591
625
|
this.bpY1 = y;
|
|
592
626
|
if (!(prev < 0 && y >= 0)) return;
|
|
593
627
|
const cross = n - 1 + prev / (prev - y);
|
|
594
|
-
|
|
595
|
-
this.lastCross
|
|
628
|
+
// Tremolo: only periods wholly inside a stroke's middle count.
|
|
629
|
+
const last = this.steady ? this.lastCross : -1;
|
|
630
|
+
this.lastCross = this.steady ? cross : -1;
|
|
596
631
|
// Settle first: the attack and a slur's glide are not steady periods.
|
|
597
632
|
if (last < 0 || n < 0.05 * this.rate) return;
|
|
598
633
|
if (this.slurAt >= 0 && n < this.slurAt + this.slurLen + 0.02 * this.rate)
|
|
@@ -600,6 +635,14 @@ export class BowedString {
|
|
|
600
635
|
const period = cross - last;
|
|
601
636
|
const err = period / this.target - 1;
|
|
602
637
|
if (Math.abs(err) > 0.1) return;
|
|
638
|
+
if (this.tremRate > 0) {
|
|
639
|
+
// The first period of a stroke still carries the turn.
|
|
640
|
+
if ((this.strokeSkip += 1) > 1) {
|
|
641
|
+
this.strokeErr += err;
|
|
642
|
+
this.strokeCount += 1;
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
603
646
|
this.lock = clamp(this.lock - LOCK_GAIN * err, -LOCK_RANGE, LOCK_RANGE);
|
|
604
647
|
}
|
|
605
648
|
|
|
@@ -20,6 +20,7 @@ import { interpolateAutomation } from "../effects/common.ts";
|
|
|
20
20
|
import { seedHash, unit } from "../dsp/rng.ts";
|
|
21
21
|
import type { EngineContext, InstrumentEngine } from "../instruments.ts";
|
|
22
22
|
import { applyBody, bodyFor, symKeys, sympathetic } from "./body.ts";
|
|
23
|
+
import { keysTrim } from "../keys/calibration.ts";
|
|
23
24
|
import { clamp } from "./loop.ts";
|
|
24
25
|
import { PluckString, type Exciter, type PluckSpec } from "./pluck.ts";
|
|
25
26
|
import {
|
|
@@ -337,6 +338,10 @@ export function renderStrings(
|
|
|
337
338
|
const cap = Math.max(1, Math.round(values.voices as number));
|
|
338
339
|
const tuning = context.tuning;
|
|
339
340
|
const bowed = isBowed(values);
|
|
341
|
+
// Calibration 1 (q08): the keyboard string presets (the harpsichord) take
|
|
342
|
+
// the keys' key-tracked level trim; other presets have no row.
|
|
343
|
+
const trimPreset =
|
|
344
|
+
(context.score.calibration ?? 0) >= 1 ? track.string?.preset : undefined;
|
|
340
345
|
const L = new Float64Array(total);
|
|
341
346
|
const R = dryR ? new Float64Array(total) : undefined;
|
|
342
347
|
const voices: Voice[] = [];
|
|
@@ -416,6 +421,7 @@ export function renderStrings(
|
|
|
416
421
|
`${note.id}:${note.startTick + (context.seedTick ?? 0)}:string`,
|
|
417
422
|
);
|
|
418
423
|
const strings: { hz: number; gain: number; ratio: number }[] = [];
|
|
424
|
+
const trim = trimPreset ? keysTrim(trimPreset, note.pitch) : 1;
|
|
419
425
|
// The course's tuning error belongs to the key, not to the strike: it
|
|
420
426
|
// is seeded by track and key (so every velocity of a key beats the
|
|
421
427
|
// same way) and centred, so the course's mean pitch is the table's.
|
|
@@ -430,7 +436,7 @@ export function renderStrings(
|
|
|
430
436
|
const ratio = 2 ** ((d * 100 + jitter) / 1200);
|
|
431
437
|
strings.push({
|
|
432
438
|
hz: baseHz * ratio,
|
|
433
|
-
gain:
|
|
439
|
+
gain: trim / Math.sqrt(unison),
|
|
434
440
|
ratio,
|
|
435
441
|
});
|
|
436
442
|
}
|
|
@@ -442,7 +448,7 @@ export function renderStrings(
|
|
|
442
448
|
hz * 2 ** (((unit(course, unison, 1) - 0.5) * 4) / 1200);
|
|
443
449
|
strings.push({
|
|
444
450
|
hz: tuned,
|
|
445
|
-
gain: oct / Math.sqrt(unison),
|
|
451
|
+
gain: (trim * oct) / Math.sqrt(unison),
|
|
446
452
|
ratio: tuned / baseHz,
|
|
447
453
|
});
|
|
448
454
|
}
|
|
@@ -110,28 +110,38 @@ function triangle(phase: number): number {
|
|
|
110
110
|
* names when a track sets synth parameters.
|
|
111
111
|
*/
|
|
112
112
|
export function legacyWave(instrument: string, phase: number): number {
|
|
113
|
+
return legacyWaveFor(instrument)(phase);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `legacyWave` with the tone chosen once: `legacyWaveFor(name)(phase)`
|
|
118
|
+
* equals `legacyWave(name, phase)`, without matching the name per sample.
|
|
119
|
+
*/
|
|
120
|
+
export function legacyWaveFor(instrument: string): (phase: number) => number {
|
|
113
121
|
const name = instrument.trim().toLowerCase();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (name.includes("
|
|
117
|
-
|
|
118
|
-
if (name.includes("triangle"))
|
|
119
|
-
|
|
122
|
+
if (name.includes("square"))
|
|
123
|
+
return (phase) => (phase - Math.floor(phase) < 0.5 ? 1 : -1);
|
|
124
|
+
if (name.includes("saw"))
|
|
125
|
+
return (phase) => 2 * (phase - Math.floor(phase)) - 1;
|
|
126
|
+
if (name.includes("triangle"))
|
|
127
|
+
return (phase) => 1 - 4 * Math.abs(phase - Math.floor(phase) - 0.5);
|
|
128
|
+
if (name.includes("bass"))
|
|
120
129
|
// A rounded fundamental plus a quiet octave gives bass tracks useful weight.
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
0.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (name.includes("piano") || name.includes("pluck"))
|
|
130
|
+
return (phase) =>
|
|
131
|
+
Math.tanh(
|
|
132
|
+
0.9 * Math.sin(2 * Math.PI * phase) +
|
|
133
|
+
0.25 * Math.sin(4 * Math.PI * phase),
|
|
134
|
+
);
|
|
135
|
+
if (name.includes("piano") || name.includes("pluck"))
|
|
127
136
|
// Add stable harmonics; the envelope above supplies the note decay.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
return (phase) => {
|
|
138
|
+
const harmonic =
|
|
139
|
+
Math.sin(4 * Math.PI * phase) * 0.28 +
|
|
140
|
+
Math.sin(6 * Math.PI * phase) * 0.12;
|
|
141
|
+
return Math.tanh(Math.sin(2 * Math.PI * phase) + harmonic);
|
|
142
|
+
};
|
|
133
143
|
// Unknown instruments deliberately fall back to the original sine voice.
|
|
134
|
-
return
|
|
144
|
+
return (phase) => Math.sin(2 * Math.PI * phase);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
/**
|
|
@@ -230,9 +240,9 @@ const BUILT_IN: Readonly<Record<string, OscillatorFactory>> = Object.freeze({
|
|
|
230
240
|
pulse: (init) => (phase, increment) => pulse(phase, increment, init.width()),
|
|
231
241
|
triangle: (init) => (init.partials ? additive(init) : triangle),
|
|
232
242
|
user: (init) => additive(init),
|
|
233
|
-
piano: () => (
|
|
234
|
-
pluck: () => (
|
|
235
|
-
bass: () => (
|
|
243
|
+
piano: () => legacyWaveFor("piano"),
|
|
244
|
+
pluck: () => legacyWaveFor("pluck"),
|
|
245
|
+
bass: () => legacyWaveFor("bass"),
|
|
236
246
|
white: (init) => () => (init.random() * 2 - 1) * 0.6,
|
|
237
247
|
pink: (init) => pinkNoise(init.random),
|
|
238
248
|
brown: (init) => {
|
package/src/audio/synth/voice.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../../../core/synth.ts";
|
|
32
32
|
import {
|
|
33
33
|
Biquad,
|
|
34
|
+
Svf,
|
|
34
35
|
CONTROL_SAMPLES,
|
|
35
36
|
clamp,
|
|
36
37
|
interpolateAutomation,
|
|
@@ -197,6 +198,8 @@ const BUTTERWORTH_Q2 = 1.3066;
|
|
|
197
198
|
class NoteFilter {
|
|
198
199
|
private readonly first = new Biquad();
|
|
199
200
|
private readonly second = new Biquad();
|
|
201
|
+
private readonly svfFirst: Svf;
|
|
202
|
+
private readonly svfSecond: Svf;
|
|
200
203
|
private readonly ladder = new Float64Array(4);
|
|
201
204
|
private ladderG = 0;
|
|
202
205
|
private ladderK = 0;
|
|
@@ -210,7 +213,22 @@ class NoteFilter {
|
|
|
210
213
|
readonly anchor: number,
|
|
211
214
|
readonly envelope: Envelope,
|
|
212
215
|
private readonly sampleRate: number,
|
|
213
|
-
) {
|
|
216
|
+
) {
|
|
217
|
+
this.svfFirst = new Svf(type);
|
|
218
|
+
this.svfSecond = new Svf(type);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A negative depth sweeps the cutoff down during the attack, often to
|
|
223
|
+
* the 20 Hz floor within a few milliseconds. A direct-form biquad
|
|
224
|
+
* re-designed every control block blows up under that fall (its state
|
|
225
|
+
* belongs to the old coefficients), so those notes run on the SVF, whose
|
|
226
|
+
* state stays valid. Positive and zero depths keep the biquad, so their
|
|
227
|
+
* renders are unchanged.
|
|
228
|
+
*/
|
|
229
|
+
private get isSvf(): boolean {
|
|
230
|
+
return this.depth < 0;
|
|
231
|
+
}
|
|
214
232
|
|
|
215
233
|
private get isLadder(): boolean {
|
|
216
234
|
return this.slope === "ladder" && this.type === "lpf";
|
|
@@ -231,6 +249,17 @@ class NoteFilter {
|
|
|
231
249
|
this.ladderK = 3.9 * clamp((q - 0.707) / 11.3, 0, 1);
|
|
232
250
|
return;
|
|
233
251
|
}
|
|
252
|
+
if (this.isSvf) {
|
|
253
|
+
if (this.slope === "24db") {
|
|
254
|
+
this.svfFirst.set(frequency, BUTTERWORTH_Q1, this.sampleRate);
|
|
255
|
+
this.svfSecond.set(
|
|
256
|
+
frequency,
|
|
257
|
+
(q * BUTTERWORTH_Q2) / 0.707,
|
|
258
|
+
this.sampleRate,
|
|
259
|
+
);
|
|
260
|
+
} else this.svfFirst.set(frequency, q, this.sampleRate);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
234
263
|
if (this.slope === "24db") {
|
|
235
264
|
this.first.set(this.type, frequency, BUTTERWORTH_Q1, this.sampleRate);
|
|
236
265
|
this.second.set(
|
|
@@ -255,6 +284,10 @@ class NoteFilter {
|
|
|
255
284
|
stages[3]! += g * (stages[2]! - stages[3]!);
|
|
256
285
|
return stages[3]! * (1 + 0.5 * this.ladderK);
|
|
257
286
|
}
|
|
287
|
+
if (this.isSvf) {
|
|
288
|
+
const once = this.svfFirst.process(input);
|
|
289
|
+
return this.slope === "24db" ? this.svfSecond.process(once) : once;
|
|
290
|
+
}
|
|
258
291
|
const once = this.first.process(input);
|
|
259
292
|
return this.slope === "24db" ? this.second.process(once) : once;
|
|
260
293
|
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The channel vocoder (0.7): a bank of cascaded RBJ band-passes analyses
|
|
3
|
+
* the modulator, each band's follower shapes the same band of the carrier.
|
|
4
|
+
* Ported from the reviewed prototype (proto/vocoder, DSP v2):
|
|
5
|
+
* - layout from bands/lo/hi/width only (sample-rate independent);
|
|
6
|
+
* - band k reads the modulator advanced by its group delay plus the attack;
|
|
7
|
+
* - formant: synthesis band k takes the envelope at fractional analysis
|
|
8
|
+
* index k - formant / 12 / log2(ratio);
|
|
9
|
+
* - enhance whitens the carrier per band (each band normalised by its own
|
|
10
|
+
* follower), so a dull carrier still speaks in every band;
|
|
11
|
+
* - fixed makeup gain from the layout only, never from the rendered span.
|
|
12
|
+
* A stereo carrier runs one analysis and one synthesis bank per channel.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
bandpass,
|
|
16
|
+
bankDesign,
|
|
17
|
+
runBiquad,
|
|
18
|
+
type Biquad,
|
|
19
|
+
} from "../dsp/bandbank.ts";
|
|
20
|
+
import { follow, followCoef, gateCurve, holdCurve } from "../dsp/follow.ts";
|
|
21
|
+
import { unit } from "../dsp/rng.ts";
|
|
22
|
+
import { at, VOCODER_BLOCK, type VocoderControl } from "./control.ts";
|
|
23
|
+
import { hissPath, unvoicedCurve } from "./detect.ts";
|
|
24
|
+
|
|
25
|
+
/** Fixed channel makeup from the layout only (fitted once on the voice fixture). */
|
|
26
|
+
export function channelMakeup(bands: number, width: number): number {
|
|
27
|
+
return (
|
|
28
|
+
(1.4 * (bands / 16) ** 0.29) / (width <= 1 ? width ** 0.35 : width ** 1.1)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Reference band level for depth < 1: envelope^depth * REF^(1 - depth). */
|
|
33
|
+
const REF = 0.02;
|
|
34
|
+
|
|
35
|
+
/** A noise sample in -1..1 at an absolute song sample. */
|
|
36
|
+
export function noiseSample(seed: number, songSample: number): number {
|
|
37
|
+
return unit(seed, songSample, 11) * 2 - 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Follower with an optional per-sample release curve and hold. */
|
|
41
|
+
function followVarying(
|
|
42
|
+
x: Float64Array,
|
|
43
|
+
attack: number,
|
|
44
|
+
release: number,
|
|
45
|
+
releaseCurve: Float64Array | undefined,
|
|
46
|
+
sampleRate: number,
|
|
47
|
+
hold?: Uint8Array,
|
|
48
|
+
): Float64Array {
|
|
49
|
+
if (!releaseCurve) return follow(x, attack, release, sampleRate, hold);
|
|
50
|
+
const a = followCoef(attack, sampleRate);
|
|
51
|
+
const out = new Float64Array(x.length);
|
|
52
|
+
let e = 0;
|
|
53
|
+
let lastRelease = Number.NaN;
|
|
54
|
+
let r = 0;
|
|
55
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
56
|
+
if (releaseCurve[i] !== lastRelease) {
|
|
57
|
+
lastRelease = releaseCurve[i]!;
|
|
58
|
+
r = followCoef(lastRelease, sampleRate);
|
|
59
|
+
}
|
|
60
|
+
if (!hold || hold[i] === 0) {
|
|
61
|
+
const v = Math.abs(x[i]!);
|
|
62
|
+
const c = v > e ? a : r;
|
|
63
|
+
e = c * e + (1 - c) * v;
|
|
64
|
+
}
|
|
65
|
+
out[i] = e;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Runs a biquad whose coefficients may change at 32-sample blocks. */
|
|
71
|
+
function runBand(
|
|
72
|
+
x: Float64Array,
|
|
73
|
+
filter: Biquad,
|
|
74
|
+
filterAt: ((i: number) => Biquad) | undefined,
|
|
75
|
+
): void {
|
|
76
|
+
if (!filterAt) {
|
|
77
|
+
runBiquad(filter, x);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let z1 = 0;
|
|
81
|
+
let z2 = 0;
|
|
82
|
+
let f = filterAt(0);
|
|
83
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
84
|
+
if (i % VOCODER_BLOCK === 0) f = filterAt(i);
|
|
85
|
+
const v = x[i]!;
|
|
86
|
+
const y = f.b0 * v + z1;
|
|
87
|
+
z1 = f.b1 * v - f.a1 * y + z2;
|
|
88
|
+
z2 = f.b2 * v - f.a2 * y;
|
|
89
|
+
x[i] = y;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The band filter run twice in cascade, in one pass when the coefficients
|
|
95
|
+
* are fixed (the same arithmetic per sample as two passes).
|
|
96
|
+
*/
|
|
97
|
+
function runBandTwice(
|
|
98
|
+
x: Float64Array,
|
|
99
|
+
filter: Biquad,
|
|
100
|
+
filterAt: ((i: number) => Biquad) | undefined,
|
|
101
|
+
): void {
|
|
102
|
+
if (filterAt) {
|
|
103
|
+
runBand(x, filter, filterAt);
|
|
104
|
+
runBand(x, filter, filterAt);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const { b0, b1, b2, a1, a2 } = filter;
|
|
108
|
+
let p1 = 0;
|
|
109
|
+
let p2 = 0;
|
|
110
|
+
let q1 = 0;
|
|
111
|
+
let q2 = 0;
|
|
112
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
113
|
+
const v = x[i]!;
|
|
114
|
+
const y = b0 * v + p1;
|
|
115
|
+
p1 = b1 * v - a1 * y + p2;
|
|
116
|
+
p2 = b2 * v - a2 * y;
|
|
117
|
+
const w = b0 * y + q1;
|
|
118
|
+
q1 = b1 * y - a1 * w + q2;
|
|
119
|
+
q2 = b2 * y - a2 * w;
|
|
120
|
+
x[i] = w;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `e[i] *= g[i]` (its own function so the loop is optimised). */
|
|
125
|
+
function multiplyInto(e: Float64Array, g: Float64Array): void {
|
|
126
|
+
for (let i = 0; i < e.length; i += 1) e[i] = e[i]! * g[i]!;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Cross-fades the carrier toward level-matched noise where `u` is unvoiced. */
|
|
130
|
+
function blendUnvoiced(
|
|
131
|
+
carrier: Float64Array,
|
|
132
|
+
u: Float64Array,
|
|
133
|
+
control: VocoderControl,
|
|
134
|
+
seed: number,
|
|
135
|
+
origin: number,
|
|
136
|
+
sr: number,
|
|
137
|
+
): void {
|
|
138
|
+
const level = follow(carrier, 0.01, 0.1, sr);
|
|
139
|
+
const curve = control.curves.unvoiced;
|
|
140
|
+
const fixed = control.settings.unvoiced;
|
|
141
|
+
for (let i = 0; i < carrier.length; i += 1) {
|
|
142
|
+
const w = u[i]! * (curve ? curve[i]! : fixed);
|
|
143
|
+
if (w <= 0) continue;
|
|
144
|
+
const noise = noiseSample(seed, origin + i);
|
|
145
|
+
carrier[i] =
|
|
146
|
+
carrier[i]! * (1 - w) + noise * 1.7 * Math.max(level[i]!, 0.05) * w;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One synthesis band into `out` with a fixed formant shift and depth: the
|
|
152
|
+
* envelope interpolated between analysis bands `ea` and `eb` by `fr`, shaped
|
|
153
|
+
* by `depth`, over the carrier band `syn` (whitened by `ec` when enhancing).
|
|
154
|
+
* A small function of its own so the JIT optimises the loop: inlined in
|
|
155
|
+
* `channelVocode`, which runs once per render, it stayed in the baseline tier.
|
|
156
|
+
*/
|
|
157
|
+
function mixBandFixed(
|
|
158
|
+
out: Float64Array,
|
|
159
|
+
syn: Float64Array,
|
|
160
|
+
ea: Float64Array,
|
|
161
|
+
eb: Float64Array,
|
|
162
|
+
fr: number,
|
|
163
|
+
ec: Float64Array | undefined,
|
|
164
|
+
depth: number,
|
|
165
|
+
scale: number,
|
|
166
|
+
): void {
|
|
167
|
+
const n = out.length;
|
|
168
|
+
const refPow = REF ** (1 - depth);
|
|
169
|
+
if (ec)
|
|
170
|
+
for (let i = 0; i < n; i += 1) {
|
|
171
|
+
const e = ea[i]! * (1 - fr) + eb[i]! * fr;
|
|
172
|
+
const level = depth === 1 ? e : e ** depth * refPow;
|
|
173
|
+
const g = level / (ec[i]! + 1e-5);
|
|
174
|
+
out[i]! += syn[i]! * Math.min(g, 1e3) * scale;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
for (let i = 0; i < n; i += 1) {
|
|
178
|
+
const e = ea[i]! * (1 - fr) + eb[i]! * fr;
|
|
179
|
+
const level = depth === 1 ? e : e ** depth * refPow;
|
|
180
|
+
out[i]! += syn[i]! * level * 8 * scale;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Vocodes each carrier channel with `mod`. The modulator is read up to the
|
|
186
|
+
* largest band advance past each output sample (zero past its end).
|
|
187
|
+
* Returns the wet signal per channel, after `gain` and before the peak guard
|
|
188
|
+
* and `mix` (`applyVocoderStage` adds those).
|
|
189
|
+
*/
|
|
190
|
+
export function channelVocode(
|
|
191
|
+
mod: Float64Array,
|
|
192
|
+
cars: readonly Float64Array[],
|
|
193
|
+
control: VocoderControl,
|
|
194
|
+
): Float64Array[] {
|
|
195
|
+
const { settings: p, sampleRate: sr, origin, seed } = control;
|
|
196
|
+
const n = Math.min(mod.length, ...cars.map((c) => c.length));
|
|
197
|
+
const { bands, ratio } = bankDesign(p, sr);
|
|
198
|
+
const nb = bands.length;
|
|
199
|
+
const widthCurve = control.curves.width;
|
|
200
|
+
// Width automation re-designs the band filters per block.
|
|
201
|
+
const designs = new Map<number, Biquad[]>();
|
|
202
|
+
const filtersFor = (width: number): Biquad[] => {
|
|
203
|
+
let known = designs.get(width);
|
|
204
|
+
if (!known) {
|
|
205
|
+
const octaves = Math.log2(ratio) * width * 1.55;
|
|
206
|
+
known = bands.map((b) =>
|
|
207
|
+
bandpass(Math.min(b.hz, 0.45 * sr), octaves, sr),
|
|
208
|
+
);
|
|
209
|
+
designs.set(width, known);
|
|
210
|
+
}
|
|
211
|
+
return known;
|
|
212
|
+
};
|
|
213
|
+
const filterAt = (k: number, offset: number) =>
|
|
214
|
+
widthCurve
|
|
215
|
+
? (i: number): Biquad =>
|
|
216
|
+
filtersFor(widthCurve[Math.min(n - 1, i + offset)]!)[k]!
|
|
217
|
+
: undefined;
|
|
218
|
+
const gate = gateCurve(mod.subarray(0, n), sr, control.gateDb);
|
|
219
|
+
if (gate && control.hold) holdCurve(gate, control.hold);
|
|
220
|
+
const unvoicedOn = p.unvoiced > 0 || control.curves.unvoiced !== undefined;
|
|
221
|
+
const u = unvoicedOn
|
|
222
|
+
? unvoicedCurve(mod.subarray(0, n), sr, p.sens, origin, gate)
|
|
223
|
+
: undefined;
|
|
224
|
+
// Analysis: one envelope per band.
|
|
225
|
+
const env: Float64Array[] = [];
|
|
226
|
+
const band = new Float64Array(n);
|
|
227
|
+
for (let k = 0; k < nb; k += 1) {
|
|
228
|
+
const b = bands[k]!;
|
|
229
|
+
if (!b.live) {
|
|
230
|
+
env.push(new Float64Array(n));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
// The modulator advanced by the band's delay, zero past its end.
|
|
234
|
+
const from = Math.min(b.advance, mod.length);
|
|
235
|
+
const take = Math.min(n, mod.length - from);
|
|
236
|
+
band.set(mod.subarray(from, from + take));
|
|
237
|
+
band.fill(0, take);
|
|
238
|
+
runBandTwice(band, b.filter, filterAt(k, b.advance));
|
|
239
|
+
const e = followVarying(
|
|
240
|
+
band,
|
|
241
|
+
p.attack,
|
|
242
|
+
p.release,
|
|
243
|
+
control.curves.release,
|
|
244
|
+
sr,
|
|
245
|
+
control.hold,
|
|
246
|
+
);
|
|
247
|
+
if (gate) multiplyInto(e, gate);
|
|
248
|
+
env.push(e);
|
|
249
|
+
}
|
|
250
|
+
const shiftOf = (i: number): number =>
|
|
251
|
+
at(control, "formant", i) / 12 / Math.log2(ratio);
|
|
252
|
+
const staticShift = control.curves.formant ? undefined : shiftOf(0);
|
|
253
|
+
const synthEnv = (k: number, i: number): number => {
|
|
254
|
+
const pos = k - (staticShift ?? shiftOf(i));
|
|
255
|
+
const i0 = Math.floor(pos);
|
|
256
|
+
const fr = pos - i0;
|
|
257
|
+
const a = i0 >= 0 && i0 < nb ? env[i0]![i]! : 0;
|
|
258
|
+
const b = i0 + 1 >= 0 && i0 + 1 < nb ? env[i0 + 1]![i]! : 0;
|
|
259
|
+
return a * (1 - fr) + b * fr;
|
|
260
|
+
};
|
|
261
|
+
const scale = channelMakeup(p.bands, p.width);
|
|
262
|
+
const depthCurve = control.curves.depth;
|
|
263
|
+
const shape = (e: number, i: number): number => {
|
|
264
|
+
const depth = depthCurve ? depthCurve[i]! : p.depth;
|
|
265
|
+
return depth === 1 ? e : e ** depth * REF ** (1 - depth);
|
|
266
|
+
};
|
|
267
|
+
const hiss =
|
|
268
|
+
p.hiss > 0 || control.curves.hiss ? hissPath(mod, n, sr) : undefined;
|
|
269
|
+
const zeros = new Float64Array(n);
|
|
270
|
+
const outs: Float64Array[] = [];
|
|
271
|
+
for (const car of cars) {
|
|
272
|
+
const carrier = car.slice(0, n);
|
|
273
|
+
if (u) blendUnvoiced(carrier, u, control, seed, origin, sr);
|
|
274
|
+
const out = new Float64Array(n);
|
|
275
|
+
const syn = new Float64Array(n);
|
|
276
|
+
for (let k = 0; k < nb; k += 1) {
|
|
277
|
+
const b = bands[k]!;
|
|
278
|
+
if (!b.live) continue;
|
|
279
|
+
syn.set(carrier);
|
|
280
|
+
runBandTwice(syn, b.filter, filterAt(k, 0));
|
|
281
|
+
// The envelope this band reads: a fixed pair of analysis bands and
|
|
282
|
+
// weight unless the formant is automated (same arithmetic as
|
|
283
|
+
// `synthEnv`, without a call per sample).
|
|
284
|
+
let ea: Float64Array = zeros;
|
|
285
|
+
let eb: Float64Array = zeros;
|
|
286
|
+
let fr = 0;
|
|
287
|
+
if (staticShift !== undefined) {
|
|
288
|
+
const pos = k - staticShift;
|
|
289
|
+
const i0 = Math.floor(pos);
|
|
290
|
+
fr = pos - i0;
|
|
291
|
+
if (i0 >= 0 && i0 < nb) ea = env[i0]!;
|
|
292
|
+
if (i0 + 1 >= 0 && i0 + 1 < nb) eb = env[i0 + 1]!;
|
|
293
|
+
}
|
|
294
|
+
const ec = p.enhance
|
|
295
|
+
? followVarying(syn, p.attack, p.release, control.curves.release, sr)
|
|
296
|
+
: undefined;
|
|
297
|
+
if (staticShift !== undefined && depthCurve === undefined)
|
|
298
|
+
mixBandFixed(out, syn, ea, eb, fr, ec, p.depth, scale);
|
|
299
|
+
else
|
|
300
|
+
for (let i = 0; i < n; i += 1) {
|
|
301
|
+
const level = shape(synthEnv(k, i), i);
|
|
302
|
+
if (ec) {
|
|
303
|
+
const g = level / (ec[i]! + 1e-5);
|
|
304
|
+
out[i]! += syn[i]! * Math.min(g, 1e3) * scale;
|
|
305
|
+
} else out[i]! += syn[i]! * level * 8 * scale;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (hiss)
|
|
309
|
+
for (let i = 0; i < n; i += 1)
|
|
310
|
+
out[i]! += at(control, "hiss", i) * hiss[i]! * (gate ? gate[i]! : 1);
|
|
311
|
+
outs.push(out);
|
|
312
|
+
}
|
|
313
|
+
return outs;
|
|
314
|
+
}
|