@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/clock.ts
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
|
+
import { loopTicksOf, timeMapFor, type TimeScore } from "../../core/tempo.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Beats and seconds through a song's tempo map, one loop at a time: the
|
|
5
|
+
* transport runs in score beats, and the map says how long each loop pass
|
|
6
|
+
* takes. Absent, the clock keeps its constant-tempo arithmetic.
|
|
7
|
+
*/
|
|
8
|
+
export type TransportMap = Readonly<{
|
|
9
|
+
loopBeats: number;
|
|
10
|
+
loopSeconds: number;
|
|
11
|
+
/** Seconds into a loop pass at `beat` (0..loopBeats). */
|
|
12
|
+
seconds(beat: number): number;
|
|
13
|
+
/** Beat inside a loop pass at `seconds` (0..loopSeconds). */
|
|
14
|
+
beat(seconds: number): number;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** The transport map for a score, or undefined at a constant tempo. */
|
|
18
|
+
export function transportMapFor(score: TimeScore): TransportMap | undefined {
|
|
19
|
+
const map = timeMapFor(score);
|
|
20
|
+
if (!map) return undefined;
|
|
21
|
+
const tpb = score.ticksPerBeat;
|
|
22
|
+
const loopBeats = loopTicksOf(score) / tpb;
|
|
23
|
+
const loopSeconds = map.seconds(loopTicksOf(score));
|
|
24
|
+
if (!(loopBeats > 0) || !(loopSeconds > 0)) return undefined;
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
loopBeats,
|
|
27
|
+
loopSeconds,
|
|
28
|
+
seconds: (beat: number) => map.seconds(beat * tpb),
|
|
29
|
+
beat: (seconds: number) => map.tick(seconds) / tpb,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
export class TransportClock {
|
|
2
34
|
private startedAtMs: number | undefined;
|
|
3
35
|
private pausedBeat = 0;
|
|
4
36
|
private bpm: number;
|
|
37
|
+
private map: TransportMap | undefined;
|
|
5
38
|
|
|
6
39
|
constructor(bpm = 120) {
|
|
7
40
|
this.bpm = bpm;
|
|
@@ -24,6 +57,7 @@ export class TransportClock {
|
|
|
24
57
|
|
|
25
58
|
beatAt(nowMs = performance.now()): number {
|
|
26
59
|
if (this.startedAtMs === undefined) return this.pausedBeat;
|
|
60
|
+
if (this.map) return Math.max(0, this.msToBeat(nowMs - this.startedAtMs));
|
|
27
61
|
return Math.max(0, (nowMs - this.startedAtMs) / this.beatToMs(1));
|
|
28
62
|
}
|
|
29
63
|
|
|
@@ -40,6 +74,25 @@ export class TransportClock {
|
|
|
40
74
|
else this.pausedBeat = beat;
|
|
41
75
|
}
|
|
42
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Follow a song's tempo map (`transportMapFor`), keeping the current
|
|
79
|
+
* beat; undefined returns to the constant `bpm`.
|
|
80
|
+
*/
|
|
81
|
+
setTimeMap(map: TransportMap | undefined, nowMs = performance.now()): void {
|
|
82
|
+
if (map === this.map) return;
|
|
83
|
+
const beat = this.beatAt(nowMs);
|
|
84
|
+
this.map = map;
|
|
85
|
+
if (this.startedAtMs !== undefined)
|
|
86
|
+
this.startedAtMs = nowMs - this.beatToMs(beat);
|
|
87
|
+
else this.pausedBeat = beat;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Follow a score's start tempo and tempo map. */
|
|
91
|
+
follow(score: TimeScore, nowMs = performance.now()): void {
|
|
92
|
+
this.setTempo(score.tempoBpm, nowMs);
|
|
93
|
+
this.setTimeMap(transportMapFor(score), nowMs);
|
|
94
|
+
}
|
|
95
|
+
|
|
43
96
|
/** Converge to a timestamped transport event from another TUI process. */
|
|
44
97
|
sync(
|
|
45
98
|
beat: number,
|
|
@@ -50,10 +103,10 @@ export class TransportClock {
|
|
|
50
103
|
): void {
|
|
51
104
|
if (!Number.isFinite(beat) || beat < 0) return;
|
|
52
105
|
if (!Number.isFinite(eventAtMs) || !Number.isFinite(nowWallMs)) return;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
106
|
+
const elapsedMs = playing ? Math.max(0, nowWallMs - eventAtMs) : 0;
|
|
107
|
+
const currentBeat = this.map
|
|
108
|
+
? this.msToBeat(this.beatToMs(beat) + elapsedMs)
|
|
109
|
+
: beat + elapsedMs / this.beatToMs(1);
|
|
57
110
|
if (playing) {
|
|
58
111
|
this.startedAtMs = nowMonotonicMs - this.beatToMs(currentBeat);
|
|
59
112
|
} else {
|
|
@@ -63,6 +116,19 @@ export class TransportClock {
|
|
|
63
116
|
}
|
|
64
117
|
|
|
65
118
|
private beatToMs(beats: number): number {
|
|
66
|
-
|
|
119
|
+
const map = this.map;
|
|
120
|
+
if (!map) return (beats * 60_000) / this.bpm;
|
|
121
|
+
const loops = Math.floor(beats / map.loopBeats);
|
|
122
|
+
const inner = beats - loops * map.loopBeats;
|
|
123
|
+
return (loops * map.loopSeconds + map.seconds(inner)) * 1000;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private msToBeat(ms: number): number {
|
|
127
|
+
const map = this.map;
|
|
128
|
+
if (!map) return ms / this.beatToMs(1);
|
|
129
|
+
const seconds = ms / 1000;
|
|
130
|
+
const loops = Math.floor(seconds / map.loopSeconds);
|
|
131
|
+
const inner = seconds - loops * map.loopSeconds;
|
|
132
|
+
return loops * map.loopBeats + map.beat(inner);
|
|
67
133
|
}
|
|
68
134
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared grain bank (0.6 granular, shared DSP): a source band-limited at
|
|
3
|
+
* one level per semitone of upward read speed, so grains read at any step
|
|
4
|
+
* stay alias-free with a 4-point Hermite read.
|
|
5
|
+
*
|
|
6
|
+
* Level `L` holds the source low-passed so that reading it at a step of up
|
|
7
|
+
* to 2^(L/12) puts the stopband edge at or below the output Nyquist (a
|
|
8
|
+
* Kaiser-windowed sinc, about -70 dB stopband, transition scaled with the
|
|
9
|
+
* step so the output passband reaches about 0.43 of the output rate at
|
|
10
|
+
* every level). Level 0 is the source itself (steps <= 1 never alias).
|
|
11
|
+
* Every level keeps the source's frame rate, so a voice that changes its
|
|
12
|
+
* step (bend, vibrato, glide) re-picks the level per 32-frame control
|
|
13
|
+
* block without moving its read position.
|
|
14
|
+
*
|
|
15
|
+
* This replaces the prototype's octave mipmap (`proto/granular/dsp.ts`,
|
|
16
|
+
* `buildMipmap`) after the 0.6 review: octave levels over-filter
|
|
17
|
+
* non-octave intervals (a shimmer fifth lost up to 11 semitones of top
|
|
18
|
+
* band) and full-length Float64 levels had no memory bound. Here a level
|
|
19
|
+
* is a sparse map of Float32 chunks filled on first read, and all chunks of
|
|
20
|
+
* all sources share one byte budget (64 MB) evicted least recently used;
|
|
21
|
+
* an eviction only costs a recompute. Output is deterministic: a chunk's
|
|
22
|
+
* contents depend only on the source, the level and the chunk index.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Frames per chunk (a power of two). */
|
|
26
|
+
export const BANK_CHUNK_FRAMES = 4096;
|
|
27
|
+
const CHUNK_SHIFT = 12;
|
|
28
|
+
/** Chunks carry one frame before and two after for the Hermite read. */
|
|
29
|
+
export const BANK_CHUNK_PAD = 3;
|
|
30
|
+
/** Highest level (five octaves up); faster reads use it and may alias. */
|
|
31
|
+
export const BANK_MAX_LEVEL = 60;
|
|
32
|
+
/** Byte budget shared by every source's chunks. */
|
|
33
|
+
export const BANK_MAX_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
const STOPBAND_DB = 70;
|
|
36
|
+
const KAISER_BETA = 0.1102 * (STOPBAND_DB - 8.7);
|
|
37
|
+
/** Taps at step 1 (scaled up with the step, capped). */
|
|
38
|
+
const BASE_TAPS = 96;
|
|
39
|
+
const MAX_TAPS = 511;
|
|
40
|
+
|
|
41
|
+
/** A mono source registered with the bank. */
|
|
42
|
+
export type BankSource = Readonly<{
|
|
43
|
+
/** Content id: a sample's sha256 or a built-in render key. */
|
|
44
|
+
id: string;
|
|
45
|
+
rate: number;
|
|
46
|
+
data: Float32Array;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
type Chunk = Float32Array;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Chunks per source (`id` and rate) keyed by level and index as one number.
|
|
53
|
+
* Grains look a chunk up every 32-frame block, so the lookup avoids string
|
|
54
|
+
* keys and the LRU order is a use counter (scanned only to evict).
|
|
55
|
+
*/
|
|
56
|
+
type Entry = { chunk: Chunk; used: number };
|
|
57
|
+
const chunks = new Map<string, Map<number, Entry>>();
|
|
58
|
+
const sourceTables = new WeakMap<BankSource, Map<number, Entry>>();
|
|
59
|
+
let count = 0;
|
|
60
|
+
let clock = 0;
|
|
61
|
+
let bytes = 0;
|
|
62
|
+
let byteCap = BANK_MAX_BYTES;
|
|
63
|
+
const ZERO_CHUNK = new Float32Array(BANK_CHUNK_FRAMES + BANK_CHUNK_PAD);
|
|
64
|
+
const kernels = new Map<number, Float64Array>();
|
|
65
|
+
|
|
66
|
+
/** The level for a read step (frames of source per output frame). */
|
|
67
|
+
export function bankLevel(step: number): number {
|
|
68
|
+
const r = step < 0 ? -step : step;
|
|
69
|
+
if (r <= 1 + 1e-9) return 0;
|
|
70
|
+
const level = Math.ceil(12 * Math.log2(r) - 1e-6);
|
|
71
|
+
return level > BANK_MAX_LEVEL ? BANK_MAX_LEVEL : level;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function besselI0(x: number): number {
|
|
75
|
+
let sum = 1;
|
|
76
|
+
let term = 1;
|
|
77
|
+
const q = (x * x) / 4;
|
|
78
|
+
for (let k = 1; k < 64; k += 1) {
|
|
79
|
+
term *= q / (k * k);
|
|
80
|
+
sum += term;
|
|
81
|
+
if (term < sum * 1e-17) break;
|
|
82
|
+
}
|
|
83
|
+
return sum;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The low-pass kernel of a level (odd length, unity DC gain). */
|
|
87
|
+
export function bankKernel(level: number): Float64Array {
|
|
88
|
+
let h = kernels.get(level);
|
|
89
|
+
if (h) return h;
|
|
90
|
+
const r = 2 ** (level / 12);
|
|
91
|
+
let taps = Math.min(MAX_TAPS, Math.ceil(BASE_TAPS * r));
|
|
92
|
+
if (taps % 2 === 0) taps += 1;
|
|
93
|
+
// Kaiser transition width for the tap count, centred inside the band.
|
|
94
|
+
const transition = (STOPBAND_DB - 8) / (2.285 * 2 * Math.PI * (taps - 1));
|
|
95
|
+
const fc = Math.max(0.01, 0.5 / r - transition / 2);
|
|
96
|
+
h = new Float64Array(taps);
|
|
97
|
+
const mid = (taps - 1) / 2;
|
|
98
|
+
const norm = besselI0(KAISER_BETA);
|
|
99
|
+
let sum = 0;
|
|
100
|
+
for (let i = 0; i < taps; i += 1) {
|
|
101
|
+
const m = i - mid;
|
|
102
|
+
const sinc =
|
|
103
|
+
m === 0 ? 2 * fc : Math.sin(2 * Math.PI * fc * m) / (Math.PI * m);
|
|
104
|
+
const ratio = m / mid;
|
|
105
|
+
const w = besselI0(KAISER_BETA * Math.sqrt(1 - ratio * ratio)) / norm;
|
|
106
|
+
h[i] = sinc * w;
|
|
107
|
+
sum += h[i]!;
|
|
108
|
+
}
|
|
109
|
+
for (let i = 0; i < taps; i += 1) h[i] = h[i]! / sum;
|
|
110
|
+
kernels.set(level, h);
|
|
111
|
+
return h;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fill(source: BankSource, level: number, index: number): Chunk {
|
|
115
|
+
const out = new Float32Array(BANK_CHUNK_FRAMES + BANK_CHUNK_PAD);
|
|
116
|
+
const data = source.data;
|
|
117
|
+
const n = data.length;
|
|
118
|
+
const first = index * BANK_CHUNK_FRAMES - 1;
|
|
119
|
+
if (level === 0) {
|
|
120
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
121
|
+
const k = first + i;
|
|
122
|
+
out[i] = k >= 0 && k < n ? data[k]! : 0;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
const h = bankKernel(level);
|
|
127
|
+
const taps = h.length;
|
|
128
|
+
const mid = (taps - 1) >> 1;
|
|
129
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
130
|
+
const centre = first + i;
|
|
131
|
+
const lo = centre - mid;
|
|
132
|
+
let from = 0;
|
|
133
|
+
let to = taps;
|
|
134
|
+
if (lo < 0) from = -lo;
|
|
135
|
+
if (lo + to > n) to = n - lo;
|
|
136
|
+
let acc = 0;
|
|
137
|
+
for (let j = from; j < to; j += 1) acc += h[j]! * data[lo + j]!;
|
|
138
|
+
out[i] = acc;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The chunk holding frames `index * 4096 - 1 .. (index + 1) * 4096 + 2` of
|
|
145
|
+
* `level`; a frame `i` of the level is `chunk[(i & 4095) + 1]`. Chunks
|
|
146
|
+
* wholly outside the source are a shared zero chunk.
|
|
147
|
+
*/
|
|
148
|
+
export function bankChunk(
|
|
149
|
+
source: BankSource,
|
|
150
|
+
level: number,
|
|
151
|
+
index: number,
|
|
152
|
+
): Chunk {
|
|
153
|
+
if (index < 0 || index * BANK_CHUNK_FRAMES > source.data.length + 2)
|
|
154
|
+
return ZERO_CHUNK;
|
|
155
|
+
let table = sourceTables.get(source);
|
|
156
|
+
if (!table) {
|
|
157
|
+
const id = `${source.id}\u0000${source.rate}`;
|
|
158
|
+
table = chunks.get(id);
|
|
159
|
+
if (!table) chunks.set(id, (table = new Map()));
|
|
160
|
+
sourceTables.set(source, table);
|
|
161
|
+
}
|
|
162
|
+
const key = level * 2 ** 32 + index;
|
|
163
|
+
clock += 1;
|
|
164
|
+
const hit = table.get(key);
|
|
165
|
+
if (hit) {
|
|
166
|
+
hit.used = clock;
|
|
167
|
+
return hit.chunk;
|
|
168
|
+
}
|
|
169
|
+
const chunk = fill(source, level, index);
|
|
170
|
+
table.set(key, { chunk, used: clock });
|
|
171
|
+
count += 1;
|
|
172
|
+
bytes += chunk.byteLength;
|
|
173
|
+
if (bytes > byteCap) evict();
|
|
174
|
+
return chunk;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Drops least recently used chunks until under the budget (keeps one). */
|
|
178
|
+
function evict(): void {
|
|
179
|
+
const all: { table: Map<number, Entry>; key: number; used: number }[] = [];
|
|
180
|
+
for (const table of chunks.values())
|
|
181
|
+
for (const [key, entry] of table)
|
|
182
|
+
all.push({ table, key, used: entry.used });
|
|
183
|
+
all.sort((a, b) => a.used - b.used);
|
|
184
|
+
for (const { table, key } of all) {
|
|
185
|
+
if (bytes <= byteCap || count <= 1) break;
|
|
186
|
+
bytes -= table.get(key)!.chunk.byteLength;
|
|
187
|
+
table.delete(key);
|
|
188
|
+
count -= 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Chunk index of a level frame. */
|
|
193
|
+
export function chunkIndex(frame: number): number {
|
|
194
|
+
return frame >> CHUNK_SHIFT;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Hermite read of `level` at a fractional frame (slow path, tests). */
|
|
198
|
+
export function bankRead(
|
|
199
|
+
source: BankSource,
|
|
200
|
+
level: number,
|
|
201
|
+
pos: number,
|
|
202
|
+
): number {
|
|
203
|
+
const i = Math.floor(pos);
|
|
204
|
+
const t = pos - i;
|
|
205
|
+
const chunk = bankChunk(source, level, i >> CHUNK_SHIFT);
|
|
206
|
+
const o = (i & (BANK_CHUNK_FRAMES - 1)) + 1;
|
|
207
|
+
const xm1 = chunk[o - 1]!;
|
|
208
|
+
const x0 = chunk[o]!;
|
|
209
|
+
const x1 = chunk[o + 1]!;
|
|
210
|
+
const x2 = chunk[o + 2]!;
|
|
211
|
+
const c1 = 0.5 * (x1 - xm1);
|
|
212
|
+
const c2 = xm1 - 2.5 * x0 + 2 * x1 - 0.5 * x2;
|
|
213
|
+
const c3 = 0.5 * (x2 - xm1) + 1.5 * (x0 - x1);
|
|
214
|
+
return ((c3 * t + c2) * t + c1) * t + x0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Bytes held by chunks now. */
|
|
218
|
+
export function bankBytes(): number {
|
|
219
|
+
return bytes;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Chunks held now. */
|
|
223
|
+
export function bankChunks(): number {
|
|
224
|
+
return count;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Drops every chunk; with `cap`, sets a new byte budget (tests). */
|
|
228
|
+
export function resetBank(cap: number = BANK_MAX_BYTES): void {
|
|
229
|
+
for (const table of chunks.values()) table.clear();
|
|
230
|
+
count = 0;
|
|
231
|
+
bytes = 0;
|
|
232
|
+
byteCap = Math.max(0, cap);
|
|
233
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared radix-2 FFT: a re-export of the convolution reverb's
|
|
3
|
+
* `fftInPlace` (cached tables; `inverse` uses e^{+i} and no scaling).
|
|
4
|
+
* Lanes that need windows or STFTs build them on this one transform.
|
|
5
|
+
*/
|
|
6
|
+
export { fftInPlace } from "../effects/convolution.ts";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small per-sample filters for 0.6 instruments. `Biquad` is the existing
|
|
3
|
+
* RBJ biquad (src/audio/effects/common.ts), re-exported unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export { Biquad, type FilterType } from "../effects/common.ts";
|
|
6
|
+
|
|
7
|
+
function clamp(x: number, lo: number, hi: number): number {
|
|
8
|
+
return x < lo ? lo : x > hi ? hi : x;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One-pole lowpass y += (1 - p)(x - y), corner `hz`. */
|
|
12
|
+
export class OnePole {
|
|
13
|
+
p = 0;
|
|
14
|
+
y = 0;
|
|
15
|
+
|
|
16
|
+
constructor(hz: number, sampleRate: number) {
|
|
17
|
+
this.set(hz, sampleRate);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(hz: number, sampleRate: number): void {
|
|
21
|
+
this.p = Math.exp(
|
|
22
|
+
(-2 * Math.PI * clamp(hz, 1, sampleRate * 0.49)) / sampleRate,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
process(x: number): number {
|
|
27
|
+
this.y = x + this.p * (this.y - x);
|
|
28
|
+
return this.y;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
reset(): void {
|
|
32
|
+
this.y = 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** DC blocker y = x - x1 + R y1 (R ≈ 0.996 at 22.05 kHz for a 15 Hz corner). */
|
|
37
|
+
export class DcBlock {
|
|
38
|
+
r: number;
|
|
39
|
+
x1 = 0;
|
|
40
|
+
y1 = 0;
|
|
41
|
+
|
|
42
|
+
constructor(sampleRate: number, hz = 15) {
|
|
43
|
+
this.r = Math.exp((-2 * Math.PI * hz) / sampleRate);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process(x: number): number {
|
|
47
|
+
const y = x - this.x1 + this.r * this.y1;
|
|
48
|
+
this.x1 = x;
|
|
49
|
+
this.y1 = y;
|
|
50
|
+
return y;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reset(): void {
|
|
54
|
+
this.x1 = 0;
|
|
55
|
+
this.y1 = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpolation and fractional delay for 0.6 instruments: 4-point Hermite
|
|
3
|
+
* reads (grains, modulated delays) and a first-order Thiran allpass
|
|
4
|
+
* fractional delay (string loops: flat magnitude, exact phase delay).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 4-point, 3rd-order Hermite (Catmull-Rom) through xm1 x0 x1 x2 at t in [0, 1). */
|
|
8
|
+
export function hermite4(
|
|
9
|
+
xm1: number,
|
|
10
|
+
x0: number,
|
|
11
|
+
x1: number,
|
|
12
|
+
x2: number,
|
|
13
|
+
t: number,
|
|
14
|
+
): number {
|
|
15
|
+
const c1 = 0.5 * (x1 - xm1);
|
|
16
|
+
const c2 = xm1 - 2.5 * x0 + 2 * x1 - 0.5 * x2;
|
|
17
|
+
const c3 = 0.5 * (x2 - xm1) + 1.5 * (x0 - x1);
|
|
18
|
+
return ((c3 * t + c2) * t + c1) * t + x0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Hermite read of `x` at fractional position `pos`; zero outside the buffer. */
|
|
22
|
+
export function hermiteAt(x: Float64Array, pos: number): number {
|
|
23
|
+
const i = Math.floor(pos);
|
|
24
|
+
const n = x.length;
|
|
25
|
+
const at = (k: number): number => (k >= 0 && k < n ? x[k]! : 0);
|
|
26
|
+
return hermite4(at(i - 1), at(i), at(i + 1), at(i + 2), pos - i);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clamp(x: number, lo: number, hi: number): number {
|
|
30
|
+
return x < lo ? lo : x > hi ? hi : x;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Phase delay (samples) at `w` rad/sample of the allpass (a + z^-1)/(1 + a z^-1). */
|
|
34
|
+
export function allpassDelay(a: number, w: number): number {
|
|
35
|
+
if (w < 1e-9) return (1 - a) / (1 + a);
|
|
36
|
+
const s = Math.sin(w);
|
|
37
|
+
const c = Math.cos(w);
|
|
38
|
+
const num = Math.atan2(-s, a + c);
|
|
39
|
+
const den = Math.atan2(-a * s, 1 + a * c);
|
|
40
|
+
let phase = num - den;
|
|
41
|
+
while (phase > 0) phase -= 2 * Math.PI;
|
|
42
|
+
while (phase <= -2 * Math.PI) phase += 2 * Math.PI;
|
|
43
|
+
return -phase / w;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* First-order Thiran allpass coefficient whose phase delay at `w` is exactly
|
|
48
|
+
* `d` samples (d in [0.5, 1.5)): Thiran's closed form, then Newton steps.
|
|
49
|
+
* With w = 0 this is (1 - d) / (1 + d), exact at DC.
|
|
50
|
+
*/
|
|
51
|
+
export function thiranFor(d: number, w = 0): number {
|
|
52
|
+
let a = (1 - d) / (1 + d);
|
|
53
|
+
if (w < 1e-9) return a;
|
|
54
|
+
for (let i = 0; i < 4; i += 1) {
|
|
55
|
+
const f = allpassDelay(a, w) - d;
|
|
56
|
+
if (Math.abs(f) < 1e-10) break;
|
|
57
|
+
const h = 1e-6;
|
|
58
|
+
const df = (allpassDelay(a + h, w) - allpassDelay(a - h, w)) / (2 * h);
|
|
59
|
+
if (df === 0) break;
|
|
60
|
+
a = clamp(a - f / df, -0.9, 0.9);
|
|
61
|
+
}
|
|
62
|
+
return a;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A delay of `delay` samples (>= 0.5): an integer delay line plus a Thiran
|
|
67
|
+
* allpass for the fraction in [0.5, 1.5), so the pole stays well inside the
|
|
68
|
+
* unit circle. `set` may be called between samples (string pitch moves);
|
|
69
|
+
* the allpass state carries over, which is the usual small transient.
|
|
70
|
+
*/
|
|
71
|
+
export class FracDelay {
|
|
72
|
+
private readonly buffer: Float64Array;
|
|
73
|
+
private readonly mask: number;
|
|
74
|
+
private write = 0;
|
|
75
|
+
private whole = 0;
|
|
76
|
+
private a = 0;
|
|
77
|
+
private u1 = 0;
|
|
78
|
+
private y1 = 0;
|
|
79
|
+
|
|
80
|
+
constructor(maxDelay: number, delay = 1, w = 0) {
|
|
81
|
+
let size = 4;
|
|
82
|
+
while (size < Math.ceil(maxDelay) + 4) size *= 2;
|
|
83
|
+
this.buffer = new Float64Array(size);
|
|
84
|
+
this.mask = size - 1;
|
|
85
|
+
this.set(delay, w);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Total delay in samples; `w` is where the phase delay is exact (rad/sample). */
|
|
89
|
+
set(delay: number, w = 0): void {
|
|
90
|
+
const total = clamp(delay, 0.5, this.mask - 2);
|
|
91
|
+
const whole = Math.max(0, Math.floor(total - 0.5));
|
|
92
|
+
this.whole = whole;
|
|
93
|
+
this.a = thiranFor(total - whole, w);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process(x: number): number {
|
|
97
|
+
const b = this.buffer;
|
|
98
|
+
b[this.write] = x;
|
|
99
|
+
const u = b[(this.write - this.whole) & this.mask]!;
|
|
100
|
+
this.write = (this.write + 1) & this.mask;
|
|
101
|
+
const y = this.a * u + this.u1 - this.a * this.y1;
|
|
102
|
+
this.u1 = u;
|
|
103
|
+
this.y1 = y;
|
|
104
|
+
return y;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
reset(): void {
|
|
108
|
+
this.buffer.fill(0);
|
|
109
|
+
this.u1 = 0;
|
|
110
|
+
this.y1 = 0;
|
|
111
|
+
}
|
|
112
|
+
}
|