@hraness/dawg 0.6.1 → 0.7.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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +160 -28
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +759 -177
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +47 -7
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/core/sections.ts
CHANGED
|
@@ -13,9 +13,19 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { DRUM_VOICES, isDrumInstrument } from "./drums.ts";
|
|
15
15
|
import { FX_LANES } from "./fx.ts";
|
|
16
|
+
import { synthKit } from "./kits.ts";
|
|
16
17
|
import { resolveString } from "./strings.ts";
|
|
17
18
|
import { modalSettings, windSettings } from "./resonators.ts";
|
|
18
|
-
import {
|
|
19
|
+
import { resolveSing } from "./sing.ts";
|
|
20
|
+
import { slicePedals, type PedalEvent, type PedalState } from "./expression.ts";
|
|
21
|
+
import {
|
|
22
|
+
bakeClipTime,
|
|
23
|
+
copyClipBars,
|
|
24
|
+
deleteClipBars,
|
|
25
|
+
insertClipBars,
|
|
26
|
+
sliceClips,
|
|
27
|
+
type ClipPiece,
|
|
28
|
+
} from "./clips.ts";
|
|
19
29
|
import {
|
|
20
30
|
SCORE_LIMITS,
|
|
21
31
|
withNoteCap,
|
|
@@ -23,6 +33,7 @@ import {
|
|
|
23
33
|
TrackScore,
|
|
24
34
|
isSamplerInstrument,
|
|
25
35
|
wavetableOf,
|
|
36
|
+
type AudioClip,
|
|
26
37
|
type AutomationPoint,
|
|
27
38
|
type FormEntry,
|
|
28
39
|
type Note,
|
|
@@ -32,10 +43,10 @@ import {
|
|
|
32
43
|
} from "./score.ts";
|
|
33
44
|
import {
|
|
34
45
|
barStartTick,
|
|
35
|
-
bpmAtTick,
|
|
36
46
|
hasTrackTime,
|
|
37
47
|
normalizeSongTime,
|
|
38
48
|
performedNotes,
|
|
49
|
+
timeMapFor,
|
|
39
50
|
type Fermata,
|
|
40
51
|
type SongTime,
|
|
41
52
|
type TempoEvent,
|
|
@@ -61,6 +72,11 @@ export function sliceSongTime(
|
|
|
61
72
|
};
|
|
62
73
|
const clampBpm = (bpm: number) =>
|
|
63
74
|
Math.min(SCORE_LIMITS.maxTempoBpm, Math.max(SCORE_LIMITS.minTempoBpm, bpm));
|
|
75
|
+
// The written tempo: a fermata's hold belongs to its own beat and moves
|
|
76
|
+
// with it, so it must not leak into the tempo restated at a seam.
|
|
77
|
+
const map = timeMapFor(score);
|
|
78
|
+
const bpmAtTick = (_score: TrackScore, tick: number) =>
|
|
79
|
+
map ? map.tempoAt(tick) : score.tempoBpm;
|
|
64
80
|
const sorted = [...pieces].sort((a, b) => a.offset - b.offset);
|
|
65
81
|
const first = sorted[0];
|
|
66
82
|
const tempoBpm = clampBpm(
|
|
@@ -147,7 +163,10 @@ export function bakeTrackTime(
|
|
|
147
163
|
() =>
|
|
148
164
|
new TrackScore({
|
|
149
165
|
...score.toJSON(),
|
|
150
|
-
tracks: score.tracks.map((
|
|
166
|
+
tracks: score.tracks.map((track) => {
|
|
167
|
+
const { time: _time, ...rest } = bakeClipTime(track);
|
|
168
|
+
return rest;
|
|
169
|
+
}),
|
|
151
170
|
notes,
|
|
152
171
|
}),
|
|
153
172
|
);
|
|
@@ -394,8 +413,73 @@ export function applySectionChanges(score: TrackScore): TrackScore {
|
|
|
394
413
|
}
|
|
395
414
|
notes.push(cut);
|
|
396
415
|
}
|
|
416
|
+
// Clips (0.7): a section that mutes a track silences its clips there, its
|
|
417
|
+
// variation gain scales them; a clip crossing such an edge is cut.
|
|
418
|
+
const total = Math.max(
|
|
419
|
+
score.bars,
|
|
420
|
+
...score.sections.map((s) => s.startBar + s.bars),
|
|
421
|
+
);
|
|
422
|
+
const clipTracks = score.tracks.map((track) => {
|
|
423
|
+
if (!track.clips) return track;
|
|
424
|
+
const pieces: ClipPiece[] = [];
|
|
425
|
+
for (let bar = 0; bar < total; bar += 1) {
|
|
426
|
+
const section = sectionAtBar(score, bar);
|
|
427
|
+
pieces.push({
|
|
428
|
+
from: bar * ticks,
|
|
429
|
+
to: (bar + 1) * ticks,
|
|
430
|
+
offset: bar * ticks,
|
|
431
|
+
...clipPieceState(section, track.id),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// The last bar runs on, so a clip sounding past the end is not cut.
|
|
435
|
+
if (pieces.length > 0)
|
|
436
|
+
pieces[pieces.length - 1] = {
|
|
437
|
+
...pieces[pieces.length - 1]!,
|
|
438
|
+
to: SCORE_LIMITS.maxTick + 1,
|
|
439
|
+
};
|
|
440
|
+
const next = withClips(track, sliceClips(track.clips, pieces, score));
|
|
441
|
+
if (JSON.stringify(next.clips) !== JSON.stringify(track.clips))
|
|
442
|
+
changed = true;
|
|
443
|
+
return next;
|
|
444
|
+
});
|
|
397
445
|
if (!changed) return score;
|
|
398
|
-
return new TrackScore({
|
|
446
|
+
return new TrackScore({
|
|
447
|
+
...score.toJSON(),
|
|
448
|
+
tracks: clipTracks,
|
|
449
|
+
notes,
|
|
450
|
+
sections: [],
|
|
451
|
+
form: [],
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** A section's mute and variation gain for one track's clips. */
|
|
456
|
+
function clipPieceState(
|
|
457
|
+
section: Section | undefined,
|
|
458
|
+
trackId: string,
|
|
459
|
+
): Pick<ClipPiece, "mute" | "gain"> {
|
|
460
|
+
if (!section) return {};
|
|
461
|
+
if (section.mute?.includes(trackId)) return { mute: true };
|
|
462
|
+
const gain = section.vary?.[trackId]?.gain;
|
|
463
|
+
return gain === undefined || gain === 1 ? {} : { gain };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** `track` with `clips` (absent when empty). */
|
|
467
|
+
function withClips(
|
|
468
|
+
track: Track,
|
|
469
|
+
clips: readonly AudioClip[] | undefined,
|
|
470
|
+
): Track {
|
|
471
|
+
const { clips: _clips, ...rest } = track;
|
|
472
|
+
return clips && clips.length > 0 ? { ...rest, clips } : rest;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** A track's clips cut to `pieces` (sections, forms, windows). */
|
|
476
|
+
function sliceTrackClips(
|
|
477
|
+
track: Track,
|
|
478
|
+
pieces: readonly ClipPiece[],
|
|
479
|
+
time: TrackScore,
|
|
480
|
+
): Track {
|
|
481
|
+
if (!track.clips) return track;
|
|
482
|
+
return withClips(track, sliceClips(track.clips, pieces, time));
|
|
399
483
|
}
|
|
400
484
|
|
|
401
485
|
/**
|
|
@@ -433,9 +517,13 @@ export function sectionScore(score: TrackScore, section: Section): TrackScore {
|
|
|
433
517
|
time: timed.time ?? null,
|
|
434
518
|
bars: section.bars,
|
|
435
519
|
tracks: score.tracks.map((track) =>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
520
|
+
sliceTrackClips(
|
|
521
|
+
slicePedals(
|
|
522
|
+
mapAutomation(track, (points) => cropPoints(points, from, to)),
|
|
523
|
+
[{ from, to, offset: 0 }],
|
|
524
|
+
),
|
|
525
|
+
[{ from, to, offset: 0, ...clipPieceState(section, track.id) }],
|
|
526
|
+
score,
|
|
439
527
|
),
|
|
440
528
|
),
|
|
441
529
|
notes,
|
|
@@ -602,6 +690,77 @@ function checkBars(bars: number): void {
|
|
|
602
690
|
);
|
|
603
691
|
}
|
|
604
692
|
|
|
693
|
+
/**
|
|
694
|
+
* The song tempo map, fermatas and pedal lanes for a ripple edit that lays
|
|
695
|
+
* `pieces` of the score end to end: they move with the music they belong
|
|
696
|
+
* to, as notes and automation do.
|
|
697
|
+
*/
|
|
698
|
+
function rippleTime(
|
|
699
|
+
score: TrackScore,
|
|
700
|
+
pieces: readonly { from: number; to: number; offset: number }[],
|
|
701
|
+
): Readonly<{
|
|
702
|
+
tempoBpm: number;
|
|
703
|
+
time: SongTime | null;
|
|
704
|
+
pedals: (track: Track) => Track;
|
|
705
|
+
}> {
|
|
706
|
+
const kept = pieces.filter((piece) => piece.to > piece.from);
|
|
707
|
+
const timed = sliceSongTime(
|
|
708
|
+
score,
|
|
709
|
+
kept.map((piece) => ({
|
|
710
|
+
...piece,
|
|
711
|
+
to: Math.min(piece.to, SCORE_LIMITS.maxTick + 1),
|
|
712
|
+
})),
|
|
713
|
+
);
|
|
714
|
+
return {
|
|
715
|
+
tempoBpm: timed.tempoBpm,
|
|
716
|
+
time: timed.time ?? null,
|
|
717
|
+
pedals: (track) => {
|
|
718
|
+
if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
|
|
719
|
+
const out: Record<string, unknown> = { ...track };
|
|
720
|
+
for (const field of ["pedal", "softPedal", "sostenuto"] as const) {
|
|
721
|
+
const events = track[field];
|
|
722
|
+
if (!events) continue;
|
|
723
|
+
const next = ripplePedal(events, kept);
|
|
724
|
+
if (next) out[field] = next;
|
|
725
|
+
else delete out[field];
|
|
726
|
+
}
|
|
727
|
+
return out as Track;
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* A pedal lane for `pieces` laid end to end with no lift at the seams (unlike
|
|
734
|
+
* a render slice): each piece starts in the state its source was in, and a
|
|
735
|
+
* gap left between pieces (inserted bars) holds the state before it.
|
|
736
|
+
*/
|
|
737
|
+
function ripplePedal(
|
|
738
|
+
events: readonly PedalEvent[],
|
|
739
|
+
pieces: readonly { from: number; to: number; offset: number }[],
|
|
740
|
+
): readonly PedalEvent[] | undefined {
|
|
741
|
+
const byTick = new Map<number, PedalState>();
|
|
742
|
+
for (const { from, to, offset } of pieces) {
|
|
743
|
+
let state: PedalState = "up";
|
|
744
|
+
for (const event of events) {
|
|
745
|
+
if (event.tick > from) break;
|
|
746
|
+
state = event.state;
|
|
747
|
+
}
|
|
748
|
+
byTick.set(offset, state);
|
|
749
|
+
for (const event of events)
|
|
750
|
+
if (event.tick > from && event.tick < to)
|
|
751
|
+
byTick.set(event.tick - from + offset, event.state);
|
|
752
|
+
}
|
|
753
|
+
const out: PedalEvent[] = [];
|
|
754
|
+
let last: PedalState = "up";
|
|
755
|
+
for (const tick of [...byTick.keys()].sort((a, b) => a - b)) {
|
|
756
|
+
const state = byTick.get(tick)!;
|
|
757
|
+
if (state === last) continue;
|
|
758
|
+
out.push({ tick, state });
|
|
759
|
+
last = state;
|
|
760
|
+
}
|
|
761
|
+
return out.length > 0 ? out : undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
605
764
|
/** Insert `count` empty bars at `atBar`, shifting later music and sections. */
|
|
606
765
|
export function insertBars(
|
|
607
766
|
score: TrackScore,
|
|
@@ -613,16 +772,32 @@ export function insertBars(
|
|
|
613
772
|
const ticks = barTicks(score);
|
|
614
773
|
const at = atBar * ticks;
|
|
615
774
|
const shift = count * ticks;
|
|
775
|
+
const ripple = rippleTime(score, [
|
|
776
|
+
{ from: 0, to: at, offset: 0 },
|
|
777
|
+
{ from: at, to: Infinity, offset: at + shift },
|
|
778
|
+
]);
|
|
616
779
|
return new TrackScore({
|
|
617
780
|
...score.toJSON(),
|
|
781
|
+
tempoBpm: ripple.tempoBpm,
|
|
782
|
+
time: ripple.time,
|
|
618
783
|
bars: score.bars + count,
|
|
619
784
|
tracks: score.tracks.map((track) =>
|
|
620
|
-
|
|
785
|
+
insertClipBars(
|
|
786
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
787
|
+
insertPoints(points, at, shift),
|
|
788
|
+
),
|
|
789
|
+
at,
|
|
790
|
+
shift,
|
|
791
|
+
),
|
|
621
792
|
),
|
|
793
|
+
// A note's start and end stay with the music they sit in, so one held
|
|
794
|
+
// across the insertion point sounds on through the new bars.
|
|
622
795
|
notes: score.notes.map((note) =>
|
|
623
796
|
note.startTick >= at
|
|
624
797
|
? { ...note, startTick: note.startTick + shift }
|
|
625
|
-
: note
|
|
798
|
+
: note.startTick + note.durationTicks > at
|
|
799
|
+
? { ...note, durationTicks: note.durationTicks + shift }
|
|
800
|
+
: note,
|
|
626
801
|
),
|
|
627
802
|
sections: score.sections.map((section) =>
|
|
628
803
|
section.startBar >= atBar
|
|
@@ -640,6 +815,12 @@ export function deleteBars(
|
|
|
640
815
|
atBar: number,
|
|
641
816
|
count: number,
|
|
642
817
|
): TrackScore {
|
|
818
|
+
if (!Number.isInteger(atBar) || atBar < 0 || !Number.isInteger(count))
|
|
819
|
+
throw new ScoreValidationError(
|
|
820
|
+
`bars to delete must start at a whole bar inside the song`,
|
|
821
|
+
);
|
|
822
|
+
// Only bars inside the song can go (a section may sit past its end).
|
|
823
|
+
count = Math.min(atBar + count, score.bars) - atBar;
|
|
643
824
|
if (count <= 0) return score;
|
|
644
825
|
if (count >= score.bars)
|
|
645
826
|
throw new ScoreValidationError("a song keeps at least one bar");
|
|
@@ -647,16 +828,21 @@ export function deleteBars(
|
|
|
647
828
|
const at = atBar * ticks;
|
|
648
829
|
const end = (atBar + count) * ticks;
|
|
649
830
|
const shift = count * ticks;
|
|
831
|
+
// Each note's start and end stay with the music they sit in; the part
|
|
832
|
+
// inside the deleted bars goes, so a note running out of them keeps its
|
|
833
|
+
// tail and one running into them is cut where they start.
|
|
834
|
+
const place = (tick: number) =>
|
|
835
|
+
tick < at ? tick : tick < end ? at : tick - shift;
|
|
650
836
|
const notes: Note[] = [];
|
|
651
837
|
for (const note of score.notes) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
838
|
+
const startTick = place(note.startTick);
|
|
839
|
+
const stop = place(note.startTick + note.durationTicks);
|
|
840
|
+
if (stop <= startTick) continue;
|
|
841
|
+
notes.push(
|
|
842
|
+
startTick === note.startTick && stop - startTick === note.durationTicks
|
|
843
|
+
? note
|
|
844
|
+
: { ...note, startTick, durationTicks: stop - startTick },
|
|
845
|
+
);
|
|
660
846
|
}
|
|
661
847
|
const sections: Section[] = [];
|
|
662
848
|
for (const section of score.sections) {
|
|
@@ -677,11 +863,23 @@ export function deleteBars(
|
|
|
677
863
|
}
|
|
678
864
|
}
|
|
679
865
|
const names = new Set(sections.map((section) => foldName(section.name)));
|
|
866
|
+
const ripple = rippleTime(score, [
|
|
867
|
+
{ from: 0, to: at, offset: 0 },
|
|
868
|
+
{ from: end, to: Infinity, offset: at },
|
|
869
|
+
]);
|
|
680
870
|
return new TrackScore({
|
|
681
871
|
...score.toJSON(),
|
|
872
|
+
tempoBpm: ripple.tempoBpm,
|
|
873
|
+
time: ripple.time,
|
|
682
874
|
bars: score.bars - count,
|
|
683
875
|
tracks: score.tracks.map((track) =>
|
|
684
|
-
|
|
876
|
+
deleteClipBars(
|
|
877
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
878
|
+
deletePoints(points, at, shift),
|
|
879
|
+
),
|
|
880
|
+
at,
|
|
881
|
+
end,
|
|
882
|
+
),
|
|
685
883
|
),
|
|
686
884
|
notes,
|
|
687
885
|
sections,
|
|
@@ -865,6 +1063,15 @@ export function duplicateSection(
|
|
|
865
1063
|
if (options.as !== undefined && findSection(score, options.as))
|
|
866
1064
|
throw new ScoreValidationError(`a section named ${options.as} exists`);
|
|
867
1065
|
const at = options.toBar ?? section.startBar + section.bars;
|
|
1066
|
+
if (
|
|
1067
|
+
!Number.isInteger(at) ||
|
|
1068
|
+
at < 0 ||
|
|
1069
|
+
at > score.bars ||
|
|
1070
|
+
(at > section.startBar && at < section.startBar + section.bars)
|
|
1071
|
+
)
|
|
1072
|
+
throw new ScoreValidationError(
|
|
1073
|
+
`a copy of ${section.name} goes at a bar 1..${score.bars + 1} outside it`,
|
|
1074
|
+
);
|
|
868
1075
|
const opened = insertBars(score, at, section.bars);
|
|
869
1076
|
const source =
|
|
870
1077
|
section.startBar >= at ? section.startBar + section.bars : section.startBar;
|
|
@@ -908,10 +1115,26 @@ export function copyBars(
|
|
|
908
1115
|
Math.min(note.durationTicks, from + length - note.startTick),
|
|
909
1116
|
),
|
|
910
1117
|
}));
|
|
1118
|
+
// The copied bars bring their tempo, fermatas and pedal; the music after
|
|
1119
|
+
// them goes on as it was.
|
|
1120
|
+
const ripple = rippleTime(score, [
|
|
1121
|
+
{ from: 0, to, offset: 0 },
|
|
1122
|
+
{ from, to: from + length, offset: to },
|
|
1123
|
+
{ from: to + length, to: Infinity, offset: to + length },
|
|
1124
|
+
]);
|
|
911
1125
|
return new TrackScore({
|
|
912
1126
|
...score.toJSON(),
|
|
1127
|
+
tempoBpm: ripple.tempoBpm,
|
|
1128
|
+
time: ripple.time,
|
|
913
1129
|
tracks: score.tracks.map((track) =>
|
|
914
|
-
|
|
1130
|
+
copyClipBars(
|
|
1131
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
1132
|
+
copyPoints(points, from, length, to),
|
|
1133
|
+
),
|
|
1134
|
+
from,
|
|
1135
|
+
length,
|
|
1136
|
+
to,
|
|
1137
|
+
),
|
|
915
1138
|
),
|
|
916
1139
|
notes: [...kept, ...copies],
|
|
917
1140
|
});
|
|
@@ -1235,17 +1458,38 @@ export function arrangedSlice(
|
|
|
1235
1458
|
): TrackScore {
|
|
1236
1459
|
const ticks = barTicks(score);
|
|
1237
1460
|
const pieces: { from: number; to: number; offset: number }[] = [];
|
|
1461
|
+
// Clips (0.7) are cut per section pass; a window's own edges inside a
|
|
1462
|
+
// pass (or where the form plays straight on) are seams, never cuts, so a
|
|
1463
|
+
// windowed render plays them as one pass does.
|
|
1464
|
+
const clipPieces: (ClipPiece & { section: Section })[] = [];
|
|
1238
1465
|
const end = fromBar + bars;
|
|
1466
|
+
let previous: FormSegment | undefined;
|
|
1239
1467
|
for (const segment of formSegments(score)) {
|
|
1240
1468
|
const segEnd = segment.startBar + segment.bars;
|
|
1469
|
+
const before = previous;
|
|
1470
|
+
previous = segment;
|
|
1241
1471
|
if (segEnd <= fromBar || segment.startBar >= end) continue;
|
|
1242
1472
|
const start = Math.max(segment.startBar, fromBar);
|
|
1243
1473
|
const stop = Math.min(segEnd, end);
|
|
1244
1474
|
const from = (segment.section.startBar + start - segment.startBar) * ticks;
|
|
1245
|
-
|
|
1475
|
+
const piece = {
|
|
1246
1476
|
from,
|
|
1247
1477
|
to: from + (stop - start) * ticks,
|
|
1248
1478
|
offset: (start - fromBar) * ticks,
|
|
1479
|
+
};
|
|
1480
|
+
pieces.push(piece);
|
|
1481
|
+
const straight =
|
|
1482
|
+
before !== undefined &&
|
|
1483
|
+
before.section.startBar + before.bars === segment.section.startBar;
|
|
1484
|
+
clipPieces.push({
|
|
1485
|
+
...piece,
|
|
1486
|
+
section: segment.section,
|
|
1487
|
+
...(stop < segEnd ? { to: from + (segEnd - start) * ticks } : {}),
|
|
1488
|
+
...(clipPieces.length === 0 &&
|
|
1489
|
+
fromBar > 0 &&
|
|
1490
|
+
(start > segment.startBar || straight)
|
|
1491
|
+
? { seam: true }
|
|
1492
|
+
: {}),
|
|
1249
1493
|
});
|
|
1250
1494
|
}
|
|
1251
1495
|
const shift = fromBar * ticks;
|
|
@@ -1256,7 +1500,14 @@ export function arrangedSlice(
|
|
|
1256
1500
|
time: timed.time ?? null,
|
|
1257
1501
|
bars,
|
|
1258
1502
|
tracks: score.tracks.map((track) =>
|
|
1259
|
-
|
|
1503
|
+
sliceTrackClips(
|
|
1504
|
+
slicePedals(sliceLanes(track, pieces), pieces),
|
|
1505
|
+
clipPieces.map(({ section, ...piece }) => ({
|
|
1506
|
+
...piece,
|
|
1507
|
+
...clipPieceState(section, track.id),
|
|
1508
|
+
})),
|
|
1509
|
+
score,
|
|
1510
|
+
),
|
|
1260
1511
|
),
|
|
1261
1512
|
notes: arrangedNotes(score, fromBar, fromBar + noteBars).map((note) => ({
|
|
1262
1513
|
...note,
|
|
@@ -1341,7 +1592,11 @@ function laneFallback(track: Track, lane: string): number | undefined {
|
|
|
1341
1592
|
? track.modal && modalSettings(track.modal)
|
|
1342
1593
|
: spec.effect === "wind"
|
|
1343
1594
|
? track.wind && windSettings(track.wind)
|
|
1344
|
-
:
|
|
1595
|
+
: spec.effect === "sing"
|
|
1596
|
+
? track.sing && resolveSing(track.sing)
|
|
1597
|
+
: (track.fx as Record<string, unknown> | undefined)?.[
|
|
1598
|
+
spec.effect
|
|
1599
|
+
]
|
|
1345
1600
|
) as Record<string, unknown> | undefined;
|
|
1346
1601
|
const stored = source?.[spec.param];
|
|
1347
1602
|
return typeof stored === "number" ? stored : spec.spec.default;
|
|
@@ -1364,6 +1619,8 @@ const TOM = DRUM_VOICES.find((voice) => voice.voice === "tom")!.pitch;
|
|
|
1364
1619
|
const TOMS = [50, 47, TOM] as const;
|
|
1365
1620
|
const KICK = DRUM_VOICES.find((voice) => voice.voice === "kick")!.pitch;
|
|
1366
1621
|
const OPEN_HAT = DRUM_VOICES.find((voice) => voice.voice === "openhat")!.pitch;
|
|
1622
|
+
/** GM crash 1, which calibration 1+ renders as a cymbal. */
|
|
1623
|
+
const CRASH = 49;
|
|
1367
1624
|
|
|
1368
1625
|
/** Generator tracks, reused by later builds and drops. */
|
|
1369
1626
|
export const GENERATOR_TRACKS = Object.freeze({
|
|
@@ -1955,7 +2212,10 @@ export type FillOptions = Readonly<{
|
|
|
1955
2212
|
/** Beats the fill lasts, default 1. */
|
|
1956
2213
|
beats?: number;
|
|
1957
2214
|
style?: FillStyle;
|
|
1958
|
-
/**
|
|
2215
|
+
/**
|
|
2216
|
+
* Crash with kick on the next downbeat, default on: GM 49 on a
|
|
2217
|
+
* calibrated song's synth kit, else an open hat.
|
|
2218
|
+
*/
|
|
1959
2219
|
crash?: boolean;
|
|
1960
2220
|
}>;
|
|
1961
2221
|
|
|
@@ -2000,6 +2260,12 @@ export function generateFill(
|
|
|
2000
2260
|
drums = GENERATOR_TRACKS.fills;
|
|
2001
2261
|
}
|
|
2002
2262
|
const style = options.style ?? "toms";
|
|
2263
|
+
// Sample kits and legacy songs have no cymbal voice: keep the open hat.
|
|
2264
|
+
const kit = next.tracks.find((track) => track.id === drums)?.kit;
|
|
2265
|
+
const crash =
|
|
2266
|
+
(next.calibration ?? 0) >= 1 && (kit === undefined || synthKit(kit))
|
|
2267
|
+
? CRASH
|
|
2268
|
+
: OPEN_HAT;
|
|
2003
2269
|
const clamped: string[] = [];
|
|
2004
2270
|
const beats = clampNote(
|
|
2005
2271
|
options.beats ?? 1,
|
|
@@ -2030,8 +2296,8 @@ export function generateFill(
|
|
|
2030
2296
|
: SNARE
|
|
2031
2297
|
: progress < 0.25
|
|
2032
2298
|
? SNARE
|
|
2033
|
-
: // High, mid, low tom down the fill (GM 50/47/45);
|
|
2034
|
-
//
|
|
2299
|
+
: // High, mid, low tom down the fill (GM 50/47/45); a
|
|
2300
|
+
// calibrated song pitches them, older songs fold them.
|
|
2035
2301
|
TOMS[Math.min(2, Math.floor(((progress - 0.25) / 0.75) * 3))]!;
|
|
2036
2302
|
fill.push({
|
|
2037
2303
|
id: nextId(),
|
|
@@ -2043,7 +2309,7 @@ export function generateFill(
|
|
|
2043
2309
|
});
|
|
2044
2310
|
}
|
|
2045
2311
|
if ((options.crash ?? true) && end < next.bars * ticks)
|
|
2046
|
-
for (const pitch of [
|
|
2312
|
+
for (const pitch of [crash, KICK])
|
|
2047
2313
|
fill.push({
|
|
2048
2314
|
id: nextId(),
|
|
2049
2315
|
trackId: drums,
|