@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/fit.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fitting sampler windows to the song's time (0.6 `bpm`, `len`, `fit` with
|
|
3
|
+
* `fitmode`). Each algorithm turns a window (already at the render rate)
|
|
4
|
+
* and its position map (`warp.ts` `FitMap`) into a fitted buffer that the
|
|
5
|
+
* sampler then plays at step 1, so `renderSamplerVoices` keeps one code
|
|
6
|
+
* path and one call site.
|
|
7
|
+
*
|
|
8
|
+
* - `repitch` reads the window along the map (tape: pitch follows speed).
|
|
9
|
+
* - `beats` cuts the window at its SuperFlux onsets and places every slice
|
|
10
|
+
* verbatim at its mapped time; every slice ends with a 2 ms fade (at the
|
|
11
|
+
* next slice's start when crowded), and a slowed slice leaves a gap (Ableton Beats without
|
|
12
|
+
* transient loops; no WSOLA, so drums keep their exact transients).
|
|
13
|
+
* - `tones` is a phase vocoder with identity phase locking (Laroche and
|
|
14
|
+
* Dolson, IEEE TSAP 1999), 85 ms Hann frames, hop N/4, analysing two
|
|
15
|
+
* frames one hop apart around the map's source position so any map works.
|
|
16
|
+
*
|
|
17
|
+
* Results are cached by content key in a 64 MB Float32 LRU (the played
|
|
18
|
+
* window only). Offline renders always compute exactly. Live renders
|
|
19
|
+
* (`withLiveFit`) compute fits up to 8 s (source or output) synchronously
|
|
20
|
+
* and longer ones in the background: until ready the voice is silent and
|
|
21
|
+
* `liveFitPending()` is true, so a live note never plays at the wrong pitch.
|
|
22
|
+
*/
|
|
23
|
+
import { ONSET_LEAD_SECONDS, detectOnsets } from "./dsp/onset.ts";
|
|
24
|
+
import {
|
|
25
|
+
HANN_SQUARED_OLA_QUARTER,
|
|
26
|
+
analyse,
|
|
27
|
+
frameSize,
|
|
28
|
+
hann,
|
|
29
|
+
inverse,
|
|
30
|
+
princarg,
|
|
31
|
+
sampleAt,
|
|
32
|
+
} from "./dsp/stft.ts";
|
|
33
|
+
import { hermite4 } from "./dsp/interp.ts";
|
|
34
|
+
import type { FitMap } from "./warp.ts";
|
|
35
|
+
|
|
36
|
+
export type FitAlgorithm = "repitch" | "beats" | "tones";
|
|
37
|
+
|
|
38
|
+
/** Cache bound, in bytes of Float32 output. */
|
|
39
|
+
export const FIT_CACHE_BYTES = 64 * 1024 * 1024;
|
|
40
|
+
/** Live renders compute windows up to this long synchronously. */
|
|
41
|
+
export const LIVE_SYNC_FIT_SECONDS = 8;
|
|
42
|
+
/** Slice end fade for `beats`. */
|
|
43
|
+
const SLICE_FADE_SECONDS = 0.002;
|
|
44
|
+
/** Phase vocoder frame for `tones`. */
|
|
45
|
+
const VOCODER_FRAME_SECONDS = 0.085;
|
|
46
|
+
/** Frames of work between yields of a background fit. */
|
|
47
|
+
const CHUNK = 64;
|
|
48
|
+
|
|
49
|
+
const cache = new Map<string, Float32Array>();
|
|
50
|
+
let cachedBytes = 0;
|
|
51
|
+
const onsetCache = new Map<string, readonly number[]>();
|
|
52
|
+
/** Background jobs by cache key, with the window they fit (one per window). */
|
|
53
|
+
const pending = new Map<
|
|
54
|
+
string,
|
|
55
|
+
{ job: Generator<void, Float32Array>; window: string }
|
|
56
|
+
>();
|
|
57
|
+
let live = false;
|
|
58
|
+
/** True when the current live render left a voice silent while fitting. */
|
|
59
|
+
let missed = false;
|
|
60
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
61
|
+
const listeners = new Set<() => void>();
|
|
62
|
+
|
|
63
|
+
/** Run `render` as a live render: long uncached fits go to the background. */
|
|
64
|
+
export function withLiveFit<T>(render: () => T): T {
|
|
65
|
+
const was = live;
|
|
66
|
+
live = true;
|
|
67
|
+
missed = false;
|
|
68
|
+
try {
|
|
69
|
+
return render();
|
|
70
|
+
} finally {
|
|
71
|
+
live = was;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True while a background fit is running ("fitting"). */
|
|
76
|
+
export function fitting(): boolean {
|
|
77
|
+
return pending.size > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when the last live render had a voice still fitting (silent). */
|
|
81
|
+
export function liveFitPending(): boolean {
|
|
82
|
+
return missed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Called once each time a background fit finishes. */
|
|
86
|
+
export function onFitReady(listener: () => void): () => void {
|
|
87
|
+
listeners.add(listener);
|
|
88
|
+
return () => listeners.delete(listener);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Drops every cached fit (tests). */
|
|
92
|
+
export function clearFitCache(): void {
|
|
93
|
+
cache.clear();
|
|
94
|
+
cachedBytes = 0;
|
|
95
|
+
onsetCache.clear();
|
|
96
|
+
pending.clear();
|
|
97
|
+
if (timer) clearTimeout(timer);
|
|
98
|
+
timer = undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The fitted buffer for `key`, computing it when needed. Returns undefined
|
|
103
|
+
* only in a live render while a long window is still fitting.
|
|
104
|
+
*/
|
|
105
|
+
export function fittedBuffer(
|
|
106
|
+
key: string,
|
|
107
|
+
window: Float32Array,
|
|
108
|
+
sampleRate: number,
|
|
109
|
+
map: FitMap,
|
|
110
|
+
algorithm: FitAlgorithm,
|
|
111
|
+
onsetKey: string,
|
|
112
|
+
length: number = map.length,
|
|
113
|
+
): Float32Array | undefined {
|
|
114
|
+
const hit = cache.get(key);
|
|
115
|
+
if (hit) {
|
|
116
|
+
cache.delete(key);
|
|
117
|
+
cache.set(key, hit);
|
|
118
|
+
return hit;
|
|
119
|
+
}
|
|
120
|
+
const work = () =>
|
|
121
|
+
fitWork(window, sampleRate, map, algorithm, length, () => {
|
|
122
|
+
const known = onsetCache.get(onsetKey);
|
|
123
|
+
if (known) return known;
|
|
124
|
+
const found = detectOnsets(window, sampleRate);
|
|
125
|
+
onsetCache.set(onsetKey, found);
|
|
126
|
+
return found;
|
|
127
|
+
});
|
|
128
|
+
// The work scales with the longer of the source window (onsets, beats)
|
|
129
|
+
// and the output (tones, repitch), so both count against the live budget.
|
|
130
|
+
const workFrames = Math.max(window.length, Math.min(length, map.length));
|
|
131
|
+
if (live && workFrames > LIVE_SYNC_FIT_SECONDS * sampleRate) {
|
|
132
|
+
missed = true;
|
|
133
|
+
if (!pending.has(key)) {
|
|
134
|
+
// A newer fit of the same window (bpm, fitmode or len changed, or a
|
|
135
|
+
// later note) replaces the stale one instead of running beside it.
|
|
136
|
+
for (const [old, entry] of pending)
|
|
137
|
+
if (entry.window === onsetKey) pending.delete(old);
|
|
138
|
+
pending.set(key, { job: work(), window: onsetKey });
|
|
139
|
+
schedule();
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
pending.delete(key);
|
|
144
|
+
const out = drain(work());
|
|
145
|
+
remember(key, out);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function drain(job: Generator<void, Float32Array>): Float32Array {
|
|
150
|
+
for (;;) {
|
|
151
|
+
const step = job.next();
|
|
152
|
+
if (step.done) return step.value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function schedule(): void {
|
|
157
|
+
if (timer) return;
|
|
158
|
+
timer = setTimeout(() => {
|
|
159
|
+
timer = undefined;
|
|
160
|
+
const next = pending.entries().next();
|
|
161
|
+
if (next.done) return;
|
|
162
|
+
const [key, { job }] = next.value;
|
|
163
|
+
const until = performance.now() + 8;
|
|
164
|
+
for (;;) {
|
|
165
|
+
const step = job.next();
|
|
166
|
+
if (step.done) {
|
|
167
|
+
pending.delete(key);
|
|
168
|
+
remember(key, step.value);
|
|
169
|
+
for (const listener of listeners) listener();
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (performance.now() > until) break;
|
|
173
|
+
}
|
|
174
|
+
if (pending.size > 0) schedule();
|
|
175
|
+
}, 0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function remember(key: string, out: Float32Array): void {
|
|
179
|
+
if (out.byteLength > FIT_CACHE_BYTES) return;
|
|
180
|
+
cache.set(key, out);
|
|
181
|
+
cachedBytes += out.byteLength;
|
|
182
|
+
for (const [old, buffer] of cache) {
|
|
183
|
+
if (cachedBytes <= FIT_CACHE_BYTES) break;
|
|
184
|
+
cache.delete(old);
|
|
185
|
+
cachedBytes -= buffer.byteLength;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Bytes held by the fit cache. */
|
|
190
|
+
export function fitCacheBytes(): number {
|
|
191
|
+
return cachedBytes;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function* fitWork(
|
|
195
|
+
window: Float32Array,
|
|
196
|
+
sampleRate: number,
|
|
197
|
+
map: FitMap,
|
|
198
|
+
algorithm: FitAlgorithm,
|
|
199
|
+
limit: number,
|
|
200
|
+
onsets: () => readonly number[],
|
|
201
|
+
): Generator<void, Float32Array> {
|
|
202
|
+
const length = Math.max(1, Math.round(Math.min(limit, map.length)));
|
|
203
|
+
if (algorithm === "repitch") {
|
|
204
|
+
const out = new Float32Array(length);
|
|
205
|
+
for (let e = 0; e < length; e += 1) {
|
|
206
|
+
out[e] = hermiteRead(window, map.at(e));
|
|
207
|
+
if (e % (CHUNK * 1024) === 0) yield;
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
if (algorithm === "beats") {
|
|
212
|
+
yield;
|
|
213
|
+
return beatsFit(window, sampleRate, onsets(), map, length);
|
|
214
|
+
}
|
|
215
|
+
return yield* tonesFit(window, sampleRate, map, length);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `beats`: each inter-onset slice copied verbatim to its mapped start,
|
|
220
|
+
* cut with a 2 ms fade at the next slice's mapped start.
|
|
221
|
+
*/
|
|
222
|
+
export function beatsFit(
|
|
223
|
+
x: Float32Array,
|
|
224
|
+
sampleRate: number,
|
|
225
|
+
onsets: readonly number[],
|
|
226
|
+
map: FitMap,
|
|
227
|
+
length: number,
|
|
228
|
+
): Float32Array {
|
|
229
|
+
const out = new Float32Array(length);
|
|
230
|
+
const marks = [0, ...onsets.filter((v) => v > 0 && v < x.length), x.length];
|
|
231
|
+
const fade = Math.max(1, Math.round(SLICE_FADE_SECONDS * sampleRate));
|
|
232
|
+
// Onsets sit 1 ms before the rise (onset.ts); anchoring at the rise puts
|
|
233
|
+
// the transient itself, not the slice edge, on the mapped time.
|
|
234
|
+
const lead = Math.round(ONSET_LEAD_SECONDS * sampleRate);
|
|
235
|
+
const place = (v: number) =>
|
|
236
|
+
v === 0 || v === x.length
|
|
237
|
+
? Math.round(map.outAt(v))
|
|
238
|
+
: Math.round(map.outAt(Math.min(x.length, v + lead))) - lead;
|
|
239
|
+
for (let s = 0; s + 1 < marks.length; s += 1) {
|
|
240
|
+
const a = marks[s]!;
|
|
241
|
+
const b = marks[s + 1]!;
|
|
242
|
+
const oa = Math.max(0, place(a));
|
|
243
|
+
const ob = Math.min(length, place(b));
|
|
244
|
+
const span = Math.min(ob - oa, b - a);
|
|
245
|
+
if (span <= 0) continue;
|
|
246
|
+
// Every slice end fades (a slowed slice would otherwise step from its
|
|
247
|
+
// last sample into the silent gap and click), as the prototype does.
|
|
248
|
+
// A slice after the first starts in the previous hit's ring-out, so it
|
|
249
|
+
// fades in over the onset lead (before its transient rises).
|
|
250
|
+
const tail = Math.min(fade, span);
|
|
251
|
+
const head = a === 0 ? 0 : Math.min(Math.max(1, lead), span - tail);
|
|
252
|
+
for (let i = 0; i < span && oa + i < length; i += 1) {
|
|
253
|
+
const g =
|
|
254
|
+
(i >= span - tail ? (span - i) / tail : 1) *
|
|
255
|
+
(i < head ? (i + 1) / (head + 1) : 1);
|
|
256
|
+
out[oa + i] = out[oa + i]! + x[a + i]! * g;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* `tones`: phase vocoder with identity phase locking. Each output hop
|
|
264
|
+
* analyses the window at the map's position and one hop before it, so the
|
|
265
|
+
* instantaneous frequency comes from the source and the map may vary.
|
|
266
|
+
*/
|
|
267
|
+
export function* tonesFit(
|
|
268
|
+
x: Float32Array,
|
|
269
|
+
sampleRate: number,
|
|
270
|
+
map: FitMap,
|
|
271
|
+
length: number,
|
|
272
|
+
): Generator<void, Float32Array> {
|
|
273
|
+
const n = frameSize(VOCODER_FRAME_SECONDS, sampleRate);
|
|
274
|
+
const hs = n / 4;
|
|
275
|
+
const half = n / 2;
|
|
276
|
+
const w = hann(n);
|
|
277
|
+
const norm = 1 / HANN_SQUARED_OLA_QUARTER;
|
|
278
|
+
const out = new Float64Array(length + n);
|
|
279
|
+
const reA = new Float64Array(n);
|
|
280
|
+
const imA = new Float64Array(n);
|
|
281
|
+
const reB = new Float64Array(n);
|
|
282
|
+
const imB = new Float64Array(n);
|
|
283
|
+
const mag = new Float64Array(half + 1);
|
|
284
|
+
const phA = new Float64Array(half + 1);
|
|
285
|
+
const syn = new Float64Array(half + 1);
|
|
286
|
+
const next = new Float64Array(half + 1);
|
|
287
|
+
const omega = new Float64Array(half + 1);
|
|
288
|
+
const prevPh = new Float64Array(half + 1);
|
|
289
|
+
let lastAt = 0;
|
|
290
|
+
const peaks: number[] = [];
|
|
291
|
+
for (let m = 0; m * hs < length; m += 1) {
|
|
292
|
+
if (m % CHUNK === CHUNK - 1) yield;
|
|
293
|
+
const at = Math.round(map.at(m * hs + half) - half);
|
|
294
|
+
// Frequencies come from the previous analysis frame when it is a
|
|
295
|
+
// usable distance back (one FFT per hop); otherwise from an extra
|
|
296
|
+
// frame exactly one hop back (freezes, jumps, the first frame).
|
|
297
|
+
const back = at - lastAt;
|
|
298
|
+
const reuse = m > 0 && back >= hs / 4 && back <= half;
|
|
299
|
+
if (!reuse) {
|
|
300
|
+
analyse(x, at - hs, w, reB, imB);
|
|
301
|
+
for (let k = 0; k <= half; k += 1)
|
|
302
|
+
prevPh[k] = Math.atan2(imB[k]!, reB[k]!);
|
|
303
|
+
}
|
|
304
|
+
const ha = reuse ? back : hs;
|
|
305
|
+
analyse(x, at, w, reA, imA);
|
|
306
|
+
for (let k = 0; k <= half; k += 1) {
|
|
307
|
+
mag[k] = Math.hypot(reA[k]!, imA[k]!);
|
|
308
|
+
phA[k] = Math.atan2(imA[k]!, reA[k]!);
|
|
309
|
+
const bin = (2 * Math.PI * k) / n;
|
|
310
|
+
omega[k] =
|
|
311
|
+
(bin * ha + princarg(phA[k]! - prevPh[k]! - bin * ha)) * (hs / ha);
|
|
312
|
+
prevPh[k] = phA[k]!;
|
|
313
|
+
}
|
|
314
|
+
lastAt = at;
|
|
315
|
+
if (m === 0) syn.set(phA);
|
|
316
|
+
else {
|
|
317
|
+
// Peaks: local maxima over ±2 bins; each bin keeps its phase offset
|
|
318
|
+
// from its nearest peak, which advances by its own frequency.
|
|
319
|
+
peaks.length = 0;
|
|
320
|
+
for (let k = 2; k <= half - 2; k += 1) {
|
|
321
|
+
const v = mag[k]!;
|
|
322
|
+
if (
|
|
323
|
+
v > mag[k - 1]! &&
|
|
324
|
+
v >= mag[k + 1]! &&
|
|
325
|
+
v > mag[k - 2]! &&
|
|
326
|
+
v >= mag[k + 2]!
|
|
327
|
+
)
|
|
328
|
+
peaks.push(k);
|
|
329
|
+
}
|
|
330
|
+
if (peaks.length === 0)
|
|
331
|
+
for (let k = 0; k <= half; k += 1) syn[k] = syn[k]! + omega[k]!;
|
|
332
|
+
else {
|
|
333
|
+
for (const p of peaks) next[p] = syn[p]! + omega[p]!;
|
|
334
|
+
let j = 0;
|
|
335
|
+
for (let k = 0; k <= half; k += 1) {
|
|
336
|
+
while (
|
|
337
|
+
j + 1 < peaks.length &&
|
|
338
|
+
Math.abs(peaks[j + 1]! - k) < Math.abs(peaks[j]! - k)
|
|
339
|
+
)
|
|
340
|
+
j += 1;
|
|
341
|
+
const p = peaks[j]!;
|
|
342
|
+
if (k !== p) next[k] = next[p]! + phA[k]! - phA[p]!;
|
|
343
|
+
}
|
|
344
|
+
syn.set(next);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (let k = 0; k <= half; k += 1) {
|
|
348
|
+
const a = mag[k]!;
|
|
349
|
+
reA[k] = a * Math.cos(syn[k]!);
|
|
350
|
+
imA[k] = a * Math.sin(syn[k]!);
|
|
351
|
+
if (k > 0 && k < half) {
|
|
352
|
+
reA[n - k] = reA[k]!;
|
|
353
|
+
imA[n - k] = -imA[k]!;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
imA[0] = 0;
|
|
357
|
+
imA[half] = 0;
|
|
358
|
+
inverse(reA, imA);
|
|
359
|
+
const o0 = m * hs;
|
|
360
|
+
for (let i = 0; i < n; i += 1)
|
|
361
|
+
out[o0 + i] = out[o0 + i]! + reA[i]! * w[i]! * norm;
|
|
362
|
+
}
|
|
363
|
+
const result = new Float32Array(length);
|
|
364
|
+
for (let i = 0; i < length; i += 1) result[i] = out[i]!;
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Window `[from, to)` of `mono` at `rate` source frames per output frame, linear reads like the sampler's; reversed when asked. */
|
|
369
|
+
export function windowAt(
|
|
370
|
+
mono: Float32Array,
|
|
371
|
+
from: number,
|
|
372
|
+
to: number,
|
|
373
|
+
rate: number,
|
|
374
|
+
reverse: boolean,
|
|
375
|
+
): Float32Array {
|
|
376
|
+
const frames = Math.max(1, Math.floor((to - from) / rate));
|
|
377
|
+
const out = new Float32Array(frames);
|
|
378
|
+
const last = mono.length - 1;
|
|
379
|
+
for (let i = 0; i < frames; i += 1) {
|
|
380
|
+
const position = reverse ? to - 1 - i * rate : from + i * rate;
|
|
381
|
+
if (position <= 0) out[i] = mono[0]!;
|
|
382
|
+
else if (position >= last) out[i] = mono[last]!;
|
|
383
|
+
else {
|
|
384
|
+
const base = Math.floor(position);
|
|
385
|
+
const a = mono[base]!;
|
|
386
|
+
out[i] = a + (mono[base + 1]! - a) * (position - base);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 4-point Hermite read of a Float32 buffer, zero outside it. */
|
|
393
|
+
function hermiteRead(x: Float32Array, pos: number): number {
|
|
394
|
+
const i = Math.floor(pos);
|
|
395
|
+
return hermite4(
|
|
396
|
+
sampleAt(x, i - 1),
|
|
397
|
+
sampleAt(x, i),
|
|
398
|
+
sampleAt(x, i + 1),
|
|
399
|
+
sampleAt(x, i + 2),
|
|
400
|
+
pos - i,
|
|
401
|
+
);
|
|
402
|
+
}
|