@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/sampler.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* `accelerate` ramps the rate; `squiz` raises pitch per zero-crossing
|
|
14
14
|
* cycle (described in Tidal/SuperDirt docs; implemented here from that
|
|
15
15
|
* description).
|
|
16
|
-
* - keyed mode repitches from `root` (rate × 2^((pitch − root)/12)
|
|
16
|
+
* - keyed mode repitches from `root` (rate × 2^((pitch − root)/12), or the
|
|
17
|
+
* tuning's key frequency over the root's 12-TET frequency) and holds
|
|
17
18
|
* for the note's length; oneshot mode maps voices to pitch slots from 36 in
|
|
18
19
|
* voice-name order and plays the whole window unless `loop` is set.
|
|
19
20
|
*
|
|
@@ -31,6 +32,29 @@ import {
|
|
|
31
32
|
type TrackScore,
|
|
32
33
|
} from "../../core/score.ts";
|
|
33
34
|
import { sampleKey, type DecodedSample, type SampleBank } from "./samples.ts";
|
|
35
|
+
import {
|
|
36
|
+
barAt,
|
|
37
|
+
loopSecondsOf,
|
|
38
|
+
performedNotes,
|
|
39
|
+
secondsAtTick,
|
|
40
|
+
} from "../../core/tempo.ts";
|
|
41
|
+
import {
|
|
42
|
+
beatFitMap,
|
|
43
|
+
sampleWarpFor,
|
|
44
|
+
spanFitMap,
|
|
45
|
+
warpedSpan,
|
|
46
|
+
type FitMap,
|
|
47
|
+
type SampleWarp,
|
|
48
|
+
} from "./warp.ts";
|
|
49
|
+
import { fittedBuffer, windowAt } from "./fit.ts";
|
|
50
|
+
import {
|
|
51
|
+
performanceTimingFor,
|
|
52
|
+
tunedTiming,
|
|
53
|
+
performNotes,
|
|
54
|
+
type NotePerformance,
|
|
55
|
+
type PerformedNote,
|
|
56
|
+
} from "../../core/expression.ts";
|
|
57
|
+
import { resolveTuning, type TuningTable } from "../../core/tuning.ts";
|
|
34
58
|
|
|
35
59
|
/** Output level of a full-scale sample at velocity 1 and gain 1. */
|
|
36
60
|
export const SAMPLE_LEVEL = 0.7;
|
|
@@ -39,6 +63,8 @@ const ATTACK_SECONDS = 0.001;
|
|
|
39
63
|
/** Fade at a natural end, a held release, a choke and a voice steal. */
|
|
40
64
|
const END_FADE_SECONDS = 0.003;
|
|
41
65
|
const RELEASE_SECONDS = 0.01;
|
|
66
|
+
/** Extra fitted frames past a note's reach (read interpolation). */
|
|
67
|
+
const REACH_MARGIN_FRAMES = 64;
|
|
42
68
|
const CHOKE_SECONDS = 0.005;
|
|
43
69
|
/** Loop seam crossfade (source time). */
|
|
44
70
|
const LOOP_CROSSFADE_SECONDS = 0.005;
|
|
@@ -46,6 +72,8 @@ const LOOP_CROSSFADE_SECONDS = 0.005;
|
|
|
46
72
|
export type SamplerTiming = Readonly<{
|
|
47
73
|
score: TrackScore;
|
|
48
74
|
sampleRate: number;
|
|
75
|
+
/** The song's tempo map in samples; absent keeps constant tempo. */
|
|
76
|
+
warp?: SampleWarp;
|
|
49
77
|
}>;
|
|
50
78
|
|
|
51
79
|
/** One planned voice: when it sounds and how it reads its sample. */
|
|
@@ -74,6 +102,10 @@ export type SamplerVoice = {
|
|
|
74
102
|
readonly ramp: number;
|
|
75
103
|
/** `squiz` ratio; 1 is off. */
|
|
76
104
|
readonly squiz: number;
|
|
105
|
+
/** Note expression: pitch in cents over time (glide, bend, vibrato). */
|
|
106
|
+
readonly cents?: NotePerformance["cents"];
|
|
107
|
+
/** Half pedal: the level fades from `from` seconds. */
|
|
108
|
+
readonly damp?: NotePerformance["damp"];
|
|
77
109
|
};
|
|
78
110
|
|
|
79
111
|
/**
|
|
@@ -96,7 +128,8 @@ function framesToTravel(distance: number, step: number, ramp: number): number {
|
|
|
96
128
|
}
|
|
97
129
|
|
|
98
130
|
function noteStartFrame(note: Note, timing: SamplerTiming): number {
|
|
99
|
-
const { score, sampleRate } = timing;
|
|
131
|
+
const { score, sampleRate, warp } = timing;
|
|
132
|
+
if (warp) return warpedSpan(warp, note.startTick, note.durationTicks).start;
|
|
100
133
|
return Math.max(
|
|
101
134
|
0,
|
|
102
135
|
Math.floor(
|
|
@@ -107,7 +140,8 @@ function noteStartFrame(note: Note, timing: SamplerTiming): number {
|
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
function noteLengthFrames(note: Note, timing: SamplerTiming): number {
|
|
110
|
-
const { score, sampleRate } = timing;
|
|
143
|
+
const { score, sampleRate, warp } = timing;
|
|
144
|
+
if (warp) return warpedSpan(warp, note.startTick, note.durationTicks).length;
|
|
111
145
|
return Math.max(
|
|
112
146
|
1,
|
|
113
147
|
Math.floor(
|
|
@@ -117,9 +151,15 @@ function noteLengthFrames(note: Note, timing: SamplerTiming): number {
|
|
|
117
151
|
);
|
|
118
152
|
}
|
|
119
153
|
|
|
120
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Which voice a note plays and the pitch ratio it plays at. A keyed voice
|
|
156
|
+
* repitches to the tuning's key frequency over its root's 12-TET frequency
|
|
157
|
+
* (a sample is recorded at concert pitch); one-shot slots ignore tuning,
|
|
158
|
+
* since their pitch picks a voice rather than a note.
|
|
159
|
+
*/
|
|
121
160
|
function voiceResolver(
|
|
122
161
|
track: Track,
|
|
162
|
+
tuning?: TuningTable,
|
|
123
163
|
): (pitch: number) => { voice: string; ratio: number } | undefined {
|
|
124
164
|
const sampler = track.sampler!;
|
|
125
165
|
if (sampler.mode === "oneshot") {
|
|
@@ -141,7 +181,14 @@ function voiceResolver(
|
|
|
141
181
|
for (const candidate of keyed)
|
|
142
182
|
if (candidate.root <= pitch) chosen = candidate;
|
|
143
183
|
if (!chosen) return undefined;
|
|
144
|
-
|
|
184
|
+
if (!tuning)
|
|
185
|
+
return { voice: chosen.voice, ratio: 2 ** ((pitch - chosen.root) / 12) };
|
|
186
|
+
const hz = tuning.hz[pitch]!;
|
|
187
|
+
if (!(hz > 0)) return undefined;
|
|
188
|
+
return {
|
|
189
|
+
voice: chosen.voice,
|
|
190
|
+
ratio: hz / (440 * 2 ** ((chosen.root - 69) / 12)),
|
|
191
|
+
};
|
|
145
192
|
};
|
|
146
193
|
}
|
|
147
194
|
|
|
@@ -154,7 +201,10 @@ export function planSamplerVoices(
|
|
|
154
201
|
): SamplerVoice[] {
|
|
155
202
|
const sampler = track.sampler;
|
|
156
203
|
if (!sampler) return [];
|
|
157
|
-
const resolve = voiceResolver(
|
|
204
|
+
const resolve = voiceResolver(
|
|
205
|
+
track,
|
|
206
|
+
resolveTuning(timing.score.tuning, track.tuning, timing.score.key),
|
|
207
|
+
);
|
|
158
208
|
const ordered = [...notes].sort(
|
|
159
209
|
(a, b) =>
|
|
160
210
|
a.startTick - b.startTick ||
|
|
@@ -174,27 +224,54 @@ export function planSamplerVoices(
|
|
|
174
224
|
voice.fade = Math.min(fade, end - voice.start);
|
|
175
225
|
};
|
|
176
226
|
for (const note of ordered) {
|
|
177
|
-
const
|
|
178
|
-
if (!
|
|
227
|
+
const resolved = resolve(note.pitch);
|
|
228
|
+
if (!resolved) continue;
|
|
229
|
+
// A note's cents repitch every voice, keyed or one-shot.
|
|
230
|
+
const target = note.cents
|
|
231
|
+
? { ...resolved, ratio: resolved.ratio * 2 ** (note.cents / 1200) }
|
|
232
|
+
: resolved;
|
|
179
233
|
const ref: SampleRef | undefined = sampler.voices[target.voice];
|
|
180
|
-
const
|
|
181
|
-
if (!ref || !
|
|
182
|
-
const speed = ref.speed ?? 1;
|
|
183
|
-
const regionStart = (ref.begin ?? 0) * sample.frames;
|
|
184
|
-
const regionEnd = (ref.end ?? 1) * sample.frames;
|
|
234
|
+
const decoded = bank.voices.get(sampleKey(track.id, target.voice));
|
|
235
|
+
if (!ref || !decoded) continue;
|
|
185
236
|
const held = noteLengthFrames(note, timing);
|
|
237
|
+
// 0.6 fit (`bpm`, `len`, or `fit` with fitmode beats|tones): the window
|
|
238
|
+
// is fitted once into a render-rate buffer that plays forward at step 1
|
|
239
|
+
// (times the keyed ratio). Absent fields keep the 0.5 path untouched.
|
|
240
|
+
// Only the frames this note can reach are fitted: a keyed or clipped
|
|
241
|
+
// note (not looping, no accelerate) ends at its held length plus the
|
|
242
|
+
// release, read at the keyed ratio; anything else may play it all.
|
|
243
|
+
const reach =
|
|
244
|
+
ref.loop !== true &&
|
|
245
|
+
(ref.accelerate ?? 0) === 0 &&
|
|
246
|
+
(sampler.mode === "keyed" || ref.clip !== undefined)
|
|
247
|
+
? Math.ceil(
|
|
248
|
+
((ref.clip === undefined
|
|
249
|
+
? held
|
|
250
|
+
: Math.max(1, Math.floor(held * ref.clip))) +
|
|
251
|
+
Math.round(RELEASE_SECONDS * sampleRate)) *
|
|
252
|
+
target.ratio,
|
|
253
|
+
) + REACH_MARGIN_FRAMES
|
|
254
|
+
: Infinity;
|
|
255
|
+
const fitted = isFitted(ref)
|
|
256
|
+
? fitVoice(ref, decoded, note.startTick, held, timing, reach)
|
|
257
|
+
: undefined;
|
|
258
|
+
if (fitted === null) continue;
|
|
259
|
+
const sample = fitted?.sample ?? decoded;
|
|
260
|
+
const speed = fitted ? 1 : (ref.speed ?? 1);
|
|
261
|
+
const regionStart = fitted ? 0 : (ref.begin ?? 0) * sample.frames;
|
|
262
|
+
const regionEnd = fitted ? sample.frames : (ref.end ?? 1) * sample.frames;
|
|
186
263
|
// `fit`, `unit: "c"` and `unit: "s"` give the window a duration; the
|
|
187
264
|
// keyed ratio still repitches on top.
|
|
188
265
|
const unit = ref.unit ?? "r";
|
|
189
|
-
const seconds =
|
|
190
|
-
?
|
|
191
|
-
:
|
|
192
|
-
?
|
|
193
|
-
|
|
194
|
-
Math.abs(speed)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
const seconds = fitted
|
|
267
|
+
? undefined
|
|
268
|
+
: ref.fit
|
|
269
|
+
? held / sampleRate
|
|
270
|
+
: unit === "c"
|
|
271
|
+
? cycleSeconds(timing.score, note.startTick) / Math.abs(speed)
|
|
272
|
+
: unit === "s"
|
|
273
|
+
? Math.abs(speed)
|
|
274
|
+
: undefined;
|
|
198
275
|
const step =
|
|
199
276
|
seconds === undefined
|
|
200
277
|
? ((Math.abs(speed) * sample.sampleRate) / sampleRate) * target.ratio
|
|
@@ -234,11 +311,18 @@ export function planSamplerVoices(
|
|
|
234
311
|
fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
|
|
235
312
|
}
|
|
236
313
|
}
|
|
237
|
-
const loopFrom =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
314
|
+
const loopFrom = fitted
|
|
315
|
+
? fitted.loopFrom
|
|
316
|
+
: ref.loopBegin === undefined
|
|
317
|
+
? regionStart
|
|
318
|
+
: ref.loopBegin * sample.frames;
|
|
319
|
+
const loopTo = fitted
|
|
320
|
+
? fitted.loopTo
|
|
321
|
+
: ref.loopEnd === undefined
|
|
322
|
+
? regionEnd
|
|
323
|
+
: ref.loopEnd * sample.frames;
|
|
241
324
|
const start = noteStartFrame(note, timing);
|
|
325
|
+
const performance = (note as PerformedNote).performance;
|
|
242
326
|
const voice: SamplerVoice = {
|
|
243
327
|
voice: target.voice,
|
|
244
328
|
sample,
|
|
@@ -257,6 +341,8 @@ export function planSamplerVoices(
|
|
|
257
341
|
loopSpan: loopTo - loopFrom,
|
|
258
342
|
ramp,
|
|
259
343
|
squiz: ref.squiz ?? 1,
|
|
344
|
+
...(performance?.cents ? { cents: performance.cents } : {}),
|
|
345
|
+
...(performance?.damp ? { damp: performance.damp } : {}),
|
|
260
346
|
};
|
|
261
347
|
if (voice.choke !== undefined) {
|
|
262
348
|
const previous = lastInGroup.get(voice.choke);
|
|
@@ -273,6 +359,111 @@ export function planSamplerVoices(
|
|
|
273
359
|
return voices;
|
|
274
360
|
}
|
|
275
361
|
|
|
362
|
+
/** A SampleRef that fits its window to the song (0.6 `bpm`, `len`, fitmode). */
|
|
363
|
+
export function isFitted(ref: SampleRef): boolean {
|
|
364
|
+
return (
|
|
365
|
+
ref.bpm !== undefined ||
|
|
366
|
+
ref.len !== undefined ||
|
|
367
|
+
(ref.fit === true && (ref.fitmode === "beats" || ref.fitmode === "tones"))
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
type FittedVoice = Readonly<{
|
|
372
|
+
sample: DecodedSample;
|
|
373
|
+
loopFrom: number;
|
|
374
|
+
loopTo: number;
|
|
375
|
+
}>;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The fitted window of one note. Precedence `fit` > `bpm` > `len`:
|
|
379
|
+
* - `fit`: the window fills the note (Strudel `fit`, with an algorithm);
|
|
380
|
+
* - `bpm`: the window advances `|speed|` source beats per song beat,
|
|
381
|
+
* following the tempo map (ramps, steps, fermatas);
|
|
382
|
+
* - `len`: the window spans `len` song beats through the tempo map.
|
|
383
|
+
* A negative `speed` reverses the window; `unit` is ignored once fitted.
|
|
384
|
+
* Returns null while a live fit is still computing (the voice stays
|
|
385
|
+
* silent rather than playing at the wrong pitch).
|
|
386
|
+
*/
|
|
387
|
+
function fitVoice(
|
|
388
|
+
ref: SampleRef,
|
|
389
|
+
decoded: DecodedSample,
|
|
390
|
+
startTick: number,
|
|
391
|
+
held: number,
|
|
392
|
+
timing: SamplerTiming,
|
|
393
|
+
reach: number,
|
|
394
|
+
): FittedVoice | null {
|
|
395
|
+
const { sampleRate, score, warp } = timing;
|
|
396
|
+
const speed = ref.speed ?? 1;
|
|
397
|
+
const reverse = speed < 0;
|
|
398
|
+
const from = (ref.begin ?? 0) * decoded.frames;
|
|
399
|
+
const to = (ref.end ?? 1) * decoded.frames;
|
|
400
|
+
const rate = decoded.sampleRate / sampleRate;
|
|
401
|
+
const window = windowAt(decoded.mono, from, to, rate, reverse);
|
|
402
|
+
const frames = window.length;
|
|
403
|
+
let map: FitMap;
|
|
404
|
+
if (ref.fit === true) map = spanFitMap(frames, held);
|
|
405
|
+
else {
|
|
406
|
+
const perBeat =
|
|
407
|
+
ref.bpm !== undefined
|
|
408
|
+
? ((sampleRate * 60) / ref.bpm) * Math.abs(speed)
|
|
409
|
+
: frames / ref.len!;
|
|
410
|
+
map = beatFitMap(
|
|
411
|
+
warp,
|
|
412
|
+
{
|
|
413
|
+
tempoBpm: score.tempoBpm,
|
|
414
|
+
ticksPerBeat: score.ticksPerBeat,
|
|
415
|
+
sampleRate,
|
|
416
|
+
},
|
|
417
|
+
startTick,
|
|
418
|
+
frames,
|
|
419
|
+
perBeat,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const algorithm = ref.fitmode ?? "repitch";
|
|
423
|
+
const windowKey = `${decoded.sha256}:${decoded.sampleRate}:${from}:${to}:${sampleRate}:${reverse ? 1 : 0}`;
|
|
424
|
+
const length = Math.min(map.length, reach);
|
|
425
|
+
const key = `${windowKey}:${algorithm}:${mapPrint(map, frames)}:${Math.round(length)}`;
|
|
426
|
+
const buffer = fittedBuffer(
|
|
427
|
+
key,
|
|
428
|
+
window,
|
|
429
|
+
sampleRate,
|
|
430
|
+
map,
|
|
431
|
+
algorithm,
|
|
432
|
+
windowKey,
|
|
433
|
+
length,
|
|
434
|
+
);
|
|
435
|
+
if (!buffer) return null;
|
|
436
|
+
const sample: DecodedSample = Object.freeze({
|
|
437
|
+
sha256: decoded.sha256,
|
|
438
|
+
sampleRate,
|
|
439
|
+
channels: 1,
|
|
440
|
+
frames: buffer.length,
|
|
441
|
+
mono: buffer,
|
|
442
|
+
});
|
|
443
|
+
// Loop points move with the map (in window frames, then out frames).
|
|
444
|
+
const loopAt = (fraction: number | undefined, fallback: number) => {
|
|
445
|
+
if (fraction === undefined) return fallback;
|
|
446
|
+
const source = fraction * decoded.frames;
|
|
447
|
+
const inWindow = (reverse ? to - source : source - from) / rate;
|
|
448
|
+
return Math.max(0, Math.min(buffer.length, map.outAt(inWindow)));
|
|
449
|
+
};
|
|
450
|
+
const a = loopAt(ref.loopBegin, 0);
|
|
451
|
+
const b = loopAt(ref.loopEnd, buffer.length);
|
|
452
|
+
return Object.freeze({
|
|
453
|
+
sample,
|
|
454
|
+
loopFrom: Math.min(a, b),
|
|
455
|
+
loopTo: Math.max(a, b),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Fingerprint of a position map: its length and 64 exact samples. */
|
|
460
|
+
function mapPrint(map: FitMap, frames: number): string {
|
|
461
|
+
const parts = [map.length.toString()];
|
|
462
|
+
for (let i = 1; i <= 64; i += 1)
|
|
463
|
+
parts.push(map.outAt((frames * i) / 64).toString());
|
|
464
|
+
return parts.join(",");
|
|
465
|
+
}
|
|
466
|
+
|
|
276
467
|
/** Linear-interpolated read, clamped to the sample. */
|
|
277
468
|
function readAt(mono: Float32Array, position: number): number {
|
|
278
469
|
const last = mono.length - 1;
|
|
@@ -293,7 +484,7 @@ export function renderSamplerVoices(
|
|
|
293
484
|
timing: SamplerTiming,
|
|
294
485
|
gainAt: (tick: number) => number,
|
|
295
486
|
): void {
|
|
296
|
-
const { sampleRate, score } = timing;
|
|
487
|
+
const { sampleRate, score, warp } = timing;
|
|
297
488
|
const samplesPerTick =
|
|
298
489
|
(sampleRate * 60) / (score.tempoBpm * score.ticksPerBeat);
|
|
299
490
|
const attack = Math.max(1, ATTACK_SECONDS * sampleRate);
|
|
@@ -311,10 +502,31 @@ export function renderSamplerVoices(
|
|
|
311
502
|
const level = SAMPLE_LEVEL * voice.gain;
|
|
312
503
|
const at = (offset: number) =>
|
|
313
504
|
readAt(mono, reverse ? regionEnd - 1 - offset : regionStart + offset);
|
|
505
|
+
// A bent voice (note expression) integrates its rate frame by frame;
|
|
506
|
+
// reads arrive in order, so the running sum is kept between calls.
|
|
507
|
+
const bent = voice.cents;
|
|
508
|
+
let bentAt = 0;
|
|
509
|
+
let bentTravel = 0;
|
|
510
|
+
const travel = (elapsed: number): number => {
|
|
511
|
+
if (!bent)
|
|
512
|
+
return ramp === 0 ? elapsed * step : travelAt(elapsed, step, ramp);
|
|
513
|
+
if (elapsed < bentAt) {
|
|
514
|
+
bentAt = 0;
|
|
515
|
+
bentTravel = 0;
|
|
516
|
+
}
|
|
517
|
+
while (bentAt < elapsed) {
|
|
518
|
+
const rate =
|
|
519
|
+
step *
|
|
520
|
+
(ramp === 0 ? 1 : Math.max(0, 1 + ramp * bentAt)) *
|
|
521
|
+
2 ** (bent(bentAt / sampleRate) / 1200);
|
|
522
|
+
bentTravel += rate;
|
|
523
|
+
bentAt += 1;
|
|
524
|
+
}
|
|
525
|
+
return bentTravel;
|
|
526
|
+
};
|
|
314
527
|
// The raw read at an elapsed frame, or undefined past a one-shot's end.
|
|
315
528
|
const read = (elapsed: number): number | undefined => {
|
|
316
|
-
const travelled =
|
|
317
|
-
ramp === 0 ? elapsed * step : travelAt(elapsed, step, ramp);
|
|
529
|
+
const travelled = travel(elapsed);
|
|
318
530
|
if (!loop) return travelled >= span ? undefined : at(travelled);
|
|
319
531
|
if (travelled < loopStart) return at(travelled);
|
|
320
532
|
const into = travelled - loopStart;
|
|
@@ -339,11 +551,16 @@ export function renderSamplerVoices(
|
|
|
339
551
|
if (value === undefined) break;
|
|
340
552
|
let envelope = Math.min(1, elapsed / attack);
|
|
341
553
|
if (index >= fadeFrom) envelope *= (voice.end - index) / voice.fade;
|
|
554
|
+
const damp = voice.damp;
|
|
555
|
+
if (damp && elapsed / sampleRate > damp.from)
|
|
556
|
+
envelope *= Math.exp(-(elapsed / sampleRate - damp.from) / damp.tau);
|
|
342
557
|
target[index]! +=
|
|
343
558
|
value *
|
|
344
559
|
envelope *
|
|
345
560
|
level *
|
|
346
|
-
gainAt(
|
|
561
|
+
gainAt(
|
|
562
|
+
warp ? warp.tick(index) : voice.startTick + elapsed / samplesPerTick,
|
|
563
|
+
);
|
|
347
564
|
}
|
|
348
565
|
}
|
|
349
566
|
}
|
|
@@ -406,15 +623,33 @@ export function samplerTailSeconds(
|
|
|
406
623
|
sampleRate: number,
|
|
407
624
|
): number {
|
|
408
625
|
if (bank.voices.size === 0) return 0;
|
|
409
|
-
const loopEnd =
|
|
410
|
-
|
|
411
|
-
const timing = { score, sampleRate };
|
|
626
|
+
const loopEnd = loopSecondsOf(score) * sampleRate;
|
|
627
|
+
const warp = sampleWarpFor(score, sampleRate);
|
|
628
|
+
const timing = { score, sampleRate, ...(warp ? { warp } : {}) };
|
|
412
629
|
let latest = 0;
|
|
630
|
+
const performance = performanceTimingFor(score);
|
|
413
631
|
for (const track of score.tracks) {
|
|
414
632
|
if (!track.sampler) continue;
|
|
415
|
-
const notes =
|
|
633
|
+
const notes = performNotes(
|
|
634
|
+
track,
|
|
635
|
+
performedNotes(score).filter((note) => note.trackId === track.id),
|
|
636
|
+
tunedTiming(performance, score, track),
|
|
637
|
+
);
|
|
416
638
|
for (const voice of planSamplerVoices(track, notes, bank, timing))
|
|
417
639
|
latest = Math.max(latest, voice.end - loopEnd);
|
|
418
640
|
}
|
|
419
641
|
return latest / sampleRate;
|
|
420
642
|
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Seconds of the bar (a `c` cycle) holding `tick`: through the meter and
|
|
646
|
+
* the tempo map when the song has `time`, else the 0.4 arithmetic.
|
|
647
|
+
*/
|
|
648
|
+
function cycleSeconds(score: TrackScore, tick: number): number {
|
|
649
|
+
if (!score.time) return (score.beatsPerBar * 60) / score.tempoBpm;
|
|
650
|
+
const bar = barAt(score, tick);
|
|
651
|
+
return (
|
|
652
|
+
secondsAtTick(score, bar.tick + bar.barTicks) -
|
|
653
|
+
secondsAtTick(score, bar.tick)
|
|
654
|
+
);
|
|
655
|
+
}
|
package/src/audio/samples.ts
CHANGED
|
@@ -32,8 +32,10 @@ import {
|
|
|
32
32
|
isLocalTableSrc,
|
|
33
33
|
isWavetableInstrument,
|
|
34
34
|
type SampleRef,
|
|
35
|
+
type Track,
|
|
35
36
|
type TrackScore,
|
|
36
37
|
} from "../../core/score.ts";
|
|
38
|
+
import { GRANULAR_SOURCE_VOICE } from "../../core/granular.ts";
|
|
37
39
|
import { PackError, PackStore } from "./packs.ts";
|
|
38
40
|
import {
|
|
39
41
|
DEFAULT_MEMORY_CACHE_BYTES,
|
|
@@ -94,10 +96,35 @@ export function hasSamplerTracks(score: TrackScore): boolean {
|
|
|
94
96
|
Object.keys(track.sampler.voices).length > 0) ||
|
|
95
97
|
packWavetable(track) !== undefined ||
|
|
96
98
|
localWavetable(track) !== undefined ||
|
|
97
|
-
reverbIrSample(track) !== undefined
|
|
99
|
+
reverbIrSample(track) !== undefined ||
|
|
100
|
+
granularSample(track) !== undefined,
|
|
98
101
|
);
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A granular track's sample source (0.6): its `granular.src` when that is a
|
|
106
|
+
* SampleRef, loaded and sha256-pinned like a sampler voice under the voice
|
|
107
|
+
* name `granular:src`. A `synth:` source or no source needs no loading.
|
|
108
|
+
*/
|
|
109
|
+
export function granularSample(track: Track): SampleRef | undefined {
|
|
110
|
+
if (track.instrument !== "granular") return undefined;
|
|
111
|
+
const src = track.granular?.src;
|
|
112
|
+
return src !== undefined && typeof src !== "string" ? src : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The sample voices a track loads: sampler voices, then a granular source. */
|
|
116
|
+
function trackSampleRefs(track: Track): [string, SampleRef][] {
|
|
117
|
+
const refs: [string, SampleRef][] = [];
|
|
118
|
+
if (isSamplerInstrument(track.instrument) && track.sampler) {
|
|
119
|
+
const names = Object.keys(track.sampler.voices).sort();
|
|
120
|
+
for (const voice of names.slice(0, SCORE_LIMITS.maxSamplerVoices))
|
|
121
|
+
refs.push([voice, track.sampler.voices[voice]!]);
|
|
122
|
+
}
|
|
123
|
+
const granular = granularSample(track);
|
|
124
|
+
if (granular) refs.push([GRANULAR_SOURCE_VOICE, granular]);
|
|
125
|
+
return refs;
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
/** The project WAV a wavetable track plays, if any. */
|
|
102
129
|
export function localWavetable(
|
|
103
130
|
track: TrackScore["tracks"][number],
|
|
@@ -585,8 +612,7 @@ export class SampleLibrary implements SampleSource {
|
|
|
585
612
|
this.inUse = new Set();
|
|
586
613
|
const urls: string[] = [];
|
|
587
614
|
for (const track of score.tracks) {
|
|
588
|
-
|
|
589
|
-
for (const ref of Object.values(track.sampler.voices)) {
|
|
615
|
+
for (const [, ref] of trackSampleRefs(track)) {
|
|
590
616
|
if (ref.sha256) this.inUse.add(ref.sha256);
|
|
591
617
|
if (ref.url) urls.push(ref.url);
|
|
592
618
|
}
|
|
@@ -656,10 +682,7 @@ export class SampleLibrary implements SampleSource {
|
|
|
656
682
|
}
|
|
657
683
|
}
|
|
658
684
|
for (const track of score.tracks) {
|
|
659
|
-
|
|
660
|
-
const names = Object.keys(track.sampler.voices).sort();
|
|
661
|
-
for (const voice of names.slice(0, SCORE_LIMITS.maxSamplerVoices)) {
|
|
662
|
-
const ref = track.sampler.voices[voice]!;
|
|
685
|
+
for (const [voice, ref] of trackSampleRefs(track)) {
|
|
663
686
|
let src = ref.src;
|
|
664
687
|
if (src.startsWith(PACK_PREFIX)) {
|
|
665
688
|
try {
|