@hraness/dawg 0.4.1 → 0.6.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 +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
package/src/audio/effects/bus.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { Track } from "../../../core/score.ts";
|
|
|
17
17
|
import {
|
|
18
18
|
CONTROL_SAMPLES,
|
|
19
19
|
interpolateAutomation,
|
|
20
|
+
tickAtSample,
|
|
20
21
|
type EffectContext,
|
|
21
22
|
} from "./common.ts";
|
|
22
23
|
import { applyDelay, applyReverb } from "./space.ts";
|
|
@@ -58,13 +59,13 @@ function send(
|
|
|
58
59
|
stemR: Float64Array,
|
|
59
60
|
base: number,
|
|
60
61
|
lane: readonly Readonly<{ tick: number; value: number }>[],
|
|
61
|
-
|
|
62
|
+
context: Pick<EffectContext, "samplesPerTick" | "warp">,
|
|
62
63
|
): void {
|
|
63
64
|
if (base <= 0 && lane.length === 0) return;
|
|
64
65
|
let level = base;
|
|
65
66
|
for (let index = 0; index < outL.length; index += 1) {
|
|
66
67
|
if (lane.length > 0 && index % CONTROL_SAMPLES === 0)
|
|
67
|
-
level = interpolateAutomation(lane, index
|
|
68
|
+
level = interpolateAutomation(lane, tickAtSample(context, index), base);
|
|
68
69
|
outL[index]! += stemL[index]! * level;
|
|
69
70
|
outR[index]! += stemR[index]! * level;
|
|
70
71
|
}
|
|
@@ -89,7 +90,7 @@ export function sendToBus(
|
|
|
89
90
|
stemR,
|
|
90
91
|
track.delay.mix,
|
|
91
92
|
track.delayMixAutomation ?? [],
|
|
92
|
-
context
|
|
93
|
+
context,
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
if (track.reverb) {
|
|
@@ -101,7 +102,7 @@ export function sendToBus(
|
|
|
101
102
|
stemR,
|
|
102
103
|
track.reverb.mix,
|
|
103
104
|
track.fxAutomation?.["reverb-mix"] ?? [],
|
|
104
|
-
context
|
|
105
|
+
context,
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The fixed per-track effects chain (`FX_CHAIN` in core/fx.ts):
|
|
3
3
|
*
|
|
4
|
-
* filter → djf → autofilter → vowel → crush → distort →
|
|
4
|
+
* filter → djf → autofilter → vowel → crush → distort → stomp → head →
|
|
5
|
+
* cab → tremolo →
|
|
5
6
|
* compressor → pan → phaser → chorus → leslie → postgain → delay → reverb
|
|
6
7
|
*
|
|
7
8
|
* Stages before pan run on the mono voice sum; pan spreads it to stereo;
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
applyVowel,
|
|
20
21
|
} from "./filter.ts";
|
|
21
22
|
import { applyChorus, applyLeslie, applyPhaser } from "./modulation.ts";
|
|
23
|
+
import { applyCab, applyHead, applyStomp } from "./rig/index.ts";
|
|
22
24
|
import { applyDelay, applyReverb } from "./space.ts";
|
|
23
25
|
|
|
24
26
|
export {
|
|
@@ -52,6 +54,9 @@ const MONO: Readonly<Partial<Record<FxName, MonoStage>>> = Object.freeze({
|
|
|
52
54
|
vowel: applyVowel,
|
|
53
55
|
crush: applyCrush,
|
|
54
56
|
distort: applyDistort,
|
|
57
|
+
stomp: applyStomp,
|
|
58
|
+
head: applyHead,
|
|
59
|
+
cab: applyCab,
|
|
55
60
|
tremolo: applyTremolo,
|
|
56
61
|
compressor: applyCompressor,
|
|
57
62
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { AutomationPoint, Track } from "../../../core/score.ts";
|
|
13
13
|
import type { FxLane, FxValues } from "../../../core/fx.ts";
|
|
14
14
|
import type { DecodedSample } from "../samples.ts";
|
|
15
|
+
import type { SampleWarp } from "../warp.ts";
|
|
15
16
|
|
|
16
17
|
/** Effect parameters are refreshed at this sample interval. */
|
|
17
18
|
export const CONTROL_SAMPLES = 32;
|
|
@@ -22,8 +23,34 @@ export type EffectContext = Readonly<{
|
|
|
22
23
|
tempoBpm: number;
|
|
23
24
|
/** Decoded `reverb.ir` samples by track id (built-in impulses need none). */
|
|
24
25
|
irs?: ReadonlyMap<string, DecodedSample>;
|
|
26
|
+
/** The song's tempo map in samples; absent: constant tempo. */
|
|
27
|
+
warp?: SampleWarp;
|
|
25
28
|
}>;
|
|
26
29
|
|
|
30
|
+
/** Score tick at sample `index`, through the tempo map when there is one. */
|
|
31
|
+
export function tickAtSample(
|
|
32
|
+
context: Pick<EffectContext, "samplesPerTick" | "warp">,
|
|
33
|
+
index: number,
|
|
34
|
+
): number {
|
|
35
|
+
return context.warp
|
|
36
|
+
? context.warp.tick(index)
|
|
37
|
+
: index / context.samplesPerTick;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tempo at sample `index` for tempo-synced LFOs and delays: through the
|
|
42
|
+
* tempo map when there is one (ramps, steps and fermata holds included),
|
|
43
|
+
* else the song's constant tempo.
|
|
44
|
+
*/
|
|
45
|
+
export function tempoAtSample(
|
|
46
|
+
context: Pick<EffectContext, "tempoBpm" | "warp">,
|
|
47
|
+
index: number,
|
|
48
|
+
): number {
|
|
49
|
+
return context.warp
|
|
50
|
+
? context.warp.bpm(context.warp.tick(index))
|
|
51
|
+
: context.tempoBpm;
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
/** Resolve a piecewise-linear automation lane, holding the static value before its first point. */
|
|
28
55
|
export function interpolateAutomation(
|
|
29
56
|
points: readonly AutomationPoint[],
|
|
@@ -58,6 +85,7 @@ export class Param {
|
|
|
58
85
|
readonly fallback: number,
|
|
59
86
|
points: readonly AutomationPoint[] | undefined,
|
|
60
87
|
private readonly samplesPerTick: number,
|
|
88
|
+
private readonly warp?: SampleWarp,
|
|
61
89
|
) {
|
|
62
90
|
this.points = points ?? [];
|
|
63
91
|
this.automated = this.points.length > 0;
|
|
@@ -67,7 +95,7 @@ export class Param {
|
|
|
67
95
|
return this.automated
|
|
68
96
|
? interpolateAutomation(
|
|
69
97
|
this.points,
|
|
70
|
-
index / this.samplesPerTick,
|
|
98
|
+
this.warp ? this.warp.tick(index) : index / this.samplesPerTick,
|
|
71
99
|
this.fallback,
|
|
72
100
|
)
|
|
73
101
|
: this.fallback;
|
|
@@ -93,6 +121,7 @@ export function fxReader(
|
|
|
93
121
|
values[name] as number,
|
|
94
122
|
track.fxAutomation?.[`${effect}-${name}` as FxLane],
|
|
95
123
|
context.samplesPerTick,
|
|
124
|
+
context.warp,
|
|
96
125
|
);
|
|
97
126
|
},
|
|
98
127
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
fxReader,
|
|
11
11
|
lfo,
|
|
12
12
|
lfoHz,
|
|
13
|
+
tempoAtSample,
|
|
13
14
|
type EffectContext,
|
|
14
15
|
} from "./common.ts";
|
|
15
16
|
|
|
@@ -42,7 +43,7 @@ export function applyTremolo(
|
|
|
42
43
|
for (let index = 0; index < buffer.length; index += 1) {
|
|
43
44
|
if (index % CONTROL_SAMPLES === 0) {
|
|
44
45
|
phasor.setHz(
|
|
45
|
-
lfoHz(sync, rate.at(index), context
|
|
46
|
+
lfoHz(sync, rate.at(index), tempoAtSample(context, index)),
|
|
46
47
|
context.sampleRate,
|
|
47
48
|
);
|
|
48
49
|
if (depth.automated) amount = depth.at(index);
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
interpolateAutomation,
|
|
16
16
|
lfo,
|
|
17
17
|
lfoHz,
|
|
18
|
+
tempoAtSample,
|
|
18
19
|
resonanceQ,
|
|
20
|
+
tickAtSample,
|
|
19
21
|
type EffectContext,
|
|
20
22
|
type FilterType,
|
|
21
23
|
} from "./common.ts";
|
|
@@ -99,7 +101,7 @@ export function applyTrackFilter(
|
|
|
99
101
|
const resonanceLane = track.resonanceAutomation ?? [];
|
|
100
102
|
if (!track.filter && automation.length === 0 && resonanceLane.length === 0)
|
|
101
103
|
return;
|
|
102
|
-
const { sampleRate
|
|
104
|
+
const { sampleRate } = context;
|
|
103
105
|
const staticCutoff = track.filter?.cutoff ?? SCORE_LIMITS.maxFilterCutoff;
|
|
104
106
|
const staticResonance = track.filter?.resonance ?? 0;
|
|
105
107
|
const automated = automation.length > 0 || resonanceLane.length > 0;
|
|
@@ -111,7 +113,7 @@ export function applyTrackFilter(
|
|
|
111
113
|
filter.update(staticCutoff, staticResonance);
|
|
112
114
|
for (let index = 0; index < buffer.length; index += 1) {
|
|
113
115
|
if (automated && index % CONTROL_SAMPLES === 0) {
|
|
114
|
-
const tick = index
|
|
116
|
+
const tick = tickAtSample(context, index);
|
|
115
117
|
filter.update(
|
|
116
118
|
interpolateAutomation(automation, tick, staticCutoff),
|
|
117
119
|
interpolateAutomation(resonanceLane, tick, staticResonance),
|
|
@@ -205,7 +207,7 @@ export function applyAutoFilter(
|
|
|
205
207
|
: release * envelope + (1 - release) * level;
|
|
206
208
|
if (index % CONTROL_SAMPLES === 0) {
|
|
207
209
|
phasor.setHz(
|
|
208
|
-
lfoHz(sync, rate.at(index), context
|
|
210
|
+
lfoHz(sync, rate.at(index), tempoAtSample(context, index)),
|
|
209
211
|
context.sampleRate,
|
|
210
212
|
);
|
|
211
213
|
followOctaves = follow.at(index);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
clamp,
|
|
10
10
|
fxReader,
|
|
11
11
|
lfoHz,
|
|
12
|
+
tempoAtSample,
|
|
12
13
|
readFractional,
|
|
13
14
|
type EffectContext,
|
|
14
15
|
} from "./common.ts";
|
|
@@ -57,7 +58,10 @@ export function applyPhaser(
|
|
|
57
58
|
};
|
|
58
59
|
for (let index = 0; index < left.length; index += 1) {
|
|
59
60
|
if (index % CONTROL_SAMPLES === 0) {
|
|
60
|
-
phasor.setHz(
|
|
61
|
+
phasor.setHz(
|
|
62
|
+
lfoHz(sync, rate.at(index), tempoAtSample(context, index)),
|
|
63
|
+
sampleRate,
|
|
64
|
+
);
|
|
61
65
|
if (depth.automated) amount = depth.at(index);
|
|
62
66
|
const frequency = clamp(
|
|
63
67
|
center + (sweep / 2) * Math.sin(2 * Math.PI * phasor.phase),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speaker cabinets (`fx.cab`): a parametric speaker, box and microphone
|
|
3
|
+
* model from cheap biquads (no impulse response, no setup cost). Ported
|
|
4
|
+
* from the 0.6 guitar prototype; `8x10`, `1x15` and `di` are dawg
|
|
5
|
+
* additions in the same model.
|
|
6
|
+
*/
|
|
7
|
+
import { Rbj, clamp } from "./filters.ts";
|
|
8
|
+
|
|
9
|
+
export const CAB_TYPES = [
|
|
10
|
+
"1x12",
|
|
11
|
+
"2x12",
|
|
12
|
+
"4x12",
|
|
13
|
+
"1x10",
|
|
14
|
+
"open",
|
|
15
|
+
"8x10",
|
|
16
|
+
"1x15",
|
|
17
|
+
"di",
|
|
18
|
+
] as const;
|
|
19
|
+
export type CabType = (typeof CAB_TYPES)[number];
|
|
20
|
+
|
|
21
|
+
type CabModel = Readonly<{
|
|
22
|
+
hp: number;
|
|
23
|
+
/** Low resonance: Hz, Q, dB. */
|
|
24
|
+
low: readonly [number, number, number];
|
|
25
|
+
peaks: readonly (readonly [number, number, number])[];
|
|
26
|
+
/** Speaker roll-off (two biquads, Butterworth 4th order); 0 = none. */
|
|
27
|
+
lp: number;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
const CABS: Record<CabType, CabModel> = {
|
|
31
|
+
"1x12": {
|
|
32
|
+
hp: 75,
|
|
33
|
+
low: [115, 1.4, 3],
|
|
34
|
+
peaks: [
|
|
35
|
+
[2100, 2, 3],
|
|
36
|
+
[3600, 3, 2],
|
|
37
|
+
],
|
|
38
|
+
lp: 5200,
|
|
39
|
+
},
|
|
40
|
+
"2x12": {
|
|
41
|
+
hp: 70,
|
|
42
|
+
low: [105, 1.3, 3.5],
|
|
43
|
+
peaks: [
|
|
44
|
+
[1200, 1.5, -2],
|
|
45
|
+
[2800, 2.2, 4.5],
|
|
46
|
+
],
|
|
47
|
+
lp: 6200,
|
|
48
|
+
},
|
|
49
|
+
"4x12": {
|
|
50
|
+
hp: 65,
|
|
51
|
+
low: [95, 1.2, 5],
|
|
52
|
+
peaks: [
|
|
53
|
+
[700, 1, -2.5],
|
|
54
|
+
[2400, 2, 4],
|
|
55
|
+
[3900, 3, 2],
|
|
56
|
+
],
|
|
57
|
+
lp: 4700,
|
|
58
|
+
},
|
|
59
|
+
"1x10": { hp: 120, low: [160, 1.5, 2], peaks: [[1500, 1.5, 3]], lp: 4300 },
|
|
60
|
+
open: { hp: 90, low: [120, 0.9, 1], peaks: [[2500, 1.2, 2]], lp: 7000 },
|
|
61
|
+
// Bass cabinets: deep, rolled off early (10s brighter than a 15).
|
|
62
|
+
"8x10": {
|
|
63
|
+
hp: 45,
|
|
64
|
+
low: [80, 1.0, 3],
|
|
65
|
+
peaks: [
|
|
66
|
+
[800, 1.2, -2],
|
|
67
|
+
[2500, 2, 3],
|
|
68
|
+
],
|
|
69
|
+
lp: 4500,
|
|
70
|
+
},
|
|
71
|
+
"1x15": {
|
|
72
|
+
hp: 40,
|
|
73
|
+
low: [70, 1.2, 4],
|
|
74
|
+
peaks: [
|
|
75
|
+
[1000, 1, -1.5],
|
|
76
|
+
[2000, 1.5, 2],
|
|
77
|
+
],
|
|
78
|
+
lp: 3500,
|
|
79
|
+
},
|
|
80
|
+
// Direct in: no speaker, only a subsonic high-pass.
|
|
81
|
+
di: { hp: 20, low: [100, 0.7, 0], peaks: [], lp: 0 },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The cab's filter chain at `sr` for mic position `mic` (0..1). */
|
|
85
|
+
export function cabFilters(type: string, mic: number, sr: number): Rbj[] {
|
|
86
|
+
const m = CABS[type as CabType] ?? CABS["2x12"];
|
|
87
|
+
if (m.lp === 0) return [new Rbj("hpf", m.hp, 0.707, sr)];
|
|
88
|
+
const edge = clamp(mic, 0, 1);
|
|
89
|
+
return [
|
|
90
|
+
new Rbj("hpf", m.hp, 0.707, sr),
|
|
91
|
+
new Rbj("peak", m.low[0], m.low[1], sr, m.low[2]),
|
|
92
|
+
...m.peaks.map(
|
|
93
|
+
([f, q, db]) => new Rbj("peak", f, q, sr, db * (1 - 0.6 * edge)),
|
|
94
|
+
),
|
|
95
|
+
new Rbj("highshelf", 3000, 0.7, sr, 2 - 8 * edge),
|
|
96
|
+
new Rbj("lpf", m.lp * (1 - 0.3 * edge), 0.54, sr),
|
|
97
|
+
new Rbj("lpf", m.lp * (1 - 0.3 * edge), 1.31, sr),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters for the guitar rig: the full RBJ cookbook biquad (peaks and
|
|
3
|
+
* shelves, which the effects chain's `Biquad` does not offer) and a
|
|
4
|
+
* one-pole high-pass. Ported from the 0.6 guitar prototype; a new class so
|
|
5
|
+
* the shared `Biquad` is never retuned.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function clamp(x: number, lo: number, hi: number): number {
|
|
9
|
+
return x < lo ? lo : x > hi ? hi : x;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dbToGain(db: number): number {
|
|
13
|
+
return 10 ** (db / 20);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RbjType = "lpf" | "hpf" | "peak" | "lowshelf" | "highshelf";
|
|
17
|
+
|
|
18
|
+
/** RBJ Audio EQ Cookbook biquad, transposed direct form II. */
|
|
19
|
+
export class Rbj {
|
|
20
|
+
b0 = 1;
|
|
21
|
+
b1 = 0;
|
|
22
|
+
b2 = 0;
|
|
23
|
+
a1 = 0;
|
|
24
|
+
a2 = 0;
|
|
25
|
+
z1 = 0;
|
|
26
|
+
z2 = 0;
|
|
27
|
+
|
|
28
|
+
constructor(type: RbjType, hz: number, q: number, sr: number, db = 0) {
|
|
29
|
+
this.set(type, hz, q, sr, db);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(type: RbjType, hz: number, q: number, sr: number, db = 0): void {
|
|
33
|
+
const w = (2 * Math.PI * clamp(hz, 10, sr * 0.49)) / sr;
|
|
34
|
+
const cw = Math.cos(w);
|
|
35
|
+
const alpha = Math.sin(w) / (2 * q);
|
|
36
|
+
const A = 10 ** (db / 40);
|
|
37
|
+
let b0: number, b1: number, b2: number, a0: number, a1: number, a2: number;
|
|
38
|
+
switch (type) {
|
|
39
|
+
case "lpf":
|
|
40
|
+
b0 = (1 - cw) / 2;
|
|
41
|
+
b1 = 1 - cw;
|
|
42
|
+
b2 = b0;
|
|
43
|
+
a0 = 1 + alpha;
|
|
44
|
+
a1 = -2 * cw;
|
|
45
|
+
a2 = 1 - alpha;
|
|
46
|
+
break;
|
|
47
|
+
case "hpf":
|
|
48
|
+
b0 = (1 + cw) / 2;
|
|
49
|
+
b1 = -(1 + cw);
|
|
50
|
+
b2 = b0;
|
|
51
|
+
a0 = 1 + alpha;
|
|
52
|
+
a1 = -2 * cw;
|
|
53
|
+
a2 = 1 - alpha;
|
|
54
|
+
break;
|
|
55
|
+
case "peak":
|
|
56
|
+
b0 = 1 + alpha * A;
|
|
57
|
+
b1 = -2 * cw;
|
|
58
|
+
b2 = 1 - alpha * A;
|
|
59
|
+
a0 = 1 + alpha / A;
|
|
60
|
+
a1 = -2 * cw;
|
|
61
|
+
a2 = 1 - alpha / A;
|
|
62
|
+
break;
|
|
63
|
+
case "lowshelf": {
|
|
64
|
+
const s = 2 * Math.sqrt(A) * alpha;
|
|
65
|
+
b0 = A * (A + 1 - (A - 1) * cw + s);
|
|
66
|
+
b1 = 2 * A * (A - 1 - (A + 1) * cw);
|
|
67
|
+
b2 = A * (A + 1 - (A - 1) * cw - s);
|
|
68
|
+
a0 = A + 1 + (A - 1) * cw + s;
|
|
69
|
+
a1 = -2 * (A - 1 + (A + 1) * cw);
|
|
70
|
+
a2 = A + 1 + (A - 1) * cw - s;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "highshelf": {
|
|
74
|
+
const s = 2 * Math.sqrt(A) * alpha;
|
|
75
|
+
b0 = A * (A + 1 + (A - 1) * cw + s);
|
|
76
|
+
b1 = -2 * A * (A - 1 + (A + 1) * cw);
|
|
77
|
+
b2 = A * (A + 1 + (A - 1) * cw - s);
|
|
78
|
+
a0 = A + 1 - (A - 1) * cw + s;
|
|
79
|
+
a1 = 2 * (A - 1 - (A + 1) * cw);
|
|
80
|
+
a2 = A + 1 - (A - 1) * cw - s;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.b0 = b0 / a0;
|
|
85
|
+
this.b1 = b1 / a0;
|
|
86
|
+
this.b2 = b2 / a0;
|
|
87
|
+
this.a1 = a1 / a0;
|
|
88
|
+
this.a2 = a2 / a0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process(x: number): number {
|
|
92
|
+
const y = this.b0 * x + this.z1;
|
|
93
|
+
this.z1 = this.b1 * x - this.a1 * y + this.z2;
|
|
94
|
+
this.z2 = this.b2 * x - this.a2 * y;
|
|
95
|
+
return y;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One-pole low-pass y = x + p (y - x). */
|
|
100
|
+
export class Lp1 {
|
|
101
|
+
p = 0;
|
|
102
|
+
y = 0;
|
|
103
|
+
|
|
104
|
+
constructor(hz: number, sr: number) {
|
|
105
|
+
this.set(hz, sr);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
set(hz: number, sr: number): void {
|
|
109
|
+
this.p = Math.exp((-2 * Math.PI * clamp(hz, 1, sr * 0.49)) / sr);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process(x: number): number {
|
|
113
|
+
this.y = x + this.p * (this.y - x);
|
|
114
|
+
return this.y;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One-pole high-pass (leaky differentiator). */
|
|
119
|
+
export class Hp1 {
|
|
120
|
+
p = 0;
|
|
121
|
+
x1 = 0;
|
|
122
|
+
y1 = 0;
|
|
123
|
+
|
|
124
|
+
constructor(hz: number, sr: number) {
|
|
125
|
+
this.p = Math.exp((-2 * Math.PI * clamp(hz, 1, sr * 0.49)) / sr);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process(x: number): number {
|
|
129
|
+
const y = ((1 + this.p) / 2) * (x - this.x1) + this.p * this.y1;
|
|
130
|
+
this.x1 = x;
|
|
131
|
+
this.y1 = y;
|
|
132
|
+
return y;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** DC blocker y = x - x1 + R y1. */
|
|
137
|
+
export class Dc {
|
|
138
|
+
r: number;
|
|
139
|
+
x1 = 0;
|
|
140
|
+
y1 = 0;
|
|
141
|
+
|
|
142
|
+
constructor(sr: number, hz: number) {
|
|
143
|
+
this.r = Math.exp((-2 * Math.PI * hz) / sr);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process(x: number): number {
|
|
147
|
+
const y = x - this.x1 + this.r * this.y1;
|
|
148
|
+
this.x1 = x;
|
|
149
|
+
this.y1 = y;
|
|
150
|
+
return y;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Noise gate before the amp (`fx.head.gate`): a peak follower with
|
|
3
|
+
* hysteresis (opens at the threshold, closes 6 dB below it), a 50 ms hold,
|
|
4
|
+
* 1 ms attack and 80 ms release, so a high-gain head does not amplify the
|
|
5
|
+
* hiss and string noise between notes.
|
|
6
|
+
*/
|
|
7
|
+
export const GATE_HYSTERESIS_DB = 6;
|
|
8
|
+
|
|
9
|
+
export function applyGate(
|
|
10
|
+
buffer: Float64Array,
|
|
11
|
+
thresholdDb: number,
|
|
12
|
+
sampleRate: number,
|
|
13
|
+
): void {
|
|
14
|
+
const open = 10 ** (thresholdDb / 20);
|
|
15
|
+
const close = 10 ** ((thresholdDb - GATE_HYSTERESIS_DB) / 20);
|
|
16
|
+
const peakRelease = Math.exp(-1 / (0.01 * sampleRate));
|
|
17
|
+
const attack = 1 - Math.exp(-1 / (0.001 * sampleRate));
|
|
18
|
+
const release = 1 - Math.exp(-1 / (0.08 * sampleRate));
|
|
19
|
+
const holdSamples = Math.round(0.05 * sampleRate);
|
|
20
|
+
let peak = 0;
|
|
21
|
+
let gain = 0;
|
|
22
|
+
let isOpen = false;
|
|
23
|
+
let hold = 0;
|
|
24
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
25
|
+
const x = buffer[i]!;
|
|
26
|
+
const level = Math.abs(x);
|
|
27
|
+
peak = level > peak ? level : peak * peakRelease;
|
|
28
|
+
if (peak >= open) {
|
|
29
|
+
isOpen = true;
|
|
30
|
+
hold = holdSamples;
|
|
31
|
+
} else if (peak < close) {
|
|
32
|
+
if (hold > 0) hold -= 1;
|
|
33
|
+
else isOpen = false;
|
|
34
|
+
}
|
|
35
|
+
const target = isOpen ? 1 : 0;
|
|
36
|
+
gain += (target - gain) * (target > gain ? attack : release);
|
|
37
|
+
buffer[i] = x * gain;
|
|
38
|
+
}
|
|
39
|
+
}
|