@hraness/dawg 0.3.0 → 0.4.1
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 +138 -0
- package/DAWG.md +608 -39
- package/README.md +4 -4
- package/core/chords.ts +1724 -0
- package/core/diff.ts +13 -1
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +735 -18
- package/core/sdk/eval-child.ts +34 -22
- package/core/sdk/eval.ts +4 -1
- package/core/sdk/print.ts +304 -22
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +3226 -21
- package/core/synth.ts +1001 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +38 -3
- package/src/agent/brief.ts +29 -4
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +17 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/tools.ts +308 -14
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +109 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +339 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +394 -0
- package/src/audio/engine.ts +112 -4
- package/src/audio/kits.ts +200 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/preview.ts +481 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/sampler.ts +157 -29
- package/src/audio/samples.ts +386 -44
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +411 -295
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +26 -4
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +298 -10
- package/src/commands/music.ts +10 -8
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +162 -2
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +1222 -30
- package/src/media/cli.ts +15 -2
- package/src/media/tools.ts +106 -1
- package/src/media/wavetable.ts +202 -0
- package/src/render.ts +22 -1
- package/src/session/naming.ts +3 -1
- package/src/tui/audition.ts +521 -0
- package/src/tui/euclid.ts +516 -0
- package/src/tui/menu.ts +1305 -244
- package/src/tui/play-chords.ts +630 -0
- package/src/tui/play-session.ts +364 -16
- package/src/tui/sketch.ts +108 -0
- package/src/web/search.ts +4 -1
- package/tui/app.ts +161 -39
- package/tui/grammar.ts +286 -0
- package/tui/highway.ts +7 -1
- package/tui/play-strip.ts +59 -14
package/src/audio/sampler.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* - `gain` scales the voice; velocity and track volume apply as for synths.
|
|
8
8
|
* - `choke` is Strudel's `cut`: a new hit in the same group stops the sounding
|
|
9
9
|
* voice with a short fade.
|
|
10
|
+
* - `loopBegin`/`loopEnd` loop a part of the window; `clip` (`legato`) cuts
|
|
11
|
+
* the voice at the note's length times the factor; `unit` "c"/"s" and
|
|
12
|
+
* `fit` turn `speed` into a duration (cycles = bars, seconds, the note);
|
|
13
|
+
* `accelerate` ramps the rate; `squiz` raises pitch per zero-crossing
|
|
14
|
+
* cycle (described in Tidal/SuperDirt docs; implemented here from that
|
|
15
|
+
* description).
|
|
10
16
|
* - keyed mode repitches from `root` (rate × 2^((pitch − root)/12)) and holds
|
|
11
17
|
* for the note's length; oneshot mode maps voices to pitch slots from 36 in
|
|
12
18
|
* voice-name order and plays the whole window unless `loop` is set.
|
|
@@ -61,8 +67,34 @@ export type SamplerVoice = {
|
|
|
61
67
|
readonly regionEnd: number;
|
|
62
68
|
readonly gain: number;
|
|
63
69
|
readonly choke: string | undefined;
|
|
70
|
+
/** Loop part as travel offsets from the playback start (begin, or end reversed). */
|
|
71
|
+
readonly loopStart: number;
|
|
72
|
+
readonly loopSpan: number;
|
|
73
|
+
/** Rate ramp per output frame (`accelerate` / reference length); 0 is off. */
|
|
74
|
+
readonly ramp: number;
|
|
75
|
+
/** `squiz` ratio; 1 is off. */
|
|
76
|
+
readonly squiz: number;
|
|
64
77
|
};
|
|
65
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Source frames travelled after `elapsed` output frames: `step·(e + k·e²/2)`
|
|
81
|
+
* with the rate held at zero once a negative ramp reaches it.
|
|
82
|
+
*/
|
|
83
|
+
function travelAt(elapsed: number, step: number, ramp: number): number {
|
|
84
|
+
if (ramp === 0) return elapsed * step;
|
|
85
|
+
const e = ramp < 0 ? Math.min(elapsed, -1 / ramp) : elapsed;
|
|
86
|
+
return step * (e + (ramp * e * e) / 2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Output frames until `distance` source frames are travelled (Infinity if never). */
|
|
90
|
+
function framesToTravel(distance: number, step: number, ramp: number): number {
|
|
91
|
+
if (ramp === 0) return distance / step;
|
|
92
|
+
const d = distance / step;
|
|
93
|
+
const disc = 1 + 2 * ramp * d;
|
|
94
|
+
if (disc < 0) return Infinity;
|
|
95
|
+
return (Math.sqrt(disc) - 1) / ramp;
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function noteStartFrame(note: Note, timing: SamplerTiming): number {
|
|
67
99
|
const { score, sampleRate } = timing;
|
|
68
100
|
return Math.max(
|
|
@@ -148,26 +180,64 @@ export function planSamplerVoices(
|
|
|
148
180
|
const sample = bank.voices.get(sampleKey(track.id, target.voice));
|
|
149
181
|
if (!ref || !sample) continue;
|
|
150
182
|
const speed = ref.speed ?? 1;
|
|
151
|
-
const step =
|
|
152
|
-
((Math.abs(speed) * sample.sampleRate) / sampleRate) * target.ratio;
|
|
153
183
|
const regionStart = (ref.begin ?? 0) * sample.frames;
|
|
154
184
|
const regionEnd = (ref.end ?? 1) * sample.frames;
|
|
155
|
-
const natural = Math.max(1, Math.floor((regionEnd - regionStart) / step));
|
|
156
185
|
const held = noteLengthFrames(note, timing);
|
|
186
|
+
// `fit`, `unit: "c"` and `unit: "s"` give the window a duration; the
|
|
187
|
+
// keyed ratio still repitches on top.
|
|
188
|
+
const unit = ref.unit ?? "r";
|
|
189
|
+
const seconds = ref.fit
|
|
190
|
+
? held / sampleRate
|
|
191
|
+
: unit === "c"
|
|
192
|
+
? (timing.score.beatsPerBar * 60) /
|
|
193
|
+
timing.score.tempoBpm /
|
|
194
|
+
Math.abs(speed)
|
|
195
|
+
: unit === "s"
|
|
196
|
+
? Math.abs(speed)
|
|
197
|
+
: undefined;
|
|
198
|
+
const step =
|
|
199
|
+
seconds === undefined
|
|
200
|
+
? ((Math.abs(speed) * sample.sampleRate) / sampleRate) * target.ratio
|
|
201
|
+
: ((regionEnd - regionStart) / (seconds * sampleRate)) * target.ratio;
|
|
202
|
+
const natural = Math.max(1, Math.floor((regionEnd - regionStart) / step));
|
|
157
203
|
const release = Math.max(1, Math.round(RELEASE_SECONDS * sampleRate));
|
|
158
204
|
const loop = ref.loop === true;
|
|
205
|
+
const clipped =
|
|
206
|
+
ref.clip === undefined ? held : Math.max(1, Math.floor(held * ref.clip));
|
|
159
207
|
let length: number;
|
|
160
208
|
let fade: number;
|
|
161
209
|
if (loop) {
|
|
162
|
-
length =
|
|
210
|
+
length = clipped + release;
|
|
163
211
|
fade = release;
|
|
164
|
-
} else if (
|
|
165
|
-
|
|
212
|
+
} else if (
|
|
213
|
+
(sampler.mode === "keyed" || ref.clip !== undefined) &&
|
|
214
|
+
clipped + release < natural
|
|
215
|
+
) {
|
|
216
|
+
length = clipped + release;
|
|
166
217
|
fade = release;
|
|
167
218
|
} else {
|
|
168
219
|
length = natural;
|
|
169
220
|
fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
|
|
170
221
|
}
|
|
222
|
+
// accelerate: the rate ramps by `accelerate`× over the planned length;
|
|
223
|
+
// a non-looping voice ends where the ramped read runs out (or stops).
|
|
224
|
+
const accelerate = ref.accelerate ?? 0;
|
|
225
|
+
const ramp = accelerate === 0 ? 0 : accelerate / length;
|
|
226
|
+
if (ramp !== 0 && !loop) {
|
|
227
|
+
const reach = Math.min(
|
|
228
|
+
framesToTravel(regionEnd - regionStart, step, ramp),
|
|
229
|
+
ramp < 0 ? -1 / ramp : Infinity,
|
|
230
|
+
);
|
|
231
|
+
const until = Math.max(1, Math.floor(reach));
|
|
232
|
+
if (until < length) {
|
|
233
|
+
length = until;
|
|
234
|
+
fade = Math.max(1, Math.round(END_FADE_SECONDS * sampleRate));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const loopFrom =
|
|
238
|
+
ref.loopBegin === undefined ? regionStart : ref.loopBegin * sample.frames;
|
|
239
|
+
const loopTo =
|
|
240
|
+
ref.loopEnd === undefined ? regionEnd : ref.loopEnd * sample.frames;
|
|
171
241
|
const start = noteStartFrame(note, timing);
|
|
172
242
|
const voice: SamplerVoice = {
|
|
173
243
|
voice: target.voice,
|
|
@@ -183,6 +253,10 @@ export function planSamplerVoices(
|
|
|
183
253
|
regionEnd,
|
|
184
254
|
gain: (ref.gain ?? 1) * Math.max(0, Math.min(1, note.velocity)),
|
|
185
255
|
choke: ref.choke,
|
|
256
|
+
loopStart: speed < 0 ? regionEnd - loopTo : loopFrom - regionStart,
|
|
257
|
+
loopSpan: loopTo - loopFrom,
|
|
258
|
+
ramp,
|
|
259
|
+
squiz: ref.squiz ?? 1,
|
|
186
260
|
};
|
|
187
261
|
if (voice.choke !== undefined) {
|
|
188
262
|
const previous = lastInGroup.get(voice.choke);
|
|
@@ -225,38 +299,44 @@ export function renderSamplerVoices(
|
|
|
225
299
|
const attack = Math.max(1, ATTACK_SECONDS * sampleRate);
|
|
226
300
|
for (const voice of voices) {
|
|
227
301
|
const { sample, step, regionStart, regionEnd, reverse, loop } = voice;
|
|
302
|
+
const { loopStart, loopSpan, ramp } = voice;
|
|
228
303
|
const mono = sample.mono;
|
|
229
304
|
const span = regionEnd - regionStart;
|
|
230
305
|
const crossfade = loop
|
|
231
|
-
? Math.min(LOOP_CROSSFADE_SECONDS * sample.sampleRate,
|
|
306
|
+
? Math.min(LOOP_CROSSFADE_SECONDS * sample.sampleRate, loopSpan / 4)
|
|
232
307
|
: 0;
|
|
233
|
-
const period =
|
|
308
|
+
const period = loopSpan - crossfade;
|
|
234
309
|
const end = Math.min(target.length, voice.end);
|
|
235
310
|
const fadeFrom = voice.end - voice.fade;
|
|
236
311
|
const level = SAMPLE_LEVEL * voice.gain;
|
|
312
|
+
const at = (offset: number) =>
|
|
313
|
+
readAt(mono, reverse ? regionEnd - 1 - offset : regionStart + offset);
|
|
314
|
+
// The raw read at an elapsed frame, or undefined past a one-shot's end.
|
|
315
|
+
const read = (elapsed: number): number | undefined => {
|
|
316
|
+
const travelled =
|
|
317
|
+
ramp === 0 ? elapsed * step : travelAt(elapsed, step, ramp);
|
|
318
|
+
if (!loop) return travelled >= span ? undefined : at(travelled);
|
|
319
|
+
if (travelled < loopStart) return at(travelled);
|
|
320
|
+
const into = travelled - loopStart;
|
|
321
|
+
const phase =
|
|
322
|
+
into < crossfade || period <= 0
|
|
323
|
+
? into % Math.max(loopSpan, 1e-9)
|
|
324
|
+
: crossfade + ((into - crossfade) % period);
|
|
325
|
+
let value = at(loopStart + phase);
|
|
326
|
+
if (crossfade > 0 && phase >= period) {
|
|
327
|
+
const mix = (phase - period) / crossfade;
|
|
328
|
+
value = value * (1 - mix) + at(loopStart + phase - period) * mix;
|
|
329
|
+
}
|
|
330
|
+
return value;
|
|
331
|
+
};
|
|
332
|
+
const squizzed =
|
|
333
|
+
voice.squiz > 1
|
|
334
|
+
? squizBuffer(read, end - voice.start, voice.squiz)
|
|
335
|
+
: undefined;
|
|
237
336
|
for (let index = voice.start; index < end; index += 1) {
|
|
238
337
|
const elapsed = index - voice.start;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
if (!loop) {
|
|
242
|
-
if (travelled >= span) break;
|
|
243
|
-
value = readAt(
|
|
244
|
-
mono,
|
|
245
|
-
reverse ? regionEnd - 1 - travelled : regionStart + travelled,
|
|
246
|
-
);
|
|
247
|
-
} else {
|
|
248
|
-
const phase =
|
|
249
|
-
travelled < crossfade || period <= 0
|
|
250
|
-
? travelled % Math.max(span, 1e-9)
|
|
251
|
-
: crossfade + ((travelled - crossfade) % period);
|
|
252
|
-
const at = (offset: number) =>
|
|
253
|
-
readAt(mono, reverse ? regionEnd - 1 - offset : regionStart + offset);
|
|
254
|
-
value = at(phase);
|
|
255
|
-
if (crossfade > 0 && phase >= period) {
|
|
256
|
-
const mix = (phase - period) / crossfade;
|
|
257
|
-
value = value * (1 - mix) + at(phase - period) * mix;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
338
|
+
const value = squizzed ? squizzed[elapsed]! : read(elapsed);
|
|
339
|
+
if (value === undefined) break;
|
|
260
340
|
let envelope = Math.min(1, elapsed / attack);
|
|
261
341
|
if (index >= fadeFrom) envelope *= (voice.end - index) / voice.fade;
|
|
262
342
|
target[index]! +=
|
|
@@ -268,6 +348,54 @@ export function renderSamplerVoices(
|
|
|
268
348
|
}
|
|
269
349
|
}
|
|
270
350
|
|
|
351
|
+
/** Longest zero-crossing cycle `squiz` treats as one (longer runs split). */
|
|
352
|
+
const SQUIZ_MAX_CYCLE = 4096;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Squiz (Tidal/SuperDirt's description: a simplistic pitch raiser): cut the
|
|
356
|
+
* voice at its upward zero crossings and play each cycle `ratio`× faster,
|
|
357
|
+
* repeating it to fill the cycle's original length. Length is kept, the
|
|
358
|
+
* pitch rises, and the timbre turns buzzy, as documented.
|
|
359
|
+
*/
|
|
360
|
+
export function squizBuffer(
|
|
361
|
+
read: (elapsed: number) => number | undefined,
|
|
362
|
+
length: number,
|
|
363
|
+
ratio: number,
|
|
364
|
+
): Float64Array {
|
|
365
|
+
const raw = new Float64Array(Math.max(0, length));
|
|
366
|
+
let filled = raw.length;
|
|
367
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
368
|
+
const value = read(i);
|
|
369
|
+
if (value === undefined) {
|
|
370
|
+
filled = i;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
raw[i] = value;
|
|
374
|
+
}
|
|
375
|
+
const out = new Float64Array(raw.length);
|
|
376
|
+
let start = 0;
|
|
377
|
+
while (start < filled) {
|
|
378
|
+
let stop = start + 1;
|
|
379
|
+
while (
|
|
380
|
+
stop < filled &&
|
|
381
|
+
stop - start < SQUIZ_MAX_CYCLE &&
|
|
382
|
+
!(raw[stop - 1]! < 0 && raw[stop]! >= 0)
|
|
383
|
+
)
|
|
384
|
+
stop += 1;
|
|
385
|
+
const size = stop - start;
|
|
386
|
+
for (let i = 0; i < size; i += 1) {
|
|
387
|
+
const position = (i * ratio) % size;
|
|
388
|
+
const base = Math.floor(position);
|
|
389
|
+
const next = base + 1 < size ? base + 1 : 0;
|
|
390
|
+
const frac = position - base;
|
|
391
|
+
out[start + i] =
|
|
392
|
+
raw[start + base]! + (raw[start + next]! - raw[start + base]!) * frac;
|
|
393
|
+
}
|
|
394
|
+
start = stop;
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
271
399
|
/**
|
|
272
400
|
* Seconds a sampler track rings past the loop end (bounded by the caller),
|
|
273
401
|
* so loop renders fold whole samples onto the start.
|