@hraness/dawg 0.4.1 → 0.5.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 +70 -0
- package/DAWG.md +293 -31
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +37 -12
- package/core/expression.ts +1241 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +536 -56
- package/core/sdk/eval-child.ts +38 -3
- package/core/sdk/print.ts +445 -14
- package/core/sdk/v1.ts +1709 -23
- package/core/sections.ts +2046 -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 +27 -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 +25 -0
- package/guides/tempo.md +26 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +2 -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/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +29 -4
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +21 -3
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +36 -2
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +2 -0
- package/src/audio/arrange.ts +471 -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/effects/bus.ts +5 -4
- 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/space.ts +92 -2
- package/src/audio/engine.ts +90 -20
- package/src/audio/live.ts +33 -6
- package/src/audio/loudness.ts +551 -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 +74 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/sampler.ts +108 -19
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +86 -0
- package/src/audio/wav.ts +301 -26
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +934 -0
- package/src/commands/help.ts +281 -24
- package/src/commands/master.ts +361 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +964 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +709 -33
- package/src/render.ts +109 -7
- 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/menu-time.ts +401 -0
- package/src/tui/menu.ts +635 -14
- 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 +414 -41
- 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/synth/voice.ts
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* of the score and the note, so cold, cached and worker renders agree.
|
|
20
20
|
*/
|
|
21
21
|
import type { Note, Track } from "../../../core/score.ts";
|
|
22
|
+
import type { PerformedNote } from "../../../core/expression.ts";
|
|
22
23
|
import type { FxLane } from "../../../core/fx.ts";
|
|
24
|
+
import { noteHz, type TuningTable } from "../../../core/tuning.ts";
|
|
23
25
|
import {
|
|
24
26
|
LEGACY_SOUNDS,
|
|
25
27
|
SOUND_ALIASES,
|
|
@@ -78,6 +80,10 @@ export type VoiceContext = Readonly<{
|
|
|
78
80
|
* (a wavetable track's table); otherwise the sound's resolver is used.
|
|
79
81
|
*/
|
|
80
82
|
oscillatorFor?: (note: Note) => OscillatorFactory | undefined;
|
|
83
|
+
/** The track's merged tuning (`resolveTuning`); absent is 12-TET. */
|
|
84
|
+
tuning?: TuningTable;
|
|
85
|
+
/** Ticks added to a note's start in its noise seed (`RenderOptions`). */
|
|
86
|
+
seedTick?: number;
|
|
81
87
|
}>;
|
|
82
88
|
|
|
83
89
|
/** Track gain at a tick (volume × volume lane), supplied by the renderer. */
|
|
@@ -258,11 +264,16 @@ function noteFilters(
|
|
|
258
264
|
c: Controls,
|
|
259
265
|
gate: number,
|
|
260
266
|
sampleRate: number,
|
|
267
|
+
accent = false,
|
|
261
268
|
): (() => NoteFilter)[] {
|
|
262
269
|
const out: (() => NoteFilter)[] = [];
|
|
263
270
|
for (const { prefix, type, param } of FILTERS) {
|
|
264
|
-
const
|
|
265
|
-
if (!c.has(param) &&
|
|
271
|
+
const written = c.number(`${prefix}env`);
|
|
272
|
+
if (!c.has(param) && written === 0) continue;
|
|
273
|
+
// TB-303 accent: an accented note opens the low-pass further and its
|
|
274
|
+
// filter envelope decays faster.
|
|
275
|
+
const boost = accent && prefix === "lp" ? c.number("faccent") : 0;
|
|
276
|
+
const depth = written + boost;
|
|
266
277
|
const cutoff = c.number(param);
|
|
267
278
|
const q = c.number(`${prefix}q`);
|
|
268
279
|
const slope = type === "lpf" ? c.text("ftype") : "12db";
|
|
@@ -276,7 +287,15 @@ function noteFilters(
|
|
|
276
287
|
q,
|
|
277
288
|
depth,
|
|
278
289
|
anchor,
|
|
279
|
-
|
|
290
|
+
boost > 0
|
|
291
|
+
? new Envelope(
|
|
292
|
+
c.number(`${prefix}attack`),
|
|
293
|
+
c.number(`${prefix}decay`) / 2,
|
|
294
|
+
c.number(`${prefix}sustain`),
|
|
295
|
+
c.number(`${prefix}release`),
|
|
296
|
+
gate,
|
|
297
|
+
)
|
|
298
|
+
: envelopeFor(c, prefix, "", gate),
|
|
280
299
|
sampleRate,
|
|
281
300
|
),
|
|
282
301
|
);
|
|
@@ -343,13 +362,25 @@ export function renderSynthNote(
|
|
|
343
362
|
start + length + Math.ceil(release * sampleRate),
|
|
344
363
|
);
|
|
345
364
|
if (end <= start) return;
|
|
346
|
-
const random = seededRandom(
|
|
365
|
+
const random = seededRandom(
|
|
366
|
+
`${note.id}:${note.startTick + (context.seedTick ?? 0)}:synth`,
|
|
367
|
+
);
|
|
347
368
|
const amp = envelopeFor(c, "", "", gate);
|
|
348
369
|
const velocity = clamp(note.velocity, 0, 1);
|
|
349
370
|
const gain = c.number("gain");
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
const
|
|
371
|
+
// Note expression (core/expression.ts): absent for plain notes, which
|
|
372
|
+
// keeps their output byte-identical.
|
|
373
|
+
const performance = (note as PerformedNote).performance;
|
|
374
|
+
const cents = performance?.cents;
|
|
375
|
+
const damp = performance?.damp;
|
|
376
|
+
const dampAt = (t: number): number =>
|
|
377
|
+
damp && t > damp.from ? Math.exp(-(t - damp.from) / damp.tau) : 1;
|
|
378
|
+
|
|
379
|
+
// Pitch: the tuning table's key frequency and the note's cents (exactly
|
|
380
|
+
// 440 · 2^((pitch − 69)/12) without either). A key a keyboard mapping
|
|
381
|
+
// leaves unmapped is silent.
|
|
382
|
+
const base = noteHz(note.pitch, note.cents, context.tuning);
|
|
383
|
+
if (!(base > 0)) return;
|
|
353
384
|
|
|
354
385
|
if (isZzfxSound(sound) && !context.oscillatorFor) {
|
|
355
386
|
const count = end - start;
|
|
@@ -371,28 +402,37 @@ export function renderSynthNote(
|
|
|
371
402
|
zdelay: c.number("zdelay"),
|
|
372
403
|
tremolo: c.number("tremolo"),
|
|
373
404
|
random,
|
|
405
|
+
...(cents ? { cents } : {}),
|
|
406
|
+
...(performance?.replaceSlide ? { slideOff: true } : {}),
|
|
374
407
|
});
|
|
375
|
-
const filters = noteFilters(
|
|
408
|
+
const filters = noteFilters(
|
|
409
|
+
c,
|
|
410
|
+
gate,
|
|
411
|
+
sampleRate,
|
|
412
|
+
performance?.accent === true,
|
|
413
|
+
).map((make) => make());
|
|
376
414
|
for (let elapsed = 0; elapsed < count; elapsed += 1) {
|
|
377
415
|
const t = elapsed / sampleRate;
|
|
378
416
|
if (elapsed % CONTROL_SAMPLES === 0)
|
|
379
417
|
for (const filter of filters) filter.update(t);
|
|
380
418
|
let value = raw[elapsed]!;
|
|
381
419
|
for (const filter of filters) value = filter.process(value);
|
|
382
|
-
|
|
420
|
+
let level =
|
|
383
421
|
amp.at(t) *
|
|
384
422
|
velocity *
|
|
385
423
|
gain *
|
|
386
424
|
VOICE_LEVEL *
|
|
387
425
|
gainAt(note.startTick + elapsed / samplesPerTick);
|
|
426
|
+
if (damp) level *= dampAt(t);
|
|
388
427
|
left[start + elapsed]! += value * level;
|
|
389
428
|
if (right) right[start + elapsed]! += value * level;
|
|
390
429
|
}
|
|
391
430
|
return;
|
|
392
431
|
}
|
|
393
|
-
|
|
432
|
+
// A note's own vibrato or bend replaces the synth's (note over track).
|
|
433
|
+
const vib = performance?.replaceVibrato ? 0 : c.number("vib");
|
|
394
434
|
const vibmod = c.number("vibmod");
|
|
395
|
-
const penv = c.number("penv");
|
|
435
|
+
const penv = performance?.replacePitchEnvelope ? 0 : c.number("penv");
|
|
396
436
|
const pitchEnv =
|
|
397
437
|
penv === 0
|
|
398
438
|
? undefined
|
|
@@ -463,7 +503,12 @@ export function renderSynthNote(
|
|
|
463
503
|
|
|
464
504
|
const fm = fmOperators(c, gate);
|
|
465
505
|
const fmPhases = fm.map(() => new Float64Array(unison));
|
|
466
|
-
const filterMakers = noteFilters(
|
|
506
|
+
const filterMakers = noteFilters(
|
|
507
|
+
c,
|
|
508
|
+
gate,
|
|
509
|
+
sampleRate,
|
|
510
|
+
performance?.accent === true,
|
|
511
|
+
);
|
|
467
512
|
const filtersL = filterMakers.map((make) => make());
|
|
468
513
|
const filtersR = right ? filterMakers.map((make) => make()) : [];
|
|
469
514
|
|
|
@@ -483,6 +528,7 @@ export function renderSynthNote(
|
|
|
483
528
|
if (pcurve >= 1) e = e * e * e;
|
|
484
529
|
semitones += penv * (e - panchor);
|
|
485
530
|
}
|
|
531
|
+
if (cents) semitones += cents(t) / 100;
|
|
486
532
|
const frequency = semitones === 0 ? base : base * 2 ** (semitones / 12);
|
|
487
533
|
if (pwsweep > 0)
|
|
488
534
|
width = clamp(
|
|
@@ -522,12 +568,13 @@ export function renderSynthNote(
|
|
|
522
568
|
}
|
|
523
569
|
for (const filter of filtersL) sampleL = filter.process(sampleL);
|
|
524
570
|
for (const filter of filtersR) sampleR = filter.process(sampleR);
|
|
525
|
-
|
|
571
|
+
let level =
|
|
526
572
|
amp.at(t) *
|
|
527
573
|
velocity *
|
|
528
574
|
gain *
|
|
529
575
|
VOICE_LEVEL *
|
|
530
576
|
gainAt(note.startTick + elapsed / samplesPerTick);
|
|
577
|
+
if (damp) level *= dampAt(t);
|
|
531
578
|
left[index]! += sampleL * level;
|
|
532
579
|
if (right) right[index]! += sampleR * level;
|
|
533
580
|
}
|
package/src/audio/synth/zzfx.ts
CHANGED
|
@@ -53,6 +53,10 @@ export type ZzfxParams = Readonly<{
|
|
|
53
53
|
zdelay: number;
|
|
54
54
|
tremolo: number;
|
|
55
55
|
random: () => number;
|
|
56
|
+
/** Note expression: a pitch offset in cents at `t` seconds. */
|
|
57
|
+
cents?: (t: number) => number;
|
|
58
|
+
/** The note glides, so `slide` and `deltaSlide` are ignored. */
|
|
59
|
+
slideOff?: boolean;
|
|
56
60
|
}>;
|
|
57
61
|
|
|
58
62
|
function wave(sound: string, phase: number, held: number): number {
|
|
@@ -98,14 +102,16 @@ export function renderZzfx(
|
|
|
98
102
|
const t = index / sampleRate;
|
|
99
103
|
// Slide and pitch jump restart every `lfo` seconds.
|
|
100
104
|
const local = params.lfo > 0 ? t % params.lfo : t;
|
|
101
|
-
let frequency =
|
|
102
|
-
base
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
let frequency = params.slideOff
|
|
106
|
+
? base
|
|
107
|
+
: base +
|
|
108
|
+
500 * params.slide * local +
|
|
109
|
+
250 * params.deltaSlide * local * local;
|
|
105
110
|
if (params.pitchJumpTime > 0 && local >= params.pitchJumpTime)
|
|
106
111
|
frequency += params.pitchJump;
|
|
107
112
|
if (params.zmod > 0)
|
|
108
113
|
frequency *= 1 + 0.5 * Math.sin(2 * Math.PI * params.zmod * t);
|
|
114
|
+
if (params.cents) frequency *= 2 ** (params.cents(t) / 1200);
|
|
109
115
|
frequency = clamp(frequency, 0, sampleRate / 2);
|
|
110
116
|
let increment = frequency / sampleRate;
|
|
111
117
|
if (params.noise > 0) increment *= 1 + params.noise * (random() * 2 - 1);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample positions through a song's tempo map (core/tempo.ts).
|
|
3
|
+
*
|
|
4
|
+
* Renderers keep their 0.4 arithmetic (`tick / samplesPerTick`) whenever
|
|
5
|
+
* `warp` is absent, which is every score without tempo events or
|
|
6
|
+
* fermatas, so those renders stay byte-identical. With a map, every tick
|
|
7
|
+
* to sample conversion goes through it instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { timeMapFor, type TimeScore } from "../../core/tempo.ts";
|
|
11
|
+
|
|
12
|
+
export type SampleWarp = Readonly<{
|
|
13
|
+
/** Fractional sample index where `tick` sounds. */
|
|
14
|
+
sample(tick: number): number;
|
|
15
|
+
/** Score tick sounding at sample `index`. */
|
|
16
|
+
tick(index: number): number;
|
|
17
|
+
/** Tempo at `tick`. */
|
|
18
|
+
bpm(tick: number): number;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export function sampleWarpFor(
|
|
22
|
+
score: TimeScore,
|
|
23
|
+
sampleRate: number,
|
|
24
|
+
): SampleWarp | undefined {
|
|
25
|
+
const map = timeMapFor(score);
|
|
26
|
+
if (!map) return undefined;
|
|
27
|
+
// Per-sample callers (automation, effects, sampler positions) read
|
|
28
|
+
// `tick` once per sample: evaluate the map once per block and interpolate
|
|
29
|
+
// between block edges. That is exact inside constant-tempo stretches,
|
|
30
|
+
// within a hair inside a ramp, and at most one block's ticks off across
|
|
31
|
+
// a step or fermata edge (about a tick at 48 kHz), for automation and
|
|
32
|
+
// modulation reads only: note onsets use `sample`, which stays exact.
|
|
33
|
+
let grid = new Float64Array(0);
|
|
34
|
+
let known = new Uint8Array(0);
|
|
35
|
+
const edge = (block: number): number => {
|
|
36
|
+
if (block >= grid.length) {
|
|
37
|
+
const size = Math.max(block + 1, grid.length * 2, 1024);
|
|
38
|
+
const nextGrid = new Float64Array(size);
|
|
39
|
+
const nextKnown = new Uint8Array(size);
|
|
40
|
+
nextGrid.set(grid);
|
|
41
|
+
nextKnown.set(known);
|
|
42
|
+
grid = nextGrid;
|
|
43
|
+
known = nextKnown;
|
|
44
|
+
}
|
|
45
|
+
if (known[block] === 0) {
|
|
46
|
+
grid[block] = map.tick((block * WARP_BLOCK) / sampleRate);
|
|
47
|
+
known[block] = 1;
|
|
48
|
+
}
|
|
49
|
+
return grid[block]!;
|
|
50
|
+
};
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
sample: (tick: number) => map.seconds(tick) * sampleRate,
|
|
53
|
+
tick: (index: number) => {
|
|
54
|
+
if (!(index >= 0) || index > MAX_GRID_SAMPLES)
|
|
55
|
+
return map.tick(index / sampleRate);
|
|
56
|
+
const block = Math.floor(index / WARP_BLOCK);
|
|
57
|
+
const into = index - block * WARP_BLOCK;
|
|
58
|
+
const from = edge(block);
|
|
59
|
+
if (into === 0) return from;
|
|
60
|
+
return from + ((edge(block + 1) - from) * into) / WARP_BLOCK;
|
|
61
|
+
},
|
|
62
|
+
bpm: (tick: number) => map.bpm(tick),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Samples between exact `tick` evaluations; ticks in between interpolate. */
|
|
67
|
+
export const WARP_BLOCK = 64;
|
|
68
|
+
/** Past this sample index (about 3 h at 48 kHz) `tick` evaluates directly. */
|
|
69
|
+
const MAX_GRID_SAMPLES = 2 ** 29;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Start sample and length of a note through a warp: a note inside a
|
|
73
|
+
* fermata beat stretches with it.
|
|
74
|
+
*/
|
|
75
|
+
export function warpedSpan(
|
|
76
|
+
warp: SampleWarp,
|
|
77
|
+
startTick: number,
|
|
78
|
+
durationTicks: number,
|
|
79
|
+
): { start: number; length: number } {
|
|
80
|
+
const from = warp.sample(startTick);
|
|
81
|
+
const to = warp.sample(startTick + durationTicks);
|
|
82
|
+
return {
|
|
83
|
+
start: Math.max(0, Math.floor(from)),
|
|
84
|
+
length: Math.max(1, Math.floor(to - from)),
|
|
85
|
+
};
|
|
86
|
+
}
|